Skip to content

Commit 229745a

Browse files
[TableOfContents] Add a label option
By default no label is shown, but if one is set it is added as a `<strong>` before the table of contents, and the table of contents and label are wrapped together in a `<div>`.
1 parent 0d41ced commit 229745a

File tree

10 files changed

+248
-6
lines changed

10 files changed

+248
-6
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the league/commonmark package.
7+
*
8+
* (c) Colin O'Dell <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace League\CommonMark\Extension\TableOfContents\Node;
15+
16+
use League\CommonMark\Exception\InvalidArgumentException;
17+
use League\CommonMark\Node\Block\AbstractBlock;
18+
19+
final class TableOfContentsWrapper extends AbstractBlock
20+
{
21+
public function getInnerToc(): TableOfContents
22+
{
23+
$children = $this->children();
24+
if (! \is_array($children)) {
25+
/** @psalm-suppress NoValue */
26+
$children = \iterator_to_array($children);
27+
}
28+
29+
if (\count($children) !== 2) {
30+
throw new InvalidArgumentException(
31+
'TableOfContentsWrapper nodes should have 2 children, found ' . \count($children)
32+
);
33+
}
34+
35+
$inner = $children[1];
36+
if (! $inner instanceof TableOfContents) {
37+
throw new InvalidArgumentException(
38+
'TableOfContentsWrapper second node should be a TableOfContents, found ' . \get_class($inner)
39+
);
40+
}
41+
42+
return $inner;
43+
}
44+
}

src/Extension/TableOfContents/TableOfContentsBuilder.php

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
namespace League\CommonMark\Extension\TableOfContents;
1515

1616
use League\CommonMark\Event\DocumentParsedEvent;
17+
use League\CommonMark\Exception\InvalidArgumentException;
1718
use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
1819
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink;
1920
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
2021
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder;
22+
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsWrapper;
23+
use League\CommonMark\Node\Block\AbstractBlock;
2124
use League\CommonMark\Node\Block\Document;
2225
use League\CommonMark\Node\NodeIterator;
2326
use League\Config\ConfigurationAwareInterface;
@@ -43,6 +46,7 @@ public function onDocumentParsed(DocumentParsedEvent $event): void
4346
(int) $this->config->get('table_of_contents/min_heading_level'),
4447
(int) $this->config->get('table_of_contents/max_heading_level'),
4548
(string) $this->config->get('heading_permalink/fragment_prefix'),
49+
(string) $this->config->get('table_of_contents/label'),
4650
);
4751

4852
$toc = $generator->generate($document);
@@ -54,7 +58,11 @@ public function onDocumentParsed(DocumentParsedEvent $event): void
5458
// Add custom CSS class(es), if defined
5559
$class = $this->config->get('table_of_contents/html_class');
5660
if ($class !== null) {
57-
$toc->data->append('attributes/class', $class);
61+
if ($toc instanceof TableOfContentsWrapper) {
62+
$toc->getInnerToc()->data->append('attributes/class', $class);
63+
} else {
64+
$toc->data->append('attributes/class', $class);
65+
}
5866
}
5967

6068
// Add the TOC to the Document
@@ -70,8 +78,20 @@ public function onDocumentParsed(DocumentParsedEvent $event): void
7078
}
7179
}
7280

73-
private function insertBeforeFirstLinkedHeading(Document $document, TableOfContents $toc): void
81+
/**
82+
* @psalm-param TableOfContents|TableOfContentsWrapper $toc
83+
*
84+
* @phpstan-param TableOfContents|TableOfContentsWrapper $toc
85+
*/
86+
private function insertBeforeFirstLinkedHeading(Document $document, AbstractBlock $toc): void
7487
{
88+
// @phpstan-ignore booleanAnd.alwaysFalse
89+
if (! $toc instanceof TableOfContents && ! $toc instanceof TableOfContentsWrapper) {
90+
throw new InvalidArgumentException(
91+
'Toc should be a TableOfContents or TableOfContentsWrapper, got ' . \get_class($toc)
92+
);
93+
}
94+
7595
foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) {
7696
if (! $node instanceof Heading) {
7797
continue;
@@ -87,8 +107,20 @@ private function insertBeforeFirstLinkedHeading(Document $document, TableOfConte
87107
}
88108
}
89109

90-
private function replacePlaceholders(Document $document, TableOfContents $toc): void
110+
/**
111+
* @psalm-param TableOfContents|TableOfContentsWrapper $toc
112+
*
113+
* @phpstan-param TableOfContents|TableOfContentsWrapper $toc
114+
*/
115+
private function replacePlaceholders(Document $document, AbstractBlock $toc): void
91116
{
117+
// @phpstan-ignore booleanAnd.alwaysFalse
118+
if (! $toc instanceof TableOfContents && ! $toc instanceof TableOfContentsWrapper) {
119+
throw new InvalidArgumentException(
120+
'Toc should be a TableOfContents or TableOfContentsWrapper, got ' . \get_class($toc)
121+
);
122+
}
123+
92124
foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) {
93125
// Add the block once we find a placeholder
94126
if (! $node instanceof TableOfContentsPlaceholder) {

src/Extension/TableOfContents/TableOfContentsExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use League\CommonMark\Extension\ConfigurableExtensionInterface;
2121
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
2222
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder;
23+
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsWrapper;
2324
use League\Config\ConfigurationBuilderInterface;
2425
use Nette\Schema\Expect;
2526

@@ -35,12 +36,14 @@ public function configureSchema(ConfigurationBuilderInterface $builder): void
3536
'max_heading_level' => Expect::int()->min(1)->max(6)->default(6),
3637
'html_class' => Expect::string()->default('table-of-contents'),
3738
'placeholder' => Expect::anyOf(Expect::string(), Expect::null())->default(null),
39+
'label' => Expect::anyOf(Expect::string(), Expect::null())->default(null),
3840
]));
3941
}
4042

