Primo Committ

This commit is contained in:
paoloar77
2024-05-07 12:17:25 +02:00
commit e73d0e5113
7204 changed files with 884387 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
*
* 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\Attributes;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\Attributes\Event\AttributesListener;
use League\CommonMark\Extension\Attributes\Parser\AttributesBlockParser;
use League\CommonMark\Extension\Attributes\Parser\AttributesInlineParser;
use League\CommonMark\Extension\ExtensionInterface;
final class AttributesExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addBlockParser(new AttributesBlockParser());
$environment->addInlineParser(new AttributesInlineParser());
$environment->addEventListener(DocumentParsedEvent::class, [new AttributesListener(), 'processDocument']);
}
}

View File

@@ -0,0 +1,141 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
*
* 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\Attributes\Event;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\FencedCode;
use League\CommonMark\Block\Element\ListBlock;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\Attributes\Node\Attributes;
use League\CommonMark\Extension\Attributes\Node\AttributesInline;
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
use League\CommonMark\Inline\Element\AbstractInline;
use League\CommonMark\Node\Node;
final class AttributesListener
{
private const DIRECTION_PREFIX = 'prefix';
private const DIRECTION_SUFFIX = 'suffix';
public function processDocument(DocumentParsedEvent $event): void
{
$walker = $event->getDocument()->walker();
while ($event = $walker->next()) {
$node = $event->getNode();
if (!$node instanceof AttributesInline && ($event->isEntering() || !$node instanceof Attributes)) {
continue;
}
[$target, $direction] = self::findTargetAndDirection($node);
if ($target instanceof AbstractBlock || $target instanceof AbstractInline) {
$parent = $target->parent();
if ($parent instanceof ListItem && $parent->parent() instanceof ListBlock && $parent->parent()->isTight()) {
$target = $parent;
}
if ($direction === self::DIRECTION_SUFFIX) {
$attributes = AttributesHelper::mergeAttributes($target, $node->getAttributes());
} else {
$attributes = AttributesHelper::mergeAttributes($node->getAttributes(), $target);
}
$target->data['attributes'] = $attributes;
}
if ($node instanceof AbstractBlock && $node->endsWithBlankLine() && $node->next() && $node->previous()) {
$previous = $node->previous();
if ($previous instanceof AbstractBlock) {
$previous->setLastLineBlank(true);
}
}
$node->detach();
}
}
/**
* @param Node $node
*
* @return array<Node|string|null>
*/
private static function findTargetAndDirection(Node $node): array
{
$target = null;
$direction = null;
$previous = $next = $node;
while (true) {
$previous = self::getPrevious($previous);
$next = self::getNext($next);
if ($previous === null && $next === null) {
if (!$node->parent() instanceof FencedCode) {
$target = $node->parent();
$direction = self::DIRECTION_SUFFIX;
}
break;
}
if ($node instanceof AttributesInline && ($previous === null || ($previous instanceof AbstractInline && $node->isBlock()))) {
continue;
}
if ($previous !== null && !self::isAttributesNode($previous)) {
$target = $previous;
$direction = self::DIRECTION_SUFFIX;
break;
}
if ($next !== null && !self::isAttributesNode($next)) {
$target = $next;
$direction = self::DIRECTION_PREFIX;
break;
}
}
return [$target, $direction];
}
private static function getPrevious(?Node $node = null): ?Node
{
$previous = $node instanceof Node ? $node->previous() : null;
if ($previous instanceof AbstractBlock && $previous->endsWithBlankLine()) {
$previous = null;
}
return $previous;
}
private static function getNext(?Node $node = null): ?Node
{
$next = $node instanceof Node ? $node->next() : null;
if ($node instanceof AbstractBlock && $node->endsWithBlankLine()) {
$next = null;
}
return $next;
}
private static function isAttributesNode(Node $node): bool
{
return $node instanceof Attributes || $node instanceof AttributesInline;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
*
* 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\Attributes\Node;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Cursor;
final class Attributes extends AbstractBlock
{
/** @var array<string, mixed> */
private $attributes;
/**
* @param array<string, mixed> $attributes
*/
public function __construct(array $attributes)
{
$this->attributes = $attributes;
}
/**
* @return array<string, mixed>
*/
public function getAttributes(): array
{
return $this->attributes;
}
public function canContain(AbstractBlock $block): bool
{
return false;
}
public function isCode(): bool
{
return false;
}
public function matchesNextLine(Cursor $cursor): bool
{
$this->setLastLineBlank($cursor->isBlank());
return false;
}
public function shouldLastLineBeBlank(Cursor $cursor, int $currentLineNumber): bool
{
return false;
}
}

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
*
* 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\Attributes\Node;
use League\CommonMark\Inline\Element\AbstractInline;
final class AttributesInline extends AbstractInline
{
/** @var array<string, mixed> */
public $attributes;
/** @var bool */
public $block;
/**
* @param array<string, mixed> $attributes
* @param bool $block
*/
public function __construct(array $attributes, bool $block)
{
$this->attributes = $attributes;
$this->block = $block;
$this->data = ['delim' => true]; // TODO: Re-implement as a delimiter?
}
/**
* @return array<string, mixed>
*/
public function getAttributes(): array
{
return $this->attributes;
}
public function isBlock(): bool
{
return $this->block;
}
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
*
* 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\Attributes\Parser;
use League\CommonMark\Block\Parser\BlockParserInterface;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
use League\CommonMark\Extension\Attributes\Node\Attributes;
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
final class AttributesBlockParser implements BlockParserInterface
{
public function parse(ContextInterface $context, Cursor $cursor): bool
{
$state = $cursor->saveState();
$attributes = AttributesHelper::parseAttributes($cursor);
if ($attributes === []) {
return false;
}
if ($cursor->getNextNonSpaceCharacter() !== null) {
$cursor->restoreState($state);
return false;
}
$context->addBlock(new Attributes($attributes));
$context->setBlocksParsed(true);
return true;
}
}

View File

@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
*
* 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\Attributes\Parser;
use League\CommonMark\Extension\Attributes\Node\AttributesInline;
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
use League\CommonMark\Inline\Element\Text;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\InlineParserContext;
final class AttributesInlineParser implements InlineParserInterface
{
/**
* {@inheritdoc}
*/
public function getCharacters(): array
{
return ['{'];
}
public function parse(InlineParserContext $inlineContext): bool
{
$cursor = $inlineContext->getCursor();
$char = (string) $cursor->peek(-1);
$attributes = AttributesHelper::parseAttributes($cursor);
if ($attributes === []) {
return false;
}
if ($char === ' ' && ($previousInline = $inlineContext->getContainer()->lastChild()) instanceof Text) {
$previousInline->setContent(\rtrim($previousInline->getContent(), ' '));
}
if ($char === '') {
$cursor->advanceToNextNonSpaceOrNewline();
}
$node = new AttributesInline($attributes, $char === ' ' || $char === '');
$inlineContext->getContainer()->appendChild($node);
return true;
}
}

View File

@@ -0,0 +1,130 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
*
* 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\Attributes\Util;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Cursor;
use League\CommonMark\Inline\Element\AbstractInline;
use League\CommonMark\Util\RegexHelper;
/**
* @internal
*/
final class AttributesHelper
{
/**
* @param Cursor $cursor
*
* @return array<string, mixed>
*/
public static function parseAttributes(Cursor $cursor): array
{
$state = $cursor->saveState();
$cursor->advanceToNextNonSpaceOrNewline();
if ($cursor->getCharacter() !== '{') {
$cursor->restoreState($state);
return [];
}
$cursor->advanceBy(1);
if ($cursor->getCharacter() === ':') {
$cursor->advanceBy(1);
}
$attributes = [];
$regex = '/^\s*([.#][_a-z0-9-]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . ')(?<!})\s*/i';
while ($attribute = \trim((string) $cursor->match($regex))) {
if ($attribute[0] === '#') {
$attributes['id'] = \substr($attribute, 1);
continue;
}
if ($attribute[0] === '.') {
$attributes['class'][] = \substr($attribute, 1);
continue;
}
[$name, $value] = \explode('=', $attribute, 2);
$first = $value[0];
$last = \substr($value, -1);
if ((($first === '"' && $last === '"') || ($first === "'" && $last === "'")) && \strlen($value) > 1) {
$value = \substr($value, 1, -1);
}
if (\strtolower(\trim($name)) === 'class') {
foreach (\array_filter(\explode(' ', \trim($value))) as $class) {
$attributes['class'][] = $class;
}
} else {
$attributes[trim($name)] = trim($value);
}
}
if ($cursor->match('/}/') === null) {
$cursor->restoreState($state);
return [];
}
if ($attributes === []) {
$cursor->restoreState($state);
return [];
}
if (isset($attributes['class'])) {
$attributes['class'] = \implode(' ', (array) $attributes['class']);
}
return $attributes;
}
/**
* @param AbstractBlock|AbstractInline|array<string, mixed> $attributes1
* @param AbstractBlock|AbstractInline|array<string, mixed> $attributes2
*
* @return array<string, mixed>
*/
public static function mergeAttributes($attributes1, $attributes2): array
{
$attributes = [];
foreach ([$attributes1, $attributes2] as $arg) {
if ($arg instanceof AbstractBlock || $arg instanceof AbstractInline) {
$arg = $arg->data['attributes'] ?? [];
}
/** @var array<string, mixed> $arg */
$arg = (array) $arg;
if (isset($arg['class'])) {
foreach (\array_filter(\explode(' ', \trim($arg['class']))) as $class) {
$attributes['class'][] = $class;
}
unset($arg['class']);
}
$attributes = \array_merge($attributes, $arg);
}
if (isset($attributes['class'])) {
$attributes['class'] = \implode(' ', $attributes['class']);
}
return $attributes;
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Autolink;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\ExtensionInterface;
final class AutolinkExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addEventListener(DocumentParsedEvent::class, new EmailAutolinkProcessor());
$environment->addEventListener(DocumentParsedEvent::class, new UrlAutolinkProcessor());
}
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Autolink;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Inline\Element\Link;
use League\CommonMark\Inline\Element\Text;
final class EmailAutolinkProcessor
{
const REGEX = '/([A-Za-z0-9.\-_+]+@[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_.]+)/';
/**
* @param DocumentParsedEvent $e
*
* @return void
*/
public function __invoke(DocumentParsedEvent $e)
{
$walker = $e->getDocument()->walker();
while ($event = $walker->next()) {
$node = $event->getNode();
if ($node instanceof Text && !($node->parent() instanceof Link)) {
self::processAutolinks($node);
}
}
}
private static function processAutolinks(Text $node): void
{
$contents = \preg_split(self::REGEX, $node->getContent(), -1, PREG_SPLIT_DELIM_CAPTURE);
if ($contents === false || \count($contents) === 1) {
return;
}
$leftovers = '';
foreach ($contents as $i => $content) {
if ($i % 2 === 0) {
$text = $leftovers . $content;
if ($text !== '') {
$node->insertBefore(new Text($leftovers . $content));
}
$leftovers = '';
continue;
}
// Does the URL end with punctuation that should be stripped?
if (\substr($content, -1) === '.') {
// Add the punctuation later
$content = \substr($content, 0, -1);
$leftovers = '.';
}
// The last character cannot be - or _
if (\in_array(\substr($content, -1), ['-', '_'])) {
$node->insertBefore(new Text($content . $leftovers));
$leftovers = '';
continue;
}
$node->insertBefore(new Link('mailto:' . $content, $content));
}
$node->detach();
}
}

View File

@@ -0,0 +1,96 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Autolink;
use League\CommonMark\Extension\Mention\MentionParser;
use League\CommonMark\Inline\Element\Link;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\InlineParserContext;
@trigger_error(sprintf('%s is deprecated; use %s instead', InlineMentionParser::class, MentionParser::class), E_USER_DEPRECATED);
/**
* @deprecated Use MentionParser instead
*/
final class InlineMentionParser implements InlineParserInterface
{
/** @var string */
private $linkPattern;
/** @var string */
private $handleRegex;
/**
* @param string $linkPattern
* @param string $handleRegex
*/
public function __construct($linkPattern, $handleRegex = '/^[A-Za-z0-9_]+(?!\w)/')
{
$this->linkPattern = $linkPattern;
$this->handleRegex = $handleRegex;
}
public function getCharacters(): array
{
return ['@'];
}
public function parse(InlineParserContext $inlineContext): bool
{
$cursor = $inlineContext->getCursor();
// The @ symbol must not have any other characters immediately prior
$previousChar = $cursor->peek(-1);
if ($previousChar !== null && $previousChar !== ' ') {
// peek() doesn't modify the cursor, so no need to restore state first
return false;
}
// Save the cursor state in case we need to rewind and bail
$previousState = $cursor->saveState();
// Advance past the @ symbol to keep parsing simpler
$cursor->advance();
// Parse the handle
$handle = $cursor->match($this->handleRegex);
if (empty($handle)) {
// Regex failed to match; this isn't a valid Twitter handle
$cursor->restoreState($previousState);
return false;
}
$url = \sprintf($this->linkPattern, $handle);
$inlineContext->getContainer()->appendChild(new Link($url, '@' . $handle));
return true;
}
/**
* @return InlineMentionParser
*/
public static function createTwitterHandleParser()
{
return new self('https://twitter.com/%s', '/^[A-Za-z0-9_]{1,15}(?!\w)/');
}
/**
* @return InlineMentionParser
*/
public static function createGithubHandleParser()
{
// RegEx adapted from https://github.com/shinnn/github-username-regex/blob/master/index.js
return new self('https://www.github.com/%s', '/^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/');
}
}

View File

@@ -0,0 +1,153 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Autolink;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Inline\Element\Link;
use League\CommonMark\Inline\Element\Text;
final class UrlAutolinkProcessor
{
// RegEx adapted from https://github.com/symfony/symfony/blob/4.2/src/Symfony/Component/Validator/Constraints/UrlValidator.php
const REGEX = '~
(?<=^|[ \\t\\n\\x0b\\x0c\\x0d*_\\~\\(]) # Can only come at the beginning of a line, after whitespace, or certain delimiting characters
(
# Must start with a supported scheme + auth, or "www"
(?:
(?:%s):// # protocol
(?:([\.\pL\pN-]+:)?([\.\pL\pN-]+)@)? # basic auth
|www\.)
(?:
(?:[\pL\pN\pS\-\.])+(?:\.?(?:[\pL\pN]|xn\-\-[\pL\pN-]+)+\.?) # a domain name
| # or
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # an IP address
| # or
\[
(?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::))))
\] # an IPv6 address
)
(?::[0-9]+)? # a port (optional)
(?:/ (?:[\pL\pN\-._\~!$&\'()*+,;=:@]|%%[0-9A-Fa-f]{2})* )* # a path
(?:\? (?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a query (optional)
(?:\# (?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a fragment (optional)
)~ixu';
/** @var string */
private $finalRegex;
/**
* @param array<int, string> $allowedProtocols
*/
public function __construct(array $allowedProtocols = ['http', 'https', 'ftp'])
{
$this->finalRegex = \sprintf(self::REGEX, \implode('|', $allowedProtocols));
}
/**
* @param DocumentParsedEvent $e
*
* @return void
*/
public function __invoke(DocumentParsedEvent $e)
{
$walker = $e->getDocument()->walker();
while ($event = $walker->next()) {
$node = $event->getNode();
if ($node instanceof Text && !($node->parent() instanceof Link)) {
self::processAutolinks($node, $this->finalRegex);
}
}
}
private static function processAutolinks(Text $node, string $regex): void
{
$contents = \preg_split($regex, $node->getContent(), -1, PREG_SPLIT_DELIM_CAPTURE);
if ($contents === false || \count($contents) === 1) {
return;
}
$leftovers = '';
foreach ($contents as $i => $content) {
// Even-indexed elements are things before/after the URLs
if ($i % 2 === 0) {
// Insert any left-over characters here as well
$text = $leftovers . $content;
if ($text !== '') {
$node->insertBefore(new Text($leftovers . $content));
}
$leftovers = '';
continue;
}
$leftovers = '';
// Does the URL end with punctuation that should be stripped?
if (\preg_match('/(.+)([?!.,:*_~]+)$/', $content, $matches)) {
// Add the punctuation later
$content = $matches[1];
$leftovers = $matches[2];
}
// Does the URL end with something that looks like an entity reference?
if (\preg_match('/(.+)(&[A-Za-z0-9]+;)$/', $content, $matches)) {
$content = $matches[1];
$leftovers = $matches[2] . $leftovers;
}
// Does the URL need its closing paren chopped off?
if (\substr($content, -1) === ')' && ($diff = self::diffParens($content)) > 0) {
$content = \substr($content, 0, -$diff);
$leftovers = str_repeat(')', $diff) . $leftovers;
}
self::addLink($node, $content);
}
$node->detach();
}
private static function addLink(Text $node, string $url): void
{
// Auto-prefix 'http://' onto 'www' URLs
if (\substr($url, 0, 4) === 'www.') {
$node->insertBefore(new Link('http://' . $url, $url));
return;
}
$node->insertBefore(new Link($url, $url));
}
/**
* @param string $content
*
* @return int
*/
private static function diffParens(string $content): int
{
// Scan the entire autolink for the total number of parentheses.
// If there is a greater number of closing parentheses than opening ones,
// we dont consider ANY of the last characters as part of the autolink,
// in order to facilitate including an autolink inside a parenthesis.
\preg_match_all('/[()]/', $content, $matches);
$charCount = ['(' => 0, ')' => 0];
foreach ($matches[0] as $char) {
$charCount[$char]++;
}
return $charCount[')'] - $charCount['('];
}
}

View File

@@ -0,0 +1,95 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension;
use League\CommonMark\Block\Element as BlockElement;
use League\CommonMark\Block\Parser as BlockParser;
use League\CommonMark\Block\Renderer as BlockRenderer;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Delimiter\Processor\EmphasisDelimiterProcessor;
use League\CommonMark\Inline\Element as InlineElement;
use League\CommonMark\Inline\Parser as InlineParser;
use League\CommonMark\Inline\Renderer as InlineRenderer;
use League\CommonMark\Util\ConfigurationInterface;
final class CommonMarkCoreExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment
->addBlockParser(new BlockParser\BlockQuoteParser(), 70)
->addBlockParser(new BlockParser\ATXHeadingParser(), 60)
->addBlockParser(new BlockParser\FencedCodeParser(), 50)
->addBlockParser(new BlockParser\HtmlBlockParser(), 40)
->addBlockParser(new BlockParser\SetExtHeadingParser(), 30)
->addBlockParser(new BlockParser\ThematicBreakParser(), 20)
->addBlockParser(new BlockParser\ListParser(), 10)
->addBlockParser(new BlockParser\IndentedCodeParser(), -100)
->addBlockParser(new BlockParser\LazyParagraphParser(), -200)
->addInlineParser(new InlineParser\NewlineParser(), 200)
->addInlineParser(new InlineParser\BacktickParser(), 150)
->addInlineParser(new InlineParser\EscapableParser(), 80)
->addInlineParser(new InlineParser\EntityParser(), 70)
->addInlineParser(new InlineParser\AutolinkParser(), 50)
->addInlineParser(new InlineParser\HtmlInlineParser(), 40)
->addInlineParser(new InlineParser\CloseBracketParser(), 30)
->addInlineParser(new InlineParser\OpenBracketParser(), 20)
->addInlineParser(new InlineParser\BangParser(), 10)
->addBlockRenderer(BlockElement\BlockQuote::class, new BlockRenderer\BlockQuoteRenderer(), 0)
->addBlockRenderer(BlockElement\Document::class, new BlockRenderer\DocumentRenderer(), 0)
->addBlockRenderer(BlockElement\FencedCode::class, new BlockRenderer\FencedCodeRenderer(), 0)
->addBlockRenderer(BlockElement\Heading::class, new BlockRenderer\HeadingRenderer(), 0)
->addBlockRenderer(BlockElement\HtmlBlock::class, new BlockRenderer\HtmlBlockRenderer(), 0)
->addBlockRenderer(BlockElement\IndentedCode::class, new BlockRenderer\IndentedCodeRenderer(), 0)
->addBlockRenderer(BlockElement\ListBlock::class, new BlockRenderer\ListBlockRenderer(), 0)
->addBlockRenderer(BlockElement\ListItem::class, new BlockRenderer\ListItemRenderer(), 0)
->addBlockRenderer(BlockElement\Paragraph::class, new BlockRenderer\ParagraphRenderer(), 0)
->addBlockRenderer(BlockElement\ThematicBreak::class, new BlockRenderer\ThematicBreakRenderer(), 0)
->addInlineRenderer(InlineElement\Code::class, new InlineRenderer\CodeRenderer(), 0)
->addInlineRenderer(InlineElement\Emphasis::class, new InlineRenderer\EmphasisRenderer(), 0)
->addInlineRenderer(InlineElement\HtmlInline::class, new InlineRenderer\HtmlInlineRenderer(), 0)
->addInlineRenderer(InlineElement\Image::class, new InlineRenderer\ImageRenderer(), 0)
->addInlineRenderer(InlineElement\Link::class, new InlineRenderer\LinkRenderer(), 0)
->addInlineRenderer(InlineElement\Newline::class, new InlineRenderer\NewlineRenderer(), 0)
->addInlineRenderer(InlineElement\Strong::class, new InlineRenderer\StrongRenderer(), 0)
->addInlineRenderer(InlineElement\Text::class, new InlineRenderer\TextRenderer(), 0)
;
$deprecatedUseAsterisk = $environment->getConfig('use_asterisk', ConfigurationInterface::MISSING);
if ($deprecatedUseAsterisk !== ConfigurationInterface::MISSING) {
@\trigger_error('The "use_asterisk" configuration option is deprecated in league/commonmark 1.6 and will be replaced with "commonmark > use_asterisk" in 2.0', \E_USER_DEPRECATED);
} else {
$deprecatedUseAsterisk = true;
}
if ($environment->getConfig('commonmark/use_asterisk', $deprecatedUseAsterisk)) {
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('*'));
}
$deprecatedUseUnderscore = $environment->getConfig('use_underscore', ConfigurationInterface::MISSING);
if ($deprecatedUseUnderscore !== ConfigurationInterface::MISSING) {
@\trigger_error('The "use_underscore" configuration option is deprecated in league/commonmark 1.6 and will be replaced with "commonmark > use_underscore" in 2.0', \E_USER_DEPRECATED);
} else {
$deprecatedUseUnderscore = true;
}
if ($environment->getConfig('commonmark/use_underscore', $deprecatedUseUnderscore)) {
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('_'));
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\DisallowedRawHtml;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
final class DisallowedRawHtmlBlockRenderer implements BlockRendererInterface, ConfigurationAwareInterface
{
/** @var BlockRendererInterface */
private $htmlBlockRenderer;
public function __construct(BlockRendererInterface $htmlBlockRenderer)
{
$this->htmlBlockRenderer = $htmlBlockRenderer;
}
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
$rendered = $this->htmlBlockRenderer->render($block, $htmlRenderer, $inTightList);
if ($rendered === '') {
return '';
}
// Match these types of tags: <title> </title> <title x="sdf"> <title/> <title />
return preg_replace('/<(\/?(?:title|textarea|style|xmp|iframe|noembed|noframes|script|plaintext)[ \/>])/i', '&lt;$1', $rendered);
}
public function setConfiguration(ConfigurationInterface $configuration)
{
if ($this->htmlBlockRenderer instanceof ConfigurationAwareInterface) {
$this->htmlBlockRenderer->setConfiguration($configuration);
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\DisallowedRawHtml;
use League\CommonMark\Block\Element\HtmlBlock;
use League\CommonMark\Block\Renderer\HtmlBlockRenderer;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Inline\Element\HtmlInline;
use League\CommonMark\Inline\Renderer\HtmlInlineRenderer;
final class DisallowedRawHtmlExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addBlockRenderer(HtmlBlock::class, new DisallowedRawHtmlBlockRenderer(new HtmlBlockRenderer()), 50);
$environment->addInlineRenderer(HtmlInline::class, new DisallowedRawHtmlInlineRenderer(new HtmlInlineRenderer()), 50);
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\DisallowedRawHtml;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\Inline\Element\AbstractInline;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
final class DisallowedRawHtmlInlineRenderer implements InlineRendererInterface, ConfigurationAwareInterface
{
/** @var InlineRendererInterface */
private $htmlInlineRenderer;
public function __construct(InlineRendererInterface $htmlBlockRenderer)
{
$this->htmlInlineRenderer = $htmlBlockRenderer;
}
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
{
$rendered = $this->htmlInlineRenderer->render($inline, $htmlRenderer);
if ($rendered === '') {
return '';
}
// Match these types of tags: <title> </title> <title x="sdf"> <title/> <title />
return preg_replace('/<(\/?(?:title|textarea|style|xmp|iframe|noembed|noframes|script|plaintext)[ \/>])/i', '&lt;$1', $rendered);
}
public function setConfiguration(ConfigurationInterface $configuration)
{
if ($this->htmlInlineRenderer instanceof ConfigurationAwareInterface) {
$this->htmlInlineRenderer->setConfiguration($configuration);
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension;
use League\CommonMark\ConfigurableEnvironmentInterface;
interface ExtensionInterface
{
/**
* @param ConfigurableEnvironmentInterface $environment
*
* @return void
*/
public function register(ConfigurableEnvironmentInterface $environment);
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\ExternalLink;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\ExtensionInterface;
final class ExternalLinkExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addEventListener(DocumentParsedEvent::class, new ExternalLinkProcessor($environment), -50);
}
}

View File

@@ -0,0 +1,131 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\ExternalLink;
use League\CommonMark\EnvironmentInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Inline\Element\Link;
final class ExternalLinkProcessor
{
public const APPLY_NONE = '';
public const APPLY_ALL = 'all';
public const APPLY_EXTERNAL = 'external';
public const APPLY_INTERNAL = 'internal';
/** @var EnvironmentInterface */
private $environment;
public function __construct(EnvironmentInterface $environment)
{
$this->environment = $environment;
}
/**
* @param DocumentParsedEvent $e
*
* @return void
*/
public function __invoke(DocumentParsedEvent $e)
{
$internalHosts = $this->environment->getConfig('external_link/internal_hosts', []);
$openInNewWindow = $this->environment->getConfig('external_link/open_in_new_window', false);
$classes = $this->environment->getConfig('external_link/html_class', '');
$walker = $e->getDocument()->walker();
while ($event = $walker->next()) {
if ($event->isEntering() && $event->getNode() instanceof Link) {
/** @var Link $link */
$link = $event->getNode();
$host = parse_url($link->getUrl(), PHP_URL_HOST);
if (empty($host)) {
// Something is terribly wrong with this URL
continue;
}
if (self::hostMatches($host, $internalHosts)) {
$link->data['external'] = false;
$this->applyRelAttribute($link, false);
continue;
}
// Host does not match our list
$this->markLinkAsExternal($link, $openInNewWindow, $classes);
}
}
}
private function markLinkAsExternal(Link $link, bool $openInNewWindow, string $classes): void
{
$link->data['external'] = true;
$link->data['attributes'] = $link->getData('attributes', []);
$this->applyRelAttribute($link, true);
if ($openInNewWindow) {
$link->data['attributes']['target'] = '_blank';
}
if (!empty($classes)) {
$link->data['attributes']['class'] = trim(($link->data['attributes']['class'] ?? '') . ' ' . $classes);
}
}
private function applyRelAttribute(Link $link, bool $isExternal): void
{
$rel = [];
$options = [
'nofollow' => $this->environment->getConfig('external_link/nofollow', self::APPLY_NONE),
'noopener' => $this->environment->getConfig('external_link/noopener', self::APPLY_EXTERNAL),
'noreferrer' => $this->environment->getConfig('external_link/noreferrer', self::APPLY_EXTERNAL),
];
foreach ($options as $type => $option) {
switch (true) {
case $option === self::APPLY_ALL:
case $isExternal && $option === self::APPLY_EXTERNAL:
case !$isExternal && $option === self::APPLY_INTERNAL:
$rel[] = $type;
}
}
if ($rel === []) {
return;
}
$link->data['attributes']['rel'] = \implode(' ', $rel);
}
/**
* @param string $host
* @param mixed $compareTo
*
* @return bool
*
* @internal This method is only public so we can easily test it. DO NOT USE THIS OUTSIDE OF THIS EXTENSION!
*/
public static function hostMatches(string $host, $compareTo)
{
foreach ((array) $compareTo as $c) {
if (strpos($c, '/') === 0) {
if (preg_match($c, $host)) {
return true;
}
} elseif ($c === $host) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,61 @@
<?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;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
final class AnonymousFootnotesListener implements ConfigurationAwareInterface
{
/** @var ConfigurationInterface */
private $config;
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(),
'#' . $this->config->get('footnote/ref_id_prefix', '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);
}
}
}
public function setConfiguration(ConfigurationInterface $config): void
{
$this->config = $config;
}
}

View File

@@ -0,0 +1,100 @@
<?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;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
final class GatherFootnotesListener implements ConfigurationAwareInterface
{
/** @var ConfigurationInterface */
private $config;
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(
'#' . $this->config->get('footnote/footnote_id_prefix', 'fn:') . $node->getReference()->getDestination(),
[]
);
/** @var Reference $backref */
foreach ($backrefs as $backref) {
$node->addBackref(new FootnoteBackref(new Reference(
$backref->getLabel(),
'#' . $this->config->get('footnote/ref_id_prefix', '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;
}
public function setConfiguration(ConfigurationInterface $config): void
{
$this->config = $config;
}
}

View File

@@ -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++;
}
}
}
}

View File

@@ -0,0 +1,53 @@
<?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;
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']);
}
}

View File

@@ -0,0 +1,75 @@
<?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\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;
}
}

View File

@@ -0,0 +1,37 @@
<?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\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;
}
}

View File

@@ -0,0 +1,39 @@
<?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\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;
}
}

View File

@@ -0,0 +1,56 @@
<?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\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<mixed> $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;
}
}

