Skip to content

Commit

Permalink
Add new Footnote Extension (#484)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
colinodell committed May 24, 2020
1 parent f40f619 commit 09e9079
Show file tree
Hide file tree
Showing 39 changed files with 1,470 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .phpstorm.meta.php
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', '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');
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');
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi

### Added

- Added new `FootnoteExtension` based on <https://github.com/rezozero/commonmark-ext-footnotes> (#474)
- Added a new `MentionParser` to replace `InlineMentionParser` with more flexibility and customization
- Added the ability to render `TableOfContents` nodes anywhere in a document (given by a placeholder)
- Added the ability to properly clone `Node` objects
Expand Down
108 changes: 108 additions & 0 deletions docs/1.5/extensions/footnotes.md
@@ -0,0 +1,108 @@
---
layout: default
title: Footnote Extension
description: The FootnoteExtension adds the ability to create footnotes in Markdown documents.
---

# Footnotes

The `FootnoteExtension` adds the ability to create footnotes in Markdown documents.

## Footnote Syntax

Sample Markdown input:

```md
Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi[^note1] leo risus, porta ac consectetur ac.

[^note1]: Elit Malesuada Ridiculus
```

Result:

```md
<p>
Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Morbi<sup id="fnref:note1"><a class="footnote-ref" href="#fn:note1" role="doc-noteref">1</a></sup> leo risus, porta ac consectetur ac.
</p>
<div class="footnotes">
<hr />
<ol>
<li class="footnote" id="fn:note1">
<p>
Elit Malesuada Ridiculus <a class="footnote-backref" rev="footnote" href="#fnref:note1">&#8617;</a>
</p>
</li>
</ol>
</div>
```

## Usage

Configure your `Environment` as usual and simply add the `FootnoteExtension`:

```php
<?php
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment;
use League\CommonMark\Extension\Footnote\FootnoteExtension;

// Obtain a pre-configured Environment with all the CommonMark parsers/renderers ready-to-go
$environment = Environment::createCommonMarkEnvironment();

// Add the extension
$environment->addExtension(new FootnoteExtension());

// Set your configuration
$config = [
// Extension defaults are shown below
// If you're happy with the defaults, feel free to remove them from this array
'footnote' => [
'backref_class' => 'footnote-backref',
'container_add_hr' => true,
'container_class' => 'footnotes',
'ref_class' => 'footnote-ref',
'ref_id_prefix' => 'fnref:',
'footnote_class' => 'footnote',
'footnote_id_prefix' => 'fn:',
],
];

// Instantiate the converter engine and start converting some Markdown!
$converter = new CommonMarkConverter($config, $environment);
echo $converter->convertToHtml('# Hello World!');
```

## Configuration

This extension can be configured by providing a `footnote` array with several nested configuration options. The defaults are shown in the code example above.

### `backref_class`

This `string` option defines which HTML class should be assigned to rendered footnote backreference elements.

### `container_add_hr`

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.

### `container_class`

This `string` option defines which HTML class should be assigned to the container at the bottom of the page which shows all the footnotes.

### `ref_class`

This `string` option defines which HTML class should be assigned to rendered footnote reference elements.

### `ref_id_prefix`

This `string` option defines the prefix prepended to footnote references.

### `footnote_class`

This `string` option defines which HTML class should be assigned to rendered footnote elements.

### `footnote_id_prefix`

This `string` option defines the prefix prepended to footnote elements.
1 change: 1 addition & 0 deletions docs/1.5/extensions/overview.md
Expand Up @@ -74,6 +74,7 @@ These extensions are not part of GFM, but can be useful in many cases:
| Extension | Purpose | Documentation |
| --------- | ------- | ------------- |
| `ExternalLinkExtension` | Tags external links with additional markup | [Documentation](/1.5/extensions/external-links/) |
| `FootnoteExtension` | Add footnote references throughout the document and show a listing of them at the bottom | [Documentation](/1.5/extensions/footnotes/) |
| `HeadingPermalinkExtension` | Makes heading elements linkable | [Documentation](/1.5/extensions/heading-permalinks/) |
| `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/) |
| `MentionParser` | Easy parsing of `@mention` and `#123`-style references | [Documentation](/1.5/extensions/mention/) |
Expand Down
1 change: 1 addition & 0 deletions docs/_data/menu.yml
Expand Up @@ -17,6 +17,7 @@ version:
'Autolinks': '/1.5/extensions/autolinks/'
'Disallowed Raw HTML': '/1.5/extensions/disallowed-raw-html/'
'External Links': '/1.5/extensions/external-links/'
'Footnotes': '/1.5/extensions/footnotes/'
'Heading Permalinks': '/1.5/extensions/heading-permalinks/'
'Inlines Only': '/1.5/extensions/inlines-only/'
'Mentions': '/1.5/extensions/mention/'
Expand Down
51 changes: 51 additions & 0 deletions src/Extension/Footnote/Event/AnonymousFootnotesListener.php
@@ -0,0 +1,51 @@
<?php

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) Rezo Zero / Ambroise Maupate
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace League\CommonMark\Extension\Footnote\Event;