4143
public function register(EnvironmentBuilderInterface $environment): void
4244
{
4345
$environment->addRenderer(TableOfContents::class, new TableOfContentsRenderer(new ListBlockRenderer()));
46+
$environment->addRenderer(TableOfContentsWrapper::class, new TableOfContentsWrapperRenderer());
4447
$environment->addEventListener(DocumentParsedEvent::class, [new TableOfContentsBuilder(), 'onDocumentParsed'], -150);
4548

4649
// phpcs:ignore SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed

src/Extension/TableOfContents/TableOfContentsGenerator.php

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@
1818
use League\CommonMark\Extension\CommonMark\Node\Block\ListData;
1919
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
2020
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
21+
use League\CommonMark\Extension\CommonMark\Node\Inline\Strong;
2122
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink;
2223
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
24+
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsWrapper;
2325
use League\CommonMark\Extension\TableOfContents\Normalizer\AsIsNormalizerStrategy;
2426
use League\CommonMark\Extension\TableOfContents\Normalizer\FlatNormalizerStrategy;
2527
use League\CommonMark\Extension\TableOfContents\Normalizer\NormalizerStrategyInterface;
2628
use League\CommonMark\Extension\TableOfContents\Normalizer\RelativeNormalizerStrategy;
29+
use League\CommonMark\Node\Block\AbstractBlock;
2730
use League\CommonMark\Node\Block\Document;
31+
use League\CommonMark\Node\Inline\Text;
2832
use League\CommonMark\Node\NodeIterator;
2933
use League\CommonMark\Node\RawMarkupContainerInterface;
3034
use League\CommonMark\Node\StringContainerHelper;
@@ -54,20 +58,30 @@ final class TableOfContentsGenerator implements TableOfContentsGeneratorInterfac
5458
/** @psalm-readonly */
5559
private string $fragmentPrefix;
5660

57-
public function __construct(string $style, string $normalizationStrategy, int $minHeadingLevel, int $maxHeadingLevel, string $fragmentPrefix)
61+
/** @psalm-readonly */
62+
private string $label;
63+
64+
public function __construct(string $style, string $normalizationStrategy, int $minHeadingLevel, int $maxHeadingLevel, string $fragmentPrefix, string $label = '')
5865
{
5966
$this->style = $style;
6067
$this->normalizationStrategy = $normalizationStrategy;
6168
$this->minHeadingLevel = $minHeadingLevel;
6269
$this->maxHeadingLevel = $maxHeadingLevel;
6370
$this->fragmentPrefix = $fragmentPrefix;
71+
$this->label = $label;
6472

6573
if ($fragmentPrefix !== '') {
6674
$this->fragmentPrefix .= '-';
6775
}
6876
}
6977

70-
public function generate(Document $document): ?TableOfContents
78+
/**
79+
* If there is a table of contents, returns either a `TableOfContents` or
80+
* `TableOfContentsWrapper` node object
81+
*
82+
* @psalm-return TableOfContents|TableOfContentsWrapper
83+
*/
84+
public function generate(Document $document): ?AbstractBlock
7185
{
7286
$toc = $this->createToc($document);
7387

@@ -111,6 +125,18 @@ public function generate(Document $document): ?TableOfContents
111125
return null;
112126
}
113127

128+
if ($this->label !== '') {
129+
$label = new Strong();
130+
$label->appendChild(new Text($this->label));
131+
$wrapper = new TableOfContentsWrapper();
132+
$wrapper->appendChild($label);
133+
$wrapper->appendChild($toc);
134+
$wrapper->setStartLine($toc->getStartLine());
135+
$wrapper->setEndLine($toc->getEndLine());
136+
137+
return $wrapper;
138+
}
139+
114140
return $toc;
115141
}
116142