View File

@@ -0,0 +1,85 @@
<?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\Parser;
use League\CommonMark\Extension\Footnote\Node\FootnoteRef;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\InlineParserContext;
use League\CommonMark\Normalizer\SlugNormalizer;
use League\CommonMark\Normalizer\TextNormalizerInterface;
use League\CommonMark\Reference\Reference;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
final class AnonymousFootnoteRefParser implements InlineParserInterface, ConfigurationAwareInterface
{
/** @var ConfigurationInterface */
private $config;
/** @var TextNormalizerInterface */
private $slugNormalizer;
public function __construct()
{
$this->slugNormalizer = new SlugNormalizer();
}
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 = $this->slugNormalizer->normalize($label);
$refLabel = \mb_substr($refLabel, 0, 20);
return new Reference(
$refLabel,
'#' . $this->config->get('footnote/footnote_id_prefix', 'fn:') . $refLabel,
$label
);
}
public function setConfiguration(ConfigurationInterface $config): void
{
$this->config = $config;
}
}

View File

@@ -0,0 +1,63 @@
<?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\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::matchFirst(
'/^\[\^([^\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)
);
}
}

View File

@@ -0,0 +1,72 @@
<?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\Parser;
use League\CommonMark\Extension\Footnote\Node\FootnoteRef;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\InlineParserContext;
use League\CommonMark\Reference\Reference;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
final class FootnoteRefParser implements InlineParserInterface, ConfigurationAwareInterface
{
/** @var ConfigurationInterface */
private $config;
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,
'#' . $this->config->get('footnote/footnote_id_prefix', 'fn:') . $label,
$label
);
}
public function setConfiguration(ConfigurationInterface $config): void
{
$this->config = $config;
}
}

View File

@@ -0,0 +1,49 @@
<?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\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 '&nbsp;' . new HtmlElement('a', $attrs, '&#8617;', true);
}
public function setConfiguration(ConfigurationInterface $configuration)
{
$this->config = $configuration;
}
}

View File

@@ -0,0 +1,52 @@
<?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\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;
}
}

View File

@@ -0,0 +1,62 @@
<?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\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;
}
}

View File

@@ -0,0 +1,64 @@
<?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\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;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
final class GithubFlavoredMarkdownExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new DisallowedRawHtmlExtension());
$environment->addExtension(new StrikethroughExtension());
$environment->addExtension(new TableExtension());
$environment->addExtension(new TaskListExtension());
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\HeadingPermalink;
use League\CommonMark\Inline\Element\AbstractInline;
/**
* Represents an anchor link within a heading
*/
final class HeadingPermalink extends AbstractInline
{
/** @var string */
private $slug;
public function __construct(string $slug)
{
$this->slug = $slug;
}
public function getSlug(): string
{
return $this->slug;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\HeadingPermalink;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\ExtensionInterface;
/**
* Extension which automatically anchor links to heading elements
*/
final class HeadingPermalinkExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addEventListener(DocumentParsedEvent::class, new HeadingPermalinkProcessor(), -100);
$environment->addInlineRenderer(HeadingPermalink::class, new HeadingPermalinkRenderer());
}
}

View File

