Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/Extension/TableOfContents/Node/TableOfContentsWrapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace League\CommonMark\Extension\TableOfContents\Node;

use League\CommonMark\Exception\InvalidArgumentException;
use League\CommonMark\Node\Block\AbstractBlock;

final class TableOfContentsWrapper extends AbstractBlock
{
public function getInnerToc(): TableOfContents
{
$children = $this->children();
if (! \is_array($children)) {
/** @psalm-suppress NoValue */
$children = \iterator_to_array($children);
}

if (\count($children) !== 2) {
throw new InvalidArgumentException(
'TableOfContentsWrapper nodes should have 2 children, found ' . \count($children)
);
}

$inner = $children[1];
if (! $inner instanceof TableOfContents) {
throw new InvalidArgumentException(
'TableOfContentsWrapper second node should be a TableOfContents, found ' . \get_class($inner)
);
}

return $inner;
}
}
38 changes: 35 additions & 3 deletions src/Extension/TableOfContents/TableOfContentsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@
namespace League\CommonMark\Extension\TableOfContents;

use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Exception\InvalidArgumentException;
use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsWrapper;
use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Node\Block\Document;
use League\CommonMark\Node\NodeIterator;
use League\Config\ConfigurationAwareInterface;
Expand All @@ -43,6 +46,7 @@ public function onDocumentParsed(DocumentParsedEvent $event): void
(int) $this->config->get('table_of_contents/min_heading_level'),
(int) $this->config->get('table_of_contents/max_heading_level'),
(string) $this->config->get('heading_permalink/fragment_prefix'),
(string) $this->config->get('table_of_contents/label'),
);

$toc = $generator->generate($document);
Expand All @@ -54,7 +58,11 @@ public function onDocumentParsed(DocumentParsedEvent $event): void
// Add custom CSS class(es), if defined
$class = $this->config->get('table_of_contents/html_class');
if ($class !== null) {
$toc->data->append('attributes/class', $class);
if ($toc instanceof TableOfContentsWrapper) {
$toc->getInnerToc()->data->append('attributes/class', $class);
} else {
$toc->data->append('attributes/class', $class);
}
}

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

