@HeinerEF Thanks for your reply! I’m coming back to this after a long while. I understand that, in how to use:
(toc: )
That means as part of the text content that you can type in the field, right? Have you also used it as part of a template? For example for a post page.
To be sure. I’m struggling with the same issue.
At point 7: 07. Alternative: A field method to generate the ToC
I want to make use of this snippet inside my template:
You can either use a kirbytag (toc) to create the ToC in a textarea field, then you need the kirbytext:after hook, or you fetch the headlines via the field method in a template, then you need the field method. In any case you
Could I combine it with the KirbyText :after hook recipe plugin? I have a page with a blocks heading and a markdown block, which has headings as KirbyText, all in one page.
If I add your mentioned code line to the plugin file I get an error “Undefined variable: page”.
As there is no hook for blocks I tried to do this but nothing happens regarding the anchor links:
<?php
function($text) {
// get the headline levels to convert from a config option, we use h2 as the default
$headings = $page->blocksfield()->toBlocks()->filterBy('type', 'h2');
// create the regex pattern to be used as first argument in `preg_replace_callback()`
$headingsPattern = is_array($headings) ? implode('|', $headings) : $headings;
// use `preg_replace_callback()` to replace matches with anchors
$text = preg_replace_callback('!<(' . $headingsPattern . ')>(.*?)</\\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;
}; ?>
<?= $page->text()->toBlocks() ?>
<?php foreach ($page->blocks()->toBlocks() as $block):
if(in_array($block->type(), $applyTo)):
echo $text;
else: ?>
<div id="<?= $block->id() ?>" class="block block-type-<?= $block->type() ?>">
<?= $block ?>
</div>
<?php endif;?>
<?php endforeach ?>
I want to combine the KirbyText headings hook with the blocks headlines logic but for blocks Kirby has no :after hook and therefore I tried to emulate it. I got the idea and code from GitHub - sylvainjule/kirby-footnotes: Footnotes plugin for Kirby 3
The goal is to even show a complete Table of Content if a mix of blocks headings and Markdown within Blocks headings (KirbyText) is used in a post.
Could you add that part to your cookbook recipe @texnixe ?
Hm, I don’t know. I think this is going to be rather messy. So heading can be in a heading block, a markdown field and even in a writer field or maybe even a list field. Would probably be the best to extract the headlines from the completely rendered HTML page.
I don’t think it makes sense to add that to the current recipe. Maybe it would make more sense if someone creates a plugin for that.
In my personal opinion, it somehow defeats the purpose of having a blocks field and then mix it with markdown or even heading nodes in a writer field. Yes, all great flexibility wise, but it makes things like adding a ToC more difficult.
I’ve got some issues with the “Of anchors and ToCs, part 1” in CookBook. The first step works (Replace headlines with anchors) with the code :
<?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;
},
]
]
]);
But then, I’m not sure how to add this first part to the second part for Generate the ToC
<?php
use Kirby\Toolkit\Collection;
use Kirby\Toolkit\Str;
use Kirby\Toolkit\Obj;
Kirby::plugin('k-cookbook/toc', [
'hooks' => [
'kirbytext:after' => [
// (…) add the callback function from above here (without the wrapper)
function($text) {
// the pattern allows passing an optional headline level `(toc: h3)`
$pattern = '!\(toc(?::\s?(h[1-6]))?\)!';
$text = preg_replace_callback($pattern, function($match) use($text) {
// get the headline level from the match
$headline = $match[1] ?? 'h2';
// find all headline matches
preg_match_all('!<' . $headline . '.*?>(.*?)</' . $headline . '>!s', $text, $matches);
// create a new collection for the headlines…
$headlines = new Collection();
// …and add all matches
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 the html for the ToC
return snippet('toc', ['headlines' => $headlines], false);
}, $text);
return $text;
},
],
],
'snippets' => [
'toc' => __DIR__ . '/snippets/toc.php'
],
]);
I’m working on the starterkit on /content/notes page, has anyone done this before ?
Inside the 'kirbytext:after' array, you need the two functions. Note that both callbacks are supposed to work with a textarea field (which is not used anywhere in the Starterkit )
I probably made a mistake, because it still doesn’t work
index.php
<?php
use Kirby\Toolkit\Collection;
use Kirby\Toolkit\Str;
use Kirby\Toolkit\Obj;
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;
},
function($texto) {
// the pattern allows passing an optional headline level `(toc: h3)`
$pattern = '!\(toc(?::\s?(h[1-6]))?\)!';
$texto = preg_replace_callback($pattern, function($match) use($texto) {
// get the headline level from the match
$headline = $match[1] ?? 'h2';
// find all headline matches
preg_match_all('!<' . $headline . '.*?>(.*?)</' . $headline . '>!s', $texto, $matches);
// create a new collection for the headlines…
$headlines = new Collection();
// …and add all matches
foreach ($matches[1] as $texto) {
$headline = new Obj([
'id' => $id = '#' . Str::slug(Str::unhtml($texto)),
'url' => $id,
'texto' => trim(strip_tags($texto)),
]);
$headlines->append($headline->url(), $headline);
}
// return the html for the ToC
return snippet('toc', ['headlines' => $headlines], false);
}, $texto);
return $texto;
},
],
],
'snippets' => [
'toc' => __DIR__ . '/snippets/toc.php'
],
]);
Toc.php
<?php if ($headlines->isNotEmpty()) : ?>
<nav class="toc">
<h2>Table of Contents</h2>
<ol>
<?php foreach($headlines as $headline): ?>
<li><a href="<?= $headline->url() ?>"><?= $headline->texto() ?></a></li>
<?php endforeach ?>
</ol>
</nav>
<?php endif ?>
Yes I add a textarea field in note.yml, I saw later Extra: ToC from blocks field at the end of the document, I’ll try to do it in second time