Upgrade framework

This commit is contained in:
2023-11-14 16:54:35 +01:00
parent 1648a5cd42
commit 4fcf6fffcc
10548 changed files with 693138 additions and 466698 deletions

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\CommonMark\Extension\Attributes;
use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\Attributes\Event\AttributesListener;
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(EnvironmentBuilderInterface $environment): void
{
$environment->addBlockStartParser(new AttributesBlockStartParser());
$environment->addInlineParser(new AttributesInlineParser());
$environment->addEventListener(DocumentParsedEvent::class, [new AttributesListener(), 'processDocument']);
}
}

View File

@@ -0,0 +1,139 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\CommonMark\Extension\Attributes\Event;
use League\CommonMark\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\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
{
private const DIRECTION_PREFIX = 'prefix';
private const DIRECTION_SUFFIX = 'suffix';
public function processDocument(DocumentParsedEvent $event): void
{
foreach ($event->getDocument()->iterator() as $node) {
if (! ($node instanceof Attributes || $node instanceof AttributesInline)) {
continue;
}
[$target, $direction] = self::findTargetAndDirection($node);
if ($target instanceof Node) {
$parent = $target->parent();
if ($parent instanceof ListItem && $parent->parent() instanceof ListBlock && $parent->parent()->isTight()) {
$target = $parent;
}
if ($direction === self::DIRECTION_SUFFIX) {
$attributes = AttributesHelper::mergeAttributes($target, $node->getAttributes());
} else {
$attributes = AttributesHelper::mergeAttributes($node->getAttributes(), $target);
}
$target->data->set('attributes', $attributes);
}
$node->detach();
}
}
/**
* @param Attributes|AttributesInline $node
*
* @return array<Node|string|null>
*/
private static function findTargetAndDirection($node): array
{
$target = null;
$direction = null;
$previous = $next = $node;
while (true) {
$previous = self::getPrevious($previous);
$next = self::getNext($next);
if ($previous === null && $next === null) {
if (! $node->parent() instanceof FencedCode) {
$target = $node->parent();
$direction = self::DIRECTION_SUFFIX;
}
break;
}
if ($node instanceof AttributesInline && ($previous === null || ($previous instanceof AbstractInline && $node->isBlock()))) {
continue;
}
if ($previous !== null && ! self::isAttributesNode($previous)) {
$target = $previous;
$direction = self::DIRECTION_SUFFIX;
break;
}
if ($next !== null && ! self::isAttributesNode($next)) {
$target = $next;
$direction = self::DIRECTION_PREFIX;
break;
}
}
return [$target, $direction];
}
/**
* Get any previous block (sibling or parent) this might apply to
*/
private static function getPrevious(?Node $node = null): ?Node
{
if ($node instanceof Attributes) {
if ($node->getTarget() === Attributes::TARGET_NEXT) {
return null;
}
if ($node->getTarget() === Attributes::TARGET_PARENT) {
return $node->parent();
}
}
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
{
if ($node instanceof Attributes && $node->getTarget() !== Attributes::TARGET_NEXT) {
return null;
}
return $node instanceof Node ? $node->next() : null;
}
private static function isAttributesNode(Node $node): bool
{
return $node instanceof Attributes || $node instanceof AttributesInline;
}
}

View File

@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\CommonMark\Extension\Attributes\Node;
use League\CommonMark\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 array $attributes;
private int $target = self::TARGET_NEXT;
/**
* @param array<string, mixed> $attributes
*/
public function __construct(array $attributes)
{
parent::__construct();
$this->attributes = $attributes;
}
/**
* @return array<string, mixed>
*/
public function getAttributes(): array
{
return $this->attributes;
}
/**
* @param array<string, mixed> $attributes
*/
public function setAttributes(array $attributes): void
{
$this->attributes = $attributes;
}
public function getTarget(): int
{
return $this->target;
}
public function setTarget(int $target): void
{
$this->target = $target;
}
}

View File

@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\CommonMark\Extension\Attributes\Node;
use League\CommonMark\Node\Inline\AbstractInline;
final class AttributesInline extends AbstractInline
{
/** @var array<string, mixed> */
private array $attributes;
private bool $block;
/**
* @param array<string, mixed> $attributes
*/
public function __construct(array $attributes, bool $block)
{
parent::__construct();
$this->attributes = $attributes;
$this->block = $block;
}
/**
* @return array<string, mixed>
*/
public function getAttributes(): array
{
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

@@ -0,0 +1,92 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\CommonMark\Extension\Attributes\Parser;
use League\CommonMark\Extension\Attributes\Node\Attributes;
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
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;
final class AttributesBlockContinueParser extends AbstractBlockContinueParser
{
private Attributes $block;
private AbstractBlock $container;
private bool $hasSubsequentLine = false;
/**
* @param array<string, mixed> $attributes The attributes identified by the block start parser
* @param AbstractBlock $container The node we were in when these attributes were discovered
*/
public function __construct(array $attributes, AbstractBlock $container)
{
$this->block = new Attributes($attributes);
$this->container = $container;
}
public function getBlock(): AbstractBlock
{
return $this->block;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
$this->hasSubsequentLine = true;
$cursor->advanceToNextNonSpaceOrTab();
// Does this next line also have attributes?
$attributes = AttributesHelper::parseAttributes($cursor);
$cursor->advanceToNextNonSpaceOrTab();
if ($cursor->isAtEnd() && $attributes !== []) {
// It does! Merge them into what we parsed previously
$this->block->setAttributes(AttributesHelper::mergeAttributes(
$this->block->getAttributes(),
$attributes
));
// Tell the core parser we've consumed everything
return BlockContinue::at($cursor);
}
// Okay, so there are no attributes on the next line
// If this next line is blank we know we can't target the next node, it must be a previous one
if ($cursor->isBlank()) {
$this->block->setTarget(Attributes::TARGET_PREVIOUS);
}
return BlockContinue::none();
}
public function closeBlock(): void
{
// Attributes appearing at the very end of the document won't have any last lines to check
// so we can make that determination here
if (! $this->hasSubsequentLine) {
$this->block->setTarget(Attributes::TARGET_PREVIOUS);
}
// We know this block must apply to the "previous" block, but that could be a sibling or parent,
// so we check the containing block to see which one it might be.
if ($this->block->getTarget() === Attributes::TARGET_PREVIOUS && $this->block->parent() === $this->container) {
$this->block->setTarget(Attributes::TARGET_PARENT);
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\CommonMark\Extension\Attributes\Parser;
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
final class AttributesBlockStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
$originalPosition = $cursor->getPosition();
$attributes = AttributesHelper::parseAttributes($cursor);
if ($attributes === [] && $originalPosition === $cursor->getPosition()) {
return BlockStart::none();
}
if ($cursor->getNextNonSpaceCharacter() !== null) {
return BlockStart::none();
}
return BlockStart::of(new AttributesBlockContinueParser($attributes, $parserState->getActiveBlockParser()->getBlock()))->at($cursor);
}
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\CommonMark\Extension\Attributes\Parser;
use League\CommonMark\Extension\Attributes\Node\AttributesInline;
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
use League\CommonMark\Node\StringContainerInterface;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
final class AttributesInlineParser implements InlineParserInterface
{
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::string('{');
}
public function parse(InlineParserContext $inlineContext): bool
{
$cursor = $inlineContext->getCursor();
$char = (string) $cursor->peek(-1);
$attributes = AttributesHelper::parseAttributes($cursor);
if ($attributes === []) {
return false;
}
if ($char === ' ' && ($prev = $inlineContext->getContainer()->lastChild()) instanceof StringContainerInterface) {
$prev->setLiteral(\rtrim($prev->getLiteral(), ' '));
}
if ($char === '') {
$cursor->advanceToNextNonSpaceOrNewline();
}
$node = new AttributesInline($attributes, $char === ' ' || $char === '');
$inlineContext->getContainer()->appendChild($node);
return true;
}
}

View File

@@ -0,0 +1,136 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\CommonMark\Extension\Attributes\Util;
use League\CommonMark\Node\Node;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Util\RegexHelper;
/**
* @internal
*/
final class AttributesHelper
{
private const SINGLE_ATTRIBUTE = '\s*([.#][_a-z0-9-]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . ')\s*';
private const ATTRIBUTE_LIST = '/^{:?(' . self::SINGLE_ATTRIBUTE . ')+}/i';
/**
* @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 [];
}
// 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 = [];
while ($attribute = \trim((string) $attributeCursor->match('/^' . self::SINGLE_ATTRIBUTE . '/i'))) {
if ($attribute[0] === '#') {
$attributes['id'] = \substr($attribute, 1);
continue;
}
if ($attribute[0] === '.') {
$attributes['class'][] = \substr($attribute, 1);
continue;
}
[$name, $value] = \explode('=', $attribute, 2);
$first = $value[0];
$last = \substr($value, -1);
if (($first === '"' && $last === '"') || ($first === "'" && $last === "'") && \strlen($value) > 1) {
$value = \substr($value, 1, -1);
}
if (\strtolower(\trim($name)) === 'class') {
foreach (\array_filter(\explode(' ', \trim($value))) as $class) {
$attributes['class'][] = $class;
}
} else {
$attributes[\trim($name)] = \trim($value);
}
}
if (isset($attributes['class'])) {
$attributes['class'] = \implode(' ', (array) $attributes['class']);
}
return $attributes;
}
/**
* @param Node|array<string, mixed> $attributes1
* @param Node|array<string, mixed> $attributes2
*
* @return array<string, mixed>
*/
public static function mergeAttributes($attributes1, $attributes2): array
{
$attributes = [];
foreach ([$attributes1, $attributes2] as $arg) {
if ($arg instanceof Node) {
$arg = $arg->data->get('attributes');
}
/** @var array<string, mixed> $arg */
$arg = (array) $arg;
if (isset($arg['class'])) {
if (\is_string($arg['class'])) {
$arg['class'] = \array_filter(\explode(' ', \trim($arg['class'])));
}
foreach ($arg['class'] as $class) {
$attributes['class'][] = $class;
}
unset($arg['class']);
}
$attributes = \array_merge($attributes, $arg);
}
if (isset($attributes['class'])) {
$attributes['class'] = \implode(' ', $attributes['class']);
}
return $attributes;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* 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\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Extension\ExtensionInterface;
final class AutolinkExtension implements ExtensionInterface
{
public function register(EnvironmentBuilderInterface $environment): void
{
$environment->addInlineParser(new EmailAutolinkParser());
$environment->addInlineParser(new UrlAutolinkParser());
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Inline\Link;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
final class EmailAutolinkParser implements InlineParserInterface
{
private const REGEX = '[A-Za-z0-9.\-_+]+@[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_.]+';
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::regex(self::REGEX);
}
public function parse(InlineParserContext $inlineContext): bool
{
$email = $inlineContext->getFullMatch();
// The last character cannot be - or _
if (\in_array(\substr($email, -1), ['-', '_'], true)) {
return false;
}
// Does the URL end with punctuation that should be stripped?
if (\substr($email, -1) === '.') {
$email = \substr($email, 0, -1);
}
$inlineContext->getCursor()->advanceBy(\strlen($email));
$inlineContext->getContainer()->appendChild(new Link('mailto:' . $email, $email));
return true;
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Inline\Link;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
final class UrlAutolinkParser implements InlineParserInterface
{
private const ALLOWED_AFTER = [null, ' ', "\t", "\n", "\x0b", "\x0c", "\x0d", '*', '_', '~', '('];
// RegEx adapted from https://github.com/symfony/symfony/blob/4.2/src/Symfony/Component/Validator/Constraints/UrlValidator.php
private const REGEX = '~
(
# 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[]
*
* @psalm-readonly
*/
private array $prefixes = ['www'];
/** @psalm-readonly */
private string $finalRegex;
/**
* @param array<int, string> $allowedProtocols
*/
public function __construct(array $allowedProtocols = ['http', 'https', 'ftp'])
{
$this->finalRegex = \sprintf(self::REGEX, \implode('|', $allowedProtocols));
foreach ($allowedProtocols as $protocol) {
$this->prefixes[] = $protocol . '://';
}
}
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::oneOf(...$this->prefixes);
}
public function parse(InlineParserContext $inlineContext): bool
{
$cursor = $inlineContext->getCursor();
// Autolinks can only come at the beginning of a line, after whitespace, or certain delimiting characters
$previousChar = $cursor->peek(-1);
if (! \in_array($previousChar, self::ALLOWED_AFTER, true)) {
return false;
}
// Check if we have a valid URL
if (! \preg_match($this->finalRegex, $cursor->getRemainder(), $matches)) {
return false;
}
$url = $matches[0];
// Does the URL end with punctuation that should be stripped?
if (\preg_match('/(.+?)([?!.,:*_~]+)$/', $url, $matches)) {
// Add the punctuation later
$url = $matches[1];
}
// Does the URL end with something that looks like an entity reference?
if (\preg_match('/(.+)(&[A-Za-z0-9]+;)$/', $url, $matches)) {
$url = $matches[1];
}
// Does the URL need unmatched parens chopped off?
if (\substr($url, -1) === ')' && ($diff = self::diffParens($url)) > 0) {
$url = \substr($url, 0, -$diff);
}
$cursor->advanceBy(\mb_strlen($url, 'UTF-8'));
// Auto-prefix 'http://' onto 'www' URLs
if (\substr($url, 0, 4) === 'www.') {
$inlineContext->getContainer()->appendChild(new Link('http://' . $url, $url));
return true;
}
$inlineContext->getContainer()->appendChild(new Link($url, $url));
return true;
}
/**
* @psalm-pure
*/
private static function diffParens(string $content): int
{
// Scan the entire autolink for the total number of parentheses.
// If there is a greater number of closing parentheses than opening ones,
// we dont consider ANY of the last characters as part of the autolink,
// in order to facilitate including an autolink inside a parenthesis.
\preg_match_all('/[()]/', $content, $matches);
$charCount = ['(' => 0, ')' => 0];
foreach ($matches[0] as $char) {
$charCount[$char]++;
}
return $charCount[')'] - $charCount['('];
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark;
use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Extension\CommonMark\Delimiter\Processor\EmphasisDelimiterProcessor;
use League\CommonMark\Extension\ConfigurableExtensionInterface;
use League\CommonMark\Node as CoreNode;
use League\CommonMark\Parser as CoreParser;
use League\CommonMark\Renderer as CoreRenderer;
use League\Config\ConfigurationBuilderInterface;
use Nette\Schema\Expect;
final class CommonMarkCoreExtension implements ConfigurableExtensionInterface
{
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),
'unordered_list_markers' => Expect::listOf('string')->min(1)->default(['*', '+', '-'])->mergeDefaults(false),
]));
}
// phpcs:disable Generic.Functions.FunctionCallArgumentSpacing.TooMuchSpaceAfterComma,Squiz.WhiteSpace.SemicolonSpacing.Incorrect
public function register(EnvironmentBuilderInterface $environment): void
{
$environment
->addBlockStartParser(new Parser\Block\BlockQuoteStartParser(), 70)
->addBlockStartParser(new Parser\Block\HeadingStartParser(), 60)
->addBlockStartParser(new Parser\Block\FencedCodeStartParser(), 50)
->addBlockStartParser(new Parser\Block\HtmlBlockStartParser(), 40)
->addBlockStartParser(new Parser\Block\ThematicBreakStartParser(), 20)
->addBlockStartParser(new Parser\Block\ListBlockStartParser(), 10)
->addBlockStartParser(new Parser\Block\IndentedCodeStartParser(), -100)
->addInlineParser(new CoreParser\Inline\NewlineParser(), 200)
->addInlineParser(new Parser\Inline\BacktickParser(), 150)
->addInlineParser(new Parser\Inline\EscapableParser(), 80)
->addInlineParser(new Parser\Inline\EntityParser(), 70)
->addInlineParser(new Parser\Inline\AutolinkParser(), 50)
->addInlineParser(new Parser\Inline\HtmlInlineParser(), 40)
->addInlineParser(new Parser\Inline\CloseBracketParser(), 30)
->addInlineParser(new Parser\Inline\OpenBracketParser(), 20)
->addInlineParser(new Parser\Inline\BangParser(), 10)
->addRenderer(Node\Block\BlockQuote::class, new Renderer\Block\BlockQuoteRenderer(), 0)
->addRenderer(CoreNode\Block\Document::class, new CoreRenderer\Block\DocumentRenderer(), 0)
->addRenderer(Node\Block\FencedCode::class, new Renderer\Block\FencedCodeRenderer(), 0)
->addRenderer(Node\Block\Heading::class, new Renderer\Block\HeadingRenderer(), 0)
->addRenderer(Node\Block\HtmlBlock::class, new Renderer\Block\HtmlBlockRenderer(), 0)
->addRenderer(Node\Block\IndentedCode::class, new Renderer\Block\IndentedCodeRenderer(), 0)
->addRenderer(Node\Block\ListBlock::class, new Renderer\Block\ListBlockRenderer(), 0)
->addRenderer(Node\Block\ListItem::class, new Renderer\Block\ListItemRenderer(), 0)
->addRenderer(CoreNode\Block\Paragraph::class, new CoreRenderer\Block\ParagraphRenderer(), 0)
->addRenderer(Node\Block\ThematicBreak::class, new Renderer\Block\ThematicBreakRenderer(), 0)
->addRenderer(Node\Inline\Code::class, new Renderer\Inline\CodeRenderer(), 0)
->addRenderer(Node\Inline\Emphasis::class, new Renderer\Inline\EmphasisRenderer(), 0)
->addRenderer(Node\Inline\HtmlInline::class, new Renderer\Inline\HtmlInlineRenderer(), 0)
->addRenderer(Node\Inline\Image::class, new Renderer\Inline\ImageRenderer(), 0)
->addRenderer(Node\Inline\Link::class, new Renderer\Inline\LinkRenderer(), 0)
->addRenderer(CoreNode\Inline\Newline::class, new CoreRenderer\Inline\NewlineRenderer(), 0)
->addRenderer(Node\Inline\Strong::class, new Renderer\Inline\StrongRenderer(), 0)
->addRenderer(CoreNode\Inline\Text::class, new CoreRenderer\Inline\TextRenderer(), 0)
;
if ($environment->getConfiguration()->get('commonmark/use_asterisk')) {
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('*'));
}
if ($environment->getConfiguration()->get('commonmark/use_underscore')) {
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('_'));
}
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
/*
* 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
*
* Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
* - (c) Atlassian Pty Ltd
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\CommonMark\Delimiter\Processor;
use League\CommonMark\Delimiter\DelimiterInterface;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
use League\CommonMark\Extension\CommonMark\Node\Inline\Emphasis;
use League\CommonMark\Extension\CommonMark\Node\Inline\Strong;
use League\CommonMark\Node\Inline\AbstractStringContainer;
use League\Config\ConfigurationAwareInterface;
use League\Config\ConfigurationInterface;
final class EmphasisDelimiterProcessor implements DelimiterProcessorInterface, ConfigurationAwareInterface
{
/** @psalm-readonly */
private string $char;
/** @psalm-readonly-allow-private-mutation */
private ConfigurationInterface $config;
/**
* @param string $char The emphasis character to use (typically '*' or '_')
*/
public function __construct(string $char)
{
$this->char = $char;
}
public function getOpeningCharacter(): string
{
return $this->char;
}
public function getClosingCharacter(): string
{
return $this->char;
}
public function getMinLength(): int
{
return 1;
}
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int
{
// "Multiple of 3" rule for internal delimiter runs
if (($opener->canClose() || $closer->canOpen()) && $closer->getOriginalLength() % 3 !== 0 && ($opener->getOriginalLength() + $closer->getOriginalLength()) % 3 === 0) {
return 0;
}
// Calculate actual number of delimiters used from this closer
if ($opener->getLength() >= 2 && $closer->getLength() >= 2) {
if ($this->config->get('commonmark/enable_strong')) {
return 2;
}
return 0;
}
if ($this->config->get('commonmark/enable_em')) {
return 1;
}
return 0;
}
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void
{
if ($delimiterUse === 1) {
$emphasis = new Emphasis($this->char);
} elseif ($delimiterUse === 2) {
$emphasis = new Strong($this->char . $this->char);
} else {
return;
}
$next = $opener->next();
while ($next !== null && $next !== $closer) {
$tmp = $next->next();
$emphasis->appendChild($next);
$next = $tmp;
}
$opener->insertAfter($emphasis);
}
public function setConfiguration(ConfigurationInterface $configuration): void
{
$this->config = $configuration;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Block;
use League\CommonMark\Node\Block\AbstractBlock;
class BlockQuote extends AbstractBlock
{
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Block;
use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Node\StringContainerInterface;
final class FencedCode extends AbstractBlock implements StringContainerInterface
{
private ?string $info = null;
private string $literal = '';
private int $length;
private string $char;
private int $offset;
public function __construct(int $length, string $char, int $offset)
{
parent::__construct();
$this->length = $length;
$this->char = $char;
$this->offset = $offset;
}
public function getInfo(): ?string
{
return $this->info;
}
/**
* @return string[]
*/
public function getInfoWords(): array
{
return \preg_split('/\s+/', $this->info ?? '') ?: [];
}
public function setInfo(string $info): void
{
$this->info = $info;
}
public function getLiteral(): string
{
return $this->literal;
}
public function setLiteral(string $literal): void
{
$this->literal = $literal;
}
public function getChar(): string
{
return $this->char;
}
public function setChar(string $char): void
{
$this->char = $char;
}
public function getLength(): int
{
return $this->length;
}
public function setLength(int $length): void
{
$this->length = $length;
}
public function getOffset(): int
{
return $this->offset;
}
public function setOffset(int $offset): void
{
$this->offset = $offset;
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Block;
use League\CommonMark\Node\Block\AbstractBlock;
final class Heading extends AbstractBlock
{
private int $level;
public function __construct(int $level)
{
parent::__construct();
$this->level = $level;
}
public function getLevel(): int
{
return $this->level;
}
public function setLevel(int $level): void
{
$this->level = $level;
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Block;
use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Node\RawMarkupContainerInterface;
final class HtmlBlock extends AbstractBlock implements RawMarkupContainerInterface
{
// Any changes to these constants should be reflected in .phpstorm.meta.php
public const TYPE_1_CODE_CONTAINER = 1;
public const TYPE_2_COMMENT = 2;
public const TYPE_3 = 3;
public const TYPE_4 = 4;
public const TYPE_5_CDATA = 5;
public const TYPE_6_BLOCK_ELEMENT = 6;
public const TYPE_7_MISC_ELEMENT = 7;
/**
* @psalm-var self::TYPE_* $type
* @phpstan-var self::TYPE_* $type
*/
private int $type;
private string $literal = '';
/**
* @psalm-param self::TYPE_* $type
*
* @phpstan-param self::TYPE_* $type
*/
public function __construct(int $type)
{
parent::__construct();
$this->type = $type;
}
/**
* @psalm-return self::TYPE_*
*
* @phpstan-return self::TYPE_*
*/
public function getType(): int
{
return $this->type;
}
/**
* @psalm-param self::TYPE_* $type
*
* @phpstan-param self::TYPE_* $type
*/
public function setType(int $type): void
{
$this->type = $type;
}
public function getLiteral(): string
{
return $this->literal;
}
public function setLiteral(string $literal): void
{
$this->literal = $literal;
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Block;
use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Node\StringContainerInterface;
final class IndentedCode extends AbstractBlock implements StringContainerInterface
{
private string $literal = '';
public function getLiteral(): string
{
return $this->literal;
}
public function setLiteral(string $literal): void
{
$this->literal = $literal;
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Block;
use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Node\Block\TightBlockInterface;
class ListBlock extends AbstractBlock implements TightBlockInterface
{
public const TYPE_BULLET = 'bullet';
public const TYPE_ORDERED = 'ordered';
public const DELIM_PERIOD = 'period';
public const DELIM_PAREN = 'paren';
protected bool $tight = false;
/** @psalm-readonly */
protected ListData $listData;
public function __construct(ListData $listData)
{
parent::__construct();
$this->listData = $listData;
}
public function getListData(): ListData
{
return $this->listData;
}
public function isTight(): bool
{
return $this->tight;
}
public function setTight(bool $tight): void
{
$this->tight = $tight;
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Block;
class ListData
{
public ?int $start = null;
public int $padding = 0;
/**
* @psalm-var ListBlock::TYPE_*
* @phpstan-var ListBlock::TYPE_*
*/
public string $type;
/**
* @psalm-var ListBlock::DELIM_*|null
* @phpstan-var ListBlock::DELIM_*|null
*/
public ?string $delimiter = null;
public ?string $bulletChar = null;
public int $markerOffset;
public function equals(ListData $data): bool
{
return $this->type === $data->type &&
$this->delimiter === $data->delimiter &&
$this->bulletChar === $data->bulletChar;
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Block;
use League\CommonMark\Node\Block\AbstractBlock;
class ListItem extends AbstractBlock
{
/** @psalm-readonly */
protected ListData $listData;
public function __construct(ListData $listData)
{
parent::__construct();
$this->listData = $listData;
}
public function getListData(): ListData
{
return $this->listData;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Block;
use League\CommonMark\Node\Block\AbstractBlock;
class ThematicBreak extends AbstractBlock
{
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Inline;
use League\CommonMark\Node\Inline\AbstractInline;
abstract class AbstractWebResource extends AbstractInline
{
protected string $url;
public function __construct(string $url)
{
parent::__construct();
$this->url = $url;
}
public function getUrl(): string
{
return $this->url;
}
public function setUrl(string $url): void
{
$this->url = $url;
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Inline;
use League\CommonMark\Node\Inline\AbstractStringContainer;
class Code extends AbstractStringContainer
{
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Inline;
use League\CommonMark\Node\Inline\AbstractInline;
use League\CommonMark\Node\Inline\DelimitedInterface;
final class Emphasis extends AbstractInline implements DelimitedInterface
{
private string $delimiter;
public function __construct(string $delimiter = '_')
{
parent::__construct();
$this->delimiter = $delimiter;
}
public function getOpeningDelimiter(): string
{
return $this->delimiter;
}
public function getClosingDelimiter(): string
{
return $this->delimiter;
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Inline;
use League\CommonMark\Node\Inline\AbstractStringContainer;
use League\CommonMark\Node\RawMarkupContainerInterface;
final class HtmlInline extends AbstractStringContainer implements RawMarkupContainerInterface
{
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Inline;
use League\CommonMark\Node\Inline\Text;
class Image extends AbstractWebResource
{
protected ?string $title = null;
public function __construct(string $url, ?string $label = null, ?string $title = null)
{
parent::__construct($url);
if ($label !== null && $label !== '') {
$this->appendChild(new Text($label));
}
$this->title = $title;
}
public function getTitle(): ?string
{
if ($this->title === '') {
return null;
}
return $this->title;
}
public function setTitle(?string $title): void
{
$this->title = $title;
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Inline;
use League\CommonMark\Node\Inline\Text;
class Link extends AbstractWebResource
{
protected ?string $title = null;
public function __construct(string $url, ?string $label = null, ?string $title = null)
{
parent::__construct($url);
if ($label !== null && $label !== '') {
$this->appendChild(new Text($label));
}
$this->title = $title;
}
public function getTitle(): ?string
{
if ($this->title === '') {
return null;
}
return $this->title;
}
public function setTitle(?string $title): void
{
$this->title = $title;
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Node\Inline;
use League\CommonMark\Node\Inline\AbstractInline;
use League\CommonMark\Node\Inline\DelimitedInterface;
final class Strong extends AbstractInline implements DelimitedInterface
{
private string $delimiter;
public function __construct(string $delimiter = '**')
{
parent::__construct();
$this->delimiter = $delimiter;
}
public function getOpeningDelimiter(): string
{
return $this->delimiter;
}
public function getClosingDelimiter(): string
{
return $this->delimiter;
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\BlockQuote;
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;
final class BlockQuoteParser extends AbstractBlockContinueParser
{
/** @psalm-readonly */
private BlockQuote $block;
public function __construct()
{
$this->block = new BlockQuote();
}
public function getBlock(): BlockQuote
{
return $this->block;
}
public function isContainer(): bool
{
return true;
}
public function canContain(AbstractBlock $childBlock): bool
{
return true;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
if (! $cursor->isIndented() && $cursor->getNextNonSpaceCharacter() === '>') {
$cursor->advanceToNextNonSpaceOrTab();
$cursor->advanceBy(1);
$cursor->advanceBySpaceOrTab();
return BlockContinue::at($cursor);
}
return BlockContinue::none();
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Block;
use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
final class BlockQuoteStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if ($cursor->isIndented()) {
return BlockStart::none();
}
if ($cursor->getNextNonSpaceCharacter() !== '>') {
return BlockStart::none();
}
$cursor->advanceToNextNonSpaceOrTab();
$cursor->advanceBy(1);
$cursor->advanceBySpaceOrTab();
return BlockStart::of(new BlockQuoteParser())->at($cursor);
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
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\Util\ArrayCollection;
use League\CommonMark\Util\RegexHelper;
final class FencedCodeParser extends AbstractBlockContinueParser
{
/** @psalm-readonly */
private FencedCode $block;
/** @var ArrayCollection<string> */
private ArrayCollection $strings;
public function __construct(int $fenceLength, string $fenceChar, int $fenceOffset)
{
$this->block = new FencedCode($fenceLength, $fenceChar, $fenceOffset);
$this->strings = new ArrayCollection();
}
public function getBlock(): FencedCode
{
return $this->block;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
// Check for closing code fence
if (! $cursor->isIndented() && $cursor->getNextNonSpaceCharacter() === $this->block->getChar()) {
$match = RegexHelper::matchFirst('/^(?:`{3,}|~{3,})(?= *$)/', $cursor->getLine(), $cursor->getNextNonSpacePosition());
if ($match !== null && \strlen($match[0]) >= $this->block->getLength()) {
// closing fence - we're at end of line, so we can finalize now
return BlockContinue::finished();
}
}
// Skip optional spaces of fence offset
// Optimization: don't attempt to match if we're at a non-space position
if ($cursor->getNextNonSpacePosition() > $cursor->getPosition()) {
$cursor->match('/^ {0,' . $this->block->getOffset() . '}/');
}
return BlockContinue::at($cursor);
}
public function addLine(string $line): void
{
$this->strings[] = $line;
}
public function closeBlock(): void
{
// first line becomes info string
$firstLine = $this->strings->first();
if ($firstLine === false) {
$firstLine = '';
}
$this->block->setInfo(RegexHelper::unescape(\trim($firstLine)));
if ($this->strings->count() === 1) {
$this->block->setLiteral('');
} else {
$this->block->setLiteral(\implode("\n", $this->strings->slice(1)) . "\n");
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Block;
use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
final class FencedCodeStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if ($cursor->isIndented() || ! \in_array($cursor->getNextNonSpaceCharacter(), ['`', '~'], true)) {
return BlockStart::none();
}
$indent = $cursor->getIndent();
$fence = $cursor->match('/^[ \t]*(?:`{3,}(?!.*`)|~{3,})/');
if ($fence === null) {
return BlockStart::none();
}
// fenced code block
$fence = \ltrim($fence, " \t");
return BlockStart::of(new FencedCodeParser(\strlen($fence), $fence[0], $indent))->at($cursor);
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
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;
final class HeadingParser extends AbstractBlockContinueParser implements BlockContinueParserWithInlinesInterface
{
/** @psalm-readonly */
private Heading $block;
private string $content;
public function __construct(int $level, string $content)
{
$this->block = new Heading($level);
$this->content = $content;
}
public function getBlock(): Heading
{
return $this->block;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
return BlockContinue::none();
}
public function parseInlines(InlineParserEngineInterface $inlineParser): void
{
$inlineParser->parse($this->content, $this->block);
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Block;
use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
use League\CommonMark\Util\RegexHelper;
class HeadingStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if ($cursor->isIndented() || ! \in_array($cursor->getNextNonSpaceCharacter(), ['#', '-', '='], true)) {
return BlockStart::none();
}
$cursor->advanceToNextNonSpaceOrTab();
if ($atxHeading = self::getAtxHeader($cursor)) {
return BlockStart::of($atxHeading)->at($cursor);
}
$setextHeadingLevel = self::getSetextHeadingLevel($cursor);
if ($setextHeadingLevel > 0) {
$content = $parserState->getParagraphContent();
if ($content !== null) {
$cursor->advanceToEnd();
return BlockStart::of(new HeadingParser($setextHeadingLevel, $content))
->at($cursor)
->replaceActiveBlockParser();
}
}
return BlockStart::none();
}
private static function getAtxHeader(Cursor $cursor): ?HeadingParser
{
$match = RegexHelper::matchFirst('/^#{1,6}(?:[ \t]+|$)/', $cursor->getRemainder());
if (! $match) {
return null;
}
$cursor->advanceToNextNonSpaceOrTab();
$cursor->advanceBy(\strlen($match[0]));
$level = \strlen(\trim($match[0]));
$str = $cursor->getRemainder();
$str = \preg_replace('/^[ \t]*#+[ \t]*$/', '', $str);
\assert(\is_string($str));
$str = \preg_replace('/[ \t]+#+[ \t]*$/', '', $str);
\assert(\is_string($str));
return new HeadingParser($level, $str);
}
private static function getSetextHeadingLevel(Cursor $cursor): int
{
$match = RegexHelper::matchFirst('/^(?:=+|-+)[ \t]*$/', $cursor->getRemainder());
if ($match === null) {
return 0;
}
return $match[0][0] === '=' ? 1 : 2;
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock;
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\Util\RegexHelper;
final class HtmlBlockParser extends AbstractBlockContinueParser
{
/** @psalm-readonly */
private HtmlBlock $block;
private string $content = '';
private bool $finished = false;
/**
* @psalm-param HtmlBlock::TYPE_* $blockType
*
* @phpstan-param HtmlBlock::TYPE_* $blockType
*/
public function __construct(int $blockType)
{
$this->block = new HtmlBlock($blockType);
}
public function getBlock(): HtmlBlock
{
return $this->block;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
if ($this->finished) {
return BlockContinue::none();
}
if ($cursor->isBlank() && \in_array($this->block->getType(), [HtmlBlock::TYPE_6_BLOCK_ELEMENT, HtmlBlock::TYPE_7_MISC_ELEMENT], true)) {
return BlockContinue::none();
}
return BlockContinue::at($cursor);
}
public function addLine(string $line): void
{
if ($this->content !== '') {
$this->content .= "\n";
}
$this->content .= $line;
// Check for end condition
// phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed
if ($this->block->getType() <= HtmlBlock::TYPE_5_CDATA) {
if (\preg_match(RegexHelper::getHtmlBlockCloseRegex($this->block->getType()), $line) === 1) {
$this->finished = true;
}
}
}
public function closeBlock(): void
{
$this->block->setLiteral($this->content);
$this->content = '';
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock;
use League\CommonMark\Node\Block\Paragraph;
use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
use League\CommonMark\Util\RegexHelper;
final class HtmlBlockStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if ($cursor->isIndented() || $cursor->getNextNonSpaceCharacter() !== '<') {
return BlockStart::none();
}
$tmpCursor = clone $cursor;
$tmpCursor->advanceToNextNonSpaceOrTab();
$line = $tmpCursor->getRemainder();
for ($blockType = 1; $blockType <= 7; $blockType++) {
/** @psalm-var HtmlBlock::TYPE_* $blockType */
/** @phpstan-var HtmlBlock::TYPE_* $blockType */
$match = RegexHelper::matchAt(
RegexHelper::getHtmlBlockOpenRegex($blockType),
$line
);
if ($match !== null && ($blockType < 7 || $this->isType7BlockAllowed($cursor, $parserState))) {
return BlockStart::of(new HtmlBlockParser($blockType))->at($cursor);
}
}
return BlockStart::none();
}
private function isType7BlockAllowed(Cursor $cursor, MarkdownParserStateInterface $parserState): bool
{
// Type 7 blocks can't interrupt paragraphs
if ($parserState->getLastMatchedBlockParser()->getBlock() instanceof Paragraph) {
return false;
}
// Even lazy ones
return ! $parserState->getActiveBlockParser()->canHaveLazyContinuationLines();
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\IndentedCode;
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\Util\ArrayCollection;
final class IndentedCodeParser extends AbstractBlockContinueParser
{
/** @psalm-readonly */
private IndentedCode $block;
/** @var ArrayCollection<string> */
private ArrayCollection $strings;
public function __construct()
{
$this->block = new IndentedCode();
$this->strings = new ArrayCollection();
}
public function getBlock(): IndentedCode
{
return $this->block;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
if ($cursor->isIndented()) {
$cursor->advanceBy(Cursor::INDENT_LEVEL, true);
return BlockContinue::at($cursor);
}
if ($cursor->isBlank()) {
$cursor->advanceToNextNonSpaceOrTab();
return BlockContinue::at($cursor);
}
return BlockContinue::none();
}
public function addLine(string $line): void
{
$this->strings[] = $line;
}
public function closeBlock(): void
{
$reversed = \array_reverse($this->strings->toArray(), true);
foreach ($reversed as $index => $line) {
if ($line !== '' && $line !== "\n" && ! \preg_match('/^(\n *)$/', $line)) {
break;
}
unset($reversed[$index]);
}
$fixed = \array_reverse($reversed);
$tmp = \implode("\n", $fixed);
if (\substr($tmp, -1) !== "\n") {
$tmp .= "\n";
}
$this->block->setLiteral($tmp);
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Block;
use League\CommonMark\Node\Block\Paragraph;
use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
final class IndentedCodeStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if (! $cursor->isIndented()) {
return BlockStart::none();
}
if ($parserState->getActiveBlockParser()->getBlock() instanceof Paragraph) {
return BlockStart::none();
}
if ($cursor->isBlank()) {
return BlockStart::none();
}
$cursor->advanceBy(Cursor::INDENT_LEVEL, true);
return BlockStart::of(new IndentedCodeParser())->at($cursor);
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Block;
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\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;
final class ListBlockParser extends AbstractBlockContinueParser
{
/** @psalm-readonly */
private ListBlock $block;
private bool $hadBlankLine = false;
private int $linesAfterBlank = 0;
public function __construct(ListData $listData)
{
$this->block = new ListBlock($listData);
}
public function getBlock(): ListBlock
{
return $this->block;
}
public function isContainer(): bool
{
return true;
}
public function canContain(AbstractBlock $childBlock): bool
{
if (! $childBlock instanceof ListItem) {
return false;
}
// Another list item is being added to this list block.
// If the previous line was blank, that means this list
// block is "loose" (not tight).
if ($this->hadBlankLine && $this->linesAfterBlank === 1) {
$this->block->setTight(false);
$this->hadBlankLine = false;
}
return true;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
if ($cursor->isBlank()) {
$this->hadBlankLine = true;
$this->linesAfterBlank = 0;
} elseif ($this->hadBlankLine) {
$this->linesAfterBlank++;
}
// List blocks themselves don't have any markers, only list items. So try to stay in the list.
// If there is a block start other than list item, canContain makes sure that this list is closed.
return BlockContinue::at($cursor);
}
}

View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
use League\CommonMark\Extension\CommonMark\Node\Block\ListData;
use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
use League\CommonMark\Util\RegexHelper;
use League\Config\ConfigurationAwareInterface;
use League\Config\ConfigurationInterface;
final class ListBlockStartParser implements BlockStartParserInterface, ConfigurationAwareInterface
{
/** @psalm-readonly-allow-private-mutation */
private ?ConfigurationInterface $config = null;
/** @psalm-readonly-allow-private-mutation */
private ?string $listMarkerRegex = null;
public function setConfiguration(ConfigurationInterface $configuration): void
{
$this->config = $configuration;
}
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if ($cursor->isIndented()) {
return BlockStart::none();
}
$listData = $this->parseList($cursor, $parserState->getParagraphContent() !== null);
if ($listData === null) {
return BlockStart::none();
}
$listItemParser = new ListItemParser($listData);
// prepend the list block if needed
$matched = $parserState->getLastMatchedBlockParser();
if (! ($matched instanceof ListBlockParser) || ! $listData->equals($matched->getBlock()->getListData())) {
$listBlockParser = new ListBlockParser($listData);
// We start out with assuming a list is tight. If we find a blank line, we set it to loose later.
$listBlockParser->getBlock()->setTight(true);
return BlockStart::of($listBlockParser, $listItemParser)->at($cursor);
}
return BlockStart::of($listItemParser)->at($cursor);
}
private function parseList(Cursor $cursor, bool $inParagraph): ?ListData
{
$indent = $cursor->getIndent();
$tmpCursor = clone $cursor;
$tmpCursor->advanceToNextNonSpaceOrTab();
$rest = $tmpCursor->getRemainder();
if (\preg_match($this->listMarkerRegex ?? $this->generateListMarkerRegex(), $rest) === 1) {
$data = new ListData();
$data->markerOffset = $indent;
$data->type = ListBlock::TYPE_BULLET;
$data->delimiter = null;
$data->bulletChar = $rest[0];
$markerLength = 1;
} elseif (($matches = RegexHelper::matchFirst('/^(\d{1,9})([.)])/', $rest)) && (! $inParagraph || $matches[1] === '1')) {
$data = new ListData();
$data->markerOffset = $indent;
$data->type = ListBlock::TYPE_ORDERED;
$data->start = (int) $matches[1];
$data->delimiter = $matches[2] === '.' ? ListBlock::DELIM_PERIOD : ListBlock::DELIM_PAREN;
$data->bulletChar = null;
$markerLength = \strlen($matches[0]);
} else {
return null;
}
// Make sure we have spaces after
$nextChar = $tmpCursor->peek($markerLength);
if (! ($nextChar === null || $nextChar === "\t" || $nextChar === ' ')) {
return null;
}
// If it interrupts paragraph, make sure first line isn't blank
if ($inParagraph && ! RegexHelper::matchAt(RegexHelper::REGEX_NON_SPACE, $rest, $markerLength)) {
return null;
}
$cursor->advanceToNextNonSpaceOrTab(); // to start of marker
$cursor->advanceBy($markerLength, true); // to end of marker
$data->padding = self::calculateListMarkerPadding($cursor, $markerLength);
return $data;
}
private static function calculateListMarkerPadding(Cursor $cursor, int $markerLength): int
{
$start = $cursor->saveState();
$spacesStartCol = $cursor->getColumn();
while ($cursor->getColumn() - $spacesStartCol < 5) {
if (! $cursor->advanceBySpaceOrTab()) {
break;
}
}
$blankItem = $cursor->peek() === null;
$spacesAfterMarker = $cursor->getColumn() - $spacesStartCol;
if ($spacesAfterMarker >= 5 || $spacesAfterMarker < 1 || $blankItem) {
$cursor->restoreState($start);
$cursor->advanceBySpaceOrTab();
return $markerLength + 1;
}
return $markerLength + $spacesAfterMarker;
}
private function generateListMarkerRegex(): string
{
// No configuration given - use the defaults
if ($this->config === null) {
return $this->listMarkerRegex = '/^[*+-]/';
}
$markers = $this->config->get('commonmark/unordered_list_markers');
\assert(\is_array($markers));
return $this->listMarkerRegex = '/^[' . \preg_quote(\implode('', $markers), '/') . ']/';
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Block;
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\Node\Block\AbstractBlock;
use League\CommonMark\Node\Block\Paragraph;
use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
use League\CommonMark\Parser\Block\BlockContinue;
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
use League\CommonMark\Parser\Cursor;
final class ListItemParser extends AbstractBlockContinueParser
{
/** @psalm-readonly */
private ListItem $block;
private bool $hadBlankLine = false;
public function __construct(ListData $listData)
{
$this->block = new ListItem($listData);
}
public function getBlock(): ListItem
{
return $this->block;
}
public function isContainer(): bool
{
return true;
}
public function canContain(AbstractBlock $childBlock): bool
{
if ($this->hadBlankLine) {
// We saw a blank line in this list item, that means the list block is loose.
//
// spec: if any of its constituent list items directly contain two block-level elements with a blank line
// between them
$parent = $this->block->parent();
if ($parent instanceof ListBlock) {
$parent->setTight(false);
}
}
return true;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
if ($cursor->isBlank()) {
if ($this->block->firstChild() === null) {
// Blank line after empty list item
return BlockContinue::none();
}
$activeBlock = $activeBlockParser->getBlock();
// If the active block is a code block, blank lines in it should not affect if the list is tight.
$this->hadBlankLine = $activeBlock instanceof Paragraph || $activeBlock instanceof ListItem;
$cursor->advanceToNextNonSpaceOrTab();
return BlockContinue::at($cursor);
}
$contentIndent = $this->block->getListData()->markerOffset + $this->getBlock()->getListData()->padding;
if ($cursor->getIndent() >= $contentIndent) {
$cursor->advanceBy($contentIndent, true);
return BlockContinue::at($cursor);
}
// Note: We'll hit this case for lazy continuation lines, they will get added later.
return BlockContinue::none();
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\ThematicBreak;
use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
use League\CommonMark\Parser\Block\BlockContinue;
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
use League\CommonMark\Parser\Cursor;
final class ThematicBreakParser extends AbstractBlockContinueParser
{
/** @psalm-readonly */
private ThematicBreak $block;
public function __construct()
{
$this->block = new ThematicBreak();
}
public function getBlock(): ThematicBreak
{
return $this->block;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
// a horizontal rule can never container > 1 line, so fail to match
return BlockContinue::none();
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Block;
use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
use League\CommonMark\Util\RegexHelper;
final class ThematicBreakStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if ($cursor->isIndented()) {
return BlockStart::none();
}
$match = RegexHelper::matchAt(RegexHelper::REGEX_THEMATIC_BREAK, $cursor->getLine(), $cursor->getNextNonSpacePosition());
if ($match === null) {
return BlockStart::none();
}
// Advance to the end of the string, consuming the entire line (of the thematic break)
$cursor->advanceToEnd();
return BlockStart::of(new ThematicBreakParser())->at($cursor);
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Inline;
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
use League\CommonMark\Util\UrlEncoder;
final class AutolinkParser implements InlineParserInterface
{
private const EMAIL_REGEX = '<([a-zA-Z0-9.!#$%&\'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>';
private const OTHER_LINK_REGEX = '<([A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]*)>';
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::regex(self::EMAIL_REGEX . '|' . self::OTHER_LINK_REGEX);
}
public function parse(InlineParserContext $inlineContext): bool
{
$inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength());
$matches = $inlineContext->getMatches();
if ($matches[1] !== '') {
$inlineContext->getContainer()->appendChild(new Link('mailto:' . UrlEncoder::unescapeAndEncode($matches[1]), $matches[1]));
return true;
}
if ($matches[2] !== '') {
$inlineContext->getContainer()->appendChild(new Link(UrlEncoder::unescapeAndEncode($matches[2]), $matches[2]));
return true;
}
return false; // This should never happen
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Inline;
use League\CommonMark\Extension\CommonMark\Node\Inline\Code;
use League\CommonMark\Node\Inline\Text;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
final class BacktickParser implements InlineParserInterface
{
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::regex('`+');
}
public function parse(InlineParserContext $inlineContext): bool
{
$ticks = $inlineContext->getFullMatch();
$cursor = $inlineContext->getCursor();
$cursor->advanceBy($inlineContext->getFullMatchLength());
$currentPosition = $cursor->getPosition();
$previousState = $cursor->saveState();
while ($matchingTicks = $cursor->match('/`+/m')) {
if ($matchingTicks !== $ticks) {
continue;
}
$code = $cursor->getSubstring($currentPosition, $cursor->getPosition() - $currentPosition - \strlen($ticks));
$c = \preg_replace('/\n/m', ' ', $code) ?? '';
if (
$c !== '' &&
$c[0] === ' ' &&
\substr($c, -1, 1) === ' ' &&
\preg_match('/[^ ]/', $c)
) {
$c = \substr($c, 1, -1);
}
$inlineContext->getContainer()->appendChild(new Code($c));
return true;
}
// If we got here, we didn't match a closing backtick sequence
$cursor->restoreState($previousState);
$inlineContext->getContainer()->appendChild(new Text($ticks));
return true;
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Inline;
use League\CommonMark\Delimiter\Delimiter;
use League\CommonMark\Node\Inline\Text;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
final class BangParser implements InlineParserInterface
{
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::string('![');
}
public function parse(InlineParserContext $inlineContext): bool
{
$cursor = $inlineContext->getCursor();
$cursor->advanceBy(2);
$node = new Text('![', ['delim' => true]);
$inlineContext->getContainer()->appendChild($node);
// Add entry to stack for this opener
$delimiter = new Delimiter('!', 1, $node, true, false, $cursor->getPosition());
$inlineContext->getDelimiterStack()->push($delimiter);
return true;
}
}

View File

@@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Inline;
use League\CommonMark\Environment\EnvironmentAwareInterface;
use League\CommonMark\Environment\EnvironmentInterface;
use League\CommonMark\Extension\CommonMark\Node\Inline\AbstractWebResource;
use League\CommonMark\Extension\CommonMark\Node\Inline\Image;
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
use League\CommonMark\Extension\Mention\Mention;
use League\CommonMark\Node\Inline\AdjacentTextMerger;
use League\CommonMark\Node\Inline\Text;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
use League\CommonMark\Reference\ReferenceInterface;
use League\CommonMark\Reference\ReferenceMapInterface;
use League\CommonMark\Util\LinkParserHelper;
use League\CommonMark\Util\RegexHelper;
final class CloseBracketParser implements InlineParserInterface, EnvironmentAwareInterface
{
/** @psalm-readonly-allow-private-mutation */
private EnvironmentInterface $environment;
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::string(']');
}
public function parse(InlineParserContext $inlineContext): bool
{
// Look through stack of delimiters for a [ or !
$opener = $inlineContext->getDelimiterStack()->searchByCharacter(['[', '!']);
if ($opener === null) {
return false;
}
if (! $opener->isActive()) {
// no matched opener; remove from emphasis stack
$inlineContext->getDelimiterStack()->removeDelimiter($opener);
return false;
}
$cursor = $inlineContext->getCursor();
$startPos = $cursor->getPosition();
$previousState = $cursor->saveState();
$cursor->advanceBy(1);
// Check to see if we have a link/image
// Inline link?
if ($result = $this->tryParseInlineLinkAndTitle($cursor)) {
$link = $result;
} elseif ($link = $this->tryParseReference($cursor, $inlineContext->getReferenceMap(), $opener->getIndex(), $startPos)) {
$reference = $link;
$link = ['url' => $link->getDestination(), 'title' => $link->getTitle()];
} else {
// No match
$inlineContext->getDelimiterStack()->removeDelimiter($opener); // Remove this opener from stack
$cursor->restoreState($previousState);
return false;
}
$isImage = $opener->getChar() === '!';
$inline = $this->createInline($link['url'], $link['title'], $isImage, $reference ?? null);
$opener->getInlineNode()->replaceWith($inline);
while (($label = $inline->next()) !== null) {
// Is there a Mention or Link contained within this link?
// CommonMark does not allow nested links, so we'll restore the original text.
if ($label instanceof Mention) {
$label->replaceWith($replacement = new Text($label->getPrefix() . $label->getIdentifier()));
$inline->appendChild($replacement);
} elseif ($label instanceof Link) {
foreach ($label->children() as $child) {
$label->insertBefore($child);
}
$label->detach();
} else {
$inline->appendChild($label);
}
}
// Process delimiters such as emphasis inside link/image
$delimiterStack = $inlineContext->getDelimiterStack();
$stackBottom = $opener->getPrevious();
$delimiterStack->processDelimiters($stackBottom, $this->environment->getDelimiterProcessors());
$delimiterStack->removeAll($stackBottom);
// Merge any adjacent Text nodes together
AdjacentTextMerger::mergeChildNodes($inline);
// processEmphasis will remove this and later delimiters.
// Now, for a link, we also remove earlier link openers (no links in links)
if (! $isImage) {
$inlineContext->getDelimiterStack()->removeEarlierMatches('[');
}
return true;
}
public function setEnvironment(EnvironmentInterface $environment): void
{
$this->environment = $environment;
}
/**
* @return array<string, string>|null
*/
private function tryParseInlineLinkAndTitle(Cursor $cursor): ?array
{
if ($cursor->getCurrentCharacter() !== '(') {
return null;
}
$previousState = $cursor->saveState();
$cursor->advanceBy(1);
$cursor->advanceToNextNonSpaceOrNewline();
if (($dest = LinkParserHelper::parseLinkDestination($cursor)) === null) {
$cursor->restoreState($previousState);
return null;
}
$cursor->advanceToNextNonSpaceOrNewline();
$previousCharacter = $cursor->peek(-1);
// We know from previous lines that we've advanced at least one space so far, so this next call should never be null
\assert(\is_string($previousCharacter));
$title = '';
// make sure there's a space before the title:
if (\preg_match(RegexHelper::REGEX_WHITESPACE_CHAR, $previousCharacter)) {
$title = LinkParserHelper::parseLinkTitle($cursor) ?? '';
}
$cursor->advanceToNextNonSpaceOrNewline();
if ($cursor->getCurrentCharacter() !== ')') {
$cursor->restoreState($previousState);
return null;
}
$cursor->advanceBy(1);
return ['url' => $dest, 'title' => $title];
}
private function tryParseReference(Cursor $cursor, ReferenceMapInterface $referenceMap, ?int $openerIndex, int $startPos): ?ReferenceInterface
{
if ($openerIndex === null) {
return null;
}
$savePos = $cursor->saveState();
$beforeLabel = $cursor->getPosition();
$n = LinkParserHelper::parseLinkLabel($cursor);
if ($n === 0 || $n === 2) {
$start = $openerIndex;
$length = $startPos - $openerIndex;
} else {
$start = $beforeLabel + 1;
$length = $n - 2;
}
$referenceLabel = $cursor->getSubstring($start, $length);
if ($n === 0) {
// If shortcut reference link, rewind before spaces we skipped
$cursor->restoreState($savePos);
}
return $referenceMap->get($referenceLabel);
}
private function createInline(string $url, string $title, bool $isImage, ?ReferenceInterface $reference = null): AbstractWebResource
{
if ($isImage) {
$inline = new Image($url, null, $title);
} else {
$inline = new Link($url, null, $title);
}
if ($reference) {
$inline->data->set('reference', $reference);
}
return $inline;
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Inline;
use League\CommonMark\Node\Inline\Text;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
use League\CommonMark\Util\Html5EntityDecoder;
use League\CommonMark\Util\RegexHelper;
final class EntityParser implements InlineParserInterface
{
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::regex(RegexHelper::PARTIAL_ENTITY);
}
public function parse(InlineParserContext $inlineContext): bool
{
$entity = $inlineContext->getFullMatch();
$inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength());
$inlineContext->getContainer()->appendChild(new Text(Html5EntityDecoder::decode($entity)));
return true;
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Inline;
use League\CommonMark\Node\Inline\Newline;
use League\CommonMark\Node\Inline\Text;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
use League\CommonMark\Util\RegexHelper;
final class EscapableParser implements InlineParserInterface
{
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::string('\\');
}
public function parse(InlineParserContext $inlineContext): bool
{
$cursor = $inlineContext->getCursor();
$nextChar = $cursor->peek();
if ($nextChar === "\n") {
$cursor->advanceBy(2);
$inlineContext->getContainer()->appendChild(new Newline(Newline::HARDBREAK));
return true;
}
if ($nextChar !== null && RegexHelper::isEscapable($nextChar)) {
$cursor->advanceBy(2);
$inlineContext->getContainer()->appendChild(new Text($nextChar));
return true;
}
$cursor->advanceBy(1);
$inlineContext->getContainer()->appendChild(new Text('\\'));
return true;
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Inline;
use League\CommonMark\Extension\CommonMark\Node\Inline\HtmlInline;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
use League\CommonMark\Util\RegexHelper;
final class HtmlInlineParser implements InlineParserInterface
{
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::regex(RegexHelper::PARTIAL_HTMLTAG)->caseSensitive();
}
public function parse(InlineParserContext $inlineContext): bool
{
$inline = $inlineContext->getFullMatch();
$inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength());
$inlineContext->getContainer()->appendChild(new HtmlInline($inline));
return true;
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Parser\Inline;
use League\CommonMark\Delimiter\Delimiter;
use League\CommonMark\Node\Inline\Text;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
final class OpenBracketParser implements InlineParserInterface
{
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::string('[');
}
public function parse(InlineParserContext $inlineContext): bool
{
$inlineContext->getCursor()->advanceBy(1);
$node = new Text('[', ['delim' => true]);
$inlineContext->getContainer()->appendChild($node);
// Add entry to stack for this opener
$delimiter = new Delimiter('[', 1, $node, true, false, $inlineContext->getCursor()->getPosition());
$inlineContext->getDelimiterStack()->push($delimiter);
return true;
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Renderer\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\BlockQuote;
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 BlockQuoteRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param BlockQuote $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
BlockQuote::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
$filling = $childRenderer->renderNodes($node->children());
$innerSeparator = $childRenderer->getInnerSeparator();
if ($filling === '') {
return new HtmlElement('blockquote', $attrs, $innerSeparator);
}
return new HtmlElement(
'blockquote',
$attrs,
$innerSeparator . $filling . $innerSeparator
);
}
public function getXmlTagName(Node $node): string
{
return 'block_quote';
}
/**
* @param BlockQuote $node
*
* @return array<string, scalar>
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Renderer\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
use League\CommonMark\Util\Xml;
use League\CommonMark\Xml\XmlNodeRendererInterface;
final class FencedCodeRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param FencedCode $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
FencedCode::assertInstanceOf($node);
$attrs = $node->data->getData('attributes');
$infoWords = $node->getInfoWords();
if (\count($infoWords) !== 0 && $infoWords[0] !== '') {
$attrs->append('class', 'language-' . $infoWords[0]);
}
return new HtmlElement(
'pre',
[],
new HtmlElement('code', $attrs->export(), Xml::escape($node->getLiteral()))
);
}
public function getXmlTagName(Node $node): string
{
return 'code_block';
}
/**
* @param FencedCode $node
*
* @return array<string, scalar>
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function getXmlAttributes(Node $node): array
{
FencedCode::assertInstanceOf($node);
if (($info = $node->getInfo()) === null || $info === '') {
return [];
}
return ['info' => $info];
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Renderer\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
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 HeadingRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param Heading $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
Heading::assertInstanceOf($node);
$tag = 'h' . $node->getLevel();
$attrs = $node->data->get('attributes');
return new HtmlElement($tag, $attrs, $childRenderer->renderNodes($node->children()));
}
public function getXmlTagName(Node $node): string
{
return 'heading';
}
/**
* @param Heading $node
*
* @return array<string, scalar>
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function getXmlAttributes(Node $node): array
{
Heading::assertInstanceOf($node);
return ['level' => $node->getLevel()];
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Renderer\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlFilter;
use League\CommonMark\Xml\XmlNodeRendererInterface;
use League\Config\ConfigurationAwareInterface;
use League\Config\ConfigurationInterface;
final class HtmlBlockRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface
{
/** @psalm-readonly-allow-private-mutation */
private ConfigurationInterface $config;
/**
* @param HtmlBlock $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): string
{
HtmlBlock::assertInstanceOf($node);
$htmlInput = $this->config->get('html_input');
return HtmlFilter::filter($node->getLiteral(), $htmlInput);
}
public function setConfiguration(ConfigurationInterface $configuration): void
{
$this->config = $configuration;
}
public function getXmlTagName(Node $node): string
{
return 'html_block';
}
/**
* {@inheritDoc}
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Renderer\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\IndentedCode;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
use League\CommonMark\Util\Xml;
use League\CommonMark\Xml\XmlNodeRendererInterface;
final class IndentedCodeRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param IndentedCode $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
IndentedCode::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
return new HtmlElement(
'pre',
[],
new HtmlElement('code', $attrs, Xml::escape($node->getLiteral()))
);
}
public function getXmlTagName(Node $node): string
{
return 'code_block';
}
/**
* @return array<string, scalar>
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Renderer\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
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 ListBlockRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param ListBlock $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
ListBlock::assertInstanceOf($node);
$listData = $node->getListData();
$tag = $listData->type === ListBlock::TYPE_BULLET ? 'ul' : 'ol';
$attrs = $node->data->get('attributes');
if ($listData->start !== null && $listData->start !== 1) {
$attrs['start'] = (string) $listData->start;
}
$innerSeparator = $childRenderer->getInnerSeparator();
return new HtmlElement($tag, $attrs, $innerSeparator . $childRenderer->renderNodes($node->children()) . $innerSeparator);
}
public function getXmlTagName(Node $node): string
{
return 'list';
}
/**
* @param ListBlock $node
*
* @return array<string, scalar>
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function getXmlAttributes(Node $node): array
{
ListBlock::assertInstanceOf($node);
$data = $node->getListData();
if ($data->type === ListBlock::TYPE_BULLET) {
return [
'type' => $data->type,
'tight' => $node->isTight() ? 'true' : 'false',
];
}
return [
'type' => $data->type,
'start' => $data->start ?? 1,
'tight' => $node->isTight(),
'delimiter' => $data->delimiter ?? ListBlock::DELIM_PERIOD,
];
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Renderer\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
use League\CommonMark\Extension\TaskList\TaskListItemMarker;
use League\CommonMark\Node\Block\Paragraph;
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 ListItemRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param ListItem $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
ListItem::assertInstanceOf($node);
$contents = $childRenderer->renderNodes($node->children());
if (\substr($contents, 0, 1) === '<' && ! $this->startsTaskListItem($node)) {
$contents = "\n" . $contents;
}
if (\substr($contents, -1, 1) === '>') {
$contents .= "\n";
}
$attrs = $node->data->get('attributes');
return new HtmlElement('li', $attrs, $contents);
}
public function getXmlTagName(Node $node): string
{
return 'item';
}
/**
* {@inheritDoc}
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
private function startsTaskListItem(ListItem $block): bool
{
$firstChild = $block->firstChild();
return $firstChild instanceof Paragraph && $firstChild->firstChild() instanceof TaskListItemMarker;
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Renderer\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\ThematicBreak;
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 ThematicBreakRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param ThematicBreak $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
ThematicBreak::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
return new HtmlElement('hr', $attrs, '', true);
}
public function getXmlTagName(Node $node): string
{
return 'thematic_break';
}
/**
* {@inheritDoc}
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Renderer\Inline;
use League\CommonMark\Extension\CommonMark\Node\Inline\Code;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
use League\CommonMark\Util\Xml;
use League\CommonMark\Xml\XmlNodeRendererInterface;
final class CodeRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param Code $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
Code::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
return new HtmlElement('code', $attrs, Xml::escape($node->getLiteral()));
}
public function getXmlTagName(Node $node): string
{
return 'code';
}
/**
* {@inheritDoc}
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Renderer\Inline;
use League\CommonMark\Extension\CommonMark\Node\Inline\Emphasis;
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 EmphasisRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param Emphasis $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
Emphasis::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
return new HtmlElement('em', $attrs, $childRenderer->renderNodes($node->children()));
}
public function getXmlTagName(Node $node): string
{
return 'emph';
}
/**
* {@inheritDoc}
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Renderer\Inline;
use League\CommonMark\Extension\CommonMark\Node\Inline\HtmlInline;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlFilter;
use League\CommonMark\Xml\XmlNodeRendererInterface;
use League\Config\ConfigurationAwareInterface;
use League\Config\ConfigurationInterface;
final class HtmlInlineRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface
{
/** @psalm-readonly-allow-private-mutation */
private ConfigurationInterface $config;
/**
* @param HtmlInline $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): string
{
HtmlInline::assertInstanceOf($node);
$htmlInput = $this->config->get('html_input');
return HtmlFilter::filter($node->getLiteral(), $htmlInput);
}
public function setConfiguration(ConfigurationInterface $configuration): void
{
$this->config = $configuration;
}
public function getXmlTagName(Node $node): string
{
return 'html_inline';
}
/**
* {@inheritDoc}
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Renderer\Inline;
use League\CommonMark\Extension\CommonMark\Node\Inline\Image;
use League\CommonMark\Node\Inline\Newline;
use League\CommonMark\Node\Node;
use League\CommonMark\Node\NodeIterator;
use League\CommonMark\Node\StringContainerInterface;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
use League\CommonMark\Util\RegexHelper;
use League\CommonMark\Xml\XmlNodeRendererInterface;
use League\Config\ConfigurationAwareInterface;
use League\Config\ConfigurationInterface;
final class ImageRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface
{
/** @psalm-readonly-allow-private-mutation */
private ConfigurationInterface $config;
/**
* @param Image $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
Image::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
$forbidUnsafeLinks = ! $this->config->get('allow_unsafe_links');
if ($forbidUnsafeLinks && RegexHelper::isLinkPotentiallyUnsafe($node->getUrl())) {
$attrs['src'] = '';
} else {
$attrs['src'] = $node->getUrl();
}
$attrs['alt'] = $this->getAltText($node);
if (($title = $node->getTitle()) !== null) {
$attrs['title'] = $title;
}
return new HtmlElement('img', $attrs, '', true);
}
public function setConfiguration(ConfigurationInterface $configuration): void
{
$this->config = $configuration;
}
public function getXmlTagName(Node $node): string
{
return 'image';
}
/**
* @param Image $node
*
* @return array<string, scalar>
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function getXmlAttributes(Node $node): array
{
Image::assertInstanceOf($node);
return [
'destination' => $node->getUrl(),
'title' => $node->getTitle() ?? '',
];
}
private function getAltText(Image $node): string
{
$altText = '';
foreach ((new NodeIterator($node)) as $n) {
if ($n instanceof StringContainerInterface) {
$altText .= $n->getLiteral();
} elseif ($n instanceof Newline) {
$altText .= "\n";
}
}
return $altText;
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Renderer\Inline;
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
use League\CommonMark\Util\RegexHelper;
use League\CommonMark\Xml\XmlNodeRendererInterface;
use League\Config\ConfigurationAwareInterface;
use League\Config\ConfigurationInterface;
final class LinkRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface
{
/** @psalm-readonly-allow-private-mutation */
private ConfigurationInterface $config;
/**
* @param Link $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
Link::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
$forbidUnsafeLinks = ! $this->config->get('allow_unsafe_links');
if (! ($forbidUnsafeLinks && RegexHelper::isLinkPotentiallyUnsafe($node->getUrl()))) {
$attrs['href'] = $node->getUrl();
}
if (($title = $node->getTitle()) !== null) {
$attrs['title'] = $title;
}
if (isset($attrs['target']) && $attrs['target'] === '_blank' && ! isset($attrs['rel'])) {
$attrs['rel'] = 'noopener noreferrer';
}
return new HtmlElement('a', $attrs, $childRenderer->renderNodes($node->children()));
}
public function setConfiguration(ConfigurationInterface $configuration): void
{
$this->config = $configuration;
}
public function getXmlTagName(Node $node): string
{
return 'link';
}
/**
* @param Link $node
*
* @return array<string, scalar>
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function getXmlAttributes(Node $node): array
{
Link::assertInstanceOf($node);
return [
'destination' => $node->getUrl(),
'title' => $node->getTitle() ?? '',
];
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/*
* 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\CommonMark\Renderer\Inline;
use League\CommonMark\Extension\CommonMark\Node\Inline\Strong;
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 StrongRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param Strong $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
Strong::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
return new HtmlElement('strong', $attrs, $childRenderer->renderNodes($node->children()));
}
public function getXmlTagName(Node $node): string
{
return 'strong';
}
/**
* {@inheritDoc}
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension;
use League\Config\ConfigurationBuilderInterface;
interface ConfigurableExtensionInterface extends ExtensionInterface
{
public function configureSchema(ConfigurationBuilderInterface $builder): void;
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/*
* 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\DefaultAttributes;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
use League\Config\ConfigurationAwareInterface;
use League\Config\ConfigurationInterface;
final class ApplyDefaultAttributesProcessor implements ConfigurationAwareInterface
{
private ConfigurationInterface $config;
public function onDocumentParsed(DocumentParsedEvent $event): void
{
/** @var array<string, array<string, mixed>> $map */
$map = $this->config->get('default_attributes');
// Don't bother iterating if no default attributes are configured
if (! $map) {
return;
}
foreach ($event->getDocument()->iterator() as $node) {
// Check to see if any default attributes were defined
if (($attributesToApply = $map[\get_class($node)] ?? []) === []) {
continue;
}
$newAttributes = [];
foreach ($attributesToApply as $name => $value) {
if (\is_callable($value)) {
$value = $value($node);
// Callables are allowed to return `null` indicating that no changes should be made
if ($value !== null) {
$newAttributes[$name] = $value;
}
} else {
$newAttributes[$name] = $value;
}
}
// Merge these attributes into the node
if (\count($newAttributes) > 0) {
$node->data->set('attributes', AttributesHelper::mergeAttributes($node, $newAttributes));
}
}
}
public function setConfiguration(ConfigurationInterface $configuration): void
{
$this->config = $configuration;
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* 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\DefaultAttributes;
use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\ConfigurableExtensionInterface;
use League\Config\ConfigurationBuilderInterface;
use Nette\Schema\Expect;
final class DefaultAttributesExtension implements ConfigurableExtensionInterface
{
public function configureSchema(ConfigurationBuilderInterface $builder): void
{
$builder->addSchema('default_attributes', Expect::arrayOf(
Expect::arrayOf(
Expect::type('string|string[]|bool|callable'), // attribute value(s)
'string' // attribute name
),
'string' // node FQCN
)->default([]));
}
public function register(EnvironmentBuilderInterface $environment): void
{
$environment->addEventListener(DocumentParsedEvent::class, [new ApplyDefaultAttributesProcessor(), 'onDocumentParsed']);
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* 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\DescriptionList;
use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\DescriptionList\Event\ConsecutiveDescriptionListMerger;
use League\CommonMark\Extension\DescriptionList\Event\LooseDescriptionHandler;
use League\CommonMark\Extension\DescriptionList\Node\Description;
use League\CommonMark\Extension\DescriptionList\Node\DescriptionList;
use League\CommonMark\Extension\DescriptionList\Node\DescriptionTerm;
use League\CommonMark\Extension\DescriptionList\Parser\DescriptionStartParser;
use League\CommonMark\Extension\DescriptionList\Renderer\DescriptionListRenderer;
use League\CommonMark\Extension\DescriptionList\Renderer\DescriptionRenderer;
use League\CommonMark\Extension\DescriptionList\Renderer\DescriptionTermRenderer;
use League\CommonMark\Extension\ExtensionInterface;
final class DescriptionListExtension implements ExtensionInterface
{
public function register(EnvironmentBuilderInterface $environment): void
{
$environment->addBlockStartParser(new DescriptionStartParser());
$environment->addEventListener(DocumentParsedEvent::class, new LooseDescriptionHandler(), 1001);
$environment->addEventListener(DocumentParsedEvent::class, new ConsecutiveDescriptionListMerger(), 1000);
$environment->addRenderer(DescriptionList::class, new DescriptionListRenderer());
$environment->addRenderer(DescriptionTerm::class, new DescriptionTermRenderer());
$environment->addRenderer(Description::class, new DescriptionRenderer());
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* 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\DescriptionList\Event;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\DescriptionList\Node\DescriptionList;
use League\CommonMark\Node\NodeIterator;
final class ConsecutiveDescriptionListMerger
{
public function __invoke(DocumentParsedEvent $event): void
{
foreach ($event->getDocument()->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) {
if (! $node instanceof DescriptionList) {
continue;
}
if (! ($prev = $node->previous()) instanceof DescriptionList) {
continue;
}
// There's another description list behind this one; merge the current one into that
foreach ($node->children() as $child) {
$prev->appendChild($child);
}
$node->detach();
}
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* 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\DescriptionList\Event;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\DescriptionList\Node\Description;
use League\CommonMark\Extension\DescriptionList\Node\DescriptionList;
use League\CommonMark\Extension\DescriptionList\Node\DescriptionTerm;
use League\CommonMark\Node\Block\Paragraph;
use League\CommonMark\Node\Inline\Newline;
use League\CommonMark\Node\NodeIterator;
final class LooseDescriptionHandler
{
public function __invoke(DocumentParsedEvent $event): void
{
foreach ($event->getDocument()->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $description) {
if (! $description instanceof Description) {
continue;
}
// Does this description need to be added to a list?
if (! $description->parent() instanceof DescriptionList) {
$list = new DescriptionList();
// Taking any preceding paragraphs with it
if (($paragraph = $description->previous()) instanceof Paragraph) {
$list->appendChild($paragraph);
}
$description->replaceWith($list);
$list->appendChild($description);
}
// Is this description preceded by a paragraph that should really be a term?
if (! (($paragraph = $description->previous()) instanceof Paragraph)) {
continue;
}
// Convert the paragraph into one or more terms
$term = new DescriptionTerm();
$paragraph->replaceWith($term);
foreach ($paragraph->children() as $child) {
if ($child instanceof Newline) {
$newTerm = new DescriptionTerm();
$term->insertAfter($newTerm);
$term = $newTerm;
continue;
}
$term->appendChild($child);
}
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* 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\DescriptionList\Node;
use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Node\Block\TightBlockInterface;
class Description extends AbstractBlock implements TightBlockInterface
{
private bool $tight;
public function __construct(bool $tight = false)
{
parent::__construct();
$this->tight = $tight;
}
public function isTight(): bool
{
return $this->tight;
}
public function setTight(bool $tight): void
{
$this->tight = $tight;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/*
* 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\DescriptionList\Node;
use League\CommonMark\Node\Block\AbstractBlock;
class DescriptionList extends AbstractBlock
{
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/*
* 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\DescriptionList\Node;
use League\CommonMark\Node\Block\AbstractBlock;
class DescriptionTerm extends AbstractBlock
{
}

View File

@@ -0,0 +1,71 @@
<?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.
*/
declare(strict_types=1);
namespace League\CommonMark\Extension\DescriptionList\Parser;
use League\CommonMark\Extension\DescriptionList\Node\Description;
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;
final class DescriptionContinueParser extends AbstractBlockContinueParser
{
private Description $block;
private int $indentation;
public function __construct(bool $tight, int $indentation)
{
$this->block = new Description($tight);
$this->indentation = $indentation;
}
public function getBlock(): Description
{
return $this->block;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
if ($cursor->isBlank()) {
if ($this->block->firstChild() === null) {
// Blank line after empty item
return BlockContinue::none();
}
$cursor->advanceToNextNonSpaceOrTab();
return BlockContinue::at($cursor);
}
if ($cursor->getIndent() >= $this->indentation) {
$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;
}
}

View File

@@ -0,0 +1,53 @@
<?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.
*/
declare(strict_types=1);
namespace League\CommonMark\Extension\DescriptionList\Parser;
use League\CommonMark\Extension\DescriptionList\Node\Description;
use League\CommonMark\Extension\DescriptionList\Node\DescriptionList;
use League\CommonMark\Extension\DescriptionList\Node\DescriptionTerm;
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;
final class DescriptionListContinueParser extends AbstractBlockContinueParser
{
private DescriptionList $block;
public function __construct()
{
$this->block = new DescriptionList();
}
public function getBlock(): DescriptionList
{
return $this->block;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
return BlockContinue::at($cursor);
}
public function isContainer(): bool
{
return true;
}
public function canContain(AbstractBlock $childBlock): bool
{
return $childBlock instanceof DescriptionTerm || $childBlock instanceof Description;
}
}

View File

@@ -0,0 +1,73 @@
<?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.
*/
declare(strict_types=1);
namespace League\CommonMark\Extension\DescriptionList\Parser;
use League\CommonMark\Extension\DescriptionList\Node\Description;
use League\CommonMark\Node\Block\Paragraph;
use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
final class DescriptionStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if ($cursor->isIndented()) {
return BlockStart::none();
}
$cursor->advanceToNextNonSpaceOrTab();
if ($cursor->match('/^:[ \t]+/') === null) {
return BlockStart::none();
}
$terms = $parserState->getParagraphContent();
$activeBlock = $parserState->getActiveBlockParser()->getBlock();
if ($terms !== null && $terms !== '') {
// New description; tight; term(s) sitting in pending block that we will replace
return BlockStart::of(...[new DescriptionListContinueParser()], ...self::splitTerms($terms), ...[new DescriptionContinueParser(true, $cursor->getPosition())])
->at($cursor)
->replaceActiveBlockParser();
}
if ($activeBlock instanceof Paragraph && $activeBlock->parent() instanceof Description) {
// Additional description in the same list as the parent description
return BlockStart::of(new DescriptionContinueParser(true, $cursor->getPosition()))->at($cursor);
}
if ($activeBlock->lastChild() instanceof Paragraph) {
// New description; loose; term(s) sitting in previous closed paragraph block
return BlockStart::of(new DescriptionContinueParser(false, $cursor->getPosition()))->at($cursor);
}
// No preceding terms
return BlockStart::none();
}
/**
* @return array<int, DescriptionTermContinueParser>
*/
private static function splitTerms(string $terms): array
{
$ret = [];
foreach (\explode("\n", $terms) as $term) {
$ret[] = new DescriptionTermContinueParser($term);
}
return $ret;
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/*
* 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\DescriptionList\Parser;
use League\CommonMark\Extension\DescriptionList\Node\DescriptionTerm;
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;
final class DescriptionTermContinueParser extends AbstractBlockContinueParser implements BlockContinueParserWithInlinesInterface
{
private DescriptionTerm $block;
private string $term;
public function __construct(string $term)
{
$this->block = new DescriptionTerm();
$this->term = $term;
}
public function getBlock(): DescriptionTerm
{
return $this->block;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
return BlockContinue::finished();
}
public function parseInlines(InlineParserEngineInterface $inlineParser): void
{
if ($this->term !== '') {
$inlineParser->parse($this->term, $this->block);
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* 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\DescriptionList\Renderer;
use League\CommonMark\Extension\DescriptionList\Node\DescriptionList;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
final class DescriptionListRenderer implements NodeRendererInterface
{
/**
* @param DescriptionList $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): HtmlElement
{
DescriptionList::assertInstanceOf($node);
$separator = $childRenderer->getBlockSeparator();
return new HtmlElement('dl', [], $separator . $childRenderer->renderNodes($node->children()) . $separator);
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* 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\DescriptionList\Renderer;
use League\CommonMark\Extension\DescriptionList\Node\Description;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
final class DescriptionRenderer implements NodeRendererInterface
{
/**
* @param Description $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
Description::assertInstanceOf($node);
return new HtmlElement('dd', [], $childRenderer->renderNodes($node->children()));
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* 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\DescriptionList\Renderer;
use League\CommonMark\Extension\DescriptionList\Node\DescriptionTerm;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
final class DescriptionTermRenderer implements NodeRendererInterface
{
/**
* @param DescriptionTerm $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
DescriptionTerm::assertInstanceOf($node);
return new HtmlElement('dt', [], $childRenderer->renderNodes($node->children()));
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/*
* 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\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 ConfigurableExtensionInterface
{
private const DEFAULT_DISALLOWED_TAGS = [
'title',
'textarea',
'style',
'xmp',
'iframe',
'noembed',
'noframes',
'script',
'plaintext',
];
public function configureSchema(ConfigurationBuilderInterface $builder): void
{
$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);
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/*
* 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\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\Config\ConfigurationAwareInterface;
use League\Config\ConfigurationInterface;
final class DisallowedRawHtmlRenderer implements NodeRendererInterface, ConfigurationAwareInterface
{
/** @psalm-readonly */
private NodeRendererInterface $innerRenderer;
/** @psalm-readonly-allow-private-mutation */
private ConfigurationInterface $config;
public function __construct(NodeRendererInterface $innerRenderer)
{
$this->innerRenderer = $innerRenderer;
}
public function render(Node $node, ChildNodeRendererInterface $childRenderer): ?string
{
$rendered = (string) $this->innerRenderer->render($node, $childRenderer);
if ($rendered === '') {
return '';
}
$tags = (array) $this->config->get('disallowed_raw_html/disallowed_tags');
if (\count($tags) === 0) {
return $rendered;
}
$regex = \sprintf('/<(\/?(?:%s)[ \/>])/i', \implode('|', \array_map('preg_quote', $tags)));
// Match these types of tags: <title> </title> <title x="sdf"> <title/> <title />
return \preg_replace($regex, '&lt;$1', $rendered);
}
public function setConfiguration(ConfigurationInterface $configuration): void
{
$this->config = $configuration;
if ($this->innerRenderer instanceof ConfigurationAwareInterface) {
$this->innerRenderer->setConfiguration($configuration);
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* 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\Embed\Bridge;
use Embed\Embed as EmbedLib;
use League\CommonMark\Extension\Embed\Embed;
use League\CommonMark\Extension\Embed\EmbedAdapterInterface;
final class OscaroteroEmbedAdapter implements EmbedAdapterInterface
{
private EmbedLib $embedLib;
public function __construct(?EmbedLib $embed = null)
{
if ($embed === null) {
if (! \class_exists(EmbedLib::class)) {
throw new \RuntimeException('The embed/embed package is not installed. Please install it with Composer to use this adapter.');
}
$embed = new EmbedLib();
}
$this->embedLib = $embed;
}
/**
* {@inheritDoc}
*/
public function updateEmbeds(array $embeds): void
{
$extractors = $this->embedLib->getMulti(...\array_map(static fn (Embed $embed) => $embed->getUrl(), $embeds));
foreach ($extractors as $i => $extractor) {
if ($extractor->code !== null) {
$embeds[$i]->setEmbedCode($extractor->code->html);
}
}
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/*
* 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\Embed;
class DomainFilteringAdapter implements EmbedAdapterInterface
{
private EmbedAdapterInterface $decorated;
private string $regex;
/**
* @param string[] $allowedDomains
*/
public function __construct(EmbedAdapterInterface $decorated, array $allowedDomains)
{
$this->decorated = $decorated;
$this->regex = self::createRegex($allowedDomains);
}
/**
* {@inheritDoc}
*/
public function updateEmbeds(array $embeds): void
{
$this->decorated->updateEmbeds(\array_values(\array_filter($embeds, function (Embed $embed): bool {
return \preg_match($this->regex, $embed->getUrl()) === 1;
})));
}
/**
* @param string[] $allowedDomains
*/
private static function createRegex(array $allowedDomains): string
{
$allowedDomains = \array_map('preg_quote', $allowedDomains);
return '/^(?:https?:\/\/)?(?:[^.]+\.)*(' . \implode('|', $allowedDomains) . ')/';
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/*
* 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\Embed;
use League\CommonMark\Node\Block\AbstractBlock;
final class Embed extends AbstractBlock
{
private string $url;
private ?string $embedCode;
public function __construct(string $url, ?string $embedCode = null)
{
parent::__construct();
$this->url = $url;
$this->embedCode = $embedCode;
}
public function getUrl(): string
{
return $this->url;
}
public function setUrl(string $url): void
{
$this->url = $url;
}
public function getEmbedCode(): ?string
{
return $this->embedCode;
}
public function setEmbedCode(?string $embedCode): void
{
$this->embedCode = $embedCode;
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* 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\Embed;
/**
* Interface for a service which updates the embed code(s) for the given array of embeds
*/
interface EmbedAdapterInterface
{
/**
* @param Embed[] $embeds
*/
public function updateEmbeds(array $embeds): void;
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* 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\Embed;
use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\ConfigurableExtensionInterface;
use League\Config\ConfigurationBuilderInterface;
use Nette\Schema\Expect;
final class EmbedExtension implements ConfigurableExtensionInterface
{
public function configureSchema(ConfigurationBuilderInterface $builder): void
{
$builder->addSchema('embed', Expect::structure([
'adapter' => Expect::type(EmbedAdapterInterface::class),
'allowed_domains' => Expect::arrayOf('string')->default([]),
'fallback' => Expect::anyOf('link', 'remove')->default('link'),
]));
}
public function register(EnvironmentBuilderInterface $environment): void
{
$adapter = $environment->getConfiguration()->get('embed.adapter');
\assert($adapter instanceof EmbedAdapterInterface);
$allowedDomains = $environment->getConfiguration()->get('embed.allowed_domains');
if ($allowedDomains !== []) {
$adapter = new DomainFilteringAdapter($adapter, $allowedDomains);
}
$environment
->addBlockStartParser(new EmbedStartParser(), 300)
->addEventListener(DocumentParsedEvent::class, new EmbedProcessor($adapter, $environment->getConfiguration()->get('embed.fallback')), 1010)
->addRenderer(Embed::class, new EmbedRenderer());
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/*
* 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\Embed;
use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Parser\Block\BlockContinue;
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
use League\CommonMark\Parser\Cursor;
class EmbedParser implements BlockContinueParserInterface
{
private Embed $embed;
public function __construct(string $url)
{
$this->embed = new Embed($url);
}
public function getBlock(): AbstractBlock
{
return $this->embed;
}
public function isContainer(): bool
{
return false;
}
public function canHaveLazyContinuationLines(): bool
{
return false;
}
public function canContain(AbstractBlock $childBlock): bool
{
return false;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
return BlockContinue::none();
}
public function addLine(string $line): void
{
}
public function closeBlock(): void
{
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
/*
* 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\Embed;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
use League\CommonMark\Node\Block\Paragraph;
use League\CommonMark\Node\Inline\Text;
use League\CommonMark\Node\NodeIterator;
final class EmbedProcessor
{
public const FALLBACK_REMOVE = 'remove';
public const FALLBACK_LINK = 'link';
private EmbedAdapterInterface $adapter;
private string $fallback;
public function __construct(EmbedAdapterInterface $adapter, string $fallback = self::FALLBACK_REMOVE)
{
$this->adapter = $adapter;
$this->fallback = $fallback;
}
public function __invoke(DocumentParsedEvent $event): void
{
$document = $event->getDocument();
$embeds = [];
foreach (new NodeIterator($document) as $node) {
if (! ($node instanceof Embed)) {
continue;
}
if ($node->parent() !== $document) {
$replacement = new Paragraph();
$replacement->appendChild(new Text($node->getUrl()));
$node->replaceWith($replacement);
} else {
$embeds[] = $node;
}
}
$this->adapter->updateEmbeds($embeds);
foreach ($embeds as $embed) {
if ($embed->getEmbedCode() !== null) {
continue;
}
if ($this->fallback === self::FALLBACK_REMOVE) {
$embed->detach();
} elseif ($this->fallback === self::FALLBACK_LINK) {
$paragraph = new Paragraph();
$paragraph->appendChild(new Link($embed->getUrl(), $embed->getUrl()));
$embed->replaceWith($paragraph);
}
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* 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\Embed;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
class EmbedRenderer implements NodeRendererInterface
{
/**
* @param Embed $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer)
{
Embed::assertInstanceOf($node);
return $node->getEmbedCode() ?? '';
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/*
* 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\Embed;
use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
use League\CommonMark\Util\LinkParserHelper;
class EmbedStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if ($cursor->isIndented() || $parserState->getParagraphContent() !== null || ! ($parserState->getActiveBlockParser()->isContainer())) {
return BlockStart::none();
}
// 0-3 leading spaces are okay
$cursor->advanceToNextNonSpaceOrTab();
// The line must begin with "https://"
if (! str_starts_with($cursor->getRemainder(), 'https://')) {
return BlockStart::none();
}
// A valid link must be found next
if (($dest = LinkParserHelper::parseLinkDestination($cursor)) === null) {
return BlockStart::none();
}
// Skip any trailing whitespace
$cursor->advanceToNextNonSpaceOrTab();
// We must be at the end of the line; otherwise, this link was not by itself
if (! $cursor->isAtEnd()) {
return BlockStart::none();
}
return BlockStart::of(new EmbedParser($dest))->at($cursor);
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/*
* 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\Environment\EnvironmentBuilderInterface;
interface ExtensionInterface
{
public function register(EnvironmentBuilderInterface $environment): void;
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\ExternalLink;
use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\ConfigurableExtensionInterface;
use League\Config\ConfigurationBuilderInterface;
use Nette\Schema\Expect;
final class ExternalLinkExtension implements ConfigurableExtensionInterface
{
public function configureSchema(ConfigurationBuilderInterface $builder): void
{
$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);
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Extension\ExternalLink;
use League\CommonMark\Event\DocumentParsedEvent;
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_EXTERNAL = 'external';
public const APPLY_INTERNAL = 'internal';
/** @psalm-readonly */
private ConfigurationInterface $config;
public function __construct(ConfigurationInterface $config)
{
$this->config = $config;
}
public function __invoke(DocumentParsedEvent $e): void
{
$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');
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->set('external', true);
$this->applyRelAttribute($link, true);
if ($openInNewWindow) {
$link->data->set('attributes/target', '_blank');
}
if ($classes !== '') {
$link->data->append('attributes/class', $classes);
}
}
private function applyRelAttribute(Link $link, bool $isExternal): void
{
$options = [
'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:
$link->data->append('attributes/rel', $type);
}
}
}
/**
* @internal This method is only public so we can easily test it. DO NOT USE THIS OUTSIDE OF THIS EXTENSION!
*
* @param mixed $compareTo
*/
public static function hostMatches(string $host, $compareTo): bool
{
foreach ((array) $compareTo as $c) {
if (\strpos($c, '/') === 0) {
if (\preg_match($c, $host)) {
return true;
}
} elseif ($c === $host) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) Rezo Zero / Ambroise Maupate
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\CommonMark\Extension\Footnote\Event;
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\Node\Block\Paragraph;
use League\CommonMark\Node\Inline\Text;
use League\CommonMark\Reference\Reference;
use League\Config\ConfigurationAwareInterface;
use League\Config\ConfigurationInterface;
final class AnonymousFootnotesListener implements ConfigurationAwareInterface
{
private ConfigurationInterface $config;
public function onDocumentParsed(DocumentParsedEvent $event): void
{
$document = $event->getDocument();
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 $configuration): void
{
$this->config = $configuration;
}
}

View File

@@ -0,0 +1,68 @@
<?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.
*/
declare(strict_types=1);
namespace League\CommonMark\Extension\Footnote\Event;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\Footnote\Node\Footnote;
use League\CommonMark\Extension\Footnote\Node\FootnoteRef;
use League\CommonMark\Node\Block\Document;
use League\CommonMark\Node\Inline\Text;
final class FixOrphanedFootnotesAndRefsListener
{
public function onDocumentParsed(DocumentParsedEvent $event): void
{
$document = $event->getDocument();
$map = $this->buildMapOfKnownFootnotesAndRefs($document);
foreach ($map['_flat'] as $node) {
if ($node instanceof FootnoteRef && ! isset($map[Footnote::class][$node->getReference()->getLabel()])) {
// Found an orphaned FootnoteRef without a corresponding Footnote
// Restore the original footnote ref text
$node->replaceWith(new Text(\sprintf('[^%s]', $node->getReference()->getLabel())));
}
// phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed
if ($node instanceof Footnote && ! isset($map[FootnoteRef::class][$node->getReference()->getLabel()])) {
// Found an orphaned Footnote without a corresponding FootnoteRef
// Remove the footnote
$node->detach();
}
}
}
/** @phpstan-ignore-next-line */
private function buildMapOfKnownFootnotesAndRefs(Document $document): array // @phpcs:ignore
{
$map = [
Footnote::class => [],
FootnoteRef::class => [],
'_flat' => [],
];
foreach ($document->iterator() as $node) {
if ($node instanceof Footnote) {
$map[Footnote::class][$node->getReference()->getLabel()] = true;
$map['_flat'][] = $node;
} elseif ($node instanceof FootnoteRef) {
$map[FootnoteRef::class][$node->getReference()->getLabel()] = true;
$map['_flat'][] = $node;
}
}
return $map;
}
}

View File

@@ -0,0 +1,106 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) Rezo Zero / Ambroise Maupate
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\CommonMark\Extension\Footnote\Event;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\Footnote\Node\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\Config\ConfigurationAwareInterface;
use League\Config\ConfigurationInterface;
final class GatherFootnotesListener implements ConfigurationAwareInterface
{
private ConfigurationInterface $config;
public function onDocumentParsed(DocumentParsedEvent $event): void
{
$document = $event->getDocument();
$footnotes = [];
foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) {
if (! $node instanceof Footnote) {
continue;
}
// Look for existing reference with footnote label
$ref = $document->getReferenceMap()->get($node->getReference()->getLabel());
if ($ref !== null) {
// Use numeric title to get footnotes order
$footnotes[(int) $ref->getTitle()] = $node;
} else {
// Footnote call is missing, append footnote at the end
$footnotes[\PHP_INT_MAX] = $node;
}
$key = '#' . $this->config->get('footnote/footnote_id_prefix') . $node->getReference()->getDestination();
if ($document->data->has($key)) {
$this->createBackrefs($node, $document->data->get($key));
}
}
// Only add a footnote container if there are any
if (\count($footnotes) === 0) {
return;
}
$container = $this->getFootnotesContainer($document);
\ksort($footnotes);
foreach ($footnotes as $footnote) {
$container->appendChild($footnote);
}
}
private function getFootnotesContainer(Document $document): FootnoteContainer
{
$footnoteContainer = new FootnoteContainer();
$document->appendChild($footnoteContainer);
return $footnoteContainer;
}
/**
* 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
{
// 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;
}
}

View File

@@ -0,0 +1,75 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
* (c) Rezo Zero / Ambroise Maupate
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\CommonMark\Extension\Footnote\Event;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\Footnote\Node\FootnoteRef;
use League\CommonMark\Reference\Reference;
final class NumberFootnotesListener
{
public function onDocumentParsed(DocumentParsedEvent $event): void
{
$document = $event->getDocument();
$nextCounter = 1;
$usedLabels = [];
$usedCounters = [];
foreach ($document->iterator() as $node) {
if (! $node instanceof FootnoteRef) {
continue;
}
$existingReference = $node->getReference();
$label = $existingReference->getLabel();
$counter = $nextCounter;
$canIncrementCounter = true;
if (\array_key_exists($label, $usedLabels)) {
/*
* Reference is used again, we need to point
* to the same footnote. But with a different ID
*/
$counter = $usedCounters[$label];
$label .= '__' . ++$usedLabels[$label];
$canIncrementCounter = false;
}
// rewrite reference title to use a numeric link
$newReference = new Reference(
$label,
$existingReference->getDestination(),
(string) $counter
);
// Override reference with numeric link
$node->setReference($newReference);
$document->getReferenceMap()->add($newReference);
/*
* Store created references in document for
* creating FootnoteBackrefs
*/
$document->data->append($existingReference->getDestination(), $newReference);
$usedLabels[$label] = 1;
$usedCounters[$label] = $nextCounter;
if ($canIncrementCounter) {
$nextCounter++;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More