Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
   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 Parser class.
  14   *
  15   * This class is responsible for turning a set of Mustache tokens into a parse tree.
  16   */
  17  class Mustache_Parser
  18  {
  19      private $lineNum;
  20      private $lineTokens;
  21      private $pragmas;
  22      private $defaultPragmas = array();
  23  
  24      private $pragmaFilters;
  25      private $pragmaBlocks;
  26  
  27      /**
  28       * Process an array of Mustache tokens and convert them into a parse tree.
  29       *
  30       * @param array $tokens Set of Mustache tokens
  31       *
  32       * @return array Mustache token parse tree
  33       */
  34      public function parse(array $tokens = array())
  35      {
  36          $this->lineNum    = -1;
  37          $this->lineTokens = 0;
  38          $this->pragmas    = $this->defaultPragmas;
  39  
  40          $this->pragmaFilters = isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS]);
  41          $this->pragmaBlocks  = isset($this->pragmas[Mustache_Engine::PRAGMA_BLOCKS]);
  42  
  43          return $this->buildTree($tokens);
  44      }
  45  
  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      /**
  64       * Helper method for recursively building a parse tree.
  65       *
  66       * @throws Mustache_Exception_SyntaxException when nesting errors or mismatched section tags are encountered
  67       *
  68       * @param array &$tokens Set of Mustache tokens
  69       * @param array $parent  Parent token (default: null)
  70       *
  71       * @return array Mustache Token parse tree
  72       */
  73      private function buildTree(array &$tokens, array $parent = null)
  74      {
  75          $nodes = array();
  76  
  77          while (!empty($tokens)) {
  78              $token = array_shift($tokens);
  79  
  80              if ($token[Mustache_Tokenizer::LINE] === $this->lineNum) {
  81                  $this->lineTokens++;
  82              } else {
  83                  $this->lineNum    = $token[Mustache_Tokenizer::LINE];
  84                  $this->lineTokens = 0;
  85              }
  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  
  95              switch ($token[Mustache_Tokenizer::TYPE]) {
  96                  case Mustache_Tokenizer::T_DELIM_CHANGE:
  97                      $this->checkIfTokenIsAllowedInParent($parent, $token);
  98                      $this->clearStandaloneLines($nodes, $tokens);
  99                      break;
 100  
 101                  case Mustache_Tokenizer::T_SECTION:
 102                  case Mustache_Tokenizer::T_INVERTED:
 103                      $this->checkIfTokenIsAllowedInParent($parent, $token);
 104                      $this->clearStandaloneLines($nodes, $tokens);
 105                      $nodes[] = $this->buildTree($tokens, $token);
 106                      break;
 107  
 108                  case Mustache_Tokenizer::T_END_SECTION:
 109                      if (!isset($parent)) {
 110                          $msg = sprintf(
 111                              'Unexpected closing tag: /%s on line %d',
 112                              $token[Mustache_Tokenizer::NAME],
 113                              $token[Mustache_Tokenizer::LINE]
 114                          );
 115                          throw new Mustache_Exception_SyntaxException($msg, $token);
 116                      }
 117  
 118                      if ($token[Mustache_Tokenizer::NAME] !== $parent[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                          );
 126                          throw new Mustache_Exception_SyntaxException($msg, $token);
 127                      }
 128  
 129                      $this->clearStandaloneLines($nodes, $tokens);
 130                      $parent[Mustache_Tokenizer::END]   = $token[Mustache_Tokenizer::INDEX];
 131                      $parent[Mustache_Tokenizer::NODES] = $nodes;
 132  
 133                      return $parent;
 134  
 135                  case Mustache_Tokenizer::T_PARTIAL:
 136                      $this->checkIfTokenIsAllowedInParent($parent, $token);
 137                      //store the whitespace prefix for laters!
 138                      if ($indent = $this->clearStandaloneLines($nodes, $tokens)) {
 139                          $token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE];
 140                      }
 141                      $nodes[] = $token;
 142                      break;
 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  
 166                  case Mustache_Tokenizer::T_PRAGMA:
 167                      $this->enablePragma($token[Mustache_Tokenizer::NAME]);
 168                      // no break
 169  
 170                  case Mustache_Tokenizer::T_COMMENT:
 171                      $this->clearStandaloneLines($nodes, $tokens);
 172                      $nodes[] = $token;
 173                      break;
 174  
 175                  default:
 176                      $nodes[] = $token;
 177                      break;
 178              }
 179          }
 180  
 181          if (isset($parent)) {
 182              $msg = sprintf(
 183                  'Missing closing tag: %s opened on line %d',
 184                  $parent[Mustache_Tokenizer::NAME],
 185                  $parent[Mustache_Tokenizer::LINE]
 186              );
 187              throw new Mustache_Exception_SyntaxException($msg, $parent);
 188          }
 189  
 190          return $nodes;
 191      }
 192  
 193      /**
 194       * Clear standalone line tokens.
 195       *
 196       * Returns a whitespace token for indenting partials, if applicable.
 197       *
 198       * @param array $nodes  Parsed nodes
 199       * @param array $tokens Tokens to be parsed
 200       *
 201       * @return array|null Resulting indent token, if any
 202       */
 203      private function clearStandaloneLines(array &$nodes, array &$tokens)
 204      {
 205          if ($this->lineTokens > 1) {
 206              // this is the third or later node on this line, so it can't be standalone
 207              return;
 208          }
 209  
 210          $prev = null;
 211          if ($this->lineTokens === 1) {
 212              // this is the second node on this line, so it can't be standalone
 213              // unless the previous node is whitespace.
 214              if ($prev = end($nodes)) {
 215                  if (!$this->tokenIsWhitespace($prev)) {
 216                      return;
 217                  }
 218              }
 219          }
 220  
 221          if ($next = reset($tokens)) {
 222              // If we're on a new line, bail.
 223              if ($next[Mustache_Tokenizer::LINE] !== $this->lineNum) {
 224                  return;
 225              }
 226  
 227              // If the next token isn't whitespace, bail.
 228              if (!$this->tokenIsWhitespace($next)) {
 229                  return;
 230              }
 231  
 232              if (count($tokens) !== 1) {
 233                  // Unless it's the last token in the template, the next token
 234                  // must end in newline for this to be standalone.
 235                  if (substr($next[Mustache_Tokenizer::VALUE], -1) !== "\n") {
 236                      return;
 237                  }
 238              }
 239  
 240              // Discard the whitespace suffix
 241              array_shift($tokens);
 242          }
 243  
 244          if ($prev) {
 245              // Return the whitespace prefix, if any
 246              return array_pop($nodes);
 247          }
 248      }
 249  
 250      /**
 251       * Check whether token is a whitespace token.
 252       *
 253       * True if token type is T_TEXT and value is all whitespace characters.
 254       *
 255       * @param array $token
 256       *
 257       * @return bool True if token is a whitespace token
 258       */
 259      private function tokenIsWhitespace(array $token)
 260      {
 261          if ($token[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_TEXT) {
 262              return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]);
 263          }
 264  
 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          }
 316      }
 317  }