Skip to content

Commit 09e9079

Browse files
authored
Add new Footnote Extension (#484)
This extension is based on https://github.com/rezozero/commonmark-ext-footnotes, imported and relicensed with permission from the maintainer: #474 (comment) In addition to importing the functionality, a number of configuration options were added, as well as some other small tweaks.
1 parent f40f619 commit 09e9079

39 files changed

+1470
-1
lines changed

.phpstorm.meta.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
expectedArguments(\League\CommonMark\Inline\Element\Newline::__construct(), 0, argumentsSet('league_commonmark_newline_types'));
2828
expectedReturnValues(\League\CommonMark\Inline\Element\Newline::getType(), argumentsSet('league_commonmark_newline_types'));
2929

30-
registerArgumentsSet('league_commonmark_options', 'renderer', 'enable_em', 'enable_strong', 'use_asterisk', 'use_underscore', 'unordered_list_markers', 'html_input', 'allow_unsafe_links', 'max_nesting_level', 'external_link', 'external_link/nofollow', 'external_link/noopener', 'external_link/noreferrer', 'heading_permalink', 'heading_permalink/html_class', 'heading_permalink/id_prefix', 'heading_permalink/inner_contents', 'heading_permalink/insert', 'heading_permalink/slug_generator', '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');
30+
registerArgumentsSet('league_commonmark_options', 'renderer', 'enable_em', 'enable_strong', 'use_asterisk', 'use_underscore', 'unordered_list_markers', 'html_input', 'allow_unsafe_links', 'max_nesting_level', 'external_link', 'external_link/nofollow', 'external_link/noopener', 'external_link/noreferrer', 'footnote', 'footnote/backref_class', 'footnote/container_add_hr', 'footnote/container_class', 'footnote/ref_class', 'footnote/ref_id_prefix', 'footnote/footnote_class', 'footnote/footnote_id_prefix', 'heading_permalink', 'heading_permalink/html_class', 'heading_permalink/id_prefix', 'heading_permalink/inner_contents', 'heading_permalink/insert', 'heading_permalink/slug_generator', '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');
3131
expectedArguments(\League\CommonMark\EnvironmentInterface::getConfig(), 0, argumentsSet('league_commonmark_options'));
3232
expectedArguments(\League\CommonMark\Util\ConfigurationInterface::get(), 0, argumentsSet('league_commonmark_options'));
3333
expectedArguments(\League\CommonMark\Util\ConfigurationInterface::set(), 0, argumentsSet('league_commonmark_options'));

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi
66

77
### Added
88

9+
- Added new `FootnoteExtension` based on <https://github.com/rezozero/commonmark-ext-footnotes> (#474)
910
- Added a new `MentionParser` to replace `InlineMentionParser` with more flexibility and customization
1011
- Added the ability to render `TableOfContents` nodes anywhere in a document (given by a placeholder)
1112
- Added the ability to properly clone `Node` objects

docs/1.5/extensions/footnotes.md

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
---
2+
layout: default
3+
title: Footnote Extension
4+
description: The FootnoteExtension adds the ability to create footnotes in Markdown documents.
5+
---
6+
7+
# Footnotes
8+
9+
The `FootnoteExtension` adds the ability to create footnotes in Markdown documents.
10+
11+
## Footnote Syntax
12+
13+
Sample Markdown input:
14+
15+
```md
16+
Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
17+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi[^note1] leo risus, porta ac consectetur ac.
18+
19+
[^note1]: Elit Malesuada Ridiculus
20+
```
21+
22+
Result:
23+
24+
```md
25+
<p>
26+
Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
27+
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
28+
Morbi<sup id="fnref:note1"><a class="footnote-ref" href="#fn:note1" role="doc-noteref">1</a></sup> leo risus, porta ac consectetur ac.
29+
</p>
30+
<div class="footnotes">
31+
<hr />
32+
<ol>
33+
<li class="footnote" id="fn:note1">
34+
<p>
35+
Elit Malesuada Ridiculus <a class="footnote-backref" rev="footnote" href="#fnref:note1">&#8617;</a>
36+
</p>
37+
</li>
38+
</ol>
39+
</div>
40+
```
41+
42+
## Usage
43+
44+
Configure your `Environment` as usual and simply add the `FootnoteExtension`:
45+
46+
```php
47+
<?php
48+
use League\CommonMark\CommonMarkConverter;
49+
use League\CommonMark\Environment;
50+
use League\CommonMark\Extension\Footnote\FootnoteExtension;
51+
52+
// Obtain a pre-configured Environment with all the CommonMark parsers/renderers ready-to-go
53+
$environment = Environment::createCommonMarkEnvironment();
54+
55+
// Add the extension
56+
$environment->addExtension(new FootnoteExtension());
57+
58+
// Set your configuration
59+
$config = [
60+
// Extension defaults are shown below
61+
// If you're happy with the defaults, feel free to remove them from this array
62+
'footnote' => [
63+
'backref_class' => 'footnote-backref',
64+
'container_add_hr' => true,
65+
'container_class' => 'footnotes',
66+
'ref_class' => 'footnote-ref',
67+
'ref_id_prefix' => 'fnref:',
68+
'footnote_class' => 'footnote',
69+
'footnote_id_prefix' => 'fn:',
70+
],
71+
];
72+
73+
// Instantiate the converter engine and start converting some Markdown!
74+
$converter = new CommonMarkConverter($config, $environment);
75+
echo $converter->convertToHtml('# Hello World!');
76+
```
77+
78+
## Configuration
79+
80+
This extension can be configured by providing a `footnote` array with several nested configuration options. The defaults are shown in the code example above.
81+
82+
### `backref_class`
83+
84+
This `string` option defines which HTML class should be assigned to rendered footnote backreference elements.
85+
86+
### `container_add_hr`
87+
88+
This `boolean` option controls whether an `<hr>` element should be added inside the container. Set this to `false` if you want more control over how the footnote section at the bottom is differentiated from the rest of the document.
89+
90+
### `container_class`
91+
92+
This `string` option defines which HTML class should be assigned to the container at the bottom of the page which shows all the footnotes.
93+
94+
### `ref_class`
95+
96+
This `string` option defines which HTML class should be assigned to rendered footnote reference elements.
97+
98+
### `ref_id_prefix`
99+
100+
This `string` option defines the prefix prepended to footnote references.
101+
102+
### `footnote_class`
103+
104+
This `string` option defines which HTML class should be assigned to rendered footnote elements.
105+
106+
### `footnote_id_prefix`
107+
108+
This `string` option defines the prefix prepended to footnote elements.

docs/1.5/extensions/overview.md

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ These extensions are not part of GFM, but can be useful in many cases:
7474
| Extension | Purpose | Documentation |
7575
| --------- | ------- | ------------- |
7676
| `ExternalLinkExtension` | Tags external links with additional markup | [Documentation](/1.5/extensions/external-links/) |
77+
| `FootnoteExtension` | Add footnote references throughout the document and show a listing of them at the bottom | [Documentation](/1.5/extensions/footnotes/) |
7778
| `HeadingPermalinkExtension` | Makes heading elements linkable | [Documentation](/1.5/extensions/heading-permalinks/) |
7879
| `InlinesOnlyExtension` | Only includes standard CommonMark inline elements - perfect for handling comments and other short bits of text where you only want bold, italic, links, etc. | [Documentation](/1.5/extensions/inlines-only/) |
7980
| `MentionParser` | Easy parsing of `@mention` and `#123`-style references | [Documentation](/1.5/extensions/mention/) |

docs/_data/menu.yml

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ version:
1717
'Autolinks': '/1.5/extensions/autolinks/'
1818
'Disallowed Raw HTML': '/1.5/extensions/disallowed-raw-html/'
1919
'External Links': '/1.5/extensions/external-links/'
20+
'Footnotes': '/1.5/extensions/footnotes/'
2021
'Heading Permalinks': '/1.5/extensions/heading-permalinks/'
2122
'Inlines Only': '/1.5/extensions/inlines-only/'
2223
'Mentions': '/1.5/extensions/mention/'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the league/commonmark package.
5+
*
6+
* (c) Colin O'Dell <[email protected]>
7+
* (c) Rezo Zero / Ambroise Maupate
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
declare(strict_types=1);
14+
15+
namespace League\CommonMark\Extension\Footnote\Event;
16+
17+
use League\CommonMark\Block\Element\Paragraph;
18+
use League\CommonMark\Event\DocumentParsedEvent;
19+
use League\CommonMark\Extension\Footnote\Node\Footnote;
20+
use League\CommonMark\Extension\Footnote\Node\FootnoteBackref;
21+
use League\CommonMark\Extension\Footnote\Node\FootnoteRef;
22+
use League\CommonMark\Inline\Element\Text;
23+
use League\CommonMark\Reference\Reference;
24+
25+
final class AnonymousFootnotesListener
26+
{
27+
public function onDocumentParsed(DocumentParsedEvent $event): void
28+
{
29+
$document = $event->getDocument();
30+
$walker = $document->walker();
31+
32+
while ($event = $walker->next()) {
33+
$node = $event->getNode();
34+
if ($node instanceof FootnoteRef && $event->isEntering() && null !== $text = $node->getContent()) {
35+
// Anonymous footnote needs to create a footnote from its content
36+
$existingReference = $node->getReference();
37+
$reference = new Reference(
38+
$existingReference->getLabel(),
39+
'#fnref:' . $existingReference->getLabel(),
40+
$existingReference->getTitle()
41+
);
42+
$footnote = new Footnote($reference);
43+
$footnote->addBackref(new FootnoteBackref($reference));
44+
$paragraph = new Paragraph();
45+
$paragraph->appendChild(new Text($text));
46+
$footnote->appendChild($paragraph);
47+
$document->appendChild($footnote);
48+
}
49+
}
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the league/commonmark package.
5+
*
6+
* (c) Colin O'Dell <[email protected]>
7+
* (c) Rezo Zero / Ambroise Maupate
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
declare(strict_types=1);
14+
15+
namespace League\CommonMark\Extension\Footnote\Event;
16+
17+
use League\CommonMark\Block\Element\Document;
18+
use League\CommonMark\Event\DocumentParsedEvent;
19+
use League\CommonMark\Extension\Footnote\Node\Footnote;
20+
use League\CommonMark\Extension\Footnote\Node\FootnoteBackref;
21+
use League\CommonMark\Extension\Footnote\Node\FootnoteContainer;
22+
use League\CommonMark\Reference\Reference;
23+
24+
final class GatherFootnotesListener
25+
{
26+
public function onDocumentParsed(DocumentParsedEvent $event): void
27+
{
28+
$document = $event->getDocument();
29+
$walker = $document->walker();
30+
31+
$footnotes = [];
32+
while ($event = $walker->next()) {
33+
if (!$event->isEntering()) {
34+
continue;
35+
}
36+
37+
$node = $event->getNode();
38+
if (!$node instanceof Footnote) {
39+
continue;
40+
}
41+
42+
// Look for existing reference with footnote label
43+
$ref = $document->getReferenceMap()->getReference($node->getReference()->getLabel());
44+
if ($ref !== null) {
45+
// Use numeric title to get footnotes order
46+
$footnotes[\intval($ref->getTitle())] = $node;
47+
} else {
48+
// Footnote call is missing, append footnote at the end
49+
$footnotes[INF] = $node;
50+
}
51+
52+
/*
53+
* Look for all footnote refs pointing to this footnote
54+
* and create each footnote backrefs.
55+
*/
56+
$backrefs = $document->getData('#fn:' . $node->getReference()->getDestination(), []);
57+
/** @var Reference $backref */
58+
foreach ($backrefs as $backref) {
59+
$node->addBackref(new FootnoteBackref(new Reference(
60+
$backref->getLabel(),
61+
'#fnref:' . $backref->getLabel(),
62+
$backref->getTitle()
63+
)));
64+
}
65+
}
66+
67+
// Only add a footnote container if there are any
68+
if (\count($footnotes) === 0) {
69+
return;
70+
}
71+
72+
$container = $this->getFootnotesContainer($document);
73+
74+
\ksort($footnotes);
75+
foreach ($footnotes as $footnote) {
76+
$container->appendChild($footnote);
77+
}
78+
}
79+
80+
private function getFootnotesContainer(Document $document): FootnoteContainer
81+
{
82+
$footnoteContainer = new FootnoteContainer();
83+
$document->appendChild($footnoteContainer);
84+
85+
return $footnoteContainer;
86+
}
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the league/commonmark package.
5+
*
6+
* (c) Colin O'Dell <[email protected]>
7+
* (c) Rezo Zero / Ambroise Maupate
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
declare(strict_types=1);
14+
15+
namespace League\CommonMark\Extension\Footnote\Event;
16+
17+
use League\CommonMark\Event\DocumentParsedEvent;
18+
use League\CommonMark\Extension\Footnote\Node\FootnoteRef;
19+
use League\CommonMark\Reference\Reference;
20+
21+
final class NumberFootnotesListener
22+
{
23+
public function onDocumentParsed(DocumentParsedEvent $event): void
24+
{
25+
$document = $event->getDocument();
26+
$walker = $document->walker();
27+
$nextCounter = 1;
28+
$usedLabels = [];
29+
$usedCounters = [];
30+
31+
while ($event = $walker->next()) {
32+
if (!$event->isEntering()) {
33+
continue;
34+
}
35+
36+
$node = $event->getNode();
37+
38+
if (!$node instanceof FootnoteRef) {
39+
continue;
40+
}
41+
42+
$existingReference = $node->getReference();
43+
$label = $existingReference->getLabel();
44+
$counter = $nextCounter;
45+
$canIncrementCounter = true;
46+
47+
if (\array_key_exists($label, $usedLabels)) {
48+
/*
49+
* Reference is used again, we need to point
50+
* to the same footnote. But with a different ID
51+
*/
52+
$counter = $usedCounters[$label];
53+
$label = $label . '__' . ++$usedLabels[$label];
54+
$canIncrementCounter = false;
55+
}
56+
57+
// rewrite reference title to use a numeric link
58+
$newReference = new Reference(
59+
$label,
60+
$existingReference->getDestination(),
61+
(string) $counter
62+
);
63+
64+
// Override reference with numeric link
65+
$node->setReference($newReference);
66+
$document->getReferenceMap()->addReference($newReference);
67+
68+
/*
69+
* Store created references in document for
70+
* creating FootnoteBackrefs
71+
*/
72+
if (false === $document->getData($existingReference->getDestination(), false)) {
73+
$document->data[$existingReference->getDestination()] = [];
74+
}
75+
76+
$document->data[$existingReference->getDestination()][] = $newReference;
77+
78+
$usedLabels[$label] = 1;
79+
$usedCounters[$label] = $nextCounter;
80+
81+
if ($canIncrementCounter) {
82+
$nextCounter++;
83+
}
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)