private function insertBeforeFirstLinkedHeading(Document $document, TableOfContents $toc): void
/**
* @psalm-param TableOfContents|TableOfContentsWrapper $toc
*
* @phpstan-param TableOfContents|TableOfContentsWrapper $toc
*/
private function insertBeforeFirstLinkedHeading(Document $document, AbstractBlock $toc): void
{
// @phpstan-ignore booleanAnd.alwaysFalse
if (! $toc instanceof TableOfContents && ! $toc instanceof TableOfContentsWrapper) {
throw new InvalidArgumentException(
'Toc should be a TableOfContents or TableOfContentsWrapper, got ' . \get_class($toc)
);
}

foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) {
if (! $node instanceof Heading) {
continue;
Expand All @@ -87,8 +107,20 @@ private function insertBeforeFirstLinkedHeading(Document $document, TableOfConte
}
}

private function replacePlaceholders(Document $document, TableOfContents $toc): void
/**
* @psalm-param TableOfContents|TableOfContentsWrapper $toc
*
* @phpstan-param TableOfContents|TableOfContentsWrapper $toc
*/
private function replacePlaceholders(Document $document, AbstractBlock $toc): void
{
// @phpstan-ignore booleanAnd.alwaysFalse
if (! $toc instanceof TableOfContents && ! $toc instanceof TableOfContentsWrapper) {
throw new InvalidArgumentException(
'Toc should be a TableOfContents or TableOfContentsWrapper, got ' . \get_class($toc)
);
}

foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) {
// Add the block once we find a placeholder
if (! $node instanceof TableOfContentsPlaceholder) {
Expand Down
3 changes: 3 additions & 0 deletions src/Extension/TableOfContents/TableOfContentsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use League\CommonMark\Extension\ConfigurableExtensionInterface;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsWrapper;
use League\Config\ConfigurationBuilderInterface;
use Nette\Schema\Expect;

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

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

// phpcs:ignore SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed
Expand Down
30 changes: 28 additions & 2 deletions src/Extension/TableOfContents/TableOfContentsGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@
use League\CommonMark\Extension\CommonMark\Node\Block\ListData;
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
use League\CommonMark\Extension\CommonMark\Node\Inline\Strong;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsWrapper;
use League\CommonMark\Extension\TableOfContents\Normalizer\AsIsNormalizerStrategy;
use League\CommonMark\Extension\TableOfContents\Normalizer\FlatNormalizerStrategy;
use League\CommonMark\Extension\TableOfContents\Normalizer\NormalizerStrategyInterface;
use League\CommonMark\Extension\TableOfContents\Normalizer\RelativeNormalizerStrategy;
use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Node\Block\Document;
use League\CommonMark\Node\Inline\Text;
use League\CommonMark\Node\NodeIterator;
use League\CommonMark\Node\RawMarkupContainerInterface;
use League\CommonMark\Node\StringContainerHelper;
Expand Down Expand Up @@ -54,20 +58,30 @@ final class TableOfContentsGenerator implements TableOfContentsGeneratorInterfac
/** @psalm-readonly */
private string $fragmentPrefix;

public function __construct(string $style, string $normalizationStrategy, int $minHeadingLevel, int $maxHeadingLevel, string $fragmentPrefix)
/** @psalm-readonly */
private string $label;

public function __construct(string $style, string $normalizationStrategy, int $minHeadingLevel, int $maxHeadingLevel, string $fragmentPrefix, string $label = '')
{
$this->style = $style;
$this->normalizationStrategy = $normalizationStrategy;
$this->minHeadingLevel = $minHeadingLevel;
$this->maxHeadingLevel = $maxHeadingLevel;
$this->fragmentPrefix = $fragmentPrefix;
$this->label = $label;

if ($fragmentPrefix !== '') {
$this->fragmentPrefix .= '-';
}
}

public function generate(Document $document): ?TableOfContents
/**
* If there is a table of contents, returns either a `TableOfContents` or
* `TableOfContentsWrapper` node object
*
* @psalm-return TableOfContents|TableOfContentsWrapper
*/
public function generate(Document $document): ?AbstractBlock
{
$toc = $this->createToc($document);

Expand Down Expand Up @@ -111,6 +125,18 @@ public function generate(Document $document): ?TableOfContents
return null;
}

if ($this->label !== '') {
$label = new Strong();
$label->appendChild(new Text($this->label));
$wrapper = new TableOfContentsWrapper();
$wrapper->appendChild($label);
$wrapper->appendChild($toc);
$wrapper->setStartLine($toc->getStartLine());
$wrapper->setEndLine($toc->getEndLine());

return $wrapper;
}

return $toc;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@
namespace League\CommonMark\Extension\TableOfContents;

use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsWrapper;
use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Node\Block\Document;

interface TableOfContentsGeneratorInterface
{
public function generate(Document $document): ?TableOfContents;
/**
* If there is a table of contents, returns either a `TableOfContents` or
* `TableOfContentsWrapper` node object.
*
* @psalm-return TableOfContents|TableOfContentsWrapper
*/
public function generate(Document $document): ?AbstractBlock;
}
65 changes: 65 additions & 0 deletions src/Extension/TableOfContents/TableOfContentsWrapperRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace League\CommonMark\Extension\TableOfContents;

use League\CommonMark\Exception\InvalidArgumentException;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsWrapper;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
use League\CommonMark\Xml\XmlNodeRendererInterface;

final class TableOfContentsWrapperRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* {@inheritDoc}
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer)
{
TableOfContentsWrapper::assertInstanceOf($node);
$children = $node->children();
if (! \is_array($children)) {
/** @psalm-suppress NoValue */
$children = \iterator_to_array($children);
}

if (\count($children) !== 2) {
throw new InvalidArgumentException(
'TableOfContentsWrapper nodes should have 2 children, found ' . \count($children)
);
}

$attrs = $node->data->get('attributes');

return new HtmlElement(
'div',
$attrs,
$childRenderer->renderNodes($children)
);
}

public function getXmlTagName(Node $node): string
{
return 'table_of_contents_wrapper';
}

/**
* @return array<string, scalar>
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}
11 changes: 11 additions & 0 deletions tests/functional/Extension/TableOfContents/md/has-label.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<div><strong>Table of Contents</strong>
<ul class="table-of-contents">
<li><a href="#content-hello-world">Hello World!</a>
<ul>
<li><a href="#content-isnt-markdown-great">Isn't Markdown Great?</a></li>
</ul>
</li>
</ul></div>
<p>This is my document.</p>
<h1><a id="content-hello-world" href="#content-hello-world" class="heading-permalink" aria-hidden="true" title="Permalink">¶</a>Hello World!</h1>
<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>
10 changes: 10 additions & 0 deletions tests/functional/Extension/TableOfContents/md/has-label.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
table_of_contents:
label: Table of Contents
---

This is my document.

# Hello World!

## Isn't Markdown Great?
10 changes: 10 additions & 0 deletions tests/functional/Extension/TableOfContents/xml/has-label.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
table_of_contents:
label: Table of Contents
---

This is my document.

# Hello World!

## Isn't Markdown Great?
33 changes: 33 additions & 0 deletions tests/functional/Extension/TableOfContents/xml/has-label.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://commonmark.org/xml/1.0">
<table_of_contents_wrapper>
<strong>
<text>Table of Contents</text>
</strong>
<table_of_contents type="bullet" tight="false">
<item>
<link destination="#content-hello-world" title="">
<text>Hello World!</text>
</link>
<list type="bullet" tight="false">
<item>
<link destination="#content-isnt-markdown-great" title="">
<text>Isn't Markdown Great?</text>
</link>
</item>
</list>
</item>
</table_of_contents>
</table_of_contents_wrapper>
<paragraph>
<text>This is my document.</text>
</paragraph>
<heading level="1">
<heading_permalink slug="hello-world" />
<text>Hello World!</text>
</heading>
<heading level="2">
<heading_permalink slug="isnt-markdown-great" />
<text>Isn't Markdown Great?</text>
</heading>
</document>