From 2fa8224be447b07447ac29ee3d0bd722521aacc3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 23 Apr 2012 13:03:38 +0200 Subject: [PATCH] added the inline tag --- CHANGELOG | 4 + doc/tags/index.rst | 1 + doc/tags/inline.rst | 127 ++++++++++++++++++++ lib/Twig/Environment.php | 14 ++- lib/Twig/Extension/Core.php | 1 + lib/Twig/Node/Include.php | 42 ++++--- lib/Twig/Node/Inline.php | 39 ++++++ lib/Twig/Node/Module.php | 21 +++- lib/Twig/Node/SandboxedModule.php | 4 +- lib/Twig/Parser.php | 14 ++- lib/Twig/TokenParser/Include.php | 27 +++-- lib/Twig/TokenParser/Inline.php | 50 ++++++++ test/Twig/Tests/Fixtures/tags/inline/basic.test | 37 ++++++ .../Tests/Fixtures/tags/inline/with_extends.test | 54 ++++++++ test/Twig/Tests/Node/ModuleTest.php | 8 +- test/Twig/Tests/Node/SandboxedModuleTest.php | 6 +- 16 files changed, 404 insertions(+), 45 deletions(-) create mode 100644 doc/tags/inline.rst create mode 100644 lib/Twig/Node/Inline.php create mode 100644 lib/Twig/TokenParser/Inline.php create mode 100644 test/Twig/Tests/Fixtures/tags/inline/basic.test create mode 100644 test/Twig/Tests/Fixtures/tags/inline/with_extends.test diff --git a/CHANGELOG b/CHANGELOG index 51bd451..a6a8d08 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +* 1.8.0 (2012-XX-XX) + + * added an inline tag + * 1.7.0 (2012-XX-XX) * fixed template line number in some exceptions diff --git a/doc/tags/index.rst b/doc/tags/index.rst index f80d782..52d0205 100644 --- a/doc/tags/index.rst +++ b/doc/tags/index.rst @@ -21,3 +21,4 @@ Tags flush do sandbox + inline diff --git a/doc/tags/inline.rst b/doc/tags/inline.rst new file mode 100644 index 0000000..23047eb --- /dev/null +++ b/doc/tags/inline.rst @@ -0,0 +1,127 @@ +``inline`` +========== + +.. versionadded:: 1.8 + The ``inline`` tag was added in Twig 1.8. + +The ``inline`` statement allows you to inline a template instead of including +it from an external file (like with the ``include`` statement): + +.. code-block:: jinja + + {% inline %} + {% extends "sidebar.twig" %} + + {% block content %} + Some content for the sidebar + {% endblock %} + {% endinline %} + +As it's not easy to understand in which circumstances it might come in handy, +let's take an example; imagine a base template shared by many pages with a +single block:: + + ┌─── Page n ──────────────────────────┐ + │ │ + │ ┌─────────────────────┐ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ └─────────────────────┘ │ + │ │ + └─────────────────────────────────────┘ + +Even pages (page 2, 4, ...) share the same structure for the block:: + + ┌─── Page 2 & 4 ──────────────────────┐ + │ │ + │ ┌── Base even ────────┐ │ + │ │ ┌─────────────────┐ │ │ + │ │ │ Content 2, ... │ │ │ + │ │ └─────────────────┘ │ │ + │ │ ┌─────────────────┐ │ │ + │ │ │ Content 2, ... │ │ │ + │ │ └─────────────────┘ │ │ + │ └─────────────────────┘ │ + │ │ + └─────────────────────────────────────┘ + +And odd pages (page 1, 3, ...) share a different structure for the block:: + + ┌─── Page 1 & 3 ──────────────────────┐ + │ │ + │ ┌── Base odd ─────────┐ │ + │ │ ┌───────┐ ┌───────┐ │ │ + │ │ │ │ │ │ │ │ + │ │ │content│ │content│ │ │ + │ │ │3, ... │ │3, ... │ │ │ + │ │ │ │ │ │ │ │ + │ │ └───────┘ └───────┘ │ │ + │ └─────────────────────┘ │ + │ │ + └─────────────────────────────────────┘ + +Without the ``inline`` tag, you have two ways to design your templates: + + * Create two base templates (one for even blocks and another one for odd + blocks) to factor out the common template code, then one template for each + page that inherits from one of the base template; + + * Inline the each custom page content directly into each page without any use + of external templates (you need to repeat the common code for all + templates). + +These two solutions do not scale well because they each have a major drawback: + + * The first solution makes you create many external files (that you won't + re-use anywhere else) and so it fails to keep your templates readable (many + code and content are out of context); + + * The second solution makes you duplicate some common code from one template + to another (so it fails to obey the "Don't repeat yourself" principle). + +In such a situation, the ``inline`` tag fixes all these issues. The common +code can be factored out in base templates (as in solution 1), and the custom +content is kept in each page (as in solution 2): + +.. code-block:: jinja + + {# template for an even template: page 2 #} + + {% extends page %} + + {% block base %} + {% inline %} + {% extends "even.twig" %} + + {% block content1 %} + Content 1 for page 2 + {% endblock %} + + {% block content2 %} + Content 2 for page 2 + {% endblock %} + {% endinline %} + {% endblock %} + +The ``inline`` tag can be customized with the same options (``with``, +``only``, ``ignore missing``) as the ``include`` tag: + +.. code-block:: jinja + + {% inline with {'foo': 'bar'} %} + ... + {% endinline %} + + {% inline with {'foo': 'bar'} only %} + ... + {% endinline %} + + {% inline ignore missing %} + ... + {% endinline %} + +.. seealso:: :doc:`include<../tags/include>` diff --git a/lib/Twig/Environment.php b/lib/Twig/Environment.php index 88eaf90..db28eff 100644 --- a/lib/Twig/Environment.php +++ b/lib/Twig/Environment.php @@ -251,13 +251,14 @@ class Twig_Environment /** * Gets the template class associated with the given string. * - * @param string $name The name for which to calculate the template class name + * @param string $name The name for which to calculate the template class name + * @param integer $indice The indice for inline templates (null for main templates) * * @return string The template class name */ - public function getTemplateClass($name) + public function getTemplateClass($name, $indice = null) { - return $this->templateClassPrefix.md5($this->loader->getCacheKey($name)); + return $this->templateClassPrefix.md5($this->loader->getCacheKey($name)).(null === $indice ? '' : '_'.$indice); } /** @@ -297,13 +298,14 @@ class Twig_Environment /** * Loads a template by name. * - * @param string $name The template name + * @param string $name The template name + * @param integer $indice The indice for inline templates (null for main templates) * * @return Twig_TemplateInterface A template instance representing the given template name */ - public function loadTemplate($name) + public function loadTemplate($name, $indice = null) { - $cls = $this->getTemplateClass($name); + $cls = $this->getTemplateClass($name, $indice); if (isset($this->loadedTemplates[$cls])) { return $this->loadedTemplates[$cls]; diff --git a/lib/Twig/Extension/Core.php b/lib/Twig/Extension/Core.php index a540c5a..52ed531 100644 --- a/lib/Twig/Extension/Core.php +++ b/lib/Twig/Extension/Core.php @@ -109,6 +109,7 @@ class Twig_Extension_Core extends Twig_Extension new Twig_TokenParser_Spaceless(), new Twig_TokenParser_Flush(), new Twig_TokenParser_Do(), + new Twig_TokenParser_Inline(), ); } diff --git a/lib/Twig/Node/Include.php b/lib/Twig/Node/Include.php index 467749b..5b6be7a 100644 --- a/lib/Twig/Node/Include.php +++ b/lib/Twig/Node/Include.php @@ -39,21 +39,46 @@ class Twig_Node_Include extends Twig_Node implements Twig_NodeOutputInterface ; } + $this->addGetTemplate($compiler); + + $compiler->raw('->display('); + + $this->addTemplateArguments($compiler); + + $compiler->raw(");\n"); + + if ($this->getAttribute('ignore_missing')) { + $compiler + ->outdent() + ->write("} catch (Twig_Error_Loader \$e) {\n") + ->indent() + ->write("// ignore missing template\n") + ->outdent() + ->write("}\n\n") + ; + } + } + + protected function addGetTemplate(Twig_Compiler $compiler) + { if ($this->getNode('expr') instanceof Twig_Node_Expression_Constant) { $compiler ->write("\$this->env->loadTemplate(") ->subcompile($this->getNode('expr')) - ->raw(")->display(") + ->raw(")") ; } else { $compiler ->write("\$template = \$this->env->resolveTemplate(") ->subcompile($this->getNode('expr')) ->raw(");\n") - ->write('$template->display(') + ->write('$template') ; } + } + protected function addTemplateArguments(Twig_Compiler $compiler) + { if (false === $this->getAttribute('only')) { if (null === $this->getNode('variables')) { $compiler->raw('$context'); @@ -71,18 +96,5 @@ class Twig_Node_Include extends Twig_Node implements Twig_NodeOutputInterface $compiler->subcompile($this->getNode('variables')); } } - - $compiler->raw(");\n"); - - if ($this->getAttribute('ignore_missing')) { - $compiler - ->outdent() - ->write("} catch (Twig_Error_Loader \$e) {\n") - ->indent() - ->write("// ignore missing template\n") - ->outdent() - ->write("}\n\n") - ; - } } } diff --git a/lib/Twig/Node/Inline.php b/lib/Twig/Node/Inline.php new file mode 100644 index 0000000..ab26b40 --- /dev/null +++ b/lib/Twig/Node/Inline.php @@ -0,0 +1,39 @@ + + */ +class Twig_Node_Inline extends Twig_Node_Include +{ + // we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module) + public function __construct($filename, $inline, Twig_Node_Expression $variables = null, $only = false, $ignoreMissing = false, $lineno, $tag = null) + { + parent::__construct(new Twig_Node_Expression_Constant('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno, $tag); + + $this->setAttribute('filename', $filename); + $this->setAttribute('inline', $inline); + } + + protected function addGetTemplate(Twig_Compiler $compiler) + { + $compiler + ->write("\$this->env->loadTemplate(") + ->string($this->getAttribute('filename')) + ->raw(', ') + ->string($this->getAttribute('inline')) + ->raw(")") + ; + } +} diff --git a/lib/Twig/Node/Module.php b/lib/Twig/Node/Module.php index 65428f9..af106ff 100644 --- a/lib/Twig/Node/Module.php +++ b/lib/Twig/Node/Module.php @@ -18,9 +18,14 @@ */ class Twig_Node_Module extends Twig_Node { - public function __construct(Twig_NodeInterface $body, Twig_Node_Expression $parent = null, Twig_NodeInterface $blocks, Twig_NodeInterface $macros, Twig_NodeInterface $traits, $filename) + public function __construct(Twig_NodeInterface $body, Twig_Node_Expression $parent = null, Twig_NodeInterface $blocks, Twig_NodeInterface $macros, Twig_NodeInterface $traits, Twig_NodeInterface $inlinedTemplates, $filename) { - parent::__construct(array('parent' => $parent, 'body' => $body, 'blocks' => $blocks, 'macros' => $macros, 'traits' => $traits), array('filename' => $filename), 1); + parent::__construct(array('parent' => $parent, 'body' => $body, 'blocks' => $blocks, 'macros' => $macros, 'traits' => $traits, 'inlined_templates' => $inlinedTemplates), array('filename' => $filename, 'inline' => null), 1); + } + + public function setInline($indice) + { + $this->setAttribute('inline', $indice); } /** @@ -31,10 +36,18 @@ class Twig_Node_Module extends Twig_Node public function compile(Twig_Compiler $compiler) { $this->compileTemplate($compiler); + + foreach ($this->getNode('inlined_templates') as $template) { + $compiler->subcompile($template); + } } protected function compileTemplate(Twig_Compiler $compiler) { + if (!$this->getAttribute('inline')) { + $compiler->write('compileClassHeader($compiler); if (count($this->getNode('blocks')) || count($this->getNode('traits')) || null === $this->getNode('parent') || $this->getNode('parent') instanceof Twig_Node_Expression_Constant) { @@ -108,10 +121,10 @@ class Twig_Node_Module extends Twig_Node protected function compileClassHeader(Twig_Compiler $compiler) { $compiler - ->write("write("\n\n") // if the filename contains */, add a blank to avoid a PHP parse error ->write("/* ".str_replace('*/', '* /', $this->getAttribute('filename'))." */\n") - ->write('class '.$compiler->getEnvironment()->getTemplateClass($this->getAttribute('filename'))) + ->write('class '.$compiler->getEnvironment()->getTemplateClass($this->getAttribute('filename'), $this->getAttribute('inline'))) ->raw(sprintf(" extends %s\n", $compiler->getEnvironment()->getBaseTemplateClass())) ->write("{\n") ->indent() diff --git a/lib/Twig/Node/SandboxedModule.php b/lib/Twig/Node/SandboxedModule.php index 655efa3..8a9ed31 100644 --- a/lib/Twig/Node/SandboxedModule.php +++ b/lib/Twig/Node/SandboxedModule.php @@ -24,7 +24,9 @@ class Twig_Node_SandboxedModule extends Twig_Node_Module public function __construct(Twig_Node_Module $node, array $usedFilters, array $usedTags, array $usedFunctions) { - parent::__construct($node->getNode('body'), $node->getNode('parent'), $node->getNode('blocks'), $node->getNode('macros'), $node->getNode('traits'), $node->getAttribute('filename'), $node->getLine(), $node->getNodeTag()); + parent::__construct($node->getNode('body'), $node->getNode('parent'), $node->getNode('blocks'), $node->getNode('macros'), $node->getNode('traits'), $node->getNode('inlined_templates'), $node->getAttribute('filename'), $node->getLine(), $node->getNodeTag()); + + $this->setAttribute('inline', $node->getAttribute('inline')); $this->usedFilters = $usedFilters; $this->usedTags = $usedTags; diff --git a/lib/Twig/Parser.php b/lib/Twig/Parser.php index e8db493..7120ea4 100644 --- a/lib/Twig/Parser.php +++ b/lib/Twig/Parser.php @@ -32,6 +32,7 @@ class Twig_Parser implements Twig_ParserInterface protected $importedFunctions; protected $tmpVarCount; protected $traits; + protected $inlinedTemplates = array(); /** * Constructor. @@ -60,7 +61,7 @@ class Twig_Parser implements Twig_ParserInterface * * @return Twig_Node_Module A node tree */ - public function parse(Twig_TokenStream $stream) + public function parse(Twig_TokenStream $stream, $test = null, $dropNeedle = false) { // push all variables into the stack to keep the current state of the parser $vars = get_object_vars($this); @@ -93,7 +94,7 @@ class Twig_Parser implements Twig_ParserInterface $this->importedFunctions = array(array()); try { - $body = $this->subparse(null); + $body = $this->subparse($test, $dropNeedle); if (null !== $this->parent) { if (null === $body = $this->filterBodyNodes($body)) { @@ -108,7 +109,7 @@ class Twig_Parser implements Twig_ParserInterface throw $e; } - $node = new Twig_Node_Module(new Twig_Node_Body(array($body)), $this->parent, new Twig_Node($this->blocks), new Twig_Node($this->macros), new Twig_Node($this->traits), $this->stream->getFilename()); + $node = new Twig_Node_Module(new Twig_Node_Body(array($body)), $this->parent, new Twig_Node($this->blocks), new Twig_Node($this->macros), new Twig_Node($this->traits), new Twig_Node($this->inlinedTemplates), $this->stream->getFilename()); $traverser = new Twig_NodeTraverser($this->env, $this->visitors); @@ -271,6 +272,13 @@ class Twig_Parser implements Twig_ParserInterface return count($this->traits) > 0; } + public function addInlinedTemplate(Twig_Node_Module $template) + { + $template->setInline(count($this->inlinedTemplates) + 1); + + $this->inlinedTemplates[] = $template; + } + public function addImportedFunction($alias, $name, Twig_Node_Expression $node) { $this->importedFunctions[0][$alias] = array('name' => $name, 'node' => $node); diff --git a/lib/Twig/TokenParser/Include.php b/lib/Twig/TokenParser/Include.php index 6725b8f..4a31786 100644 --- a/lib/Twig/TokenParser/Include.php +++ b/lib/Twig/TokenParser/Include.php @@ -32,31 +32,40 @@ class Twig_TokenParser_Include extends Twig_TokenParser { $expr = $this->parser->getExpressionParser()->parseExpression(); + list($variables, $only, $ignoreMissing) = $this->parseArguments(); + + return new Twig_Node_Include($expr, $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); + } + + protected function parseArguments() + { + $stream = $this->parser->getStream(); + $ignoreMissing = false; - if ($this->parser->getStream()->test(Twig_Token::NAME_TYPE, 'ignore')) { - $this->parser->getStream()->next(); - $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, 'missing'); + if ($stream->test(Twig_Token::NAME_TYPE, 'ignore')) { + $stream->next(); + $stream->expect(Twig_Token::NAME_TYPE, 'missing'); $ignoreMissing = true; } $variables = null; - if ($this->parser->getStream()->test(Twig_Token::NAME_TYPE, 'with')) { - $this->parser->getStream()->next(); + if ($stream->test(Twig_Token::NAME_TYPE, 'with')) { + $stream->next(); $variables = $this->parser->getExpressionParser()->parseExpression(); } $only = false; - if ($this->parser->getStream()->test(Twig_Token::NAME_TYPE, 'only')) { - $this->parser->getStream()->next(); + if ($stream->test(Twig_Token::NAME_TYPE, 'only')) { + $stream->next(); $only = true; } - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); - return new Twig_Node_Include($expr, $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); + return array($variables, $only, $ignoreMissing); } /** diff --git a/lib/Twig/TokenParser/Inline.php b/lib/Twig/TokenParser/Inline.php new file mode 100644 index 0000000..9c5a0a5 --- /dev/null +++ b/lib/Twig/TokenParser/Inline.php @@ -0,0 +1,50 @@ +parseArguments(); + + $module = $this->parser->parse($this->parser->getStream(), array($this, 'decideBlockEnd'), true); + $this->parser->addInlinedTemplate($module); + + $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + + return new Twig_Node_Inline($module->getAttribute('filename'), $module->getAttribute('inline'), $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); + } + + public function decideBlockEnd(Twig_Token $token) + { + return $token->test('endinline'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'inline'; + } +} diff --git a/test/Twig/Tests/Fixtures/tags/inline/basic.test b/test/Twig/Tests/Fixtures/tags/inline/basic.test new file mode 100644 index 0000000..46d28f2 --- /dev/null +++ b/test/Twig/Tests/Fixtures/tags/inline/basic.test @@ -0,0 +1,37 @@ +--TEST-- +"inline" tag +--TEMPLATE-- +FOO +{% inline %} + {% extends "foo.twig" %} + + {% block c1 %} + {{ parent() }} + block1extended + {% endblock %} +{% endinline %} + +BAR +--TEMPLATE(foo.twig)-- +A +{% block c1 %} + block1 +{% endblock %} +B +{% block c2 %} + block2 +{% endblock %} +C +--DATA-- +return array() +--EXPECT-- +FOO + +A + block1 + + block1extended + B + block2 +C +BAR diff --git a/test/Twig/Tests/Fixtures/tags/inline/with_extends.test b/test/Twig/Tests/Fixtures/tags/inline/with_extends.test new file mode 100644 index 0000000..b7509f9 --- /dev/null +++ b/test/Twig/Tests/Fixtures/tags/inline/with_extends.test @@ -0,0 +1,54 @@ +--TEST-- +"inline" tag +--TEMPLATE-- +{% extends "base.twig" %} + +{% block c1 %} + {{ parent() }} + blockc1baseextended +{% endblock %} + +{% block c2 %} + {% inline %} + {% extends "foo.twig" %} + + {% block c1 %} + {{ parent() }} + block1extended + {% endblock %} + {% endinline %} +{% endblock %} +--TEMPLATE(base.twig)-- +A +{% block c1 %} + blockc1base +{% endblock %} +{% block c2 %} + block2base +{% endblock %} +B +--TEMPLATE(foo.twig)-- +A +{% block c1 %} + block1 +{% endblock %} +B +{% block c2 %} + block2 +{% endblock %} +C +--DATA-- +return array() +--EXPECT-- +A + blockc1base + + blockc1baseextended + +A + block1 + + block1extended + B + block2 +CB \ No newline at end of file diff --git a/test/Twig/Tests/Node/ModuleTest.php b/test/Twig/Tests/Node/ModuleTest.php index f60f105..592949b 100644 --- a/test/Twig/Tests/Node/ModuleTest.php +++ b/test/Twig/Tests/Node/ModuleTest.php @@ -24,7 +24,7 @@ class Twig_Tests_Node_ModuleTest extends Twig_Tests_Node_TestCase $macros = new Twig_Node(); $traits = new Twig_Node(); $filename = 'foo.twig'; - $node = new Twig_Node_Module($body, $parent, $blocks, $macros, $traits, $filename); + $node = new Twig_Node_Module($body, $parent, $blocks, $macros, $traits, new Twig_Node(array()), $filename); $this->assertEquals($body, $node->getNode('body')); $this->assertEquals($blocks, $node->getNode('blocks')); @@ -62,7 +62,7 @@ class Twig_Tests_Node_ModuleTest extends Twig_Tests_Node_TestCase $traits = new Twig_Node(); $filename = 'foo.twig'; - $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $traits, $filename); + $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $traits, new Twig_Node(array()), $filename); $tests[] = array($node, <<assertEquals($body, $node->getNode('body')); @@ -58,7 +58,7 @@ class Twig_Tests_Node_SandboxedModuleTest extends Twig_Tests_Node_TestCase $traits = new Twig_Node(); $filename = 'foo.twig'; - $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $traits, $filename); + $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $traits, new Twig_Node(array()), $filename); $node = new Twig_Node_SandboxedModule($node, array('for'), array('upper'), array('cycle')); $tests[] = array($node, <<