From 84789cbcd44f82e96cacd557f5f8af77b1077230 Mon Sep 17 00:00:00 2001 From: Matias Griese Date: Tue, 31 Oct 2017 16:50:05 +0200 Subject: [PATCH] Added `{% script %}` and `{% style %}` tags for Twig templates --- CHANGELOG.md | 1 + .../Grav/Common/Twig/Node/TwigNodeScript.php | 101 +++++++++++++++++ .../Grav/Common/Twig/Node/TwigNodeStyle.php | 97 ++++++++++++++++ .../TwigNodeTryCatch.php} | 4 +- .../TokenParser/TwigTokenParserScript.php | 105 ++++++++++++++++++ .../Twig/TokenParser/TwigTokenParserStyle.php | 98 ++++++++++++++++ .../TwigTokenParserTryCatch.php} | 8 +- system/src/Grav/Common/Twig/TwigExtension.php | 19 ++-- 8 files changed, 416 insertions(+), 17 deletions(-) create mode 100644 system/src/Grav/Common/Twig/Node/TwigNodeScript.php create mode 100644 system/src/Grav/Common/Twig/Node/TwigNodeStyle.php rename system/src/Grav/Common/Twig/{TwigNodeTry.php => Node/TwigNodeTryCatch.php} (94%) create mode 100644 system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php create mode 100644 system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php rename system/src/Grav/Common/Twig/{TokenParserTry.php => TokenParser/TwigTokenParserTryCatch.php} (87%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c0b59f4f..c836ede2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Added `Grav\Framework\Page` interfaces * Added `|nicenumber` Twig filter * Added `{% try %} ... {% catch %} Error: {{ e.message }} {% endcatch %}` tag to allow basic exception handling inside Twig + * Added `{% script %}` and `{% style %}` tags for Twig templates * Deprecated GravTrait 1. [](#improved) * Make it possible to include debug bar also into non-HTML responses diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeScript.php b/system/src/Grav/Common/Twig/Node/TwigNodeScript.php new file mode 100644 index 000000000..40d6e5e86 --- /dev/null +++ b/system/src/Grav/Common/Twig/Node/TwigNodeScript.php @@ -0,0 +1,101 @@ + $body, 'file' => $file, 'group' => $group, 'priority' => $priority, 'attributes' => $attributes], [], $lineno, $tag); + } + /** + * Compiles the node to PHP. + * + * @param \Twig_Compiler $compiler A Twig_Compiler instance + */ + public function compile(\Twig_Compiler $compiler) + { + $compiler->addDebugInfo($this); + + if ($this->getNode('attributes') !== null) { + $compiler + ->write('$attributes = ') + ->subcompile($this->getNode('attributes')) + ->raw(";\n") + ->write("if (\$attributes !== null && !is_array(\$attributes)) {\n") + ->indent() + ->write("throw new UnexpectedValueException('{% {$this->tagName} with x %}: x is not an array');\n") + ->outdent() + ->write("}\n"); + } else { + $compiler->write('$attributes = [];' . "\n"); + } + + if ($this->getNode('group') !== null) { + $compiler + ->write('$group = ') + ->subcompile($this->getNode('group')) + ->raw(";\n") + ->write("if (\$group !== null && !is_string(\$group)) {\n") + ->indent() + ->write("throw new UnexpectedValueException('{% {$this->tagName} in x %}: x is not a string');\n") + ->outdent() + ->write("}\n"); + } else { + $compiler->write('$group = null;' . "\n"); + } + + if ($this->getNode('priority') !== null) { + $compiler + ->write('$priority = (int)(') + ->subcompile($this->getNode('priority')) + ->raw(");\n"); + } else { + $compiler->write('$priority = null;' . "\n"); + } + + $compiler->write("\$assets = \\Grav\\Common\\Grav::instance()['assets'];\n"); + + if ($this->getNode('file') !== null) { + $compiler + ->write('$file = ') + ->subcompile($this->getNode('file')) + ->write(";\n") + ->write("\$pipeline = !empty(\$attributes['pipeline']);\n") + ->write("\$loading = !empty(\$attributes['defer']) ? 'defer' : (!empty(\$attributes['async']) ? 'async' : null);\n") + ->write("\$assets->addJs(\$file, \$priority, \$pipeline, \$loading, \$group);\n"); + } else { + $compiler + ->write("ob_start();\n") + ->subcompile($this->getNode('body')) + ->write("\$content = ob_end_clean();") + ->write("\$assets->addInlineJs(\$content, \$priority, \$group, \$attributes);\n"); + } + } +} diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeStyle.php b/system/src/Grav/Common/Twig/Node/TwigNodeStyle.php new file mode 100644 index 000000000..0cb239541 --- /dev/null +++ b/system/src/Grav/Common/Twig/Node/TwigNodeStyle.php @@ -0,0 +1,97 @@ + $body, 'file' => $file, 'group' => $group, 'priority' => $priority, 'attributes' => $attributes], [], $lineno, $tag); + } + /** + * Compiles the node to PHP. + * + * @param \Twig_Compiler $compiler A Twig_Compiler instance + */ + public function compile(\Twig_Compiler $compiler) + { + $compiler->addDebugInfo($this); + + if ($this->getNode('attributes') !== null) { + $compiler + ->write('$attributes = ') + ->subcompile($this->getNode('attributes')) + ->raw(";\n") + ->write("if (\$attributes !== null && !is_array(\$attributes)) {\n") + ->indent() + ->write("throw new UnexpectedValueException('{% {$this->tagName} with x %}: x is not an array');\n") + ->outdent() + ->write("}\n"); + } else { + $compiler->write('$attributes = [];' . "\n"); + } + + if ($this->getNode('group') !== null) { + $compiler + ->write('$group = ') + ->subcompile($this->getNode('group')) + ->raw(";\n") + ->write("if (\$group !== null && !is_string(\$group)) {\n") + ->indent() + ->write("throw new UnexpectedValueException('{% {$this->tagName} in x %}: x is not a string');\n") + ->outdent() + ->write("}\n"); + } else { + $compiler->write('$group = null;' . "\n"); + } + + if ($this->getNode('priority') !== null) { + $compiler + ->write('$priority = (int)(') + ->subcompile($this->getNode('priority')) + ->raw(");\n"); + } else { + $compiler->write('$priority = null;' . "\n"); + } + + $compiler->write("\$assets = \\Grav\\Common\\Grav::instance()['assets'];\n"); + + if ($this->getNode('file') !== null) { + $compiler + ->write('$file = ') + ->subcompile($this->getNode('file')) + ->write(";\n") + ->write("\$pipeline = !empty(\$attributes['pipeline']);\n") + ->write("\$assets->addCss(\$file, \$priority, \$pipeline, \$group);\n"); + } else { + $compiler + ->write("ob_start();\n") + ->subcompile($this->getNode('body')) + ->write("\$content = ob_end_clean();") + ->write("\$assets->addInlineCss(\$content, \$priority, \$group);\n"); + } + } +} diff --git a/system/src/Grav/Common/Twig/TwigNodeTry.php b/system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php similarity index 94% rename from system/src/Grav/Common/Twig/TwigNodeTry.php rename to system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php index 009556557..ee7e36e2f 100644 --- a/system/src/Grav/Common/Twig/TwigNodeTry.php +++ b/system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php @@ -6,9 +6,9 @@ * @license MIT License; see LICENSE file for details. */ -namespace Grav\Common\Twig; +namespace Grav\Common\Twig\Node; -class TwigNodeTry extends \Twig_Node +class TwigNodeTryCatch extends \Twig_Node { public function __construct(\Twig_NodeInterface $try, \Twig_NodeInterface $catch = null, $lineno, $tag = null) { diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php new file mode 100644 index 000000000..8677452a5 --- /dev/null +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php @@ -0,0 +1,105 @@ +getLine(); + $stream = $this->parser->getStream(); + + list($file, $group, $priority, $attributes) = $this->parseArguments($token); + + $content = null; + if ($file === null) { + $content = $this->parser->subparse([$this, 'decideBlockEnd'], true); + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + } + + return new TwigNodeScript($content, $file, $group, $priority, $attributes, $lineno, $this->getTag()); + } + + /** + * @param \Twig_Token $token + * @return array + */ + protected function parseArguments(\Twig_Token $token) + { + $stream = $this->parser->getStream(); + + if ($stream->test(\Twig_Token::BLOCK_END_TYPE)) { + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + + return [null, null, null, null]; + } + + $file = null; + if (!$stream->nextIf([\Twig_Token::NAME_TYPE, \Twig_Token::OPERATOR_TYPE])) { + $file = $this->parser->getExpressionParser()->parseExpression(); + } + + $group = null; + if ($stream->nextIf(\Twig_Token::OPERATOR_TYPE, 'in')) { + $group = $this->parser->getExpressionParser()->parseExpression(); + } + + $priority = null; + if ($stream->nextIf(\Twig_Token::NAME_TYPE, 'priority')) { + $priority = $this->parser->getExpressionParser()->parseExpression(); + } + + $attributes = null; + if ($stream->nextIf(\Twig_Token::NAME_TYPE, 'with')) { + $attributes = $this->parser->getExpressionParser()->parseExpression(); + } + + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + + return [$file, $group, $priority, $attributes]; + } + + /** + * @param \Twig_Token $token + * @return bool + */ + public function decideBlockEnd(\Twig_Token $token) + { + return $token->test('endscript'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'script'; + } +} diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php new file mode 100644 index 000000000..3be798f0c --- /dev/null +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php @@ -0,0 +1,98 @@ +getLine(); + $stream = $this->parser->getStream(); + + list ($file, $group, $priority, $attributes) = $this->parseArguments($token); + + $content = null; + if (!$file) { + $content = $this->parser->subparse([$this, 'decideBlockEnd'], true); + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + } + + return new TwigNodeStyle($content, $file, $group, $priority, $attributes, $lineno, $this->getTag()); + } + + /** + * @param \Twig_Token $token + * @return array + */ + protected function parseArguments(\Twig_Token $token) + { + $stream = $this->parser->getStream(); + + $file = null; + if (!$stream->nextIf([\Twig_Token::NAME_TYPE, \Twig_Token::OPERATOR_TYPE, \Twig_Token::BLOCK_END_TYPE])) { + $file = $this->parser->getExpressionParser()->parseExpression(); + } + + $group = null; + if ($stream->nextIf(\Twig_Token::OPERATOR_TYPE, 'in')) { + $group = $this->parser->getExpressionParser()->parseExpression(); + } + + $priority = null; + if ($stream->nextIf(\Twig_Token::NAME_TYPE, 'priority')) { + $priority = $this->parser->getExpressionParser()->parseExpression(); + } + + $attributes = null; + if ($stream->nextIf(\Twig_Token::NAME_TYPE, 'with')) { + $attributes = $this->parser->getExpressionParser()->parseExpression(); + } + + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + + return [$file, $group, $priority, $attributes]; + } + + /** + * @param \Twig_Token $token + * @return bool + */ + public function decideBlockEnd(\Twig_Token $token) + { + return $token->test('endstyle'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'style'; + } +} diff --git a/system/src/Grav/Common/Twig/TokenParserTry.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserTryCatch.php similarity index 87% rename from system/src/Grav/Common/Twig/TokenParserTry.php rename to system/src/Grav/Common/Twig/TokenParser/TwigTokenParserTryCatch.php index 74465540f..b225da345 100644 --- a/system/src/Grav/Common/Twig/TokenParserTry.php +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserTryCatch.php @@ -6,7 +6,9 @@ * @license MIT License; see LICENSE file for details. */ -namespace Grav\Common\Twig; +namespace Grav\Common\Twig\TokenParser; + +use Grav\Common\Twig\Node\TwigNodeTryCatch; /** * Handles try/catch in template file. @@ -19,7 +21,7 @@ namespace Grav\Common\Twig; * {% endcatch %} * */ -class TokenParserTry extends \Twig_TokenParser +class TwigTokenParserTryCatch extends \Twig_TokenParser { /** * Parses a token and returns a node. @@ -41,7 +43,7 @@ class TokenParserTry extends \Twig_TokenParser $stream->next(); $stream->expect(\Twig_Token::BLOCK_END_TYPE); - return new TwigNodeTry($try, $catch, $lineno, $this->getTag()); + return new TwigNodeTryCatch($try, $catch, $lineno, $this->getTag()); } public function decideCatch(\Twig_Token $token) diff --git a/system/src/Grav/Common/Twig/TwigExtension.php b/system/src/Grav/Common/Twig/TwigExtension.php index b1ee96da1..fcd9abdfb 100644 --- a/system/src/Grav/Common/Twig/TwigExtension.php +++ b/system/src/Grav/Common/Twig/TwigExtension.php @@ -11,6 +11,9 @@ namespace Grav\Common\Twig; use Grav\Common\Grav; use Grav\Common\Page\Collection; use Grav\Common\Page\Media; +use Grav\Common\Twig\TokenParser\TwigTokenParserScript; +use Grav\Common\Twig\TokenParser\TwigTokenParserStyle; +use Grav\Common\Twig\TokenParser\TwigTokenParserTryCatch; use Grav\Common\Utils; use Grav\Common\Markdown\Parsedown; use Grav\Common\Markdown\ParsedownExtra; @@ -18,7 +21,7 @@ use Grav\Common\Uri; use Grav\Common\Helpers\Base32; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; -class TwigExtension extends \Twig_Extension +class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsInterface { protected $grav; protected $debugger; @@ -34,16 +37,6 @@ class TwigExtension extends \Twig_Extension $this->config = $this->grav['config']; } - /** - * Returns extension name. - * - * @return string - */ - public function getName() - { - return 'GravTwigExtension'; - } - /** * Register some standard globals * @@ -154,7 +147,9 @@ class TwigExtension extends \Twig_Extension public function getTokenParsers() { return [ - new TokenParserTry(), + new TwigTokenParserTryCatch(), + new TwigTokenParserScript(), + new TwigTokenParserStyle(), ]; }