From 3b20fbc9f46a4f57205e665f6c6b1ba19138c9d1 Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Sat, 23 May 2020 09:51:25 -0400 Subject: [PATCH] Add new Footnote Extension This extension is based on https://github.com/rezozero/commonmark-ext-footnotes, imported and relicensed with permission from the maintainer: https://github.com/thephpleague/commonmark/issues/474#issuecomment-631425455 In addition to importing the functionality, a number of configuration options were added, as well as some other small tweaks. --- .phpstorm.meta.php | 2 +- CHANGELOG.md | 1 + docs/1.5/extensions/footnotes.md | 108 ++++++++++++++++++ docs/1.5/extensions/overview.md | 1 + docs/_data/menu.yml | 1 + .../Event/AnonymousFootnotesListener.php | 51 +++++++++ .../Event/GatherFootnotesListener.php | 88 ++++++++++++++ .../Event/NumberFootnotesListener.php | 86 ++++++++++++++ src/Extension/Footnote/FootnoteExtension.php | 53 +++++++++ src/Extension/Footnote/Node/Footnote.php | 75 ++++++++++++ .../Footnote/Node/FootnoteBackref.php | 37 ++++++ .../Footnote/Node/FootnoteContainer.php | 39 +++++++ src/Extension/Footnote/Node/FootnoteRef.php | 56 +++++++++ .../Parser/AnonymousFootnoteRefParser.php | 62 ++++++++++ .../Footnote/Parser/FootnoteParser.php | 63 ++++++++++ .../Footnote/Parser/FootnoteRefParser.php | 58 ++++++++++ .../Renderer/FootnoteBackrefRenderer.php | 49 ++++++++ .../Renderer/FootnoteContainerRenderer.php | 52 +++++++++ .../Footnote/Renderer/FootnoteRefRenderer.php | 62 ++++++++++ .../Footnote/Renderer/FootnoteRenderer.php | 64 +++++++++++ .../Extension/Footnote/LocalDataTest.php | 77 +++++++++++++ .../Extension/Footnote/data/anonymous.html | 5 + .../Extension/Footnote/data/anonymous.md | 4 + .../Extension/Footnote/data/duplicated.html | 15 +++ .../Extension/Footnote/data/duplicated.md | 18 +++ .../Extension/Footnote/data/just-link.html | 1 + .../Extension/Footnote/data/just-link.md | 1 + .../Extension/Footnote/data/regular.html | 14 +++ .../Extension/Footnote/data/regular.md | 19 +++ .../Footnote/data/with-link-anonymous.html | 2 + .../Footnote/data/with-link-anonymous.md | 1 + .../Extension/Footnote/data/with-link.html | 2 + .../Extension/Footnote/data/with-link.md | 3 + .../Extension/Footnote/data/xss.html | 2 + .../functional/Extension/Footnote/data/xss.md | 1 + .../Renderer/FootnoteBackrefRendererTest.php | 66 +++++++++++ .../FootnoteContainerRendererTest.php | 75 ++++++++++++ .../Renderer/FootnoteRefRendererTest.php | 79 +++++++++++++ .../Renderer/FootnoteRendererTest.php | 79 +++++++++++++ 39 files changed, 1471 insertions(+), 1 deletion(-) create mode 100644 docs/1.5/extensions/footnotes.md create mode 100644 src/Extension/Footnote/Event/AnonymousFootnotesListener.php create mode 100644 src/Extension/Footnote/Event/GatherFootnotesListener.php create mode 100644 src/Extension/Footnote/Event/NumberFootnotesListener.php create mode 100644 src/Extension/Footnote/FootnoteExtension.php create mode 100644 src/Extension/Footnote/Node/Footnote.php create mode 100644 src/Extension/Footnote/Node/FootnoteBackref.php create mode 100644 src/Extension/Footnote/Node/FootnoteContainer.php create mode 100644 src/Extension/Footnote/Node/FootnoteRef.php create mode 100644 src/Extension/Footnote/Parser/AnonymousFootnoteRefParser.php create mode 100644 src/Extension/Footnote/Parser/FootnoteParser.php create mode 100644 src/Extension/Footnote/Parser/FootnoteRefParser.php create mode 100644 src/Extension/Footnote/Renderer/FootnoteBackrefRenderer.php create mode 100644 src/Extension/Footnote/Renderer/FootnoteContainerRenderer.php create mode 100644 src/Extension/Footnote/Renderer/FootnoteRefRenderer.php create mode 100644 src/Extension/Footnote/Renderer/FootnoteRenderer.php create mode 100644 tests/functional/Extension/Footnote/LocalDataTest.php create mode 100644 tests/functional/Extension/Footnote/data/anonymous.html create mode 100644 tests/functional/Extension/Footnote/data/anonymous.md create mode 100644 tests/functional/Extension/Footnote/data/duplicated.html create mode 100644 tests/functional/Extension/Footnote/data/duplicated.md create mode 100644 tests/functional/Extension/Footnote/data/just-link.html create mode 100644 tests/functional/Extension/Footnote/data/just-link.md create mode 100644 tests/functional/Extension/Footnote/data/regular.html create mode 100644 tests/functional/Extension/Footnote/data/regular.md create mode 100644 tests/functional/Extension/Footnote/data/with-link-anonymous.html create mode 100644 tests/functional/Extension/Footnote/data/with-link-anonymous.md create mode 100644 tests/functional/Extension/Footnote/data/with-link.html create mode 100644 tests/functional/Extension/Footnote/data/with-link.md create mode 100644 tests/functional/Extension/Footnote/data/xss.html create mode 100644 tests/functional/Extension/Footnote/data/xss.md create mode 100644 tests/unit/Extension/Footnote/Renderer/FootnoteBackrefRendererTest.php create mode 100644 tests/unit/Extension/Footnote/Renderer/FootnoteContainerRendererTest.php create mode 100644 tests/unit/Extension/Footnote/Renderer/FootnoteRefRendererTest.php create mode 100644 tests/unit/Extension/Footnote/Renderer/FootnoteRendererTest.php diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index 1868fd3da6..c60c80c015 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -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')); diff --git a/CHANGELOG.md b/CHANGELOG.md index dab51f57d6..b1ead088c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi ### Added + - Added new `FootnoteExtension` based on (#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 diff --git a/docs/1.5/extensions/footnotes.md b/docs/1.5/extensions/footnotes.md new file mode 100644 index 0000000000..9afb170a62 --- /dev/null +++ b/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 +

+ Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Morbi1 leo risus, porta ac consectetur ac. +

+
+
+
    +
  1. +

    + Elit Malesuada Ridiculus +

    +
  2. +
+
+``` + +## Usage + +Configure your `Environment` as usual and simply add the `FootnoteExtension`: + +```php +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 `
` 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. diff --git a/docs/1.5/extensions/overview.md b/docs/1.5/extensions/overview.md index 067ac2f689..d8bb535913 100644 --- a/docs/1.5/extensions/overview.md +++ b/docs/1.5/extensions/overview.md @@ -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/) | diff --git a/docs/_data/menu.yml b/docs/_data/menu.yml index 8958fd2227..7c0137e181 100644 --- a/docs/_data/menu.yml +++ b/docs/_data/menu.yml @@ -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/' diff --git a/src/Extension/Footnote/Event/AnonymousFootnotesListener.php b/src/Extension/Footnote/Event/AnonymousFootnotesListener.php new file mode 100644 index 0000000000..a1ff53cf7b --- /dev/null +++ b/src/Extension/Footnote/Event/AnonymousFootnotesListener.php @@ -0,0 +1,51 @@ + + * (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); + } + } + } +} diff --git a/src/Extension/Footnote/Event/GatherFootnotesListener.php b/src/Extension/Footnote/Event/GatherFootnotesListener.php new file mode 100644 index 0000000000..3671263320 --- /dev/null +++ b/src/Extension/Footnote/Event/GatherFootnotesListener.php @@ -0,0 +1,88 @@ + + * (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\EnvironmentInterface; +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; + } +} diff --git a/src/Extension/Footnote/Event/NumberFootnotesListener.php b/src/Extension/Footnote/Event/NumberFootnotesListener.php new file mode 100644 index 0000000000..5b3f992503 --- /dev/null +++ b/src/Extension/Footnote/Event/NumberFootnotesListener.php @@ -0,0 +1,86 @@ + + * (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++; + } + } + } +} diff --git a/src/Extension/Footnote/FootnoteExtension.php b/src/Extension/Footnote/FootnoteExtension.php new file mode 100644 index 0000000000..1e896f9cbc --- /dev/null +++ b/src/Extension/Footnote/FootnoteExtension.php @@ -0,0 +1,53 @@ + + * (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; + +use League\CommonMark\ConfigurableEnvironmentInterface; +use League\CommonMark\Event\DocumentParsedEvent; +use League\CommonMark\Extension\ExtensionInterface; +use League\CommonMark\Extension\Footnote\Event\AnonymousFootnotesListener; +use League\CommonMark\Extension\Footnote\Event\GatherFootnotesListener; +use League\CommonMark\Extension\Footnote\Event\NumberFootnotesListener; +use League\CommonMark\Extension\Footnote\Node\Footnote; +use League\CommonMark\Extension\Footnote\Node\FootnoteBackref; +use League\CommonMark\Extension\Footnote\Node\FootnoteContainer; +use League\CommonMark\Extension\Footnote\Node\FootnoteRef; +use League\CommonMark\Extension\Footnote\Parser\AnonymousFootnoteRefParser; +use League\CommonMark\Extension\Footnote\Parser\FootnoteParser; +use League\CommonMark\Extension\Footnote\Parser\FootnoteRefParser; +use League\CommonMark\Extension\Footnote\Renderer\FootnoteBackrefRenderer; +use League\CommonMark\Extension\Footnote\Renderer\FootnoteContainerRenderer; +use League\CommonMark\Extension\Footnote\Renderer\FootnoteRefRenderer; +use League\CommonMark\Extension\Footnote\Renderer\FootnoteRenderer; + +final class FootnoteExtension implements ExtensionInterface +{ + public function register(ConfigurableEnvironmentInterface $environment) + { + $environment->addBlockParser(new FootnoteParser(), 51); + $environment->addInlineParser(new AnonymousFootnoteRefParser(), 35); + $environment->addInlineParser(new FootnoteRefParser(), 51); + + $environment->addBlockRenderer(FootnoteContainer::class, new FootnoteContainerRenderer()); + $environment->addBlockRenderer(Footnote::class, new FootnoteRenderer()); + + $environment->addInlineRenderer(FootnoteRef::class, new FootnoteRefRenderer()); + $environment->addInlineRenderer(FootnoteBackref::class, new FootnoteBackrefRenderer()); + + $environment->addEventListener(DocumentParsedEvent::class, [new AnonymousFootnotesListener(), 'onDocumentParsed']); + $environment->addEventListener(DocumentParsedEvent::class, [new NumberFootnotesListener(), 'onDocumentParsed']); + $environment->addEventListener(DocumentParsedEvent::class, [new GatherFootnotesListener(), 'onDocumentParsed']); + } +} diff --git a/src/Extension/Footnote/Node/Footnote.php b/src/Extension/Footnote/Node/Footnote.php new file mode 100644 index 0000000000..d296d1930e --- /dev/null +++ b/src/Extension/Footnote/Node/Footnote.php @@ -0,0 +1,75 @@ + + * (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\Node; + +use League\CommonMark\Block\Element\AbstractBlock; +use League\CommonMark\Cursor; +use League\CommonMark\Reference\ReferenceInterface; + +/** + * @method children() AbstractBlock[] + */ +final class Footnote extends AbstractBlock +{ + /** + * @var FootnoteBackref[] + */ + private $backrefs = []; + + /** + * @var ReferenceInterface + */ + private $reference; + + public function __construct(ReferenceInterface $reference) + { + $this->reference = $reference; + } + + public function canContain(AbstractBlock $block): bool + { + return true; + } + + public function isCode(): bool + { + return false; + } + + public function matchesNextLine(Cursor $cursor): bool + { + return false; + } + + public function getReference(): ReferenceInterface + { + return $this->reference; + } + + public function addBackref(FootnoteBackref $backref): self + { + $this->backrefs[] = $backref; + + return $this; + } + + /** + * @return FootnoteBackref[] + */ + public function getBackrefs(): array + { + return $this->backrefs; + } +} diff --git a/src/Extension/Footnote/Node/FootnoteBackref.php b/src/Extension/Footnote/Node/FootnoteBackref.php new file mode 100644 index 0000000000..91747d31c6 --- /dev/null +++ b/src/Extension/Footnote/Node/FootnoteBackref.php @@ -0,0 +1,37 @@ + + * (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\Node; + +use League\CommonMark\Inline\Element\AbstractInline; +use League\CommonMark\Reference\ReferenceInterface; + +/** + * Link from the footnote on the bottom of the document back to the reference + */ +final class FootnoteBackref extends AbstractInline +{ + /** @var ReferenceInterface */ + private $reference; + + public function __construct(ReferenceInterface $reference) + { + $this->reference = $reference; + } + + public function getReference(): ReferenceInterface + { + return $this->reference; + } +} diff --git a/src/Extension/Footnote/Node/FootnoteContainer.php b/src/Extension/Footnote/Node/FootnoteContainer.php new file mode 100644 index 0000000000..ad151efe5b --- /dev/null +++ b/src/Extension/Footnote/Node/FootnoteContainer.php @@ -0,0 +1,39 @@ + + * (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\Node; + +use League\CommonMark\Block\Element\AbstractBlock; +use League\CommonMark\Cursor; + +/** + * @method children() AbstractBlock[] + */ +final class FootnoteContainer extends AbstractBlock +{ + public function canContain(AbstractBlock $block): bool + { + return $block instanceof Footnote; + } + + public function isCode(): bool + { + return false; + } + + public function matchesNextLine(Cursor $cursor): bool + { + return false; + } +} diff --git a/src/Extension/Footnote/Node/FootnoteRef.php b/src/Extension/Footnote/Node/FootnoteRef.php new file mode 100644 index 0000000000..27b98b5573 --- /dev/null +++ b/src/Extension/Footnote/Node/FootnoteRef.php @@ -0,0 +1,56 @@ + + * (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\Node; + +use League\CommonMark\Inline\Element\AbstractInline; +use League\CommonMark\Reference\ReferenceInterface; + +final class FootnoteRef extends AbstractInline +{ + /** @var ReferenceInterface */ + private $reference; + + /** @var string|null */ + private $content; + + /** + * @param ReferenceInterface $reference + * @param string|null $content + * @param array $data + */ + public function __construct(ReferenceInterface $reference, ?string $content = null, array $data = []) + { + $this->reference = $reference; + $this->content = $content; + $this->data = $data; + } + + public function getReference(): ReferenceInterface + { + return $this->reference; + } + + public function setReference(ReferenceInterface $reference): FootnoteRef + { + $this->reference = $reference; + + return $this; + } + + public function getContent(): ?string + { + return $this->content; + } +} diff --git a/src/Extension/Footnote/Parser/AnonymousFootnoteRefParser.php b/src/Extension/Footnote/Parser/AnonymousFootnoteRefParser.php new file mode 100644 index 0000000000..44c2793377 --- /dev/null +++ b/src/Extension/Footnote/Parser/AnonymousFootnoteRefParser.php @@ -0,0 +1,62 @@ + + * (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\Parser; + +use League\CommonMark\Extension\Footnote\Node\FootnoteRef; +use League\CommonMark\Inline\Parser\InlineParserInterface; +use League\CommonMark\InlineParserContext; +use League\CommonMark\Reference\Reference; + +final class AnonymousFootnoteRefParser implements InlineParserInterface +{ + public function getCharacters(): array + { + return ['^']; + } + + public function parse(InlineParserContext $inlineContext): bool + { + $container = $inlineContext->getContainer(); + $cursor = $inlineContext->getCursor(); + $nextChar = $cursor->peek(); + if ($nextChar !== '[') { + return false; + } + $state = $cursor->saveState(); + + $m = $cursor->match('/\^\[[^\n^\]]+\]/'); + if ($m !== null) { + if (\preg_match('#\^\[([^\]]+)\]#', $m, $matches) > 0) { + $reference = $this->createReference($matches[1]); + $container->appendChild(new FootnoteRef($reference, $matches[1])); + + return true; + } + } + + $cursor->restoreState($state); + + return false; + } + + private function createReference(string $label): Reference + { + $refLabel = Reference::normalizeReference($label); + $refLabel = \mb_strtolower(\str_replace(' ', '-', $refLabel)); + $refLabel = \substr($refLabel, 0, 20); + + return new Reference($refLabel, '#fn:' . $refLabel, $label); + } +} diff --git a/src/Extension/Footnote/Parser/FootnoteParser.php b/src/Extension/Footnote/Parser/FootnoteParser.php new file mode 100644 index 0000000000..231c2aa5e0 --- /dev/null +++ b/src/Extension/Footnote/Parser/FootnoteParser.php @@ -0,0 +1,63 @@ + + * (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\Parser; + +use League\CommonMark\Block\Parser\BlockParserInterface; +use League\CommonMark\ContextInterface; +use League\CommonMark\Cursor; +use League\CommonMark\Extension\Footnote\Node\Footnote; +use League\CommonMark\Reference\Reference; +use League\CommonMark\Util\RegexHelper; + +final class FootnoteParser implements BlockParserInterface +{ + public function parse(ContextInterface $context, Cursor $cursor): bool + { + if ($cursor->isIndented()) { + return false; + } + + $match = RegexHelper::matchAll( + '/^\[\^([^\n^\]]+)\]\:\s/', + $cursor->getLine(), + $cursor->getNextNonSpacePosition() + ); + + if (!$match) { + return false; + } + + $cursor->advanceToNextNonSpaceOrTab(); + $cursor->advanceBy(\strlen($match[0])); + $str = $cursor->getRemainder(); + \preg_replace('/^\[\^([^\n^\]]+)\]\:\s/', '', $str); + + if (\preg_match('/^\[\^([^\n^\]]+)\]\:\s/', $match[0], $matches) > 0) { + $context->addBlock($this->createFootnote($matches[1])); + $context->setBlocksParsed(true); + + return true; + } + + return false; + } + + private function createFootnote(string $label): Footnote + { + return new Footnote( + new Reference($label, $label, $label) + ); + } +} diff --git a/src/Extension/Footnote/Parser/FootnoteRefParser.php b/src/Extension/Footnote/Parser/FootnoteRefParser.php new file mode 100644 index 0000000000..496124e7ca --- /dev/null +++ b/src/Extension/Footnote/Parser/FootnoteRefParser.php @@ -0,0 +1,58 @@ + + * (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\Parser; + +use League\CommonMark\Extension\Footnote\Node\FootnoteRef; +use League\CommonMark\Inline\Parser\InlineParserInterface; +use League\CommonMark\InlineParserContext; +use League\CommonMark\Reference\Reference; + +final class FootnoteRefParser implements InlineParserInterface +{ + public function getCharacters(): array + { + return ['[']; + } + + public function parse(InlineParserContext $inlineContext): bool + { + $container = $inlineContext->getContainer(); + $cursor = $inlineContext->getCursor(); + $nextChar = $cursor->peek(); + if ($nextChar !== '^') { + return false; + } + + $state = $cursor->saveState(); + + $m = $cursor->match('#\[\^([^\]]+)\]#'); + if ($m !== null) { + if (\preg_match('#\[\^([^\]]+)\]#', $m, $matches) > 0) { + $container->appendChild(new FootnoteRef($this->createReference($matches[1]))); + + return true; + } + } + + $cursor->restoreState($state); + + return false; + } + + private function createReference(string $label): Reference + { + return new Reference($label, '#fn:' . $label, $label); + } +} diff --git a/src/Extension/Footnote/Renderer/FootnoteBackrefRenderer.php b/src/Extension/Footnote/Renderer/FootnoteBackrefRenderer.php new file mode 100644 index 0000000000..1bbf68943d --- /dev/null +++ b/src/Extension/Footnote/Renderer/FootnoteBackrefRenderer.php @@ -0,0 +1,49 @@ + + * (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\Renderer; + +use League\CommonMark\ElementRendererInterface; +use League\CommonMark\Extension\Footnote\Node\FootnoteBackref; +use League\CommonMark\HtmlElement; +use League\CommonMark\Inline\Element\AbstractInline; +use League\CommonMark\Inline\Renderer\InlineRendererInterface; +use League\CommonMark\Util\ConfigurationAwareInterface; +use League\CommonMark\Util\ConfigurationInterface; + +final class FootnoteBackrefRenderer implements InlineRendererInterface, ConfigurationAwareInterface +{ + /** @var ConfigurationInterface */ + private $config; + + public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer) + { + if (!($inline instanceof FootnoteBackref)) { + throw new \InvalidArgumentException('Incompatible inline type: ' . \get_class($inline)); + } + + $attrs = $inline->getData('attributes', []); + $attrs['class'] = $attrs['class'] ?? $this->config->get('footnote/backref_class', 'footnote-backref'); + $attrs['rev'] = 'footnote'; + $attrs['href'] = \mb_strtolower($inline->getReference()->getDestination()); + $attrs['role'] = 'doc-backlink'; + + return ' ' . new HtmlElement('a', $attrs, '↩', true); + } + + public function setConfiguration(ConfigurationInterface $configuration) + { + $this->config = $configuration; + } +} diff --git a/src/Extension/Footnote/Renderer/FootnoteContainerRenderer.php b/src/Extension/Footnote/Renderer/FootnoteContainerRenderer.php new file mode 100644 index 0000000000..24be14ea60 --- /dev/null +++ b/src/Extension/Footnote/Renderer/FootnoteContainerRenderer.php @@ -0,0 +1,52 @@ + + * (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\Renderer; + +use League\CommonMark\Block\Element\AbstractBlock; +use League\CommonMark\Block\Renderer\BlockRendererInterface; +use League\CommonMark\ElementRendererInterface; +use League\CommonMark\Extension\Footnote\Node\FootnoteContainer; +use League\CommonMark\HtmlElement; +use League\CommonMark\Util\ConfigurationAwareInterface; +use League\CommonMark\Util\ConfigurationInterface; + +final class FootnoteContainerRenderer implements BlockRendererInterface, ConfigurationAwareInterface +{ + /** @var ConfigurationInterface */ + private $config; + + public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false) + { + if (!($block instanceof FootnoteContainer)) { + throw new \InvalidArgumentException('Incompatible block type: ' . \get_class($block)); + } + + $attrs = $block->getData('attributes', []); + $attrs['class'] = $attrs['class'] ?? $this->config->get('footnote/container_class', 'footnotes'); + $attrs['role'] = 'doc-endnotes'; + + $contents = new HtmlElement('ol', [], $htmlRenderer->renderBlocks($block->children())); + if ($this->config->get('footnote/container_add_hr', true)) { + $contents = [new HtmlElement('hr', [], null, true), $contents]; + } + + return new HtmlElement('div', $attrs, $contents); + } + + public function setConfiguration(ConfigurationInterface $configuration) + { + $this->config = $configuration; + } +} diff --git a/src/Extension/Footnote/Renderer/FootnoteRefRenderer.php b/src/Extension/Footnote/Renderer/FootnoteRefRenderer.php new file mode 100644 index 0000000000..3b3c6ee7a1 --- /dev/null +++ b/src/Extension/Footnote/Renderer/FootnoteRefRenderer.php @@ -0,0 +1,62 @@ + + * (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\Renderer; + +use League\CommonMark\ElementRendererInterface; +use League\CommonMark\Extension\Footnote\Node\FootnoteRef; +use League\CommonMark\HtmlElement; +use League\CommonMark\Inline\Element\AbstractInline; +use League\CommonMark\Inline\Renderer\InlineRendererInterface; +use League\CommonMark\Util\ConfigurationAwareInterface; +use League\CommonMark\Util\ConfigurationInterface; + +final class FootnoteRefRenderer implements InlineRendererInterface, ConfigurationAwareInterface +{ + /** @var ConfigurationInterface */ + private $config; + + public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer) + { + if (!($inline instanceof FootnoteRef)) { + throw new \InvalidArgumentException('Incompatible inline type: ' . \get_class($inline)); + } + + $attrs = $inline->getData('attributes', []); + $class = $attrs['class'] ?? $this->config->get('footnote/ref_class', 'footnote-ref'); + $idPrefix = $this->config->get('footnote/ref_id_prefix', 'fnref:'); + + return new HtmlElement( + 'sup', + [ + 'id' => $idPrefix . \mb_strtolower($inline->getReference()->getLabel()), + ], + new HTMLElement( + 'a', + [ + 'class' => $class, + 'href' => \mb_strtolower($inline->getReference()->getDestination()), + 'role' => 'doc-noteref', + ], + $inline->getReference()->getTitle() + ), + true + ); + } + + public function setConfiguration(ConfigurationInterface $configuration) + { + $this->config = $configuration; + } +} diff --git a/src/Extension/Footnote/Renderer/FootnoteRenderer.php b/src/Extension/Footnote/Renderer/FootnoteRenderer.php new file mode 100644 index 0000000000..f18e35a2a1 --- /dev/null +++ b/src/Extension/Footnote/Renderer/FootnoteRenderer.php @@ -0,0 +1,64 @@ + + * (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\Renderer; + +use League\CommonMark\Block\Element\AbstractBlock; +use League\CommonMark\Block\Renderer\BlockRendererInterface; +use League\CommonMark\ElementRendererInterface; +use League\CommonMark\Extension\Footnote\Node\Footnote; +use League\CommonMark\HtmlElement; +use League\CommonMark\Util\ConfigurationAwareInterface; +use League\CommonMark\Util\ConfigurationInterface; + +final class FootnoteRenderer implements BlockRendererInterface, ConfigurationAwareInterface +{ + /** @var ConfigurationInterface */ + private $config; + + /** + * @param Footnote $block + * @param ElementRendererInterface $htmlRenderer + * @param bool $inTightList + * + * @return HtmlElement + */ + public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false) + { + if (!($block instanceof Footnote)) { + throw new \InvalidArgumentException('Incompatible block type: ' . \get_class($block)); + } + + $attrs = $block->getData('attributes', []); + $attrs['class'] = $attrs['class'] ?? $this->config->get('footnote/footnote_class', 'footnote'); + $attrs['id'] = $this->config->get('footnote/footnote_id_prefix', 'fn:') . \mb_strtolower($block->getReference()->getLabel()); + $attrs['role'] = 'doc-endnote'; + + foreach ($block->getBackrefs() as $backref) { + $block->lastChild()->appendChild($backref); + } + + return new HtmlElement( + 'li', + $attrs, + $htmlRenderer->renderBlocks($block->children()), + true + ); + } + + public function setConfiguration(ConfigurationInterface $configuration) + { + $this->config = $configuration; + } +} diff --git a/tests/functional/Extension/Footnote/LocalDataTest.php b/tests/functional/Extension/Footnote/LocalDataTest.php new file mode 100644 index 0000000000..cf802faaeb --- /dev/null +++ b/tests/functional/Extension/Footnote/LocalDataTest.php @@ -0,0 +1,77 @@ + + * (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\Tests\Functional\Extension\Footnote; + +use League\CommonMark\CommonMarkConverter; +use League\CommonMark\Environment; +use League\CommonMark\Extension\Footnote\FootnoteExtension; +use League\CommonMark\Tests\Functional\AbstractLocalDataTest; + +/** + * @internal + */ +final class LocalDataTest extends AbstractLocalDataTest +{ + /** @var CommonMarkConverter */ + private $commonMarkConverter; + + /** @var CommonMarkConverter */ + private $gfmConverter; + + protected function setUp(): void + { + /* + * Test with minimal extensions + */ + $environment = Environment::createCommonMarkEnvironment(); + $environment->addExtension(new FootnoteExtension()); + $this->commonMarkConverter = new CommonMarkConverter([], $environment); + + /* + * Test with other extensions + */ + $gfmEnvironment = Environment::createGFMEnvironment(); + $gfmEnvironment->addExtension(new FootnoteExtension()); + $this->gfmConverter = new CommonMarkConverter([], $gfmEnvironment); + } + + /** + * @dataProvider dataProvider + */ + public function testRenderer(string $markdown, string $html, string $testName): void + { + $this->converter = $this->commonMarkConverter; + $this->assertMarkdownRendersAs($markdown, $html, $testName); + } + + /** + * @dataProvider dataProvider + */ + public function testExtraRenderer(string $markdown, string $html, string $testName): void + { + $this->converter = $this->gfmConverter; + $this->assertMarkdownRendersAs($markdown, $html, $testName); + } + + /** + * @return iterable + */ + public function dataProvider(): iterable + { + foreach ($this->loadTests(__DIR__ . '/data', '*.md') as $test) { + yield $test; + } + } +} diff --git a/tests/functional/Extension/Footnote/data/anonymous.html b/tests/functional/Extension/Footnote/data/anonymous.html new file mode 100644 index 0000000000..35a366602e --- /dev/null +++ b/tests/functional/Extension/Footnote/data/anonymous.html @@ -0,0 +1,5 @@ +

Donec id elit non mi porta gravida at eget metus. Maecenas sed diam eget risus varius blandit sit amet non magna. +Nullam quis risus eget urna mollis1 ornare vel eu leo. Donec id elit non mi +porta gravida at eget metus. Duis mollis, est non commodo luctus, nisi erat porttitor +ligula, eget lacinia odio sem nec elit.

+

  1. This is an anonymous footnote 

diff --git a/tests/functional/Extension/Footnote/data/anonymous.md b/tests/functional/Extension/Footnote/data/anonymous.md new file mode 100644 index 0000000000..84746ffbeb --- /dev/null +++ b/tests/functional/Extension/Footnote/data/anonymous.md @@ -0,0 +1,4 @@ +Donec id elit non mi porta gravida at eget metus. Maecenas sed diam eget risus varius blandit sit amet non magna. +Nullam quis risus eget urna mollis^[This is an anonymous footnote] ornare vel eu leo. Donec id elit non mi +porta gravida at eget metus. Duis mollis, est non commodo luctus, nisi erat porttitor +ligula, eget lacinia odio sem nec elit. diff --git a/tests/functional/Extension/Footnote/data/duplicated.html b/tests/functional/Extension/Footnote/data/duplicated.html new file mode 100644 index 0000000000..f781ca27d5 --- /dev/null +++ b/tests/functional/Extension/Footnote/data/duplicated.html @@ -0,0 +1,15 @@ +

Donec sed odio dui1.

+

Donec id elit non mi porta gravida at eget metus. Maecenas sed diam eget risus varius blandit sit amet non magna. +Nullam quis risus eget urna mollis2 ornare vel eu leo. Donec id elit non mi +porta gravida at eget metus. Duis mollis, est non commodo luctus, nisi erat porttitor +ligula, eget lacinia odio sem nec elit.

+

Cras mattis consectetur purus sit amet fermentum. Duis mollis, est non commodo luctus, +nisi erat porttitor ligula, eget lacinia odio3 sem nec elit. Donec sed odio dui. +Cras mattis consectetur purus sit amet fermentum. Fusce dapibus1, tellus ac cursus +commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. +Integer posuere4 erat a ante venenatis dapibus posuere velit aliquet3.

+

  1. Risus Euismod Pharetra  

  2. +
  3. Etiam porta sem malesuada magna mollis euismod. Praesent commodo cursus magna, vel scelerisque nisl +consectetur et. Maecenas faucibus mollis interdum. Maecenas faucibus mollis interdum. 

  4. +
  5. Ullamcorper Etiam Fringilla (test)  

  6. +
  7. Ref 4 

diff --git a/tests/functional/Extension/Footnote/data/duplicated.md b/tests/functional/Extension/Footnote/data/duplicated.md new file mode 100644 index 0000000000..80303e3c99 --- /dev/null +++ b/tests/functional/Extension/Footnote/data/duplicated.md @@ -0,0 +1,18 @@ +Donec sed odio dui[^ref]. + +Donec id elit non mi porta gravida at eget metus. Maecenas sed diam eget risus varius blandit sit amet non magna. +Nullam quis risus eget urna mollis[^ref2] ornare vel eu leo. Donec id elit non mi +porta gravida at eget metus. Duis mollis, est non commodo luctus, nisi erat porttitor +ligula, eget lacinia odio sem nec elit. + +Cras mattis consectetur purus sit amet fermentum. Duis mollis, est non commodo luctus, +nisi erat porttitor ligula, eget lacinia odio[^ref3] sem nec elit. Donec sed odio dui. +Cras mattis consectetur purus sit amet fermentum. Fusce dapibus[^ref], tellus ac cursus +commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. +Integer posuere[^ref4] erat a ante venenatis dapibus posuere velit aliquet[^ref3]. + +[^ref]: Risus Euismod Pharetra +[^ref2]: Etiam porta sem malesuada magna mollis euismod. Praesent commodo cursus magna, vel scelerisque nisl +consectetur et. Maecenas faucibus mollis interdum. Maecenas faucibus mollis interdum. +[^ref3]: Ullamcorper Etiam Fringilla (test) +[^ref4]: Ref 4 diff --git a/tests/functional/Extension/Footnote/data/just-link.html b/tests/functional/Extension/Footnote/data/just-link.html new file mode 100644 index 0000000000..65667d53c2 --- /dev/null +++ b/tests/functional/Extension/Footnote/data/just-link.html @@ -0,0 +1 @@ +

