1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Latte;
9:
10:
11: 12: 13:
14: class Compiler
15: {
16: use Strict;
17:
18:
19: const
20: CONTENT_HTML = Engine::CONTENT_HTML,
21: CONTENT_XHTML = Engine::CONTENT_XHTML,
22: CONTENT_XML = Engine::CONTENT_XML,
23: CONTENT_JS = Engine::CONTENT_JS,
24: CONTENT_CSS = Engine::CONTENT_CSS,
25: CONTENT_ICAL = Engine::CONTENT_ICAL,
26: CONTENT_TEXT = Engine::CONTENT_TEXT;
27:
28:
29: const
30: CONTEXT_HTML_TEXT = null,
31: CONTEXT_HTML_TAG = 'Tag',
32: CONTEXT_HTML_ATTRIBUTE = 'Attr',
33: CONTEXT_HTML_ATTRIBUTE_JS = 'AttrJs',
34: CONTEXT_HTML_ATTRIBUTE_CSS = 'AttrCss',
35: CONTEXT_HTML_ATTRIBUTE_URL = 'AttrUrl',
36: CONTEXT_HTML_ATTRIBUTE_UNQUOTED_URL = 'AttrUnquotedUrl',
37: = 'Comment',
38: = 'Bogus',
39: CONTEXT_HTML_CSS = 'Css',
40: CONTEXT_HTML_JS = 'Js',
41:
42: CONTEXT_XML_TEXT = self::CONTEXT_HTML_TEXT,
43: CONTEXT_XML_TAG = self::CONTEXT_HTML_TAG,
44: CONTEXT_XML_ATTRIBUTE = self::CONTEXT_HTML_ATTRIBUTE,
45: = self::CONTEXT_HTML_COMMENT,
46: = self::CONTEXT_HTML_BOGUS_COMMENT;
47:
48:
49: private $tokens;
50:
51:
52: private $output;
53:
54:
55: private $position;
56:
57:
58: private $macros;
59:
60:
61: private $flags;
62:
63:
64: private $htmlNode;
65:
66:
67: private $macroNode;
68:
69:
70: private $placeholders = [];
71:
72:
73: private $contentType = self::CONTENT_HTML;
74:
75:
76: private $context;
77:
78:
79: private $lastAttrValue;
80:
81:
82: private $tagOffset;
83:
84:
85: private $inHead;
86:
87:
88: private $methods = [];
89:
90:
91: private $properties = [];
92:
93:
94: 95: 96: 97: 98:
99: public function addMacro($name, IMacro $macro, $flags = null)
100: {
101: if (!isset($this->flags[$name])) {
102: $this->flags[$name] = $flags ?: IMacro::DEFAULT_FLAGS;
103: } elseif ($flags && $this->flags[$name] !== $flags) {
104: throw new \LogicException("Incompatible flags for macro $name.");
105: }
106: $this->macros[$name][] = $macro;
107: return $this;
108: }
109:
110:
111: 112: 113: 114: 115:
116: public function compile(array $tokens, $className)
117: {
118: $this->tokens = $tokens;
119: $output = '';
120: $this->output = &$output;
121: $this->inHead = true;
122: $this->htmlNode = $this->macroNode = $this->context = null;
123: $this->placeholders = $this->properties = [];
124: $this->methods = ['main' => null, 'prepare' => null];
125:
126: $macroHandlers = new \SplObjectStorage;
127: array_map([$macroHandlers, 'attach'], call_user_func_array('array_merge', $this->macros));
128:
129: foreach ($macroHandlers as $handler) {
130: $handler->initialize($this);
131: }
132:
133: foreach ($tokens as $this->position => $token) {
134: if ($this->inHead && !($token->type === $token::COMMENT
135: || $token->type === $token::MACRO_TAG && isset($this->flags[$token->name]) && $this->flags[$token->name] & IMacro::ALLOWED_IN_HEAD
136: || $token->type === $token::TEXT && trim($token->text) === ''
137: )) {
138: $this->inHead = false;
139: }
140: $this->{"process$token->type"}($token);
141: }
142:
143: while ($this->htmlNode) {
144: if (!empty($this->htmlNode->macroAttrs)) {
145: throw new CompileException('Missing ' . self::printEndTag($this->htmlNode));
146: }
147: $this->htmlNode = $this->htmlNode->parentNode;
148: }
149:
150: while ($this->macroNode) {
151: if (~$this->flags[$this->macroNode->name] & IMacro::AUTO_CLOSE) {
152: throw new CompileException('Missing ' . self::printEndTag($this->macroNode));
153: }
154: $this->closeMacro($this->macroNode->name);
155: }
156:
157: $prepare = $epilogs = '';
158: foreach ($macroHandlers as $handler) {
159: $res = $handler->finalize();
160: $prepare .= empty($res[0]) ? '' : "<?php $res[0] ?>";
161: $epilogs = (empty($res[1]) ? '' : "<?php $res[1] ?>") . $epilogs;
162: }
163:
164: $this->addMethod('main', $this->expandTokens("extract(\$this->params);?>\n$output$epilogs<?php return get_defined_vars();"));
165:
166: if ($prepare) {
167: $this->addMethod('prepare', "extract(\$this->params);?>$prepare<?php");
168: }
169: if ($this->contentType !== self::CONTENT_HTML) {
170: $this->addProperty('contentType', $this->contentType);
171: }
172:
173: foreach ($this->properties as $name => $value) {
174: $members[] = "\tpublic $$name = " . PhpHelpers::dump($value) . ';';
175: }
176: foreach (array_filter($this->methods) as $name => $method) {
177: $members[] = "\n\tfunction $name($method[arguments])\n\t{\n" . ($method['body'] ? "\t\t$method[body]\n" : '') . "\t}";
178: }
179:
180: return "<?php\n"
181: . "use Latte\\Runtime as LR;\n\n"
182: . "class $className extends Latte\\Runtime\\Template\n{\n"
183: . implode("\n\n", $members)
184: . "\n\n}\n";
185: }
186:
187:
188: 189: 190:
191: public function setContentType($type)
192: {
193: $this->contentType = $type;
194: $this->context = null;
195: return $this;
196: }
197:
198:
199: 200: 201:
202: public function getContentType()
203: {
204: trigger_error(__METHOD__ . ' is deprecated.', E_USER_DEPRECATED);
205: return $this->contentType;
206: }
207:
208:
209: 210: 211:
212: public function setContext($context)
213: {
214: trigger_error(__METHOD__ . ' is deprecated.', E_USER_DEPRECATED);
215: $this->context = $context;
216: return $this;
217: }
218:
219:
220: 221: 222:
223: public function getContext()
224: {
225: trigger_error(__METHOD__ . ' is deprecated.', E_USER_DEPRECATED);
226: return $this->context;
227: }
228:
229:
230: 231: 232:
233: public function getMacroNode()
234: {
235: return $this->macroNode;
236: }
237:
238:
239: 240: 241: 242:
243: public function getLine()
244: {
245: return isset($this->tokens[$this->position]) ? $this->tokens[$this->position]->line : null;
246: }
247:
248:
249: 250: 251:
252: public function isInHead()
253: {
254: return $this->inHead;
255: }
256:
257:
258: 259: 260: 261: 262:
263: public function addMethod($name, $body, $arguments = '')
264: {
265: $this->methods[$name] = ['body' => trim($body), 'arguments' => $arguments];
266: }
267:
268:
269: 270: 271: 272: 273:
274: public function getMethods()
275: {
276: return $this->methods;
277: }
278:
279:
280: 281: 282: 283: 284:
285: public function addProperty($name, $value)
286: {
287: $this->properties[$name] = $value;
288: }
289:
290:
291: 292: 293: 294: 295:
296: public function getProperties()
297: {
298: return $this->properties;
299: }
300:
301:
302:
303: public function expandTokens($s)
304: {
305: return strtr($s, $this->placeholders);
306: }
307:
308:
309: private function processText(Token $token)
310: {
311: if ($this->lastAttrValue === '' && $this->context && Helpers::startsWith($this->context, self::CONTEXT_HTML_ATTRIBUTE)) {
312: $this->lastAttrValue = $token->text;
313: }
314: $this->output .= $this->escape($token->text);
315: }
316:
317:
318: private function processMacroTag(Token $token)
319: {
320: if ($this->context === self::CONTEXT_HTML_TAG || $this->context && Helpers::startsWith($this->context, self::CONTEXT_HTML_ATTRIBUTE)) {
321: $this->lastAttrValue = true;
322: }
323:
324: $isRightmost = !isset($this->tokens[$this->position + 1])
325: || substr($this->tokens[$this->position + 1]->text, 0, 1) === "\n";
326:
327: if ($token->closing) {
328: $this->closeMacro($token->name, $token->value, $token->modifiers, $isRightmost);
329: } else {
330: if (!$token->empty && isset($this->flags[$token->name]) && $this->flags[$token->name] & IMacro::AUTO_EMPTY) {
331: $pos = $this->position;
332: while (($t = isset($this->tokens[++$pos]) ? $this->tokens[$pos] : null)
333: && ($t->type !== Token::MACRO_TAG || $t->name !== $token->name)
334: && ($t->type !== Token::HTML_ATTRIBUTE_BEGIN || $t->name !== Parser::N_PREFIX . $token->name));
335: $token->empty = $t ? !$t->closing : true;
336: }
337: $node = $this->openMacro($token->name, $token->value, $token->modifiers, $isRightmost);
338: if ($token->empty) {
339: if ($node->empty) {
340: throw new CompileException("Unexpected /} in tag {$token->text}");
341: }
342: $this->closeMacro($token->name, null, null, $isRightmost);
343: }
344: }
345: }
346:
347:
348: private function processHtmlTagBegin(Token $token)
349: {
350: if ($token->closing) {
351: while ($this->htmlNode) {
352: if (strcasecmp($this->htmlNode->name, $token->name) === 0) {
353: break;
354: }
355: if ($this->htmlNode->macroAttrs) {
356: throw new CompileException("Unexpected </$token->name>, expecting " . self::printEndTag($this->htmlNode));
357: }
358: $this->htmlNode = $this->htmlNode->parentNode;
359: }
360: if (!$this->htmlNode) {
361: $this->htmlNode = new HtmlNode($token->name);
362: }
363: $this->htmlNode->closing = true;
364: $this->htmlNode->endLine = $this->getLine();
365: $this->context = self::CONTEXT_HTML_TEXT;
366:
367: } elseif ($token->text === '<!--') {
368: $this->context = self::CONTEXT_HTML_COMMENT;
369:
370: } elseif ($token->text === '<?' || $token->text === '<!') {
371: $this->context = self::CONTEXT_HTML_BOGUS_COMMENT;
372: $this->output .= $token->text === '<?' ? '<<?php ?>?' : '<!';
373: return;
374:
375: } else {
376: $this->htmlNode = new HtmlNode($token->name, $this->htmlNode);
377: $this->htmlNode->startLine = $this->getLine();
378: $this->context = self::CONTEXT_HTML_TAG;
379: }
380: $this->tagOffset = strlen($this->output);
381: $this->output .= $token->text;
382: }
383:
384:
385: private function processHtmlTagEnd(Token $token)
386: {
387: if (in_array($this->context, [self::CONTEXT_HTML_COMMENT, self::CONTEXT_HTML_BOGUS_COMMENT], true)) {
388: $this->output .= $token->text;
389: $this->context = self::CONTEXT_HTML_TEXT;
390: return;
391: }
392:
393: $htmlNode = $this->htmlNode;
394: $end = '';
395:
396: if (!$htmlNode->closing) {
397: $htmlNode->empty = strpos($token->text, '/') !== false;
398: if (in_array($this->contentType, [self::CONTENT_HTML, self::CONTENT_XHTML], true)) {
399: $emptyElement = isset(Helpers::$emptyElements[strtolower($htmlNode->name)]);
400: $htmlNode->empty = $htmlNode->empty || $emptyElement;
401: if ($htmlNode->empty) {
402: $space = substr(strstr($token->text, '>'), 1);
403: if ($emptyElement) {
404: $token->text = ($this->contentType === self::CONTENT_XHTML ? ' />' : '>') . $space;
405: } else {
406: $token->text = '>';
407: $end = "</$htmlNode->name>" . $space;
408: }
409: }
410: }
411: }
412:
413: if ($htmlNode->macroAttrs) {
414: $html = substr($this->output, $this->tagOffset) . $token->text;
415: $this->output = substr($this->output, 0, $this->tagOffset);
416: $this->writeAttrsMacro($html);
417: } else {
418: $this->output .= $token->text . $end;
419: }
420:
421: if ($htmlNode->empty) {
422: $htmlNode->closing = true;
423: if ($htmlNode->macroAttrs) {
424: $this->writeAttrsMacro($end);
425: }
426: }
427:
428: $this->context = self::CONTEXT_HTML_TEXT;
429:
430: if ($htmlNode->closing) {
431: $this->htmlNode = $this->htmlNode->parentNode;
432:
433: } elseif ((($lower = strtolower($htmlNode->name)) === 'script' || $lower === 'style')
434: && (!isset($htmlNode->attrs['type']) || preg_match('#(java|j|ecma|live)script|json|css#i', $htmlNode->attrs['type']))
435: ) {
436: $this->context = $lower === 'script' ? self::CONTEXT_HTML_JS : self::CONTEXT_HTML_CSS;
437: }
438: }
439:
440:
441: private function processHtmlAttributeBegin(Token $token)
442: {
443: if (Helpers::startsWith($token->name, Parser::N_PREFIX)) {
444: $name = substr($token->name, strlen(Parser::N_PREFIX));
445: if (isset($this->htmlNode->macroAttrs[$name])) {
446: throw new CompileException("Found multiple attributes $token->name.");
447:
448: } elseif ($this->macroNode && $this->macroNode->htmlNode === $this->htmlNode) {
449: throw new CompileException("n:attributes must not appear inside macro; found $token->name inside {{$this->macroNode->name}}.");
450: }
451: $this->htmlNode->macroAttrs[$name] = $token->value;
452: return;
453: }
454:
455: $this->lastAttrValue = &$this->htmlNode->attrs[$token->name];
456: $this->output .= $this->escape($token->text);
457:
458: $lower = strtolower($token->name);
459: if (in_array($token->value, ['"', "'"], true)) {
460: $this->lastAttrValue = '';
461: $this->context = self::CONTEXT_HTML_ATTRIBUTE;
462: if (in_array($this->contentType, [self::CONTENT_HTML, self::CONTENT_XHTML], true)) {
463: if (Helpers::startsWith($lower, 'on')) {
464: $this->context = self::CONTEXT_HTML_ATTRIBUTE_JS;
465: } elseif ($lower === 'style') {
466: $this->context = self::CONTEXT_HTML_ATTRIBUTE_CSS;
467: }
468: }
469: } else {
470: $this->lastAttrValue = $token->value;
471: $this->context = self::CONTEXT_HTML_TAG;
472: }
473:
474: if (in_array($this->contentType, [self::CONTENT_HTML, self::CONTENT_XHTML], true)
475: && (in_array($lower, ['href', 'src', 'action', 'formaction'], true)
476: || ($lower === 'data' && strtolower($this->htmlNode->name) === 'object'))
477: ) {
478: $this->context = $this->context === self::CONTEXT_HTML_TAG ? self::CONTEXT_HTML_ATTRIBUTE_UNQUOTED_URL : self::CONTEXT_HTML_ATTRIBUTE_URL;
479: }
480: }
481:
482:
483: private function processHtmlAttributeEnd(Token $token)
484: {
485: $this->context = self::CONTEXT_HTML_TAG;
486: $this->output .= $token->text;
487: }
488:
489:
490: private function (Token $token)
491: {
492: $leftOfs = ($tmp = strrpos($this->output, "\n")) === false ? 0 : $tmp + 1;
493: $isLeftmost = trim(substr($this->output, $leftOfs)) === '';
494: $isRightmost = substr($token->text, -1) === "\n";
495: if ($isLeftmost && $isRightmost) {
496: $this->output = substr($this->output, 0, $leftOfs);
497: } else {
498: $this->output .= substr($token->text, strlen(rtrim($token->text, "\n")));
499: }
500: }
501:
502:
503: private function escape($s)
504: {
505: return preg_replace_callback('#<(\z|\?xml|\?)#', function ($m) {
506: if ($m[1] === '?') {
507: trigger_error('Inline <?php ... ?> is deprecated, use {php ... } on line ' . $this->getLine(), E_USER_DEPRECATED);
508: return '<?';
509: } else {
510: return '<<?php ?>' . $m[1];
511: }
512: }, $s);
513: }
514:
515:
516:
517:
518:
519: 520: 521: 522: 523: 524: 525: 526: 527:
528: public function openMacro($name, $args = null, $modifiers = null, $isRightmost = false, $nPrefix = null)
529: {
530: $node = $this->expandMacro($name, $args, $modifiers, $nPrefix);
531: if ($node->empty) {
532: $this->writeCode((string) $node->openingCode, $node->replaced, $isRightmost);
533: if ($node->prefix && $node->prefix !== MacroNode::PREFIX_TAG) {
534: $this->htmlNode->attrCode .= $node->attrCode;
535: }
536: } else {
537: $this->macroNode = $node;
538: $node->saved = [&$this->output, $isRightmost];
539: $this->output = &$node->content;
540: $this->output = '';
541: }
542: return $node;
543: }
544:
545:
546: 547: 548: 549: 550: 551: 552: 553: 554:
555: public function closeMacro($name, $args = null, $modifiers = null, $isRightmost = false, $nPrefix = null)
556: {
557: $node = $this->macroNode;
558:
559: if (!$node || ($node->name !== $name && $name !== '') || $modifiers
560: || ($args && $node->args && !Helpers::startsWith("$node->args ", "$args "))
561: || $nPrefix !== $node->prefix
562: ) {
563: $name = $nPrefix
564: ? "</{$this->htmlNode->name}> for " . Parser::N_PREFIX . implode(' and ' . Parser::N_PREFIX, array_keys($this->htmlNode->macroAttrs))
565: : '{/' . $name . ($args ? ' ' . $args : '') . $modifiers . '}';
566: throw new CompileException("Unexpected $name" . ($node ? ', expecting ' . self::printEndTag($node->prefix ? $this->htmlNode : $node) : ''));
567: }
568:
569: $this->macroNode = $node->parentNode;
570: if (!$node->args) {
571: $node->setArgs($args);
572: }
573:
574: if ($node->prefix === MacroNode::PREFIX_NONE) {
575: $parts = explode($node->htmlNode->innerMarker, $node->content);
576: if (count($parts) === 3) {
577: $node->innerContent = $parts[1];
578: }
579: }
580:
581: $node->closing = true;
582: $node->endLine = $node->prefix ? $node->htmlNode->endLine : $this->getLine();
583: $node->macro->nodeClosed($node);
584:
585: if (isset($parts[1]) && $node->innerContent !== $parts[1]) {
586: $node->content = implode($node->htmlNode->innerMarker, [$parts[0], $node->innerContent, $parts[2]]);
587: }
588:
589: if ($node->prefix && $node->prefix !== MacroNode::PREFIX_TAG) {
590: $this->htmlNode->attrCode .= $node->attrCode;
591: }
592: $this->output = &$node->saved[0];
593: $this->writeCode((string) $node->openingCode, $node->replaced, $node->saved[1]);
594: $this->output .= $node->content;
595: $this->writeCode((string) $node->closingCode, $node->replaced, $isRightmost);
596: return $node;
597: }
598:
599:
600: private function writeCode($code, $isReplaced, $isRightmost)
601: {
602: if ($isRightmost) {
603: $leftOfs = ($tmp = strrpos($this->output, "\n")) === false ? 0 : $tmp + 1;
604: $isLeftmost = trim(substr($this->output, $leftOfs)) === '';
605: if ($isReplaced === null) {
606: $isReplaced = preg_match('#<\?php.*\secho\s#As', $code);
607: }
608: if ($isLeftmost && !$isReplaced) {
609: $this->output = substr($this->output, 0, $leftOfs);
610: if (substr($code, -2) !== '?>') {
611: $code .= '<?php ?>';
612: }
613: } elseif (substr($code, -2) === '?>') {
614: $code .= "\n";
615: }
616: }
617: $this->output .= $code;
618: }
619:
620:
621: 622: 623: 624: 625: 626:
627: public function writeAttrsMacro($html)
628: {
629:
630:
631: $attrs = $this->htmlNode->macroAttrs;
632: $left = $right = [];
633:
634: foreach ($this->macros as $name => $foo) {
635: $attrName = MacroNode::PREFIX_INNER . "-$name";
636: if (isset($attrs[$attrName])) {
637: if ($this->htmlNode->closing) {
638: $left[] = function () use ($name) {
639: $this->closeMacro($name, '', null, false, MacroNode::PREFIX_INNER);
640: };
641: } else {
642: array_unshift($right, function () use ($name, $attrs, $attrName) {
643: if ($this->openMacro($name, $attrs[$attrName], null, false, MacroNode::PREFIX_INNER)->empty) {
644: throw new CompileException("Unable to use empty macro as n:$attrName.");
645: }
646: });
647: }
648: unset($attrs[$attrName]);
649: }
650: }
651:
652: $innerMarker = '';
653: if ($this->htmlNode->closing) {
654: $left[] = function () {
655: $this->output .= $this->htmlNode->innerMarker;
656: };
657: } else {
658: array_unshift($right, function () use (&$innerMarker) {
659: $this->output .= $innerMarker;
660: });
661: }
662:
663:
664: foreach (array_reverse($this->macros) as $name => $foo) {
665: $attrName = MacroNode::PREFIX_TAG . "-$name";
666: if (isset($attrs[$attrName])) {
667: $left[] = function () use ($name, $attrs, $attrName) {
668: if ($this->openMacro($name, $attrs[$attrName], null, false, MacroNode::PREFIX_TAG)->empty) {
669: throw new CompileException("Unable to use empty macro as n:$attrName.");
670: }
671: };
672: array_unshift($right, function () use ($name) {
673: $this->closeMacro($name, '', null, false, MacroNode::PREFIX_TAG);
674: });
675: unset($attrs[$attrName]);
676: }
677: }
678:
679: foreach ($this->macros as $name => $foo) {
680: if (isset($attrs[$name])) {
681: if ($this->htmlNode->closing) {
682: $right[] = function () use ($name) {
683: $this->closeMacro($name, '', null, false, MacroNode::PREFIX_NONE);
684: };
685: } else {
686: array_unshift($left, function () use ($name, $attrs, &$innerMarker) {
687: $node = $this->openMacro($name, $attrs[$name], null, false, MacroNode::PREFIX_NONE);
688: if ($node->empty) {
689: unset($this->htmlNode->macroAttrs[$name]);
690: } elseif (!$innerMarker) {
691: $this->htmlNode->innerMarker = $innerMarker = '<n:q' . count($this->placeholders) . 'q>';
692: $this->placeholders[$innerMarker] = '';
693: }
694: });
695: }
696: unset($attrs[$name]);
697: }
698: }
699:
700: if ($attrs) {
701: throw new CompileException('Unknown attribute ' . Parser::N_PREFIX
702: . implode(' and ' . Parser::N_PREFIX, array_keys($attrs)));
703: }
704:
705: if (!$this->htmlNode->closing) {
706: $this->htmlNode->attrCode = &$this->placeholders[$uniq = ' n:q' . count($this->placeholders) . 'q'];
707: $html = substr_replace($html, $uniq, strrpos($html, '/>') ?: strrpos($html, '>'), 0);
708: }
709:
710: foreach ($left as $func) {
711: $func();
712: }
713:
714: $this->output .= $html;
715:
716: foreach ($right as $func) {
717: $func();
718: }
719:
720: if ($right && substr($this->output, -2) === '?>') {
721: $this->output .= "\n";
722: }
723: }
724:
725:
726: 727: 728: 729: 730: 731: 732: 733:
734: public function expandMacro($name, $args, $modifiers = null, $nPrefix = null)
735: {
736: $inScript = in_array($this->context, [self::CONTEXT_HTML_JS, self::CONTEXT_HTML_CSS], true);
737:
738: if (empty($this->macros[$name])) {
739: $hint = ($t = Helpers::getSuggestion(array_keys($this->macros), $name)) ? ", did you mean {{$t}}?" : '';
740: throw new CompileException("Unknown macro {{$name}}$hint" . ($inScript ? ' (in JavaScript or CSS, try to put a space after bracket or use n:syntax=off)' : ''));
741: }
742:
743: if ($modifiers && preg_match('#\|(no)?safeurl(?!\w)#i', $modifiers, $m)) {
744: $hint = $m[1] ? '|nocheck' : '|checkurl';
745: $modifiers = str_replace($m[0], $hint, $modifiers);
746: trigger_error("Modifier $m[0] is deprecated, please replace it with $hint.", E_USER_DEPRECATED);
747: }
748:
749: if (strpbrk($name, '=~%^&_')) {
750: if (in_array($this->context, [self::CONTEXT_HTML_ATTRIBUTE_URL, self::CONTEXT_HTML_ATTRIBUTE_UNQUOTED_URL], true)) {
751: if (!Helpers::removeFilter($modifiers, 'nosafeurl|nocheck') && !preg_match('#\|datastream(?=\s|\||\z)#i', $modifiers)) {
752: $modifiers .= '|checkurl';
753: }
754: }
755:
756: if (!Helpers::removeFilter($modifiers, 'noescape')) {
757: $modifiers .= '|escape';
758: if ($inScript && $name === '=' && preg_match('#["\'] *\z#', $this->tokens[$this->position - 1]->text)) {
759: throw new CompileException("Do not place {$this->tokens[$this->position]->text} inside quotes.");
760: }
761: }
762: }
763:
764: if ($nPrefix === MacroNode::PREFIX_INNER && !strcasecmp($this->htmlNode->name, 'script')) {
765: $context = [$this->contentType, self::CONTEXT_HTML_JS];
766: } elseif ($nPrefix === MacroNode::PREFIX_INNER && !strcasecmp($this->htmlNode->name, 'style')) {
767: $context = [$this->contentType, self::CONTEXT_HTML_CSS];
768: } elseif ($nPrefix) {
769: $context = [$this->contentType, self::CONTEXT_HTML_TEXT];
770: } else {
771: $context = [$this->contentType, $this->context];
772: }
773:
774: foreach (array_reverse($this->macros[$name]) as $macro) {
775: $node = new MacroNode($macro, $name, $args, $modifiers, $this->macroNode, $this->htmlNode, $nPrefix);
776: $node->context = $context;
777: $node->startLine = $nPrefix ? $this->htmlNode->startLine : $this->getLine();
778: if ($macro->nodeOpened($node) !== false) {
779: return $node;
780: }
781: }
782:
783: throw new CompileException('Unknown ' . ($nPrefix
784: ? 'attribute ' . Parser::N_PREFIX . ($nPrefix === MacroNode::PREFIX_NONE ? '' : "$nPrefix-") . $name
785: : 'macro {' . $name . ($args ? " $args" : '') . '}'
786: ));
787: }
788:
789:
790: private static function printEndTag($node)
791: {
792: if ($node instanceof HtmlNode) {
793: return "</{$node->name}> for " . Parser::N_PREFIX
794: . implode(' and ' . Parser::N_PREFIX, array_keys($node->macroAttrs));
795: } else {
796: return "{/$node->name}";
797: }
798: }
799: }
800: