smarty-php / smarty

Smarty is a template engine for PHP, facilitating the separation of presentation (HTML/CSS) from application logic.
Other
2.26k stars 712 forks source link

Compile-time block tag #1064

Closed weary-adventurer closed 2 months ago

weary-adventurer commented 2 months ago

I would like to have a way to process a block tag at compile time. For example, to highlight a code snippet:

$smarty->registerPlugin("block", "highlight", function ($params, $content, $template, &$repeat) {
    if (!$repeat) {
        $hl = new \Highlight\Highlighter();
        $result = $hl->highlight($params["lang"], $content);
        return "<pre><code class=\"hljs $result->language\">$result->value</code></pre>";
    }
});
{highlight lang=html}
<script>
var x = 1 + 2;
</script>
{/highlight}

Currently this runs at runtime even thought the code is constant. It is possible to do it with a compile-time inline tag:

$smarty->registerPlugin("compiler", "highlight", function ($params, $smarty) {
    $hl = new \Highlight\Highlighter();
    $result = $hl->highlight($params["lang"], file_get_contents($params["file"]));
    return "<pre><code class=\"hljs $result->language\">$result->value</code></pre>";
});
{highlight file=test.html lang=html}

But for some reason, there is no compile-time block tag. I think there must be a "blockcompiler" so it's possible to preprocess arbitrary content.

wisskid commented 2 months ago

In Smarty v5, you should be able to create a TagCompiler that implements \Smarty\Compile\CompilerInterface and return it from getTagCompiler in your custom Extension. Or actually two, one that handles highlight and one that handles highlightclose.

weary-adventurer commented 2 months ago

Thanks! Can you make a simple example that shows how to get attributes and content between opening and closing tags? I've looked at the \Smarty\Compile\Base usages for tags but none of them seem to be getting the contents directly.

I assume it will look something like this:

<?php

class HighlightTag extends \Smarty\Compile\Base {
    public function compile($args, $compiler, $parameter = [], $tag = null, $function = null) {
        // ...
    }
}

class HighlightCloseTag extends \Smarty\Compile\Base {
    public function compile($args, $compiler, $parameter = [], $tag = null, $function = null) {
        // TODO: Get attributes from {highlight lang=xyz}
        // TODO: Get content between {highlight}xyz{/highlight}
        // TODO: Return processed content
    }
}

class HighlightExtension extends \Smarty\Extension\Base {
    public function getTagCompiler(string $tag): \Smarty\Compile\CompilerInterface {
        switch ($tag) {
            case "highlight": return new HighlightTag();
            case "highlightclose": return new HighlightCloseTag();
        }
        return null;
    }
}

$smarty->addExtension(new HighlightExtension());

?>
wisskid commented 2 months ago

Wow, turns out this was a bit harder than I expected, but this seems to do the trick:

<?php

require 'vendor/autoload.php';
use Smarty\Smarty;

class HighlightTag extends \Smarty\Compile\Base {
    public function compile($args, $compiler, $parameter = [], $tag = null, $function = null): string
    {

        $this->openTag(
            $compiler,
            'highlight',
            [
                $this->getAttributes($compiler, $args),
                $compiler->getParser()->current_buffer,
            ]
        );

        // Init temporary context
        $compiler->getParser()->current_buffer = new \Smarty\ParseTree\Template();
        return '';
    }
}

class HighlightCloseTag extends \Smarty\Compile\Base {
    public function compile($args, $compiler, $parameter = [], $tag = null, $function = null): string
    {
        $saved_data = $this->closeTag($compiler, ['highlight']);

        $_attr = $saved_data[0];
        // maybe use the attribute here

        $blockCode = $compiler->getParser()->current_buffer;

        // setup buffer for template function code
        $template = new \Smarty\ParseTree\Template();

        $template->append_subtree($compiler->getParser(), new \Smarty\ParseTree\Tag($compiler->getParser(),
                "<div style='background-color:yellow'>"
        ));
        $template->append_subtree($compiler->getParser(),
            $blockCode
        );
        $template->append_subtree($compiler->getParser(), new \Smarty\ParseTree\Tag($compiler->getParser(),
            "</div>"
        ));

        $output = $template->to_smarty_php($compiler->getParser());

        // restore old buffer
        $compiler->getParser()->current_buffer = $saved_data[1];
        return $output;
    }
}

class HighlightExtension extends \Smarty\Extension\Base {
    public function getTagCompiler(string $tag): ?\Smarty\Compile\CompilerInterface {
        switch ($tag) {
            case "highlight": return new HighlightTag();
            case "highlightclose": return new HighlightCloseTag();
        }
        return null;
    }
}

$smarty = new Smarty();
$smarty->addExtension(new HighlightExtension());
$smarty->display('string: BEFORE {highlight} my <bold>attempt</bold> dfsdf {/highlight} AFTER');
wisskid commented 2 months ago

Note that this relies heavily on internal Smarty methods that might very well change in the future. @weary-adventurer

wisskid commented 2 months ago

@weary-adventurer PS2: $blockCode is a parse tree, not a plain string. It might contain (parsed) smarty tags, for example. You cannot just run in through Highlight\Highlighter.

weary-adventurer commented 2 months ago

Thanks, this is pretty good. Would it be possible to wrap it into a "blockcompiler" plugin or plugins are no longer supported and it's now done with extensions? There are already "block" and "compiler" plugins that are very easy to use and it makes sense to add a "blockcompiler" as well.

Either way, I think it would be a good idea to add this example to documentation.

wisskid commented 2 months ago

Extensions are the preferred way now, but since this depends so heavily on internal smarty methods, end because of PS2 above, I don't really feel like documenting it.