1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Latte;
9:
10:
11: 12: 13:
14: class PhpWriter
15: {
16: use Strict;
17:
18:
19: private $tokens;
20:
21:
22: private $modifiers;
23:
24:
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: 46: 47: 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: 104: 105: 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: 119: 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: 131: 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: 144: 145: 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: 157: 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: 173: 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 ($tokens->isCurrent('function', 'class', 'interface', 'trait') && $tokens->isNext($tokens::T_SYMBOL, '&')
201: || $tokens->isCurrent('return', 'yield') && !$brackets
202: ) {
203: throw new CompileException("Forbidden keyword '{$tokens->currentValue()}' inside macro.");
204: }
205: }
206: if ($brackets) {
207: throw new CompileException('Missing ' . array_pop($brackets));
208: }
209: $tokens->position = $pos;
210: }
211:
212:
213: 214: 215: 216:
217: public function (MacroTokens $tokens)
218: {
219: $res = new MacroTokens;
220: while ($tokens->nextToken()) {
221: if (!$tokens->isCurrent($tokens::T_COMMENT)) {
222: $res->append($tokens->currentToken());
223: }
224: }
225: return $res;
226: }
227:
228:
229: 230: 231: 232:
233: public function shortTernaryPass(MacroTokens $tokens)
234: {
235: $res = new MacroTokens;
236: $inTernary = [];
237: while ($tokens->nextToken()) {
238: if ($tokens->isCurrent('?')) {
239: $inTernary[] = $tokens->depth;
240:
241: } elseif ($tokens->isCurrent(':')) {
242: array_pop($inTernary);
243:
244: } elseif ($tokens->isCurrent(',', ')', ']', '|') && end($inTernary) === $tokens->depth + $tokens->isCurrent(')', ']')) {
245: $res->append(' : NULL');
246: array_pop($inTernary);
247: }
248: $res->append($tokens->currentToken());
249: }
250:
251: if ($inTernary) {
252: $res->append(' : NULL');
253: }
254: return $res;
255: }
256:
257:
258: 259: 260: 261:
262: public function expandCastPass(MacroTokens $tokens)
263: {
264: $res = new MacroTokens('[');
265: $expand = null;
266: while ($tokens->nextToken()) {
267: if ($tokens->isCurrent('(expand)') && $tokens->depth === 0) {
268: $expand = true;
269: $res->append('],');
270: } elseif ($expand && $tokens->isCurrent(',') && !$tokens->depth) {
271: $expand = false;
272: $res->append(', [');
273: } else {
274: $res->append($tokens->currentToken());
275: }
276: }
277:
278: if ($expand === null) {
279: $res->append(']');
280: } else {
281: $res->prepend('array_merge(')->append($expand ? ', [])' : '])');
282: }
283: return $res;
284: }
285:
286:
287: 288: 289: 290:
291: public function quotingPass(MacroTokens $tokens)
292: {
293: $res = new MacroTokens;
294: while ($tokens->nextToken()) {
295: $res->append($tokens->isCurrent($tokens::T_SYMBOL)
296: && (!$tokens->isPrev() || $tokens->isPrev(',', '(', '[', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', '=', 'and', 'or', 'xor', '??'))
297: && (!$tokens->isNext() || $tokens->isNext(',', ';', ')', ']', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', 'and', 'or', 'xor', '??'))
298: && !preg_match('#^[A-Z_][A-Z0-9_]{2,}$#', $tokens->currentValue())
299: ? "'" . $tokens->currentValue() . "'"
300: : $tokens->currentToken()
301: );
302: }
303: return $res;
304: }
305:
306:
307: 308: 309: 310:
311: public function inOperatorPass(MacroTokens $tokens)
312: {
313: while ($tokens->nextToken()) {
314: if ($tokens->isCurrent($tokens::T_VARIABLE)) {
315: $start = $tokens->position;
316: $depth = $tokens->depth;
317: $expr = $arr = [];
318:
319: $expr[] = $tokens->currentToken();
320: while ($tokens->isNext($tokens::T_VARIABLE, $tokens::T_SYMBOL, $tokens::T_NUMBER, $tokens::T_STRING, '[', ']', '(', ')', '->')
321: && !$tokens->isNext('in')) {
322: $expr[] = $tokens->nextToken();
323: }
324:
325: if ($depth === $tokens->depth && $tokens->nextValue('in') && ($arr[] = $tokens->nextToken('['))) {
326: while ($tokens->isNext()) {
327: $arr[] = $tokens->nextToken();
328: if ($tokens->isCurrent(']') && $tokens->depth === $depth) {
329: $new = array_merge($tokens->parse('in_array('), $expr, $tokens->parse(', '), $arr, $tokens->parse(', TRUE)'));
330: array_splice($tokens->tokens, $start, $tokens->position - $start + 1, $new);
331: $tokens->position = $start + count($new) - 1;
332: continue 2;
333: }
334: }
335: }
336: $tokens->position = $start;
337: }
338: }
339: return $tokens->reset();
340: }
341:
342:
343: 344: 345: 346:
347: public function inlineModifierPass(MacroTokens $tokens)
348: {
349: $result = new MacroTokens;
350: while ($tokens->nextToken()) {
351: if ($tokens->isCurrent('(', '[')) {
352: $result->tokens = array_merge($result->tokens, $this->inlineModifierInner($tokens));
353: } else {
354: $result->append($tokens->currentToken());
355: }
356: }
357: return $result;
358: }
359:
360:
361: private function inlineModifierInner(MacroTokens $tokens)
362: {
363: $isFunctionOrArray = $tokens->isPrev($tokens::T_VARIABLE, $tokens::T_SYMBOL) || $tokens->isCurrent('[');
364: $result = new MacroTokens;
365: $args = new MacroTokens;
366: $modifiers = new MacroTokens;
367: $current = $args;
368: $anyModifier = false;
369: $result->append($tokens->currentToken());
370:
371: while ($tokens->nextToken()) {
372: if ($tokens->isCurrent('(', '[')) {
373: $current->tokens = array_merge($current->tokens, $this->inlineModifierInner($tokens));
374:
375: } elseif ($current !== $modifiers && $tokens->isCurrent('|')) {
376: $anyModifier = true;
377: $current = $modifiers;
378:
379: } elseif ($tokens->isCurrent(')', ']') || ($isFunctionOrArray && $tokens->isCurrent(','))) {
380: $partTokens = count($modifiers->tokens)
381: ? $this->modifierPass($modifiers, $args->tokens)->tokens
382: : $args->tokens;
383: $result->tokens = array_merge($result->tokens, $partTokens);
384: if ($tokens->isCurrent(',')) {
385: $result->append($tokens->currentToken());
386: $args = new MacroTokens;
387: $modifiers = new MacroTokens;
388: $current = $args;
389: continue;
390: } elseif ($isFunctionOrArray || !$anyModifier) {
391: $result->append($tokens->currentToken());
392: } else {
393: array_shift($result->tokens);
394: }
395: return $result->tokens;
396:
397: } else {
398: $current->append($tokens->currentToken());
399: }
400: }
401: throw new CompileException('Unbalanced brackets.');
402: }
403:
404:
405: 406: 407: 408: 409: 410: 411:
412: public function modifierPass(MacroTokens $tokens, $var, $isContent = false)
413: {
414: $inside = false;
415: $res = new MacroTokens($var);
416: while ($tokens->nextToken()) {
417: if ($tokens->isCurrent($tokens::T_WHITESPACE)) {
418: $res->append(' ');
419:
420: } elseif ($inside) {
421: if ($tokens->isCurrent(':', ',')) {
422: $res->append(', ');
423: $tokens->nextAll($tokens::T_WHITESPACE);
424:
425: } elseif ($tokens->isCurrent('|')) {
426: $res->append(')');
427: $inside = false;
428:
429: } else {
430: $res->append($tokens->currentToken());
431: }
432: } else {
433: if ($tokens->isCurrent($tokens::T_SYMBOL)) {
434: if ($tokens->isCurrent('escape')) {
435: if ($isContent) {
436: $res->prepend('LR\Filters::convertTo($_fi, ' . var_export(implode($this->context), true) . ', ')
437: ->append(')');
438: } else {
439: $res = $this->escapePass($res);
440: }
441: $tokens->nextToken('|');
442: } elseif (!strcasecmp($tokens->currentValue(), 'checkurl')) {
443: $res->prepend('LR\Filters::safeUrl(');
444: $inside = true;
445: } else {
446: $name = strtolower($tokens->currentValue());
447: $res->prepend($isContent
448: ? '$this->filters->filterContent(' . var_export($name, true) . ', $_fi, '
449: : 'call_user_func($this->filters->' . $name . ', '
450: );
451: $inside = true;
452: }
453: } else {
454: throw new CompileException("Modifier name must be alphanumeric string, '{$tokens->currentValue()}' given.");
455: }
456: }
457: }
458: if ($inside) {
459: $res->append(')');
460: }
461: return $res;
462: }
463:
464:
465: 466: 467: 468:
469: public function escapePass(MacroTokens $tokens)
470: {
471: $tokens = clone $tokens;
472: list($contentType, $context) = $this->context;
473: switch ($contentType) {
474: case Compiler::CONTENT_XHTML:
475: case Compiler::CONTENT_HTML:
476: switch ($context) {
477: case Compiler::CONTEXT_HTML_TEXT:
478: return $tokens->prepend('LR\Filters::escapeHtmlText(')->append(')');
479: case Compiler::CONTEXT_HTML_TAG:
480: case Compiler::CONTEXT_HTML_ATTRIBUTE_UNQUOTED_URL:
481: return $tokens->prepend('LR\Filters::escapeHtmlAttrUnquoted(')->append(')');
482: case Compiler::CONTEXT_HTML_ATTRIBUTE:
483: case Compiler::CONTEXT_HTML_ATTRIBUTE_URL:
484: return $tokens->prepend('LR\Filters::escapeHtmlAttr(')->append(')');
485: case Compiler::CONTEXT_HTML_ATTRIBUTE_JS:
486: return $tokens->prepend('LR\Filters::escapeHtmlAttr(LR\Filters::escapeJs(')->append('))');
487: case Compiler::CONTEXT_HTML_ATTRIBUTE_CSS:
488: return $tokens->prepend('LR\Filters::escapeHtmlAttr(LR\Filters::escapeCss(')->append('))');
489: case Compiler::CONTEXT_HTML_COMMENT:
490: return $tokens->prepend('LR\Filters::escapeHtmlComment(')->append(')');
491: case Compiler::CONTEXT_HTML_BOGUS_COMMENT:
492: return $tokens->prepend('LR\Filters::escapeHtml(')->append(')');
493: case Compiler::CONTEXT_HTML_JS:
494: case Compiler::CONTEXT_HTML_CSS:
495: return $tokens->prepend('LR\Filters::escape' . ucfirst($context) . '(')->append(')');
496: default:
497: throw new CompileException("Unknown context $contentType, $context.");
498: }
499:
500: case Compiler::CONTENT_XML:
501: switch ($context) {
502: case Compiler::CONTEXT_XML_TEXT:
503: case Compiler::CONTEXT_XML_ATTRIBUTE:
504: case Compiler::CONTEXT_XML_BOGUS_COMMENT:
505: return $tokens->prepend('LR\Filters::escapeXml(')->append(')');
506: case Compiler::CONTEXT_XML_COMMENT:
507: return $tokens->prepend('LR\Filters::escapeHtmlComment(')->append(')');
508: case Compiler::CONTEXT_XML_TAG:
509: return $tokens->prepend('LR\Filters::escapeXmlAttrUnquoted(')->append(')');
510: default:
511: throw new CompileException("Unknown context $contentType, $context.");
512: }
513:
514: case Compiler::CONTENT_JS:
515: case Compiler::CONTENT_CSS:
516: case Compiler::CONTENT_ICAL:
517: return $tokens->prepend('LR\Filters::escape' . ucfirst($contentType) . '(')->append(')');
518: case Compiler::CONTENT_TEXT:
519: return $tokens;
520: case null:
521: return $tokens->prepend('call_user_func($this->filters->escape, ')->append(')');
522: default:
523: throw new CompileException("Unknown context $contentType.");
524: }
525: }
526: }
527: