xref: /template/strap/vendor/salesforce/handlebars-php/src/Handlebars/Tokenizer.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
1*04fd306cSNickeau<?php
2*04fd306cSNickeau/**
3*04fd306cSNickeau * Handlebars tokenizer (based on mustache)
4*04fd306cSNickeau *
5*04fd306cSNickeau * @category  Xamin
6*04fd306cSNickeau * @package   Handlebars
7*04fd306cSNickeau * @author    Justin Hileman <dontknow@example.org>
8*04fd306cSNickeau * @author    fzerorubigd <fzerorubigd@gmail.com>
9*04fd306cSNickeau * @author    Behrooz Shabani <everplays@gmail.com>
10*04fd306cSNickeau * @author    Mardix <https://github.com/mardix>
11*04fd306cSNickeau * @copyright 2012 (c) ParsPooyesh Co
12*04fd306cSNickeau * @copyright 2013 (c) Behrooz Shabani
13*04fd306cSNickeau * @copyright 2013 (c) Mardix
14*04fd306cSNickeau * @license   MIT
15*04fd306cSNickeau * @link      http://voodoophp.org/docs/handlebars
16*04fd306cSNickeau */
17*04fd306cSNickeau
18*04fd306cSNickeaunamespace Handlebars;
19*04fd306cSNickeau
20*04fd306cSNickeauclass Tokenizer
21*04fd306cSNickeau{
22*04fd306cSNickeau
23*04fd306cSNickeau    // Finite state machine states
24*04fd306cSNickeau    const IN_TEXT = 0;
25*04fd306cSNickeau    const IN_TAG_TYPE = 1;
26*04fd306cSNickeau    const IN_TAG = 2;
27*04fd306cSNickeau
28*04fd306cSNickeau    // Token types
29*04fd306cSNickeau    const T_SECTION = '#';
30*04fd306cSNickeau    const T_INVERTED = '^';
31*04fd306cSNickeau    const T_END_SECTION = '/';
32*04fd306cSNickeau    const T_COMMENT = '!';
33*04fd306cSNickeau    // XXX: remove partials support from tokenizer and make it a helper?
34*04fd306cSNickeau    const T_PARTIAL = '>';
35*04fd306cSNickeau    const T_PARTIAL_2 = '<';
36*04fd306cSNickeau    const T_DELIM_CHANGE = '=';
37*04fd306cSNickeau    const T_ESCAPED = '_v';
38*04fd306cSNickeau    const T_UNESCAPED = '{';
39*04fd306cSNickeau    const T_UNESCAPED_2 = '&';
40*04fd306cSNickeau    const T_TEXT = '_t';
41*04fd306cSNickeau
42*04fd306cSNickeau    // Valid token types
43*04fd306cSNickeau    private $tagTypes = [
44*04fd306cSNickeau        self::T_SECTION => true,
45*04fd306cSNickeau        self::T_INVERTED => true,
46*04fd306cSNickeau        self::T_END_SECTION => true,
47*04fd306cSNickeau        self::T_COMMENT => true,
48*04fd306cSNickeau        self::T_PARTIAL => true,
49*04fd306cSNickeau        self::T_PARTIAL_2 => true,
50*04fd306cSNickeau        self::T_DELIM_CHANGE => true,
51*04fd306cSNickeau        self::T_ESCAPED => true,
52*04fd306cSNickeau        self::T_UNESCAPED => true,
53*04fd306cSNickeau        self::T_UNESCAPED_2 => true,
54*04fd306cSNickeau    ];
55*04fd306cSNickeau
56*04fd306cSNickeau    // Interpolated tags
57*04fd306cSNickeau    private $interpolatedTags = [
58*04fd306cSNickeau        self::T_ESCAPED => true,
59*04fd306cSNickeau        self::T_UNESCAPED => true,
60*04fd306cSNickeau        self::T_UNESCAPED_2 => true,
61*04fd306cSNickeau    ];
62*04fd306cSNickeau
63*04fd306cSNickeau    // Token properties
64*04fd306cSNickeau    const TYPE = 'type';
65*04fd306cSNickeau    const NAME = 'name';
66*04fd306cSNickeau    const OTAG = 'otag';
67*04fd306cSNickeau    const CTAG = 'ctag';
68*04fd306cSNickeau    const INDEX = 'index';
69*04fd306cSNickeau    const END = 'end';
70*04fd306cSNickeau    const INDENT = 'indent';
71*04fd306cSNickeau    const NODES = 'nodes';
72*04fd306cSNickeau    const VALUE = 'value';
73*04fd306cSNickeau    const ARGS = 'args';
74*04fd306cSNickeau
75*04fd306cSNickeau    protected $state;
76*04fd306cSNickeau    protected $tagType;
77*04fd306cSNickeau    protected $tag;
78*04fd306cSNickeau    protected $buffer;
79*04fd306cSNickeau    protected $tokens;
80*04fd306cSNickeau    protected $seenTag;
81*04fd306cSNickeau    protected $lineStart;
82*04fd306cSNickeau    protected $otag;
83*04fd306cSNickeau    protected $ctag;
84*04fd306cSNickeau
85*04fd306cSNickeau    /**
86*04fd306cSNickeau     * Scan and tokenize template source.
87*04fd306cSNickeau     *
88*04fd306cSNickeau     * @param string $text       Mustache template source to tokenize
89*04fd306cSNickeau     * @param string $delimiters Optional, pass opening and closing delimiters
90*04fd306cSNickeau     *
91*04fd306cSNickeau     * @return array Set of Mustache tokens
92*04fd306cSNickeau     */
93*04fd306cSNickeau    public function scan($text, $delimiters = null)
94*04fd306cSNickeau    {
95*04fd306cSNickeau        if ($text instanceof HandlebarsString) {
96*04fd306cSNickeau            $text = $text->getString();
97*04fd306cSNickeau        }
98*04fd306cSNickeau
99*04fd306cSNickeau        $this->reset();
100*04fd306cSNickeau
101*04fd306cSNickeau        if ($delimiters !== null && $delimiters = trim($delimiters)) {
102*04fd306cSNickeau            list($otag, $ctag) = explode(' ', $delimiters);
103*04fd306cSNickeau            $this->otag = $otag;
104*04fd306cSNickeau            $this->ctag = $ctag;
105*04fd306cSNickeau        }
106*04fd306cSNickeau
107*04fd306cSNickeau        $openingTagLength = strlen($this->otag);
108*04fd306cSNickeau        $closingTagLength = strlen($this->ctag);
109*04fd306cSNickeau        $firstOpeningTagCharacter = $this->otag[0];
110*04fd306cSNickeau        $firstClosingTagCharacter = $this->ctag[0];
111*04fd306cSNickeau
112*04fd306cSNickeau        $len = strlen($text);
113*04fd306cSNickeau
114*04fd306cSNickeau        for ($i = 0; $i < $len; $i++) {
115*04fd306cSNickeau
116*04fd306cSNickeau            $character = $text[$i];
117*04fd306cSNickeau
118*04fd306cSNickeau            switch ($this->state) {
119*04fd306cSNickeau
120*04fd306cSNickeau                case self::IN_TEXT:
121*04fd306cSNickeau                    if ($character === $firstOpeningTagCharacter && $this->tagChange($this->otag, $text, $i, $openingTagLength)
122*04fd306cSNickeau                    ) {
123*04fd306cSNickeau                        $i--;
124*04fd306cSNickeau                        $this->flushBuffer();
125*04fd306cSNickeau                        $this->state = self::IN_TAG_TYPE;
126*04fd306cSNickeau                    } else {
127*04fd306cSNickeau                        if ($character == "\n") {
128*04fd306cSNickeau                            $this->filterLine();
129*04fd306cSNickeau                        } else {
130*04fd306cSNickeau                            $this->buffer .= $character;
131*04fd306cSNickeau                        }
132*04fd306cSNickeau                    }
133*04fd306cSNickeau                    break;
134*04fd306cSNickeau
135*04fd306cSNickeau                case self::IN_TAG_TYPE:
136*04fd306cSNickeau
137*04fd306cSNickeau                    $i += $openingTagLength - 1;
138*04fd306cSNickeau                    if (isset($this->tagTypes[$text[$i + 1]])) {
139*04fd306cSNickeau                        $tag = $text[$i + 1];
140*04fd306cSNickeau                        $this->tagType = $tag;
141*04fd306cSNickeau                    } else {
142*04fd306cSNickeau                        $tag = null;
143*04fd306cSNickeau                        $this->tagType = self::T_ESCAPED;
144*04fd306cSNickeau                    }
145*04fd306cSNickeau
146*04fd306cSNickeau                    if ($this->tagType === self::T_DELIM_CHANGE) {
147*04fd306cSNickeau                        $i = $this->changeDelimiters($text, $i);
148*04fd306cSNickeau                        $openingTagLength = strlen($this->otag);
149*04fd306cSNickeau                        $closingTagLength = strlen($this->ctag);
150*04fd306cSNickeau                        $firstOpeningTagCharacter = $this->otag[0];
151*04fd306cSNickeau                        $firstClosingTagCharacter = $this->ctag[0];
152*04fd306cSNickeau
153*04fd306cSNickeau                        $this->state = self::IN_TEXT;
154*04fd306cSNickeau                    } else {
155*04fd306cSNickeau                        if ($tag !== null) {
156*04fd306cSNickeau                            $i++;
157*04fd306cSNickeau                        }
158*04fd306cSNickeau                        $this->state = self::IN_TAG;
159*04fd306cSNickeau                    }
160*04fd306cSNickeau                    $this->seenTag = $i;
161*04fd306cSNickeau                    break;
162*04fd306cSNickeau
163*04fd306cSNickeau                default:
164*04fd306cSNickeau                    if ($character === $firstClosingTagCharacter && $this->tagChange($this->ctag, $text, $i, $closingTagLength)) {
165*04fd306cSNickeau                        // Sections (Helpers) can accept parameters
166*04fd306cSNickeau                        // Same thing for Partials (little known fact)
167*04fd306cSNickeau                        if (in_array($this->tagType, [
168*04fd306cSNickeau                                self::T_SECTION,
169*04fd306cSNickeau                                self::T_PARTIAL,
170*04fd306cSNickeau                                self::T_PARTIAL_2]
171*04fd306cSNickeau                        )) {
172*04fd306cSNickeau                            $newBuffer = explode(' ', trim($this->buffer), 2);
173*04fd306cSNickeau                            $args = '';
174*04fd306cSNickeau                            if (count($newBuffer) == 2) {
175*04fd306cSNickeau                                $args = $newBuffer[1];
176*04fd306cSNickeau                            }
177*04fd306cSNickeau                            $this->buffer = $newBuffer[0];
178*04fd306cSNickeau                        }
179*04fd306cSNickeau                        $t = [
180*04fd306cSNickeau                            self::TYPE => $this->tagType,
181*04fd306cSNickeau                            self::NAME => trim($this->buffer),
182*04fd306cSNickeau                            self::OTAG => $this->otag,
183*04fd306cSNickeau                            self::CTAG => $this->ctag,
184*04fd306cSNickeau                            self::INDEX => ($this->tagType == self::T_END_SECTION) ?
185*04fd306cSNickeau                                $this->seenTag - $openingTagLength :
186*04fd306cSNickeau                                $i + strlen($this->ctag),
187*04fd306cSNickeau                        ];
188*04fd306cSNickeau                        if (isset($args)) {
189*04fd306cSNickeau                            $t[self::ARGS] = $args;
190*04fd306cSNickeau                        }
191*04fd306cSNickeau                        $this->tokens[] = $t;
192*04fd306cSNickeau                        unset($t);
193*04fd306cSNickeau                        unset($args);
194*04fd306cSNickeau                        $this->buffer = '';
195*04fd306cSNickeau                        $i += strlen($this->ctag) - 1;
196*04fd306cSNickeau                        $this->state = self::IN_TEXT;
197*04fd306cSNickeau                        if ($this->tagType == self::T_UNESCAPED) {
198*04fd306cSNickeau                            if ($this->ctag == '}}') {
199*04fd306cSNickeau                                $i++;
200*04fd306cSNickeau                            } else {
201*04fd306cSNickeau                                // Clean up `{{{ tripleStache }}}` style tokens.
202*04fd306cSNickeau                                $lastIndex = count($this->tokens) - 1;
203*04fd306cSNickeau                                $lastName = $this->tokens[$lastIndex][self::NAME];
204*04fd306cSNickeau                                if (substr($lastName, -1) === '}') {
205*04fd306cSNickeau                                    $this->tokens[$lastIndex][self::NAME] = trim(
206*04fd306cSNickeau                                        substr($lastName, 0, -1)
207*04fd306cSNickeau                                    );
208*04fd306cSNickeau                                }
209*04fd306cSNickeau                            }
210*04fd306cSNickeau                        }
211*04fd306cSNickeau                    } else {
212*04fd306cSNickeau                        $this->buffer .= $character;
213*04fd306cSNickeau                    }
214*04fd306cSNickeau                    break;
215*04fd306cSNickeau            }
216*04fd306cSNickeau
217*04fd306cSNickeau        }
218*04fd306cSNickeau
219*04fd306cSNickeau        $this->filterLine(true);
220*04fd306cSNickeau
221*04fd306cSNickeau        return $this->tokens;
222*04fd306cSNickeau    }
223*04fd306cSNickeau
224*04fd306cSNickeau    /**
225*04fd306cSNickeau     * Helper function to reset tokenizer internal state.
226*04fd306cSNickeau     *
227*04fd306cSNickeau     * @return void
228*04fd306cSNickeau     */
229*04fd306cSNickeau    protected function reset()
230*04fd306cSNickeau    {
231*04fd306cSNickeau        $this->state = self::IN_TEXT;
232*04fd306cSNickeau        $this->tagType = null;
233*04fd306cSNickeau        $this->tag = null;
234*04fd306cSNickeau        $this->buffer = '';
235*04fd306cSNickeau        $this->tokens = [];
236*04fd306cSNickeau        $this->seenTag = false;
237*04fd306cSNickeau        $this->lineStart = 0;
238*04fd306cSNickeau        $this->otag = '{{';
239*04fd306cSNickeau        $this->ctag = '}}';
240*04fd306cSNickeau    }
241*04fd306cSNickeau
242*04fd306cSNickeau    /**
243*04fd306cSNickeau     * Flush the current buffer to a token.
244*04fd306cSNickeau     *
245*04fd306cSNickeau     * @return void
246*04fd306cSNickeau     */
247*04fd306cSNickeau    protected function flushBuffer()
248*04fd306cSNickeau    {
249*04fd306cSNickeau        if (!empty($this->buffer)) {
250*04fd306cSNickeau            $this->tokens[] = [
251*04fd306cSNickeau                self::TYPE => self::T_TEXT,
252*04fd306cSNickeau                self::VALUE => $this->buffer
253*04fd306cSNickeau            ];
254*04fd306cSNickeau            $this->buffer = '';
255*04fd306cSNickeau        }
256*04fd306cSNickeau    }
257*04fd306cSNickeau
258*04fd306cSNickeau    /**
259*04fd306cSNickeau     * Test whether the current line is entirely made up of whitespace.
260*04fd306cSNickeau     *
261*04fd306cSNickeau     * @return boolean True if the current line is all whitespace
262*04fd306cSNickeau     */
263*04fd306cSNickeau    protected function lineIsWhitespace()
264*04fd306cSNickeau    {
265*04fd306cSNickeau        $tokensCount = count($this->tokens);
266*04fd306cSNickeau        for ($j = $this->lineStart; $j < $tokensCount; $j++) {
267*04fd306cSNickeau            $token = $this->tokens[$j];
268*04fd306cSNickeau            if (isset($this->tagTypes[$token[self::TYPE]])) {
269*04fd306cSNickeau                if (isset($this->interpolatedTags[$token[self::TYPE]])) {
270*04fd306cSNickeau                    return false;
271*04fd306cSNickeau                }
272*04fd306cSNickeau            } elseif ($token[self::TYPE] == self::T_TEXT) {
273*04fd306cSNickeau                if (preg_match('/\S/', $token[self::VALUE])) {
274*04fd306cSNickeau                    return false;
275*04fd306cSNickeau                }
276*04fd306cSNickeau            }
277*04fd306cSNickeau        }
278*04fd306cSNickeau
279*04fd306cSNickeau        return true;
280*04fd306cSNickeau    }
281*04fd306cSNickeau
282*04fd306cSNickeau    /**
283*04fd306cSNickeau     * Filter out whitespace-only lines and store indent levels for partials.
284*04fd306cSNickeau     *
285*04fd306cSNickeau     * @param bool $noNewLine Suppress the newline? (default: false)
286*04fd306cSNickeau     *
287*04fd306cSNickeau     * @return void
288*04fd306cSNickeau     */
289*04fd306cSNickeau    protected function filterLine($noNewLine = false)
290*04fd306cSNickeau    {
291*04fd306cSNickeau        $this->flushBuffer();
292*04fd306cSNickeau        if ($this->seenTag && $this->lineIsWhitespace()) {
293*04fd306cSNickeau            $tokensCount = count($this->tokens);
294*04fd306cSNickeau            for ($j = $this->lineStart; $j < $tokensCount; $j++) {
295*04fd306cSNickeau                if ($this->tokens[$j][self::TYPE] == self::T_TEXT) {
296*04fd306cSNickeau                    if (isset($this->tokens[$j + 1])
297*04fd306cSNickeau                        && $this->tokens[$j + 1][self::TYPE] == self::T_PARTIAL
298*04fd306cSNickeau                    ) {
299*04fd306cSNickeau                        $this->tokens[$j + 1][self::INDENT]
300*04fd306cSNickeau                            = $this->tokens[$j][self::VALUE];
301*04fd306cSNickeau                    }
302*04fd306cSNickeau
303*04fd306cSNickeau                    $this->tokens[$j] = null;
304*04fd306cSNickeau                }
305*04fd306cSNickeau            }
306*04fd306cSNickeau        } elseif (!$noNewLine) {
307*04fd306cSNickeau            $this->tokens[] = [self::TYPE => self::T_TEXT, self::VALUE => "\n"];
308*04fd306cSNickeau        }
309*04fd306cSNickeau
310*04fd306cSNickeau        $this->seenTag = false;
311*04fd306cSNickeau        $this->lineStart = count($this->tokens);
312*04fd306cSNickeau    }
313*04fd306cSNickeau
314*04fd306cSNickeau    /**
315*04fd306cSNickeau     * Change the current Mustache delimiters. Set new `otag` and `ctag` values.
316*04fd306cSNickeau     *
317*04fd306cSNickeau     * @param string $text  Mustache template source
318*04fd306cSNickeau     * @param int    $index Current tokenizer index
319*04fd306cSNickeau     *
320*04fd306cSNickeau     * @return int New index value
321*04fd306cSNickeau     */
322*04fd306cSNickeau    protected function changeDelimiters($text, $index)
323*04fd306cSNickeau    {
324*04fd306cSNickeau        $startIndex = strpos($text, '=', $index) + 1;
325*04fd306cSNickeau        $close = '=' . $this->ctag;
326*04fd306cSNickeau        $closeIndex = strpos($text, $close, $index);
327*04fd306cSNickeau
328*04fd306cSNickeau        list($otag, $ctag) = explode(
329*04fd306cSNickeau            ' ',
330*04fd306cSNickeau            trim(substr($text, $startIndex, $closeIndex - $startIndex))
331*04fd306cSNickeau        );
332*04fd306cSNickeau        $this->otag = $otag;
333*04fd306cSNickeau        $this->ctag = $ctag;
334*04fd306cSNickeau
335*04fd306cSNickeau        return $closeIndex + strlen($close) - 1;
336*04fd306cSNickeau    }
337*04fd306cSNickeau
338*04fd306cSNickeau    /**
339*04fd306cSNickeau     * Test whether it's time to change tags.
340*04fd306cSNickeau     *
341*04fd306cSNickeau     * @param string $tag Current tag name
342*04fd306cSNickeau     * @param string $text Mustache template source
343*04fd306cSNickeau     * @param int $index Current tokenizer index
344*04fd306cSNickeau     * @param int $tagLength Length of the opening/closing tag string
345*04fd306cSNickeau     *
346*04fd306cSNickeau     * @return boolean True if this is a closing section tag
347*04fd306cSNickeau     */
348*04fd306cSNickeau    protected function tagChange($tag, $text, $index, $tagLength)
349*04fd306cSNickeau    {
350*04fd306cSNickeau        return substr($text, $index, $tagLength) === $tag;
351*04fd306cSNickeau    }
352*04fd306cSNickeau
353*04fd306cSNickeau}
354