src/Extension/TableOfContents/TableOfContentsGeneratorInterface.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,17 @@
1414
namespace League\CommonMark\Extension\TableOfContents;
1515

1616
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
17+
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsWrapper;
18+
use League\CommonMark\Node\Block\AbstractBlock;
1719
use League\CommonMark\Node\Block\Document;
1820

1921
interface TableOfContentsGeneratorInterface
2022
{
21-
public function generate(Document $document): ?TableOfContents;
23+
/**
24+
* If there is a table of contents, returns either a `TableOfContents` or
25+
* `TableOfContentsWrapper` node object.
26+
*
27+
* @psalm-return TableOfContents|TableOfContentsWrapper
28+
*/
29+
public function generate(Document $document): ?AbstractBlock;
2230
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the league/commonmark package.
7+
*
8+
* (c) Colin O'Dell <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace League\CommonMark\Extension\TableOfContents;
15+
16+
use League\CommonMark\Exception\InvalidArgumentException;
17+
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsWrapper;
18+
use League\CommonMark\Node\Node;
19+
use League\CommonMark\Renderer\ChildNodeRendererInterface;
20+
use League\CommonMark\Renderer\NodeRendererInterface;
21+
use League\CommonMark\Util\HtmlElement;
22+
use League\CommonMark\Xml\XmlNodeRendererInterface;
23+
24+
final class TableOfContentsWrapperRenderer implements NodeRendererInterface, XmlNodeRendererInterface
25+
{
26+
/**
27+
* {@inheritDoc}
28+
*/
29+
public function render(Node $node, ChildNodeRendererInterface $childRenderer)
30+
{
31+
TableOfContentsWrapper::assertInstanceOf($node);
32+
$children = $node->children();
33+
if (! \is_array($children)) {
34+
/** @psalm-suppress NoValue */
35+
$children = \iterator_to_array($children);
36+
}
37+
38+
if (\count($children) !== 2) {
39+
throw new InvalidArgumentException(
40+
'TableOfContentsWrapper nodes should have 2 children, found ' . \count($children)
41+
);
42+
}
43+
44+
$attrs = $node->data->get('attributes');
45+
46+
return new HtmlElement(
47+
'div',
48+
$attrs,
49+
$childRenderer->renderNodes($children)
50+
);
51+
}
52+
53+
public function getXmlTagName(Node $node): string
54+
{
55+
return 'table_of_contents_wrapper';
56+
}
57+
58+
/**
59+
* @return array<string, scalar>
60+
*/
61+
public function getXmlAttributes(Node $node): array
62+
{
63+
return [];
64+
}
65+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<div><strong>Table of Contents</strong>
2+
<ul class="table-of-contents">
3+
<li><a href="#content-hello-world">Hello World!</a>
4+
<ul>
5+
<li><a href="#content-isnt-markdown-great">Isn't Markdown Great?</a></li>
6+
</ul>
7+
</li>
8+
</ul></div>
9+
<p>This is my document.</p>
10+
<h1><a id="content-hello-world" href="#content-hello-world" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Hello World!</h1>
11+
<h2><a id="content-isnt-markdown-great" href="#content-isnt-markdown-great" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Isn't Markdown Great?</h2>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
table_of_contents:
3+
label: Table of Contents
4+
---
5+
6+
This is my document.
7+
8+
# Hello World!
9+
10+
## Isn't Markdown Great?
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
table_of_contents:
3+
label: Table of Contents
4+
---
5+
6+
This is my document.
7+
8+
# Hello World!
9+
10+
## Isn't Markdown Great?
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<document xmlns="http://commonmark.org/xml/1.0">
3+
<table_of_contents_wrapper>
4+
<strong>
5+
<text>Table of Contents</text>
6+
</strong>
7+
<table_of_contents type="bullet" tight="false">
8+
<item>
9+
<link destination="#content-hello-world" title="">
10+
<text>Hello World!</text>
11+
</link>
12+
<list type="bullet" tight="false">
13+
<item>
14+
<link destination="#content-isnt-markdown-great" title="">
15+
<text>Isn't Markdown Great?</text>
16+
</link>
17+
</item>
18+
</list>
19+
</item>
20+
</table_of_contents>
21+
</table_of_contents_wrapper>
22+
<paragraph>
23+
<text>This is my document.</text>
24+
</paragraph>
25+
<heading level="1">
26+
<heading_permalink slug="hello-world" />
27+
<text>Hello World!</text>
28+
</heading>
29+
<heading level="2">
30+
<heading_permalink slug="isnt-markdown-great" />
31+
<text>Isn't Markdown Great?</text>
32+
</heading>
33+
</document>

0 commit comments

Comments
 (0)