1: <?php
2: /*
3: * Copyright (c) 2014 @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.0.0
27: */
28: namespace Peach\Markup;
29: use Peach\Util\Strings;
30:
31: /**
32: * 与えられたノードを HTML や XML などの文字列に変換するクラスです.
33: */
34: class DefaultContext extends Context
35: {
36: /**
37: * マークアップ時の改行やインデント処理を担当します.
38: * @var Indent
39: */
40: private $indent;
41:
42: /**
43: * タグの出力方式 (空要素や boolean 属性などの扱い) を制御します.
44: * @var Renderer
45: */
46: private $renderer;
47:
48: /**
49: * 開始タグの直後に改行するかどうかの判定を行います.
50: * @var BreakControl
51: */
52: private $breakControl;
53:
54: /**
55: * TRUE の場合はノードの前後にインデントと改行を付加します.
56: * @var bool
57: */
58: private $isIndentMode;
59:
60: /**
61: * TRUE の場合はコメントの内部を処理している状態とみなします.
62: * コメントの内部にあるコメントは無視するようになります.
63: * @var bool
64: */
65: private $isCommentMode;
66:
67: /**
68: * handle() メソッド実行時の処理結果が格納されます.
69: * @var string
70: */
71: private $result;
72:
73: /**
74: * 指定された Renderer, Indent, BreakControl オブジェクトを使って
75: * マークアップを行う DefaultContext オブジェクトを構築します.
76: *
77: * @param Renderer $renderer
78: * @param Indent $indent
79: * @param BreakControl $breakControl
80: */
81: public function __construct(Renderer $renderer, Indent $indent = null, BreakControl $breakControl = null)
82: {
83: if (!isset($indent)) {
84: $indent = new Indent();
85: }
86: if (!isset($breakControl)) {
87: $breakControl = DefaultBreakControl::getInstance();
88: }
89: $this->renderer = $renderer;
90: $this->indent = $indent;
91: $this->breakControl = $breakControl;
92: $this->isIndentMode = true;
93: $this->isCommentMode = false;
94: $this->result = "";
95: }
96:
97: /**
98: * コメントノードを読み込みます.
99: * @param Comment $comment
100: */
101: public function handleComment(Comment $comment)
102: {
103: if ($this->isCommentMode) {
104: $this->formatChildNodes($comment);
105: return;
106: }
107:
108: $this->isCommentMode = true;
109: $prefix = $this->escapeEndComment($comment->getPrefix());
110: $suffix = $this->escapeEndComment($comment->getSuffix());
111: $this->result .= $this->indent() . "<!--{$prefix}";
112: if ($this->isIndentMode) {
113: if ($this->checkBreakModeInComment($comment)) {
114: $breakCode = $this->breakCode();
115: $this->result .= $breakCode;
116: $this->formatChildNodes($comment);
117: $this->result .= $breakCode;
118: $this->result .= $this->indent();
119: } else {
120: $this->isIndentMode = false;
121: $this->formatChildNodes($comment);
122: $this->isIndentMode = true;
123: }
124: } else {
125: $this->formatChildNodes($comment);
126: }
127: $this->result .= "{$suffix}-->";
128: $this->isCommentMode = false;
129: }
130:
131: /**
132: * コメントノードを 1 行 ("<--foobar-->") で記述するか改行するかの判定を行います.
133: *
134: * @param Comment $comment
135: * @return bool
136: */
137: private function checkBreakModeInComment(Comment $comment)
138: {
139: $nodes = $comment->getChildNodes();
140: switch (count($nodes)) {
141: case 0:
142: return false;
143: case 1:
144: $node = $nodes[0];
145: if ($node instanceof Comment) {
146: return $this->checkBreakModeInComment($node);
147: }
148:
149: return ($node instanceof Element);
150: default:
151: return true;
152: }
153: }
154:
155: /**
156: * Text ノードを読み込みます.
157: * @param Text $text
158: */
159: public function handleText(Text $text) {
160: $this->result .= $this->indent() . $this->escape($text->getText());
161: }
162:
163: /**
164: * Code を読み込みます.
165: * @param Code $code
166: */
167: public function handleCode(Code $code)
168: {
169: $text = $code->getText();
170: if (!strlen($text)) {
171: return;
172: }
173:
174: $lines = Strings::getLines($text);
175: $indent = $this->indent();
176: $this->result .= $indent;
177: $this->result .= implode($this->breakCode() . $indent, $lines);
178: }
179:
180: /**
181: * EmptyElement を読み込みます.
182: * @param EmptyElement $element
183: * @see Context::handleEmptyElement()
184: */
185: public function handleEmptyElement(EmptyElement $element) {
186: $this->result .= $this->indent() . $this->renderer->formatEmptyTag($element);
187: }
188:
189: /**
190: * ContainerElement を読み込みます.
191: * @param ContainerElement $element
192: * @see Context::handleContainerElement()
193: */
194: public function handleContainerElement(ContainerElement $element)
195: {
196: $this->result .= $this->indent() . $this->renderer->formatStartTag($element);
197: if ($this->isIndentMode) {
198: if ($this->breakControl->breaks($element)) {
199: $this->result .= $this->indent->stepUp();
200: $this->result .= $this->formatChildNodes($element);
201: $this->result .= $this->breakCode();
202: $this->result .= $this->indent->stepDown();
203: } else {
204: $this->isIndentMode = false;
205: $this->formatChildNodes($element);
206: $this->isIndentMode = true;
207: }
208: } else {
209: $this->formatChildNodes($element);
210: }
211: $this->result .= $this->renderer->formatEndTag($element);
212: }
213:
214: /**
215: * NodeList を変換します.
216: * @param NodeList $node
217: */
218: public function handleNodeList(NodeList $node)
219: {
220: $this->formatChildNodes($node);
221: }
222:
223: /**
224: * マークアップされたコードを返します.
225: * @return string
226: */
227: public function getResult()
228: {
229: return $this->result;
230: }
231:
232: /**
233: * 指定されたコンテナの子ノードを書式化します.
234: * 各子ノードの出力結果の末尾には, 改行コードで連結されます. (インデントモードが ON の場合)
235: * 末尾の子ノードの出力結果の後ろに改行コードは付きません.
236: *
237: * @param Container $container
238: */
239: private function formatChildNodes(Container $container)
240: {
241: $nextBreak = "";
242: $breakCode = $this->breakCode();
243: $childNodes = $container->getChildNodes();
244: foreach ($childNodes as $child) {
245: $this->result .= $nextBreak;
246: $this->handle($child);
247: $nextBreak = $breakCode;
248: }
249: }
250:
251: /**
252: * None を処理します. 何もせずに終了します.
253: *
254: * @param None $none
255: */
256: public function handleNone(None $none)
257: {
258: }
259:
260: /**
261: * インデントモードが ON の場合は空白文字, OFF の場合は空文字列を返します.
262: * @return string
263: */
264: private function indent()
265: {
266: return $this->isIndentMode ? $this->indent->indent() : "";
267: }
268:
269: /**
270: * インデントモードが ON の場合は改行コード, OFF の場合は空文字列を返します.
271: * @return string
272: */
273: private function breakCode()
274: {
275: return $this->isIndentMode ? $this->indent->breakCode() : "";
276: }
277:
278: /**
279: * 予期しないインデントを回避するため, 改行コードを文字参照に置き換えます.
280: * @param string $text
281: * @return string
282: */
283: private function escape($text)
284: {
285: return preg_replace("/\\r\\n|\\r|\\n/", "
", htmlspecialchars($text));
286: }
287:
288: /**
289: * 意図しないタイミングでコメントノードが終了するのを防ぐため, "-->" を文字参照に置き換えます.
290: * @param string $text
291: * @return string
292: */
293: private function escapeEndComment($text)
294: {
295: return str_replace("-->", "-->", $text);
296: }
297: }
298: