Namespaces

  • Latte
    • Loaders
    • Macros
    • Runtime
  • Nette
    • Application
      • Responses
      • Routers
      • UI
    • Bridges
      • ApplicationDI
      • ApplicationLatte
      • ApplicationTracy
      • CacheDI
      • CacheLatte
      • DatabaseDI
      • DatabaseTracy
      • DITracy
      • FormsDI
      • FormsLatte
      • Framework
      • HttpDI
      • HttpTracy
      • MailDI
      • ReflectionDI
      • SecurityDI
      • SecurityTracy
    • Caching
      • Storages
    • ComponentModel
    • Database
      • Conventions
      • Drivers
      • Table
    • DI
      • Config
        • Adapters
      • Extensions
    • Forms
      • Controls
      • Rendering
    • Http
    • Iterators
    • Loaders
    • Localization
    • Mail
    • Neon
    • PhpGenerator
    • Reflection
    • Security
    • Tokenizer
    • Utils
  • Tracy
    • Bridges
      • Nette
  • none

Classes

  • Compiler
  • Engine
  • HtmlNode
  • MacroNode
  • MacroTokens
  • Parser
  • PhpWriter
  • Token

Interfaces

  • ILoader
  • IMacro

Traits

  • Strict

Exceptions

  • CompileException
  • RegexpException
  • RuntimeException
  • Overview
  • Namespace
  • Class
  • Tree
  • Deprecated
  1: <?php
  2: 
  3: /**
  4:  * This file is part of the Latte (https://latte.nette.org)
  5:  * Copyright (c) 2008 David Grudl (https://davidgrudl.com)
  6:  */
  7: 
  8: namespace Latte;
  9: 
 10: 
 11: /**
 12:  * Latte compiler.
 13:  */
 14: class Compiler
 15: {
 16:     use Strict;
 17: 
 18:     /** Context-aware escaping content types */
 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:     /** @internal Context-aware escaping HTML contexts */
 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:         CONTEXT_HTML_COMMENT = 'Comment',
 38:         CONTEXT_HTML_BOGUS_COMMENT = '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:         CONTEXT_XML_COMMENT = self::CONTEXT_HTML_COMMENT,
 46:         CONTEXT_XML_BOGUS_COMMENT = self::CONTEXT_HTML_BOGUS_COMMENT;
 47: 
 48:     /** @var Token[] */
 49:     private $tokens;
 50: 
 51:     /** @var string pointer to current node content */
 52:     private $output;
 53: 
 54:     /** @var int  position on source template */
 55:     private $position;
 56: 
 57:     /** @var array of [name => IMacro[]] */
 58:     private $macros;
 59: 
 60:     /** @var int[] IMacro flags */
 61:     private $flags;
 62: 
 63:     /** @var HtmlNode */
 64:     private $htmlNode;
 65: 
 66:     /** @var MacroNode */
 67:     private $macroNode;
 68: 
 69:     /** @var string[] */
 70:     private $placeholders = [];
 71: 
 72:     /** @var string */
 73:     private $contentType = self::CONTENT_HTML;
 74: 
 75:     /** @var string|null */
 76:     private $context;
 77: 
 78:     /** @var mixed */
 79:     private $lastAttrValue;
 80: 
 81:     /** @var int */
 82:     private $tagOffset;
 83: 
 84:     /** @var bool */
 85:     private $inHead;
 86: 
 87:     /** @var array of [name => [body, arguments]] */
 88:     private $methods = [];
 89: 
 90:     /** @var array of [name => serialized value] */
 91:     private $properties = [];
 92: 
 93: 
 94:     /**
 95:      * Adds new macro with IMacro flags.
 96:      * @param  string
 97:      * @return static
 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:      * Compiles tokens to PHP code.
113:      * @param  Token[]
114:      * @return string
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:      * @return static
190:      */
191:     public function setContentType($type)
192:     {
193:         $this->contentType = $type;
194:         $this->context = null;
195:         return $this;
196:     }
197: 
198: 
199:     /**
200:      * @deprecated
201:      */
202:     public function getContentType()
203:     {
204:         trigger_error(__METHOD__ . ' is deprecated.', E_USER_DEPRECATED);
205:         return $this->contentType;
206:     }
207: 
208: 
209:     /**
210:      * @internal
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:      * @deprecated
222:      */
223:     public function getContext()
224:     {
225:         trigger_error(__METHOD__ . ' is deprecated.', E_USER_DEPRECATED);
226:         return $this->context;
227:     }
228: 
229: 
230:     /**
231:      * @return MacroNode|null
232:      */
233:     public function getMacroNode()
234:     {
235:         return $this->macroNode;
236:     }
237: 
238: 
239:     /**
240:      * Returns current line number.
241:      * @return int|null
242:      */
243:     public function getLine()
244:     {
245:         return isset($this->tokens[$this->position]) ? $this->tokens[$this->position]->line : null;
246:     }
247: 
248: 
249:     /**
250:      * @return bool
251:      */
252:     public function isInHead()
253:     {
254:         return $this->inHead;
255:     }
256: 
257: 
258:     /**
259:      * Adds custom method to template.
260:      * @return void
261:      * @internal
262:      */
263:     public function addMethod($name, $body, $arguments = '')
264:     {
265:         $this->methods[$name] = ['body' => trim($body), 'arguments' => $arguments];
266:     }
267: 
268: 
269:     /**
270:      * Returns custom methods.
271:      * @return array
272:      * @internal
273:      */
274:     public function getMethods()
275:     {
276:         return $this->methods;
277:     }
278: 
279: 
280:     /**
281:      * Adds custom property to template.
282:      * @return void
283:      * @internal
284:      */
285:     public function addProperty($name, $value)
286:     {
287:         $this->properties[$name] = $value;
288:     }
289: 
290: 
291:     /**
292:      * Returns custom properites.
293:      * @return array
294:      * @internal
295:      */
296:     public function getProperties()
297:     {
298:         return $this->properties;
299:     }
300: 
301: 
302:     /** @internal */
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 ?>?' : '<!'; // bypass error in escape()
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) { // auto-correct
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 processComment(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:     /********************* macros ****************d*g**/
517: 
518: 
519:     /**
520:      * Generates code for {macro ...} to the output.
521:      * @param  string
522:      * @param  string
523:      * @param  string
524:      * @param  bool
525:      * @return MacroNode
526:      * @internal
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:      * Generates code for {/macro ...} to the output.
548:      * @param  string
549:      * @param  string
550:      * @param  string
551:      * @param  bool
552:      * @return MacroNode
553:      * @internal
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) { // markers may be destroyed by inner macro
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); // alone macro without output -> remove indentation
610:                 if (substr($code, -2) !== '?>') {
611:                     $code .= '<?php ?>'; // consume new line
612:                 }
613:             } elseif (substr($code, -2) === '?>') {
614:                 $code .= "\n"; // double newline to avoid newline eating by PHP
615:             }
616:         }
617:         $this->output .= $code;
618:     }
619: 
620: 
621:     /**
622:      * Generates code for macro <tag n:attr> to the output.
623:      * @param  string HTML tag
624:      * @return void
625:      * @internal
626:      */
627:     public function writeAttrsMacro($html)
628:     {
629:         //     none-2 none-1 tag-1 tag-2       <el attr-1 attr-2>   /tag-2 /tag-1 [none-2] [none-1] inner-2 inner-1
630:         // /inner-1 /inner-2 [none-1] [none-2] tag-1 tag-2  </el>   /tag-2 /tag-1 /none-1 /none-2
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]); // don't call closeMacro
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:      * Expands macro and returns node & code.
728:      * @param  string
729:      * @param  string
730:      * @param  string
731:      * @return MacroNode
732:      * @internal
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: 
Nette 2.4-20170829 API API documentation generated by ApiGen 2.8.0