use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\Footnote\Node\Footnote;
use League\CommonMark\Extension\Footnote\Node\FootnoteBackref;
use League\CommonMark\Extension\Footnote\Node\FootnoteRef;
use League\CommonMark\Inline\Element\Text;
use League\CommonMark\Reference\Reference;

final class AnonymousFootnotesListener
{
public function onDocumentParsed(DocumentParsedEvent $event): void
{
$document = $event->getDocument();
$walker = $document->walker();

while ($event = $walker->next()) {
$node = $event->getNode();
if ($node instanceof FootnoteRef && $event->isEntering() && null !== $text = $node->getContent()) {
// Anonymous footnote needs to create a footnote from its content
$existingReference = $node->getReference();
$reference = new Reference(
$existingReference->getLabel(),
'#fnref:' . $existingReference->getLabel(),
$existingReference->getTitle()
);
$footnote = new Footnote($reference);
$footnote->addBackref(new FootnoteBackref($reference));
$paragraph = new Paragraph();
$paragraph->appendChild(new Text($text));
$footnote->appendChild($paragraph);
$document->appendChild($footnote);
}
}
}
}
87 changes: 87 additions & 0 deletions src/Extension/Footnote/Event/GatherFootnotesListener.php
@@ -0,0 +1,87 @@
<?php

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) Rezo Zero / Ambroise Maupate
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace League\CommonMark\Extension\Footnote\Event;

use League\CommonMark\Block\Element\Document;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\Footnote\Node\Footnote;
use League\CommonMark\Extension\Footnote\Node\FootnoteBackref;
use League\CommonMark\Extension\Footnote\Node\FootnoteContainer;
use League\CommonMark\Reference\Reference;

final class GatherFootnotesListener
{
public function onDocumentParsed(DocumentParsedEvent $event): void
{
$document = $event->getDocument();
$walker = $document->walker();

$footnotes = [];
while ($event = $walker->next()) {
if (!$event->isEntering()) {
continue;
}

$node = $event->getNode();
if (!$node instanceof Footnote) {
continue;
}

// Look for existing reference with footnote label
$ref = $document->getReferenceMap()->getReference($node->getReference()->getLabel());
if ($ref !== null) {
// Use numeric title to get footnotes order
$footnotes[\intval($ref->getTitle())] = $node;
} else {
// Footnote call is missing, append footnote at the end
$footnotes[INF] = $node;
}

/*
* Look for all footnote refs pointing to this footnote
* and create each footnote backrefs.
*/
$backrefs = $document->getData('#fn:' . $node->getReference()->getDestination(), []);
/** @var Reference $backref */
foreach ($backrefs as $backref) {
$node->addBackref(new FootnoteBackref(new Reference(
$backref->getLabel(),
'#fnref:' . $backref->getLabel(),
$backref->getTitle()
)));
}
}

// Only add a footnote container if there are any
if (\count($footnotes) === 0) {
return;
}

$container = $this->getFootnotesContainer($document);

\ksort($footnotes);
foreach ($footnotes as $footnote) {
$container->appendChild($footnote);
}
}

private function getFootnotesContainer(Document $document): FootnoteContainer
{
$footnoteContainer = new FootnoteContainer();
$document->appendChild($footnoteContainer);

return $footnoteContainer;
}
}
86 changes: 86 additions & 0 deletions src/Extension/Footnote/Event/NumberFootnotesListener.php
@@ -0,0 +1,86 @@
<?php

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) Rezo Zero / Ambroise Maupate
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace League\CommonMark\Extension\Footnote\Event;

use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\Footnote\Node\FootnoteRef;
use League\CommonMark\Reference\Reference;

final class NumberFootnotesListener
{
public function onDocumentParsed(DocumentParsedEvent $event): void
{
$document = $event->getDocument();
$walker = $document->walker();
$nextCounter = 1;
$usedLabels = [];
$usedCounters = [];

while ($event = $walker->next()) {
if (!$event->isEntering()) {
continue;
}

$node = $event->getNode();

if (!$node instanceof FootnoteRef) {
continue;
}

$existingReference = $node->getReference();
$label = $existingReference->getLabel();
$counter = $nextCounter;
$canIncrementCounter = true;

if (\array_key_exists($label, $usedLabels)) {
/*
* Reference is used again, we need to point
* to the same footnote. But with a different ID
*/
$counter = $usedCounters[$label];
$label = $label . '__' . ++$usedLabels[$label];
$canIncrementCounter = false;
}

// rewrite reference title to use a numeric link
$newReference = new Reference(
$label,
$existingReference->getDestination(),
(string) $counter
);

// Override reference with numeric link
$node->setReference($newReference);
$document->getReferenceMap()->addReference($newReference);

/*
* Store created references in document for
* creating FootnoteBackrefs
*/
if (false === $document->getData($existingReference->getDestination(), false)) {
$document->data[$existingReference->getDestination()] = [];
}

$document->data[$existingReference->getDestination()][] = $newReference;

$usedLabels[$label] = 1;
$usedCounters[$label] = $nextCounter;

if ($canIncrementCounter) {
$nextCounter++;
}
}
}
}

0 comments on commit 09e9079

Please sign in to comment.