1<?php
2
3/*
4 * This file is part of Mustache.php.
5 *
6 * (c) 2010-2017 Justin Hileman
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12/**
13 * Mustache Compiler class.
14 *
15 * This class is responsible for turning a Mustache token parse tree into normal PHP source code.
16 */
17class Mustache_Compiler
18{
19    private $pragmas;
20    private $defaultPragmas = array();
21    private $sections;
22    private $blocks;
23    private $source;
24    private $indentNextLine;
25    private $customEscape;
26    private $entityFlags;
27    private $charset;
28    private $strictCallables;
29
30    /**
31     * Compile a Mustache token parse tree into PHP source code.
32     *
33     * @param string $source          Mustache Template source code
34     * @param string $tree            Parse tree of Mustache tokens
35     * @param string $name            Mustache Template class name
36     * @param bool   $customEscape    (default: false)
37     * @param string $charset         (default: 'UTF-8')
38     * @param bool   $strictCallables (default: false)
39     * @param int    $entityFlags     (default: ENT_COMPAT)
40     *
41     * @return string Generated PHP source code
42     */
43    public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT)
44    {
45        $this->pragmas         = $this->defaultPragmas;
46        $this->sections        = array();
47        $this->blocks          = array();
48        $this->source          = $source;
49        $this->indentNextLine  = true;
50        $this->customEscape    = $customEscape;
51        $this->entityFlags     = $entityFlags;
52        $this->charset         = $charset;
53        $this->strictCallables = $strictCallables;
54
55        return $this->writeCode($tree, $name);
56    }
57
58    /**
59     * Enable pragmas across all templates, regardless of the presence of pragma
60     * tags in the individual templates.
61     *
62     * @internal Users should set global pragmas in Mustache_Engine, not here :)
63     *
64     * @param string[] $pragmas
65     */
66    public function setPragmas(array $pragmas)
67    {
68        $this->pragmas = array();
69        foreach ($pragmas as $pragma) {
70            $this->pragmas[$pragma] = true;
71        }
72        $this->defaultPragmas = $this->pragmas;
73    }
74
75    /**
76     * Helper function for walking the Mustache token parse tree.
77     *
78     * @throws Mustache_Exception_SyntaxException upon encountering unknown token types
79     *
80     * @param array $tree  Parse tree of Mustache tokens
81     * @param int   $level (default: 0)
82     *
83     * @return string Generated PHP source code
84     */
85    private function walk(array $tree, $level = 0)
86    {
87        $code = '';
88        $level++;
89        foreach ($tree as $node) {
90            switch ($node[Mustache_Tokenizer::TYPE]) {
91                case Mustache_Tokenizer::T_PRAGMA:
92                    $this->pragmas[$node[Mustache_Tokenizer::NAME]] = true;
93                    break;
94
95                case Mustache_Tokenizer::T_SECTION:
96                    $code .= $this->section(
97                        $node[Mustache_Tokenizer::NODES],
98                        $node[Mustache_Tokenizer::NAME],
99                        isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
100                        $node[Mustache_Tokenizer::INDEX],
101                        $node[Mustache_Tokenizer::END],
102                        $node[Mustache_Tokenizer::OTAG],
103                        $node[Mustache_Tokenizer::CTAG],
104                        $level
105                    );
106                    break;
107
108                case Mustache_Tokenizer::T_INVERTED:
109                    $code .= $this->invertedSection(
110                        $node[Mustache_Tokenizer::NODES],
111                        $node[Mustache_Tokenizer::NAME],
112                        isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
113                        $level
114                    );
115                    break;
116
117                case Mustache_Tokenizer::T_PARTIAL:
118                    $code .= $this->partial(
119                        $node[Mustache_Tokenizer::NAME],
120                        isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
121                        $level
122                    );
123                    break;
124
125                case Mustache_Tokenizer::T_PARENT:
126                    $code .= $this->parent(
127                        $node[Mustache_Tokenizer::NAME],
128                        isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
129                        $node[Mustache_Tokenizer::NODES],
130                        $level
131                    );
132                    break;
133
134                case Mustache_Tokenizer::T_BLOCK_ARG:
135                    $code .= $this->blockArg(
136                        $node[Mustache_Tokenizer::NODES],
137                        $node[Mustache_Tokenizer::NAME],
138                        $node[Mustache_Tokenizer::INDEX],
139                        $node[Mustache_Tokenizer::END],
140                        $node[Mustache_Tokenizer::OTAG],
141                        $node[Mustache_Tokenizer::CTAG],
142                        $level
143                    );
144                    break;
145
146                case Mustache_Tokenizer::T_BLOCK_VAR:
147                    $code .= $this->blockVar(
148                        $node[Mustache_Tokenizer::NODES],
149                        $node[Mustache_Tokenizer::NAME],
150                        $node[Mustache_Tokenizer::INDEX],
151                        $node[Mustache_Tokenizer::END],
152                        $node[Mustache_Tokenizer::OTAG],
153                        $node[Mustache_Tokenizer::CTAG],
154                        $level
155                    );
156                    break;
157
158                case Mustache_Tokenizer::T_COMMENT:
159                    break;
160
161                case Mustache_Tokenizer::T_ESCAPED:
162                case Mustache_Tokenizer::T_UNESCAPED:
163                case Mustache_Tokenizer::T_UNESCAPED_2:
164                    $code .= $this->variable(
165                        $node[Mustache_Tokenizer::NAME],
166                        isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
167                        $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_ESCAPED,
168                        $level
169                    );
170                    break;
171
172                case Mustache_Tokenizer::T_TEXT:
173                    $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level);
174                    break;
175
176                default:
177                    throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node);
178            }
179        }
180
181        return $code;
182    }
183
184    const KLASS = '<?php
185
186        class %s extends Mustache_Template
187        {
188            private $lambdaHelper;%s
189
190            public function renderInternal(Mustache_Context $context, $indent = \'\')
191            {
192                $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context);
193                $buffer = \'\';
194        %s
195
196                return $buffer;
197            }
198        %s
199        %s
200        }';
201
202    const KLASS_NO_LAMBDAS = '<?php
203
204        class %s extends Mustache_Template
205        {%s
206            public function renderInternal(Mustache_Context $context, $indent = \'\')
207            {
208                $buffer = \'\';
209        %s
210
211                return $buffer;
212            }
213        }';
214
215    const STRICT_CALLABLE = 'protected $strictCallables = true;';
216
217    /**
218     * Generate Mustache Template class PHP source.
219     *
220     * @param array  $tree Parse tree of Mustache tokens
221     * @param string $name Mustache Template class name
222     *
223     * @return string Generated PHP source code
224     */
225    private function writeCode($tree, $name)
226    {
227        $code     = $this->walk($tree);
228        $sections = implode("\n", $this->sections);
229        $blocks   = implode("\n", $this->blocks);
230        $klass    = empty($this->sections) && empty($this->blocks) ? self::KLASS_NO_LAMBDAS : self::KLASS;
231
232        $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : '';
233
234        return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections, $blocks);
235    }
236
237    const BLOCK_VAR = '
238        $blockFunction = $context->findInBlock(%s);
239        if (is_callable($blockFunction)) {
240            $buffer .= call_user_func($blockFunction, $context);
241        %s}
242    ';
243
244    const BLOCK_VAR_ELSE = '} else {%s';
245
246    /**
247     * Generate Mustache Template inheritance block variable PHP source.
248     *
249     * @param array  $nodes Array of child tokens
250     * @param string $id    Section name
251     * @param int    $start Section start offset
252     * @param int    $end   Section end offset
253     * @param string $otag  Current Mustache opening tag
254     * @param string $ctag  Current Mustache closing tag
255     * @param int    $level
256     *
257     * @return string Generated PHP source code
258     */
259    private function blockVar($nodes, $id, $start, $end, $otag, $ctag, $level)
260    {
261        $id = var_export($id, true);
262
263        $else = $this->walk($nodes, $level);
264        if ($else !== '') {
265            $else = sprintf($this->prepare(self::BLOCK_VAR_ELSE, $level + 1, false, true), $else);
266        }
267
268        return sprintf($this->prepare(self::BLOCK_VAR, $level), $id, $else);
269    }
270
271    const BLOCK_ARG = '%s => array($this, \'block%s\'),';
272
273    /**
274     * Generate Mustache Template inheritance block argument PHP source.
275     *
276     * @param array  $nodes Array of child tokens
277     * @param string $id    Section name
278     * @param int    $start Section start offset
279     * @param int    $end   Section end offset
280     * @param string $otag  Current Mustache opening tag
281     * @param string $ctag  Current Mustache closing tag
282     * @param int    $level
283     *
284     * @return string Generated PHP source code
285     */
286    private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
287    {
288        $key = $this->block($nodes);
289        $keystr = var_export($key, true);
290        $id = var_export($id, true);
291
292        return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key);
293    }
294
295    const BLOCK_FUNCTION = '
296        public function block%s($context)
297        {
298            $indent = $buffer = \'\';%s
299
300            return $buffer;
301        }
302    ';
303
304    /**
305     * Generate Mustache Template inheritance block function PHP source.
306     *
307     * @param array $nodes Array of child tokens
308     *
309     * @return string key of new block function
310     */
311    private function block($nodes)
312    {
313        $code = $this->walk($nodes, 0);
314        $key = ucfirst(md5($code));
315
316        if (!isset($this->blocks[$key])) {
317            $this->blocks[$key] = sprintf($this->prepare(self::BLOCK_FUNCTION, 0), $key, $code);
318        }
319
320        return $key;
321    }
322
323    const SECTION_CALL = '
324        // %s section
325        $value = $context->%s(%s);%s
326        $buffer .= $this->section%s($context, $indent, $value);
327    ';
328
329    const SECTION = '
330        private function section%s(Mustache_Context $context, $indent, $value)
331        {
332            $buffer = \'\';
333
334            if (%s) {
335                $source = %s;
336                $result = call_user_func($value, $source, %s);
337                if (strpos($result, \'{{\') === false) {
338                    $buffer .= $result;
339                } else {
340                    $buffer .= $this->mustache
341                        ->loadLambda((string) $result%s)
342                        ->renderInternal($context);
343                }
344            } elseif (!empty($value)) {
345                $values = $this->isIterable($value) ? $value : array($value);
346                foreach ($values as $value) {
347                    $context->push($value);
348                    %s
349                    $context->pop();
350                }
351            }
352
353            return $buffer;
354        }
355    ';
356
357    /**
358     * Generate Mustache Template section PHP source.
359     *
360     * @param array    $nodes   Array of child tokens
361     * @param string   $id      Section name
362     * @param string[] $filters Array of filters
363     * @param int      $start   Section start offset
364     * @param int      $end     Section end offset
365     * @param string   $otag    Current Mustache opening tag
366     * @param string   $ctag    Current Mustache closing tag
367     * @param int      $level
368     *
369     * @return string Generated section PHP source code
370     */
371    private function section($nodes, $id, $filters, $start, $end, $otag, $ctag, $level)
372    {
373        $source   = var_export(substr($this->source, $start, $end - $start), true);
374        $callable = $this->getCallable();
375
376        if ($otag !== '{{' || $ctag !== '}}') {
377            $delimTag = var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
378            $helper = sprintf('$this->lambdaHelper->withDelimiters(%s)', $delimTag);
379            $delims = ', ' . $delimTag;
380        } else {
381            $helper = '$this->lambdaHelper';
382            $delims = '';
383        }
384
385        $key = ucfirst(md5($delims . "\n" . $source));
386
387        if (!isset($this->sections[$key])) {
388            $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $delims, $this->walk($nodes, 2));
389        }
390
391        $method  = $this->getFindMethod($id);
392        $id      = var_export($id, true);
393        $filters = $this->getFilters($filters, $level);
394
395        return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $method, $id, $filters, $key);
396    }
397
398    const INVERTED_SECTION = '
399        // %s inverted section
400        $value = $context->%s(%s);%s
401        if (empty($value)) {
402            %s
403        }
404    ';
405
406    /**
407     * Generate Mustache Template inverted section PHP source.
408     *
409     * @param array    $nodes   Array of child tokens
410     * @param string   $id      Section name
411     * @param string[] $filters Array of filters
412     * @param int      $level
413     *
414     * @return string Generated inverted section PHP source code
415     */
416    private function invertedSection($nodes, $id, $filters, $level)
417    {
418        $method  = $this->getFindMethod($id);
419        $id      = var_export($id, true);
420        $filters = $this->getFilters($filters, $level);
421
422        return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $filters, $this->walk($nodes, $level));
423    }
424
425    const PARTIAL_INDENT = ', $indent . %s';
426    const PARTIAL = '
427        if ($partial = $this->mustache->loadPartial(%s)) {
428            $buffer .= $partial->renderInternal($context%s);
429        }
430    ';
431
432    /**
433     * Generate Mustache Template partial call PHP source.
434     *
435     * @param string $id     Partial name
436     * @param string $indent Whitespace indent to apply to partial
437     * @param int    $level
438     *
439     * @return string Generated partial call PHP source code
440     */
441    private function partial($id, $indent, $level)
442    {
443        if ($indent !== '') {
444            $indentParam = sprintf(self::PARTIAL_INDENT, var_export($indent, true));
445        } else {
446            $indentParam = '';
447        }
448
449        return sprintf(
450            $this->prepare(self::PARTIAL, $level),
451            var_export($id, true),
452            $indentParam
453        );
454    }
455
456    const PARENT = '
457        if ($parent = $this->mustache->loadPartial(%s)) {
458            $context->pushBlockContext(array(%s
459            ));
460            $buffer .= $parent->renderInternal($context, $indent);
461            $context->popBlockContext();
462        }
463    ';
464
465    const PARENT_NO_CONTEXT = '
466        if ($parent = $this->mustache->loadPartial(%s)) {
467            $buffer .= $parent->renderInternal($context, $indent);
468        }
469    ';
470
471    /**
472     * Generate Mustache Template inheritance parent call PHP source.
473     *
474     * @param string $id       Parent tag name
475     * @param string $indent   Whitespace indent to apply to parent
476     * @param array  $children Child nodes
477     * @param int    $level
478     *
479     * @return string Generated PHP source code
480     */
481    private function parent($id, $indent, array $children, $level)
482    {
483        $realChildren = array_filter($children, array(__CLASS__, 'onlyBlockArgs'));
484
485        if (empty($realChildren)) {
486            return sprintf($this->prepare(self::PARENT_NO_CONTEXT, $level), var_export($id, true));
487        }
488
489        return sprintf(
490            $this->prepare(self::PARENT, $level),
491            var_export($id, true),
492            $this->walk($realChildren, $level + 1)
493        );
494    }
495
496    /**
497     * Helper method for filtering out non-block-arg tokens.
498     *
499     * @param array $node
500     *
501     * @return bool True if $node is a block arg token
502     */
503    private static function onlyBlockArgs(array $node)
504    {
505        return $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_BLOCK_ARG;
506    }
507
508    const VARIABLE = '
509        $value = $this->resolveValue($context->%s(%s), $context);%s
510        $buffer .= %s%s;
511    ';
512
513    /**
514     * Generate Mustache Template variable interpolation PHP source.
515     *
516     * @param string   $id      Variable name
517     * @param string[] $filters Array of filters
518     * @param bool     $escape  Escape the variable value for output?
519     * @param int      $level
520     *
521     * @return string Generated variable interpolation PHP source
522     */
523    private function variable($id, $filters, $escape, $level)
524    {
525        $method  = $this->getFindMethod($id);
526        $id      = ($method !== 'last') ? var_export($id, true) : '';
527        $filters = $this->getFilters($filters, $level);
528        $value   = $escape ? $this->getEscape() : '$value';
529
530        return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value);
531    }
532
533    const FILTER = '
534        $filter = $context->%s(%s);
535        if (!(%s)) {
536            throw new Mustache_Exception_UnknownFilterException(%s);
537        }
538        $value = call_user_func($filter, $value);%s
539    ';
540
541    /**
542     * Generate Mustache Template variable filtering PHP source.
543     *
544     * @param string[] $filters Array of filters
545     * @param int      $level
546     *
547     * @return string Generated filter PHP source
548     */
549    private function getFilters(array $filters, $level)
550    {
551        if (empty($filters)) {
552            return '';
553        }
554
555        $name     = array_shift($filters);
556        $method   = $this->getFindMethod($name);
557        $filter   = ($method !== 'last') ? var_export($name, true) : '';
558        $callable = $this->getCallable('$filter');
559        $msg      = var_export($name, true);
560
561        return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $callable, $msg, $this->getFilters($filters, $level));
562    }
563
564    const LINE = '$buffer .= "\n";';
565    const TEXT = '$buffer .= %s%s;';
566
567    /**
568     * Generate Mustache Template output Buffer call PHP source.
569     *
570     * @param string $text
571     * @param int    $level
572     *
573     * @return string Generated output Buffer call PHP source
574     */
575    private function text($text, $level)
576    {
577        $indentNextLine = (substr($text, -1) === "\n");
578        $code = sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
579        $this->indentNextLine = $indentNextLine;
580
581        return $code;
582    }
583
584    /**
585     * Prepare PHP source code snippet for output.
586     *
587     * @param string $text
588     * @param int    $bonus          Additional indent level (default: 0)
589     * @param bool   $prependNewline Prepend a newline to the snippet? (default: true)
590     * @param bool   $appendNewline  Append a newline to the snippet? (default: false)
591     *
592     * @return string PHP source code snippet
593     */
594    private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false)
595    {
596        $text = ($prependNewline ? "\n" : '') . trim($text);
597        if ($prependNewline) {
598            $bonus++;
599        }
600        if ($appendNewline) {
601            $text .= "\n";
602        }
603
604        return preg_replace("/\n( {8})?/", "\n" . str_repeat(' ', $bonus * 4), $text);
605    }
606
607    const DEFAULT_ESCAPE = 'htmlspecialchars(%s, %s, %s)';
608    const CUSTOM_ESCAPE  = 'call_user_func($this->mustache->getEscape(), %s)';
609
610    /**
611     * Get the current escaper.
612     *
613     * @param string $value (default: '$value')
614     *
615     * @return string Either a custom callback, or an inline call to `htmlspecialchars`
616     */
617    private function getEscape($value = '$value')
618    {
619        if ($this->customEscape) {
620            return sprintf(self::CUSTOM_ESCAPE, $value);
621        }
622
623        return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->entityFlags, true), var_export($this->charset, true));
624    }
625
626    /**
627     * Select the appropriate Context `find` method for a given $id.
628     *
629     * The return value will be one of `find`, `findDot`, `findAnchoredDot` or `last`.
630     *
631     * @see Mustache_Context::find
632     * @see Mustache_Context::findDot
633     * @see Mustache_Context::last
634     *
635     * @param string $id Variable name
636     *
637     * @return string `find` method name
638     */
639    private function getFindMethod($id)
640    {
641        if ($id === '.') {
642            return 'last';
643        }
644
645        if (isset($this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) && $this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) {
646            if (substr($id, 0, 1) === '.') {
647                return 'findAnchoredDot';
648            }
649        }
650
651        if (strpos($id, '.') === false) {
652            return 'find';
653        }
654
655        return 'findDot';
656    }
657
658    const IS_CALLABLE        = '!is_string(%s) && is_callable(%s)';
659    const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)';
660
661    /**
662     * Helper function to compile strict vs lax "is callable" logic.
663     *
664     * @param string $variable (default: '$value')
665     *
666     * @return string "is callable" logic
667     */
668    private function getCallable($variable = '$value')
669    {
670        $tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE;
671
672        return sprintf($tpl, $variable, $variable);
673    }
674
675    const LINE_INDENT = '$indent . ';
676
677    /**
678     * Get the current $indent prefix to write to the buffer.
679     *
680     * @return string "$indent . " or ""
681     */
682    private function flushIndent()
683    {
684        if (!$this->indentNextLine) {
685            return '';
686        }
687
688        $this->indentNextLine = false;
689
690        return self::LINE_INDENT;
691    }
692}
693