| @@ -2,9 +2,9 @@ | ||
| 2 | 2 | |
| 3 | 3 | /* |
| 4 | 4 | * This file is part of Mustache.php. |
| 5 | 5 | * |
| 6 | - * (c) 2012 Justin Hileman | |
| 6 | + * (c) 2010-2017 Justin Hileman | |
| 7 | 7 | * |
| 8 | 8 | * For the full copyright and license information, please view the LICENSE |
| 9 | 9 | * file that was distributed with this source code. |
| 10 | 10 | */ |
| @@ -17,9 +17,14 @@ | ||
| 17 | 17 | class Mustache_Parser |
| 18 | 18 | { |
| 19 | 19 | private $lineNum; |
| 20 | 20 | private $lineTokens; |
| 21 | + private $pragmas; | |
| 22 | + private $defaultPragmas = array(); | |
| 21 | 23 | |
| 24 | + private $pragmaFilters; | |
| 25 | + private $pragmaBlocks; | |
| 26 | + | |
| 22 | 27 | /** |
| 23 | 28 | * Process an array of Mustache tokens and convert them into a parse tree. |
| 24 | 29 | * |
| 25 | 30 | * @param array $tokens Set of Mustache tokens |
| @@ -29,23 +34,44 @@ | ||
| 29 | 34 | public function parse(array $tokens = array()) |
| 30 | 35 | { |
| 31 | 36 | $this->lineNum = -1; |
| 32 | 37 | $this->lineTokens = 0; |
| 38 | + $this->pragmas = $this->defaultPragmas; | |
| 33 | 39 | |
| 40 | + $this->pragmaFilters = isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS]); | |
| 41 | + $this->pragmaBlocks = isset($this->pragmas[Mustache_Engine::PRAGMA_BLOCKS]); | |
| 42 | + | |
| 34 | 43 | return $this->buildTree($tokens); |
| 35 | 44 | } |
| 36 | 45 | |
| 37 | 46 | /** |
| 47 | + * Enable pragmas across all templates, regardless of the presence of pragma | |
| 48 | + * tags in the individual templates. | |
| 49 | + * | |
| 50 | + * @internal Users should set global pragmas in Mustache_Engine, not here :) | |
| 51 | + * | |
| 52 | + * @param string[] $pragmas | |
| 53 | + */ | |
| 54 | + public function setPragmas(array $pragmas) | |
| 55 | + { | |
| 56 | + $this->pragmas = array(); | |
| 57 | + foreach ($pragmas as $pragma) { | |
| 58 | + $this->enablePragma($pragma); | |
| 59 | + } | |
| 60 | + $this->defaultPragmas = $this->pragmas; | |
| 61 | + } | |
| 62 | + | |
| 63 | + /** | |
| 38 | 64 | * Helper method for recursively building a parse tree. |
| 39 | 65 | * |
| 40 | - * @throws Mustache_Exception_SyntaxException when nesting errors or mismatched section tags are encountered. | |
| 66 | + * @throws Mustache_Exception_SyntaxException when nesting errors or mismatched section tags are encountered | |
| 41 | 67 | * |
| 42 | 68 | * @param array &$tokens Set of Mustache tokens |
| 43 | - * @param array $parent Parent token (default: null) | |
| 69 | + * @param array $parent Parent token (default: null) | |
| 44 | 70 | * |
| 45 | 71 | * @return array Mustache Token parse tree |
| 46 | 72 | */ |
| 47 | - private function buildTree(array &$tokens, array $parent = null) | |
| 73 | + private function buildTree(array &$tokens, ?array $parent = null) | |
| 48 | 74 | { |
| 49 | 75 | $nodes = array(); |
| 50 | 76 | |
| 51 | 77 | while (!empty($tokens)) { |
| @@ -57,15 +83,25 @@ | ||
| 57 | 83 | $this->lineNum = $token[Mustache_Tokenizer::LINE]; |
| 58 | 84 | $this->lineTokens = 0; |
| 59 | 85 | } |
| 60 | 86 | |
| 87 | + if ($this->pragmaFilters && isset($token[Mustache_Tokenizer::NAME])) { | |
| 88 | + list($name, $filters) = $this->getNameAndFilters($token[Mustache_Tokenizer::NAME]); | |
| 89 | + if (!empty($filters)) { | |
| 90 | + $token[Mustache_Tokenizer::NAME] = $name; | |
| 91 | + $token[Mustache_Tokenizer::FILTERS] = $filters; | |
| 92 | + } | |
| 93 | + } | |
| 94 | + | |
| 61 | 95 | switch ($token[Mustache_Tokenizer::TYPE]) { |
| 62 | 96 | case Mustache_Tokenizer::T_DELIM_CHANGE: |
| 97 | + $this->checkIfTokenIsAllowedInParent($parent, $token); | |
| 63 | 98 | $this->clearStandaloneLines($nodes, $tokens); |
| 64 | 99 | break; |
| 65 | 100 | |
| 66 | 101 | case Mustache_Tokenizer::T_SECTION: |
| 67 | 102 | case Mustache_Tokenizer::T_INVERTED: |
| 103 | + $this->checkIfTokenIsAllowedInParent($parent, $token); | |
| 68 | 104 | $this->clearStandaloneLines($nodes, $tokens); |
| 69 | 105 | $nodes[] = $this->buildTree($tokens, $token); |
| 70 | 106 | break; |
| 71 | 107 | |
| @@ -70,14 +106,24 @@ | ||
| 70 | 106 | break; |
| 71 | 107 | |
| 72 | 108 | case Mustache_Tokenizer::T_END_SECTION: |
| 73 | 109 | if (!isset($parent)) { |
| 74 | - $msg = sprintf('Unexpected closing tag: /%s', $token[Mustache_Tokenizer::NAME]); | |
| 110 | + $msg = sprintf( | |
| 111 | + 'Unexpected closing tag: /%s on line %d', | |
| 112 | + $token[Mustache_Tokenizer::NAME], | |
| 113 | + $token[Mustache_Tokenizer::LINE] | |
| 114 | + ); | |
| 75 | 115 | throw new Mustache_Exception_SyntaxException($msg, $token); |
| 76 | 116 | } |
| 77 | 117 | |
| 78 | 118 | if ($token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME]) { |
| 79 | - $msg = sprintf('Nesting error: %s vs. %s', $parent[Mustache_Tokenizer::NAME], $token[Mustache_Tokenizer::NAME]); | |
| 119 | + $msg = sprintf( | |
| 120 | + 'Nesting error: %s (on line %d) vs. %s (on line %d)', | |
| 121 | + $parent[Mustache_Tokenizer::NAME], | |
| 122 | + $parent[Mustache_Tokenizer::LINE], | |
| 123 | + $token[Mustache_Tokenizer::NAME], | |
| 124 | + $token[Mustache_Tokenizer::LINE] | |
| 125 | + ); | |
| 80 | 126 | throw new Mustache_Exception_SyntaxException($msg, $token); |
| 81 | 127 | } |
| 82 | 128 | |
| 83 | 129 | $this->clearStandaloneLines($nodes, $tokens); |
| @@ -84,13 +130,12 @@ | ||
| 84 | 130 | $parent[Mustache_Tokenizer::END] = $token[Mustache_Tokenizer::INDEX]; |
| 85 | 131 | $parent[Mustache_Tokenizer::NODES] = $nodes; |
| 86 | 132 | |
| 87 | 133 | return $parent; |
| 88 | - break; | |
| 89 | 134 | |
| 90 | 135 | case Mustache_Tokenizer::T_PARTIAL: |
| 91 | - case Mustache_Tokenizer::T_PARTIAL_2: | |
| 92 | - // store the whitespace prefix for laters! | |
| 136 | + $this->checkIfTokenIsAllowedInParent($parent, $token); | |
| 137 | + //store the whitespace prefix for laters! | |
| 93 | 138 | if ($indent = $this->clearStandaloneLines($nodes, $tokens)) { |
| 94 | 139 | $token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE]; |
| 95 | 140 | } |
| 96 | 141 | $nodes[] = $token; |
| @@ -95,9 +140,34 @@ | ||
| 95 | 140 | } |
| 96 | 141 | $nodes[] = $token; |
| 97 | 142 | break; |
| 98 | 143 | |
| 144 | + case Mustache_Tokenizer::T_PARENT: | |
| 145 | + $this->checkIfTokenIsAllowedInParent($parent, $token); | |
| 146 | + $nodes[] = $this->buildTree($tokens, $token); | |
| 147 | + break; | |
| 148 | + | |
| 149 | + case Mustache_Tokenizer::T_BLOCK_VAR: | |
| 150 | + if ($this->pragmaBlocks) { | |
| 151 | + // BLOCKS pragma is enabled, let's do this! | |
| 152 | + if (isset($parent) && $parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) { | |
| 153 | + $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_BLOCK_ARG; | |
| 154 | + } | |
| 155 | + $this->clearStandaloneLines($nodes, $tokens); | |
| 156 | + $nodes[] = $this->buildTree($tokens, $token); | |
| 157 | + } else { | |
| 158 | + // pretend this was just a normal "escaped" token... | |
| 159 | + $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_ESCAPED; | |
| 160 | + // TODO: figure out how to figure out if there was a space after this dollar: | |
| 161 | + $token[Mustache_Tokenizer::NAME] = '$' . $token[Mustache_Tokenizer::NAME]; | |
| 162 | + $nodes[] = $token; | |
| 163 | + } | |
| 164 | + break; | |
| 165 | + | |
| 99 | 166 | case Mustache_Tokenizer::T_PRAGMA: |
| 167 | + $this->enablePragma($token[Mustache_Tokenizer::NAME]); | |
| 168 | + // no break | |
| 169 | + | |
| 100 | 170 | case Mustache_Tokenizer::T_COMMENT: |
| 101 | 171 | $this->clearStandaloneLines($nodes, $tokens); |
| 102 | 172 | $nodes[] = $token; |
| 103 | 173 | break; |
| @@ -108,9 +178,13 @@ | ||
| 108 | 178 | } |
| 109 | 179 | } |
| 110 | 180 | |
| 111 | 181 | if (isset($parent)) { |
| 112 | - $msg = sprintf('Missing closing tag: %s', $parent[Mustache_Tokenizer::NAME]); | |
| 182 | + $msg = sprintf( | |
| 183 | + 'Missing closing tag: %s opened on line %d', | |
| 184 | + $parent[Mustache_Tokenizer::NAME], | |
| 185 | + $parent[Mustache_Tokenizer::LINE] | |
| 186 | + ); | |
| 113 | 187 | throw new Mustache_Exception_SyntaxException($msg, $parent); |
| 114 | 188 | } |
| 115 | 189 | |
| 116 | 190 | return $nodes; |
| @@ -120,12 +194,12 @@ | ||
| 120 | 194 | * Clear standalone line tokens. |
| 121 | 195 | * |
| 122 | 196 | * Returns a whitespace token for indenting partials, if applicable. |
| 123 | 197 | * |
| 124 | - * @param array $nodes Parsed nodes. | |
| 125 | - * @param array $tokens Tokens to be parsed. | |
| 198 | + * @param array $nodes Parsed nodes | |
| 199 | + * @param array $tokens Tokens to be parsed | |
| 126 | 200 | * |
| 127 | - * @return array Resulting indent token, if any. | |
| 201 | + * @return array|null Resulting indent token, if any | |
| 128 | 202 | */ |
| 129 | 203 | private function clearStandaloneLines(array &$nodes, array &$tokens) |
| 130 | 204 | { |
| 131 | 205 | if ($this->lineTokens > 1) { |
| @@ -143,9 +217,8 @@ | ||
| 143 | 217 | } |
| 144 | 218 | } |
| 145 | 219 | } |
| 146 | 220 | |
| 147 | - $next = null; | |
| 148 | 221 | if ($next = reset($tokens)) { |
| 149 | 222 | // If we're on a new line, bail. |
| 150 | 223 | if ($next[Mustache_Tokenizer::LINE] !== $this->lineNum) { |
| 151 | 224 | return; |
| @@ -180,15 +253,65 @@ | ||
| 180 | 253 | * True if token type is T_TEXT and value is all whitespace characters. |
| 181 | 254 | * |
| 182 | 255 | * @param array $token |
| 183 | 256 | * |
| 184 | - * @return boolean True if token is a whitespace token | |
| 257 | + * @return bool True if token is a whitespace token | |
| 185 | 258 | */ |
| 186 | 259 | private function tokenIsWhitespace(array $token) |
| 187 | 260 | { |
| 188 | - if ($token[Mustache_Tokenizer::TYPE] == Mustache_Tokenizer::T_TEXT) { | |
| 261 | + if ($token[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_TEXT) { | |
| 189 | 262 | return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]); |
| 190 | 263 | } |
| 191 | 264 | |
| 192 | 265 | return false; |
| 266 | + } | |
| 267 | + | |
| 268 | + /** | |
| 269 | + * Check whether a token is allowed inside a parent tag. | |
| 270 | + * | |
| 271 | + * @throws Mustache_Exception_SyntaxException if an invalid token is found inside a parent tag | |
| 272 | + * | |
| 273 | + * @param array|null $parent | |
| 274 | + * @param array $token | |
| 275 | + */ | |
| 276 | + private function checkIfTokenIsAllowedInParent($parent, array $token) | |
| 277 | + { | |
| 278 | + if (isset($parent) && $parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) { | |
| 279 | + throw new Mustache_Exception_SyntaxException('Illegal content in < parent tag', $token); | |
| 280 | + } | |
| 281 | + } | |
| 282 | + | |
| 283 | + /** | |
| 284 | + * Split a tag name into name and filters. | |
| 285 | + * | |
| 286 | + * @param string $name | |
| 287 | + * | |
| 288 | + * @return array [Tag name, Array of filters] | |
| 289 | + */ | |
| 290 | + private function getNameAndFilters($name) | |
| 291 | + { | |
| 292 | + $filters = array_map('trim', explode('|', $name)); | |
| 293 | + $name = array_shift($filters); | |
| 294 | + | |
| 295 | + return array($name, $filters); | |
| 296 | + } | |
| 297 | + | |
| 298 | + /** | |
| 299 | + * Enable a pragma. | |
| 300 | + * | |
| 301 | + * @param string $name | |
| 302 | + */ | |
| 303 | + private function enablePragma($name) | |
| 304 | + { | |
| 305 | + $this->pragmas[$name] = true; | |
| 306 | + | |
| 307 | + switch ($name) { | |
| 308 | + case Mustache_Engine::PRAGMA_BLOCKS: | |
| 309 | + $this->pragmaBlocks = true; | |
| 310 | + break; | |
| 311 | + | |
| 312 | + case Mustache_Engine::PRAGMA_FILTERS: | |
| 313 | + $this->pragmaFilters = true; | |
| 314 | + break; | |
| 315 | + } | |
| 193 | 316 | } |
| 194 | 317 | } |