1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Latte;
9:
10:
11: 12: 13:
14: class PhpWriter extends Object
15: {
16:
17: private $tokens;
18:
19:
20: private $modifiers;
21:
22:
23: private $compiler;
24:
25:
26: public static function using(MacroNode $node, Compiler $compiler = NULL)
27: {
28: return new static($node->tokenizer, $node->modifiers, $compiler);
29: }
30:
31:
32: public function __construct(MacroTokens $tokens, $modifiers = NULL, Compiler $compiler = NULL)
33: {
34: $this->tokens = $tokens;
35: $this->modifiers = $modifiers;
36: $this->compiler = $compiler;
37: }
38:
39:
40: 41: 42: 43: 44:
45: public function write($mask)
46: {
47: $mask = preg_replace('#%(node|\d+)\.#', '%$1_', $mask);
48: $me = $this;
49: $mask = preg_replace_callback('#%escape(\(([^()]*+|(?1))+\))#', function ($m) use ($me) {
50: return $me->escapeFilter(new MacroTokens(substr($m[1], 1, -1)))->joinAll();
51: }, $mask);
52: $mask = preg_replace_callback('#%modify(\(([^()]*+|(?1))+\))#', function ($m) use ($me) {
53: return $me->formatModifiers(substr($m[1], 1, -1));
54: }, $mask);
55:
56: $args = func_get_args();
57: $pos = $this->tokens->position;
58: $word = strpos($mask, '%node_word') === FALSE ? NULL : $this->tokens->fetchWord();
59:
60: $code = preg_replace_callback('#([,+]\s*)?%(node_|\d+_|)(word|var|raw|array|args)(\?)?(\s*\+\s*)?()#',
61: function ($m) use ($me, $word, & $args) {
62: list(, $l, $source, $format, $cond, $r) = $m;
63:
64: switch ($source) {
65: case 'node_':
66: $arg = $word; break;
67: case '':
68: $arg = next($args); break;
69: default:
70: $arg = $args[(int) $source + 1]; break;
71: }
72:
73: switch ($format) {
74: case 'word':
75: $code = $me->formatWord($arg); break;
76: case 'args':
77: $code = $me->formatArgs(); break;
78: case 'array':
79: $code = $me->formatArray();
80: $code = $cond && $code === 'array()' ? '' : $code; break;
81: case 'var':
82: $code = var_export($arg, TRUE); break;
83: case 'raw':
84: $code = (string) $arg; break;
85: }
86:
87: if ($cond && $code === '') {
88: return $r ? $l : $r;
89: } else {
90: return $l . $code . $r;
91: }
92: }, $mask);
93:
94: $this->tokens->position = $pos;
95: return $code;
96: }
97:
98:
99: 100: 101: 102: 103:
104: public function formatModifiers($var)
105: {
106: $tokens = new MacroTokens(ltrim($this->modifiers, '|'));
107: $tokens = $this->preprocess($tokens);
108: $tokens = $this->modifiersFilter($tokens, $var);
109: $tokens = $this->quoteFilter($tokens);
110: return $tokens->joinAll();
111: }
112:
113:
114: 115: 116: 117:
118: public function formatArgs(MacroTokens $tokens = NULL)
119: {
120: $tokens = $this->preprocess($tokens);
121: $tokens = $this->quoteFilter($tokens);
122: return $tokens->joinAll();
123: }
124:
125:
126: 127: 128: 129:
130: public function formatArray(MacroTokens $tokens = NULL)
131: {
132: $tokens = $this->preprocess($tokens);
133: $tokens = $this->expandFilter($tokens);
134: $tokens = $this->quoteFilter($tokens);
135: return $tokens->joinAll();
136: }
137:
138:
139: 140: 141: 142: 143:
144: public function formatWord($s)
145: {
146: return (is_numeric($s) || preg_match('#^\$|[\'"]|^true\z|^false\z|^null\z#i', $s))
147: ? $this->formatArgs(new MacroTokens($s))
148: : '"' . $s . '"';
149: }
150:
151:
152: 153: 154: 155:
156: public function preprocess(MacroTokens $tokens = NULL)
157: {
158: $tokens = $tokens === NULL ? $this->tokens : $tokens;
159: $tokens = $this->removeCommentsFilter($tokens);
160: $tokens = $this->shortTernaryFilter($tokens);
161: $tokens = $this->shortArraysFilter($tokens);
162: return $tokens;
163: }
164:
165:
166: 167: 168: 169:
170: public function (MacroTokens $tokens)
171: {
172: $res = new MacroTokens;
173: while ($tokens->nextToken()) {
174: if (!$tokens->isCurrent(MacroTokens::T_COMMENT)) {
175: $res->append($tokens->currentToken());
176: }
177: }
178: return $res;
179: }
180:
181:
182: 183: 184: 185:
186: public function shortTernaryFilter(MacroTokens $tokens)
187: {
188: $res = new MacroTokens;
189: $inTernary = array();
190: while ($tokens->nextToken()) {
191: if ($tokens->isCurrent('?')) {
192: $inTernary[] = $tokens->depth;
193:
194: } elseif ($tokens->isCurrent(':')) {
195: array_pop($inTernary);
196:
197: } elseif ($tokens->isCurrent(',', ')', ']') && end($inTernary) === $tokens->depth + !$tokens->isCurrent(',')) {
198: $res->append(' : NULL');
199: array_pop($inTernary);
200: }
201: $res->append($tokens->currentToken());
202: }
203:
204: if ($inTernary) {
205: $res->append(' : NULL');
206: }
207: return $res;
208: }
209:
210:
211: 212: 213: 214:
215: public function shortArraysFilter(MacroTokens $tokens)
216: {
217: $res = new MacroTokens;
218: $arrays = array();
219: while ($tokens->nextToken()) {
220: if ($tokens->isCurrent('[')) {
221: if ($arrays[] = !$tokens->isPrev(']', ')', MacroTokens::T_SYMBOL, MacroTokens::T_VARIABLE, MacroTokens::T_KEYWORD)) {
222: $res->append('array(');
223: continue;
224:
225: }
226: } elseif ($tokens->isCurrent(']')) {
227: if (array_pop($arrays) === TRUE) {
228: $res->append(')');
229: continue;
230: }
231: }
232: $res->append($tokens->currentToken());
233: }
234: return $res;
235: }
236:
237:
238: 239: 240: 241:
242: public function expandFilter(MacroTokens $tokens)
243: {
244: $res = new MacroTokens('array(');
245: $expand = NULL;
246: while ($tokens->nextToken()) {
247: if ($tokens->isCurrent('(expand)') && $tokens->depth === 0) {
248: $expand = TRUE;
249: $res->append('),');
250: } elseif ($expand && $tokens->isCurrent(',') && !$tokens->depth) {
251: $expand = FALSE;
252: $res->append(', array(');
253: } else {
254: $res->append($tokens->currentToken());
255: }
256: }
257:
258: if ($expand !== NULL) {
259: $res->prepend('array_merge(')->append($expand ? ', array()' : ')');
260: }
261: return $res->append(')');
262: }
263:
264:
265: 266: 267: 268:
269: public function quoteFilter(MacroTokens $tokens)
270: {
271: $res = new MacroTokens;
272: while ($tokens->nextToken()) {
273: $res->append($tokens->isCurrent(MacroTokens::T_SYMBOL)
274: && (!$tokens->isPrev() || $tokens->isPrev(',', '(', '[', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', '=', 'and', 'or', 'xor', '??'))
275: && (!$tokens->isNext() || $tokens->isNext(',', ';', ')', ']', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', 'and', 'or', 'xor', '??'))
276: ? "'" . $tokens->currentValue() . "'"
277: : $tokens->currentToken()
278: );
279: }
280: return $res;
281: }
282:
283:
284: 285: 286: 287: 288: 289: 290:
291: public function modifiersFilter(MacroTokens $tokens, $var)
292: {
293: $inside = FALSE;
294: $res = new MacroTokens($var);
295: while ($tokens->nextToken()) {
296: if ($tokens->isCurrent(MacroTokens::T_WHITESPACE)) {
297: $res->append(' ');
298:
299: } elseif ($inside) {
300: if ($tokens->isCurrent(':', ',')) {
301: $res->append(', ');
302: $tokens->nextAll(MacroTokens::T_WHITESPACE);
303:
304: } elseif ($tokens->isCurrent('|')) {
305: $res->append(')');
306: $inside = FALSE;
307:
308: } else {
309: $res->append($tokens->currentToken());
310: }
311: } else {
312: if ($tokens->isCurrent(MacroTokens::T_SYMBOL)) {
313: if ($this->compiler && $tokens->isCurrent('escape')) {
314: $res = $this->escapeFilter($res);
315: $tokens->nextToken('|');
316: } elseif (!strcasecmp($tokens->currentValue(), 'safeurl')) {
317: $res->prepend('Latte\Runtime\Filters::safeUrl(');
318: $inside = TRUE;
319: } else {
320: $res->prepend('$template->' . $tokens->currentValue() . '(');
321: $inside = TRUE;
322: }
323: } else {
324: throw new CompileException("Modifier name must be alphanumeric string, '{$tokens->currentValue()}' given.");
325: }
326: }
327: }
328: if ($inside) {
329: $res->append(')');
330: }
331: return $res;
332: }
333:
334:
335: 336: 337: 338:
339: public function escapeFilter(MacroTokens $tokens)
340: {
341: $tokens = clone $tokens;
342: switch ($this->compiler->getContentType()) {
343: case Compiler::CONTENT_XHTML:
344: case Compiler::CONTENT_HTML:
345: $context = $this->compiler->getContext();
346: switch ($context[0]) {
347: case Compiler::CONTEXT_SINGLE_QUOTED_ATTR:
348: case Compiler::CONTEXT_DOUBLE_QUOTED_ATTR:
349: case Compiler::CONTEXT_UNQUOTED_ATTR:
350: if ($context[1] === Compiler::CONTENT_JS) {
351: $tokens->prepend('Latte\Runtime\Filters::escapeJs(')->append(')');
352: } elseif ($context[1] === Compiler::CONTENT_CSS) {
353: $tokens->prepend('Latte\Runtime\Filters::escapeCss(')->append(')');
354: }
355: $tokens->prepend('Latte\Runtime\Filters::escapeHtml(')->append($context[0] === Compiler::CONTEXT_SINGLE_QUOTED_ATTR ? ', ENT_QUOTES)' : ', ENT_COMPAT)');
356: if ($context[0] === Compiler::CONTEXT_UNQUOTED_ATTR) {
357: $tokens->prepend("'\"', ")->append(", '\"'");
358: }
359: return $tokens;
360: case Compiler::CONTEXT_COMMENT:
361: return $tokens->prepend('Latte\Runtime\Filters::escapeHtmlComment(')->append(')');
362: case Compiler::CONTENT_JS:
363: case Compiler::CONTENT_CSS:
364: return $tokens->prepend('Latte\Runtime\Filters::escape' . ucfirst($context[0]) . '(')->append(')');
365: default:
366: return $tokens->prepend('Latte\Runtime\Filters::escapeHtml(')->append(', ENT_NOQUOTES)');
367: }
368:
369: case Compiler::CONTENT_XML:
370: $context = $this->compiler->getContext();
371: switch ($context[0]) {
372: case Compiler::CONTEXT_COMMENT:
373: return $tokens->prepend('Latte\Runtime\Filters::escapeHtmlComment(')->append(')');
374: default:
375: $tokens->prepend('Latte\Runtime\Filters::escapeXml(')->append(')');
376: if ($context[0] === Compiler::CONTEXT_UNQUOTED_ATTR) {
377: $tokens->prepend("'\"', ")->append(", '\"'");
378: }
379: return $tokens;
380: }
381:
382: case Compiler::CONTENT_JS:
383: case Compiler::CONTENT_CSS:
384: case Compiler::CONTENT_ICAL:
385: return $tokens->prepend('Latte\Runtime\Filters::escape' . ucfirst($this->compiler->getContentType()) . '(')->append(')');
386: case Compiler::CONTENT_TEXT:
387: return $tokens;
388: default:
389: return $tokens->prepend('$template->escape(')->append(')');
390: }
391: }
392:
393: }
394: