This commit is contained in:
Paolo A
2024-08-13 13:44:16 +00:00
parent 1bbb23088d
commit e796d76612
4001 changed files with 30101 additions and 40075 deletions

View File

@@ -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']);
}

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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 === '') {

View File

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