1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11:
12:
13:
14:
15: 16: 17: 18: 19: 20:
21: class Parser extends Object
22: {
23:
24: const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';
25:
26:
27: const N_PREFIX = 'n:';
28:
29:
30: private $macroRe;
31:
32:
33: private $input;
34:
35:
36: private $output;
37:
38:
39: private $offset;
40:
41:
42: private $macros;
43:
44:
45: private $macroHandlers;
46:
47:
48: private $htmlNodes = array();
49:
50:
51: private $macroNodes = array();
52:
53:
54: public $context;
55:
56:
57: public $templateId;
58:
59:
60: const CONTEXT_TEXT = 'text',
61: CONTEXT_CDATA = 'cdata',
62: CONTEXT_TAG = 'tag',
63: CONTEXT_ATTRIBUTE = 'attribute',
64: CONTEXT_NONE = 'none',
65: CONTEXT_COMMENT = 'comment';
66:
67:
68:
69: public function __construct()
70: {
71: $this->macroHandlers = new SplObjectStorage;
72: $this->setDelimiters('\\{(?![\\s\'"{}])', '\\}');
73: $this->context = array(self::CONTEXT_NONE, 'text');
74: }
75:
76:
77:
78: 79: 80: 81: 82:
83: public function addMacro($name, IMacro $macro)
84: {
85: $this->macros[$name][] = $macro;
86: $this->macroHandlers->attach($macro);
87: return $this;
88: }
89:
90:
91:
92: 93: 94: 95: 96:
97: public function parse($s)
98: {
99: if (!Strings::checkEncoding($s)) {
100: throw new LatteException('Template is not valid UTF-8 stream.');
101: }
102: $s = str_replace("\r\n", "\n", $s);
103:
104: $this->templateId = Strings::random();
105: $this->input = & $s;
106: $this->offset = 0;
107: $this->output = '';
108: $this->htmlNodes = $this->macroNodes = array();
109:
110: foreach ($this->macroHandlers as $handler) {
111: $handler->initialize($this);
112: }
113:
114: $len = strlen($s);
115:
116: try {
117: while ($this->offset < $len) {
118: $matches = $this->{"context".$this->context[0]}();
119:
120: if (!$matches) {
121: break;
122:
123: } elseif (!empty($matches['comment'])) {
124:
125: } elseif (!empty($matches['macro'])) {
126: list($macroName, $macroArgs, $macroModifiers) = $this->parseMacro($matches['macro']);
127: $isRightmost = $this->offset >= $len || $this->input[$this->offset] === "\n";
128: $this->writeMacro($macroName, $macroArgs, $macroModifiers, $isRightmost);
129:
130: } else {
131: $this->output .= $matches[0];
132: }
133: }
134: } catch (LatteException $e) {
135: if (!$e->sourceLine) {
136: $e->sourceLine = $this->getLine();
137: }
138: throw $e;
139: }
140:
141: $this->output .= substr($this->input, $this->offset);
142:
143: foreach ($this->htmlNodes as $node) {
144: if (!empty($node->attrs)) {
145: throw new LatteException("Missing end tag </$node->name> for macro-attribute " . self::N_PREFIX
146: . implode(' and ' . self::N_PREFIX, array_keys($node->attrs)) . ".", 0, $this->getLine());
147: }
148: }
149:
150: $prologs = $epilogs = '';
151: foreach ($this->macroHandlers as $handler) {
152: $res = $handler->finalize();
153: $prologs .= isset($res[0]) ? "<?php $res[0]\n?>" : '';
154: $epilogs .= isset($res[1]) ? "<?php $res[1]\n?>" : '';
155: }
156: $this->output = ($prologs ? $prologs . "<?php\n//\n// main template\n//\n?>\n" : '') . $this->output . $epilogs;
157:
158: if ($this->macroNodes) {
159: throw new LatteException("There are unclosed macros.", 0, $this->getLine());
160: }
161:
162: return $this->output;
163: }
164:
165:
166:
167: 168: 169:
170: private function contextText()
171: {
172: $matches = $this->match('~
173: (?:(?<=\n|^)[ \t]*)?<(?P<closing>/?)(?P<tag>[a-z0-9:]+)| ## begin of HTML tag <tag </tag - ignores <!DOCTYPE
174: <(?P<htmlcomment>!--)| ## begin of HTML comment <!--
175: '.$this->macroRe.' ## curly tag
176: ~xsi');
177:
178: if (!$matches || !empty($matches['macro']) || !empty($matches['comment'])) {
179:
180: } elseif (!empty($matches['htmlcomment'])) {
181: $this->context = array(self::CONTEXT_COMMENT);
182:
183: } elseif (empty($matches['closing'])) {
184: $this->htmlNodes[] = $node = new HtmlNode($matches['tag']);
185: $node->offset = strlen($this->output);
186: $this->context = array(self::CONTEXT_TAG);
187:
188: } else {
189: do {
190: $node = array_pop($this->htmlNodes);
191: if (!$node) {
192: $node = new HtmlNode($matches['tag']);
193: }
194: } while (strcasecmp($node->name, $matches['tag']));
195: $this->htmlNodes[] = $node;
196: $node->closing = TRUE;
197: $node->offset = strlen($this->output);
198: $this->context = array(self::CONTEXT_TAG);
199: }
200: return $matches;
201: }
202:
203:
204:
205: 206: 207:
208: private function contextCData()
209: {
210: $node = end($this->htmlNodes);
211: $matches = $this->match('~
212: </'.$node->name.'(?![a-z0-9:])| ## end HTML tag </tag
213: '.$this->macroRe.' ## curly tag
214: ~xsi');
215:
216: if ($matches && empty($matches['macro']) && empty($matches['comment'])) {
217: $node->closing = TRUE;
218: $node->offset = strlen($this->output);
219: $this->context = array(self::CONTEXT_TAG);
220: }
221: return $matches;
222: }
223:
224:
225:
226: 227: 228:
229: private function contextTag()
230: {
231: $matches = $this->match('~
232: (?P<end>\ ?/?>)(?P<tagnewline>[ \t]*\n)?| ## end of HTML tag
233: '.$this->macroRe.'| ## curly tag
234: \s*(?P<attr>[^\s/>={]+)(?:\s*=\s*(?P<value>["\']|[^\s/>{]+))? ## begin of HTML attribute
235: ~xsi');
236:
237: if (!$matches || !empty($matches['macro']) || !empty($matches['comment'])) {
238:
239: } elseif (!empty($matches['end'])) {
240: $node = end($this->htmlNodes);
241: $isEmpty = !$node->closing && (strpos($matches['end'], '/') !== FALSE || $node->isEmpty);
242:
243: if ($isEmpty) {
244: $matches[0] = (Html::$xhtml ? ' />' : '>')
245: . (isset($matches['tagnewline']) ? $matches['tagnewline'] : '');
246: }
247:
248: if (!empty($node->attrs)) {
249: $code = substr($this->output, $node->offset) . $matches[0];
250: $this->output = substr($this->output, 0, $node->offset);
251: $this->writeAttrsMacro($code, $node->attrs, $node->closing);
252: if ($isEmpty) {
253: $this->writeAttrsMacro('', $node->attrs, TRUE);
254: }
255: $matches[0] = '';
256: }
257:
258: if ($isEmpty) {
259: $node->closing = TRUE;
260: }
261:
262: if (!$node->closing && (strcasecmp($node->name, 'script') === 0 || strcasecmp($node->name, 'style') === 0)) {
263: $this->context = array(self::CONTEXT_CDATA, strcasecmp($node->name, 'style') ? 'js' : 'css');
264: } else {
265: $this->context = array(self::CONTEXT_TEXT);
266: if ($node->closing) {
267: array_pop($this->htmlNodes);
268: }
269: }
270:
271: } else {
272: $name = $matches['attr'];
273: $value = isset($matches['value']) ? $matches['value'] : '';
274: $node = end($this->htmlNodes);
275:
276: if (Strings::startsWith($name, self::N_PREFIX)) {
277: $name = substr($name, strlen(self::N_PREFIX));
278: if ($value === '"' || $value === "'") {
279: if ($matches = $this->match('~(.*?)' . $value . '~xsi')) {
280: $value = $matches[1];
281: }
282: }
283: $node->attrs[$name] = $value;
284: $matches[0] = '';
285:
286: } elseif ($value === '"' || $value === "'") {
287: $this->context = array(self::CONTEXT_ATTRIBUTE, $name, $value);
288: }
289: }
290: return $matches;
291: }
292:
293:
294:
295: 296: 297:
298: private function contextAttribute()
299: {
300: $matches = $this->match('~
301: (' . $this->context[2] . ')| ## 1) end of HTML attribute
302: '.$this->macroRe.' ## curly tag
303: ~xsi');
304:
305: if ($matches && empty($matches['macro']) && empty($matches['comment'])) {
306: $this->context = array(self::CONTEXT_TAG);
307: }
308: return $matches;
309: }
310:
311:
312:
313: 314: 315:
316: private function contextComment()
317: {
318: $matches = $this->match('~
319: (--\s*>)| ## 1) end of HTML comment
320: '.$this->macroRe.' ## curly tag
321: ~xsi');
322:
323: if ($matches && empty($matches['macro']) && empty($matches['comment'])) {
324: $this->context = array(self::CONTEXT_TEXT);
325: }
326: return $matches;
327: }
328:
329:
330:
331: 332: 333:
334: private function contextNone()
335: {
336: $matches = $this->match('~
337: '.$this->macroRe.' ## curly tag
338: ~xsi');
339: return $matches;
340: }
341:
342:
343:
344: 345: 346: 347: 348:
349: private function match($re)
350: {
351: if ($matches = Strings::match($this->input, $re, PREG_OFFSET_CAPTURE, $this->offset)) {
352: $this->output .= substr($this->input, $this->offset, $matches[0][1] - $this->offset);
353: $this->offset = $matches[0][1] + strlen($matches[0][0]);
354: foreach ($matches as $k => $v) $matches[$k] = $v[0];
355: }
356: return $matches;
357: }
358:
359:
360:
361: 362: 363: 364:
365: public function getLine()
366: {
367: return $this->input && $this->offset ? substr_count($this->input, "\n", 0, $this->offset - 1) + 1 : NULL;
368: }
369:
370:
371:
372: 373: 374: 375: 376: 377:
378: public function setDelimiters($left, $right)
379: {
380: $this->macroRe = '
381: (?P<comment>' . $left . '\\*.*?\\*' . $right . '\n{0,2})|
382: ' . $left . '
383: (?P<macro>(?:' . self::RE_STRING . '|[^\'"]+?)*?)
384: ' . $right . '
385: (?P<rmargin>[ \t]*(?=\n))?
386: ';
387: return $this;
388: }
389:
390:
391:
392:
393:
394:
395:
396: 397: 398: 399: 400: 401: 402: 403:
404: public function writeMacro($name, $args = NULL, $modifiers = NULL, $isRightmost = FALSE)
405: {
406: $isLeftmost = trim(substr($this->output, $leftOfs = strrpos("\n$this->output", "\n"))) === '';
407:
408: if ($name[0] === '/') {
409: $node = end($this->macroNodes);
410:
411: if (!$node || ("/$node->name" !== $name && '/' !== $name) || $modifiers
412: || ($args && $node->args && !Strings::startsWith("$node->args ", "$args "))
413: ) {
414: $name .= $args ? ' ' : '';
415: throw new LatteException("Unexpected macro {{$name}{$args}{$modifiers}}"
416: . ($node ? ", expecting {/$node->name}" . ($args && $node->args ? " or eventually {/$node->name $node->args}" : '') : ''),
417: 0, $this->getLine());
418: }
419:
420: array_pop($this->macroNodes);
421: if (!$node->args) {
422: $node->setArgs($args);
423: }
424: if ($isLeftmost && $isRightmost) {
425: $this->output = substr($this->output, 0, $leftOfs);
426: }
427:
428: $code = $node->close(substr($this->output, $node->offset));
429:
430: if (!$isLeftmost && $isRightmost && substr($code, -2) === '?>') {
431: $code .= "\n";
432: }
433: $this->output = substr($this->output, 0, $node->offset) . $node->content. $code;
434:
435: } else {
436: list($node, $code) = $this->expandMacro($name, $args, $modifiers);
437: if (!$node->isEmpty) {
438: $this->macroNodes[] = $node;
439: }
440:
441: if ($isRightmost) {
442: if ($isLeftmost && substr($code, 0, 11) !== '<?php echo ') {
443: $this->output = substr($this->output, 0, $leftOfs);
444: } elseif (substr($code, -2) === '?>') {
445: $code .= "\n";
446: }
447: }
448:
449: $this->output .= $code;
450: $node->offset = strlen($this->output);
451: }
452: }
453:
454:
455:
456: 457: 458: 459: 460: 461: 462:
463: public function writeAttrsMacro($code, $attrs, $closing)
464: {
465: $left = $right = array();
466: foreach ($this->macros as $name => $foo) {
467: if ($name[0] === '@') {
468: $name = substr($name, 1);
469: if (isset($attrs[$name])) {
470: if (!$closing) {
471: $pos = strrpos($code, '>');
472: if ($code[$pos-1] === '/') {
473: $pos--;
474: }
475: list(, $macroCode) = $this->expandMacro("@$name", $attrs[$name]);
476: $code = substr_replace($code, $macroCode, $pos, 0);
477: }
478: unset($attrs[$name]);
479: }
480: }
481:
482: $macro = $closing ? "/$name" : $name;
483: if (isset($attrs[$name])) {
484: if ($closing) {
485: $right[] = array($macro, '');
486: } else {
487: array_unshift($left, array($macro, $attrs[$name]));
488: }
489: }
490:
491: $innerName = "inner-$name";
492: if (isset($attrs[$innerName])) {
493: if ($closing) {
494: $left[] = array($macro, '');
495: } else {
496: array_unshift($right, array($macro, $attrs[$innerName]));
497: }
498: }
499:
500: $tagName = "tag-$name";
501: if (isset($attrs[$tagName])) {
502: array_unshift($left, array($name, $attrs[$tagName]));
503: $right[] = array("/$name", '');
504: }
505:
506: unset($attrs[$name], $attrs[$innerName], $attrs[$tagName]);
507: }
508:
509: if ($attrs) {
510: throw new LatteException("Unknown macro-attribute " . self::N_PREFIX
511: . implode(' and ' . self::N_PREFIX, array_keys($attrs)), 0, $this->getLine());
512: }
513:
514: foreach ($left as $item) {
515: $this->writeMacro($item[0], $item[1]);
516: if (substr($this->output, -2) === '?>') {
517: $this->output .= "\n";
518: }
519: }
520: $this->output .= $code;
521:
522: foreach ($right as $item) {
523: $this->writeMacro($item[0], $item[1]);
524: if (substr($this->output, -2) === '?>') {
525: $this->output .= "\n";
526: }
527: }
528: }
529:
530:
531:
532: 533: 534: 535: 536: 537: 538:
539: public function expandMacro($name, $args, $modifiers = NULL)
540: {
541: if (empty($this->macros[$name])) {
542: throw new LatteException("Unknown macro {{$name}}", 0, $this->getLine());
543: }
544: foreach (array_reverse($this->macros[$name]) as $macro) {
545: $node = new MacroNode($macro, $name, $args, $modifiers, $this->macroNodes ? end($this->macroNodes) : NULL);
546: $code = $macro->nodeOpened($node);
547: if ($code !== FALSE) {
548: return array($node, $code);
549: }
550: }
551: throw new LatteException("Unhandled macro {{$name}}", 0, $this->getLine());
552: }
553:
554:
555:
556: 557: 558: 559: 560:
561: public function parseMacro($macro)
562: {
563: $match = Strings::match($macro, '~^
564: (
565: (?P<name>\?|/?[a-z]\w*+(?:[.:]\w+)*+(?!::|\())| ## ?, name, /name, but not function( or class::
566: (?P<noescape>!?)(?P<shortname>/?[=\~#%^&_]?) ## [!] [=] expression to print
567: )(?P<args>.*?)
568: (?P<modifiers>\|[a-z](?:'.Parser::RE_STRING.'|[^\'"]+)*)?
569: ()$~isx');
570:
571: if (!$match) {
572: return FALSE;
573: }
574: if ($match['name'] === '') {
575: $match['name'] = ($tmp=$match['shortname']) ? $tmp : '=';
576: if (!$match['noescape'] && substr($match['shortname'], 0, 1) !== '/') {
577: $match['modifiers'] .= '|escape';
578: }
579: }
580: return array($match['name'], trim($match['args']), $match['modifiers']);
581: }
582:
583: }
584: