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