Table of contents for Kirby 3

Hello everyone, I need to create a simple table of content based in the headings on a text field. Any one has an experience with this in kirby 3?

I tried with the plugin mentioned here Kirby TOC - Table of contents but is not working on kirby 3.

Thanks!

You can use this field method as a starting point: https://github.com/getkirby/getkirby.com/blob/1bb7a4b42b787066e22e9eb8c21699b9e69c723e/site/plugins/site/fieldMethods.php#L31

On the getkirby.com website we use a snippet that gets the headlines via this method.

You can either do it in the same way, with rendering a snippet in your templates, or, if you prefer using a Kirbytag, create a toc Kirbytags using the method.

Hi @texnixe I will check what you said. Thanks a lot!

Not tested but this may also work.

Depends on what you need, if it’s just one level of headlines like on the getkirby.com website, you don’t need a library but just a few lines of code.

For multiple levels of H-tags, the solution I suggested above won’t work.

Thank you both! I’m “extending” the field method that @texnixe suggested to support different levels and I think it might work.

Maybe a bit late, but I just converted my old Kirby TOC plugin to an independent class. It will work as a starting point for a Kirby plugin, but can also work without Kirby.

Beta

It will work with multiple heading levels.

$toc = new TOC();
$text = $toc->anchorHeadings($text);

echo $toc->list($text); ?>
echo $text;

Thanks! I managed to solve it following @texnixe suggestion. But for sure your solution is going to be useful for others.

How do I implement this into a usable KirbyTag?
I can’t seem to work it out.

EDIT: Have figured it out.

Hi Guido Ferreyra,

Would you mind sharing your implementation? Can’t figure out how to make it work.
Would be much appreciated!

EDIT: Have figured it out.

For anyone interested I’ve updated my repo with changed syntax, much simpler, improved performance and added docs.

$toc = new PHPTableOfContents($html);

// Output the table of contents
echo $toc->list();

// Output the content
echo $toc->html();
2 Likes

@texnixe Can you point me at some doc/examples for implementing this fieldMethods plugin please?

I’m feeling my way into PHP and I think I’ve run into the practical limit of my (limited) abilities … I’ve read the plugin docs, but they seem to be more about creating plugins and less about how to use them in your code.

@mikehargreaves Could you be more specific about what information exactly is missing and what you need?

If you follow the link to the getkirby.com repo, you can see how the method is implemented, you would have to extract this, put it into a plugin file, so that you can then use it in your templates.

The snippet gets called here with the method: https://github.com/getkirby/getkirby.com/blob/1bb7a4b42b787066e22e9eb8c21699b9e69c723e/site/templates/text.php#L10

And the snippet itself: https://github.com/getkirby/getkirby.com/blob/1bb7a4b42b787066e22e9eb8c21699b9e69c723e/site/snippets/toc.php

Let me know if you need more information.

@texnixe The piece I was missing was the (very basic) how to call the plugin methods – is that the right term? – I’d pieced parts of it together looking at the plugin code, but never having written any code that calls a plugin, I wasn’t really sure how to go about it. The examples you provided are perfect, thank you.

Hello all! I was in need of the same, so I did the following (which is not workign at the moment).

Created the toc.php snippet file:

<?php if($item->count() > 3): ?>
  <nav class="toc | -mb:large">

    <h2 class="h4">Table of Contents</h2>

    <ol>
      <?php foreach($item as $headline): ?>
        <li><a href="<?= $headline->url() ?>"><span><?= $headline->text() ?></span></a></li>
      <?php endforeach ?>
    </ol>

  </nav>
<?php endif ?>

I called the snipper in my template:

<?php snippet('toc', $page->text()->headlines()) ?>

Then I created the plugin toc/index.php extracting the code from the repo:

<?php

Kirby::plugin('theme/toc', [

  'headlines' => function($field, $headline = 'h2') {

    preg_match_all('!<' . $headline . '.*?>(.*?)</' . $headline . '>!s', $field->kt()->value(), $matches);

    $headlines = new Collection;

    foreach ($matches[1] as $text) {
      $headline = new Obj([
        'id'   => $id = '#' . Str::slug(Str::unhtml($text)),
        'url'  => $id,
        'text' => trim(strip_tags($text)),
      ]);

      $headlines->append($headline->url(), $headline);

    }

    return $headlines;

  }

]);

I’m assuming this last part I’m not doing it right.

Were you able to solve it?

You have to pass an array of key/value pairs as second argument!

<?php snippet('toc', ['item' => $page->text()->headlines()]) ?>

(I’d use items or headlines here instead of the singular item, because I always find it confusing to loop through a single item, but that’s just personal preference)

Hi! Thanks for replying. I’ve debug mode configured, and I’m getting the following error:

Whoops \ Exception \ ErrorException (E_NOTICE)
Object of class Kirby\Cms\Field could not be converted to int

That’s apparently coming from the same snippet file I copyed above.

Do you have any clue what could it be?

There’s also another issue, because your plugin is missing the name of the extension you want to create.

Have a look at the docs for field methods: https://getkirby.com/docs/reference/plugins/extensions/field-methods

Which reminds me that writing a TOC recipe is next on my list.

Thanks! I’ll take a look and try to solve it. It would also be great to have that recipe to follow a proper way to do it, since I’m just learning and examples help a lot. Thanks again!

Some time ago I ported the wonderful Kirby 2 code from @jenstornell (PHP Table of Contents: https://forum.getkirby.com/t/table-of-contents-for-kirby-3/13780/11) to Kirby 3.

In the following I show the main parts. Because of the CSS code and the license file, I refer to the Kirby 2 code in the above link to @jenstornell’s code on GitHub.

Install

Create a new directory “site\plugins\jenstornell_toc_k3\” with the file “site\plugins\jenstornell_toc_k3\index.php” like:

<?php // site\plugins\jenstornell_toc_k3\index.php
      // based on https://github.com/jenstornell/php-table-of-contents/blob/master/example/example.php, version 1.2
      // adapted for K3 by HeinerEF - 07.-11.01.2020

@include_once __DIR__ . '/src/table-of-contents.php';

Kirby::plugin('jenstornell/php-table-of-contents', [
  'hooks' => [
    'kirbytext:after' => function ($text) {
      $mypos = (stripos($text, '(toc: )') === FALSE);
      if (!($mypos)) {
        $toc = new PHPTableOfContents($text);
        $text = preg_replace('#<p>\(toc: \)</p>#i', "\n" . '      <div class="toc">'
              . "\n" . '        <p>Table of contents of the page:</p>'
              . $toc->list() . '      </div>' . "\n", $toc->html());
      };
      return $text;
    }
  ]
]);

Create a new directory “site\plugins\jenstornell_toc_k3\src\” with the file “site\plugins\jenstornell_toc_k3\src\table-of-contents.php” like:

<?php // site\plugins\jenstornell_toc_k3\src\table-of-contents.php
      // based on https://github.com/jenstornell/php-table-of-contents/blob/master/src/table-of-contents.php, version 1.2
      // some changes by HeinerEF for K3 - 07.-10.01.2020 (proper tag hierarchy from h2 to h6)
      // use "Str::slug(  )" from K3 by HeinerEF

class PHPTableOfContents {
  private $headings;
  private $html;

  public function __construct($html) {
    $this->html = $html;
    $this->setHeadings();
  }

  // Html
  public function html() {
    $html = $this->html;
    $matches = $this->headings;
    foreach($matches[1] as $index => $item) {
      $html = str_replace(
        '>' . $item . '</h',
        '><a id="' . $matches[2][$index] . '"></a>' . $item . ' </h', // 25.04.2020 by HeinerEF
        $html
      );
    }
    return $html;
  }

  // Generate table of content nested list
  // 07.-10.01.2020 changes by HeinerEF (proper tag hierarchy from h2 to h6)
  public function list() {
    $out = '';
    $old_depth = 0;
    $matches = $this->headings;
    foreach($matches[1] as $key => $item) {
      $depth = substr($matches[0][$key], 2, 1) - 2;
      if($old_depth > $depth) {
        while ($old_depth > $depth) {
          $old_depth--;
          $out .= "\n          </ol><!-- #".($old_depth+2)." -->";
        };
      } elseif($old_depth < $depth) {
        $old_depth++;
        $out .= "\n          <li>\n";
        $out .=   "          <ol><!-- #".($old_depth+1)." -->";
      };
      $out .= sprintf("
          <li>
            <span></span>
            <a href='#%s'>%s</a>
          </li>", $matches[2][$key], $item);
    }
    while ($old_depth > 0) {
      $old_depth--;
      $out .= "\n          </ol><!-- #".($old_depth+2)." -->";
    };
    return "\n        <ol><!-- #1 -->" . $out . "\n        </ol><!-- #1 -->\n";
  }

  // Set headings
  private function setHeadings() {
    preg_match_all('|<h[^>]+>(.*)</h[^>]+>|iU', $this->html, $matches);
    $slugs = [];
    foreach($matches[1] as $item) {
      $slugs[] = Str::slug($item);  // changed by HeinerEF using K3
    }
    $this->headings = $matches;
    $this->headings[2] = $slugs;
  }
}

How to use

In the Panel field add three extra lines


(toc: )

where you want to have the TOC.

Remark:

Of course, with this technique it is necessary for all headings to be different in the text for the links to work.

Good luck!