Ils peuvent y avoir gratuitement accès pour une audience devant un tribunal.

diff --git a/tests/functional/Extension/Footnote/data/just-link.md b/tests/functional/Extension/Footnote/data/just-link.md new file mode 100644 index 0000000000..ff833b690f --- /dev/null +++ b/tests/functional/Extension/Footnote/data/just-link.md @@ -0,0 +1 @@ +Ils peuvent y avoir [gratuitement accès](https://www.gov.uk) pour une audience devant un tribunal. diff --git a/tests/functional/Extension/Footnote/data/regular.html b/tests/functional/Extension/Footnote/data/regular.html new file mode 100644 index 0000000000..8860e3bb76 --- /dev/null +++ b/tests/functional/Extension/Footnote/data/regular.html @@ -0,0 +1,14 @@ +

Donec sed odio dui1.

+

Donec id elit non mi porta gravida at eget metus. Maecenas sed diam eget risus varius blandit sit amet non magna. +Nullam quis risus eget urna mollis2 ornare vel eu leo. Donec id elit non mi +porta gravida at eget metus. Duis mollis, est non commodo luctus, nisi erat porttitor +ligula, eget lacinia odio sem nec elit.

+

Cras mattis consectetur purus sit amet fermentum. Duis mollis, est non commodo luctus, +nisi erat porttitor ligula, eget lacinia odio3 sem nec elit. Donec sed odio dui. +Cras mattis consectetur purus sit amet fermentum. Fusce dapibus, tellus ac cursus +commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. +Integer posuere erat a ante venenatis dapibus posuere velit aliquet.

+

  1. Risus Euismod Pharetra 

  2. +
  3. Etiam porta sem malesuada magna mollis euismod. Praesent commodo cursus magna, vel scelerisque nisl +consectetur et. Maecenas faucibus mollis interdum. Maecenas faucibus mollis interdum. 

  4. +
  5. Ullamcorper Etiam Fringilla (test) 

diff --git a/tests/functional/Extension/Footnote/data/regular.md b/tests/functional/Extension/Footnote/data/regular.md new file mode 100644 index 0000000000..85f6cae764 --- /dev/null +++ b/tests/functional/Extension/Footnote/data/regular.md @@ -0,0 +1,19 @@ +Donec sed odio dui[^ref]. + +Donec id elit non mi porta gravida at eget metus. Maecenas sed diam eget risus varius blandit sit amet non magna. +Nullam quis risus eget urna mollis[^ref2] ornare vel eu leo. Donec id elit non mi +porta gravida at eget metus. Duis mollis, est non commodo luctus, nisi erat porttitor +ligula, eget lacinia odio sem nec elit. + +[^ref2]: Etiam porta sem malesuada magna mollis euismod. Praesent commodo cursus magna, vel scelerisque nisl +consectetur et. Maecenas faucibus mollis interdum. Maecenas faucibus mollis interdum. + +Cras mattis consectetur purus sit amet fermentum. Duis mollis, est non commodo luctus, +nisi erat porttitor ligula, eget lacinia odio[^ref3] sem nec elit. Donec sed odio dui. +Cras mattis consectetur purus sit amet fermentum. Fusce dapibus, tellus ac cursus +commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. +Integer posuere erat a ante venenatis dapibus posuere velit aliquet. + +[^ref]: Risus Euismod Pharetra +[^ref3]: Ullamcorper Etiam Fringilla (test) + diff --git a/tests/functional/Extension/Footnote/data/with-link-anonymous.html b/tests/functional/Extension/Footnote/data/with-link-anonymous.html new file mode 100644 index 0000000000..a2550f42dc --- /dev/null +++ b/tests/functional/Extension/Footnote/data/with-link-anonymous.html @@ -0,0 +1,2 @@ +

Ils peuvent y avoir gratuitement accès pour une audience devant un tribunal.1

+

  1. Super note 

diff --git a/tests/functional/Extension/Footnote/data/with-link-anonymous.md b/tests/functional/Extension/Footnote/data/with-link-anonymous.md new file mode 100644 index 0000000000..0e093b3617 --- /dev/null +++ b/tests/functional/Extension/Footnote/data/with-link-anonymous.md @@ -0,0 +1 @@ +Ils peuvent y avoir [gratuitement accès](https://www.gov.uk) pour une audience devant un tribunal.^[Super note] diff --git a/tests/functional/Extension/Footnote/data/with-link.html b/tests/functional/Extension/Footnote/data/with-link.html new file mode 100644 index 0000000000..8c4c1f2334 --- /dev/null +++ b/tests/functional/Extension/Footnote/data/with-link.html @@ -0,0 +1,2 @@ +

Ils peuvent y avoir gratuitement accès pour une audience devant un tribunal.1

+

  1. Information communiquée à Prison Insider par Matei Clej, interprète judiciaire, le 18 avril 2019. 

diff --git a/tests/functional/Extension/Footnote/data/with-link.md b/tests/functional/Extension/Footnote/data/with-link.md new file mode 100644 index 0000000000..35fd88c3f0 --- /dev/null +++ b/tests/functional/Extension/Footnote/data/with-link.md @@ -0,0 +1,3 @@ +Ils peuvent y avoir [gratuitement accès](https://www.gov.uk) pour une audience devant un tribunal.[^matei] + +[^matei]: Information **communiquée à *Prison Insider*** par Matei Clej, interprète judiciaire, le 18 avril 2019. diff --git a/tests/functional/Extension/Footnote/data/xss.html b/tests/functional/Extension/Footnote/data/xss.html new file mode 100644 index 0000000000..f2df86f1fe --- /dev/null +++ b/tests/functional/Extension/Footnote/data/xss.html @@ -0,0 +1,2 @@ +

Hello1 World!

+

  1. <script>javascript:alert('XSS')</script> 

diff --git a/tests/functional/Extension/Footnote/data/xss.md b/tests/functional/Extension/Footnote/data/xss.md new file mode 100644 index 0000000000..ed8ead861e --- /dev/null +++ b/tests/functional/Extension/Footnote/data/xss.md @@ -0,0 +1 @@ +Hello^[] World! diff --git a/tests/unit/Extension/Footnote/Renderer/FootnoteBackrefRendererTest.php b/tests/unit/Extension/Footnote/Renderer/FootnoteBackrefRendererTest.php new file mode 100644 index 0000000000..25cdc1f7fc --- /dev/null +++ b/tests/unit/Extension/Footnote/Renderer/FootnoteBackrefRendererTest.php @@ -0,0 +1,66 @@ + + * + * 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\Tests\Unit\Extension\Footnote\Renderer; + +use League\CommonMark\Extension\Footnote\Node\FootnoteBackref; +use League\CommonMark\Extension\Footnote\Renderer\FootnoteBackrefRenderer; +use League\CommonMark\Reference\Reference; +use League\CommonMark\Tests\Unit\FakeHtmlRenderer; +use League\CommonMark\Util\Configuration; +use PHPUnit\Framework\TestCase; + +final class FootnoteBackrefRendererTest extends TestCase +{ + public function testDefaultAttributes(): void + { + $renderer = new FootnoteBackrefRenderer(); + $renderer->setConfiguration(new Configuration()); + + $fakeReference = new Reference('label', 'dest', 'title'); + $footnoteBackref = new FootnoteBackref($fakeReference); + + $output = $renderer->render($footnoteBackref, new FakeHtmlRenderer()); + + $this->assertStringContainsString('class="footnote-backref"', $output); + $this->assertStringContainsString('rev="footnote"', $output); + $this->assertStringContainsString('role="doc-backlink"', $output); + } + + public function testCustomClassAddedViaAST(): void + { + $renderer = new FootnoteBackrefRenderer(); + $renderer->setConfiguration(new Configuration()); + + $fakeReference = new Reference('label', 'dest', 'title'); + $footnoteBackref = new FootnoteBackref($fakeReference); + $footnoteBackref->data['attributes']['class'] = 'custom class'; + + $output = $renderer->render($footnoteBackref, new FakeHtmlRenderer()); + + $this->assertStringContainsString('class="custom class"', $output); + } + + public function testClassConfiguration(): void + { + $renderer = new FootnoteBackrefRenderer(); + $renderer->setConfiguration(new Configuration(['footnote' => ['backref_class' => 'my-custom-class']])); + + $fakeReference = new Reference('label', 'dest', 'title'); + $footnoteBackref = new FootnoteBackref($fakeReference); + + $output = $renderer->render($footnoteBackref, new FakeHtmlRenderer()); + + $this->assertStringContainsString('class="my-custom-class"', $output); + } +} diff --git a/tests/unit/Extension/Footnote/Renderer/FootnoteContainerRendererTest.php b/tests/unit/Extension/Footnote/Renderer/FootnoteContainerRendererTest.php new file mode 100644 index 0000000000..1c6c462c3e --- /dev/null +++ b/tests/unit/Extension/Footnote/Renderer/FootnoteContainerRendererTest.php @@ -0,0 +1,75 @@ + + * + * 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\Tests\Unit\Extension\Footnote\Renderer; + +use League\CommonMark\Extension\Footnote\Node\FootnoteContainer; +use League\CommonMark\Extension\Footnote\Renderer\FootnoteContainerRenderer; +use League\CommonMark\Tests\Unit\FakeHtmlRenderer; +use League\CommonMark\Util\Configuration; +use PHPUnit\Framework\TestCase; + +final class FootnoteContainerRendererTest extends TestCase +{ + public function testDefaultSettings(): void + { + $renderer = new FootnoteContainerRenderer(); + $renderer->setConfiguration(new Configuration()); + + $container = new FootnoteContainer(); + + $output = $renderer->render($container, new FakeHtmlRenderer(), false); + + $this->assertSame('footnotes', $output->getAttribute('class')); + $this->assertSame('doc-endnotes', $output->getAttribute('role')); + + $this->assertStringContainsString('
', $output->getContents()); + } + + public function testCustomClassAddedViaAST(): void + { + $renderer = new FootnoteContainerRenderer(); + $renderer->setConfiguration(new Configuration()); + + $container = new FootnoteContainer(); + $container->data['attributes']['class'] = 'custom class'; + + $output = $renderer->render($container, new FakeHtmlRenderer(), false); + + $this->assertSame('custom class', $output->getAttribute('class')); + } + + public function testClassConfiguration(): void + { + $renderer = new FootnoteContainerRenderer(); + $renderer->setConfiguration(new Configuration(['footnote' => ['container_class' => 'my-custom-class']])); + + $container = new FootnoteContainer(); + + $output = $renderer->render($container, new FakeHtmlRenderer(), false); + + $this->assertSame('my-custom-class', $output->getAttribute('class')); + } + + public function testAddHRConfiguration() + { + $renderer = new FootnoteContainerRenderer(); + $renderer->setConfiguration(new Configuration(['footnote' => ['container_add_hr' => false]])); + + $container = new FootnoteContainer(); + + $output = $renderer->render($container, new FakeHtmlRenderer(), false); + + $this->assertStringNotContainsString('
', $output->getContents()); + } +} diff --git a/tests/unit/Extension/Footnote/Renderer/FootnoteRefRendererTest.php b/tests/unit/Extension/Footnote/Renderer/FootnoteRefRendererTest.php new file mode 100644 index 0000000000..485bd04e69 --- /dev/null +++ b/tests/unit/Extension/Footnote/Renderer/FootnoteRefRendererTest.php @@ -0,0 +1,79 @@ + + * + * 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\Tests\Unit\Extension\Footnote\Renderer; + +use League\CommonMark\Extension\Footnote\Node\FootnoteRef; +use League\CommonMark\Extension\Footnote\Renderer\FootnoteRefRenderer; +use League\CommonMark\Reference\Reference; +use League\CommonMark\Tests\Unit\FakeHtmlRenderer; +use League\CommonMark\Util\Configuration; +use PHPUnit\Framework\TestCase; + +final class FootnoteRefRendererTest extends TestCase +{ + public function testDefaultAttributes(): void + { + $renderer = new FootnoteRefRenderer(); + $renderer->setConfiguration(new Configuration()); + + $fakeReference = new Reference('label', 'dest', 'title'); + $footnoteRef = new FootnoteRef($fakeReference); + + $output = (string) $renderer->render($footnoteRef, new FakeHtmlRenderer()); + + $this->assertStringContainsString('id="fnref:label"', $output); + $this->assertStringContainsString('class="footnote-ref"', $output); + $this->assertStringContainsString('role="doc-noteref"', $output); + } + + public function testCustomClassAddedViaAST(): void + { + $renderer = new FootnoteRefRenderer(); + $renderer->setConfiguration(new Configuration()); + + $fakeReference = new Reference('label', 'dest', 'title'); + $footnoteRef = new FootnoteRef($fakeReference); + $footnoteRef->data['attributes']['class'] = 'custom class'; + + $output = (string) $renderer->render($footnoteRef, new FakeHtmlRenderer()); + + $this->assertStringContainsString('class="custom class"', $output); + } + + public function testClassConfiguration(): void + { + $renderer = new FootnoteRefRenderer(); + $renderer->setConfiguration(new Configuration(['footnote' => ['ref_class' => 'my-custom-class']])); + + $fakeReference = new Reference('label', 'dest', 'title'); + $footnoteRef = new FootnoteRef($fakeReference); + + $output = (string) $renderer->render($footnoteRef, new FakeHtmlRenderer()); + + $this->assertStringContainsString('class="my-custom-class"', $output); + } + + public function testIdPrefixConfiguration(): void + { + $renderer = new FootnoteRefRenderer(); + $renderer->setConfiguration(new Configuration(['footnote' => ['ref_id_prefix' => 'custom-']])); + + $fakeReference = new Reference('label', 'dest', 'title'); + $footnoteRef = new FootnoteRef($fakeReference); + + $output = (string) $renderer->render($footnoteRef, new FakeHtmlRenderer()); + + $this->assertStringContainsString('id="custom-label"', $output); + } +} diff --git a/tests/unit/Extension/Footnote/Renderer/FootnoteRendererTest.php b/tests/unit/Extension/Footnote/Renderer/FootnoteRendererTest.php new file mode 100644 index 0000000000..4cbfad30fb --- /dev/null +++ b/tests/unit/Extension/Footnote/Renderer/FootnoteRendererTest.php @@ -0,0 +1,79 @@ + + * + * 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\Tests\Unit\Extension\Footnote\Renderer; + +use League\CommonMark\Extension\Footnote\Node\Footnote; +use League\CommonMark\Extension\Footnote\Renderer\FootnoteRenderer; +use League\CommonMark\Reference\Reference; +use League\CommonMark\Tests\Unit\FakeHtmlRenderer; +use League\CommonMark\Util\Configuration; +use PHPUnit\Framework\TestCase; + +final class FootnoteRendererTest extends TestCase +{ + public function testDefaultAttributes(): void + { + $renderer = new FootnoteRenderer(); + $renderer->setConfiguration(new Configuration()); + + $fakeReference = new Reference('label', 'dest', 'title'); + $footnote = new Footnote($fakeReference); + + $output = $renderer->render($footnote, new FakeHtmlRenderer(), false); + + $this->assertSame('footnote', $output->getAttribute('class')); + $this->assertSame('fn:label', $output->getAttribute('id')); + $this->assertSame('doc-endnote', $output->getAttribute('role')); + } + + public function testCustomClassAddedViaAST(): void + { + $renderer = new FootnoteRenderer(); + $renderer->setConfiguration(new Configuration()); + + $fakeReference = new Reference('label', 'dest', 'title'); + $footnote = new Footnote($fakeReference); + $footnote->data['attributes']['class'] = 'custom class'; + + $output = $renderer->render($footnote, new FakeHtmlRenderer(), false); + + $this->assertSame('custom class', $output->getAttribute('class')); + } + + public function testClassConfiguration(): void + { + $renderer = new FootnoteRenderer(); + $renderer->setConfiguration(new Configuration(['footnote' => ['footnote_class' => 'my-custom-class']])); + + $fakeReference = new Reference('label', 'dest', 'title'); + $footnote = new Footnote($fakeReference); + + $output = $renderer->render($footnote, new FakeHtmlRenderer(), false); + + $this->assertSame('my-custom-class', $output->getAttribute('class')); + } + + public function testIdPrefixConfiguration(): void + { + $renderer = new FootnoteRenderer(); + $renderer->setConfiguration(new Configuration(['footnote' => ['footnote_id_prefix' => 'custom-']])); + + $fakeReference = new Reference('label', 'dest', 'title'); + $footnote = new Footnote($fakeReference); + + $output = $renderer->render($footnote, new FakeHtmlRenderer(), false); + + $this->assertSame('custom-label', $output->getAttribute('id')); + } +}