Skip to content

Commit

Permalink
Allow Table of Contents to be placed manually
Browse files Browse the repository at this point in the history
  • Loading branch information
colinodell committed May 16, 2020
1 parent d96e776 commit fe22268
Show file tree
Hide file tree
Showing 15 changed files with 223 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .phpstorm.meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
expectedArguments(\League\CommonMark\Inline\Element\Newline::__construct(), 0, argumentsSet('league_commonmark_newline_types'));
expectedReturnValues(\League\CommonMark\Inline\Element\Newline::getType(), argumentsSet('league_commonmark_newline_types'));

registerArgumentsSet('league_commonmark_options', 'renderer', 'enable_em', 'enable_strong', 'use_asterisk', 'use_underscore', 'unordered_list_markers', 'html_input', 'allow_unsafe_links', 'max_nesting_level', 'heading_permalink', 'heading_permalink/html_class', 'heading_permalink/id_prefix', 'heading_permalink/inner_contents', 'heading_permalink/insert', 'heading_permalink/title', 'table_of_contents', 'table_of_contents/style', 'table_of_contents/normalize', 'table_of_contents/position', 'table_of_contents/html_class', 'table_of_contents/min_heading_level', 'table_of_contents/max_heading_level');
registerArgumentsSet('league_commonmark_options', 'renderer', 'enable_em', 'enable_strong', 'use_asterisk', 'use_underscore', 'unordered_list_markers', 'html_input', 'allow_unsafe_links', 'max_nesting_level', 'heading_permalink', 'heading_permalink/html_class', 'heading_permalink/id_prefix', 'heading_permalink/inner_contents', 'heading_permalink/insert', 'heading_permalink/title', 'table_of_contents', 'table_of_contents/style', 'table_of_contents/normalize', 'table_of_contents/position', 'table_of_contents/html_class', 'table_of_contents/min_heading_level', 'table_of_contents/max_heading_level', 'table_of_contents/placeholder');
expectedArguments(\League\CommonMark\EnvironmentInterface::getConfig(), 0, argumentsSet('league_commonmark_options'));
expectedArguments(\League\CommonMark\Util\ConfigurationInterface::get(), 0, argumentsSet('league_commonmark_options'));
expectedArguments(\League\CommonMark\Util\ConfigurationInterface::set(), 0, argumentsSet('league_commonmark_options'));
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi

### Added

- Added the ability to render `TableOfContents` nodes anywhere in a document (given by a placeholder)
- Added the ability to properly clone `Node` objects
- Added new classes:
- `TableOfContentsGenerator`
- `TableOfContentsGeneratorInterface`
- `TableOfContentsPlaceholder`
- `TableOfContentsPlaceholderParser`
- `TableOfContentsPlaceholderRenderer`

### Changed

Expand All @@ -18,6 +22,7 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi
### Deprecated

- Deprecated `League\CommonMark\Extension\TableOfContents\TableOfContents` (use the one in the new `Node` sub-namespace instead)
- Deprecated the `STYLE_` and `NORMALIZE_` constants in `TableOfContentsBuilder` (use the ones in `TableOfContentsGenerator` instead)

## [1.4.3] - 2020-05-04

Expand Down
6 changes: 6 additions & 0 deletions docs/1.5/extensions/table-of-contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ $config = [
'min_heading_level' => 1,
'max_heading_level' => 6,
'normalize' => 'relative',
'placeholder' => null,
],
];

Expand Down Expand Up @@ -71,9 +72,14 @@ This `string` controls where in the document your table of contents will be plac

- **`'top'`** (default) - Insert at the very top of the document, before any content
- `'before-headings'` - Insert just before the very first heading - useful if you want to have some descriptive text show above the table of content.
- `'placeholder'` - Location is manually defined by a user-provided placeholder somewhere in the document (see the `placeholder` option below)

If you'd like to customize this further, you can implement a [custom event listener](/1.5/customization/event-dispatcher/#registering-listeners) to locate the `TableOfContents` node and reposition it somewhere else in the document prior to rendering.

### `placeholder`

When combined with `'position' => 'placeholder'`, this setting tells the extension which placeholder content should be replaced with the Table of Contents. For example, if you set this option to `[TOC]`, then any lines in your document consisting of that `[TOC]` placeholder will be replaced by the Table of Contents. Note that this option has no default value - you must provide this string yourself.

### `style`

This `string` option controls what style of HTML list should be used to render the table of contents:
Expand Down
33 changes: 33 additions & 0 deletions src/Extension/TableOfContents/Node/TableOfContentsPlaceholder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* 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\Block\Element\AbstractBlock;
use League\CommonMark\Cursor;

final class TableOfContentsPlaceholder extends AbstractBlock
{
public function canContain(AbstractBlock $block): bool
{
return false;
}

public function isCode(): bool
{
return false;
}

public function matchesNextLine(Cursor $cursor): bool
{
return false;
}
}
21 changes: 21 additions & 0 deletions src/Extension/TableOfContents/TableOfContentsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use League\CommonMark\Exception\InvalidOptionException;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;

Expand Down Expand Up @@ -49,6 +50,7 @@ final class TableOfContentsBuilder implements ConfigurationAwareInterface

public const POSITION_TOP = 'top';
public const POSITION_BEFORE_HEADINGS = 'before-headings';
public const POSITION_PLACEHOLDER = 'placeholder';

/** @var ConfigurationInterface */
private $config;
Expand Down Expand Up @@ -82,6 +84,8 @@ public function onDocumentParsed(DocumentParsedEvent $event): void
$document->prependChild($toc);
} elseif ($position === self::POSITION_BEFORE_HEADINGS) {
$this->insertBeforeFirstLinkedHeading($document, $toc);
} elseif ($position === self::POSITION_PLACEHOLDER) {
$this->replacePlaceholders($document, $toc);
} else {
throw new InvalidOptionException(\sprintf('Invalid config option "%s" for "table_of_contents/position"', $position));
}
Expand All @@ -99,6 +103,23 @@ private function insertBeforeFirstLinkedHeading(Document $document, TableOfConte
}
}

private function replacePlaceholders(Document $document, TableOfContents $toc): void
{
$walker = $document->walker();
while ($event = $walker->next()) {
// Add the block once we find a placeholder (and we're about to leave it)
if (!$event->getNode() instanceof TableOfContentsPlaceholder) {
continue;
}

if ($event->isEntering()) {
continue;
}

$event->getNode()->replaceWith(clone $toc);
}
}

public function setConfiguration(ConfigurationInterface $config)
{
$this->config = $config;
Expand Down
7 changes: 7 additions & 0 deletions src/Extension/TableOfContents/TableOfContentsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder;

final class TableOfContentsExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment): void
{
$environment->addEventListener(DocumentParsedEvent::class, [new TableOfContentsBuilder(), 'onDocumentParsed'], -150);

if ($environment->getConfig('table_of_contents/position') === TableOfContentsBuilder::POSITION_PLACEHOLDER) {
$environment->addBlockParser(new TableOfContentsPlaceholderParser(), 200);
// If a placeholder cannot be replaced with a TOC element this renderer will ensure the parser won't error out
$environment->addBlockRenderer(TableOfContentsPlaceholder::class, new TableOfContentsPlaceholderRenderer());
}
}
}
47 changes: 47 additions & 0 deletions src/Extension/TableOfContents/TableOfContentsPlaceholderParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* 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\Block\Parser\BlockParserInterface;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;

