Sorry, we don't support your browser.  Install a modern browser

Syntax improvements to KirbyTags#262

I think the KirbyTag syntax is clever, but maybe it can be dropped in favor of the curly bracket one?

In #518 of the old ideas repo, I suggested a field method that allows us to perform template substitutions. This resulted in the $field->replace() method, which allows editors to write this:

View my new {​{​ site.title }} website (link: # text: here)!

…and if you use $field->replace() in your template, the site title will appear in the text.

Couldn’t we use this syntax for KirbyTags as well? Like so:

View my new {​{​ site.title }} website {​{​ link: # text: here }}!


  • It’s a lot more common and easily recognized
  • It’s more obvious and pops out when you have a lot of text, making it easier to see which parts are dynamic
  • You wouldn’t have two separate syntaxes, just one, resulting in less complexity
  • It’s a simpler syntax, meaning it’d be easier to implement and could even have better performance

This last part is most important because it will make it easier to have nested KirbyTags:

Check out {​{​ link: shop text: our latest {​{​ date: year }} products }}!

This would be extra useful when it comes to custom KirbyTags.

If the replace() method merges into kirbytext(), you could pass the dynamic data like this:

echo $page->myField()->kirbytext([
    'foo' => 42

…and you use foo in the panel like this:

My field content uses {​{​ site.title }}, but also {​{​ foo }}
and {​{​ link: # text: an example link }}.

It’d be awesome to give all of that power to the editor with a single method call.

2 months ago

I really like the idea! I haven’t thought this through yet, but one issue I thought about was how Kirby can reliably detect whether the tag uses the query language or the KirbyTag language (the implementation of both is vastly different and needs to stay separate because of the different use-case). Just checking for a colon is likely not going to work.

2 months ago

Just checking for a colon is risky, yes, but you could check for the KirbyTag name as well, as it’s also required.

For example, if you have:

{​{​":") }}
{​{​ link: text: foo }}

You first run a low-cost regex like \{\{[^}]+\}\} that just matches anything enclosed in braces. Or, if we’re going to have nested KirbyTags support, a slightly more invloved approach where you count {​{​ and }} pairs.

Then, you pass whatever’s in the braces to this regex ^\s*[a-z]+: which will match this:

{​{​ link: text: foo }}
{​{​ link: # }}
{​{​ link: }}

…but not this:

{​{​ site.find('notes').foo(':') }}
{​{​ }}
{​{​ link }}

If it matches - it’s a KirbyTag. If not - it’s a query.

2 months ago

As an addendum, I’d also like to propose a change to the tag value delimiters. Currently, if you have:

(mytag: value attr: test)

There’s no delimiter between value and attr. I personally find this to be more readable:

(mytag: value; attr: test)

This would also remove the need to rely on the available tag attributes to determine what’s a key and what’s a value. If you have a tag foo with no specified attr in its config and you have this content:

(foo: bar: baz)

…you’ll get "bar: baz" as $tag->value. However, if the tag implementation changes and bar becomes one of its available attributes, $tag->value will now be empty and you’ll have $tag->bar equal "baz" instead. In other words, what used to be the value of the tag suddenly turns to an attribute and all previous content would break.

On the other hand, if you rely on ; to separate attributes, you don’t need to know all the possible attributes of a tag in order to get the values. This would make the syntax slightly more complex, but more readable, reliable, and easier to implement. If you have this:

(foo: bar: baz)

…you always have a tag with value "bar: baz". If the developer later introduces a new attribute to that tag, nothing breaks. You simply use the new attribute like this:

(foo: bar: baz; bar: baz)

…which results in $tag->value set to "bar: baz" and $tag->bar set to "baz". Or, with the new proposed syntax:

{​{​ foo: bar: baz; bar: baz }}

If the tag is not an inline element (like a link) and is a full-width element (like an image), you could split it to multiple lines for even better readability:

image: myawesomepicture.jpg;
alt: This is an awesome picture;
title: I took this image in the park;
2 months ago
Changed the title from "Switch to handlebars syntax for KirbyTags" to "Syntax improvements to KirbyTags"
2 months ago

I’m not sure about the semicolon field separators to be honest. I see your points and agree with you on those. On the other hand I see the following disadvantages:

  • The more we formalize the syntax, the harder it will be to learn for beginners, especially for non-technical editors.
  • It would make the semicolon another special character that then cannot be used anymore in the field content without escaping. However it is often used in sentences, especially in typical KirbyTag use-cases like picture descriptions.
2 months ago

I don’t think that adding more makes it harder to learn. It just makes it clearer what it does, which can make it easier to learn. For example:

(link: shop text: our latest products)

When I first read this in my head, I say link: shop text even though text doesn’t belong to the link part. In regular text, you use punctuation to separate (or group) parts in the sentence that belong together. Classic example:

Let’s eat, John.


Let’s eat John.

Could you imagine reading a book with no punctuation?

As for the separator, you could use a pipe | since it’s rare enough. It’s also taller than other characters, making it stand out even more:

{​{​ link: shop | text: our latest products }}

or maybe even two slashes?

{​{​ link: shop // text: our latest products }}

As usual, I’m just shooting ideas. The separator character is really up to opinion, but my main point is that having one will make the syntax easier, not harder. If not for the editor, then for anyone who reads it.

2 months ago

Not having to know all possible tag attributes upfront will also allow us to have non-formal KirbyTags that are not defined as a plugin, but rather in the kirbytext() call itself:

We are sponsored by {​{​ logo: kirby.svg }}
echo $page->myField()->kirbytext([
    'logo' => function ($filename) use ($page) {
        if ($file = $page->file($filename)) {
            return Html::img($file->url());

Here, kirby.svg is passed as $filename to the closure function. This can be useful for tiny features where an entire plugin would be overkill.

2 months ago

If nested KirbyTags are supported, you could go nuts:

We are sponsored by {​{​ logo: {​{​ page.sponsorLogo.toFile }} }}
2 months ago