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
      • Traits
    • Reflection
    • Security
    • Tokenizer
    • Utils
  • Tracy
    • Bridges
      • Nette
  • none

Classes

  • Compiler
  • Engine
  • Helpers
  • HtmlNode
  • MacroNode
  • MacroTokens
  • Parser
  • PhpHelpers
  • PhpWriter
  • Token
  • TokenIterator
  • Tokenizer

Interfaces

  • ILoader
  • IMacro

Traits

  • Strict

Exceptions

  • CompileException
  • RegexpException
  • RuntimeException
  • Overview
  • Namespace
  • Class
  • Tree
  • Deprecated
  • Other releases
  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:  * PHP code generator helpers.
 13:  */
 14: class PhpWriter
 15: {
 16:     use Strict;
 17: 
 18:     /** @var MacroTokens */
 19:     private $tokens;
 20: 
 21:     /** @var string */
 22:     private $modifiers;
 23: 
 24:     /** @var array|null */
 25:     private $context;
 26: 
 27: 
 28:     public static function using(MacroNode $node)
 29:     {
 30:         $me = new static($node->tokenizer, null, $node->context);
 31:         $me->modifiers = &$node->modifiers;
 32:         return $me;
 33:     }
 34: 
 35: 
 36:     public function __construct(MacroTokens $tokens, $modifiers = null, array $context = null)
 37:     {
 38:         $this->tokens = $tokens;
 39:         $this->modifiers = $modifiers;
 40:         $this->context = $context;
 41:     }
 42: 
 43: 
 44:     /**
 45:      * Expands %node.word, %node.array, %node.args, %escape(), %modify(), %var, %raw, %word in code.
 46:      * @param  string
 47:      * @return string
 48:      */
 49:     public function write($mask)
 50:     {
 51:         $mask = preg_replace('#%(node|\d+)\.#', '%$1_', $mask);
 52:         $mask = preg_replace_callback('#%escape(\(([^()]*+|(?1))+\))#', function ($m) {
 53:             return $this->escapePass(new MacroTokens(substr($m[1], 1, -1)))->joinAll();
 54:         }, $mask);
 55:         $mask = preg_replace_callback('#%modify(Content)?(\(([^()]*+|(?2))+\))#', function ($m) {
 56:             return $this->formatModifiers(substr($m[2], 1, -1), (bool) $m[1]);
 57:         }, $mask);
 58: 
 59:         $args = func_get_args();
 60:         $pos = $this->tokens->position;
 61:         $word = strpos($mask, '%node_word') === false ? null : $this->tokens->fetchWord();
 62: 
 63:         $code = preg_replace_callback('#([,+]\s*)?%(node_|\d+_|)(word|var|raw|array|args)(\?)?(\s*\+\s*)?()#',
 64:         function ($m) use ($word, &$args) {
 65:             list(, $l, $source, $format, $cond, $r) = $m;
 66: 
 67:             switch ($source) {
 68:                 case 'node_':
 69:                     $arg = $word; break;
 70:                 case '':
 71:                     $arg = next($args); break;
 72:                 default:
 73:                     $arg = $args[(int) $source + 1]; break;
 74:             }
 75: 
 76:             switch ($format) {
 77:                 case 'word':
 78:                     $code = $this->formatWord($arg); break;
 79:                 case 'args':
 80:                     $code = $this->formatArgs(); break;
 81:                 case 'array':
 82:                     $code = $this->formatArray();
 83:                     $code = $cond && $code === '[]' ? '' : $code; break;
 84:                 case 'var':
 85:                     $code = var_export($arg, true); break;
 86:                 case 'raw':
 87:                     $code = (string) $arg; break;
 88:             }
 89: 
 90:             if ($cond && $code === '') {
 91:                 return $r ? $l : $r;
 92:             } else {
 93:                 return $l . $code . $r;
 94:             }
 95:         }, $mask);
 96: 
 97:         $this->tokens->position = $pos;
 98:         return $code;
 99:     }
100: 
101: 
102:     /**
103:      * Formats modifiers calling.
104:      * @param  string
105:      * @return string
106:      */
107:     public function formatModifiers($var, $isContent = false)
108:     {
109:         $tokens = new MacroTokens(ltrim($this->modifiers, '|'));
110:         $tokens = $this->preprocess($tokens);
111:         $tokens = $this->modifierPass($tokens, $var, $isContent);
112:         $tokens = $this->quotingPass($tokens);
113:         return $tokens->joinAll();
114:     }
115: 
116: 
117:     /**
118:      * Formats macro arguments to PHP code. (It advances tokenizer to the end as a side effect.)
119:      * @return string
120:      */
121:     public function formatArgs(MacroTokens $tokens = null)
122:     {
123:         $tokens = $this->preprocess($tokens);
124:         $tokens = $this->quotingPass($tokens);
125:         return $tokens->joinAll();
126:     }
127: 
128: 
129:     /**
130:      * Formats macro arguments to PHP array. (It advances tokenizer to the end as a side effect.)
131:      * @return string
132:      */
133:     public function formatArray(MacroTokens $tokens = null)
134:     {
135:         $tokens = $this->preprocess($tokens);
136:         $tokens = $this->expandCastPass($tokens);
137:         $tokens = $this->quotingPass($tokens);
138:         return $tokens->joinAll();
139:     }
140: 
141: 
142:     /**
143:      * Formats parameter to PHP string.
144:      * @param  string
145:      * @return string
146:      */
147:     public function formatWord($s)
148:     {
149:         return (is_numeric($s) || preg_match('#^\$|[\'"]|^(true|TRUE)\z|^(false|FALSE)\z|^(null|NULL)\z|^[\w\\\\]{3,}::[A-Z0-9_]{2,}\z#', $s))
150:             ? $this->formatArgs(new MacroTokens($s))
151:             : '"' . $s . '"';
152:     }
153: 
154: 
155:     /**
156:      * Preprocessor for tokens. (It advances tokenizer to the end as a side effect.)
157:      * @return MacroTokens
158:      */
159:     public function preprocess(MacroTokens $tokens = null)
160:     {
161:         $tokens = $tokens === null ? $this->tokens : $tokens;
162:         $this->validateTokens($tokens);
163:         $tokens = $this->removeCommentsPass($tokens);
164:         $tokens = $this->shortTernaryPass($tokens);
165:         $tokens = $this->inlineModifierPass($tokens);
166:         $tokens = $this->inOperatorPass($tokens);
167:         return $tokens;
168:     }
169: 
170: 
171:     /**
172:      * @throws CompileException
173:      * @return void
174:      */
175:     public function validateTokens(MacroTokens $tokens)
176:     {
177:         $deprecatedVars = array_flip(['$template', '$_b', '$_l', '$_g', '$_args', '$_fi', '$_control', '$_presenter', '$_form', '$_input', '$_label', '$_snippetMode']);
178:         $brackets = [];
179:         $pos = $tokens->position;
180:         while ($tokens->nextToken()) {
181:             if ($tokens->isCurrent('?>')) {
182:                 throw new CompileException('Forbidden ?> inside macro');
183: 
184:             } elseif ($tokens->isCurrent($tokens::T_VARIABLE) && isset($deprecatedVars[$tokens->currentValue()])) {
185:                 trigger_error("Variable {$tokens->currentValue()} is deprecated.", E_USER_DEPRECATED);
186: 
187:             } elseif ($tokens->isCurrent($tokens::T_SYMBOL)
188:                 && !$tokens->isPrev('::') && !$tokens->isNext('::') && !$tokens->isPrev('->') && !$tokens->isNext('\\')
189:                 && preg_match('#^[A-Z0-9]{3,}$#', $val = $tokens->currentValue())
190:             ) {
191:                 trigger_error("Replace literal $val with constant('$val')", E_USER_DEPRECATED);
192: 
193:             } elseif ($tokens->isCurrent('(', '[', '{')) {
194:                 static $counterpart = ['(' => ')', '[' => ']', '{' => '}'];
195:                 $brackets[] = $counterpart[$tokens->currentValue()];
196: 
197:             } elseif ($tokens->isCurrent(')', ']', '}') && $tokens->currentValue() !== array_pop($brackets)) {
198:                 throw new CompileException('Unexpected ' . $tokens->currentValue());
199: 
200:             } elseif (
201:                 $tokens->isCurrent('function', 'class', 'interface', 'trait')
202:                 && $tokens->isNext($tokens::T_SYMBOL, '&')
203:                 || $tokens->isCurrent('return', 'yield')
204:                 && !$brackets
205:             ) {
206:                 throw new CompileException("Forbidden keyword '{$tokens->currentValue()}' inside macro.");
207:             }
208:         }
209:         if ($brackets) {
210:             throw new CompileException('Missing ' . array_pop($brackets));
211:         }
212:         $tokens->position = $pos;
213:     }
214: 
215: 
216:     /**
217:      * Removes PHP comments.
218:      * @return MacroTokens
219:      */
220:     public function removeCommentsPass(MacroTokens $tokens)
221:     {
222:         $res = new MacroTokens;
223:         while ($tokens->nextToken()) {
224:             if (!$tokens->isCurrent($tokens::T_COMMENT)) {
225:                 $res->append($tokens->currentToken());
226:             }
227:         }
228:         return $res;
229:     }
230: 
231: 
232:     /**
233:      * Simplified ternary expressions without third part.
234:      * @return MacroTokens
235:      */
236:     public function shortTernaryPass(MacroTokens $tokens)
237:     {
238:         $res = new MacroTokens;
239:         $inTernary = [];
240:         while ($tokens->nextToken()) {
241:             if ($tokens->isCurrent('?')) {
242:                 $inTernary[] = $tokens->depth;
243: 
244:             } elseif ($tokens->isCurrent(':')) {
245:                 array_pop($inTernary);
246: 
247:             } elseif ($tokens->isCurrent(',', ')', ']', '|') && end($inTernary) === $tokens->depth + $tokens->isCurrent(')', ']')) {
248:                 $res->append(' : NULL');
249:                 array_pop($inTernary);
250:             }
251:             $res->append($tokens->currentToken());
252:         }
253: 
254:         if ($inTernary) {
255:             $res->append(' : NULL');
256:         }
257:         return $res;
258:     }
259: 
260: 
261:     /**
262:      * Pseudocast (expand).
263:      * @return MacroTokens
264:      */
265:     public function expandCastPass(MacroTokens $tokens)
266:     {
267:         $res = new MacroTokens('[');
268:         $expand = null;
269:         while ($tokens->nextToken()) {
270:             if ($tokens->isCurrent('(expand)') && $tokens->depth === 0) {
271:                 $expand = true;
272:                 $res->append('],');
273:             } elseif ($expand && $tokens->isCurrent(',') && !$tokens->depth) {
274:                 $expand = false;
275:                 $res->append(', [');
276:             } else {
277:                 $res->append($tokens->currentToken());
278:             }
279:         }
280: 
281:         if ($expand === null) {
282:             $res->append(']');
283:         } else {
284:             $res->prepend('array_merge(')->append($expand ? ', [])' : '])');
285:         }
286:         return $res;
287:     }
288: 
289: 
290:     /**
291:      * Quotes symbols to strings.
292:      * @return MacroTokens
293:      */
294:     public function quotingPass(MacroTokens $tokens)
295:     {
296:         $res = new MacroTokens;
297:         while ($tokens->nextToken()) {
298:             $res->append($tokens->isCurrent($tokens::T_SYMBOL)
299:                 && (!$tokens->isPrev() || $tokens->isPrev(',', '(', '[', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', '=', 'and', 'or', 'xor', '??'))
300:                 && (!$tokens->isNext() || $tokens->isNext(',', ';', ')', ']', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', 'and', 'or', 'xor', '??'))
301:                 && !preg_match('#^[A-Z_][A-Z0-9_]{2,}$#', $tokens->currentValue())
302:                 ? "'" . $tokens->currentValue() . "'"
303:                 : $tokens->currentToken()
304:             );
305:         }
306:         return $res;
307:     }
308: 
309: 
310:     /**
311:      * Syntax $entry in [item1, item2].
312:      * @return MacroTokens
313:      */
314:     public function inOperatorPass(MacroTokens $tokens)
315:     {
316:         while ($tokens->nextToken()) {
317:             if ($tokens->isCurrent($tokens::T_VARIABLE)) {
318:                 $start = $tokens->position;
319:                 $depth = $tokens->depth;
320:                 $expr = $arr = [];
321: 
322:                 $expr[] = $tokens->currentToken();
323:                 while ($tokens->isNext($tokens::T_VARIABLE, $tokens::T_SYMBOL, $tokens::T_NUMBER, $tokens::T_STRING, '[', ']', '(', ')', '->')
324:                     && !$tokens->isNext('in')) {
325:                     $expr[] = $tokens->nextToken();
326:                 }
327: 
328:                 if ($depth === $tokens->depth && $tokens->nextValue('in') && ($arr[] = $tokens->nextToken('['))) {
329:                     while ($tokens->isNext()) {
330:                         $arr[] = $tokens->nextToken();
331:                         if ($tokens->isCurrent(']') && $tokens->depth === $depth) {
332:                             $new = array_merge($tokens->parse('in_array('), $expr, $tokens->parse(', '), $arr, $tokens->parse(', TRUE)'));
333:                             array_splice($tokens->tokens, $start, $tokens->position - $start + 1, $new);
334:                             $tokens->position = $start + count($new) - 1;
335:                             continue 2;
336:                         }
337:                     }
338:                 }
339:                 $tokens->position = $start;
340:             }
341:         }
342:         return $tokens->reset();
343:     }
344: 
345: 
346:     /**
347:      * Process inline filters ($var|filter)
348:      * @return MacroTokens
349:      */
350:     public function inlineModifierPass(MacroTokens $tokens)
351:     {
352:         $result = new MacroTokens;
353:         while ($tokens->nextToken()) {
354:             if ($tokens->isCurrent('(', '[')) {
355:                 $result->tokens = array_merge($result->tokens, $this->inlineModifierInner($tokens));
356:             } else {
357:                 $result->append($tokens->currentToken());
358:             }
359:         }
360:         return $result;
361:     }
362: 
363: 
364:     private function inlineModifierInner(MacroTokens $tokens)
365:     {
366:         $isFunctionOrArray = $tokens->isPrev($tokens::T_VARIABLE, $tokens::T_SYMBOL) || $tokens->isCurrent('[');
367:         $result = new MacroTokens;
368:         $args = new MacroTokens;
369:         $modifiers = new MacroTokens;
370:         $current = $args;
371:         $anyModifier = false;
372:         $result->append($tokens->currentToken());
373: 
374:         while ($tokens->nextToken()) {
375:             if ($tokens->isCurrent('(', '[')) {
376:                 $current->tokens = array_merge($current->tokens, $this->inlineModifierInner($tokens));
377: 
378:             } elseif ($current !== $modifiers && $tokens->isCurrent('|')) {
379:                 $anyModifier = true;
380:                 $current = $modifiers;
381: 
382:             } elseif ($tokens->isCurrent(')', ']') || ($isFunctionOrArray && $tokens->isCurrent(','))) {
383:                 $partTokens = count($modifiers->tokens)
384:                     ? $this->modifierPass($modifiers, $args->tokens)->tokens
385:                     : $args->tokens;
386:                 $result->tokens = array_merge($result->tokens, $partTokens);
387:                 if ($tokens->isCurrent(',')) {
388:                     $result->append($tokens->currentToken());
389:                     $args = new MacroTokens;
390:                     $modifiers = new MacroTokens;
391:                     $current = $args;
392:                     continue;
393:                 } elseif ($isFunctionOrArray || !$anyModifier) {
394:                     $result->append($tokens->currentToken());
395:                 } else {
396:                     array_shift($result->tokens);
397:                 }
398:                 return $result->tokens;
399: 
400:             } else {
401:                 $current->append($tokens->currentToken());
402:             }
403:         }
404:         throw new CompileException('Unbalanced brackets.');
405:     }
406: 
407: 
408:     /**
409:      * Formats modifiers calling.
410:      * @param  MacroTokens
411:      * @param  string|array
412:      * @throws CompileException
413:      * @return MacroTokens
414:      */
415:     public function modifierPass(MacroTokens $tokens, $var, $isContent = false)
416:     {
417:         $inside = false;
418:         $res = new MacroTokens($var);
419:         while ($tokens->nextToken()) {
420:             if ($tokens->isCurrent($tokens::T_WHITESPACE)) {
421:                 $res->append(' ');
422: 
423:             } elseif ($inside) {
424:                 if ($tokens->isCurrent(':', ',')) {
425:                     $res->append(', ');
426:                     $tokens->nextAll($tokens::T_WHITESPACE);
427: 
428:                 } elseif ($tokens->isCurrent('|')) {
429:                     $res->append(')');
430:                     $inside = false;
431: 
432:                 } else {
433:                     $res->append($tokens->currentToken());
434:                 }
435:             } else {
436:                 if ($tokens->isCurrent($tokens::T_SYMBOL)) {
437:                     if ($tokens->isCurrent('escape')) {
438:                         if ($isContent) {
439:                             $res->prepend('LR\Filters::convertTo($_fi, ' . var_export(implode($this->context), true) . ', ')
440:                                 ->append(')');
441:                         } else {
442:                             $res = $this->escapePass($res);
443:                         }
444:                         $tokens->nextToken('|');
445:                     } elseif (!strcasecmp($tokens->currentValue(), 'checkurl')) {
446:                         $res->prepend('LR\Filters::safeUrl(');
447:                         $inside = true;
448:                     } else {
449:                         $name = strtolower($tokens->currentValue());
450:                         $res->prepend($isContent
451:                             ? '$this->filters->filterContent(' . var_export($name, true) . ', $_fi, '
452:                             : 'call_user_func($this->filters->' . $name . ', '
453:                         );
454:                         $inside = true;
455:                     }
456:                 } else {
457:                     throw new CompileException("Modifier name must be alphanumeric string, '{$tokens->currentValue()}' given.");
458:                 }
459:             }
460:         }
461:         if ($inside) {
462:             $res->append(')');
463:         }
464:         return $res;
465:     }
466: 
467: 
468:     /**
469:      * Escapes expression in tokens.
470:      * @return MacroTokens
471:      */
472:     public function escapePass(MacroTokens $tokens)
473:     {
474:         $tokens = clone $tokens;
475:         list($contentType, $context) = $this->context;
476:         switch ($contentType) {
477:             case Compiler::CONTENT_XHTML:
478:             case Compiler::CONTENT_HTML:
479:                 switch ($context) {
480:                     case Compiler::CONTEXT_HTML_TEXT:
481:                         return $tokens->prepend('LR\Filters::escapeHtmlText(')->append(')');
482:                     case Compiler::CONTEXT_HTML_TAG:
483:                     case Compiler::CONTEXT_HTML_ATTRIBUTE_UNQUOTED_URL:
484:                         return $tokens->prepend('LR\Filters::escapeHtmlAttrUnquoted(')->append(')');
485:                     case Compiler::CONTEXT_HTML_ATTRIBUTE:
486:                     case Compiler::CONTEXT_HTML_ATTRIBUTE_URL:
487:                         return $tokens->prepend('LR\Filters::escapeHtmlAttr(')->append(')');
488:                     case Compiler::CONTEXT_HTML_ATTRIBUTE_JS:
489:                         return $tokens->prepend('LR\Filters::escapeHtmlAttr(LR\Filters::escapeJs(')->append('))');
490:                     case Compiler::CONTEXT_HTML_ATTRIBUTE_CSS:
491:                         return $tokens->prepend('LR\Filters::escapeHtmlAttr(LR\Filters::escapeCss(')->append('))');
492:                     case Compiler::CONTEXT_HTML_COMMENT:
493:                         return $tokens->prepend('LR\Filters::escapeHtmlComment(')->append(')');
494:                     case Compiler::CONTEXT_HTML_BOGUS_COMMENT:
495:                         return $tokens->prepend('LR\Filters::escapeHtml(')->append(')');
496:                     case Compiler::CONTEXT_HTML_JS:
497:                     case Compiler::CONTEXT_HTML_CSS:
498:                         return $tokens->prepend('LR\Filters::escape' . ucfirst($context) . '(')->append(')');
499:                     default:
500:                         throw new CompileException("Unknown context $contentType, $context.");
501:                 }
502:                 // break omitted
503:             case Compiler::CONTENT_XML:
504:                 switch ($context) {
505:                     case Compiler::CONTEXT_XML_TEXT:
506:                     case Compiler::CONTEXT_XML_ATTRIBUTE:
507:                     case Compiler::CONTEXT_XML_BOGUS_COMMENT:
508:                         return $tokens->prepend('LR\Filters::escapeXml(')->append(')');
509:                     case Compiler::CONTEXT_XML_COMMENT:
510:                         return $tokens->prepend('LR\Filters::escapeHtmlComment(')->append(')');
511:                     case Compiler::CONTEXT_XML_TAG:
512:                         return $tokens->prepend('LR\Filters::escapeXmlAttrUnquoted(')->append(')');
513:                     default:
514:                         throw new CompileException("Unknown context $contentType, $context.");
515:                 }
516:                 // break omitted
517:             case Compiler::CONTENT_JS:
518:             case Compiler::CONTENT_CSS:
519:             case Compiler::CONTENT_ICAL:
520:                 return $tokens->prepend('LR\Filters::escape' . ucfirst($contentType) . '(')->append(')');
521:             case Compiler::CONTENT_TEXT:
522:                 return $tokens;
523:             case null:
524:                 return $tokens->prepend('call_user_func($this->filters->escape, ')->append(')');
525:             default:
526:                 throw new CompileException("Unknown context $contentType.");
527:         }
528:     }
529: }
530: 
Nette 2.4-20180918 API API documentation generated by ApiGen 2.8.0