Skip to content

Kirby 3.9.8

Columns in Kirbytext

KirbyTags are really great for use cases like adding images or links, but they come to a limit if you want to nest KirbyTags or want to add more than just a single line of text.

One such use case is adding columns inside a textarea field. In this recipe, we show you how to solve such uses with KirbyTag Hooks. When we are ready, you can add columns like this:

(columns…)

Left column

++++

Right column

(…columns)

We use a starting and an ending tag, and columns are separated by a ++++ separator. The number of columns is not limited. Just add more ++++ separators for more columns.

Note that this syntax is only one way of solving this and we stick with this for compatibility reasons. On this website, we often use custom HTML tags, for example an <info> tag to create the markup for this info box.

Implementation

KirbyText has a feature called pre and post filters. Pre filters are applied to text before KirbyTags and Markdown is parsed. Post filters are applied afterwards.

The filter plugin

KirbyTag Hooks are callbacks, which can be added to the kirbytags:before or kirbytags:after keys. Those callbacks can receive multiple arguments, here we only need the first parameter $text, which contains the raw text, which can be modified and must be returned.

Our final result looks like this:

/site/plugins/columns/index.php
<?php
Kirby::plugin('kirby/columns', [
  'hooks' => [
    'kirbytags:before' => function ($text, array $data = []) {

      $text = preg_replace_callback('!\(columns(…|\.{3})\)(.*?)\((…|\.{3})columns\)!is', function($matches) use($text, $data) {

        $columns        = preg_split('!(\n|\r\n)\+{4}\s+(\n|\r\n)!', $matches[2]);
        $html           = [];
        $classItem      = $this->option('kirby.columns.item', 'column');
        $classContainer = $this->option('kirby.columns.container', 'columns');

        foreach ($columns as $column) {
            $html[] = '<div class="' . $classItem . '">' . $this->kirbytext($column, $data) . '</div>';
        }

        return '<div class="' . $classContainer . '">' . implode($html) . '</div>';

      }, $text);

      return $text;
    }

  ]
]);

You can simply put this code into /site/plugins/columns/index.php and start using columns in your KirbyText fields. If you want to understand the details, continue reading.

The regular expression voodoo

As the first step to achieve the syntax for the columns, we need to fetch the columns tags:

$text = preg_replace_callback('!\(columns(…|\.{3})\)(.*?)\((…|\.{3})columns\)!is', function($matches) use($text) {
  // do something with the stuff inside the brackets here
}, $text);

Let's examine the regular expression:

!\(columns(…|\.{3})\)(.*)\((…|\.{3})columns\)!is

The exclamation marks define the beginning and the end of the expression.

is at the end makes sure the expression is case insensitive and the s tells the expression to include new lines.

We could simplify the inner part like this:

\(columns…\)(.*)\(…columns\)

The backslashes are there to escape the brackets usually used to group matches, which you can see here: (.*). This little thing translates to: "take everything between the opening columns tag and the closing columns tag and put it in a group".

The final expression contains a bit more complexity to make sure an editor can use either an ellipsis or three dots:

(columns…) or (columns...)

This is achieved with a simple "or" clause, which looks like this:

(…|\.{3})

The \.{3} translates into: "a dot which repeats three times". A dot is another magic character in regular expressions and therefore has to be escaped with the backslash again.

Finally our regular expression fetches what we want and passes the matches to the callback function.

function($matches) use($text) {
  // …
}

The $matches variable is an array with the following content:

  1. the entire match starting with the beginning columns tag and ending with the closing tag
  2. the first "or" group (… or ...)
  3. the content between the tags
  4. the second "or" group (… or ...)

Since arrays start their index at zero, we can get the content between the tags with $matches[2]

Splitting content into columns

Now that we got the content in between the tags, we can look for our separators and split the content into nice pieces for the columns.

$columns = preg_split('!(\n|\r\n)\+{4}\s+(\n|\r\n)!', $matches[2]);

Since we are regular expression experts now, the expression above isn't that complicated anymore. The only new things are \R, which stands for any line break, and \s, which stands for spaces. So the expression above translates to: "Split the content when there's a line break, followed by four plus signs, follwed by one or more spaces, followed by a line break again."

With this, we get a $columns array with text separated into chunks for our columns.

Nested KirbyText

The column tags are only useful if an editor can use KirbyText and Markdown inside of them. To achieve this, we have to manually parse the content for each column as KirbyText.

We can achieve this by passing each column to the $kirby->kirbytext() method ($this in this context references the Kirby object).

$html = [];

foreach ($columns as $column) {
    $html[] = '<div class="' . $classItem . '">' . $this->kirbytext($column, $data) . '</div>';
}

The final HTML

We end up with an $html array with entries for each column. The HTML for each column looks like this:

<div class="column">column content</div>

A simple implode function together with a wrapping div makes everything complete.

return '<div class="columns">' . implode($html) . '</div>';

As a last step, we add configurable class names to the div tags, so that you can set different class names in your config.php file, for example:

/site/config.php
return [
  'kirby.columns.container' => 'grid', // instead of columns
  'kirby.columns.item' => 'grid-item', // instead of column
];

CSS

To make the columns display correctly as columns, we finally need some CSS, in this example, we use the flexbox model, but you can adapt the markup classes and the styles to fit your needs (use floats, inline-blocks, CSS Grid layout…)

.columns {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  margin-left: -1rem;
  margin-right: -1rem;
}

.column {
  flex: 0 1 100%;
  margin-left: 1rem;
  margin-right: 1rem;
  max-width: 100%;
}

@media (min-width: 790px) {
  .column {
    flex: 1;
  }
}

Once you've added the code above to your CSS file for your site, you should be able to see your grid.