1: <?php
2: /*
3: * Copyright (c) 2016 @trashtoy
4: * https://github.com/trashtoy/
5: *
6: * Permission is hereby granted, free of charge, to any person obtaining a copy of
7: * this software and associated documentation files (the "Software"), to deal in
8: * the Software without restriction, including without limitation the rights to use,
9: * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
10: * Software, and to permit persons to whom the Software is furnished to do so,
11: * subject to the following conditions:
12: *
13: * The above copyright notice and this permission notice shall be included in all
14: * copies or substantial portions of the Software.
15: *
16: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17: * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18: * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19: * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20: * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21: * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22: */
23: /**
24: * PHP class file.
25: * @auhtor trashtoy
26: * @since 2.2.0
27: */
28: namespace Peach\Markup;
29:
30: use InvalidArgumentException;
31: use Peach\Util\Values;
32:
33: /**
34: * HTML の出力に特化した Helper です.
35: * XML 宣言, DOCTYPE 宣言, select 要素やコメントの出力機能などを備えています.
36: *
37: * このクラスは通常コンストラクタではなく newInstance() メソッドを使って初期化を行います.
38: * フォーマットのきめ細やかなカスタマイズを行いたい場合のみコンストラクタから生成してください.
39: */
40: class HtmlHelper extends AbstractHelper
41: {
42:
43: /**
44: * HTML 4.01 Strict のモードです.
45: *
46: * @var string
47: */
48: const MODE_HTML4_STRICT = "html4s";
49:
50: /**
51: * HTML 4.01 Transitional のモードです.
52: *
53: * @var string
54: */
55: const MODE_HTML4_TRANSITIONAL = "html4t";
56:
57: /**
58: * XHTML 1.0 Strict のモードです.
59: *
60: * @var string
61: */
62: const MODE_XHTML1_STRICT = "xhtml1s";
63:
64: /**
65: * XHTML 1.0 Transitional のモードです.
66: *
67: * @var string
68: */
69: const MODE_XHTML1_TRANSITIONAL = "xhtml1t";
70:
71: /**
72: * XHTML 1.1 のモードです.
73: *
74: * @var string
75: */
76: const MODE_XHTML1_1 = "xhtml1_1";
77:
78: /**
79: * HTML5 のモードです.
80: *
81: * @var string
82: */
83: const MODE_HTML5 = "html5";
84:
85: /**
86: * このオブジェクトが出力する文書型宣言のコードです.
87: *
88: * @var string
89: */
90: private $docType;
91:
92: /**
93: * このオブジェクトが XHTML 形式かどうかをあらわします.
94: * xmlDec() メソッドの返り値に影響します.
95: *
96: * @var bool
97: */
98: private $isXhtml;
99:
100: /**
101: * 指定された Helper オブジェクトを利用して HTML タグの出力を行う, 新しい
102: * HtmlHelper オブジェクトを構築します.
103: *
104: * 第 2 引数の文字列は docType() メソッドの返り値に関係します.
105: * もしも未指定の場合, このオブジェクトが docType() メソッドで生成する
106: * Component は何も出力しません.
107: * 第 3 引数のフラグは xmlDec() メソッドの返り値に関係します.
108: * true の場合は xmlDec() が返す Component は XML 宣言を出力しますが,
109: * それ以外の場合は何も出力しません.
110: *
111: * @param Helper $parent カスタマイズ対象の Helper オブジェクト
112: * @param string $docType この Helper が生成する文書型宣言の文字列
113: * @param bool $isXhtml XHTML として生成する場合のみ true
114: */
115: public function __construct(Helper $parent, $docType = null, $isXhtml = null)
116: {
117: parent::__construct($parent);
118: $this->docType = $docType;
119: $this->isXhtml = $isXhtml;
120: }
121:
122: /**
123: * 指定されたモードで HtmlHelper オブジェクトを生成します.
124: * 引数には以下の定数を指定してください.
125: *
126: * - {@link HtmlHelper::MODE_HTML4_STRICT}
127: * - {@link HtmlHelper::MODE_HTML4_TRANSITIONAL}
128: * - {@link HtmlHelper::MODE_XHTML1_STRICT}
129: * - {@link HtmlHelper::MODE_XHTML1_TRANSITIONAL}
130: * - {@link HtmlHelper::MODE_XHTML1_1}
131: * - {@link HtmlHelper::MODE_HTML5}
132: *
133: * @param string $mode
134: * @return HtmlHelper
135: */
136: public static function newInstance($mode = null)
137: {
138: $actualMode = self::detectMode($mode);
139: $baseHelper = self::createBaseHelper($actualMode);
140: $docType = self::getDocTypeFromMode($actualMode);
141: $isXhtml = self::checkXhtmlFromMode($actualMode);
142: return new self($baseHelper, $docType, $isXhtml);
143: }
144:
145: /**
146: * 指定されたモードに応じた BaseHelper を返します.
147: *
148: * @param string $mode
149: * @return BaseHelper
150: */
151: private static function createBaseHelper($mode)
152: {
153: $isXhtml = self::checkXhtmlFromMode($mode);
154: $builder = self::createBuilder($isXhtml);
155: $emptyNodeNames = self::getEmptyNodeNames();
156: return new BaseHelper($builder, $emptyNodeNames);
157: }
158:
159: /**
160: * XML 宣言をあらわす Component を返します.
161: * もしもこの HtmlHelper が XHTML モードで生成された場合,
162: * このメソッドは以下のコードを出力する {@link Code} オブジェクトを返します.
163: * <code>
164: * <?xml version="1.0" encoding="UTF-8"?>
165: * </code>
166: *
167: * それ以外は {@link None} オブジェクトを返します.
168: *
169: * @return Component XML 宣言をあらわす Code オブジェクトまたは None
170: */
171: public function xmlDec()
172: {
173: return $this->isXhtml ? new Code('<?xml version="1.0" encoding="UTF-8"?>') : None::getInstance();
174: }
175:
176: /**
177: * 文書型宣言をあらわす Code オブジェクトを返します.
178: * 返り値の Component が出力するコードは, このオブジェクトの初期化時に指定されたモード
179: * (あるいはコンストラクタの第 2 引数) に応じて異なる文書型宣言となります.
180: *
181: * @return Component 文書型宣言をあらわす Code オブジェクト. ただし初期化時に指定されていない場合は None
182: */
183: public function docType()
184: {
185: return strlen($this->docType) ? new Code($this->docType) : None::getInstance();
186: }
187:
188: /**
189: * 指定された内容のコメントノードを作成します.
190: * 引数にノードを指定した場合, そのノードの内容をコメントアウトします.
191: *
192: * 第 2, 第 3 引数にコメントの接頭辞・接尾辞を含めることが出来ます.
193: *
194: * @param string|Component $contents コメントにしたいテキストまたはノード
195: * @param string $prefix コメントの接頭辞
196: * @param string $suffix コメントの接尾辞
197: * @return HelperObject
198: */
199: public function comment($contents = null, $prefix = "", $suffix = "")
200: {
201: $comment = new Comment($prefix, $suffix);
202: return $this->tag($comment)->append($contents);
203: }
204:
205: /**
206: * IE 9 以前の Internet Explorer で採用されている条件付きコメントを生成します.
207: * 以下にサンプルを挙げます.
208: * <code>
209: * echo $htmlHelper->conditionalComment("lt IE 7", "He died on April 9, 2014.")->write();
210: * </code>
211: * このコードは次の文字列を出力します.
212: * <code>
213: * <!--[if lt IE 7]>He died on April 9, 2014.<![endif]-->
214: * </code>
215: * 第 2 引数を省略した場合は空の条件付きコメントを生成します.
216: *
217: * @param string $cond 条件文 ("lt IE 7" など)
218: * @param string|Component $contents 条件付きコメントで囲みたいテキストまたはノード
219: * @return HelperObject 条件付きコメントを表現する HelperObject
220: */
221: public function conditionalComment($cond, $contents = null)
222: {
223: return $this->comment($contents, "[if {$cond}]>", "<![endif]");
224: }
225:
226: /**
227: * HTML の select 要素を生成します.
228: * 第 1 引数にはデフォルトで選択されている値,
229: * 第 2 引数には選択肢を配列で指定します.
230: * キーがラベル, 値がそのラベルに割り当てられたデータとなります.
231: *
232: * 引数を二次元配列にすることで, 一次元目のキーを optgroup にすることが出来ます.
233: * 以下にサンプルを挙げます.
234: * <code>
235: * $candidates = array(
236: * "Fruit" => array(
237: * "Apple" => 1,
238: * "Orange" => 2,
239: * "Pear" => 3,
240: * "Peach" => 4,
241: * ),
242: * "Dessert" => array(
243: * "Chocolate" => 5,
244: * "Doughnut" => 6,
245: * "Ice cream" => 7,
246: * ),
247: * "Others" => 8,
248: * );
249: * $select = Html::createSelectElement("6", $candidates, array("class" => "sample", "name" => "favorite"));
250: * </code>
251: * この要素を出力すると以下の結果が得られます.
252: * <code>
253: * <select class="sample" name="favorite">
254: * <optgroup label="Fruit">
255: * <option value="1">Apple</option>
256: * <option value="2">Orange</option>
257: * <option value="3">Pear</option>
258: * <option value="4">Peach</option>
259: * </optgroup>
260: * <optgroup label="Dessert">
261: * <option value="5">Chocolate</option>
262: * <option value="6" selected>Doughnut</option>
263: * <option value="7">Ice cream</option>
264: * </optgroup>
265: * <option value="8">Others</option>
266: * </select>
267: * </code>
268: *
269: * @param string $current デフォルト値
270: * @param array $candidates 選択肢の一覧
271: * @param array $attr 追加で指定する属性 (class, id, style など)
272: * @return ContainerElement HTML の select 要素
273: */
274: public function createSelectElement($current, array $candidates, array $attr = array())
275: {
276: $currentText = Values::stringValue($current);
277: $select = new ContainerElement("select");
278: $select->setAttributes($attr);
279: $select->appendNode(self::createOptions($currentText, $candidates));
280: return $select;
281: }
282:
283: /**
284: * select 要素に含まれる option の一覧を作成します.
285: *
286: * @param string $current デフォルト値
287: * @param array $candidates 選択肢の一覧
288: * @return NodeList option 要素の一覧
289: */
290: private function createOptions($current, array $candidates)
291: {
292: $result = new NodeList();
293: foreach ($candidates as $key => $value) {
294: if (is_array($value)) {
295: $optgroup = new ContainerElement("optgroup");
296: $optgroup->setAttribute("label", $key);
297: $optgroup->appendNode($this->createOptions($current, $value));
298: $result->appendNode($optgroup);
299: continue;
300: }
301:
302: $option = new ContainerElement("option");
303: $option->setAttribute("value", $value);
304: $value = Values::stringValue($value);
305: if ($current === $value) {
306: $option->setAttribute("selected");
307: }
308: $option->appendNode($key);
309: $result->appendNode($option);
310: }
311: return $result;
312: }
313:
314: /**
315: * HTML の select 要素を生成し, 結果を HelperObject として返します.
316: * 引数および処理内容は
317: * {@link Html::createSelectElement()}
318: * と全く同じですが, 生成された要素を HelperObject でラップするところが異なります.
319: *
320: * @see HtmlHelper::createSelectElement
321: * @param string $current デフォルト値
322: * @param array $candidates 選択肢の一覧
323: * @param array $attr 追加で指定する属性 (class, id, style など)
324: * @return HelperObject
325: */
326: public function select($current, array $candidates, array $attr = array())
327: {
328: return $this->tag(self::createSelectElement($current, $candidates, $attr));
329: }
330:
331: /**
332: * newInstance() の引数で指定された文字列のバリデーションを行います.
333: * 未対応の文字列が指定された場合は InvalidArgumentException をスローします.
334: *
335: * @param string $param
336: * @return string
337: * @throws InvalidArgumentException
338: */
339: private static function detectMode($param)
340: {
341: static $mapping = null;
342: if ($mapping === null) {
343: $keys = array(
344: self::MODE_HTML4_TRANSITIONAL,
345: self::MODE_HTML4_STRICT,
346: self::MODE_XHTML1_STRICT,
347: self::MODE_XHTML1_TRANSITIONAL,
348: self::MODE_XHTML1_1,
349: self::MODE_HTML5,
350: );
351: $altMapping = array(
352: "" => self::MODE_HTML5,
353: "html" => self::MODE_HTML5,
354: "html4" => self::MODE_HTML4_TRANSITIONAL,
355: "xhtml" => self::MODE_XHTML1_TRANSITIONAL,
356: );
357: $mapping = array_merge(array_combine($keys, $keys), $altMapping);
358: }
359:
360: $key = strtolower($param);
361: if (array_key_exists($key, $mapping)) {
362: return $mapping[$key];
363: } else {
364: throw new InvalidArgumentException("Invalid mode name: {$param}");
365: }
366: }
367:
368: /**
369: * 指定されたモードが XHTML かどうかを判定します.
370: *
371: * @param string $mode モード (ただし detectMode() でバリデーション済み)
372: * @return bool XHTML と判定された場合のみ true
373: */
374: private static function checkXhtmlFromMode($mode)
375: {
376: static $xhtmlModes = array(
377: self::MODE_XHTML1_STRICT,
378: self::MODE_XHTML1_TRANSITIONAL,
379: self::MODE_XHTML1_1,
380: );
381: return in_array($mode, $xhtmlModes);
382: }
383:
384: /**
385: * 指定されたモードに応じた DOCTYPE 宣言の文字列を返します.
386: *
387: * @param string $mode モード (ただし detectMode() でバリデーション済み)
388: * @return string DOCTYPE 宣言
389: */
390: private static function getDocTypeFromMode($mode)
391: {
392: static $docTypeList = array(
393: self::MODE_HTML4_STRICT => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
394: self::MODE_HTML4_TRANSITIONAL => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
395: self::MODE_XHTML1_STRICT => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
396: self::MODE_XHTML1_TRANSITIONAL => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
397: self::MODE_XHTML1_1 => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
398: self::MODE_HTML5 => '<!DOCTYPE html>',
399: );
400: return $docTypeList[$mode];
401: }
402:
403: /**
404: * 新しい DefaultBuilder を生成します.
405: * @param bool $isXhtml XHTML 形式の場合は true, HTML 形式の場合は false
406: * @return DefaultBuilder
407: */
408: private static function createBuilder($isXhtml = false)
409: {
410: // @codeCoverageIgnoreStart
411: static $breakControl = null;
412: if (!isset($breakControl)) {
413: $breakControl = new NameBreakControl(
414: array("html", "head", "body", "ul", "ol", "dl", "table"),
415: array("pre", "code", "textarea")
416: );
417: }
418: // @codeCoverageIgnoreEnd
419:
420: $renderer = $isXhtml ? XmlRenderer::getInstance() : SgmlRenderer::getInstance();
421: $builder = new DefaultBuilder();
422: $builder->setBreakControl($breakControl);
423: $builder->setRenderer($renderer);
424: return $builder;
425: }
426:
427: /**
428: * HTML4, XHTML1, HTML5 にて定義されている空要素タグまたは Void elements
429: * の一覧を返します.
430: * 返り値の配列は, 以下に挙げる要素名をマージしたものとなります.
431: *
432: * Empty elements by XHTML1 (https://www.w3.org/TR/xhtml1/)
433: * - area, base, basefont, br, col, frame, hr, img, input, isindex, link, meta, param
434: * Void elements by HTML5 (https://www.w3.org/TR/html5/syntax.html#void-elements)
435: * - area, base, br, col, embed, hr, img, input, keygen, link, meta, param, source, track, wbr
436: *
437: * @return array
438: * @ignore
439: */
440: private static function getEmptyNodeNames()
441: {
442: static $emptyList = null;
443: if ($emptyList === null) {
444: $emptyList = array("area", "base", "basefont", "br", "col", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source", "track", "wbr");
445: }
446: return $emptyList;
447: }
448: }
449: