Ok, so I am trying to follow @texnixe guide on creating a table of contents as I want to understand plugins more. Now I am probably being stupid here but I can’t get it to work, well I can in some parts.
I have created a site>plugins>toc folder which contains an index.php
Also, create a site>plugins>toc>snippets folder with a file called toc.php
For the main index.php file I have added the following. A hook to create the anchors which works fine. And I am using the field method to generate to TOC.
<?php
Kirby::plugin('k-cookbook/toc', [
'hooks' => [
'kirbytext:after' => [
function($text) {
// get the headline levels to convert from a config option, we use h2 as the default
$headlines = option('k-cookbook.toc.headlines', 'h2|h3');
// create the regex pattern to be used as first argument in `preg_replace_callback()`
$headlinesPattern = is_array($headlines) ? implode('|', $headlines) : $headlines;
// use `preg_replace_callback()` to replace matches with anchors
$text = preg_replace_callback('!<(' . $headlinesPattern . ')>(.*?)</\\1>!s', function ($match) {
// create the id from the headline text
$id = Str::slug(Str::unhtml($match[2]));
// return the modified headline:
// $match[1] contains the match for the first subpattern, i.e. `h2`, `h3` etc.
// $match[2] contains the match for the second subpattern, i.e. the actual headline text
return '<' . $match[1] . ' id="' . $id . '"><a href="#' . $id . '">' . $match[2] . '</a></' . $match[1] . '>';
}, $text);
return $text;
},
],
'fieldMethods' => [
'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;
}
]
]
]);
The anchors are working fine but no TOC, I am being a noob I know it, probably not understanding what I am doing. Happy to be embarrassed but someone please put me out of my misery as to what I am doing wrong.
Apologies, I had not expanded that in the file before I copied.
<?php
Kirby::plugin('k-cookbook/toc', [
'hooks' => [
'kirbytext:after' => [
function($text) {
// get the headline levels to convert from a config option, we use h2 as the default
$headlines = option('k-cookbook.toc.headlines', 'h2|h3');
// create the regex pattern to be used as first argument in `preg_replace_callback()`
$headlinesPattern = is_array($headlines) ? implode('|', $headlines) : $headlines;
// use `preg_replace_callback()` to replace matches with anchors
$text = preg_replace_callback('!<(' . $headlinesPattern . ')>(.*?)</\\1>!s', function ($match) {
// create the id from the headline text
$id = Str::slug(Str::unhtml($match[2]));
// return the modified headline:
// $match[1] contains the match for the first subpattern, i.e. `h2`, `h3` etc.
// $match[2] contains the match for the second subpattern, i.e. the actual headline text
return '<' . $match[1] . ' id="' . $id . '"><a href="#' . $id . '">' . $match[2] . '</a></' . $match[1] . '>';
}, $text);
return $text;
},
],
'fieldMethods' => [
'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;
}
],
'snippets' => [
'toc' => __DIR__ . '/snippets/toc.php'
],
]
]);
The index.php seems to have an error, make sure that all arrays etc are closed in the right places… That’s why the snippet is not properly registered At least if your real code is what you posted here.
It does appear to be related to the snippet, even if I remove the if statement around the table of contents in toc.php still nothing, I thought maybe the issue was that it couldn’t find any headlines.
Oh well, nevermind, will spend a bit more time on it before throwing the towel in.
Copy the snippet into /site/snippets to see if that works.
This is what you index.php should look like
<?php
Kirby::plugin('k-cookbook/toc', [
'hooks' => [
'kirbytext:after' => [
function($text) {
// get the headline levels to convert from a config option, we use h2 as the default
$headlines = option('k-cookbook.toc.headlines', 'h2|h3');
// create the regex pattern to be used as first argument in `preg_replace_callback()`
$headlinesPattern = is_array($headlines) ? implode('|', $headlines) : $headlines;
// use `preg_replace_callback()` to replace matches with anchors
$text = preg_replace_callback('!<(' . $headlinesPattern . ')>(.*?)</\\1>!s', function ($match) {
// create the id from the headline text
$id = Str::slug(Str::unhtml($match[2]));
// return the modified headline:
// $match[1] contains the match for the first subpattern, i.e. `h2`, `h3` etc.
// $match[2] contains the match for the second subpattern, i.e. the actual headline text
return '<' . $match[1] . ' id="' . $id . '"><a href="#' . $id . '">' . $match[2] . '</a></' . $match[1] . '>';
}, $text);
return $text;
},
],
],
'fieldMethods' => [
'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;
}
],
'snippets' => [
'toc' => __DIR__ . '/snippets/toc.php'
],
]);
So I use Sublime Text 3, I checked the closures in index.php several times, all looked fine, no errors were reported. Got annoyed, cried a little, stamped my feet and then for some reason decided to open the file in VSCode. Boom! The first thing I see, a closure issue, staring right at me…
fixed that and table of contents working.
Really sorry @texnixe, no excuse for wasting your time on that one.
It will need changes, yes, but getting the headings from blocks is actually very easy if you use heading blocks. Because you can filter blocks by type.
To add the anchors, you can then just modify the heading block snippets.
So if you stick to heading blocks, you don’t need a plugin, just the adapted heading snippets and a snippet to output the TOC.
It will be more complicated if you use headings inside the writer field.
Can I jump in here? I would like to use a ToC block, but only for long blogposts (actually just 1 post, so I am tempted to just do it manually in HTML inside a textarea, but once you have a tool you actually need it more often, so maybe a block would be nice).
So, could you elaborate on what you said here?
Where do I filter the blocks by type? And probably: How do I filter them?
I have the headings block already extended with an ID field. Is that enough for the anchors (I guess so)
You say I need a snippet to output the ToC, but don’t you mean a “block”, not a snippet? I mean I have to insert that ToC on the page and how would a snippet do that when you don’t need a ToC on every blogpost, but just long ones?
Yes, I mean a snippet that you would call in your template where you want to show the ToC. If it shouldn’t be shown for each blog post, you can do this conditionally, like if the post has more than x headlines or you can use a checkbox/toggle in the post itself (Show ToC?).