@@ -0,0 +1,147 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\HeadingPermalink;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Block\Element\Heading;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Exception\InvalidOptionException;
use League\CommonMark\Extension\HeadingPermalink\Slug\SlugGeneratorInterface as DeprecatedSlugGeneratorInterface;
use League\CommonMark\Inline\Element\AbstractStringContainer;
use League\CommonMark\Node\Node;
use League\CommonMark\Normalizer\SlugNormalizer;
use League\CommonMark\Normalizer\TextNormalizerInterface;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
/**
* Searches the Document for Heading elements and adds HeadingPermalinks to each one
*/
final class HeadingPermalinkProcessor implements ConfigurationAwareInterface
{
const INSERT_BEFORE = 'before';
const INSERT_AFTER = 'after';
/** @var TextNormalizerInterface|DeprecatedSlugGeneratorInterface */
private $slugNormalizer;
/** @var ConfigurationInterface */
private $config;
/**
* @param TextNormalizerInterface|DeprecatedSlugGeneratorInterface|null $slugNormalizer
*/
public function __construct($slugNormalizer = null)
{
if ($slugNormalizer instanceof DeprecatedSlugGeneratorInterface) {
@trigger_error(sprintf('Passing a %s into the %s constructor is deprecated; use a %s instead', DeprecatedSlugGeneratorInterface::class, self::class, TextNormalizerInterface::class), E_USER_DEPRECATED);
}
$this->slugNormalizer = $slugNormalizer ?? new SlugNormalizer();
}
public function setConfiguration(ConfigurationInterface $configuration)
{
$this->config = $configuration;
}
public function __invoke(DocumentParsedEvent $e): void
{
$this->useNormalizerFromConfigurationIfProvided();
$walker = $e->getDocument()->walker();
while ($event = $walker->next()) {
$node = $event->getNode();
if ($node instanceof Heading && $event->isEntering()) {
$this->addHeadingLink($node, $e->getDocument());
}
}
}
private function useNormalizerFromConfigurationIfProvided(): void
{
$generator = $this->config->get('heading_permalink/slug_normalizer');
if ($generator === null) {
return;
}
if (!($generator instanceof DeprecatedSlugGeneratorInterface || $generator instanceof TextNormalizerInterface)) {
throw new InvalidOptionException('The heading_permalink/slug_normalizer option must be an instance of ' . TextNormalizerInterface::class);
}
$this->slugNormalizer = $generator;
}
private function addHeadingLink(Heading $heading, Document $document): void
{
$text = $this->getChildText($heading);
if ($this->slugNormalizer instanceof DeprecatedSlugGeneratorInterface) {
$slug = $this->slugNormalizer->createSlug($text);
} else {
$slug = $this->slugNormalizer->normalize($text, $heading);
}
$slug = $this->ensureUnique($slug, $document);
$headingLinkAnchor = new HeadingPermalink($slug);
switch ($this->config->get('heading_permalink/insert', 'before')) {
case self::INSERT_BEFORE:
$heading->prependChild($headingLinkAnchor);
return;
case self::INSERT_AFTER:
$heading->appendChild($headingLinkAnchor);
return;
default:
throw new \RuntimeException("Invalid configuration value for heading_permalink/insert; expected 'before' or 'after'");
}
}
/**
* @deprecated Not needed in 2.0
*/
private function getChildText(Node $node): string
{
$text = '';
$walker = $node->walker();
while ($event = $walker->next()) {
if ($event->isEntering() && (($child = $event->getNode()) instanceof AbstractStringContainer)) {
$text .= $child->getContent();
}
}
return $text;
}
private function ensureUnique(string $proposed, Document $document): string
{
// Quick path, it's a unique ID
if (!isset($document->data['heading_ids'][$proposed])) {
$document->data['heading_ids'][$proposed] = true;
return $proposed;
}
$extension = 0;
do {
++$extension;
} while (isset($document->data['heading_ids']["$proposed-$extension"]));
$document->data['heading_ids']["$proposed-$extension"] = true;
return "$proposed-$extension";
}
}

View File

