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 (
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: 218: 219:
220: public function (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: 234: 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: 263: 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: 292: 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: 312: 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: 348: 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: 410: 411: 412: 413: 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: 470: 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:
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:
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: