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