PageRenderTime 50ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/mustache/src/Mustache/Parser.php

https://bitbucket.org/moodle/moodle
PHP | 317 lines | 186 code | 40 blank | 91 comment | 29 complexity | fcbd8b53d590f68b11491d52d2390cdb MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0
  1. <?php
  2. /*
  3. * This file is part of Mustache.php.
  4. *
  5. * (c) 2010-2017 Justin Hileman
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. /**
  11. * Mustache Parser class.
  12. *
  13. * This class is responsible for turning a set of Mustache tokens into a parse tree.
  14. */
  15. class Mustache_Parser
  16. {
  17. private $lineNum;
  18. private $lineTokens;
  19. private $pragmas;
  20. private $defaultPragmas = array();
  21. private $pragmaFilters;
  22. private $pragmaBlocks;
  23. /**
  24. * Process an array of Mustache tokens and convert them into a parse tree.
  25. *
  26. * @param array $tokens Set of Mustache tokens
  27. *
  28. * @return array Mustache token parse tree
  29. */
  30. public function parse(array $tokens = array())
  31. {
  32. $this->lineNum = -1;
  33. $this->lineTokens = 0;
  34. $this->pragmas = $this->defaultPragmas;
  35. $this->pragmaFilters = isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS]);
  36. $this->pragmaBlocks = isset($this->pragmas[Mustache_Engine::PRAGMA_BLOCKS]);
  37. return $this->buildTree($tokens);
  38. }
  39. /**
  40. * Enable pragmas across all templates, regardless of the presence of pragma
  41. * tags in the individual templates.
  42. *
  43. * @internal Users should set global pragmas in Mustache_Engine, not here :)
  44. *
  45. * @param string[] $pragmas
  46. */
  47. public function setPragmas(array $pragmas)
  48. {
  49. $this->pragmas = array();
  50. foreach ($pragmas as $pragma) {
  51. $this->enablePragma($pragma);
  52. }
  53. $this->defaultPragmas = $this->pragmas;
  54. }
  55. /**
  56. * Helper method for recursively building a parse tree.
  57. *
  58. * @throws Mustache_Exception_SyntaxException when nesting errors or mismatched section tags are encountered
  59. *
  60. * @param array &$tokens Set of Mustache tokens
  61. * @param array $parent Parent token (default: null)
  62. *
  63. * @return array Mustache Token parse tree
  64. */
  65. private function buildTree(array &$tokens, array $parent = null)
  66. {
  67. $nodes = array();
  68. while (!empty($tokens)) {
  69. $token = array_shift($tokens);
  70. if ($token[Mustache_Tokenizer::LINE] === $this->lineNum) {
  71. $this->lineTokens++;
  72. } else {
  73. $this->lineNum = $token[Mustache_Tokenizer::LINE];
  74. $this->lineTokens = 0;
  75. }
  76. if ($this->pragmaFilters && isset($token[Mustache_Tokenizer::NAME])) {
  77. list($name, $filters) = $this->getNameAndFilters($token[Mustache_Tokenizer::NAME]);
  78. if (!empty($filters)) {
  79. $token[Mustache_Tokenizer::NAME] = $name;
  80. $token[Mustache_Tokenizer::FILTERS] = $filters;
  81. }
  82. }
  83. switch ($token[Mustache_Tokenizer::TYPE]) {
  84. case Mustache_Tokenizer::T_DELIM_CHANGE:
  85. $this->checkIfTokenIsAllowedInParent($parent, $token);
  86. $this->clearStandaloneLines($nodes, $tokens);
  87. break;
  88. case Mustache_Tokenizer::T_SECTION:
  89. case Mustache_Tokenizer::T_INVERTED:
  90. $this->checkIfTokenIsAllowedInParent($parent, $token);
  91. $this->clearStandaloneLines($nodes, $tokens);
  92. $nodes[] = $this->buildTree($tokens, $token);
  93. break;
  94. case Mustache_Tokenizer::T_END_SECTION:
  95. if (!isset($parent)) {
  96. $msg = sprintf(
  97. 'Unexpected closing tag: /%s on line %d',
  98. $token[Mustache_Tokenizer::NAME],
  99. $token[Mustache_Tokenizer::LINE]
  100. );
  101. throw new Mustache_Exception_SyntaxException($msg, $token);
  102. }
  103. if ($token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME]) {
  104. $msg = sprintf(
  105. 'Nesting error: %s (on line %d) vs. %s (on line %d)',
  106. $parent[Mustache_Tokenizer::NAME],
  107. $parent[Mustache_Tokenizer::LINE],
  108. $token[Mustache_Tokenizer::NAME],
  109. $token[Mustache_Tokenizer::LINE]
  110. );
  111. throw new Mustache_Exception_SyntaxException($msg, $token);
  112. }
  113. $this->clearStandaloneLines($nodes, $tokens);
  114. $parent[Mustache_Tokenizer::END] = $token[Mustache_Tokenizer::INDEX];
  115. $parent[Mustache_Tokenizer::NODES] = $nodes;
  116. return $parent;
  117. case Mustache_Tokenizer::T_PARTIAL:
  118. $this->checkIfTokenIsAllowedInParent($parent, $token);
  119. //store the whitespace prefix for laters!
  120. if ($indent = $this->clearStandaloneLines($nodes, $tokens)) {
  121. $token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE];
  122. }
  123. $nodes[] = $token;
  124. break;
  125. case Mustache_Tokenizer::T_PARENT:
  126. $this->checkIfTokenIsAllowedInParent($parent, $token);
  127. $nodes[] = $this->buildTree($tokens, $token);
  128. break;
  129. case Mustache_Tokenizer::T_BLOCK_VAR:
  130. if ($this->pragmaBlocks) {
  131. // BLOCKS pragma is enabled, let's do this!
  132. if (isset($parent) && $parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) {
  133. $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_BLOCK_ARG;
  134. }
  135. $this->clearStandaloneLines($nodes, $tokens);
  136. $nodes[] = $this->buildTree($tokens, $token);
  137. } else {
  138. // pretend this was just a normal "escaped" token...
  139. $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_ESCAPED;
  140. // TODO: figure out how to figure out if there was a space after this dollar:
  141. $token[Mustache_Tokenizer::NAME] = '$' . $token[Mustache_Tokenizer::NAME];
  142. $nodes[] = $token;
  143. }
  144. break;
  145. case Mustache_Tokenizer::T_PRAGMA:
  146. $this->enablePragma($token[Mustache_Tokenizer::NAME]);
  147. // no break
  148. case Mustache_Tokenizer::T_COMMENT:
  149. $this->clearStandaloneLines($nodes, $tokens);
  150. $nodes[] = $token;
  151. break;
  152. default:
  153. $nodes[] = $token;
  154. break;
  155. }
  156. }
  157. if (isset($parent)) {
  158. $msg = sprintf(
  159. 'Missing closing tag: %s opened on line %d',
  160. $parent[Mustache_Tokenizer::NAME],
  161. $parent[Mustache_Tokenizer::LINE]
  162. );
  163. throw new Mustache_Exception_SyntaxException($msg, $parent);
  164. }
  165. return $nodes;
  166. }
  167. /**
  168. * Clear standalone line tokens.
  169. *
  170. * Returns a whitespace token for indenting partials, if applicable.
  171. *
  172. * @param array $nodes Parsed nodes
  173. * @param array $tokens Tokens to be parsed
  174. *
  175. * @return array|null Resulting indent token, if any
  176. */
  177. private function clearStandaloneLines(array &$nodes, array &$tokens)
  178. {
  179. if ($this->lineTokens > 1) {
  180. // this is the third or later node on this line, so it can't be standalone
  181. return;
  182. }
  183. $prev = null;
  184. if ($this->lineTokens === 1) {
  185. // this is the second node on this line, so it can't be standalone
  186. // unless the previous node is whitespace.
  187. if ($prev = end($nodes)) {
  188. if (!$this->tokenIsWhitespace($prev)) {
  189. return;
  190. }
  191. }
  192. }
  193. if ($next = reset($tokens)) {
  194. // If we're on a new line, bail.
  195. if ($next[Mustache_Tokenizer::LINE] !== $this->lineNum) {
  196. return;
  197. }
  198. // If the next token isn't whitespace, bail.
  199. if (!$this->tokenIsWhitespace($next)) {
  200. return;
  201. }
  202. if (count($tokens) !== 1) {
  203. // Unless it's the last token in the template, the next token
  204. // must end in newline for this to be standalone.
  205. if (substr($next[Mustache_Tokenizer::VALUE], -1) !== "\n") {
  206. return;
  207. }
  208. }
  209. // Discard the whitespace suffix
  210. array_shift($tokens);
  211. }
  212. if ($prev) {
  213. // Return the whitespace prefix, if any
  214. return array_pop($nodes);
  215. }
  216. }
  217. /**
  218. * Check whether token is a whitespace token.
  219. *
  220. * True if token type is T_TEXT and value is all whitespace characters.
  221. *
  222. * @param array $token
  223. *
  224. * @return bool True if token is a whitespace token
  225. */
  226. private function tokenIsWhitespace(array $token)
  227. {
  228. if ($token[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_TEXT) {
  229. return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]);
  230. }
  231. return false;
  232. }
  233. /**
  234. * Check whether a token is allowed inside a parent tag.
  235. *
  236. * @throws Mustache_Exception_SyntaxException if an invalid token is found inside a parent tag
  237. *
  238. * @param array|null $parent
  239. * @param array $token
  240. */
  241. private function checkIfTokenIsAllowedInParent($parent, array $token)
  242. {
  243. if (isset($parent) && $parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) {
  244. throw new Mustache_Exception_SyntaxException('Illegal content in < parent tag', $token);
  245. }
  246. }
  247. /**
  248. * Split a tag name into name and filters.
  249. *
  250. * @param string $name
  251. *
  252. * @return array [Tag name, Array of filters]
  253. */
  254. private function getNameAndFilters($name)
  255. {
  256. $filters = array_map('trim', explode('|', $name));
  257. $name = array_shift($filters);
  258. return array($name, $filters);
  259. }
  260. /**
  261. * Enable a pragma.
  262. *
  263. * @param string $name
  264. */
  265. private function enablePragma($name)
  266. {
  267. $this->pragmas[$name] = true;
  268. switch ($name) {
  269. case Mustache_Engine::PRAGMA_BLOCKS:
  270. $this->pragmaBlocks = true;
  271. break;
  272. case Mustache_Engine::PRAGMA_FILTERS:
  273. $this->pragmaFilters = true;
  274. break;
  275. }
  276. }
  277. }