Commaaa2
This commit is contained in:
@@ -14,18 +14,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Attributes;
|
||||
|
||||
use League\CommonMark\ConfigurableEnvironmentInterface;
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
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\AttributesBlockStartParser;
|
||||
use League\CommonMark\Extension\Attributes\Parser\AttributesInlineParser;
|
||||
use League\CommonMark\Extension\ExtensionInterface;
|
||||
|
||||
final class AttributesExtension implements ExtensionInterface
|
||||
{
|
||||
public function register(ConfigurableEnvironmentInterface $environment)
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$environment->addBlockParser(new AttributesBlockParser());
|
||||
$environment->addBlockStartParser(new AttributesBlockStartParser());
|
||||
$environment->addInlineParser(new AttributesInlineParser());
|
||||
$environment->addEventListener(DocumentParsedEvent::class, [new AttributesListener(), 'processDocument']);
|
||||
}
|
||||
|
||||
@@ -14,15 +14,14 @@ 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\Extension\CommonMark\Node\Block\FencedCode;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
|
||||
use League\CommonMark\Node\Inline\AbstractInline;
|
||||
use League\CommonMark\Node\Node;
|
||||
|
||||
final class AttributesListener
|
||||
@@ -32,16 +31,14 @@ final class AttributesListener
|
||||
|
||||
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)) {
|
||||
foreach ($event->getDocument()->iterator() as $node) {
|
||||
if (! ($node instanceof Attributes || $node instanceof AttributesInline)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$target, $direction] = self::findTargetAndDirection($node);
|
||||
|
||||
if ($target instanceof AbstractBlock || $target instanceof AbstractInline) {
|
||||
if ($target instanceof Node) {
|
||||
$parent = $target->parent();
|
||||
if ($parent instanceof ListItem && $parent->parent() instanceof ListBlock && $parent->parent()->isTight()) {
|
||||
$target = $parent;
|
||||
@@ -53,14 +50,7 @@ final class AttributesListener
|
||||
$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);
|
||||
}
|
||||
$target->data->set('attributes', $attributes);
|
||||
}
|
||||
|
||||
$node->detach();
|
||||
@@ -68,22 +58,22 @@ final class AttributesListener
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
* @param Attributes|AttributesInline $node
|
||||
*
|
||||
* @return array<Node|string|null>
|
||||
*/
|
||||
private static function findTargetAndDirection(Node $node): array
|
||||
private static function findTargetAndDirection($node): array
|
||||
{
|
||||
$target = null;
|
||||
$target = null;
|
||||
$direction = null;
|
||||
$previous = $next = $node;
|
||||
$previous = $next = $node;
|
||||
while (true) {
|
||||
$previous = self::getPrevious($previous);
|
||||
$next = self::getNext($next);
|
||||
$next = self::getNext($next);
|
||||
|
||||
if ($previous === null && $next === null) {
|
||||
if (!$node->parent() instanceof FencedCode) {
|
||||
$target = $node->parent();
|
||||
if (! $node->parent() instanceof FencedCode) {
|
||||
$target = $node->parent();
|
||||
$direction = self::DIRECTION_SUFFIX;
|
||||
}
|
||||
|
||||
@@ -94,15 +84,15 @@ final class AttributesListener
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($previous !== null && !self::isAttributesNode($previous)) {
|
||||
$target = $previous;
|
||||
if ($previous !== null && ! self::isAttributesNode($previous)) {
|
||||
$target = $previous;
|
||||
$direction = self::DIRECTION_SUFFIX;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($next !== null && !self::isAttributesNode($next)) {
|
||||
$target = $next;
|
||||
if ($next !== null && ! self::isAttributesNode($next)) {
|
||||
$target = $next;
|
||||
$direction = self::DIRECTION_PREFIX;
|
||||
|
||||
break;
|
||||
@@ -112,26 +102,34 @@ final class AttributesListener
|
||||
return [$target, $direction];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any previous block (sibling or parent) this might apply to
|
||||
*/
|
||||
private static function getPrevious(?Node $node = null): ?Node
|
||||
{
|
||||
$previous = $node instanceof Node ? $node->previous() : null;
|
||||
if ($node instanceof Attributes) {
|
||||
if ($node->getTarget() === Attributes::TARGET_NEXT) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($previous instanceof AbstractBlock && $previous->endsWithBlankLine()) {
|
||||
$previous = null;
|
||||
if ($node->getTarget() === Attributes::TARGET_PARENT) {
|
||||
return $node->parent();
|
||||
}
|
||||
}
|
||||
|
||||
return $previous;
|
||||
return $node instanceof Node ? $node->previous() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any previous block (sibling or parent) this might apply to
|
||||
*/
|
||||
private static function getNext(?Node $node = null): ?Node
|
||||
{
|
||||
$next = $node instanceof Node ? $node->next() : null;
|
||||
|
||||
if ($node instanceof AbstractBlock && $node->endsWithBlankLine()) {
|
||||
$next = null;
|
||||
if ($node instanceof Attributes && $node->getTarget() !== Attributes::TARGET_NEXT) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $next;
|
||||
return $node instanceof Node ? $node->next() : null;
|
||||
}
|
||||
|
||||
private static function isAttributesNode(Node $node): bool
|
||||
|
||||
@@ -14,19 +14,26 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Attributes\Node;
|
||||
|
||||
use League\CommonMark\Block\Element\AbstractBlock;
|
||||
use League\CommonMark\Cursor;
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
|
||||
final class Attributes extends AbstractBlock
|
||||
{
|
||||
public const TARGET_PARENT = 0;
|
||||
public const TARGET_PREVIOUS = 1;
|
||||
public const TARGET_NEXT = 2;
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
private $attributes;
|
||||
private array $attributes;
|
||||
|
||||
private int $target = self::TARGET_NEXT;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function __construct(array $attributes)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->attributes = $attributes;
|
||||
}
|
||||
|
||||
@@ -38,25 +45,21 @@ final class Attributes extends AbstractBlock
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
public function canContain(AbstractBlock $block): bool
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function setAttributes(array $attributes): void
|
||||
{
|
||||
return false;
|
||||
$this->attributes = $attributes;
|
||||
}
|
||||
|
||||
public function isCode(): bool
|
||||
public function getTarget(): int
|
||||
{
|
||||
return false;
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
public function matchesNextLine(Cursor $cursor): bool
|
||||
public function setTarget(int $target): void
|
||||
{
|
||||
$this->setLastLineBlank($cursor->isBlank());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function shouldLastLineBeBlank(Cursor $cursor, int $currentLineNumber): bool
|
||||
{
|
||||
return false;
|
||||
$this->target = $target;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,25 +14,24 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Attributes\Node;
|
||||
|
||||
use League\CommonMark\Inline\Element\AbstractInline;
|
||||
use League\CommonMark\Node\Inline\AbstractInline;
|
||||
|
||||
final class AttributesInline extends AbstractInline
|
||||
{
|
||||
/** @var array<string, mixed> */
|
||||
public $attributes;
|
||||
private array $attributes;
|
||||
|
||||
/** @var bool */
|
||||
public $block;
|
||||
private bool $block;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
* @param bool $block
|
||||
*/
|
||||
public function __construct(array $attributes, bool $block)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->attributes = $attributes;
|
||||
$this->block = $block;
|
||||
$this->data = ['delim' => true]; // TODO: Re-implement as a delimiter?
|
||||
$this->block = $block;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,6 +42,14 @@ final class AttributesInline extends AbstractInline
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function setAttributes(array $attributes): void
|
||||
{
|
||||
$this->attributes = $attributes;
|
||||
}
|
||||
|
||||
public function isBlock(): bool
|
||||
{
|
||||
return $this->block;
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -16,33 +16,30 @@ 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;
|
||||
use League\CommonMark\Node\StringContainerInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserMatch;
|
||||
use League\CommonMark\Parser\InlineParserContext;
|
||||
|
||||
final class AttributesInlineParser implements InlineParserInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCharacters(): array
|
||||
public function getMatchDefinition(): InlineParserMatch
|
||||
{
|
||||
return ['{'];
|
||||
return InlineParserMatch::string('{');
|
||||
}
|
||||
|
||||
public function parse(InlineParserContext $inlineContext): bool
|
||||
{
|
||||
$cursor = $inlineContext->getCursor();
|
||||
|
||||
$char = (string) $cursor->peek(-1);
|
||||
$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 === ' ' && ($prev = $inlineContext->getContainer()->lastChild()) instanceof StringContainerInterface) {
|
||||
$prev->setLiteral(\rtrim($prev->getLiteral(), ' '));
|
||||
}
|
||||
|
||||
if ($char === '') {
|
||||
|
||||
@@ -14,9 +14,8 @@ 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\Node\Node;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
use League\CommonMark\Util\RegexHelper;
|
||||
|
||||
/**
|
||||
@@ -24,29 +23,43 @@ use League\CommonMark\Util\RegexHelper;
|
||||
*/
|
||||
final class AttributesHelper
|
||||
{
|
||||
private const SINGLE_ATTRIBUTE = '\s*([.]-?[_a-z][^\s}]*|[#][^\s}]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . '?)\s*';
|
||||
private const ATTRIBUTE_LIST = '/^{:?(' . self::SINGLE_ATTRIBUTE . ')+}(?!})/i';
|
||||
|
||||
/**
|
||||
* @param Cursor $cursor
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function parseAttributes(Cursor $cursor): array
|
||||
{
|
||||
$state = $cursor->saveState();
|
||||
$cursor->advanceToNextNonSpaceOrNewline();
|
||||
|
||||
// Quick check to see if we might have attributes
|
||||
if ($cursor->getCharacter() !== '{') {
|
||||
$cursor->restoreState($state);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$cursor->advanceBy(1);
|
||||
if ($cursor->getCharacter() === ':') {
|
||||
$cursor->advanceBy(1);
|
||||
// Attempt to match the entire attribute list expression
|
||||
// While this is less performant than checking for '{' now and '}' later, it simplifies
|
||||
// matching individual attributes since they won't need to look ahead for the closing '}'
|
||||
// while dealing with the fact that attributes can technically contain curly braces.
|
||||
// So we'll just match the start and end braces up front.
|
||||
$attributeExpression = $cursor->match(self::ATTRIBUTE_LIST);
|
||||
if ($attributeExpression === null) {
|
||||
$cursor->restoreState($state);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Trim the leading '{' or '{:' and the trailing '}'
|
||||
$attributeExpression = \ltrim(\substr($attributeExpression, 1, -1), ':');
|
||||
$attributeCursor = new Cursor($attributeExpression);
|
||||
|
||||
/** @var array<string, mixed> $attributes */
|
||||
$attributes = [];
|
||||
$regex = '/^\s*([.#][_a-z0-9-]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . ')(?<!})\s*/i';
|
||||
while ($attribute = \trim((string) $cursor->match($regex))) {
|
||||
while ($attribute = \trim((string) $attributeCursor->match('/^' . self::SINGLE_ATTRIBUTE . '/i'))) {
|
||||
if ($attribute[0] === '#') {
|
||||
$attributes['id'] = \substr($attribute, 1);
|
||||
|
||||
@@ -59,10 +72,18 @@ final class AttributesHelper
|
||||
continue;
|
||||
}
|
||||
|
||||
[$name, $value] = \explode('=', $attribute, 2);
|
||||
$parts = \explode('=', $attribute, 2);
|
||||
if (\count($parts) === 1) {
|
||||
$attributes[$attribute] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @psalm-suppress PossiblyUndefinedArrayOffset */
|
||||
[$name, $value] = $parts;
|
||||
|
||||
$first = $value[0];
|
||||
$last = \substr($value, -1);
|
||||
if ((($first === '"' && $last === '"') || ($first === "'" && $last === "'")) && \strlen($value) > 1) {
|
||||
$last = \substr($value, -1);
|
||||
if (($first === '"' && $last === '"') || ($first === "'" && $last === "'") && \strlen($value) > 1) {
|
||||
$value = \substr($value, 1, -1);
|
||||
}
|
||||
|
||||
@@ -71,22 +92,10 @@ final class AttributesHelper
|
||||
$attributes['class'][] = $class;
|
||||
}
|
||||
} else {
|
||||
$attributes[trim($name)] = trim($value);
|
||||
$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']);
|
||||
}
|
||||
@@ -95,8 +104,8 @@ final class AttributesHelper
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AbstractBlock|AbstractInline|array<string, mixed> $attributes1
|
||||
* @param AbstractBlock|AbstractInline|array<string, mixed> $attributes2
|
||||
* @param Node|array<string, mixed> $attributes1
|
||||
* @param Node|array<string, mixed> $attributes2
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
@@ -104,14 +113,18 @@ final class AttributesHelper
|
||||
{
|
||||
$attributes = [];
|
||||
foreach ([$attributes1, $attributes2] as $arg) {
|
||||
if ($arg instanceof AbstractBlock || $arg instanceof AbstractInline) {
|
||||
$arg = $arg->data['attributes'] ?? [];
|
||||
if ($arg instanceof Node) {
|
||||
$arg = $arg->data->get('attributes');
|
||||
}
|
||||
|
||||
/** @var array<string, mixed> $arg */
|
||||
$arg = (array) $arg;
|
||||
if (isset($arg['class'])) {
|
||||
foreach (\array_filter(\explode(' ', \trim($arg['class']))) as $class) {
|
||||
if (\is_string($arg['class'])) {
|
||||
$arg['class'] = \array_filter(\explode(' ', \trim($arg['class'])));
|
||||
}
|
||||
|
||||
foreach ($arg['class'] as $class) {
|
||||
$attributes['class'][] = $class;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,15 +13,27 @@
|
||||
|
||||
namespace League\CommonMark\Extension\Autolink;
|
||||
|
||||
use League\CommonMark\ConfigurableEnvironmentInterface;
|
||||
use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Extension\ExtensionInterface;
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\Config\ConfigurationBuilderInterface;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
final class AutolinkExtension implements ExtensionInterface
|
||||
final class AutolinkExtension implements ConfigurableExtensionInterface
|
||||
{
|
||||
public function register(ConfigurableEnvironmentInterface $environment)
|
||||
public function configureSchema(ConfigurationBuilderInterface $builder): void
|
||||
{
|
||||
$environment->addEventListener(DocumentParsedEvent::class, new EmailAutolinkProcessor());
|
||||
$environment->addEventListener(DocumentParsedEvent::class, new UrlAutolinkProcessor());
|
||||
$builder->addSchema('autolink', Expect::structure([
|
||||
'allowed_protocols' => Expect::listOf('string')->default(['http', 'https', 'ftp'])->mergeDefaults(false),
|
||||
'default_protocol' => Expect::string()->default('http'),
|
||||
]));
|
||||
}
|
||||
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$environment->addInlineParser(new EmailAutolinkParser());
|
||||
$environment->addInlineParser(new UrlAutolinkParser(
|
||||
$environment->getConfiguration()->get('autolink.allowed_protocols'),
|
||||
$environment->getConfiguration()->get('autolink.default_protocol'),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
<?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)/');
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
<?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 don’t 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['('];
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
<?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('_'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?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', '<$1', $rendered);
|
||||
}
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $configuration)
|
||||
{
|
||||
if ($this->htmlBlockRenderer instanceof ConfigurationAwareInterface) {
|
||||
$this->htmlBlockRenderer->setConfiguration($configuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,18 +13,39 @@
|
||||
|
||||
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;
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Inline\HtmlInline;
|
||||
use League\CommonMark\Extension\CommonMark\Renderer\Block\HtmlBlockRenderer;
|
||||
use League\CommonMark\Extension\CommonMark\Renderer\Inline\HtmlInlineRenderer;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\Config\ConfigurationBuilderInterface;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
final class DisallowedRawHtmlExtension implements ExtensionInterface
|
||||
final class DisallowedRawHtmlExtension implements ConfigurableExtensionInterface
|
||||
{
|
||||
public function register(ConfigurableEnvironmentInterface $environment)
|
||||
private const DEFAULT_DISALLOWED_TAGS = [
|
||||
'title',
|
||||
'textarea',
|
||||
'style',
|
||||
'xmp',
|
||||
'iframe',
|
||||
'noembed',
|
||||
'noframes',
|
||||
'script',
|
||||
'plaintext',
|
||||
];
|
||||
|
||||
public function configureSchema(ConfigurationBuilderInterface $builder): void
|
||||
{
|
||||
$environment->addBlockRenderer(HtmlBlock::class, new DisallowedRawHtmlBlockRenderer(new HtmlBlockRenderer()), 50);
|
||||
$environment->addInlineRenderer(HtmlInline::class, new DisallowedRawHtmlInlineRenderer(new HtmlInlineRenderer()), 50);
|
||||
$builder->addSchema('disallowed_raw_html', Expect::structure([
|
||||
'disallowed_tags' => Expect::listOf('string')->default(self::DEFAULT_DISALLOWED_TAGS)->mergeDefaults(false),
|
||||
]));
|
||||
}
|
||||
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$environment->addRenderer(HtmlBlock::class, new DisallowedRawHtmlRenderer(new HtmlBlockRenderer()), 50);
|
||||
$environment->addRenderer(HtmlInline::class, new DisallowedRawHtmlRenderer(new HtmlInlineRenderer()), 50);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<?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', '<$1', $rendered);
|
||||
}
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $configuration)
|
||||
{
|
||||
if ($this->htmlInlineRenderer instanceof ConfigurationAwareInterface) {
|
||||
$this->htmlInlineRenderer->setConfiguration($configuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -14,14 +16,9 @@
|
||||
|
||||
namespace League\CommonMark\Extension;
|
||||
|
||||
use League\CommonMark\ConfigurableEnvironmentInterface;
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
|
||||
interface ExtensionInterface
|
||||
{
|
||||
/**
|
||||
* @param ConfigurableEnvironmentInterface $environment
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(ConfigurableEnvironmentInterface $environment);
|
||||
public function register(EnvironmentBuilderInterface $environment): void;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,14 +13,35 @@
|
||||
|
||||
namespace League\CommonMark\Extension\ExternalLink;
|
||||
|
||||
use League\CommonMark\ConfigurableEnvironmentInterface;
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Extension\ExtensionInterface;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\Config\ConfigurationBuilderInterface;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
final class ExternalLinkExtension implements ExtensionInterface
|
||||
final class ExternalLinkExtension implements ConfigurableExtensionInterface
|
||||
{
|
||||
public function register(ConfigurableEnvironmentInterface $environment)
|
||||
public function configureSchema(ConfigurationBuilderInterface $builder): void
|
||||
{
|
||||
$environment->addEventListener(DocumentParsedEvent::class, new ExternalLinkProcessor($environment), -50);
|
||||
$applyOptions = [
|
||||
ExternalLinkProcessor::APPLY_NONE,
|
||||
ExternalLinkProcessor::APPLY_ALL,
|
||||
ExternalLinkProcessor::APPLY_INTERNAL,
|
||||
ExternalLinkProcessor::APPLY_EXTERNAL,
|
||||
];
|
||||
|
||||
$builder->addSchema('external_link', Expect::structure([
|
||||
'internal_hosts' => Expect::type('string|string[]'),
|
||||
'open_in_new_window' => Expect::bool(false),
|
||||
'html_class' => Expect::string()->default(''),
|
||||
'nofollow' => Expect::anyOf(...$applyOptions)->default(ExternalLinkProcessor::APPLY_NONE),
|
||||
'noopener' => Expect::anyOf(...$applyOptions)->default(ExternalLinkProcessor::APPLY_EXTERNAL),
|
||||
'noreferrer' => Expect::anyOf(...$applyOptions)->default(ExternalLinkProcessor::APPLY_EXTERNAL),
|
||||
]));
|
||||
}
|
||||
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$environment->addEventListener(DocumentParsedEvent::class, new ExternalLinkProcessor($environment->getConfiguration()), -50);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,114 +13,100 @@
|
||||
|
||||
namespace League\CommonMark\Extension\ExternalLink;
|
||||
|
||||
use League\CommonMark\EnvironmentInterface;
|
||||
use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Inline\Element\Link;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
|
||||
use League\Config\ConfigurationInterface;
|
||||
|
||||
final class ExternalLinkProcessor
|
||||
{
|
||||
public const APPLY_NONE = '';
|
||||
public const APPLY_ALL = 'all';
|
||||
public const APPLY_NONE = '';
|
||||
public const APPLY_ALL = 'all';
|
||||
public const APPLY_EXTERNAL = 'external';
|
||||
public const APPLY_INTERNAL = 'internal';
|
||||
|
||||
/** @var EnvironmentInterface */
|
||||
private $environment;
|
||||
/** @psalm-readonly */
|
||||
private ConfigurationInterface $config;
|
||||
|
||||
public function __construct(EnvironmentInterface $environment)
|
||||
public function __construct(ConfigurationInterface $config)
|
||||
{
|
||||
$this->environment = $environment;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DocumentParsedEvent $e
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(DocumentParsedEvent $e)
|
||||
public function __invoke(DocumentParsedEvent $e): void
|
||||
{
|
||||
$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', '');
|
||||
$internalHosts = $this->config->get('external_link/internal_hosts');
|
||||
$openInNewWindow = $this->config->get('external_link/open_in_new_window');
|
||||
$classes = $this->config->get('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);
|
||||
foreach ($e->getDocument()->iterator() as $link) {
|
||||
if (! ($link instanceof Link)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$host = \parse_url($link->getUrl(), PHP_URL_HOST);
|
||||
if (! \is_string($host)) {
|
||||
// Something is terribly wrong with this URL
|
||||
continue;
|
||||
}
|
||||
|
||||
if (self::hostMatches($host, $internalHosts)) {
|
||||
$link->data->set('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', []);
|
||||
$link->data->set('external', true);
|
||||
$this->applyRelAttribute($link, true);
|
||||
|
||||
if ($openInNewWindow) {
|
||||
$link->data['attributes']['target'] = '_blank';
|
||||
$link->data->set('attributes/target', '_blank');
|
||||
}
|
||||
|
||||
if (!empty($classes)) {
|
||||
$link->data['attributes']['class'] = trim(($link->data['attributes']['class'] ?? '') . ' ' . $classes);
|
||||
if ($classes !== '') {
|
||||
$link->data->append('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),
|
||||
'nofollow' => $this->config->get('external_link/nofollow'),
|
||||
'noopener' => $this->config->get('external_link/noopener'),
|
||||
'noreferrer' => $this->config->get('external_link/noreferrer'),
|
||||
];
|
||||
|
||||
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;
|
||||
case ! $isExternal && $option === self::APPLY_INTERNAL:
|
||||
$link->data->append('attributes/rel', $type);
|
||||
}
|
||||
}
|
||||
|
||||
if ($rel === []) {
|
||||
return;
|
||||
// No rel attributes? Mark the attribute as 'false' so LinkRenderer doesn't add defaults
|
||||
if (! $link->data->has('attributes/rel')) {
|
||||
$link->data->set('attributes/rel', false);
|
||||
}
|
||||
|
||||
$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!
|
||||
*
|
||||
* @param non-empty-string|list<non-empty-string> $compareTo
|
||||
*/
|
||||
public static function hostMatches(string $host, $compareTo)
|
||||
public static function hostMatches(string $host, $compareTo): bool
|
||||
{
|
||||
foreach ((array) $compareTo as $c) {
|
||||
if (strpos($c, '/') === 0) {
|
||||
if (preg_match($c, $host)) {
|
||||
if (\strpos($c, '/') === 0) {
|
||||
if (\preg_match($c, $host)) {
|
||||
return true;
|
||||
}
|
||||
} elseif ($c === $host) {
|
||||
|
||||
@@ -14,48 +14,49 @@ 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\Node\Block\Paragraph;
|
||||
use League\CommonMark\Node\Inline\Text;
|
||||
use League\CommonMark\Reference\Reference;
|
||||
use League\CommonMark\Util\ConfigurationAwareInterface;
|
||||
use League\CommonMark\Util\ConfigurationInterface;
|
||||
use League\Config\ConfigurationAwareInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
|
||||
final class AnonymousFootnotesListener implements ConfigurationAwareInterface
|
||||
{
|
||||
/** @var ConfigurationInterface */
|
||||
private $config;
|
||||
private ConfigurationInterface $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);
|
||||
foreach ($document->iterator() as $node) {
|
||||
if (! $node instanceof FootnoteRef || ($text = $node->getContent()) === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Anonymous footnote needs to create a footnote from its content
|
||||
$existingReference = $node->getReference();
|
||||
$newReference = new Reference(
|
||||
$existingReference->getLabel(),
|
||||
'#' . $this->config->get('footnote/ref_id_prefix') . $existingReference->getLabel(),
|
||||
$existingReference->getTitle()
|
||||
);
|
||||
|
||||
$paragraph = new Paragraph();
|
||||
$paragraph->appendChild(new Text($text));
|
||||
$paragraph->appendChild(new FootnoteBackref($newReference));
|
||||
|
||||
$footnote = new Footnote($newReference);
|
||||
$footnote->appendChild($paragraph);
|
||||
|
||||
$document->appendChild($footnote);
|
||||
}
|
||||
}
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $config): void
|
||||
public function setConfiguration(ConfigurationInterface $configuration): void
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->config = $configuration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,61 +14,43 @@ 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\Node\Block\Document;
|
||||
use League\CommonMark\Node\NodeIterator;
|
||||
use League\CommonMark\Reference\Reference;
|
||||
use League\CommonMark\Util\ConfigurationAwareInterface;
|
||||
use League\CommonMark\Util\ConfigurationInterface;
|
||||
use League\Config\ConfigurationAwareInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
|
||||
final class GatherFootnotesListener implements ConfigurationAwareInterface
|
||||
{
|
||||
/** @var ConfigurationInterface */
|
||||
private $config;
|
||||
private ConfigurationInterface $config;
|
||||
|
||||
public function onDocumentParsed(DocumentParsedEvent $event): void
|
||||
{
|
||||
$document = $event->getDocument();
|
||||
$walker = $document->walker();
|
||||
|
||||
$document = $event->getDocument();
|
||||
$footnotes = [];
|
||||
while ($event = $walker->next()) {
|
||||
if (!$event->isEntering()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$node = $event->getNode();
|
||||
if (!$node instanceof Footnote) {
|
||||
foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) {
|
||||
if (! $node instanceof Footnote) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Look for existing reference with footnote label
|
||||
$ref = $document->getReferenceMap()->getReference($node->getReference()->getLabel());
|
||||
$ref = $document->getReferenceMap()->get($node->getReference()->getLabel());
|
||||
if ($ref !== null) {
|
||||
// Use numeric title to get footnotes order
|
||||
$footnotes[\intval($ref->getTitle())] = $node;
|
||||
$footnotes[(int) $ref->getTitle()] = $node;
|
||||
} else {
|
||||
// Footnote call is missing, append footnote at the end
|
||||
$footnotes[INF] = $node;
|
||||
$footnotes[\PHP_INT_MAX] = $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()
|
||||
)));
|
||||
$key = '#' . $this->config->get('footnote/footnote_id_prefix') . $node->getReference()->getDestination();
|
||||
if ($document->data->has($key)) {
|
||||
$this->createBackrefs($node, $document->data->get($key));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +75,32 @@ final class GatherFootnotesListener implements ConfigurationAwareInterface
|
||||
return $footnoteContainer;
|
||||
}
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $config): void
|
||||
/**
|
||||
* Look for all footnote refs pointing to this footnote and create each footnote backrefs.
|
||||
*
|
||||
* @param Footnote $node The target footnote
|
||||
* @param Reference[] $backrefs References to create backrefs for
|
||||
*/
|
||||
private function createBackrefs(Footnote $node, array $backrefs): void
|
||||
{
|
||||
$this->config = $config;
|
||||
// Backrefs should be added to the child paragraph
|
||||
$target = $node->lastChild();
|
||||
if ($target === null) {
|
||||
// This should never happen, but you never know
|
||||
$target = $node;
|
||||
}
|
||||
|
||||
foreach ($backrefs as $backref) {
|
||||
$target->appendChild(new FootnoteBackref(new Reference(
|
||||
$backref->getLabel(),
|
||||
'#' . $this->config->get('footnote/ref_id_prefix') . $backref->getLabel(),
|
||||
$backref->getTitle()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $configuration): void
|
||||
{
|
||||
$this->config = $configuration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,26 +22,19 @@ final class NumberFootnotesListener
|
||||
{
|
||||
public function onDocumentParsed(DocumentParsedEvent $event): void
|
||||
{
|
||||
$document = $event->getDocument();
|
||||
$walker = $document->walker();
|
||||
$nextCounter = 1;
|
||||
$usedLabels = [];
|
||||
$document = $event->getDocument();
|
||||
$nextCounter = 1;
|
||||
$usedLabels = [];
|
||||
$usedCounters = [];
|
||||
|
||||
while ($event = $walker->next()) {
|
||||
if (!$event->isEntering()) {
|
||||
foreach ($document->iterator() as $node) {
|
||||
if (! $node instanceof FootnoteRef) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$node = $event->getNode();
|
||||
|
||||
if (!$node instanceof FootnoteRef) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existingReference = $node->getReference();
|
||||
$label = $existingReference->getLabel();
|
||||
$counter = $nextCounter;
|
||||
$existingReference = $node->getReference();
|
||||
$label = $existingReference->getLabel();
|
||||
$counter = $nextCounter;
|
||||
$canIncrementCounter = true;
|
||||
|
||||
if (\array_key_exists($label, $usedLabels)) {
|
||||
@@ -49,8 +42,8 @@ final class NumberFootnotesListener
|
||||
* Reference is used again, we need to point
|
||||
* to the same footnote. But with a different ID
|
||||
*/
|
||||
$counter = $usedCounters[$label];
|
||||
$label = $label . '__' . ++$usedLabels[$label];
|
||||
$counter = $usedCounters[$label];
|
||||
$label .= '__' . ++$usedLabels[$label];
|
||||
$canIncrementCounter = false;
|
||||
}
|
||||
|
||||
@@ -63,19 +56,15 @@ final class NumberFootnotesListener
|
||||
|
||||
// Override reference with numeric link
|
||||
$node->setReference($newReference);
|
||||
$document->getReferenceMap()->addReference($newReference);
|
||||
$document->getReferenceMap()->add($newReference);
|
||||
|
||||
/*
|
||||
* Store created references in document for
|
||||
* creating FootnoteBackrefs
|
||||
*/
|
||||
if (false === $document->getData($existingReference->getDestination(), false)) {
|
||||
$document->data[$existingReference->getDestination()] = [];
|
||||
}
|
||||
$document->data->append($existingReference->getDestination(), $newReference);
|
||||
|
||||
$document->data[$existingReference->getDestination()][] = $newReference;
|
||||
|
||||
$usedLabels[$label] = 1;
|
||||
$usedLabels[$label] = 1;
|
||||
$usedCounters[$label] = $nextCounter;
|
||||
|
||||
if ($canIncrementCounter) {
|
||||
|
||||
@@ -14,10 +14,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Footnote;
|
||||
|
||||
use League\CommonMark\ConfigurableEnvironmentInterface;
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Extension\ExtensionInterface;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\CommonMark\Extension\Footnote\Event\AnonymousFootnotesListener;
|
||||
use League\CommonMark\Extension\Footnote\Event\FixOrphanedFootnotesAndRefsListener;
|
||||
use League\CommonMark\Extension\Footnote\Event\GatherFootnotesListener;
|
||||
use League\CommonMark\Extension\Footnote\Event\NumberFootnotesListener;
|
||||
use League\CommonMark\Extension\Footnote\Node\Footnote;
|
||||
@@ -25,29 +26,45 @@ 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\Parser\FootnoteStartParser;
|
||||
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;
|
||||
use League\Config\ConfigurationBuilderInterface;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
final class FootnoteExtension implements ExtensionInterface
|
||||
final class FootnoteExtension implements ConfigurableExtensionInterface
|
||||
{
|
||||
public function register(ConfigurableEnvironmentInterface $environment)
|
||||
public function configureSchema(ConfigurationBuilderInterface $builder): void
|
||||
{
|
||||
$environment->addBlockParser(new FootnoteParser(), 51);
|
||||
$builder->addSchema('footnote', Expect::structure([
|
||||
'backref_class' => Expect::string('footnote-backref'),
|
||||
'backref_symbol' => Expect::string('↩'),
|
||||
'container_add_hr' => Expect::bool(true),
|
||||
'container_class' => Expect::string('footnotes'),
|
||||
'ref_class' => Expect::string('footnote-ref'),
|
||||
'ref_id_prefix' => Expect::string('fnref:'),
|
||||
'footnote_class' => Expect::string('footnote'),
|
||||
'footnote_id_prefix' => Expect::string('fn:'),
|
||||
]));
|
||||
}
|
||||
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$environment->addBlockStartParser(new FootnoteStartParser(), 51);
|
||||
$environment->addInlineParser(new AnonymousFootnoteRefParser(), 35);
|
||||
$environment->addInlineParser(new FootnoteRefParser(), 51);
|
||||
|
||||
$environment->addBlockRenderer(FootnoteContainer::class, new FootnoteContainerRenderer());
|
||||
$environment->addBlockRenderer(Footnote::class, new FootnoteRenderer());
|
||||
$environment->addRenderer(FootnoteContainer::class, new FootnoteContainerRenderer());
|
||||
$environment->addRenderer(Footnote::class, new FootnoteRenderer());
|
||||
$environment->addRenderer(FootnoteRef::class, new FootnoteRefRenderer());
|
||||
$environment->addRenderer(FootnoteBackref::class, new FootnoteBackrefRenderer());
|
||||
|
||||
$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']);
|
||||
$environment->addEventListener(DocumentParsedEvent::class, [new AnonymousFootnotesListener(), 'onDocumentParsed'], 40);
|
||||
$environment->addEventListener(DocumentParsedEvent::class, [new FixOrphanedFootnotesAndRefsListener(), 'onDocumentParsed'], 30);
|
||||
$environment->addEventListener(DocumentParsedEvent::class, [new NumberFootnotesListener(), 'onDocumentParsed'], 20);
|
||||
$environment->addEventListener(DocumentParsedEvent::class, [new GatherFootnotesListener(), 'onDocumentParsed'], 10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,62 +14,24 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Footnote\Node;
|
||||
|
||||
use League\CommonMark\Block\Element\AbstractBlock;
|
||||
use League\CommonMark\Cursor;
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
use League\CommonMark\Reference\ReferenceInterface;
|
||||
use League\CommonMark\Reference\ReferenceableInterface;
|
||||
|
||||
/**
|
||||
* @method children() AbstractBlock[]
|
||||
*/
|
||||
final class Footnote extends AbstractBlock
|
||||
final class Footnote extends AbstractBlock implements ReferenceableInterface
|
||||
{
|
||||
/**
|
||||
* @var FootnoteBackref[]
|
||||
*/
|
||||
private $backrefs = [];
|
||||
|
||||
/**
|
||||
* @var ReferenceInterface
|
||||
*/
|
||||
private $reference;
|
||||
/** @psalm-readonly */
|
||||
private ReferenceInterface $reference;
|
||||
|
||||
public function __construct(ReferenceInterface $reference)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,22 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Footnote\Node;
|
||||
|
||||
use League\CommonMark\Inline\Element\AbstractInline;
|
||||
use League\CommonMark\Node\Inline\AbstractInline;
|
||||
use League\CommonMark\Reference\ReferenceInterface;
|
||||
use League\CommonMark\Reference\ReferenceableInterface;
|
||||
|
||||
/**
|
||||
* Link from the footnote on the bottom of the document back to the reference
|
||||
*/
|
||||
final class FootnoteBackref extends AbstractInline
|
||||
final class FootnoteBackref extends AbstractInline implements ReferenceableInterface
|
||||
{
|
||||
/** @var ReferenceInterface */
|
||||
private $reference;
|
||||
/** @psalm-readonly */
|
||||
private ReferenceInterface $reference;
|
||||
|
||||
public function __construct(ReferenceInterface $reference)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->reference = $reference;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,26 +14,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Footnote\Node;
|
||||
|
||||
use League\CommonMark\Block\Element\AbstractBlock;
|
||||
use League\CommonMark\Cursor;
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,27 +14,30 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Footnote\Node;
|
||||
|
||||
use League\CommonMark\Inline\Element\AbstractInline;
|
||||
use League\CommonMark\Node\Inline\AbstractInline;
|
||||
use League\CommonMark\Reference\ReferenceInterface;
|
||||
use League\CommonMark\Reference\ReferenceableInterface;
|
||||
|
||||
final class FootnoteRef extends AbstractInline
|
||||
final class FootnoteRef extends AbstractInline implements ReferenceableInterface
|
||||
{
|
||||
/** @var ReferenceInterface */
|
||||
private $reference;
|
||||
private ReferenceInterface $reference;
|
||||
|
||||
/** @var string|null */
|
||||
private $content;
|
||||
/** @psalm-readonly */
|
||||
private ?string $content = null;
|
||||
|
||||
/**
|
||||
* @param ReferenceInterface $reference
|
||||
* @param string|null $content
|
||||
* @param array<mixed> $data
|
||||
* @param array<mixed> $data
|
||||
*/
|
||||
public function __construct(ReferenceInterface $reference, ?string $content = null, array $data = [])
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->reference = $reference;
|
||||
$this->content = $content;
|
||||
$this->data = $data;
|
||||
$this->content = $content;
|
||||
|
||||
if (\count($data) > 0) {
|
||||
$this->data->import($data);
|
||||
}
|
||||
}
|
||||
|
||||
public function getReference(): ReferenceInterface
|
||||
@@ -42,11 +45,9 @@ final class FootnoteRef extends AbstractInline
|
||||
return $this->reference;
|
||||
}
|
||||
|
||||
public function setReference(ReferenceInterface $reference): FootnoteRef
|
||||
public function setReference(ReferenceInterface $reference): void
|
||||
{
|
||||
$this->reference = $reference;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContent(): ?string
|
||||
|
||||
@@ -14,72 +14,53 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Footnote\Parser;
|
||||
|
||||
use League\CommonMark\Environment\EnvironmentAwareInterface;
|
||||
use League\CommonMark\Environment\EnvironmentInterface;
|
||||
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\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserMatch;
|
||||
use League\CommonMark\Parser\InlineParserContext;
|
||||
use League\CommonMark\Reference\Reference;
|
||||
use League\CommonMark\Util\ConfigurationAwareInterface;
|
||||
use League\CommonMark\Util\ConfigurationInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
|
||||
final class AnonymousFootnoteRefParser implements InlineParserInterface, ConfigurationAwareInterface
|
||||
final class AnonymousFootnoteRefParser implements InlineParserInterface, EnvironmentAwareInterface
|
||||
{
|
||||
/** @var ConfigurationInterface */
|
||||
private $config;
|
||||
private ConfigurationInterface $config;
|
||||
|
||||
/** @var TextNormalizerInterface */
|
||||
private $slugNormalizer;
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private TextNormalizerInterface $slugNormalizer;
|
||||
|
||||
public function __construct()
|
||||
public function getMatchDefinition(): InlineParserMatch
|
||||
{
|
||||
$this->slugNormalizer = new SlugNormalizer();
|
||||
}
|
||||
|
||||
public function getCharacters(): array
|
||||
{
|
||||
return ['^'];
|
||||
return InlineParserMatch::regex('\^\[([^\]]+)\]');
|
||||
}
|
||||
|
||||
public function parse(InlineParserContext $inlineContext): bool
|
||||
{
|
||||
$container = $inlineContext->getContainer();
|
||||
$cursor = $inlineContext->getCursor();
|
||||
$nextChar = $cursor->peek();
|
||||
if ($nextChar !== '[') {
|
||||
return false;
|
||||
}
|
||||
$state = $cursor->saveState();
|
||||
$inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength());
|
||||
|
||||
$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]));
|
||||
[$label] = $inlineContext->getSubMatches();
|
||||
$reference = $this->createReference($label);
|
||||
$inlineContext->getContainer()->appendChild(new FootnoteRef($reference, $label));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$cursor->restoreState($state);
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createReference(string $label): Reference
|
||||
{
|
||||
$refLabel = $this->slugNormalizer->normalize($label);
|
||||
$refLabel = \mb_substr($refLabel, 0, 20);
|
||||
$refLabel = $this->slugNormalizer->normalize($label, ['length' => 20]);
|
||||
|
||||
return new Reference(
|
||||
$refLabel,
|
||||
'#' . $this->config->get('footnote/footnote_id_prefix', 'fn:') . $refLabel,
|
||||
'#' . $this->config->get('footnote/footnote_id_prefix') . $refLabel,
|
||||
$label
|
||||
);
|
||||
}
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $config): void
|
||||
public function setEnvironment(EnvironmentInterface $environment): void
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->config = $environment->getConfiguration();
|
||||
$this->slugNormalizer = $environment->getSlugNormalizer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,50 +14,55 @@ 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;
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
|
||||
use League\CommonMark\Parser\Block\BlockContinue;
|
||||
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
use League\CommonMark\Reference\ReferenceInterface;
|
||||
|
||||
final class FootnoteParser implements BlockParserInterface
|
||||
final class FootnoteParser extends AbstractBlockContinueParser
|
||||
{
|
||||
public function parse(ContextInterface $context, Cursor $cursor): bool
|
||||
/** @psalm-readonly */
|
||||
private Footnote $block;
|
||||
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private ?int $indentation = null;
|
||||
|
||||
public function __construct(ReferenceInterface $reference)
|
||||
{
|
||||
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;
|
||||
$this->block = new Footnote($reference);
|
||||
}
|
||||
|
||||
private function createFootnote(string $label): Footnote
|
||||
public function getBlock(): Footnote
|
||||
{
|
||||
return new Footnote(
|
||||
new Reference($label, $label, $label)
|
||||
);
|
||||
return $this->block;
|
||||
}
|
||||
|
||||
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
|
||||
{
|
||||
if ($cursor->isBlank()) {
|
||||
return BlockContinue::at($cursor);
|
||||
}
|
||||
|
||||
if ($cursor->isIndented()) {
|
||||
$this->indentation ??= $cursor->getIndent();
|
||||
$cursor->advanceBy($this->indentation, true);
|
||||
|
||||
return BlockContinue::at($cursor);
|
||||
}
|
||||
|
||||
return BlockContinue::none();
|
||||
}
|
||||
|
||||
public function isContainer(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canContain(AbstractBlock $childBlock): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,58 +15,43 @@ 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\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserMatch;
|
||||
use League\CommonMark\Parser\InlineParserContext;
|
||||
use League\CommonMark\Reference\Reference;
|
||||
use League\CommonMark\Util\ConfigurationAwareInterface;
|
||||
use League\CommonMark\Util\ConfigurationInterface;
|
||||
use League\Config\ConfigurationAwareInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
|
||||
final class FootnoteRefParser implements InlineParserInterface, ConfigurationAwareInterface
|
||||
{
|
||||
/** @var ConfigurationInterface */
|
||||
private $config;
|
||||
private ConfigurationInterface $config;
|
||||
|
||||
public function getCharacters(): array
|
||||
public function getMatchDefinition(): InlineParserMatch
|
||||
{
|
||||
return ['['];
|
||||
return InlineParserMatch::regex('\[\^([^\s\]]+)\]');
|
||||
}
|
||||
|
||||
public function parse(InlineParserContext $inlineContext): bool
|
||||
{
|
||||
$container = $inlineContext->getContainer();
|
||||
$cursor = $inlineContext->getCursor();
|
||||
$nextChar = $cursor->peek();
|
||||
if ($nextChar !== '^') {
|
||||
return false;
|
||||
}
|
||||
$inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength());
|
||||
|
||||
$state = $cursor->saveState();
|
||||
[$label] = $inlineContext->getSubMatches();
|
||||
$inlineContext->getContainer()->appendChild(new FootnoteRef($this->createReference($label)));
|
||||
|
||||
$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;
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createReference(string $label): Reference
|
||||
{
|
||||
return new Reference(
|
||||
$label,
|
||||
'#' . $this->config->get('footnote/footnote_id_prefix', 'fn:') . $label,
|
||||
'#' . $this->config->get('footnote/footnote_id_prefix') . $label,
|
||||
$label
|
||||
);
|
||||
}
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $config): void
|
||||
public function setConfiguration(ConfigurationInterface $configuration): void
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->config = $configuration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,36 +14,68 @@ 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;
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface;
|
||||
use League\CommonMark\Renderer\NodeRendererInterface;
|
||||
use League\CommonMark\Util\HtmlElement;
|
||||
use League\CommonMark\Xml\XmlNodeRendererInterface;
|
||||
use League\Config\ConfigurationAwareInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
|
||||
final class FootnoteBackrefRenderer implements InlineRendererInterface, ConfigurationAwareInterface
|
||||
final class FootnoteBackrefRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface
|
||||
{
|
||||
/** @var ConfigurationInterface */
|
||||
private $config;
|
||||
public const DEFAULT_SYMBOL = '↩';
|
||||
|
||||
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
|
||||
private ConfigurationInterface $config;
|
||||
|
||||
/**
|
||||
* @param FootnoteBackref $node
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer): string
|
||||
{
|
||||
if (!($inline instanceof FootnoteBackref)) {
|
||||
throw new \InvalidArgumentException('Incompatible inline type: ' . \get_class($inline));
|
||||
}
|
||||
FootnoteBackref::assertInstanceOf($node);
|
||||
|
||||
$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';
|
||||
$attrs = $node->data->getData('attributes');
|
||||
|
||||
return ' ' . new HtmlElement('a', $attrs, '↩', true);
|
||||
$attrs->append('class', $this->config->get('footnote/backref_class'));
|
||||
$attrs->set('rev', 'footnote');
|
||||
$attrs->set('href', \mb_strtolower($node->getReference()->getDestination(), 'UTF-8'));
|
||||
$attrs->set('role', 'doc-backlink');
|
||||
|
||||
$symbol = $this->config->get('footnote/backref_symbol');
|
||||
\assert(\is_string($symbol));
|
||||
|
||||
return ' ' . new HtmlElement('a', $attrs->export(), \htmlspecialchars($symbol), true);
|
||||
}
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $configuration)
|
||||
public function setConfiguration(ConfigurationInterface $configuration): void
|
||||
{
|
||||
$this->config = $configuration;
|
||||
}
|
||||
|
||||
public function getXmlTagName(Node $node): string
|
||||
{
|
||||
return 'footnote_backref';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FootnoteBackref $node
|
||||
*
|
||||
* @return array<string, scalar>
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function getXmlAttributes(Node $node): array
|
||||
{
|
||||
FootnoteBackref::assertInstanceOf($node);
|
||||
|
||||
return [
|
||||
'reference' => $node->getReference()->getLabel(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,39 +14,58 @@ 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;
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface;
|
||||
use League\CommonMark\Renderer\NodeRendererInterface;
|
||||
use League\CommonMark\Util\HtmlElement;
|
||||
use League\CommonMark\Xml\XmlNodeRendererInterface;
|
||||
use League\Config\ConfigurationAwareInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
|
||||
final class FootnoteContainerRenderer implements BlockRendererInterface, ConfigurationAwareInterface
|
||||
final class FootnoteContainerRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface
|
||||
{
|
||||
/** @var ConfigurationInterface */
|
||||
private $config;
|
||||
private ConfigurationInterface $config;
|
||||
|
||||
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
|
||||
/**
|
||||
* @param FootnoteContainer $node
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
|
||||
{
|
||||
if (!($block instanceof FootnoteContainer)) {
|
||||
throw new \InvalidArgumentException('Incompatible block type: ' . \get_class($block));
|
||||
}
|
||||
FootnoteContainer::assertInstanceOf($node);
|
||||
|
||||
$attrs = $block->getData('attributes', []);
|
||||
$attrs['class'] = $attrs['class'] ?? $this->config->get('footnote/container_class', 'footnotes');
|
||||
$attrs['role'] = 'doc-endnotes';
|
||||
$attrs = $node->data->getData('attributes');
|
||||
|
||||
$contents = new HtmlElement('ol', [], $htmlRenderer->renderBlocks($block->children()));
|
||||
if ($this->config->get('footnote/container_add_hr', true)) {
|
||||
$attrs->append('class', $this->config->get('footnote/container_class'));
|
||||
$attrs->set('role', 'doc-endnotes');
|
||||
|
||||
$contents = new HtmlElement('ol', [], $childRenderer->renderNodes($node->children()));
|
||||
if ($this->config->get('footnote/container_add_hr')) {
|
||||
$contents = [new HtmlElement('hr', [], null, true), $contents];
|
||||
}
|
||||
|
||||
return new HtmlElement('div', $attrs, $contents);
|
||||
return new HtmlElement('div', $attrs->export(), $contents);
|
||||
}
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $configuration)
|
||||
public function setConfiguration(ConfigurationInterface $configuration): void
|
||||
{
|
||||
$this->config = $configuration;
|
||||
}
|
||||
|
||||
public function getXmlTagName(Node $node): string
|
||||
{
|
||||
return 'footnote_container';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, scalar>
|
||||
*/
|
||||
public function getXmlAttributes(Node $node): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,49 +14,74 @@ 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;
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface;
|
||||
use League\CommonMark\Renderer\NodeRendererInterface;
|
||||
use League\CommonMark\Util\HtmlElement;
|
||||
use League\CommonMark\Xml\XmlNodeRendererInterface;
|
||||
use League\Config\ConfigurationAwareInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
|
||||
final class FootnoteRefRenderer implements InlineRendererInterface, ConfigurationAwareInterface
|
||||
final class FootnoteRefRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface
|
||||
{
|
||||
/** @var ConfigurationInterface */
|
||||
private $config;
|
||||
private ConfigurationInterface $config;
|
||||
|
||||
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
|
||||
/**
|
||||
* @param FootnoteRef $node
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
|
||||
{
|
||||
if (!($inline instanceof FootnoteRef)) {
|
||||
throw new \InvalidArgumentException('Incompatible inline type: ' . \get_class($inline));
|
||||
}
|
||||
FootnoteRef::assertInstanceOf($node);
|
||||
|
||||
$attrs = $inline->getData('attributes', []);
|
||||
$class = $attrs['class'] ?? $this->config->get('footnote/ref_class', 'footnote-ref');
|
||||
$idPrefix = $this->config->get('footnote/ref_id_prefix', 'fnref:');
|
||||
$attrs = $node->data->getData('attributes');
|
||||
$attrs->append('class', $this->config->get('footnote/ref_class'));
|
||||
$attrs->set('href', \mb_strtolower($node->getReference()->getDestination(), 'UTF-8'));
|
||||
$attrs->set('role', 'doc-noteref');
|
||||
|
||||
$idPrefix = $this->config->get('footnote/ref_id_prefix');
|
||||
|
||||
return new HtmlElement(
|
||||
'sup',
|
||||
[
|
||||
'id' => $idPrefix . \mb_strtolower($inline->getReference()->getLabel()),
|
||||
'id' => $idPrefix . \mb_strtolower($node->getReference()->getLabel(), 'UTF-8'),
|
||||
],
|
||||
new HTMLElement(
|
||||
new HtmlElement(
|
||||
'a',
|
||||
[
|
||||
'class' => $class,
|
||||
'href' => \mb_strtolower($inline->getReference()->getDestination()),
|
||||
'role' => 'doc-noteref',
|
||||
],
|
||||
$inline->getReference()->getTitle()
|
||||
$attrs->export(),
|
||||
$node->getReference()->getTitle()
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $configuration)
|
||||
public function setConfiguration(ConfigurationInterface $configuration): void
|
||||
{
|
||||
$this->config = $configuration;
|
||||
}
|
||||
|
||||
public function getXmlTagName(Node $node): string
|
||||
{
|
||||
return 'footnote_ref';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FootnoteRef $node
|
||||
*
|
||||
* @return array<string, scalar>
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function getXmlAttributes(Node $node): array
|
||||
{
|
||||
FootnoteRef::assertInstanceOf($node);
|
||||
|
||||
return [
|
||||
'reference' => $node->getReference()->getLabel(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,51 +14,67 @@ 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;
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface;
|
||||
use League\CommonMark\Renderer\NodeRendererInterface;
|
||||
use League\CommonMark\Util\HtmlElement;
|
||||
use League\CommonMark\Xml\XmlNodeRendererInterface;
|
||||
use League\Config\ConfigurationAwareInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
|
||||
final class FootnoteRenderer implements BlockRendererInterface, ConfigurationAwareInterface
|
||||
final class FootnoteRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface
|
||||
{
|
||||
/** @var ConfigurationInterface */
|
||||
private $config;
|
||||
private ConfigurationInterface $config;
|
||||
|
||||
/**
|
||||
* @param Footnote $block
|
||||
* @param ElementRendererInterface $htmlRenderer
|
||||
* @param bool $inTightList
|
||||
* @param Footnote $node
|
||||
*
|
||||
* @return HtmlElement
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
|
||||
{
|
||||
if (!($block instanceof Footnote)) {
|
||||
throw new \InvalidArgumentException('Incompatible block type: ' . \get_class($block));
|
||||
}
|
||||
Footnote::assertInstanceOf($node);
|
||||
|
||||
$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';
|
||||
$attrs = $node->data->getData('attributes');
|
||||
|
||||
foreach ($block->getBackrefs() as $backref) {
|
||||
$block->lastChild()->appendChild($backref);
|
||||
}
|
||||
$attrs->append('class', $this->config->get('footnote/footnote_class'));
|
||||
$attrs->set('id', $this->config->get('footnote/footnote_id_prefix') . \mb_strtolower($node->getReference()->getLabel(), 'UTF-8'));
|
||||
$attrs->set('role', 'doc-endnote');
|
||||
|
||||
return new HtmlElement(
|
||||
'li',
|
||||
$attrs,
|
||||
$htmlRenderer->renderBlocks($block->children()),
|
||||
$attrs->export(),
|
||||
$childRenderer->renderNodes($node->children()),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $configuration)
|
||||
public function setConfiguration(ConfigurationInterface $configuration): void
|
||||
{
|
||||
$this->config = $configuration;
|
||||
}
|
||||
|
||||
public function getXmlTagName(Node $node): string
|
||||
{
|
||||
return 'footnote';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Footnote $node
|
||||
*
|
||||
* @return array<string, scalar>
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function getXmlAttributes(Node $node): array
|
||||
{
|
||||
Footnote::assertInstanceOf($node);
|
||||
|
||||
return [
|
||||
'reference' => $node->getReference()->getLabel(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,7 +13,7 @@
|
||||
|
||||
namespace League\CommonMark\Extension;
|
||||
|
||||
use League\CommonMark\ConfigurableEnvironmentInterface;
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Extension\Autolink\AutolinkExtension;
|
||||
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
|
||||
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
|
||||
@@ -20,7 +22,7 @@ use League\CommonMark\Extension\TaskList\TaskListExtension;
|
||||
|
||||
final class GithubFlavoredMarkdownExtension implements ExtensionInterface
|
||||
{
|
||||
public function register(ConfigurableEnvironmentInterface $environment)
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$environment->addExtension(new AutolinkExtension());
|
||||
$environment->addExtension(new DisallowedRawHtmlExtension());
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,18 +13,20 @@
|
||||
|
||||
namespace League\CommonMark\Extension\HeadingPermalink;
|
||||
|
||||
use League\CommonMark\Inline\Element\AbstractInline;
|
||||
use League\CommonMark\Node\Inline\AbstractInline;
|
||||
|
||||
/**
|
||||
* Represents an anchor link within a heading
|
||||
*/
|
||||
final class HeadingPermalink extends AbstractInline
|
||||
{
|
||||
/** @var string */
|
||||
private $slug;
|
||||
/** @psalm-readonly */
|
||||
private string $slug;
|
||||
|
||||
public function __construct(string $slug)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->slug = $slug;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,18 +13,37 @@
|
||||
|
||||
namespace League\CommonMark\Extension\HeadingPermalink;
|
||||
|
||||
use League\CommonMark\ConfigurableEnvironmentInterface;
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Extension\ExtensionInterface;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\Config\ConfigurationBuilderInterface;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
/**
|
||||
* Extension which automatically anchor links to heading elements
|
||||
*/
|
||||
final class HeadingPermalinkExtension implements ExtensionInterface
|
||||
final class HeadingPermalinkExtension implements ConfigurableExtensionInterface
|
||||
{
|
||||
public function register(ConfigurableEnvironmentInterface $environment)
|
||||
public function configureSchema(ConfigurationBuilderInterface $builder): void
|
||||
{
|
||||
$builder->addSchema('heading_permalink', Expect::structure([
|
||||
'min_heading_level' => Expect::int()->min(1)->max(6)->default(1),
|
||||
'max_heading_level' => Expect::int()->min(1)->max(6)->default(6),
|
||||
'insert' => Expect::anyOf(HeadingPermalinkProcessor::INSERT_BEFORE, HeadingPermalinkProcessor::INSERT_AFTER, HeadingPermalinkProcessor::INSERT_NONE)->default(HeadingPermalinkProcessor::INSERT_BEFORE),
|
||||
'id_prefix' => Expect::string()->default('content'),
|
||||
'apply_id_to_heading' => Expect::bool()->default(false),
|
||||
'heading_class' => Expect::string()->default(''),
|
||||
'fragment_prefix' => Expect::string()->default('content'),
|
||||
'html_class' => Expect::string()->default('heading-permalink'),
|
||||
'title' => Expect::string()->default('Permalink'),
|
||||
'symbol' => Expect::string()->default(HeadingPermalinkRenderer::DEFAULT_SYMBOL),
|
||||
'aria_hidden' => Expect::bool()->default(true),
|
||||
]));
|
||||
}
|
||||
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$environment->addEventListener(DocumentParsedEvent::class, new HeadingPermalinkProcessor(), -100);
|
||||
$environment->addInlineRenderer(HeadingPermalink::class, new HeadingPermalinkRenderer());
|
||||
$environment->addRenderer(HeadingPermalink::class, new HeadingPermalinkRenderer());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,91 +13,77 @@
|
||||
|
||||
namespace League\CommonMark\Extension\HeadingPermalink;
|
||||
|
||||
use League\CommonMark\Block\Element\Document;
|
||||
use League\CommonMark\Block\Element\Heading;
|
||||
use League\CommonMark\Environment\EnvironmentAwareInterface;
|
||||
use League\CommonMark\Environment\EnvironmentInterface;
|
||||
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\Extension\CommonMark\Node\Block\Heading;
|
||||
use League\CommonMark\Node\NodeIterator;
|
||||
use League\CommonMark\Node\RawMarkupContainerInterface;
|
||||
use League\CommonMark\Node\StringContainerHelper;
|
||||
use League\CommonMark\Normalizer\TextNormalizerInterface;
|
||||
use League\CommonMark\Util\ConfigurationAwareInterface;
|
||||
use League\CommonMark\Util\ConfigurationInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
use League\Config\Exception\InvalidConfigurationException;
|
||||
|
||||
/**
|
||||
* Searches the Document for Heading elements and adds HeadingPermalinks to each one
|
||||
*/
|
||||
final class HeadingPermalinkProcessor implements ConfigurationAwareInterface
|
||||
final class HeadingPermalinkProcessor implements EnvironmentAwareInterface
|
||||
{
|
||||
const INSERT_BEFORE = 'before';
|
||||
const INSERT_AFTER = 'after';
|
||||
public const INSERT_BEFORE = 'before';
|
||||
public const INSERT_AFTER = 'after';
|
||||
public const INSERT_NONE = 'none';
|
||||
|
||||
/** @var TextNormalizerInterface|DeprecatedSlugGeneratorInterface */
|
||||
private $slugNormalizer;
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private TextNormalizerInterface $slugNormalizer;
|
||||
|
||||
/** @var ConfigurationInterface */
|
||||
private $config;
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private ConfigurationInterface $config;
|
||||
|
||||
/**
|
||||
* @param TextNormalizerInterface|DeprecatedSlugGeneratorInterface|null $slugNormalizer
|
||||
*/
|
||||
public function __construct($slugNormalizer = null)
|
||||
public function setEnvironment(EnvironmentInterface $environment): void
|
||||
{
|
||||
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;
|
||||
$this->config = $environment->getConfiguration();
|
||||
$this->slugNormalizer = $environment->getSlugNormalizer();
|
||||
}
|
||||
|
||||
public function __invoke(DocumentParsedEvent $e): void
|
||||
{
|
||||
$this->useNormalizerFromConfigurationIfProvided();
|
||||
$min = (int) $this->config->get('heading_permalink/min_heading_level');
|
||||
$max = (int) $this->config->get('heading_permalink/max_heading_level');
|
||||
$applyToHeading = (bool) $this->config->get('heading_permalink/apply_id_to_heading');
|
||||
$idPrefix = (string) $this->config->get('heading_permalink/id_prefix');
|
||||
$slugLength = (int) $this->config->get('slug_normalizer/max_length');
|
||||
$headingClass = (string) $this->config->get('heading_permalink/heading_class');
|
||||
|
||||
$walker = $e->getDocument()->walker();
|
||||
if ($idPrefix !== '') {
|
||||
$idPrefix .= '-';
|
||||
}
|
||||
|
||||
while ($event = $walker->next()) {
|
||||
$node = $event->getNode();
|
||||
if ($node instanceof Heading && $event->isEntering()) {
|
||||
$this->addHeadingLink($node, $e->getDocument());
|
||||
foreach ($e->getDocument()->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) {
|
||||
if ($node instanceof Heading && $node->getLevel() >= $min && $node->getLevel() <= $max) {
|
||||
$this->addHeadingLink($node, $slugLength, $idPrefix, $applyToHeading, $headingClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function useNormalizerFromConfigurationIfProvided(): void
|
||||
private function addHeadingLink(Heading $heading, int $slugLength, string $idPrefix, bool $applyToHeading, string $headingClass): void
|
||||
{
|
||||
$generator = $this->config->get('heading_permalink/slug_normalizer');
|
||||
if ($generator === null) {
|
||||
return;
|
||||
$text = StringContainerHelper::getChildText($heading, [RawMarkupContainerInterface::class]);
|
||||
$slug = $this->slugNormalizer->normalize($text, [
|
||||
'node' => $heading,
|
||||
'length' => $slugLength,
|
||||
]);
|
||||
|
||||
if ($applyToHeading) {
|
||||
$heading->data->set('attributes/id', $idPrefix . $slug);
|
||||
}
|
||||
|
||||
if (!($generator instanceof DeprecatedSlugGeneratorInterface || $generator instanceof TextNormalizerInterface)) {
|
||||
throw new InvalidOptionException('The heading_permalink/slug_normalizer option must be an instance of ' . TextNormalizerInterface::class);
|
||||
if ($headingClass !== '') {
|
||||
$heading->data->append('attributes/class', $headingClass);
|
||||
}
|
||||
|
||||
$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')) {
|
||||
switch ($this->config->get('heading_permalink/insert')) {
|
||||
case self::INSERT_BEFORE:
|
||||
$heading->prependChild($headingLinkAnchor);
|
||||
|
||||
@@ -103,45 +91,11 @@ final class HeadingPermalinkProcessor implements ConfigurationAwareInterface
|
||||
case self::INSERT_AFTER:
|
||||
$heading->appendChild($headingLinkAnchor);
|
||||
|
||||
return;
|
||||
case self::INSERT_NONE:
|
||||
return;
|
||||
default:
|
||||
throw new \RuntimeException("Invalid configuration value for heading_permalink/insert; expected 'before' or 'after'");
|
||||
throw new InvalidConfigurationException("Invalid configuration value for heading_permalink/insert; expected 'before', 'after', or 'none'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,62 +13,94 @@
|
||||
|
||||
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;
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface;
|
||||
use League\CommonMark\Renderer\NodeRendererInterface;
|
||||
use League\CommonMark\Util\HtmlElement;
|
||||
use League\CommonMark\Xml\XmlNodeRendererInterface;
|
||||
use League\Config\ConfigurationAwareInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
|
||||
/**
|
||||
* Renders the HeadingPermalink elements
|
||||
*/
|
||||
final class HeadingPermalinkRenderer implements InlineRendererInterface, ConfigurationAwareInterface
|
||||
final class HeadingPermalinkRenderer implements NodeRendererInterface, XmlNodeRendererInterface, 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>';
|
||||
public const DEFAULT_SYMBOL = '¶';
|
||||
|
||||
const DEFAULT_SYMBOL = '¶';
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private ConfigurationInterface $config;
|
||||
|
||||
/** @var ConfigurationInterface */
|
||||
private $config;
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $configuration)
|
||||
public function setConfiguration(ConfigurationInterface $configuration): void
|
||||
{
|
||||
$this->config = $configuration;
|
||||
}
|
||||
|
||||
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
|
||||
/**
|
||||
* @param HeadingPermalink $node
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
|
||||
{
|
||||
if (!$inline instanceof HeadingPermalink) {
|
||||
throw new \InvalidArgumentException('Incompatible inline type: ' . \get_class($inline));
|
||||
HeadingPermalink::assertInstanceOf($node);
|
||||
|
||||
$slug = $node->getSlug();
|
||||
|
||||
$fragmentPrefix = (string) $this->config->get('heading_permalink/fragment_prefix');
|
||||
if ($fragmentPrefix !== '') {
|
||||
$fragmentPrefix .= '-';
|
||||
}
|
||||
|
||||
$slug = $inline->getSlug();
|
||||
$attrs = $node->data->getData('attributes');
|
||||
$appendId = ! $this->config->get('heading_permalink/apply_id_to_heading');
|
||||
|
||||
$idPrefix = (string) $this->config->get('heading_permalink/id_prefix', 'user-content');
|
||||
if ($idPrefix !== '') {
|
||||
$idPrefix .= '-';
|
||||
if ($appendId) {
|
||||
$idPrefix = (string) $this->config->get('heading_permalink/id_prefix');
|
||||
|
||||
if ($idPrefix !== '') {
|
||||
$idPrefix .= '-';
|
||||
}
|
||||
|
||||
$attrs->set('id', $idPrefix . $slug);
|
||||
}
|
||||
|
||||
$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'),
|
||||
$attrs->set('href', '#' . $fragmentPrefix . $slug);
|
||||
$attrs->append('class', $this->config->get('heading_permalink/html_class'));
|
||||
|
||||
$hidden = $this->config->get('heading_permalink/aria_hidden');
|
||||
if ($hidden) {
|
||||
$attrs->set('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
$attrs->set('title', $this->config->get('heading_permalink/title'));
|
||||
|
||||
$symbol = $this->config->get('heading_permalink/symbol');
|
||||
\assert(\is_string($symbol));
|
||||
|
||||
return new HtmlElement('a', $attrs->export(), \htmlspecialchars($symbol), false);
|
||||
}
|
||||
|
||||
public function getXmlTagName(Node $node): string
|
||||
{
|
||||
return 'heading_permalink';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HeadingPermalink $node
|
||||
*
|
||||
* @return array<string, scalar>
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function getXmlAttributes(Node $node): array
|
||||
{
|
||||
HeadingPermalink::assertInstanceOf($node);
|
||||
|
||||
return [
|
||||
'slug' => $node->getSlug(),
|
||||
];
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,34 +13,21 @@
|
||||
|
||||
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;
|
||||
use League\CommonMark\Node\Block\Document;
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface;
|
||||
use League\CommonMark\Renderer\NodeRendererInterface;
|
||||
|
||||
/**
|
||||
* Simply renders child elements as-is, adding newlines as needed.
|
||||
*/
|
||||
final class ChildRenderer implements BlockRendererInterface
|
||||
final class ChildRenderer implements NodeRendererInterface
|
||||
{
|
||||
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer): string
|
||||
{
|
||||
$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";
|
||||
$out = $childRenderer->renderNodes($node->children());
|
||||
if (! $node instanceof Document) {
|
||||
$out .= $childRenderer->getBlockSeparator();
|
||||
}
|
||||
|
||||
return $out;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,52 +13,60 @@
|
||||
|
||||
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;
|
||||
use League\CommonMark as Core;
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Extension\CommonMark;
|
||||
use League\CommonMark\Extension\CommonMark\Delimiter\Processor\EmphasisDelimiterProcessor;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\Config\ConfigurationBuilderInterface;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
final class InlinesOnlyExtension implements ExtensionInterface
|
||||
final class InlinesOnlyExtension implements ConfigurableExtensionInterface
|
||||
{
|
||||
public function register(ConfigurableEnvironmentInterface $environment)
|
||||
public function configureSchema(ConfigurationBuilderInterface $builder): void
|
||||
{
|
||||
$builder->addSchema('commonmark', Expect::structure([
|
||||
'use_asterisk' => Expect::bool(true),
|
||||
'use_underscore' => Expect::bool(true),
|
||||
'enable_strong' => Expect::bool(true),
|
||||
'enable_em' => Expect::bool(true),
|
||||
]));
|
||||
}
|
||||
|
||||
// phpcs:disable Generic.Functions.FunctionCallArgumentSpacing.TooMuchSpaceAfterComma,Squiz.WhiteSpace.SemicolonSpacing.Incorrect
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$childRenderer = new ChildRenderer();
|
||||
|
||||
$environment
|
||||
->addBlockParser(new BlockParser\LazyParagraphParser(), -200)
|
||||
->addInlineParser(new Core\Parser\Inline\NewlineParser(), 200)
|
||||
->addInlineParser(new CommonMark\Parser\Inline\BacktickParser(), 150)
|
||||
->addInlineParser(new CommonMark\Parser\Inline\EscapableParser(), 80)
|
||||
->addInlineParser(new CommonMark\Parser\Inline\EntityParser(), 70)
|
||||
->addInlineParser(new CommonMark\Parser\Inline\AutolinkParser(), 50)
|
||||
->addInlineParser(new CommonMark\Parser\Inline\HtmlInlineParser(), 40)
|
||||
->addInlineParser(new CommonMark\Parser\Inline\CloseBracketParser(), 30)
|
||||
->addInlineParser(new CommonMark\Parser\Inline\OpenBracketParser(), 20)
|
||||
->addInlineParser(new CommonMark\Parser\Inline\BangParser(), 10)
|
||||
|
||||
->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)
|
||||
->addRenderer(Core\Node\Block\Document::class, $childRenderer, 0)
|
||||
->addRenderer(Core\Node\Block\Paragraph::class, $childRenderer, 0)
|
||||
|
||||
->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)
|
||||
->addRenderer(CommonMark\Node\Inline\Code::class, new CommonMark\Renderer\Inline\CodeRenderer(), 0)
|
||||
->addRenderer(CommonMark\Node\Inline\Emphasis::class, new CommonMark\Renderer\Inline\EmphasisRenderer(), 0)
|
||||
->addRenderer(CommonMark\Node\Inline\HtmlInline::class, new CommonMark\Renderer\Inline\HtmlInlineRenderer(), 0)
|
||||
->addRenderer(CommonMark\Node\Inline\Image::class, new CommonMark\Renderer\Inline\ImageRenderer(), 0)
|
||||
->addRenderer(CommonMark\Node\Inline\Link::class, new CommonMark\Renderer\Inline\LinkRenderer(), 0)
|
||||
->addRenderer(Core\Node\Inline\Newline::class, new Core\Renderer\Inline\NewlineRenderer(), 0)
|
||||
->addRenderer(CommonMark\Node\Inline\Strong::class, new CommonMark\Renderer\Inline\StrongRenderer(), 0)
|
||||
->addRenderer(Core\Node\Inline\Text::class, new Core\Renderer\Inline\TextRenderer(), 0)
|
||||
;
|
||||
|
||||
if ($environment->getConfig('use_asterisk', true)) {
|
||||
if ($environment->getConfiguration()->get('commonmark/use_asterisk')) {
|
||||
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('*'));
|
||||
}
|
||||
if ($environment->getConfig('use_underscore', true)) {
|
||||
|
||||
if ($environment->getConfiguration()->get('commonmark/use_underscore')) {
|
||||
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('_'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,8 +13,9 @@
|
||||
|
||||
namespace League\CommonMark\Extension\Mention\Generator;
|
||||
|
||||
use League\CommonMark\Exception\LogicException;
|
||||
use League\CommonMark\Extension\Mention\Mention;
|
||||
use League\CommonMark\Inline\Element\AbstractInline;
|
||||
use League\CommonMark\Node\Inline\AbstractInline;
|
||||
|
||||
final class CallbackGenerator implements MentionGeneratorInterface
|
||||
{
|
||||
@@ -28,14 +31,17 @@ final class CallbackGenerator implements MentionGeneratorInterface
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LogicException
|
||||
*/
|
||||
public function generateMention(Mention $mention): ?AbstractInline
|
||||
{
|
||||
$result = \call_user_func_array($this->callback, [$mention]);
|
||||
$result = \call_user_func($this->callback, $mention);
|
||||
if ($result === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($result instanceof AbstractInline && !($result instanceof Mention)) {
|
||||
if ($result instanceof AbstractInline && ! ($result instanceof Mention)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -43,6 +49,6 @@ final class CallbackGenerator implements MentionGeneratorInterface
|
||||
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');
|
||||
throw new LogicException('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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -12,14 +14,9 @@
|
||||
namespace League\CommonMark\Extension\Mention\Generator;
|
||||
|
||||
use League\CommonMark\Extension\Mention\Mention;
|
||||
use League\CommonMark\Inline\Element\AbstractInline;
|
||||
use League\CommonMark\Node\Inline\AbstractInline;
|
||||
|
||||
interface MentionGeneratorInterface
|
||||
{
|
||||
/**
|
||||
* @param Mention $mention
|
||||
*
|
||||
* @return AbstractInline|null
|
||||
*/
|
||||
public function generateMention(Mention $mention): ?AbstractInline;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -12,12 +14,11 @@
|
||||
namespace League\CommonMark\Extension\Mention\Generator;
|
||||
|
||||
use League\CommonMark\Extension\Mention\Mention;
|
||||
use League\CommonMark\Inline\Element\AbstractInline;
|
||||
use League\CommonMark\Node\Inline\AbstractInline;
|
||||
|
||||
final class StringTemplateLinkGenerator implements MentionGeneratorInterface
|
||||
{
|
||||
/** @var string */
|
||||
private $urlTemplate;
|
||||
private string $urlTemplate;
|
||||
|
||||
public function __construct(string $urlTemplate)
|
||||
{
|
||||
@@ -26,6 +27,8 @@ final class StringTemplateLinkGenerator implements MentionGeneratorInterface
|
||||
|
||||
public function generateMention(Mention $mention): ?AbstractInline
|
||||
{
|
||||
return $mention->setUrl(\sprintf($this->urlTemplate, $mention->getIdentifier()));
|
||||
$mention->setUrl(\sprintf($this->urlTemplate, $mention->getIdentifier()));
|
||||
|
||||
return $mention;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -14,69 +16,56 @@
|
||||
|
||||
namespace League\CommonMark\Extension\Mention;
|
||||
|
||||
use League\CommonMark\Inline\Element\Link;
|
||||
use League\CommonMark\Inline\Element\Text;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
|
||||
use League\CommonMark\Node\Inline\Text;
|
||||
|
||||
class Mention extends Link
|
||||
{
|
||||
/** @var string */
|
||||
private $symbol;
|
||||
private string $name;
|
||||
|
||||
/** @var string */
|
||||
private $identifier;
|
||||
private string $prefix;
|
||||
|
||||
/**
|
||||
* @param string $symbol
|
||||
* @param string $identifier
|
||||
* @param string $label
|
||||
*/
|
||||
public function __construct(string $symbol, string $identifier, string $label = null)
|
||||
private string $identifier;
|
||||
|
||||
public function __construct(string $name, string $prefix, string $identifier, ?string $label = null)
|
||||
{
|
||||
$this->symbol = $symbol;
|
||||
$this->name = $name;
|
||||
$this->prefix = $prefix;
|
||||
$this->identifier = $identifier;
|
||||
|
||||
parent::__construct('', $label ?? \sprintf('%s%s', $symbol, $identifier));
|
||||
parent::__construct('', $label ?? \sprintf('%s%s', $prefix, $identifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
if (($labelNode = $this->findLabelNode()) === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $labelNode->getContent();
|
||||
return $labelNode->getLiteral();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSymbol(): string
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->symbol;
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getPrefix(): string
|
||||
{
|
||||
return $this->prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasUrl(): bool
|
||||
{
|
||||
return !empty($this->url);
|
||||
return $this->url !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $label
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setLabel(string $label): self
|
||||
@@ -86,7 +75,7 @@ class Mention extends Link
|
||||
$this->prependChild($labelNode);
|
||||
}
|
||||
|
||||
$labelNode->setContent($label);
|
||||
$labelNode->setLiteral($label);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,55 +13,49 @@
|
||||
|
||||
namespace League\CommonMark\Extension\Mention;
|
||||
|
||||
use League\CommonMark\ConfigurableEnvironmentInterface;
|
||||
use League\CommonMark\Exception\InvalidOptionException;
|
||||
use League\CommonMark\Extension\ExtensionInterface;
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\CommonMark\Extension\Mention\Generator\MentionGeneratorInterface;
|
||||
use League\Config\ConfigurationBuilderInterface;
|
||||
use League\Config\Exception\InvalidConfigurationException;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
final class MentionExtension implements ExtensionInterface
|
||||
final class MentionExtension implements ConfigurableExtensionInterface
|
||||
{
|
||||
public function register(ConfigurableEnvironmentInterface $environment)
|
||||
public function configureSchema(ConfigurationBuilderInterface $builder): void
|
||||
{
|
||||
$mentions = $environment->getConfig('mentions', []);
|
||||
$isAValidPartialRegex = static function (string $regex): bool {
|
||||
$regex = '/' . $regex . '/i';
|
||||
|
||||
return @\preg_match($regex, '') !== false;
|
||||
};
|
||||
|
||||
$builder->addSchema('mentions', Expect::arrayOf(
|
||||
Expect::structure([
|
||||
'prefix' => Expect::string()->required(),
|
||||
'pattern' => Expect::string()->assert($isAValidPartialRegex, 'Pattern must not include starting/ending delimiters (like "/")')->required(),
|
||||
'generator' => Expect::anyOf(
|
||||
Expect::type(MentionGeneratorInterface::class),
|
||||
Expect::string(),
|
||||
Expect::type('callable')
|
||||
)->required(),
|
||||
])
|
||||
));
|
||||
}
|
||||
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$mentions = $environment->getConfiguration()->get('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']));
|
||||
$environment->addInlineParser(new MentionParser($name, $mention['prefix'], $mention['pattern'], $mention['generator']));
|
||||
} elseif (\is_string($mention['generator'])) {
|
||||
$environment->addInlineParser(MentionParser::createWithStringTemplate($name, $mention['prefix'], $mention['pattern'], $mention['generator']));
|
||||
} elseif (\is_callable($mention['generator'])) {
|
||||
$environment->addInlineParser(MentionParser::createWithCallback($name, $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));
|
||||
throw new InvalidConfigurationException(\sprintf('The "generator" provided for the "%s" MentionParser configuration must be a string template, callable, or an object that implements %s.', $name, MentionGeneratorInterface::class));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function isAValidPartialRegex(string $regex): bool
|
||||
{
|
||||
$regex = '/' . $regex . '/i';
|
||||
|
||||
return @\preg_match($regex, '') !== false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -14,78 +16,72 @@ 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;
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserMatch;
|
||||
use League\CommonMark\Parser\InlineParserContext;
|
||||
|
||||
final class MentionParser implements InlineParserInterface
|
||||
{
|
||||
/** @var string */
|
||||
private $symbol;
|
||||
/** @psalm-readonly */
|
||||
private string $name;
|
||||
|
||||
/** @var string */
|
||||
private $mentionRegex;
|
||||
/** @psalm-readonly */
|
||||
private string $prefix;
|
||||
|
||||
/** @var MentionGeneratorInterface */
|
||||
private $mentionGenerator;
|
||||
/** @psalm-readonly */
|
||||
private string $identifierPattern;
|
||||
|
||||
public function __construct(string $symbol, string $mentionRegex, MentionGeneratorInterface $mentionGenerator)
|
||||
/** @psalm-readonly */
|
||||
private MentionGeneratorInterface $mentionGenerator;
|
||||
|
||||
public function __construct(string $name, string $prefix, string $identifierPattern, MentionGeneratorInterface $mentionGenerator)
|
||||
{
|
||||
$this->symbol = $symbol;
|
||||
$this->mentionRegex = $mentionRegex;
|
||||
$this->mentionGenerator = $mentionGenerator;
|
||||
$this->name = $name;
|
||||
$this->prefix = $prefix;
|
||||
$this->identifierPattern = $identifierPattern;
|
||||
$this->mentionGenerator = $mentionGenerator;
|
||||
}
|
||||
|
||||
public function getCharacters(): array
|
||||
public function getMatchDefinition(): InlineParserMatch
|
||||
{
|
||||
return [$this->symbol];
|
||||
return InlineParserMatch::join(
|
||||
InlineParserMatch::string($this->prefix),
|
||||
InlineParserMatch::regex($this->identifierPattern)
|
||||
);
|
||||
}
|
||||
|
||||
public function parse(InlineParserContext $inlineContext): bool
|
||||
{
|
||||
$cursor = $inlineContext->getCursor();
|
||||
|
||||
// The symbol must not have any other characters immediately prior
|
||||
// The prefix 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();
|
||||
[$prefix, $identifier] = $inlineContext->getSubMatches();
|
||||
|
||||
// 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));
|
||||
$mention = $this->mentionGenerator->generateMention(new Mention($this->name, $prefix, $identifier));
|
||||
|
||||
if ($mention === null) {
|
||||
$cursor->restoreState($previousState);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$cursor->advanceBy($inlineContext->getFullMatchLength());
|
||||
$inlineContext->getContainer()->appendChild($mention);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function createWithStringTemplate(string $symbol, string $mentionRegex, string $urlTemplate): MentionParser
|
||||
public static function createWithStringTemplate(string $name, string $prefix, string $mentionRegex, string $urlTemplate): MentionParser
|
||||
{
|
||||
return new self($symbol, $mentionRegex, new StringTemplateLinkGenerator($urlTemplate));
|
||||
return new self($name, $prefix, $mentionRegex, new StringTemplateLinkGenerator($urlTemplate));
|
||||
}
|
||||
|
||||
public static function createWithCallback(string $symbol, string $mentionRegex, callable $callback): MentionParser
|
||||
public static function createWithCallback(string $name, string $prefix, string $mentionRegex, callable $callback): MentionParser
|
||||
{
|
||||
return new self($symbol, $mentionRegex, new CallbackGenerator($callback));
|
||||
return new self($name, $prefix, $mentionRegex, new CallbackGenerator($callback));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -14,15 +16,15 @@
|
||||
|
||||
namespace League\CommonMark\Extension\SmartPunct;
|
||||
|
||||
use League\CommonMark\Inline\Element\AbstractStringContainer;
|
||||
use League\CommonMark\Node\Inline\AbstractStringContainer;
|
||||
|
||||
final class Quote extends AbstractStringContainer
|
||||
{
|
||||
public const DOUBLE_QUOTE = '"';
|
||||
public const DOUBLE_QUOTE = '"';
|
||||
public const DOUBLE_QUOTE_OPENER = '“';
|
||||
public const DOUBLE_QUOTE_CLOSER = '”';
|
||||
|
||||
public const SINGLE_QUOTE = "'";
|
||||
public const SINGLE_QUOTE = "'";
|
||||
public const SINGLE_QUOTE_OPENER = '‘';
|
||||
public const SINGLE_QUOTE_CLOSER = '’';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -15,21 +17,26 @@
|
||||
namespace League\CommonMark\Extension\SmartPunct;
|
||||
|
||||
use League\CommonMark\Delimiter\Delimiter;
|
||||
use League\CommonMark\Inline\Parser\InlineParserInterface;
|
||||
use League\CommonMark\InlineParserContext;
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserMatch;
|
||||
use League\CommonMark\Parser\InlineParserContext;
|
||||
use League\CommonMark\Util\RegexHelper;
|
||||
|
||||
final class QuoteParser implements InlineParserInterface
|
||||
{
|
||||
/**
|
||||
* @deprecated This constant is no longer used and will be removed in a future major release
|
||||
*/
|
||||
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[]
|
||||
* @deprecated This constant is no longer used and will be removed in a future major release
|
||||
*/
|
||||
public function getCharacters(): array
|
||||
public const SINGLE_QUOTES = [Quote::SINGLE_QUOTE, Quote::SINGLE_QUOTE_OPENER, Quote::SINGLE_QUOTE_CLOSER];
|
||||
|
||||
public function getMatchDefinition(): InlineParserMatch
|
||||
{
|
||||
return array_merge(self::DOUBLE_QUOTES, self::SINGLE_QUOTES);
|
||||
return InlineParserMatch::oneOf(Quote::SINGLE_QUOTE, Quote::DOUBLE_QUOTE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,8 +44,8 @@ final class QuoteParser implements InlineParserInterface
|
||||
*/
|
||||
public function parse(InlineParserContext $inlineContext): bool
|
||||
{
|
||||
$char = $inlineContext->getFullMatch();
|
||||
$cursor = $inlineContext->getCursor();
|
||||
$normalizedCharacter = $this->getNormalizedQuoteCharacter($cursor->getCharacter());
|
||||
|
||||
$charBefore = $cursor->peek(-1);
|
||||
if ($charBefore === null) {
|
||||
@@ -47,57 +54,43 @@ final class QuoteParser implements InlineParserInterface
|
||||
|
||||
$cursor->advance();
|
||||
|
||||
$charAfter = $cursor->getCharacter();
|
||||
$charAfter = $cursor->getCurrentCharacter();
|
||||
if ($charAfter === null) {
|
||||
$charAfter = "\n";
|
||||
}
|
||||
|
||||
[$leftFlanking, $rightFlanking] = $this->determineFlanking($charBefore, $charAfter);
|
||||
$canOpen = $leftFlanking && !$rightFlanking;
|
||||
$canClose = $rightFlanking;
|
||||
$canOpen = $leftFlanking && ! $rightFlanking;
|
||||
$canClose = $rightFlanking;
|
||||
|
||||
$node = new Quote($normalizedCharacter, ['delim' => true]);
|
||||
$node = new Quote($char, ['delim' => true]);
|
||||
$inlineContext->getContainer()->appendChild($node);
|
||||
|
||||
// Add entry to stack to this opener
|
||||
$inlineContext->getDelimiterStack()->push(new Delimiter($normalizedCharacter, 1, $node, $canOpen, $canClose));
|
||||
$inlineContext->getDelimiterStack()->push(new Delimiter($char, 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)
|
||||
private function determineFlanking(string $charBefore, string $charAfter): array
|
||||
{
|
||||
$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);
|
||||
$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);
|
||||
$leftFlanking = ! $afterIsWhitespace &&
|
||||
! ($afterIsPunctuation &&
|
||||
! $beforeIsWhitespace &&
|
||||
! $beforeIsPunctuation);
|
||||
|
||||
$rightFlanking = !$beforeIsWhitespace &&
|
||||
!($beforeIsPunctuation &&
|
||||
!$afterIsWhitespace &&
|
||||
!$afterIsPunctuation);
|
||||
$rightFlanking = ! $beforeIsWhitespace &&
|
||||
! ($beforeIsPunctuation &&
|
||||
! $afterIsWhitespace &&
|
||||
! $afterIsPunctuation);
|
||||
|
||||
return [$leftFlanking, $rightFlanking];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -16,24 +18,24 @@ namespace League\CommonMark\Extension\SmartPunct;
|
||||
|
||||
use League\CommonMark\Delimiter\DelimiterInterface;
|
||||
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
|
||||
use League\CommonMark\Inline\Element\AbstractStringContainer;
|
||||
use League\CommonMark\Node\Inline\AbstractStringContainer;
|
||||
|
||||
final class QuoteProcessor implements DelimiterProcessorInterface
|
||||
{
|
||||
/** @var string */
|
||||
private $normalizedCharacter;
|
||||
/** @psalm-readonly */
|
||||
private string $normalizedCharacter;
|
||||
|
||||
/** @var string */
|
||||
private $openerCharacter;
|
||||
/** @psalm-readonly */
|
||||
private string $openerCharacter;
|
||||
|
||||
/** @var string */
|
||||
private $closerCharacter;
|
||||
/** @psalm-readonly */
|
||||
private string $closerCharacter;
|
||||
|
||||
private function __construct(string $char, string $opener, string $closer)
|
||||
{
|
||||
$this->normalizedCharacter = $char;
|
||||
$this->openerCharacter = $opener;
|
||||
$this->closerCharacter = $closer;
|
||||
$this->openerCharacter = $opener;
|
||||
$this->closerCharacter = $closer;
|
||||
}
|
||||
|
||||
public function getOpeningCharacter(): string
|
||||
@@ -56,7 +58,7 @@ final class QuoteProcessor implements DelimiterProcessorInterface
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse)
|
||||
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void
|
||||
{
|
||||
$opener->insertAfter(new Quote($this->openerCharacter));
|
||||
$closer->insertBefore(new Quote($this->closerCharacter));
|
||||
@@ -64,11 +66,6 @@ final class QuoteProcessor implements DelimiterProcessorInterface
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
@@ -77,11 +74,6 @@ final class QuoteProcessor implements DelimiterProcessorInterface
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -14,36 +16,49 @@
|
||||
|
||||
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;
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\CommonMark\Node\Block\Document;
|
||||
use League\CommonMark\Node\Block\Paragraph;
|
||||
use League\CommonMark\Node\Inline\Text;
|
||||
use League\CommonMark\Renderer\Block as CoreBlockRenderer;
|
||||
use League\CommonMark\Renderer\Inline as CoreInlineRenderer;
|
||||
use League\Config\ConfigurationBuilderInterface;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
final class SmartPunctExtension implements ExtensionInterface
|
||||
final class SmartPunctExtension implements ConfigurableExtensionInterface
|
||||
{
|
||||
public function register(ConfigurableEnvironmentInterface $environment)
|
||||
public function configureSchema(ConfigurationBuilderInterface $builder): void
|
||||
{
|
||||
$builder->addSchema('smartpunct', Expect::structure([
|
||||
'double_quote_opener' => Expect::string(Quote::DOUBLE_QUOTE_OPENER),
|
||||
'double_quote_closer' => Expect::string(Quote::DOUBLE_QUOTE_CLOSER),
|
||||
'single_quote_opener' => Expect::string(Quote::SINGLE_QUOTE_OPENER),
|
||||
'single_quote_closer' => Expect::string(Quote::SINGLE_QUOTE_CLOSER),
|
||||
]));
|
||||
}
|
||||
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$environment
|
||||
->addInlineParser(new QuoteParser(), 10)
|
||||
->addInlineParser(new PunctuationParser(), 0)
|
||||
->addInlineParser(new DashParser(), 0)
|
||||
->addInlineParser(new EllipsesParser(), 0)
|
||||
|
||||
->addDelimiterProcessor(QuoteProcessor::createDoubleQuoteProcessor(
|
||||
$environment->getConfig('smartpunct/double_quote_opener', Quote::DOUBLE_QUOTE_OPENER),
|
||||
$environment->getConfig('smartpunct/double_quote_closer', Quote::DOUBLE_QUOTE_CLOSER)
|
||||
$environment->getConfiguration()->get('smartpunct/double_quote_opener'),
|
||||
$environment->getConfiguration()->get('smartpunct/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)
|
||||
$environment->getConfiguration()->get('smartpunct/single_quote_opener'),
|
||||
$environment->getConfiguration()->get('smartpunct/single_quote_closer')
|
||||
))
|
||||
|
||||
->addBlockRenderer(Document::class, new CoreBlockRenderer\DocumentRenderer(), 0)
|
||||
->addBlockRenderer(Paragraph::class, new CoreBlockRenderer\ParagraphRenderer(), 0)
|
||||
->addEventListener(DocumentParsedEvent::class, new ReplaceUnpairedQuotesListener())
|
||||
|
||||
->addInlineRenderer(Quote::class, new QuoteRenderer(), 100)
|
||||
->addInlineRenderer(Text::class, new CoreInlineRenderer\TextRenderer(), 0)
|
||||
;
|
||||
->addRenderer(Document::class, new CoreBlockRenderer\DocumentRenderer(), 0)
|
||||
->addRenderer(Paragraph::class, new CoreBlockRenderer\ParagraphRenderer(), 0)
|
||||
->addRenderer(Text::class, new CoreInlineRenderer\TextRenderer(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,12 +13,27 @@
|
||||
|
||||
namespace League\CommonMark\Extension\Strikethrough;
|
||||
|
||||
use League\CommonMark\Inline\Element\AbstractInline;
|
||||
use League\CommonMark\Node\Inline\AbstractInline;
|
||||
use League\CommonMark\Node\Inline\DelimitedInterface;
|
||||
|
||||
final class Strikethrough extends AbstractInline
|
||||
final class Strikethrough extends AbstractInline implements DelimitedInterface
|
||||
{
|
||||
public function isContainer(): bool
|
||||
private string $delimiter;
|
||||
|
||||
public function __construct(string $delimiter = '~~')
|
||||
{
|
||||
return true;
|
||||
parent::__construct();
|
||||
|
||||
$this->delimiter = $delimiter;
|
||||
}
|
||||
|
||||
public function getOpeningDelimiter(): string
|
||||
{
|
||||
return $this->delimiter;
|
||||
}
|
||||
|
||||
public function getClosingDelimiter(): string
|
||||
{
|
||||
return $this->delimiter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -13,7 +15,7 @@ namespace League\CommonMark\Extension\Strikethrough;
|
||||
|
||||
use League\CommonMark\Delimiter\DelimiterInterface;
|
||||
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
|
||||
use League\CommonMark\Inline\Element\AbstractStringContainer;
|
||||
use League\CommonMark\Node\Inline\AbstractStringContainer;
|
||||
|
||||
final class StrikethroughDelimiterProcessor implements DelimiterProcessorInterface
|
||||
{
|
||||
@@ -29,19 +31,25 @@ final class StrikethroughDelimiterProcessor implements DelimiterProcessorInterfa
|
||||
|
||||
public function getMinLength(): int
|
||||
{
|
||||
return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int
|
||||
{
|
||||
$min = \min($opener->getLength(), $closer->getLength());
|
||||
if ($opener->getLength() > 2 && $closer->getLength() > 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $min >= 2 ? $min : 0;
|
||||
if ($opener->getLength() !== $closer->getLength()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return \min($opener->getLength(), $closer->getLength());
|
||||
}
|
||||
|
||||
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse)
|
||||
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void
|
||||
{
|
||||
$strikethrough = new Strikethrough();
|
||||
$strikethrough = new Strikethrough(\str_repeat('~', $delimiterUse));
|
||||
|
||||
$tmp = $opener->next();
|
||||
while ($tmp !== null && $tmp !== $closer) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,14 +13,14 @@
|
||||
|
||||
namespace League\CommonMark\Extension\Strikethrough;
|
||||
|
||||
use League\CommonMark\ConfigurableEnvironmentInterface;
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Extension\ExtensionInterface;
|
||||
|
||||
final class StrikethroughExtension implements ExtensionInterface
|
||||
{
|
||||
public function register(ConfigurableEnvironmentInterface $environment)
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor());
|
||||
$environment->addInlineRenderer(Strikethrough::class, new StrikethroughRenderer());
|
||||
$environment->addRenderer(Strikethrough::class, new StrikethroughRenderer());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,19 +13,38 @@
|
||||
|
||||
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;
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface;
|
||||
use League\CommonMark\Renderer\NodeRendererInterface;
|
||||
use League\CommonMark\Util\HtmlElement;
|
||||
use League\CommonMark\Xml\XmlNodeRendererInterface;
|
||||
|
||||
final class StrikethroughRenderer implements InlineRendererInterface
|
||||
final class StrikethroughRenderer implements NodeRendererInterface, XmlNodeRendererInterface
|
||||
{
|
||||
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
|
||||
/**
|
||||
* @param Strikethrough $node
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
|
||||
{
|
||||
if (!($inline instanceof Strikethrough)) {
|
||||
throw new \InvalidArgumentException('Incompatible inline type: ' . get_class($inline));
|
||||
}
|
||||
Strikethrough::assertInstanceOf($node);
|
||||
|
||||
return new HtmlElement('del', $inline->getData('attributes', []), $htmlRenderer->renderInlines($inline->children()));
|
||||
return new HtmlElement('del', $node->data->get('attributes'), $childRenderer->renderNodes($node->children()));
|
||||
}
|
||||
|
||||
public function getXmlTagName(Node $node): string
|
||||
{
|
||||
return 'strikethrough';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getXmlAttributes(Node $node): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,55 +15,8 @@ declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
|
||||
final class Table extends AbstractStringContainerBlock implements InlineContainerInterface
|
||||
final class Table extends AbstractBlock
|
||||
{
|
||||
/** @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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,52 +15,85 @@ declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
|
||||
final class TableCell extends AbstractStringContainerBlock implements InlineContainerInterface
|
||||
final class TableCell extends AbstractBlock
|
||||
{
|
||||
const TYPE_HEAD = 'th';
|
||||
const TYPE_BODY = 'td';
|
||||
public const TYPE_HEADER = 'header';
|
||||
public const TYPE_DATA = 'data';
|
||||
|
||||
const ALIGN_LEFT = 'left';
|
||||
const ALIGN_RIGHT = 'right';
|
||||
const ALIGN_CENTER = 'center';
|
||||
public const ALIGN_LEFT = 'left';
|
||||
public const ALIGN_RIGHT = 'right';
|
||||
public const ALIGN_CENTER = 'center';
|
||||
|
||||
/** @var string */
|
||||
public $type = self::TYPE_BODY;
|
||||
/**
|
||||
* @psalm-var self::TYPE_*
|
||||
* @phpstan-var self::TYPE_*
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private string $type = self::TYPE_DATA;
|
||||
|
||||
/** @var string|null */
|
||||
public $align;
|
||||
/**
|
||||
* @psalm-var self::ALIGN_*|null
|
||||
* @phpstan-var self::ALIGN_*|null
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private ?string $align = null;
|
||||
|
||||
public function __construct(string $string = '', string $type = self::TYPE_BODY, string $align = null)
|
||||
/**
|
||||
* @psalm-param self::TYPE_* $type
|
||||
* @psalm-param self::ALIGN_*|null $align
|
||||
*
|
||||
* @phpstan-param self::TYPE_* $type
|
||||
* @phpstan-param self::ALIGN_*|null $align
|
||||
*/
|
||||
public function __construct(string $type = self::TYPE_DATA, ?string $align = null)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->finalStringContents = $string;
|
||||
$this->addLine($string);
|
||||
$this->type = $type;
|
||||
|
||||
$this->type = $type;
|
||||
$this->align = $align;
|
||||
}
|
||||
|
||||
public function canContain(AbstractBlock $block): bool
|
||||
/**
|
||||
* @psalm-return self::TYPE_*
|
||||
*
|
||||
* @phpstan-return self::TYPE_*
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return false;
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function isCode(): bool
|
||||
/**
|
||||
* @psalm-param self::TYPE_* $type
|
||||
*
|
||||
* @phpstan-param self::TYPE_* $type
|
||||
*/
|
||||
public function setType(string $type): void
|
||||
{
|
||||
return false;
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function matchesNextLine(Cursor $cursor): bool
|
||||
/**
|
||||
* @psalm-return self::ALIGN_*|null
|
||||
*
|
||||
* @phpstan-return self::ALIGN_*|null
|
||||
*/
|
||||
public function getAlign(): ?string
|
||||
{
|
||||
return false;
|
||||
return $this->align;
|
||||
}
|
||||
|
||||
public function handleRemainingContents(ContextInterface $context, Cursor $cursor): void
|
||||
/**
|
||||
* @psalm-param self::ALIGN_*|null $align
|
||||
*
|
||||
* @phpstan-param self::ALIGN_*|null $align
|
||||
*/
|
||||
public function setAlign(?string $align): void
|
||||
{
|
||||
$this->align = $align;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,25 +15,75 @@ declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface;
|
||||
use League\CommonMark\Renderer\NodeRendererInterface;
|
||||
use League\CommonMark\Util\HtmlElement;
|
||||
use League\CommonMark\Xml\XmlNodeRendererInterface;
|
||||
|
||||
final class TableCellRenderer implements BlockRendererInterface
|
||||
final class TableCellRenderer implements NodeRendererInterface, XmlNodeRendererInterface
|
||||
{
|
||||
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
|
||||
private const DEFAULT_ATTRIBUTES = [
|
||||
TableCell::ALIGN_LEFT => ['align' => 'left'],
|
||||
TableCell::ALIGN_CENTER => ['align' => 'center'],
|
||||
TableCell::ALIGN_RIGHT => ['align' => 'right'],
|
||||
];
|
||||
|
||||
/** @var array<TableCell::ALIGN_*, array<string, string|string[]|bool>> */
|
||||
private array $alignmentAttributes;
|
||||
|
||||
/**
|
||||
* @param array<TableCell::ALIGN_*, array<string, string|string[]|bool>> $alignmentAttributes
|
||||
*/
|
||||
public function __construct(array $alignmentAttributes = self::DEFAULT_ATTRIBUTES)
|
||||
{
|
||||
if (!$block instanceof TableCell) {
|
||||
throw new \InvalidArgumentException('Incompatible block type: ' . get_class($block));
|
||||
$this->alignmentAttributes = $alignmentAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TableCell $node
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
|
||||
{
|
||||
TableCell::assertInstanceOf($node);
|
||||
|
||||
$attrs = $node->data->get('attributes');
|
||||
if (($alignment = $node->getAlign()) !== null) {
|
||||
$attrs = AttributesHelper::mergeAttributes($attrs, $this->alignmentAttributes[$alignment]);
|
||||
}
|
||||
|
||||
$attrs = $block->getData('attributes', []);
|
||||
$tag = $node->getType() === TableCell::TYPE_HEADER ? 'th' : 'td';
|
||||
|
||||
if ($block->align !== null) {
|
||||
$attrs['align'] = $block->align;
|
||||
return new HtmlElement($tag, $attrs, $childRenderer->renderNodes($node->children()));
|
||||
}
|
||||
|
||||
public function getXmlTagName(Node $node): string
|
||||
{
|
||||
return 'table_cell';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TableCell $node
|
||||
*
|
||||
* @return array<string, scalar>
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function getXmlAttributes(Node $node): array
|
||||
{
|
||||
TableCell::assertInstanceOf($node);
|
||||
|
||||
$ret = ['type' => $node->getType()];
|
||||
|
||||
if (($align = $node->getAlign()) !== null) {
|
||||
$ret['align'] = $align;
|
||||
}
|
||||
|
||||
return new HtmlElement($block->type, $attrs, $htmlRenderer->renderInlines($block->children()));
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,20 +15,48 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Table;
|
||||
|
||||
use League\CommonMark\ConfigurableEnvironmentInterface;
|
||||
use League\CommonMark\Extension\ExtensionInterface;
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\CommonMark\Renderer\HtmlDecorator;
|
||||
use League\Config\ConfigurationBuilderInterface;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
final class TableExtension implements ExtensionInterface
|
||||
final class TableExtension implements ConfigurableExtensionInterface
|
||||
{
|
||||
public function register(ConfigurableEnvironmentInterface $environment): void
|
||||
public function configureSchema(ConfigurationBuilderInterface $builder): void
|
||||
{
|
||||
$environment
|
||||
->addBlockParser(new TableParser())
|
||||
$attributeArraySchema = Expect::arrayOf(
|
||||
Expect::type('string|string[]|bool'), // attribute value(s)
|
||||
'string' // attribute name
|
||||
)->mergeDefaults(false);
|
||||
|
||||
->addBlockRenderer(Table::class, new TableRenderer())
|
||||
->addBlockRenderer(TableSection::class, new TableSectionRenderer())
|
||||
->addBlockRenderer(TableRow::class, new TableRowRenderer())
|
||||
->addBlockRenderer(TableCell::class, new TableCellRenderer())
|
||||
;
|
||||
$builder->addSchema('table', Expect::structure([
|
||||
'wrap' => Expect::structure([
|
||||
'enabled' => Expect::bool()->default(false),
|
||||
'tag' => Expect::string()->default('div'),
|
||||
'attributes' => Expect::arrayOf(Expect::string()),
|
||||
]),
|
||||
'alignment_attributes' => Expect::structure([
|
||||
'left' => (clone $attributeArraySchema)->default(['align' => 'left']),
|
||||
'center' => (clone $attributeArraySchema)->default(['align' => 'center']),
|
||||
'right' => (clone $attributeArraySchema)->default(['align' => 'right']),
|
||||
]),
|
||||
]));
|
||||
}
|
||||
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$tableRenderer = new TableRenderer();
|
||||
if ($environment->getConfiguration()->get('table/wrap/enabled')) {
|
||||
$tableRenderer = new HtmlDecorator($tableRenderer, $environment->getConfiguration()->get('table/wrap/tag'), $environment->getConfiguration()->get('table/wrap/attributes'));
|
||||
}
|
||||
|
||||
$environment
|
||||
->addBlockStartParser(new TableStartParser())
|
||||
|
||||
->addRenderer(Table::class, $tableRenderer)
|
||||
->addRenderer(TableSection::class, new TableSectionRenderer())
|
||||
->addRenderer(TableRow::class, new TableRowRenderer())
|
||||
->addRenderer(TableCell::class, new TableCellRenderer($environment->getConfiguration()->get('table/alignment_attributes')));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,144 +15,158 @@ declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
|
||||
use League\CommonMark\Parser\Block\BlockContinue;
|
||||
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
|
||||
use League\CommonMark\Parser\Block\BlockContinueParserWithInlinesInterface;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
use League\CommonMark\Parser\InlineParserEngineInterface;
|
||||
use League\CommonMark\Util\ArrayCollection;
|
||||
|
||||
final class TableParser implements BlockParserInterface, EnvironmentAwareInterface
|
||||
final class TableParser extends AbstractBlockContinueParser implements BlockContinueParserWithInlinesInterface
|
||||
{
|
||||
/** @psalm-readonly */
|
||||
private Table $block;
|
||||
|
||||
/**
|
||||
* @var EnvironmentInterface
|
||||
* @var ArrayCollection<string>
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private $environment;
|
||||
private ArrayCollection $bodyLines;
|
||||
|
||||
public function parse(ContextInterface $context, Cursor $cursor): bool
|
||||
/**
|
||||
* @var array<int, string|null>
|
||||
* @psalm-var array<int, TableCell::ALIGN_*|null>
|
||||
* @phpstan-var array<int, TableCell::ALIGN_*|null>
|
||||
*
|
||||
* @psalm-readonly
|
||||
*/
|
||||
private array $columns;
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private array $headerCells;
|
||||
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private bool $nextIsSeparatorLine = true;
|
||||
|
||||
/**
|
||||
* @param array<int, string|null> $columns
|
||||
* @param array<int, string> $headerCells
|
||||
*
|
||||
* @psalm-param array<int, TableCell::ALIGN_*|null> $columns
|
||||
*
|
||||
* @phpstan-param array<int, TableCell::ALIGN_*|null> $columns
|
||||
*/
|
||||
public function __construct(array $columns, array $headerCells)
|
||||
{
|
||||
$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);
|
||||
}
|
||||
$this->block = new Table();
|
||||
$this->bodyLines = new ArrayCollection();
|
||||
$this->columns = $columns;
|
||||
$this->headerCells = $headerCells;
|
||||
}
|
||||
|
||||
public function canHaveLazyContinuationLines(): bool
|
||||
{
|
||||
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
|
||||
public function getBlock(): Table
|
||||
{
|
||||
$cells = $this->split(new Cursor(\trim($line)));
|
||||
return $this->block;
|
||||
}
|
||||
|
||||
if (empty($cells)) {
|
||||
return null;
|
||||
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
|
||||
{
|
||||
if (\strpos($cursor->getLine(), '|') === false) {
|
||||
return BlockContinue::none();
|
||||
}
|
||||
|
||||
// The header row must match the delimiter row in the number of cells
|
||||
if ($type === TableCell::TYPE_HEAD && \count($cells) !== \count($columns)) {
|
||||
return null;
|
||||
return BlockContinue::at($cursor);
|
||||
}
|
||||
|
||||
public function addLine(string $line): void
|
||||
{
|
||||
if ($this->nextIsSeparatorLine) {
|
||||
$this->nextIsSeparatorLine = false;
|
||||
} else {
|
||||
$this->bodyLines[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
public function parseInlines(InlineParserEngineInterface $inlineParser): void
|
||||
{
|
||||
$headerColumns = \count($this->headerCells);
|
||||
|
||||
$head = new TableSection(TableSection::TYPE_HEAD);
|
||||
$this->block->appendChild($head);
|
||||
|
||||
$headerRow = new TableRow();
|
||||
$head->appendChild($headerRow);
|
||||
for ($i = 0; $i < $headerColumns; $i++) {
|
||||
$cell = $this->headerCells[$i];
|
||||
$tableCell = $this->parseCell($cell, $i, $inlineParser);
|
||||
$tableCell->setType(TableCell::TYPE_HEADER);
|
||||
$headerRow->appendChild($tableCell);
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
$row = new TableRow();
|
||||
foreach ($cells as $i => $cell) {
|
||||
if (!array_key_exists($i, $columns)) {
|
||||
return $row;
|
||||
$body = null;
|
||||
foreach ($this->bodyLines as $rowLine) {
|
||||
$cells = self::split($rowLine);
|
||||
$row = new TableRow();
|
||||
|
||||
// Body can not have more columns than head
|
||||
for ($i = 0; $i < $headerColumns; $i++) {
|
||||
$cell = $cells[$i] ?? '';
|
||||
$tableCell = $this->parseCell($cell, $i, $inlineParser);
|
||||
$row->appendChild($tableCell);
|
||||
}
|
||||
|
||||
$row->appendChild(new TableCell(trim($cell), $type, $columns[$i]));
|
||||
if ($body === null) {
|
||||
// It's valid to have a table without body. In that case, don't add an empty TableBody node.
|
||||
$body = new TableSection();
|
||||
$this->block->appendChild($body);
|
||||
}
|
||||
|
||||
$body->appendChild($row);
|
||||
}
|
||||
}
|
||||
|
||||
private function parseCell(string $cell, int $column, InlineParserEngineInterface $inlineParser): TableCell
|
||||
{
|
||||
$tableCell = new TableCell();
|
||||
|
||||
if ($column < \count($this->columns)) {
|
||||
$tableCell->setAlign($this->columns[$column]);
|
||||
}
|
||||
|
||||
for ($j = count($columns) - 1; $j > $i; --$j) {
|
||||
$row->appendChild(new TableCell('', $type, null));
|
||||
}
|
||||
$inlineParser->parse(\trim($cell), $tableCell);
|
||||
|
||||
return $row;
|
||||
return $tableCell;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Cursor $cursor
|
||||
* @internal
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function split(Cursor $cursor): array
|
||||
public static function split(string $line): array
|
||||
{
|
||||
if ($cursor->getCharacter() === '|') {
|
||||
$cursor = new Cursor(\trim($line));
|
||||
|
||||
if ($cursor->getCurrentCharacter() === '|') {
|
||||
$cursor->advanceBy(1);
|
||||
}
|
||||
|
||||
$cells = [];
|
||||
$sb = '';
|
||||
$sb = '';
|
||||
|
||||
while (!$cursor->isAtEnd()) {
|
||||
switch ($c = $cursor->getCharacter()) {
|
||||
while (! $cursor->isAtEnd()) {
|
||||
switch ($c = $cursor->getCurrentCharacter()) {
|
||||
case '\\':
|
||||
if ($cursor->peek() === '|') {
|
||||
// Pipe is special for table parsing. An escaped pipe doesn't result in a new cell, but is
|
||||
@@ -164,14 +178,16 @@ final class TableParser implements BlockParserInterface, EnvironmentAwareInterfa
|
||||
// Preserve backslash before other characters or at end of line.
|
||||
$sb .= '\\';
|
||||
}
|
||||
|
||||
break;
|
||||
case '|':
|
||||
$cells[] = $sb;
|
||||
$sb = '';
|
||||
$sb = '';
|
||||
break;
|
||||
default:
|
||||
$sb .= $c;
|
||||
}
|
||||
|
||||
$cursor->advanceBy(1);
|
||||
}
|
||||
|
||||
@@ -181,104 +197,4 @@ final class TableParser implements BlockParserInterface, EnvironmentAwareInterfa
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,25 +15,44 @@ declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface;
|
||||
use League\CommonMark\Renderer\NodeRendererInterface;
|
||||
use League\CommonMark\Util\HtmlElement;
|
||||
use League\CommonMark\Xml\XmlNodeRendererInterface;
|
||||
|
||||
final class TableRenderer implements BlockRendererInterface
|
||||
final class TableRenderer implements NodeRendererInterface, XmlNodeRendererInterface
|
||||
{
|
||||
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
|
||||
/**
|
||||
* @param Table $node
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
|
||||
{
|
||||
if (!$block instanceof Table) {
|
||||
throw new \InvalidArgumentException('Incompatible block type: ' . get_class($block));
|
||||
}
|
||||
Table::assertInstanceOf($node);
|
||||
|
||||
$attrs = $block->getData('attributes', []);
|
||||
$attrs = $node->data->get('attributes');
|
||||
|
||||
$separator = $htmlRenderer->getOption('inner_separator', "\n");
|
||||
$separator = $childRenderer->getInnerSeparator();
|
||||
|
||||
$children = $htmlRenderer->renderBlocks($block->children());
|
||||
$children = $childRenderer->renderNodes($node->children());
|
||||
|
||||
return new HtmlElement('table', $attrs, $separator . \trim($children) . $separator);
|
||||
}
|
||||
|
||||
public function getXmlTagName(Node $node): string
|
||||
{
|
||||
return 'table';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getXmlAttributes(Node $node): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,34 +15,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Table;
|
||||
|
||||
use League\CommonMark\Block\Element\AbstractBlock;
|
||||
use League\CommonMark\Cursor;
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,23 +15,42 @@ declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface;
|
||||
use League\CommonMark\Renderer\NodeRendererInterface;
|
||||
use League\CommonMark\Util\HtmlElement;
|
||||
use League\CommonMark\Xml\XmlNodeRendererInterface;
|
||||
|
||||
final class TableRowRenderer implements BlockRendererInterface
|
||||
final class TableRowRenderer implements NodeRendererInterface, XmlNodeRendererInterface
|
||||
{
|
||||
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
|
||||
/**
|
||||
* @param TableRow $node
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
|
||||
{
|
||||
if (!$block instanceof TableRow) {
|
||||
throw new \InvalidArgumentException('Incompatible block type: ' . get_class($block));
|
||||
}
|
||||
TableRow::assertInstanceOf($node);
|
||||
|
||||
$attrs = $block->getData('attributes', []);
|
||||
$attrs = $node->data->get('attributes');
|
||||
|
||||
$separator = $htmlRenderer->getOption('inner_separator', "\n");
|
||||
$separator = $childRenderer->getInnerSeparator();
|
||||
|
||||
return new HtmlElement('tr', $attrs, $separator . $htmlRenderer->renderBlocks($block->children()) . $separator);
|
||||
return new HtmlElement('tr', $attrs, $separator . $childRenderer->renderNodes($node->children()) . $separator);
|
||||
}
|
||||
|
||||
public function getXmlTagName(Node $node): string
|
||||
{
|
||||
return 'table_row';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getXmlAttributes(Node $node): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,52 +15,50 @@ declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
|
||||
final class TableSection extends AbstractStringContainerBlock implements InlineContainerInterface
|
||||
final class TableSection extends AbstractBlock
|
||||
{
|
||||
const TYPE_HEAD = 'thead';
|
||||
const TYPE_BODY = 'tbody';
|
||||
public const TYPE_HEAD = 'head';
|
||||
public const TYPE_BODY = 'body';
|
||||
|
||||
/** @var string */
|
||||
public $type = self::TYPE_BODY;
|
||||
/**
|
||||
* @psalm-var self::TYPE_*
|
||||
* @phpstan-var self::TYPE_*
|
||||
*
|
||||
* @psalm-readonly
|
||||
*/
|
||||
private string $type;
|
||||
|
||||
/**
|
||||
* @psalm-param self::TYPE_* $type
|
||||
*
|
||||
* @phpstan-param self::TYPE_* $type
|
||||
*/
|
||||
public function __construct(string $type = self::TYPE_BODY)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return self::TYPE_*
|
||||
*
|
||||
* @phpstan-return self::TYPE_*
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function isHead(): bool
|
||||
{
|
||||
return self::TYPE_HEAD === $this->type;
|
||||
return $this->type === self::TYPE_HEAD;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
return $this->type === self::TYPE_BODY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,27 +15,56 @@ declare(strict_types=1);
|
||||
|
||||
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;
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface;
|
||||
use League\CommonMark\Renderer\NodeRendererInterface;
|
||||
use League\CommonMark\Util\HtmlElement;
|
||||
use League\CommonMark\Xml\XmlNodeRendererInterface;
|
||||
|
||||
final class TableSectionRenderer implements BlockRendererInterface
|
||||
final class TableSectionRenderer implements NodeRendererInterface, XmlNodeRendererInterface
|
||||
{
|
||||
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
|
||||
/**
|
||||
* @param TableSection $node
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer)
|
||||
{
|
||||
if (!$block instanceof TableSection) {
|
||||
throw new \InvalidArgumentException('Incompatible block type: ' . get_class($block));
|
||||
}
|
||||
TableSection::assertInstanceOf($node);
|
||||
|
||||
if (!$block->hasChildren()) {
|
||||
if (! $node->hasChildren()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$attrs = $block->getData('attributes', []);
|
||||
$attrs = $node->data->get('attributes');
|
||||
|
||||
$separator = $htmlRenderer->getOption('inner_separator', "\n");
|
||||
$separator = $childRenderer->getInnerSeparator();
|
||||
|
||||
return new HtmlElement($block->type, $attrs, $separator . $htmlRenderer->renderBlocks($block->children()) . $separator);
|
||||
$tag = $node->getType() === TableSection::TYPE_HEAD ? 'thead' : 'tbody';
|
||||
|
||||
return new HtmlElement($tag, $attrs, $separator . $childRenderer->renderNodes($node->children()) . $separator);
|
||||
}
|
||||
|
||||
public function getXmlTagName(Node $node): string
|
||||
{
|
||||
return 'table_section';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TableSection $node
|
||||
*
|
||||
* @return array<string, scalar>
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function getXmlAttributes(Node $node): array
|
||||
{
|
||||
TableSection::assertInstanceOf($node);
|
||||
|
||||
return [
|
||||
'type' => $node->getType(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,11 +13,8 @@
|
||||
|
||||
namespace League\CommonMark\Extension\TableOfContents\Node;
|
||||
|
||||
use League\CommonMark\Block\Element\ListBlock;
|
||||
use League\CommonMark\Extension\TableOfContents\TableOfContents as DeprecatedTableOfContents;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
|
||||
|
||||
final class TableOfContents extends ListBlock
|
||||
{
|
||||
}
|
||||
|
||||
\class_exists(DeprecatedTableOfContents::class);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,23 +13,8 @@
|
||||
|
||||
namespace League\CommonMark\Extension\TableOfContents\Node;
|
||||
|
||||
use League\CommonMark\Block\Element\AbstractBlock;
|
||||
use League\CommonMark\Cursor;
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,18 +13,20 @@
|
||||
|
||||
namespace League\CommonMark\Extension\TableOfContents\Normalizer;
|
||||
|
||||
use League\CommonMark\Block\Element\ListBlock;
|
||||
use League\CommonMark\Block\Element\ListItem;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\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;
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private ListBlock $parentListBlock;
|
||||
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private int $parentLevel = 1;
|
||||
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private ?ListItem $lastListItem = null;
|
||||
|
||||
public function __construct(TableOfContents $toc)
|
||||
{
|
||||
@@ -43,16 +47,17 @@ final class AsIsNormalizerStrategy implements NormalizerStrategyInterface
|
||||
$newListBlock->setEndLine($listItemToAdd->getEndLine());
|
||||
$this->lastListItem->appendChild($newListBlock);
|
||||
$this->parentListBlock = $newListBlock;
|
||||
$this->lastListItem = null;
|
||||
$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) {
|
||||
$search = $this->parentListBlock;
|
||||
while ($search = $search->parent()) {
|
||||
if ($search instanceof ListBlock) {
|
||||
$this->parentListBlock = $search;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -65,6 +70,3 @@ final class AsIsNormalizerStrategy implements NormalizerStrategyInterface
|
||||
$this->lastListItem = $listItemToAdd;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger autoload without causing a deprecated error
|
||||
\class_exists(TableOfContents::class);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,13 +13,13 @@
|
||||
|
||||
namespace League\CommonMark\Extension\TableOfContents\Normalizer;
|
||||
|
||||
use League\CommonMark\Block\Element\ListItem;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
|
||||
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
|
||||
|
||||
final class FlatNormalizerStrategy implements NormalizerStrategyInterface
|
||||
{
|
||||
/** @var TableOfContents */
|
||||
private $toc;
|
||||
/** @psalm-readonly */
|
||||
private TableOfContents $toc;
|
||||
|
||||
public function __construct(TableOfContents $toc)
|
||||
{
|
||||
@@ -29,6 +31,3 @@ final class FlatNormalizerStrategy implements NormalizerStrategyInterface
|
||||
$this->toc->appendChild($listItemToAdd);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger autoload without causing a deprecated error
|
||||
\class_exists(TableOfContents::class);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,7 +13,7 @@
|
||||
|
||||
namespace League\CommonMark\Extension\TableOfContents\Normalizer;
|
||||
|
||||
use League\CommonMark\Block\Element\ListItem;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
|
||||
|
||||
interface NormalizerStrategyInterface
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,17 +13,21 @@
|
||||
|
||||
namespace League\CommonMark\Extension\TableOfContents\Normalizer;
|
||||
|
||||
use League\CommonMark\Block\Element\ListBlock;
|
||||
use League\CommonMark\Block\Element\ListItem;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
|
||||
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
|
||||
|
||||
final class RelativeNormalizerStrategy implements NormalizerStrategyInterface
|
||||
{
|
||||
/** @var TableOfContents */
|
||||
private $toc;
|
||||
/** @psalm-readonly */
|
||||
private TableOfContents $toc;
|
||||
|
||||
/** @var array<int, ListItem> */
|
||||
private $listItemStack = [];
|
||||
/**
|
||||
* @var array<int, ListItem>
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private array $listItemStack = [];
|
||||
|
||||
public function __construct(TableOfContents $toc)
|
||||
{
|
||||
@@ -30,18 +36,15 @@ final class RelativeNormalizerStrategy implements NormalizerStrategyInterface
|
||||
|
||||
public function addItem(int $level, ListItem $listItemToAdd): void
|
||||
{
|
||||
\end($this->listItemStack);
|
||||
$previousLevel = \key($this->listItemStack);
|
||||
$previousLevel = \array_key_last($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);
|
||||
\array_pop($this->listItemStack);
|
||||
$previousLevel = \array_key_last($this->listItemStack);
|
||||
}
|
||||
|
||||
/** @var ListItem|false $lastListItem */
|
||||
$lastListItem = \current($this->listItemStack);
|
||||
$lastListItem = \end($this->listItemStack);
|
||||
|
||||
// Need to go one level deeper? Add that level
|
||||
if ($lastListItem !== false && $level > $previousLevel) {
|
||||
@@ -62,6 +65,3 @@ final class RelativeNormalizerStrategy implements NormalizerStrategyInterface
|
||||
$this->listItemStack[$level] = $listItemToAdd;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger autoload without causing a deprecated error
|
||||
\class_exists(TableOfContents::class);
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<?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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,59 +13,36 @@
|
||||
|
||||
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\CommonMark\Node\Block\Heading;
|
||||
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;
|
||||
use League\CommonMark\Node\Block\Document;
|
||||
use League\CommonMark\Node\NodeIterator;
|
||||
use League\Config\ConfigurationAwareInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
use League\Config\Exception\InvalidConfigurationException;
|
||||
|
||||
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_TOP = 'top';
|
||||
public const POSITION_BEFORE_HEADINGS = 'before-headings';
|
||||
public const POSITION_PLACEHOLDER = 'placeholder';
|
||||
public const POSITION_PLACEHOLDER = 'placeholder';
|
||||
|
||||
/** @var ConfigurationInterface */
|
||||
private $config;
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private ConfigurationInterface $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)
|
||||
(string) $this->config->get('table_of_contents/style'),
|
||||
(string) $this->config->get('table_of_contents/normalize'),
|
||||
(int) $this->config->get('table_of_contents/min_heading_level'),
|
||||
(int) $this->config->get('table_of_contents/max_heading_level'),
|
||||
(string) $this->config->get('heading_permalink/fragment_prefix'),
|
||||
);
|
||||
|
||||
$toc = $generator->generate($document);
|
||||
@@ -73,13 +52,13 @@ final class TableOfContentsBuilder implements ConfigurationAwareInterface
|
||||
}
|
||||
|
||||
// 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;
|
||||
$class = $this->config->get('table_of_contents/html_class');
|
||||
if ($class !== null) {
|
||||
$toc->data->append('attributes/class', $class);
|
||||
}
|
||||
|
||||
// Add the TOC to the Document
|
||||
$position = $this->config->get('table_of_contents/position', self::POSITION_TOP);
|
||||
$position = $this->config->get('table_of_contents/position');
|
||||
if ($position === self::POSITION_TOP) {
|
||||
$document->prependChild($toc);
|
||||
} elseif ($position === self::POSITION_BEFORE_HEADINGS) {
|
||||
@@ -87,41 +66,41 @@ final class TableOfContentsBuilder implements ConfigurationAwareInterface
|
||||
} elseif ($position === self::POSITION_PLACEHOLDER) {
|
||||
$this->replacePlaceholders($document, $toc);
|
||||
} else {
|
||||
throw new InvalidOptionException(\sprintf('Invalid config option "%s" for "table_of_contents/position"', $position));
|
||||
throw InvalidConfigurationException::forConfigOption('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);
|
||||
foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) {
|
||||
if (! $node instanceof Heading) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return;
|
||||
foreach ($node->children() as $child) {
|
||||
if ($child instanceof HeadingPermalink) {
|
||||
$node->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) {
|
||||
foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) {
|
||||
// Add the block once we find a placeholder
|
||||
if (! $node instanceof TableOfContentsPlaceholder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($event->isEntering()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$event->getNode()->replaceWith(clone $toc);
|
||||
$node->replaceWith(clone $toc);
|
||||
}
|
||||
}
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $config)
|
||||
public function setConfiguration(ConfigurationInterface $configuration): void
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->config = $configuration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,21 +13,41 @@
|
||||
|
||||
namespace League\CommonMark\Extension\TableOfContents;
|
||||
|
||||
use League\CommonMark\ConfigurableEnvironmentInterface;
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Extension\ExtensionInterface;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
|
||||
use League\CommonMark\Extension\CommonMark\Renderer\Block\ListBlockRenderer;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
|
||||
use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder;
|
||||
use League\Config\ConfigurationBuilderInterface;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
final class TableOfContentsExtension implements ExtensionInterface
|
||||
final class TableOfContentsExtension implements ConfigurableExtensionInterface
|
||||
{
|
||||
public function register(ConfigurableEnvironmentInterface $environment): void
|
||||
public function configureSchema(ConfigurationBuilderInterface $builder): void
|
||||
{
|
||||
$builder->addSchema('table_of_contents', Expect::structure([
|
||||
'position' => Expect::anyOf(TableOfContentsBuilder::POSITION_BEFORE_HEADINGS, TableOfContentsBuilder::POSITION_PLACEHOLDER, TableOfContentsBuilder::POSITION_TOP)->default(TableOfContentsBuilder::POSITION_TOP),
|
||||
'style' => Expect::anyOf(ListBlock::TYPE_BULLET, ListBlock::TYPE_ORDERED)->default(ListBlock::TYPE_BULLET),
|
||||
'normalize' => Expect::anyOf(TableOfContentsGenerator::NORMALIZE_RELATIVE, TableOfContentsGenerator::NORMALIZE_FLAT, TableOfContentsGenerator::NORMALIZE_DISABLED)->default(TableOfContentsGenerator::NORMALIZE_RELATIVE),
|
||||
'min_heading_level' => Expect::int()->min(1)->max(6)->default(1),
|
||||
'max_heading_level' => Expect::int()->min(1)->max(6)->default(6),
|
||||
'html_class' => Expect::string()->default('table-of-contents'),
|
||||
'placeholder' => Expect::anyOf(Expect::string(), Expect::null())->default(null),
|
||||
]));
|
||||
}
|
||||
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$environment->addRenderer(TableOfContents::class, new TableOfContentsRenderer(new ListBlockRenderer()));
|
||||
$environment->addEventListener(DocumentParsedEvent::class, [new TableOfContentsBuilder(), 'onDocumentParsed'], -150);
|
||||
|
||||
if ($environment->getConfig('table_of_contents/position') === TableOfContentsBuilder::POSITION_PLACEHOLDER) {
|
||||
$environment->addBlockParser(new TableOfContentsPlaceholderParser(), 200);
|
||||
// phpcs:ignore SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed
|
||||
if ($environment->getConfiguration()->get('table_of_contents/position') === TableOfContentsBuilder::POSITION_PLACEHOLDER) {
|
||||
$environment->addBlockStartParser(TableOfContentsPlaceholderParser::blockStartParser(), 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());
|
||||
$environment->addRenderer(TableOfContentsPlaceholder::class, new TableOfContentsPlaceholderRenderer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,46 +13,58 @@
|
||||
|
||||
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\CommonMark\Node\Block\Heading;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListData;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
|
||||
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;
|
||||
use League\CommonMark\Node\Block\Document;
|
||||
use League\CommonMark\Node\NodeIterator;
|
||||
use League\CommonMark\Node\RawMarkupContainerInterface;
|
||||
use League\CommonMark\Node\StringContainerHelper;
|
||||
use League\Config\Exception\InvalidConfigurationException;
|
||||
|
||||
final class TableOfContentsGenerator implements TableOfContentsGeneratorInterface
|
||||
{
|
||||
public const STYLE_BULLET = ListBlock::TYPE_BULLET;
|
||||
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';
|
||||
public const NORMALIZE_FLAT = 'flat';
|
||||
|
||||
/** @var string */
|
||||
private $style;
|
||||
/** @var string */
|
||||
private $normalizationStrategy;
|
||||
/** @var int */
|
||||
private $minHeadingLevel;
|
||||
/** @var int */
|
||||
private $maxHeadingLevel;
|
||||
/** @psalm-readonly */
|
||||
private string $style;
|
||||
|
||||
public function __construct(string $style, string $normalizationStrategy, int $minHeadingLevel, int $maxHeadingLevel)
|
||||
/** @psalm-readonly */
|
||||
private string $normalizationStrategy;
|
||||
|
||||
/** @psalm-readonly */
|
||||
private int $minHeadingLevel;
|
||||
|
||||
/** @psalm-readonly */
|
||||
private int $maxHeadingLevel;
|
||||
|
||||
/** @psalm-readonly */
|
||||
private string $fragmentPrefix;
|
||||
|
||||
public function __construct(string $style, string $normalizationStrategy, int $minHeadingLevel, int $maxHeadingLevel, string $fragmentPrefix)
|
||||
{
|
||||
$this->style = $style;
|
||||
$this->style = $style;
|
||||
$this->normalizationStrategy = $normalizationStrategy;
|
||||
$this->minHeadingLevel = $minHeadingLevel;
|
||||
$this->maxHeadingLevel = $maxHeadingLevel;
|
||||
$this->minHeadingLevel = $minHeadingLevel;
|
||||
$this->maxHeadingLevel = $maxHeadingLevel;
|
||||
$this->fragmentPrefix = $fragmentPrefix;
|
||||
|
||||
if ($fragmentPrefix !== '') {
|
||||
$this->fragmentPrefix .= '-';
|
||||
}
|
||||
}
|
||||
|
||||
public function generate(Document $document): ?TableOfContents
|
||||
@@ -64,7 +78,7 @@ final class TableOfContentsGenerator implements TableOfContentsGeneratorInterfac
|
||||
foreach ($this->getHeadingLinks($document) as $headingLink) {
|
||||
$heading = $headingLink->parent();
|
||||
// Make sure this is actually tied to a heading
|
||||
if (!$heading instanceof Heading) {
|
||||
if (! $heading instanceof Heading) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -74,30 +88,26 @@ final class TableOfContentsGenerator implements TableOfContentsGeneratorInterfac
|
||||
}
|
||||
|
||||
// Keep track of the first heading we see - we might need this later
|
||||
$firstHeading = $firstHeading ?? $heading;
|
||||
$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);
|
||||
$link = new Link('#' . $this->fragmentPrefix . $headingLink->getSlug(), StringContainerHelper::getChildText($heading, [RawMarkupContainerInterface::class]));
|
||||
|
||||
$listItem = new ListItem($toc->getListData());
|
||||
$listItem->setStartLine($heading->getStartLine());
|
||||
$listItem->setEndLine($heading->getEndLine());
|
||||
$listItem->appendChild($paragraph);
|
||||
$listItem->appendChild($link);
|
||||
|
||||
// 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) {
|
||||
if (! $toc->hasChildren() || $firstHeading === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -113,7 +123,7 @@ final class TableOfContentsGenerator implements TableOfContentsGeneratorInterfac
|
||||
} 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));
|
||||
throw new InvalidConfigurationException(\sprintf('Invalid table of contents list style: "%s"', $this->style));
|
||||
}
|
||||
|
||||
$toc = new TableOfContents($listData);
|
||||
@@ -125,16 +135,19 @@ final class TableOfContentsGenerator implements TableOfContentsGeneratorInterfac
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Document $document
|
||||
*
|
||||
* @return iterable<HeadingPermalink>
|
||||
*/
|
||||
private function getHeadingLinks(Document $document)
|
||||
private function getHeadingLinks(Document $document): iterable
|
||||
{
|
||||
$walker = $document->walker();
|
||||
while ($event = $walker->next()) {
|
||||
if ($event->isEntering() && ($node = $event->getNode()) instanceof HeadingPermalink) {
|
||||
yield $node;
|
||||
foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) {
|
||||
if (! $node instanceof Heading) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($node->children() as $child) {
|
||||
if ($child instanceof HeadingPermalink) {
|
||||
yield $child;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,24 +162,7 @@ final class TableOfContentsGenerator implements TableOfContentsGeneratorInterfac
|
||||
case self::NORMALIZE_FLAT:
|
||||
return new FlatNormalizerStrategy($toc);
|
||||
default:
|
||||
throw new InvalidOptionException(\sprintf('Invalid table of contents normalization strategy "%s"', $this->normalizationStrategy));
|
||||
throw new InvalidConfigurationException(\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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,13 +13,10 @@
|
||||
|
||||
namespace League\CommonMark\Extension\TableOfContents;
|
||||
|
||||
use League\CommonMark\Block\Element\Document;
|
||||
use League\CommonMark\Extension\TableOfContents\Node\TableOfContents;
|
||||
use League\CommonMark\Node\Block\Document;
|
||||
|
||||
interface TableOfContentsGeneratorInterface
|
||||
{
|
||||
public function generate(Document $document): ?TableOfContents;
|
||||
}
|
||||
|
||||
// Trigger autoload without causing a deprecated error
|
||||
\class_exists(TableOfContents::class);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,37 +13,62 @@
|
||||
|
||||
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;
|
||||
use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
|
||||
use League\CommonMark\Parser\Block\BlockContinue;
|
||||
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
|
||||
use League\CommonMark\Parser\Block\BlockStart;
|
||||
use League\CommonMark\Parser\Block\BlockStartParserInterface;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
use League\CommonMark\Parser\MarkdownParserStateInterface;
|
||||
use League\Config\ConfigurationAwareInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
|
||||
final class TableOfContentsPlaceholderParser implements BlockParserInterface, ConfigurationAwareInterface
|
||||
final class TableOfContentsPlaceholderParser extends AbstractBlockContinueParser
|
||||
{
|
||||
/** @var ConfigurationInterface */
|
||||
private $config;
|
||||
/** @psalm-readonly */
|
||||
private TableOfContentsPlaceholder $block;
|
||||
|
||||
public function parse(ContextInterface $context, Cursor $cursor): bool
|
||||
public function __construct()
|
||||
{
|
||||
$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;
|
||||
$this->block = new TableOfContentsPlaceholder();
|
||||
}
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $configuration)
|
||||
public function getBlock(): TableOfContentsPlaceholder
|
||||
{
|
||||
$this->config = $configuration;
|
||||
return $this->block;
|
||||
}
|
||||
|
||||
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
|
||||
{
|
||||
return BlockContinue::none();
|
||||
}
|
||||
|
||||
public static function blockStartParser(): BlockStartParserInterface
|
||||
{
|
||||
return new class () implements BlockStartParserInterface, ConfigurationAwareInterface {
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private ConfigurationInterface $config;
|
||||
|
||||
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
|
||||
{
|
||||
$placeholder = $this->config->get('table_of_contents/placeholder');
|
||||
if ($placeholder === null) {
|
||||
return BlockStart::none();
|
||||
}
|
||||
|
||||
// The placeholder must be the only thing on the line
|
||||
if ($cursor->match('/^' . \preg_quote($placeholder, '/') . '$/') === null) {
|
||||
return BlockStart::none();
|
||||
}
|
||||
|
||||
return BlockStart::of(new TableOfContentsPlaceholderParser())->at($cursor);
|
||||
}
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $configuration): void
|
||||
{
|
||||
$this->config = $configuration;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,14 +13,28 @@
|
||||
|
||||
namespace League\CommonMark\Extension\TableOfContents;
|
||||
|
||||
use League\CommonMark\Block\Element\AbstractBlock;
|
||||
use League\CommonMark\Block\Renderer\BlockRendererInterface;
|
||||
use League\CommonMark\ElementRendererInterface;
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface;
|
||||
use League\CommonMark\Renderer\NodeRendererInterface;
|
||||
use League\CommonMark\Xml\XmlNodeRendererInterface;
|
||||
|
||||
final class TableOfContentsPlaceholderRenderer implements BlockRendererInterface
|
||||
final class TableOfContentsPlaceholderRenderer implements NodeRendererInterface, XmlNodeRendererInterface
|
||||
{
|
||||
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer): string
|
||||
{
|
||||
return '<!-- table of contents -->';
|
||||
}
|
||||
|
||||
public function getXmlTagName(Node $node): string
|
||||
{
|
||||
return 'table_of_contents_placeholder';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, scalar>
|
||||
*/
|
||||
public function getXmlAttributes(Node $node): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,14 +13,14 @@
|
||||
|
||||
namespace League\CommonMark\Extension\TaskList;
|
||||
|
||||
use League\CommonMark\ConfigurableEnvironmentInterface;
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Extension\ExtensionInterface;
|
||||
|
||||
final class TaskListExtension implements ExtensionInterface
|
||||
{
|
||||
public function register(ConfigurableEnvironmentInterface $environment)
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$environment->addInlineParser(new TaskListItemMarkerParser(), 35);
|
||||
$environment->addInlineRenderer(TaskListItemMarker::class, new TaskListItemMarkerRenderer());
|
||||
$environment->addRenderer(TaskListItemMarker::class, new TaskListItemMarkerRenderer());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,15 +13,17 @@
|
||||
|
||||
namespace League\CommonMark\Extension\TaskList;
|
||||
|
||||
use League\CommonMark\Inline\Element\AbstractInline;
|
||||
use League\CommonMark\Node\Inline\AbstractInline;
|
||||
|
||||
final class TaskListItemMarker extends AbstractInline
|
||||
{
|
||||
/** @var bool */
|
||||
protected $checked = false;
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private bool $checked;
|
||||
|
||||
public function __construct(bool $isCompleted)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->checked = $isCompleted;
|
||||
}
|
||||
|
||||
@@ -28,10 +32,8 @@ final class TaskListItemMarker extends AbstractInline
|
||||
return $this->checked;
|
||||
}
|
||||
|
||||
public function setChecked(bool $checked): self
|
||||
public function setChecked(bool $checked): void
|
||||
{
|
||||
$this->checked = $checked;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,16 +13,17 @@
|
||||
|
||||
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;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
|
||||
use League\CommonMark\Node\Block\Paragraph;
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserMatch;
|
||||
use League\CommonMark\Parser\InlineParserContext;
|
||||
|
||||
final class TaskListItemMarkerParser implements InlineParserInterface
|
||||
{
|
||||
public function getCharacters(): array
|
||||
public function getMatchDefinition(): InlineParserMatch
|
||||
{
|
||||
return ['['];
|
||||
return InlineParserMatch::oneOf('[ ]', '[x]');
|
||||
}
|
||||
|
||||
public function parse(InlineParserContext $inlineContext): bool
|
||||
@@ -28,17 +31,14 @@ final class TaskListItemMarkerParser implements InlineParserInterface
|
||||
$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)) {
|
||||
if ($container->hasChildren() || ! ($container instanceof Paragraph && $container->parent() && $container->parent() instanceof ListItem)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cursor = $inlineContext->getCursor();
|
||||
$cursor = $inlineContext->getCursor();
|
||||
$oldState = $cursor->saveState();
|
||||
|
||||
$m = $cursor->match('/\[[ xX]\]/');
|
||||
if ($m === null) {
|
||||
return false;
|
||||
}
|
||||
$cursor->advanceBy(3);
|
||||
|
||||
if ($cursor->getNextNonSpaceCharacter() === null) {
|
||||
$cursor->restoreState($oldState);
|
||||
@@ -46,7 +46,7 @@ final class TaskListItemMarkerParser implements InlineParserInterface
|
||||
return false;
|
||||
}
|
||||
|
||||
$isChecked = $m !== '[ ]';
|
||||
$isChecked = $inlineContext->getFullMatch() !== '[ ]';
|
||||
|
||||
$container->appendChild(new TaskListItemMarker($isChecked));
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
@@ -11,28 +13,29 @@
|
||||
|
||||
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;
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface;
|
||||
use League\CommonMark\Renderer\NodeRendererInterface;
|
||||
use League\CommonMark\Util\HtmlElement;
|
||||
use League\CommonMark\Xml\XmlNodeRendererInterface;
|
||||
|
||||
final class TaskListItemMarkerRenderer implements InlineRendererInterface
|
||||
final class TaskListItemMarkerRenderer implements NodeRendererInterface, XmlNodeRendererInterface
|
||||
{
|
||||
/**
|
||||
* @param TaskListItemMarker $inline
|
||||
* @param ElementRendererInterface $htmlRenderer
|
||||
* @param TaskListItemMarker $node
|
||||
*
|
||||
* @return HtmlElement|string|null
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
|
||||
{
|
||||
if (!($inline instanceof TaskListItemMarker)) {
|
||||
throw new \InvalidArgumentException('Incompatible inline type: ' . \get_class($inline));
|
||||
}
|
||||
TaskListItemMarker::assertInstanceOf($node);
|
||||
|
||||
$checkbox = new HtmlElement('input', [], '', true);
|
||||
$attrs = $node->data->get('attributes');
|
||||
$checkbox = new HtmlElement('input', $attrs, '', true);
|
||||
|
||||
if ($inline->isChecked()) {
|
||||
if ($node->isChecked()) {
|
||||
$checkbox->setAttribute('checked', '');
|
||||
}
|
||||
|
||||
@@ -41,4 +44,27 @@ final class TaskListItemMarkerRenderer implements InlineRendererInterface
|
||||
|
||||
return $checkbox;
|
||||
}
|
||||
|
||||
public function getXmlTagName(Node $node): string
|
||||
{
|
||||
return 'task_list_item_marker';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TaskListItemMarker $node
|
||||
*
|
||||
* @return array<string, scalar>
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function getXmlAttributes(Node $node): array
|
||||
{
|
||||
TaskListItemMarker::assertInstanceOf($node);
|
||||
|
||||
if ($node->isChecked()) {
|
||||
return ['checked' => 'checked'];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user