final class TableOfContentsPlaceholderParser implements BlockParserInterface, ConfigurationAwareInterface
{
/** @var ConfigurationInterface */
private $config;

public function parse(ContextInterface $context, Cursor $cursor): bool
{
$placeholder = $this->config->get('table_of_contents/placeholder');
if ($placeholder === null) {
return false;
}

// The placeholder must be the only thing on the line
if ($cursor->match('/^' . \preg_quote($placeholder, '/') . '$/') === null) {
return false;
}

$context->addBlock(new TableOfContentsPlaceholder());

return true;
}

public function setConfiguration(ConfigurationInterface $configuration)
{
$this->config = $configuration;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* 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\Block\Element\AbstractBlock;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\ElementRendererInterface;

final class TableOfContentsPlaceholderRenderer implements BlockRendererInterface
{
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
return '<!-- table of contents -->';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ public function testWithPositionBeforeHeadings()
}
}

public function testWithPositionPlaceholder()
{
$this->setUpConverter([
'table_of_contents' => [
'position' => 'placeholder',
'placeholder' => '[TOC]',
],
]);

foreach ($this->loadTests(__DIR__ . '/data', 'position-placeholder*.md') as [$markdown, $html, $testName]) {
$this->assertMarkdownRendersAs($markdown, $html, $testName);
}
}

public function testWithCustomClass()
{
$this->setUpConverter([
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<p>This is my document.</p>
<h1><a id="user-content-hello-world" href="#hello-world" name="hello-world" class="heading-permalink" aria-hidden="true" title="Permalink"><svg class="heading-permalink-icon" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Hello World!</h1>
<h2><a id="user-content-isnt-markdown-great" href="#isnt-markdown-great" name="isnt-markdown-great" class="heading-permalink" aria-hidden="true" title="Permalink"><svg class="heading-permalink-icon" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Isn't Markdown Great?</h2>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
This is my document.

# Hello World!

## Isn't Markdown Great?
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<p>This is my document.</p>
<ul class="table-of-contents">
<li>
<p><a href="#another-copy-of-my-toc-is-here">Another copy of my TOC is here</a></p>
<ul>
<li>
<p><a href="#this-contains-something-that-looks-like-a-placeholder-but-actually-isnt">This contains something that looks like a placeholder but actually isn't</a></p>
</li>
<li>
<p><a href="#just-a-link-reference-down-here">Just a link reference down here</a></p>
</li>
</ul>
</li>
</ul>
<h1><a id="user-content-another-copy-of-my-toc-is-here" href="#another-copy-of-my-toc-is-here" name="another-copy-of-my-toc-is-here" class="heading-permalink" aria-hidden="true" title="Permalink"><svg class="heading-permalink-icon" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Another copy of my TOC is here</h1>
<ul class="table-of-contents">
<li>
<p><a href="#another-copy-of-my-toc-is-here">Another copy of my TOC is here</a></p>
<ul>
<li>
<p><a href="#this-contains-something-that-looks-like-a-placeholder-but-actually-isnt">This contains something that looks like a placeholder but actually isn't</a></p>
</li>
<li>
<p><a href="#just-a-link-reference-down-here">Just a link reference down here</a></p>
</li>
</ul>
</li>
</ul>
<h2><a id="user-content-this-contains-something-that-looks-like-a-placeholder-but-actually-isnt" href="#this-contains-something-that-looks-like-a-placeholder-but-actually-isnt" name="this-contains-something-that-looks-like-a-placeholder-but-actually-isnt" class="heading-permalink" aria-hidden="true" title="Permalink"><svg class="heading-permalink-icon" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>This contains something that looks like a placeholder but actually isn't</h2>
<p>This is not a placeholder, but a link reference: <a href="https://www.example.com" title="Link Reference">TOC</a></p>
<p><a href="https://www.example.com" title="Link Reference">TOC</a> This should also be handled as a link reference</p>
<h2><a id="user-content-just-a-link-reference-down-here" href="#just-a-link-reference-down-here" name="just-a-link-reference-down-here" class="heading-permalink" aria-hidden="true" title="Permalink"><svg class="heading-permalink-icon" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Just a link reference down here</h2>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
This is my document.

[TOC]

# Another copy of my TOC is here

[TOC]

## This contains something that looks like a placeholder but actually isn't

This is not a placeholder, but a link reference: [TOC]

[TOC] This should also be handled as a link reference

## Just a link reference down here

[TOC]: https://www.example.com "Link Reference"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<p>Here is my placeholder:</p>
<!-- table of contents -->
<p>But I don't actually have any heading elements here!</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Here is my placeholder:

[TOC]

But I don't actually have any heading elements here!

0 comments on commit fe22268

Please sign in to comment.