@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\HeadingPermalink;
use League\CommonMark\ElementRendererInterface;
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;
/**
* Renders the HeadingPermalink elements
*/
final class HeadingPermalinkRenderer implements InlineRendererInterface, ConfigurationAwareInterface
{
/** @deprecated */
const DEFAULT_INNER_CONTENTS = '<svg class="heading-permalink-icon" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>';
const DEFAULT_SYMBOL = '¶';
/** @var ConfigurationInterface */
private $config;
public function setConfiguration(ConfigurationInterface $configuration)
{
$this->config = $configuration;
}
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
{
if (!$inline instanceof HeadingPermalink) {
throw new \InvalidArgumentException('Incompatible inline type: ' . \get_class($inline));
}
$slug = $inline->getSlug();
$idPrefix = (string) $this->config->get('heading_permalink/id_prefix', 'user-content');
if ($idPrefix !== '') {
$idPrefix .= '-';
}
$attrs = [
'id' => $idPrefix . $slug,
'href' => '#' . $slug,
'name' => $slug,
'class' => $this->config->get('heading_permalink/html_class', 'heading-permalink'),
'aria-hidden' => 'true',
'title' => $this->config->get('heading_permalink/title', 'Permalink'),
];
$innerContents = $this->config->get('heading_permalink/inner_contents');
if ($innerContents !== null) {
@trigger_error(sprintf('The %s config option is deprecated; use %s instead', 'inner_contents', 'symbol'), E_USER_DEPRECATED);
return new HtmlElement('a', $attrs, $innerContents, false);
}
$symbol = $this->config->get('heading_permalink/symbol', self::DEFAULT_SYMBOL);
return new HtmlElement('a', $attrs, \htmlspecialchars($symbol), false);
}
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\HeadingPermalink\Slug;
use League\CommonMark\Normalizer\SlugNormalizer;
@trigger_error(sprintf('%s is deprecated; use %s instead', DefaultSlugGenerator::class, SlugNormalizer::class), E_USER_DEPRECATED);
/**
* Creates URL-friendly strings
*
* @deprecated Use League\CommonMark\Normalizer\SlugNormalizer instead
*/
final class DefaultSlugGenerator implements SlugGeneratorInterface
{
public function createSlug(string $input): string
{
// Trim whitespace
$slug = \trim($input);
// Convert to lowercase
$slug = \mb_strtolower($slug);
// Try replacing whitespace with a dash
$slug = \preg_replace('/\s+/u', '-', $slug) ?? $slug;
// Try removing characters other than letters, numbers, and marks.
$slug = \preg_replace('/[^\p{L}\p{Nd}\p{Nl}\p{M}-]+/u', '', $slug) ?? $slug;
return $slug;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\HeadingPermalink\Slug;
use League\CommonMark\Normalizer\TextNormalizerInterface;
@trigger_error(sprintf('%s is deprecated; use %s instead', SlugGeneratorInterface::class, TextNormalizerInterface::class), E_USER_DEPRECATED);
/**
* @deprecated Use League\CommonMark\Normalizer\TextNormalizerInterface instead
*/
interface SlugGeneratorInterface
{
/**
* Create a URL-friendly slug based on the given input string
*
* @param string $input
*
* @return string
*/
public function createSlug(string $input): string;
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\InlinesOnly;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Block\Element\InlineContainerInterface;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\Inline\Element\AbstractInline;
/**
* Simply renders child elements as-is, adding newlines as needed.
*/
final class ChildRenderer implements BlockRendererInterface
{
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
$out = '';
if ($block instanceof InlineContainerInterface) {
/** @var iterable<AbstractInline> $children */
$children = $block->children();
$out .= $htmlRenderer->renderInlines($children);
} else {
/** @var iterable<AbstractBlock> $children */
$children = $block->children();
$out .= $htmlRenderer->renderBlocks($children);
}
if (!($block instanceof Document)) {
$out .= "\n";
}
return $out;
}
}

View File

@@ -0,0 +1,63 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\InlinesOnly;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\Block\Parser as BlockParser;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Delimiter\Processor\EmphasisDelimiterProcessor;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Inline\Element as InlineElement;
use League\CommonMark\Inline\Parser as InlineParser;
use League\CommonMark\Inline\Renderer as InlineRenderer;
final class InlinesOnlyExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$childRenderer = new ChildRenderer();
$environment
->addBlockParser(new BlockParser\LazyParagraphParser(), -200)
->addInlineParser(new InlineParser\NewlineParser(), 200)
->addInlineParser(new InlineParser\BacktickParser(), 150)
->addInlineParser(new InlineParser\EscapableParser(), 80)
->addInlineParser(new InlineParser\EntityParser(), 70)
->addInlineParser(new InlineParser\AutolinkParser(), 50)
->addInlineParser(new InlineParser\HtmlInlineParser(), 40)
->addInlineParser(new InlineParser\CloseBracketParser(), 30)
->addInlineParser(new InlineParser\OpenBracketParser(), 20)
->addInlineParser(new InlineParser\BangParser(), 10)
->addBlockRenderer(Document::class, $childRenderer, 0)
->addBlockRenderer(Paragraph::class, $childRenderer, 0)
->addInlineRenderer(InlineElement\Code::class, new InlineRenderer\CodeRenderer(), 0)
->addInlineRenderer(InlineElement\Emphasis::class, new InlineRenderer\EmphasisRenderer(), 0)
->addInlineRenderer(InlineElement\HtmlInline::class, new InlineRenderer\HtmlInlineRenderer(), 0)
->addInlineRenderer(InlineElement\Image::class, new InlineRenderer\ImageRenderer(), 0)
->addInlineRenderer(InlineElement\Link::class, new InlineRenderer\LinkRenderer(), 0)
->addInlineRenderer(InlineElement\Newline::class, new InlineRenderer\NewlineRenderer(), 0)
->addInlineRenderer(InlineElement\Strong::class, new InlineRenderer\StrongRenderer(), 0)
->addInlineRenderer(InlineElement\Text::class, new InlineRenderer\TextRenderer(), 0)
;
if ($environment->getConfig('use_asterisk', true)) {
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('*'));
}
if ($environment->getConfig('use_underscore', true)) {
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('_'));
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Mention\Generator;
use League\CommonMark\Extension\Mention\Mention;
use League\CommonMark\Inline\Element\AbstractInline;
final class CallbackGenerator implements MentionGeneratorInterface
{
/**
* A callback function which sets the URL on the passed mention and returns the mention, return a new AbstractInline based object or null if the mention is not a match
*
* @var callable(Mention): ?AbstractInline
*/
private $callback;
public function __construct(callable $callback)
{
$this->callback = $callback;
}
public function generateMention(Mention $mention): ?AbstractInline
{
$result = \call_user_func_array($this->callback, [$mention]);
if ($result === null) {
return null;
}
if ($result instanceof AbstractInline && !($result instanceof Mention)) {
return $result;
}
if ($result instanceof Mention && $result->hasUrl()) {
return $mention;
}
throw new \RuntimeException('CallbackGenerator callable must set the URL on the passed mention and return the mention, return a new AbstractInline based object or null if the mention is not a match');
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Mention\Generator;
use League\CommonMark\Extension\Mention\Mention;
use League\CommonMark\Inline\Element\AbstractInline;
interface MentionGeneratorInterface
{
/**
* @param Mention $mention
*
* @return AbstractInline|null
*/
public function generateMention(Mention $mention): ?AbstractInline;
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Mention\Generator;
use League\CommonMark\Extension\Mention\Mention;
use League\CommonMark\Inline\Element\AbstractInline;
final class StringTemplateLinkGenerator implements MentionGeneratorInterface
{
/** @var string */
private $urlTemplate;
public function __construct(string $urlTemplate)
{
$this->urlTemplate = $urlTemplate;
}
public function generateMention(Mention $mention): ?AbstractInline
{
return $mention->setUrl(\sprintf($this->urlTemplate, $mention->getIdentifier()));
}
}

View File

@@ -0,0 +1,104 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Mention;
use League\CommonMark\Inline\Element\Link;
use League\CommonMark\Inline\Element\Text;
class Mention extends Link
{
/** @var string */
private $symbol;
/** @var string */
private $identifier;
/**
* @param string $symbol
* @param string $identifier
* @param string $label
*/
public function __construct(string $symbol, string $identifier, string $label = null)
{
$this->symbol = $symbol;
$this->identifier = $identifier;
parent::__construct('', $label ?? \sprintf('%s%s', $symbol, $identifier));
}
/**
* @return string|null
*/
public function getLabel(): ?string
{
if (($labelNode = $this->findLabelNode()) === null) {
return null;
}
return $labelNode->getContent();
}
/**
* @return string
*/
public function getIdentifier(): string
{
return $this->identifier;
}
/**
* @return string
*/
public function getSymbol(): string
{
return $this->symbol;
}
/**
* @return bool
*/
public function hasUrl(): bool
{
return !empty($this->url);
}
/**
* @param string $label
*
* @return $this
*/
public function setLabel(string $label): self
{
if (($labelNode = $this->findLabelNode()) === null) {
$labelNode = new Text();
$this->prependChild($labelNode);
}
$labelNode->setContent($label);
return $this;
}
private function findLabelNode(): ?Text
{
foreach ($this->children() as $child) {
if ($child instanceof Text) {
return $child;
}
}
return null;
}
}

View File

@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Mention;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Exception\InvalidOptionException;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Extension\Mention\Generator\MentionGeneratorInterface;
final class MentionExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$mentions = $environment->getConfig('mentions', []);
foreach ($mentions as $name => $mention) {
if (\array_key_exists('symbol', $mention)) {
@\trigger_error('The "mentions/*/symbol" configuration option is deprecated in league/commonmark 1.6; rename "symbol" to "prefix" for compatibility with 2.0', \E_USER_DEPRECATED);
$mention['prefix'] = $mention['symbol'];
}
if (\array_key_exists('pattern', $mention)) {
// v2.0 does not allow full regex patterns passed into the configuration
if (!self::isAValidPartialRegex($mention['pattern'])) {
throw new InvalidOptionException(\sprintf('Option "mentions/%s/pattern" must not include starting/ending delimiters (like "/")', $name));
}
$mention['pattern'] = '/' . $mention['pattern'] . '/i';
} elseif (\array_key_exists('regex', $mention)) {
@\trigger_error('The "mentions/*/regex" configuration option is deprecated in league/commonmark 1.6; rename "regex" to "pattern" for compatibility with 2.0', \E_USER_DEPRECATED);
$mention['pattern'] = $mention['regex'];
}
foreach (['prefix', 'pattern', 'generator'] as $key) {
if (empty($mention[$key])) {
throw new \RuntimeException("Missing \"$key\" from MentionParser configuration");
}
}
if ($mention['generator'] instanceof MentionGeneratorInterface) {
$environment->addInlineParser(new MentionParser($mention['prefix'], $mention['pattern'], $mention['generator']));
} elseif (is_string($mention['generator'])) {
$environment->addInlineParser(MentionParser::createWithStringTemplate($mention['prefix'], $mention['pattern'], $mention['generator']));
} elseif (is_callable($mention['generator'])) {
$environment->addInlineParser(MentionParser::createWithCallback($mention['prefix'], $mention['pattern'], $mention['generator']));
} else {
throw new \RuntimeException(sprintf('The "generator" provided for the MentionParser configuration must be a string template, callable, or an object that implements %s.', MentionGeneratorInterface::class));
}
}
}
private static function isAValidPartialRegex(string $regex): bool
{
$regex = '/' . $regex . '/i';
return @\preg_match($regex, '') !== false;
}
}

View File

@@ -0,0 +1,91 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Mention;
use League\CommonMark\Extension\Mention\Generator\CallbackGenerator;
use League\CommonMark\Extension\Mention\Generator\MentionGeneratorInterface;
use League\CommonMark\Extension\Mention\Generator\StringTemplateLinkGenerator;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\InlineParserContext;
final class MentionParser implements InlineParserInterface
{
/** @var string */
private $symbol;
/** @var string */
private $mentionRegex;
/** @var MentionGeneratorInterface */
private $mentionGenerator;
public function __construct(string $symbol, string $mentionRegex, MentionGeneratorInterface $mentionGenerator)
{
$this->symbol = $symbol;
$this->mentionRegex = $mentionRegex;
$this->mentionGenerator = $mentionGenerator;
}
public function getCharacters(): array
{
return [$this->symbol];
}
public function parse(InlineParserContext $inlineContext): bool
{
$cursor = $inlineContext->getCursor();
// The symbol must not have any other characters immediately prior
$previousChar = $cursor->peek(-1);
if ($previousChar !== null && \preg_match('/\w/', $previousChar)) {
// peek() doesn't modify the cursor, so no need to restore state first
return false;
}
// Save the cursor state in case we need to rewind and bail
$previousState = $cursor->saveState();
// Advance past the symbol to keep parsing simpler
$cursor->advance();
// Parse the mention match value
$identifier = $cursor->match($this->mentionRegex);
if ($identifier === null) {
// Regex failed to match; this isn't a valid mention
$cursor->restoreState($previousState);
return false;
}
$mention = $this->mentionGenerator->generateMention(new Mention($this->symbol, $identifier));
if ($mention === null) {
$cursor->restoreState($previousState);
return false;
}
$inlineContext->getContainer()->appendChild($mention);
return true;
}
public static function createWithStringTemplate(string $symbol, string $mentionRegex, string $urlTemplate): MentionParser
{
return new self($symbol, $mentionRegex, new StringTemplateLinkGenerator($urlTemplate));
}
public static function createWithCallback(string $symbol, string $mentionRegex, callable $callback): MentionParser
{
return new self($symbol, $mentionRegex, new CallbackGenerator($callback));
}
}

View File

@@ -0,0 +1,70 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\SmartPunct;
use League\CommonMark\Inline\Element\Text;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\InlineParserContext;
final class PunctuationParser implements InlineParserInterface
{
/**
* @return string[]
*/
public function getCharacters(): array
{
return ['-', '.'];
}
public function parse(InlineParserContext $inlineContext): bool
{
$cursor = $inlineContext->getCursor();
$ch = $cursor->getCharacter();
// Ellipses
if ($ch === '.' && $matched = $cursor->match('/^\\.( ?\\.)\\1/')) {
$inlineContext->getContainer()->appendChild(new Text('…'));
return true;
}
// Em/En-dashes
elseif ($ch === '-' && $matched = $cursor->match('/^(?<!-)(-{2,})/')) {
$count = strlen($matched);
$en_dash = '';
$en_count = 0;
$em_dash = '—';
$em_count = 0;
if ($count % 3 === 0) { // If divisible by 3, use all em dashes
$em_count = $count / 3;
} elseif ($count % 2 === 0) { // If divisible by 2, use all en dashes
$en_count = $count / 2;
} elseif ($count % 3 === 2) { // If 2 extra dashes, use en dash for last 2; em dashes for rest
$em_count = ($count - 2) / 3;
$en_count = 1;
} else { // Use en dashes for last 4 hyphens; em dashes for rest
$em_count = ($count - 4) / 3;
$en_count = 2;
}
$inlineContext->getContainer()->appendChild(new Text(
str_repeat($em_dash, (int) $em_count) . str_repeat($en_dash, (int) $en_count)
));
return true;
}
return false;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\SmartPunct;
use League\CommonMark\Inline\Element\AbstractStringContainer;
final class Quote extends AbstractStringContainer
{
public const DOUBLE_QUOTE = '"';
public const DOUBLE_QUOTE_OPENER = '“';
public const DOUBLE_QUOTE_CLOSER = '”';
public const SINGLE_QUOTE = "'";
public const SINGLE_QUOTE_OPENER = '';
public const SINGLE_QUOTE_CLOSER = '';
}

View File

@@ -0,0 +1,104 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\SmartPunct;
use League\CommonMark\Delimiter\Delimiter;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\InlineParserContext;
use League\CommonMark\Util\RegexHelper;
final class QuoteParser implements InlineParserInterface
{
public const DOUBLE_QUOTES = [Quote::DOUBLE_QUOTE, Quote::DOUBLE_QUOTE_OPENER, Quote::DOUBLE_QUOTE_CLOSER];
public const SINGLE_QUOTES = [Quote::SINGLE_QUOTE, Quote::SINGLE_QUOTE_OPENER, Quote::SINGLE_QUOTE_CLOSER];
/**
* @return string[]
*/
public function getCharacters(): array
{
return array_merge(self::DOUBLE_QUOTES, self::SINGLE_QUOTES);
}
/**
* Normalizes any quote characters found and manually adds them to the delimiter stack
*/
public function parse(InlineParserContext $inlineContext): bool
{
$cursor = $inlineContext->getCursor();
$normalizedCharacter = $this->getNormalizedQuoteCharacter($cursor->getCharacter());
$charBefore = $cursor->peek(-1);
if ($charBefore === null) {
$charBefore = "\n";
}
$cursor->advance();
$charAfter = $cursor->getCharacter();
if ($charAfter === null) {
$charAfter = "\n";
}
[$leftFlanking, $rightFlanking] = $this->determineFlanking($charBefore, $charAfter);
$canOpen = $leftFlanking && !$rightFlanking;
$canClose = $rightFlanking;
$node = new Quote($normalizedCharacter, ['delim' => true]);
$inlineContext->getContainer()->appendChild($node);
// Add entry to stack to this opener
$inlineContext->getDelimiterStack()->push(new Delimiter($normalizedCharacter, 1, $node, $canOpen, $canClose));
return true;
}
private function getNormalizedQuoteCharacter(string $character): string
{
if (in_array($character, self::DOUBLE_QUOTES)) {
return Quote::DOUBLE_QUOTE;
} elseif (in_array($character, self::SINGLE_QUOTES)) {
return Quote::SINGLE_QUOTE;
}
return $character;
}
/**
* @param string $charBefore
* @param string $charAfter
*
* @return bool[]
*/
private function determineFlanking(string $charBefore, string $charAfter)
{
$afterIsWhitespace = preg_match('/\pZ|\s/u', $charAfter);
$afterIsPunctuation = preg_match(RegexHelper::REGEX_PUNCTUATION, $charAfter);
$beforeIsWhitespace = preg_match('/\pZ|\s/u', $charBefore);
$beforeIsPunctuation = preg_match(RegexHelper::REGEX_PUNCTUATION, $charBefore);
$leftFlanking = !$afterIsWhitespace &&
!($afterIsPunctuation &&
!$beforeIsWhitespace &&
!$beforeIsPunctuation);
$rightFlanking = !$beforeIsWhitespace &&
!($beforeIsPunctuation &&
!$afterIsWhitespace &&
!$afterIsPunctuation);
return [$leftFlanking, $rightFlanking];
}
}

View File

@@ -0,0 +1,90 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\SmartPunct;
use League\CommonMark\Delimiter\DelimiterInterface;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
use League\CommonMark\Inline\Element\AbstractStringContainer;
final class QuoteProcessor implements DelimiterProcessorInterface
{
/** @var string */
private $normalizedCharacter;
/** @var string */
private $openerCharacter;
/** @var string */
private $closerCharacter;
private function __construct(string $char, string $opener, string $closer)
{
$this->normalizedCharacter = $char;
$this->openerCharacter = $opener;
$this->closerCharacter = $closer;
}
public function getOpeningCharacter(): string
{
return $this->normalizedCharacter;
}
public function getClosingCharacter(): string
{
return $this->normalizedCharacter;
}
public function getMinLength(): int
{
return 1;
}
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int
{
return 1;
}
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse)
{
$opener->insertAfter(new Quote($this->openerCharacter));
$closer->insertBefore(new Quote($this->closerCharacter));
}
/**
* Create a double-quote processor
*
* @param string $opener
* @param string $closer
*
* @return QuoteProcessor
*/
public static function createDoubleQuoteProcessor(string $opener = Quote::DOUBLE_QUOTE_OPENER, string $closer = Quote::DOUBLE_QUOTE_CLOSER): self
{
return new self(Quote::DOUBLE_QUOTE, $opener, $closer);
}
/**
* Create a single-quote processor
*
* @param string $opener
* @param string $closer
*
* @return QuoteProcessor
*/
public static function createSingleQuoteProcessor(string $opener = Quote::SINGLE_QUOTE_OPENER, string $closer = Quote::SINGLE_QUOTE_CLOSER): self
{
return new self(Quote::SINGLE_QUOTE, $opener, $closer);
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\SmartPunct;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
use League\CommonMark\Inline\Element\AbstractInline;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
final class QuoteRenderer implements InlineRendererInterface
{
/**
* @param Quote $inline
* @param ElementRendererInterface $htmlRenderer
*
* @return HtmlElement|string|null
*/
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
{
if (!$inline instanceof Quote) {
throw new \InvalidArgumentException(sprintf('Expected an instance of "%s", got "%s" instead', Quote::class, get_class($inline)));
}
// Handles unpaired quotes which remain after processing delimiters
if ($inline->getContent() === Quote::SINGLE_QUOTE) {
// Render as an apostrophe
return Quote::SINGLE_QUOTE_CLOSER;
} elseif ($inline->getContent() === Quote::DOUBLE_QUOTE) {
// Render as an opening quote
return Quote::DOUBLE_QUOTE_OPENER;
}
return $inline->getContent();
}
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\SmartPunct;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\Block\Renderer as CoreBlockRenderer;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Inline\Element\Text;
use League\CommonMark\Inline\Renderer as CoreInlineRenderer;
final class SmartPunctExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment
->addInlineParser(new QuoteParser(), 10)
->addInlineParser(new PunctuationParser(), 0)
->addDelimiterProcessor(QuoteProcessor::createDoubleQuoteProcessor(
$environment->getConfig('smartpunct/double_quote_opener', Quote::DOUBLE_QUOTE_OPENER),
$environment->getConfig('smartpunct/double_quote_closer', Quote::DOUBLE_QUOTE_CLOSER)
))
->addDelimiterProcessor(QuoteProcessor::createSingleQuoteProcessor(
$environment->getConfig('smartpunct/single_quote_opener', Quote::SINGLE_QUOTE_OPENER),
$environment->getConfig('smartpunct/single_quote_closer', Quote::SINGLE_QUOTE_CLOSER)
))
->addBlockRenderer(Document::class, new CoreBlockRenderer\DocumentRenderer(), 0)
->addBlockRenderer(Paragraph::class, new CoreBlockRenderer\ParagraphRenderer(), 0)
->addInlineRenderer(Quote::class, new QuoteRenderer(), 100)
->addInlineRenderer(Text::class, new CoreInlineRenderer\TextRenderer(), 0)
;
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com)
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Strikethrough;
use League\CommonMark\Inline\Element\AbstractInline;
final class Strikethrough extends AbstractInline
{
public function isContainer(): bool
{
return true;
}
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com)
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Strikethrough;
use League\CommonMark\Delimiter\DelimiterInterface;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
use League\CommonMark\Inline\Element\AbstractStringContainer;
final class StrikethroughDelimiterProcessor implements DelimiterProcessorInterface
{
public function getOpeningCharacter(): string
{
return '~';
}
public function getClosingCharacter(): string
{
return '~';
}
public function getMinLength(): int
{
return 2;
}
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int
{
$min = \min($opener->getLength(), $closer->getLength());
return $min >= 2 ? $min : 0;
}
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse)
{
$strikethrough = new Strikethrough();
$tmp = $opener->next();
while ($tmp !== null && $tmp !== $closer) {
$next = $tmp->next();
$strikethrough->appendChild($tmp);
$tmp = $next;
}
$opener->insertAfter($strikethrough);
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com)
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Strikethrough;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Extension\ExtensionInterface;
final class StrikethroughExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor());
$environment->addInlineRenderer(Strikethrough::class, new StrikethroughRenderer());
}
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com)
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Strikethrough;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
use League\CommonMark\Inline\Element\AbstractInline;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
final class StrikethroughRenderer implements InlineRendererInterface
{
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
{
if (!($inline instanceof Strikethrough)) {
throw new \InvalidArgumentException('Incompatible inline type: ' . get_class($inline));
}
return new HtmlElement('del', $inline->getData('attributes', []), $htmlRenderer->renderInlines($inline->children()));
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\AbstractStringContainerBlock;
use League\CommonMark\Block\Element\InlineContainerInterface;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
final class Table extends AbstractStringContainerBlock implements InlineContainerInterface
{
/** @var TableSection */
private $head;
/** @var TableSection */
private $body;
/** @var \Closure */
private $parser;
public function __construct(\Closure $parser)
{
parent::__construct();
$this->appendChild($this->head = new TableSection(TableSection::TYPE_HEAD));
$this->appendChild($this->body = new TableSection(TableSection::TYPE_BODY));
$this->parser = $parser;
}
public function canContain(AbstractBlock $block): bool
{
return $block instanceof TableSection;
}
public function isCode(): bool
{
return false;
}
public function getHead(): TableSection
{
return $this->head;
}
public function getBody(): TableSection
{
return $this->body;
}
public function matchesNextLine(Cursor $cursor): bool
{
return call_user_func($this->parser, $cursor, $this);
}
public function handleRemainingContents(ContextInterface $context, Cursor $cursor): void
{
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\AbstractStringContainerBlock;
use League\CommonMark\Block\Element\InlineContainerInterface;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
final class TableCell extends AbstractStringContainerBlock implements InlineContainerInterface
{
const TYPE_HEAD = 'th';
const TYPE_BODY = 'td';
const ALIGN_LEFT = 'left';
const ALIGN_RIGHT = 'right';
const ALIGN_CENTER = 'center';
/** @var string */
public $type = self::TYPE_BODY;
/** @var string|null */
public $align;
public function __construct(string $string = '', string $type = self::TYPE_BODY, string $align = null)
{
parent::__construct();
$this->finalStringContents = $string;
$this->addLine($string);
$this->type = $type;
$this->align = $align;
}
public function canContain(AbstractBlock $block): bool
{
return false;
}
public function isCode(): bool
{
return false;
}
public function matchesNextLine(Cursor $cursor): bool
{
return false;
}
public function handleRemainingContents(ContextInterface $context, Cursor $cursor): void
{
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
final class TableCellRenderer implements BlockRendererInterface
{
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!$block instanceof TableCell) {
throw new \InvalidArgumentException('Incompatible block type: ' . get_class($block));
}
$attrs = $block->getData('attributes', []);
if ($block->align !== null) {
$attrs['align'] = $block->align;
}
return new HtmlElement($block->type, $attrs, $htmlRenderer->renderInlines($block->children()));
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Extension\ExtensionInterface;
final class TableExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment): void
{
$environment
->addBlockParser(new TableParser())
->addBlockRenderer(Table::class, new TableRenderer())
->addBlockRenderer(TableSection::class, new TableSectionRenderer())
->addBlockRenderer(TableRow::class, new TableRowRenderer())
->addBlockRenderer(TableCell::class, new TableCellRenderer())
;
}
}

View File

@@ -0,0 +1,284 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\Block\Parser\BlockParserInterface;
use League\CommonMark\Context;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
use League\CommonMark\EnvironmentAwareInterface;
use League\CommonMark\EnvironmentInterface;
final class TableParser implements BlockParserInterface, EnvironmentAwareInterface
{
/**
* @var EnvironmentInterface
*/
private $environment;
public function parse(ContextInterface $context, Cursor $cursor): bool
{
$container = $context->getContainer();
if (!$container instanceof Paragraph) {
return false;
}
$lines = $container->getStrings();
if (count($lines) === 0) {
return false;
}
$lastLine = \array_pop($lines);
if (\strpos($lastLine, '|') === false) {
return false;
}
$oldState = $cursor->saveState();
$cursor->advanceToNextNonSpaceOrTab();
$columns = $this->parseColumns($cursor);
if (empty($columns)) {
$cursor->restoreState($oldState);
return false;
}
$head = $this->parseRow(trim((string) $lastLine), $columns, TableCell::TYPE_HEAD);
if (null === $head) {
$cursor->restoreState($oldState);
return false;
}
$table = new Table(function (Cursor $cursor, Table $table) use ($columns): bool {
// The next line cannot be a new block start
// This is a bit inefficient, but it's the only feasible way to check
// given the current v1 API.
if (self::isANewBlock($this->environment, $cursor->getLine())) {
return false;
}
$row = $this->parseRow(\trim($cursor->getLine()), $columns);
if (null === $row) {
return false;
}
$table->getBody()->appendChild($row);
return true;
});
$table->getHead()->appendChild($head);
if (count($lines) >= 1) {
$paragraph = new Paragraph();
foreach ($lines as $line) {
$paragraph->addLine($line);
}
$context->replaceContainerBlock($paragraph);
$context->addBlock($table);
} else {
$context->replaceContainerBlock($table);
}
return true;
}
/**
* @param string $line
* @param array<int, string> $columns
* @param string $type
*
* @return TableRow|null
*/
private function parseRow(string $line, array $columns, string $type = TableCell::TYPE_BODY): ?TableRow
{
$cells = $this->split(new Cursor(\trim($line)));
if (empty($cells)) {
return null;
}
// The header row must match the delimiter row in the number of cells
if ($type === TableCell::TYPE_HEAD && \count($cells) !== \count($columns)) {
return null;
}
$i = 0;
$row = new TableRow();
foreach ($cells as $i => $cell) {
if (!array_key_exists($i, $columns)) {
return $row;
}
$row->appendChild(new TableCell(trim($cell), $type, $columns[$i]));
}
for ($j = count($columns) - 1; $j > $i; --$j) {
$row->appendChild(new TableCell('', $type, null));
}
return $row;
}
/**
* @param Cursor $cursor
*
* @return array<int, string>
*/
private function split(Cursor $cursor): array
{
if ($cursor->getCharacter() === '|') {
$cursor->advanceBy(1);
}
$cells = [];
$sb = '';
while (!$cursor->isAtEnd()) {
switch ($c = $cursor->getCharacter()) {
case '\\':
if ($cursor->peek() === '|') {
// Pipe is special for table parsing. An escaped pipe doesn't result in a new cell, but is
// passed down to inline parsing as an unescaped pipe. Note that that applies even for the `\|`
// in an input like `\\|` - in other words, table parsing doesn't support escaping backslashes.
$sb .= '|';
$cursor->advanceBy(1);
} else {
// Preserve backslash before other characters or at end of line.
$sb .= '\\';
}
break;
case '|':
$cells[] = $sb;
$sb = '';
break;
default:
$sb .= $c;
}
$cursor->advanceBy(1);
}
if ($sb !== '') {
$cells[] = $sb;
}
return $cells;
}
/**
* @param Cursor $cursor
*
* @return array<int, string>
*/
private function parseColumns(Cursor $cursor): array
{
$columns = [];
$pipes = 0;
$valid = false;
while (!$cursor->isAtEnd()) {
switch ($c = $cursor->getCharacter()) {
case '|':
$cursor->advanceBy(1);
$pipes++;
if ($pipes > 1) {
// More than one adjacent pipe not allowed
return [];
}
// Need at least one pipe, even for a one-column table
$valid = true;
break;
case '-':
case ':':
if ($pipes === 0 && !empty($columns)) {
// Need a pipe after the first column (first column doesn't need to start with one)
return [];
}
$left = false;
$right = false;
if ($c === ':') {
$left = true;
$cursor->advanceBy(1);
}
if ($cursor->match('/^-+/') === null) {
// Need at least one dash
return [];
}
if ($cursor->getCharacter() === ':') {
$right = true;
$cursor->advanceBy(1);
}
$columns[] = $this->getAlignment($left, $right);
// Next, need another pipe
$pipes = 0;
break;
case ' ':
case "\t":
// White space is allowed between pipes and columns
$cursor->advanceToNextNonSpaceOrTab();
break;
default:
// Any other character is invalid
return [];
}
}
if (!$valid) {
return [];
}
return $columns;
}
private static function getAlignment(bool $left, bool $right): ?string
{
if ($left && $right) {
return TableCell::ALIGN_CENTER;
} elseif ($left) {
return TableCell::ALIGN_LEFT;
} elseif ($right) {
return TableCell::ALIGN_RIGHT;
}
return null;
}
public function setEnvironment(EnvironmentInterface $environment)
{
$this->environment = $environment;
}
private static function isANewBlock(EnvironmentInterface $environment, string $line): bool
{
$context = new Context(new Document(), $environment);
$context->setNextLine($line);
$cursor = new Cursor($line);
/** @var BlockParserInterface $parser */
foreach ($environment->getBlockParsers() as $parser) {
if ($parser->parse($context, $cursor)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
final class TableRenderer implements BlockRendererInterface
{
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!$block instanceof Table) {
throw new \InvalidArgumentException('Incompatible block type: ' . get_class($block));
}
$attrs = $block->getData('attributes', []);
$separator = $htmlRenderer->getOption('inner_separator', "\n");
$children = $htmlRenderer->renderBlocks($block->children());
return new HtmlElement('table', $attrs, $separator . \trim($children) . $separator);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Cursor;
use League\CommonMark\Node\Node;
final class TableRow extends AbstractBlock
{
public function canContain(AbstractBlock $block): bool
{
return $block instanceof TableCell;
}
public function isCode(): bool
{
return false;
}
public function matchesNextLine(Cursor $cursor): bool
{
return false;
}
/**
* @return AbstractBlock[]
*/
public function children(): iterable
{
return array_filter((array) parent::children(), static function (Node $child): bool {
return $child instanceof AbstractBlock;
});
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
final class TableRowRenderer implements BlockRendererInterface
{
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!$block instanceof TableRow) {
throw new \InvalidArgumentException('Incompatible block type: ' . get_class($block));
}
$attrs = $block->getData('attributes', []);
$separator = $htmlRenderer->getOption('inner_separator', "\n");
return new HtmlElement('tr', $attrs, $separator . $htmlRenderer->renderBlocks($block->children()) . $separator);
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\AbstractStringContainerBlock;
use League\CommonMark\Block\Element\InlineContainerInterface;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
final class TableSection extends AbstractStringContainerBlock implements InlineContainerInterface
{
const TYPE_HEAD = 'thead';
const TYPE_BODY = 'tbody';
/** @var string */
public $type = self::TYPE_BODY;
public function __construct(string $type = self::TYPE_BODY)
{
parent::__construct();
$this->type = $type;
}
public function isHead(): bool
{
return self::TYPE_HEAD === $this->type;
}
public function isBody(): bool
{
return self::TYPE_BODY === $this->type;
}
public function canContain(AbstractBlock $block): bool
{
return $block instanceof TableRow;
}
public function isCode(): bool
{
return false;
}
public function matchesNextLine(Cursor $cursor): bool
{
return false;
}
public function handleRemainingContents(ContextInterface $context, Cursor $cursor): void
{
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* This is part of the league/commonmark package.
*
* (c) Martin Hasoň <martin.hason@gmail.com>
* (c) Webuni s.r.o. <info@webuni.cz>
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\Table;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
final class TableSectionRenderer implements BlockRendererInterface
{
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
if (!$block instanceof TableSection) {
throw new \InvalidArgumentException('Incompatible block type: ' . get_class($block));
}
if (!$block->hasChildren()) {
return '';
}
$attrs = $block->getData('attributes', []);
$separator = $htmlRenderer->getOption('inner_separator', "\n");
return new HtmlElement($block->type, $attrs, $separator . $htmlRenderer->renderBlocks($block->children()) . $separator);
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TableOfContents\Node;
use League\CommonMark\Block\Element\ListBlock;
use League\CommonMark\Extension\TableOfContents\TableOfContents as DeprecatedTableOfContents;
final class TableOfContents extends ListBlock
{
}
\class_exists(DeprecatedTableOfContents::class);

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TableOfContents\Node;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Cursor;
final class TableOfContentsPlaceholder extends AbstractBlock
{
public function canContain(AbstractBlock $block): bool
{
return false;
}
public function isCode(): bool
{
return false;
}
public function matchesNextLine(Cursor $cursor): bool
{
return false;
}
}

View File

@@ -0,0 +1,70 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TableOfContents\Normalizer;
use League\CommonMark\Block\Element\ListBlock;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
final class AsIsNormalizerStrategy implements NormalizerStrategyInterface
{
/** @var ListBlock */
private $parentListBlock;
/** @var int */
private $parentLevel = 1;
/** @var ListItem|null */
private $lastListItem;
public function __construct(TableOfContents $toc)
{
$this->parentListBlock = $toc;
}
public function addItem(int $level, ListItem $listItemToAdd): void
{
while ($level > $this->parentLevel) {
// Descend downwards, creating new ListBlocks if needed, until we reach the correct depth
if ($this->lastListItem === null) {
$this->lastListItem = new ListItem($this->parentListBlock->getListData());
$this->parentListBlock->appendChild($this->lastListItem);
}
$newListBlock = new ListBlock($this->parentListBlock->getListData());
$newListBlock->setStartLine($listItemToAdd->getStartLine());
$newListBlock->setEndLine($listItemToAdd->getEndLine());
$this->lastListItem->appendChild($newListBlock);
$this->parentListBlock = $newListBlock;
$this->lastListItem = null;
$this->parentLevel++;
}
while ($level < $this->parentLevel) {
// Search upwards for the previous parent list block
while (true) {
$this->parentListBlock = $this->parentListBlock->parent();
if ($this->parentListBlock instanceof ListBlock) {
break;
}
}
$this->parentLevel--;
}
$this->parentListBlock->appendChild($listItemToAdd);
$this->lastListItem = $listItemToAdd;
}
}
// Trigger autoload without causing a deprecated error
\class_exists(TableOfContents::class);

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TableOfContents\Normalizer;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
final class FlatNormalizerStrategy implements NormalizerStrategyInterface
{
/** @var TableOfContents */
private $toc;
public function __construct(TableOfContents $toc)
{
$this->toc = $toc;
}
public function addItem(int $level, ListItem $listItemToAdd): void
{
$this->toc->appendChild($listItemToAdd);
}
}
// Trigger autoload without causing a deprecated error
\class_exists(TableOfContents::class);

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TableOfContents\Normalizer;
use League\CommonMark\Block\Element\ListItem;
interface NormalizerStrategyInterface
{
public function addItem(int $level, ListItem $listItemToAdd): void;
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TableOfContents\Normalizer;
use League\CommonMark\Block\Element\ListBlock;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
final class RelativeNormalizerStrategy implements NormalizerStrategyInterface
{
/** @var TableOfContents */
private $toc;
/** @var array<int, ListItem> */
private $listItemStack = [];
public function __construct(TableOfContents $toc)
{
$this->toc = $toc;
}
public function addItem(int $level, ListItem $listItemToAdd): void
{
\end($this->listItemStack);
$previousLevel = \key($this->listItemStack);
// Pop the stack if we're too deep
while ($previousLevel !== null && $level < $previousLevel) {
array_pop($this->listItemStack);
\end($this->listItemStack);
$previousLevel = \key($this->listItemStack);
}
/** @var ListItem|false $lastListItem */
$lastListItem = \current($this->listItemStack);
// Need to go one level deeper? Add that level
if ($lastListItem !== false && $level > $previousLevel) {
$targetListBlock = new ListBlock($lastListItem->getListData());
$targetListBlock->setStartLine($listItemToAdd->getStartLine());
$targetListBlock->setEndLine($listItemToAdd->getEndLine());
$lastListItem->appendChild($targetListBlock);
// Otherwise we're at the right level
// If there's no stack we're adding this item directly to the TOC element
} elseif ($lastListItem === false) {
$targetListBlock = $this->toc;
// Otherwise add it to the last list item
} else {
$targetListBlock = $lastListItem->parent();
}
$targetListBlock->appendChild($listItemToAdd);
$this->listItemStack[$level] = $listItemToAdd;
}
}
// Trigger autoload without causing a deprecated error
\class_exists(TableOfContents::class);

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TableOfContents;
use League\CommonMark\Block\Element\ListBlock;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents as NewTableOfContents;
if (!class_exists(NewTableOfContents::class)) {
@trigger_error(sprintf('TableOfContents has moved to a new namespace; use %s instead', NewTableOfContents::class), \E_USER_DEPRECATED);
}
\class_alias(NewTableOfContents::class, TableOfContents::class);
if (false) {
/**
* @deprecated This class has moved to the Node sub-namespace; use that instead
*/
final class TableOfContents extends ListBlock
{
}
}

View File

@@ -0,0 +1,127 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TableOfContents;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Block\Element\Heading;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Exception\InvalidOptionException;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
final class TableOfContentsBuilder implements ConfigurationAwareInterface
{
/**
* @deprecated Use TableOfContentsGenerator::STYLE_BULLET instead
*/
public const STYLE_BULLET = TableOfContentsGenerator::STYLE_BULLET;
/**
* @deprecated Use TableOfContentsGenerator::STYLE_ORDERED instead
*/
public const STYLE_ORDERED = TableOfContentsGenerator::STYLE_ORDERED;
/**
* @deprecated Use TableOfContentsGenerator::NORMALIZE_DISABLED instead
*/
public const NORMALIZE_DISABLED = TableOfContentsGenerator::NORMALIZE_DISABLED;
/**
* @deprecated Use TableOfContentsGenerator::NORMALIZE_RELATIVE instead
*/
public const NORMALIZE_RELATIVE = TableOfContentsGenerator::NORMALIZE_RELATIVE;
/**
* @deprecated Use TableOfContentsGenerator::NORMALIZE_FLAT instead
*/
public const NORMALIZE_FLAT = TableOfContentsGenerator::NORMALIZE_FLAT;
public const POSITION_TOP = 'top';
public const POSITION_BEFORE_HEADINGS = 'before-headings';
public const POSITION_PLACEHOLDER = 'placeholder';
/** @var ConfigurationInterface */
private $config;
public function onDocumentParsed(DocumentParsedEvent $event): void
{
$document = $event->getDocument();
$generator = new TableOfContentsGenerator(
$this->config->get('table_of_contents/style', TableOfContentsGenerator::STYLE_BULLET),
$this->config->get('table_of_contents/normalize', TableOfContentsGenerator::NORMALIZE_RELATIVE),
(int) $this->config->get('table_of_contents/min_heading_level', 1),
(int) $this->config->get('table_of_contents/max_heading_level', 6)
);
$toc = $generator->generate($document);
if ($toc === null) {
// No linkable headers exist, so no TOC could be generated
return;
}
// Add custom CSS class(es), if defined
$class = $this->config->get('table_of_contents/html_class', 'table-of-contents');
if (!empty($class)) {
$toc->data['attributes']['class'] = $class;
}
// Add the TOC to the Document
$position = $this->config->get('table_of_contents/position', self::POSITION_TOP);
if ($position === self::POSITION_TOP) {
$document->prependChild($toc);
} elseif ($position === self::POSITION_BEFORE_HEADINGS) {
$this->insertBeforeFirstLinkedHeading($document, $toc);
} elseif ($position === self::POSITION_PLACEHOLDER) {
$this->replacePlaceholders($document, $toc);
} else {
throw new InvalidOptionException(\sprintf('Invalid config option "%s" for "table_of_contents/position"', $position));
}
}
private function insertBeforeFirstLinkedHeading(Document $document, TableOfContents $toc): void
{
$walker = $document->walker();
while ($event = $walker->next()) {
if ($event->isEntering() && ($node = $event->getNode()) instanceof HeadingPermalink && ($parent = $node->parent()) instanceof Heading) {
$parent->insertBefore($toc);
return;
}
}
}
private function replacePlaceholders(Document $document, TableOfContents $toc): void
{
$walker = $document->walker();
while ($event = $walker->next()) {
// Add the block once we find a placeholder (and we're about to leave it)
if (!$event->getNode() instanceof TableOfContentsPlaceholder) {
continue;
}
if ($event->isEntering()) {
continue;
}
$event->getNode()->replaceWith(clone $toc);
}
}
public function setConfiguration(ConfigurationInterface $config)
{
$this->config = $config;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TableOfContents;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder;
final class TableOfContentsExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment): void
{
$environment->addEventListener(DocumentParsedEvent::class, [new TableOfContentsBuilder(), 'onDocumentParsed'], -150);
if ($environment->getConfig('table_of_contents/position') === TableOfContentsBuilder::POSITION_PLACEHOLDER) {
$environment->addBlockParser(new TableOfContentsPlaceholderParser(), 200);
// If a placeholder cannot be replaced with a TOC element this renderer will ensure the parser won't error out
$environment->addBlockRenderer(TableOfContentsPlaceholder::class, new TableOfContentsPlaceholderRenderer());
}
}
}

View File

@@ -0,0 +1,172 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TableOfContents;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Block\Element\Heading;
use League\CommonMark\Block\Element\ListBlock;
use League\CommonMark\Block\Element\ListData;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\Exception\InvalidOptionException;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
use League\CommonMark\Extension\TableOfContents\Normalizer\AsIsNormalizerStrategy;
use League\CommonMark\Extension\TableOfContents\Normalizer\FlatNormalizerStrategy;
use League\CommonMark\Extension\TableOfContents\Normalizer\NormalizerStrategyInterface;
use League\CommonMark\Extension\TableOfContents\Normalizer\RelativeNormalizerStrategy;
use League\CommonMark\Inline\Element\AbstractStringContainer;
use League\CommonMark\Inline\Element\Link;
final class TableOfContentsGenerator implements TableOfContentsGeneratorInterface
{
public const STYLE_BULLET = ListBlock::TYPE_BULLET;
public const STYLE_ORDERED = ListBlock::TYPE_ORDERED;
public const NORMALIZE_DISABLED = 'as-is';
public const NORMALIZE_RELATIVE = 'relative';
public const NORMALIZE_FLAT = 'flat';
/** @var string */
private $style;
/** @var string */
private $normalizationStrategy;
/** @var int */
private $minHeadingLevel;
/** @var int */
private $maxHeadingLevel;
public function __construct(string $style, string $normalizationStrategy, int $minHeadingLevel, int $maxHeadingLevel)
{
$this->style = $style;
$this->normalizationStrategy = $normalizationStrategy;
$this->minHeadingLevel = $minHeadingLevel;
$this->maxHeadingLevel = $maxHeadingLevel;
}
public function generate(Document $document): ?TableOfContents
{
$toc = $this->createToc($document);
$normalizer = $this->getNormalizer($toc);
$firstHeading = null;
foreach ($this->getHeadingLinks($document) as $headingLink) {
$heading = $headingLink->parent();
// Make sure this is actually tied to a heading
if (!$heading instanceof Heading) {
continue;
}
// Skip any headings outside the configured min/max levels
if ($heading->getLevel() < $this->minHeadingLevel || $heading->getLevel() > $this->maxHeadingLevel) {
continue;
}
// Keep track of the first heading we see - we might need this later
$firstHeading = $firstHeading ?? $heading;
// Keep track of the start and end lines
$toc->setStartLine($firstHeading->getStartLine());
$toc->setEndLine($heading->getEndLine());
// Create the new link
$link = new Link('#' . $headingLink->getSlug(), self::getHeadingText($heading));
$paragraph = new Paragraph();
$paragraph->setStartLine($heading->getStartLine());
$paragraph->setEndLine($heading->getEndLine());
$paragraph->appendChild($link);
$listItem = new ListItem($toc->getListData());
$listItem->setStartLine($heading->getStartLine());
$listItem->setEndLine($heading->getEndLine());
$listItem->appendChild($paragraph);
// Add it to the correct place
$normalizer->addItem($heading->getLevel(), $listItem);
}
// Don't add the TOC if no headings were present
if (!$toc->hasChildren() || $firstHeading === null) {
return null;
}
return $toc;
}
private function createToc(Document $document): TableOfContents
{
$listData = new ListData();
if ($this->style === self::STYLE_BULLET) {
$listData->type = ListBlock::TYPE_BULLET;
} elseif ($this->style === self::STYLE_ORDERED) {
$listData->type = ListBlock::TYPE_ORDERED;
} else {
throw new InvalidOptionException(\sprintf('Invalid table of contents list style "%s"', $this->style));
}
$toc = new TableOfContents($listData);
$toc->setStartLine($document->getStartLine());
$toc->setEndLine($document->getEndLine());
return $toc;
}
/**
* @param Document $document
*
* @return iterable<HeadingPermalink>
*/
private function getHeadingLinks(Document $document)
{
$walker = $document->walker();
while ($event = $walker->next()) {
if ($event->isEntering() && ($node = $event->getNode()) instanceof HeadingPermalink) {
yield $node;
}
}
}
private function getNormalizer(TableOfContents $toc): NormalizerStrategyInterface
{
switch ($this->normalizationStrategy) {
case self::NORMALIZE_DISABLED:
return new AsIsNormalizerStrategy($toc);
case self::NORMALIZE_RELATIVE:
return new RelativeNormalizerStrategy($toc);
case self::NORMALIZE_FLAT:
return new FlatNormalizerStrategy($toc);
default:
throw new InvalidOptionException(\sprintf('Invalid table of contents normalization strategy "%s"', $this->normalizationStrategy));
}
}
/**
* @return string
*/
private static function getHeadingText(Heading $heading)
{
$text = '';
$walker = $heading->walker();
while ($event = $walker->next()) {
if ($event->isEntering() && ($child = $event->getNode()) instanceof AbstractStringContainer) {
$text .= $child->getContent();
}
}
return $text;
}
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TableOfContents;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
interface TableOfContentsGeneratorInterface
{
public function generate(Document $document): ?TableOfContents;
}
// Trigger autoload without causing a deprecated error
\class_exists(TableOfContents::class);

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TableOfContents;
use League\CommonMark\Block\Parser\BlockParserInterface;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder;
use League\CommonMark\Util\ConfigurationAwareInterface;
use League\CommonMark\Util\ConfigurationInterface;
final class TableOfContentsPlaceholderParser implements BlockParserInterface, ConfigurationAwareInterface
{
/** @var ConfigurationInterface */
private $config;
public function parse(ContextInterface $context, Cursor $cursor): bool
{
$placeholder = $this->config->get('table_of_contents/placeholder');
if ($placeholder === null) {
return false;
}
// The placeholder must be the only thing on the line
if ($cursor->match('/^' . \preg_quote($placeholder, '/') . '$/') === null) {
return false;
}
$context->addBlock(new TableOfContentsPlaceholder());
return true;
}
public function setConfiguration(ConfigurationInterface $configuration)
{
$this->config = $configuration;
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TableOfContents;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\ElementRendererInterface;
final class TableOfContentsPlaceholderRenderer implements BlockRendererInterface
{
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
{
return '<!-- table of contents -->';
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TaskList;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Extension\ExtensionInterface;
final class TaskListExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addInlineParser(new TaskListItemMarkerParser(), 35);
$environment->addInlineRenderer(TaskListItemMarker::class, new TaskListItemMarkerRenderer());
}
}

View File

@@ -0,0 +1,37 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TaskList;
use League\CommonMark\Inline\Element\AbstractInline;
final class TaskListItemMarker extends AbstractInline
{
/** @var bool */
protected $checked = false;
public function __construct(bool $isCompleted)
{
$this->checked = $isCompleted;
}
public function isChecked(): bool
{
return $this->checked;
}
public function setChecked(bool $checked): self
{
$this->checked = $checked;
return $this;
}
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TaskList;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\InlineParserContext;
final class TaskListItemMarkerParser implements InlineParserInterface
{
public function getCharacters(): array
{
return ['['];
}
public function parse(InlineParserContext $inlineContext): bool
{
$container = $inlineContext->getContainer();
// Checkbox must come at the beginning of the first paragraph of the list item
if ($container->hasChildren() || !($container instanceof Paragraph && $container->parent() && $container->parent() instanceof ListItem)) {
return false;
}
$cursor = $inlineContext->getCursor();
$oldState = $cursor->saveState();
$m = $cursor->match('/\[[ xX]\]/');
if ($m === null) {
return false;
}
if ($cursor->getNextNonSpaceCharacter() === null) {
$cursor->restoreState($oldState);
return false;
}
$isChecked = $m !== '[ ]';
$container->appendChild(new TaskListItemMarker($isChecked));
return true;
}
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\TaskList;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
use League\CommonMark\Inline\Element\AbstractInline;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
final class TaskListItemMarkerRenderer implements InlineRendererInterface
{
/**
* @param TaskListItemMarker $inline
* @param ElementRendererInterface $htmlRenderer
*
* @return HtmlElement|string|null
*/
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
{
if (!($inline instanceof TaskListItemMarker)) {
throw new \InvalidArgumentException('Incompatible inline type: ' . \get_class($inline));
}
$checkbox = new HtmlElement('input', [], '', true);
if ($inline->isChecked()) {
$checkbox->setAttribute('checked', '');
}
$checkbox->setAttribute('disabled', '');
$checkbox->setAttribute('type', 'checkbox');
return $checkbox;
}
}