lineNum = -1; $this->lineTokens = 0; return $this->buildTree($tokens); } /** * Helper method for recursively building a parse tree. * * @throws Mustache_Exception_SyntaxException when nesting errors or mismatched section tags are encountered. * * @param array &$tokens Set of Mustache tokens * @param array $parent Parent token (default: null) * * @return array Mustache Token parse tree */ private function buildTree(array &$tokens, array $parent = null) { $nodes = array(); while (!empty($tokens)) { $token = array_shift($tokens); if ($token[Mustache_Tokenizer::LINE] === $this->lineNum) { $this->lineTokens++; } else { $this->lineNum = $token[Mustache_Tokenizer::LINE]; $this->lineTokens = 0; } switch ($token[Mustache_Tokenizer::TYPE]) { case Mustache_Tokenizer::T_DELIM_CHANGE: $this->clearStandaloneLines($nodes, $tokens); break; case Mustache_Tokenizer::T_SECTION: case Mustache_Tokenizer::T_INVERTED: $this->clearStandaloneLines($nodes, $tokens); $nodes[] = $this->buildTree($tokens, $token); break; case Mustache_Tokenizer::T_END_SECTION: if (!isset($parent)) { $msg = sprintf('Unexpected closing tag: /%s', $token[Mustache_Tokenizer::NAME]); throw new Mustache_Exception_SyntaxException($msg, $token); } if ($token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME]) { $msg = sprintf('Nesting error: %s vs. %s', $parent[Mustache_Tokenizer::NAME], $token[Mustache_Tokenizer::NAME]); throw new Mustache_Exception_SyntaxException($msg, $token); } $this->clearStandaloneLines($nodes, $tokens); $parent[Mustache_Tokenizer::END] = $token[Mustache_Tokenizer::INDEX]; $parent[Mustache_Tokenizer::NODES] = $nodes; return $parent; break; case Mustache_Tokenizer::T_PARTIAL: case Mustache_Tokenizer::T_PARTIAL_2: // store the whitespace prefix for laters! if ($indent = $this->clearStandaloneLines($nodes, $tokens)) { $token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE]; } $nodes[] = $token; break; case Mustache_Tokenizer::T_PRAGMA: case Mustache_Tokenizer::T_COMMENT: $this->clearStandaloneLines($nodes, $tokens); $nodes[] = $token; break; default: $nodes[] = $token; break; } } if (isset($parent)) { $msg = sprintf('Missing closing tag: %s', $parent[Mustache_Tokenizer::NAME]); throw new Mustache_Exception_SyntaxException($msg, $parent); } return $nodes; } /** * Clear standalone line tokens. * * Returns a whitespace token for indenting partials, if applicable. * * @param array $nodes Parsed nodes. * @param array $tokens Tokens to be parsed. * * @return array Resulting indent token, if any. */ private function clearStandaloneLines(array &$nodes, array &$tokens) { if ($this->lineTokens > 1) { // this is the third or later node on this line, so it can't be standalone return; } $prev = null; if ($this->lineTokens === 1) { // this is the second node on this line, so it can't be standalone // unless the previous node is whitespace. if ($prev = end($nodes)) { if (!$this->tokenIsWhitespace($prev)) { return; } } } $next = null; if ($next = reset($tokens)) { // If we're on a new line, bail. if ($next[Mustache_Tokenizer::LINE] !== $this->lineNum) { return; } // If the next token isn't whitespace, bail. if (!$this->tokenIsWhitespace($next)) { return; } if (count($tokens) !== 1) { // Unless it's the last token in the template, the next token // must end in newline for this to be standalone. if (substr($next[Mustache_Tokenizer::VALUE], -1) !== "\n") { return; } } // Discard the whitespace suffix array_shift($tokens); } if ($prev) { // Return the whitespace prefix, if any return array_pop($nodes); } } /** * Check whether token is a whitespace token. * * True if token type is T_TEXT and value is all whitespace characters. * * @param array $token * * @return boolean True if token is a whitespace token */ private function tokenIsWhitespace(array $token) { if ($token[Mustache_Tokenizer::TYPE] == Mustache_Tokenizer::T_TEXT) { return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]); } return false; } }