Current Path : /data/web/virtuals/51568/virtual/www/subdom/agenda/vendor/latte/latte/src/Latte/Compiler/ |
Current File : /data/web/virtuals/51568/virtual/www/subdom/agenda/vendor/latte/latte/src/Latte/Compiler/Parser.php |
<?php /** * This file is part of the Latte (https://latte.nette.org) * Copyright (c) 2008 David Grudl (https://davidgrudl.com) */ declare(strict_types=1); namespace Latte; /** * Latte parser. */ class Parser { use Strict; /** @internal regular expression for single & double quoted PHP string */ public const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*+\'|"(?:\\\\.|[^"\\\\])*+"'; /** @internal HTML tag name for Latte needs (actually is [a-zA-Z][^\s/>]*) */ public const RE_TAG_NAME = '[a-zA-Z][a-zA-Z0-9:_.-]*'; /** @internal special HTML attribute prefix */ public const N_PREFIX = 'n:'; /** Context-aware escaping content types */ public const CONTENT_HTML = Engine::CONTENT_HTML, CONTENT_XHTML = Engine::CONTENT_XHTML, CONTENT_XML = Engine::CONTENT_XML, CONTENT_TEXT = Engine::CONTENT_TEXT; /** @internal states */ public const CONTEXT_NONE = 'none', CONTEXT_MACRO = 'macro', CONTEXT_HTML_TEXT = 'htmlText', CONTEXT_HTML_TAG = 'htmlTag', CONTEXT_HTML_ATTRIBUTE = 'htmlAttribute', CONTEXT_HTML_COMMENT = 'htmlComment', CONTEXT_HTML_CDATA = 'htmlCData'; /** @var string default macro tag syntax */ public $defaultSyntax = 'latte'; /** @var array<string, array{string, string}> */ public $syntaxes = [ 'latte' => ['\{(?![\s\'"{}])', '\}'], // {...} 'double' => ['\{\{(?![\s\'"{}])', '\}\}'], // {{...}} 'off' => ['\{(?=/syntax\})', '\}'], // {/syntax} ]; /** @var string[] */ private $delimiters; /** @var string source template */ private $input; /** @var Token[] */ private $output; /** @var int position on source template */ private $offset; /** @var int */ private $line; /** @var array{string, mixed} */ private $context = [self::CONTEXT_HTML_TEXT, null]; /** @var string|null */ private $lastHtmlTag; /** @var string|null used by filter() */ private $syntaxEndTag; /** @var int */ private $syntaxEndLevel = 0; /** @var bool */ private $xmlMode; /** * Process all {macros} and <tags/>. * @return Token[] */ public function parse(string $input): array { if (Helpers::startsWith($input, "\u{FEFF}")) { // BOM $input = substr($input, 3); } $this->input = $input = str_replace("\r\n", "\n", $input); $this->offset = 0; $this->line = 1; $this->output = []; if (!preg_match('##u', $input)) { preg_match('#(?:[\x00-\x7F]|[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3})*+#A', $input, $m); $this->line += substr_count($m[0], "\n"); throw new CompileException('Template is not valid UTF-8 stream.'); } elseif (preg_match('#[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]#', $input, $m, PREG_OFFSET_CAPTURE)) { $this->line += substr_count($input, "\n", 0, $m[0][1]); throw new CompileException('Template contains control character \x' . dechex(ord($m[0][0]))); } $this->setSyntax($this->defaultSyntax); $this->lastHtmlTag = $this->syntaxEndTag = null; $tokenCount = 0; while ($this->offset < strlen($input)) { if ($this->{'context' . $this->context[0]}() === false) { break; } while ($tokenCount < count($this->output)) { $this->filter($this->output[$tokenCount++]); } } if ($this->context[0] === self::CONTEXT_MACRO) { throw new CompileException('Malformed tag.'); } if ($this->offset < strlen($input)) { $this->addToken(Token::TEXT, substr($this->input, $this->offset)); } return $this->output; } /** * Handles CONTEXT_HTML_TEXT. */ private function contextHtmlText(): bool { $matches = $this->match('~ (?:(?<=\n|^)[ \t]*)?<(?P<closing>/?)(?P<tag>' . self::RE_TAG_NAME . ')| ## begin of HTML tag <tag </tag - ignores <!DOCTYPE <(?P<htmlcomment>!(?:--(?!>))?|\?)| ## begin of <!, <!--, <!DOCTYPE, <? (?P<macro>' . $this->delimiters[0] . ') ~xsi'); if (!empty($matches['htmlcomment'])) { // <! <? $this->addToken(Token::HTML_TAG_BEGIN, $matches[0]); $end = $matches['htmlcomment'] === '!--' ? '--' : ($matches['htmlcomment'] === '?' && $this->xmlMode ? '\?' : ''); $this->setContext(self::CONTEXT_HTML_COMMENT, $end); return true; } elseif (!empty($matches['tag'])) { // <tag or </tag $token = $this->addToken(Token::HTML_TAG_BEGIN, $matches[0]); $token->name = $matches['tag']; $token->closing = (bool) $matches['closing']; $this->lastHtmlTag = $matches['closing'] . strtolower($matches['tag']); $this->setContext(self::CONTEXT_HTML_TAG); return true; } else { return $this->processMacro($matches); } } /** * Handles CONTEXT_HTML_CDATA. */ private function contextHtmlCData(): bool { $matches = $this->match('~ </(?P<tag>' . $this->lastHtmlTag . ')(?=[\s/>])| ## end HTML tag </tag (?P<macro>' . $this->delimiters[0] . ') ~xsi'); if (empty($matches['tag'])) { return $this->processMacro($matches); } // </tag $token = $this->addToken(Token::HTML_TAG_BEGIN, $matches[0]); $token->name = $this->lastHtmlTag; $token->closing = true; $this->lastHtmlTag = '/' . $this->lastHtmlTag; $this->setContext(self::CONTEXT_HTML_TAG); return true; } /** * Handles CONTEXT_HTML_TAG. */ private function contextHtmlTag(): bool { $matches = $this->match('~ (?P<end>\s?/?>)([ \t]*\n)?| ## end of HTML tag (?P<macro>' . $this->delimiters[0] . ')| \s*(?P<attr>[^\s"\'>/={]+)(?:\s*=\s*(?P<value>["\']|[^\s"\'=<>`{]+))? ## beginning of HTML attribute ~xsi'); if (!empty($matches['end'])) { // end of HTML tag /> $this->addToken(Token::HTML_TAG_END, $matches[0]); $empty = strpos($matches[0], '/') !== false; $this->setContext(!$this->xmlMode && !$empty && in_array($this->lastHtmlTag, ['script', 'style'], true) ? self::CONTEXT_HTML_CDATA : self::CONTEXT_HTML_TEXT); return true; } elseif (isset($matches['attr']) && $matches['attr'] !== '') { // HTML attribute $token = $this->addToken(Token::HTML_ATTRIBUTE_BEGIN, $matches[0]); $token->name = $matches['attr']; $token->value = $matches['value'] ?? ''; if ($token->value === '"' || $token->value === "'") { // attribute = "' if (Helpers::startsWith($token->name, self::N_PREFIX)) { $token->value = ''; if ($m = $this->match('~(.*?)' . $matches['value'] . '~xsi')) { $token->value = $m[1]; $token->text .= $m[0]; } } else { $this->setContext(self::CONTEXT_HTML_ATTRIBUTE, $matches['value']); } } return true; } else { return $this->processMacro($matches); } } /** * Handles CONTEXT_HTML_ATTRIBUTE. */ private function contextHtmlAttribute(): bool { $matches = $this->match('~ (?P<quote>' . $this->context[1] . ')| ## end of HTML attribute (?P<macro>' . $this->delimiters[0] . ') ~xsi'); if (empty($matches['quote'])) { return $this->processMacro($matches); } // (attribute end) '" $this->addToken(Token::HTML_ATTRIBUTE_END, $matches[0]); $this->setContext(self::CONTEXT_HTML_TAG); return true; } /** * Handles CONTEXT_HTML_COMMENT. */ private function contextHtmlComment(): bool { $matches = $this->match('~ (?P<htmlcomment>' . $this->context[1] . '>)| ## end of HTML comment (?P<macro>' . $this->delimiters[0] . ') ~xsi'); if (empty($matches['htmlcomment'])) { return $this->processMacro($matches); } // --> $this->addToken(Token::HTML_TAG_END, $matches[0]); $this->setContext(self::CONTEXT_HTML_TEXT); return true; } /** * Handles CONTEXT_NONE. */ private function contextNone(): bool { $matches = $this->match('~ (?P<macro>' . $this->delimiters[0] . ') ~xsi'); return $this->processMacro($matches); } /** * Handles CONTEXT_MACRO. */ private function contextMacro(): bool { $matches = $this->match('~ (?P<comment>\*.*?\*' . $this->delimiters[1] . '\n{0,2})| (?P<macro>(?> ' . self::RE_STRING . '| \{(?>' . self::RE_STRING . '|[^\'"{}])*+\}| [^\'"{}]+ )++) ' . $this->delimiters[1] . ' (?P<rmargin>[ \t]*(?=\n))? ~xsiA'); if (!empty($matches['macro'])) { $token = $this->addToken(Token::MACRO_TAG, $this->context[1][1] . $matches[0]); [$token->name, $token->value, $token->modifiers, $token->empty, $token->closing] = $this->parseMacroTag($matches['macro']); $this->context = $this->context[1][0]; return true; } elseif (!empty($matches['comment'])) { $this->addToken(Token::COMMENT, $this->context[1][1] . $matches[0]); $this->context = $this->context[1][0]; return true; } else { throw new CompileException('Malformed tag contents.'); } } /** * @param string[] $matches */ private function processMacro(array $matches): bool { if (empty($matches['macro'])) { return false; } // {macro} or {* *} $this->setContext(self::CONTEXT_MACRO, [$this->context, $matches['macro']]); return true; } /** * Matches next token. * @return string[] */ private function match(string $re): array { if (!preg_match($re, $this->input, $matches, PREG_OFFSET_CAPTURE, $this->offset)) { if (preg_last_error()) { throw new RegexpException(null, preg_last_error()); } return []; } $value = substr($this->input, $this->offset, $matches[0][1] - $this->offset); if ($value !== '') { $this->addToken(Token::TEXT, $value); } $this->offset = $matches[0][1] + strlen($matches[0][0]); foreach ($matches as $k => $v) { $matches[$k] = $v[0]; } return $matches; } /** * @param string $type Parser::CONTENT_HTML, CONTENT_XHTML, CONTENT_XML or CONTENT_TEXT * @return static */ public function setContentType(string $type) { if (in_array($type, [self::CONTENT_HTML, self::CONTENT_XHTML, self::CONTENT_XML], true)) { $this->setContext(self::CONTEXT_HTML_TEXT); $this->xmlMode = $type === self::CONTENT_XML; } else { $this->setContext(self::CONTEXT_NONE); } return $this; } /** * @param mixed $quote * @return static */ public function setContext(string $context, $quote = null) { $this->context = [$context, $quote]; return $this; } /** * Changes macro tag delimiters. * @return static */ public function setSyntax(?string $type) { $type = $type ?? $this->defaultSyntax; if (!isset($this->syntaxes[$type])) { throw new \InvalidArgumentException("Unknown syntax '$type'"); } $this->setDelimiters($this->syntaxes[$type][0], $this->syntaxes[$type][1]); return $this; } /** * Changes macro tag delimiters (as regular expression). * @return static */ public function setDelimiters(string $left, string $right) { $this->delimiters = [$left, $right]; return $this; } /** * Parses macro tag to name, arguments a modifiers parts. * @param string $tag {name arguments | modifiers} * @return array{string, string, string, bool, bool}|null * @internal */ public function parseMacroTag(string $tag): ?array { if (!preg_match('~^ (?P<closing>/?) (?P<name>=|_(?!_)|[a-z]\w*+(?:[.:-]\w+)*+(?!::|\(|\\\\)|) ## name, /name, but not function( or class:: or namespace\ (?P<args>(?:' . self::RE_STRING . '|[^\'"])*?) (?P<modifiers>(?<!\|)\|[a-z](?P<modArgs>(?:' . self::RE_STRING . '|(?:\((?P>modArgs)\))|[^\'"/()]|/(?=.))*+))? (?P<empty>/?$) ()$~Disx', $tag, $match)) { if (preg_last_error()) { throw new RegexpException(null, preg_last_error()); } return null; } if ($match['name'] === '') { $match['name'] = $match['closing'] ? '' : '='; } return [$match['name'], trim($match['args']), $match['modifiers'], (bool) $match['empty'], (bool) $match['closing']]; } private function addToken(string $type, string $text): Token { $this->output[] = $token = new Token; $token->type = $type; $token->text = $text; $token->line = $this->line; $this->line += substr_count($text, "\n"); return $token; } public function getLine(): int { return $this->line; } /** * Process low-level macros. */ protected function filter(Token $token): void { if ($token->type === Token::MACRO_TAG && $token->name === 'syntax') { $this->setSyntax($token->closing ? $this->defaultSyntax : $token->value); $token->type = Token::COMMENT; } elseif ($token->type === Token::HTML_ATTRIBUTE_BEGIN && $token->name === 'n:syntax') { $this->setSyntax($token->value); $this->syntaxEndTag = $this->lastHtmlTag; $this->syntaxEndLevel = 1; $token->type = Token::COMMENT; } elseif ($token->type === Token::HTML_TAG_BEGIN && $this->lastHtmlTag === $this->syntaxEndTag) { $this->syntaxEndLevel++; } elseif ( $token->type === Token::HTML_TAG_END && $this->lastHtmlTag === ('/' . $this->syntaxEndTag) && --$this->syntaxEndLevel === 0 ) { $this->setSyntax($this->defaultSyntax); } elseif ($token->type === Token::MACRO_TAG && $token->name === 'contentType') { if (strpos($token->value, 'html') !== false) { $this->setContentType(self::CONTENT_HTML); } elseif (strpos($token->value, 'xml') !== false) { $this->setContentType(self::CONTENT_XML); } else { $this->setContentType(self::CONTENT_TEXT); } } } }