updated streamline-setup v2

This commit is contained in:
2025-01-15 08:53:49 -08:00
committed by alec.turner
parent a2ce9248f0
commit 4b569f81b0
20228 changed files with 2932048 additions and 63204 deletions
@@ -23,7 +23,7 @@ use League\CommonMark\Util\RegexHelper;
*/
final class AttributesHelper
{
private const SINGLE_ATTRIBUTE = '\s*([.#][_a-z0-9-]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . ')\s*';
private const SINGLE_ATTRIBUTE = '\s*([.]-?[_a-z][^\s}]*|[#][^\s}]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . ')\s*';
private const ATTRIBUTE_LIST = '/^{:?(' . self::SINGLE_ATTRIBUTE . ')+}/i';
/**
@@ -75,6 +75,11 @@ final class AttributesHelper
/** @psalm-suppress PossiblyUndefinedArrayOffset */
[$name, $value] = \explode('=', $attribute, 2);
if ($value === 'true') {
$attributes[$name] = true;
continue;
}
$first = $value[0];
$last = \substr($value, -1);
if (($first === '"' && $last === '"') || ($first === "'" && $last === "'") && \strlen($value) > 1) {
@@ -14,13 +14,26 @@ declare(strict_types=1);
namespace League\CommonMark\Extension\Autolink;
use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Extension\ConfigurableExtensionInterface;
use League\Config\ConfigurationBuilderInterface;
use Nette\Schema\Expect;
final class AutolinkExtension implements ExtensionInterface
final class AutolinkExtension implements ConfigurableExtensionInterface
{
public function configureSchema(ConfigurationBuilderInterface $builder): void
{
$builder->addSchema('autolink', Expect::structure([
'allowed_protocols' => Expect::listOf('string')->default(['http', 'https', 'ftp'])->mergeDefaults(false),
'default_protocol' => Expect::string()->default('http'),
]));
}
public function register(EnvironmentBuilderInterface $environment): void
{
$environment->addInlineParser(new EmailAutolinkParser());
$environment->addInlineParser(new UrlAutolinkParser());
$environment->addInlineParser(new UrlAutolinkParser(
$environment->getConfiguration()->get('autolink.allowed_protocols'),
$environment->getConfiguration()->get('autolink.default_protocol'),
));
}
}
@@ -34,7 +34,7 @@ final class UrlAutolinkParser implements InlineParserInterface
(?:
(?:xn--[a-z0-9-]++\.)*+xn--[a-z0-9-]++ # a domain name using punycode
|
(?:[\pL\pN\pS\pM\-\_]++\.)+[\pL\pN\pM]++ # a multi-level domain name
(?:[\pL\pN\pS\pM\-\_]++\.){1,127}[\pL\pN\pM]++ # a multi-level domain name; total length must be 253 bytes or less
|
[a-z0-9\-\_]++ # a single-level domain name
)\.?
@@ -56,7 +56,7 @@ final class UrlAutolinkParser implements InlineParserInterface
*
* @psalm-readonly
*/
private array $prefixes = ['www'];
private array $prefixes = ['www.'];
/**
* @psalm-var non-empty-string
@@ -65,10 +65,12 @@ final class UrlAutolinkParser implements InlineParserInterface
*/
private string $finalRegex;
private string $defaultProtocol;
/**
* @param array<int, string> $allowedProtocols
*/
public function __construct(array $allowedProtocols = ['http', 'https', 'ftp'])
public function __construct(array $allowedProtocols = ['http', 'https', 'ftp'], string $defaultProtocol = 'http')
{
/**
* @psalm-suppress PropertyTypeCoercion
@@ -78,6 +80,8 @@ final class UrlAutolinkParser implements InlineParserInterface
foreach ($allowedProtocols as $protocol) {
$this->prefixes[] = $protocol . '://';
}
$this->defaultProtocol = $defaultProtocol;
}
public function getMatchDefinition(): InlineParserMatch
@@ -120,9 +124,9 @@ final class UrlAutolinkParser implements InlineParserInterface
$cursor->advanceBy(\mb_strlen($url, 'UTF-8'));
// Auto-prefix 'http://' onto 'www' URLs
// Auto-prefix 'http(s)://' onto 'www' URLs
if (\substr($url, 0, 4) === 'www.') {
$inlineContext->getContainer()->appendChild(new Link('http://' . $url, $url));
$inlineContext->getContainer()->appendChild(new Link($this->defaultProtocol . '://' . $url, $url));
return true;
}
@@ -20,14 +20,14 @@ declare(strict_types=1);
namespace League\CommonMark\Extension\CommonMark\Delimiter\Processor;
use League\CommonMark\Delimiter\DelimiterInterface;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
use League\CommonMark\Delimiter\Processor\CacheableDelimiterProcessorInterface;
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
final class EmphasisDelimiterProcessor implements CacheableDelimiterProcessorInterface, ConfigurationAwareInterface
{
/** @psalm-readonly */
private string $char;
@@ -105,4 +105,15 @@ final class EmphasisDelimiterProcessor implements DelimiterProcessorInterface, C
{
$this->config = $configuration;
}
public function getCacheKey(DelimiterInterface $closer): string
{
return \sprintf(
'%s-%s-%d-%d',
$this->char,
$closer->canOpen() ? 'canOpen' : 'cannotOpen',
$closer->getOriginalLength() % 3,
$closer->getLength(),
);
}
}
@@ -27,7 +27,7 @@ class ListBlock extends AbstractBlock implements TightBlockInterface
public const DELIM_PERIOD = 'period';
public const DELIM_PAREN = 'paren';
protected bool $tight = false;
protected bool $tight = false; // TODO Make lists tight by default in v3
/** @psalm-readonly */
protected ListData $listData;
@@ -44,7 +44,7 @@ final class FencedCodeParser extends AbstractBlockContinueParser
{
// Check for closing code fence
if (! $cursor->isIndented() && $cursor->getNextNonSpaceCharacter() === $this->block->getChar()) {
$match = RegexHelper::matchFirst('/^(?:`{3,}|~{3,})(?= *$)/', $cursor->getLine(), $cursor->getNextNonSpacePosition());
$match = RegexHelper::matchFirst('/^(?:`{3,}|~{3,})(?=[ \t]*$)/', $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();
@@ -63,21 +63,14 @@ final class IndentedCodeParser extends AbstractBlockContinueParser
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;
}
$lines = $this->strings->toArray();
unset($reversed[$index]);
// Note that indented code block cannot be empty, so $lines will always have at least one non-empty element
while (\preg_match('/^[ \t]*$/', \end($lines))) { // @phpstan-ignore-line
\array_pop($lines);
}
$fixed = \array_reverse($reversed);
$tmp = \implode("\n", $fixed);
if (\substr($tmp, -1) !== "\n") {
$tmp .= "\n";
}
$this->block->setLiteral($tmp);
$this->block->setLiteral(\implode("\n", $lines) . "\n");
$this->block->setEndLine($this->block->getStartLine() + \count($lines) - 1);
}
}
@@ -27,10 +27,6 @@ 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);
@@ -48,32 +44,50 @@ final class ListBlockParser extends AbstractBlockContinueParser
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;
return $childBlock instanceof ListItem;
}
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);
}
public function closeBlock(): void
{
$item = $this->block->firstChild();
while ($item instanceof AbstractBlock) {
// check for non-final list item ending with blank line:
if ($item->next() !== null && self::endsWithBlankLine($item)) {
$this->block->setTight(false);
break;
}
// recurse into children of list item, to see if there are spaces between any of them
$subitem = $item->firstChild();
while ($subitem instanceof AbstractBlock) {
if ($subitem->next() && self::endsWithBlankLine($subitem)) {
$this->block->setTight(false);
break 2;
}
$subitem = $subitem->next();
}
$item = $item->next();
}
$lastChild = $this->block->lastChild();
if ($lastChild instanceof AbstractBlock) {
$this->block->setEndLine($lastChild->getEndLine());
}
}
private static function endsWithBlankLine(AbstractBlock $block): bool
{
$next = $block->next();
return $next instanceof AbstractBlock && $block->getEndLine() !== $next->getStartLine() - 1;
}
}
@@ -58,6 +58,7 @@ final class ListBlockStartParser implements BlockStartParserInterface, Configura
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.
// TODO for 3.0: Just make them tight by default in the block so we can remove this call
$listBlockParser->getBlock()->setTight(true);
return BlockStart::of($listBlockParser, $listItemParser)->at($cursor);
@@ -13,11 +13,9 @@ declare(strict_types=1);
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;
@@ -28,8 +26,6 @@ final class ListItemParser extends AbstractBlockContinueParser
/** @psalm-readonly */
private ListItem $block;
private bool $hadBlankLine = false;
public function __construct(ListData $listData)
{
$this->block = new ListItem($listData);
@@ -47,18 +43,7 @@ final class ListItemParser extends AbstractBlockContinueParser
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;
return ! $childBlock instanceof ListItem;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
@@ -69,9 +54,6 @@ final class ListItemParser extends AbstractBlockContinueParser
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);
@@ -87,4 +69,14 @@ final class ListItemParser extends AbstractBlockContinueParser
// Note: We'll hit this case for lazy continuation lines, they will get added later.
return BlockContinue::none();
}
public function closeBlock(): void
{
if (($lastChild = $this->block->lastChild()) instanceof AbstractBlock) {
$this->block->setEndLine($lastChild->getEndLine());
} else {
// Empty list item
$this->block->setEndLine($this->block->getStartLine());
}
}
}
@@ -18,12 +18,27 @@ 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\Cursor;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
final class BacktickParser implements InlineParserInterface
{
/**
* Max bound for backtick code span delimiters.
*
* @see https://github.com/commonmark/cmark/commit/8ed5c9d
*/
private const MAX_BACKTICKS = 1000;
/** @var \WeakReference<Cursor>|null */
private ?\WeakReference $lastCursor = null;
private bool $lastCursorScanned = false;
/** @var array<int, int> backtick count => position of known ender */
private array $seenBackticks = [];
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::regex('`+');
@@ -38,11 +53,7 @@ final class BacktickParser implements InlineParserInterface
$currentPosition = $cursor->getPosition();
$previousState = $cursor->saveState();
while ($matchingTicks = $cursor->match('/`+/m')) {
if ($matchingTicks !== $ticks) {
continue;
}
if ($this->findMatchingTicks(\strlen($ticks), $cursor)) {
$code = $cursor->getSubstring($currentPosition, $cursor->getPosition() - $currentPosition - \strlen($ticks));
$c = \preg_replace('/\n/m', ' ', $code) ?? '';
@@ -67,4 +78,55 @@ final class BacktickParser implements InlineParserInterface
return true;
}
/**
* Locates the matching closer for a backtick code span.
*
* Leverages some caching to avoid traversing the same cursor multiple times when
* we've already seen all the potential backtick closers.
*
* @see https://github.com/commonmark/cmark/commit/8ed5c9d
*
* @param int $openTickLength Number of backticks in the opening sequence
* @param Cursor $cursor Cursor to scan
*
* @return bool True if a matching closer was found, false otherwise
*/
private function findMatchingTicks(int $openTickLength, Cursor $cursor): bool
{
// Reset the seenBackticks cache if this is a new cursor
if ($this->lastCursor === null || $this->lastCursor->get() !== $cursor) {
$this->seenBackticks = [];
$this->lastCursor = \WeakReference::create($cursor);
$this->lastCursorScanned = false;
}
if ($openTickLength > self::MAX_BACKTICKS) {
return false;
}
// Return if we already know there's no closer
if ($this->lastCursorScanned && isset($this->seenBackticks[$openTickLength]) && $this->seenBackticks[$openTickLength] <= $cursor->getPosition()) {
return false;
}
while ($ticks = $cursor->match('/`{1,' . self::MAX_BACKTICKS . '}/m')) {
$numTicks = \strlen($ticks);
// Did we find the closer?
if ($numTicks === $openTickLength) {
return true;
}
// Store position of closer
if ($numTicks <= self::MAX_BACKTICKS) {
$this->seenBackticks[$numTicks] = $cursor->getPosition() - $numTicks;
}
}
// Got through whole input without finding closer
$this->lastCursorScanned = true;
return false;
}
}
@@ -16,7 +16,6 @@ declare(strict_types=1);
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;
@@ -38,8 +37,7 @@ final class BangParser implements InlineParserInterface
$inlineContext->getContainer()->appendChild($node);
// Add entry to stack for this opener
$delimiter = new Delimiter('!', 1, $node, true, false, $cursor->getPosition());
$inlineContext->getDelimiterStack()->push($delimiter);
$inlineContext->getDelimiterStack()->addBracket($node, $cursor->getPosition(), true);
return true;
}
@@ -16,6 +16,7 @@ declare(strict_types=1);
namespace League\CommonMark\Extension\CommonMark\Parser\Inline;
use League\CommonMark\Delimiter\Bracket;
use League\CommonMark\Environment\EnvironmentAwareInterface;
use League\CommonMark\Environment\EnvironmentInterface;
use League\CommonMark\Extension\CommonMark\Node\Inline\AbstractWebResource;
@@ -46,14 +47,14 @@ final class CloseBracketParser implements InlineParserInterface, EnvironmentAwar
public function parse(InlineParserContext $inlineContext): bool
{
// Look through stack of delimiters for a [ or !
$opener = $inlineContext->getDelimiterStack()->searchByCharacter(['[', '!']);
$opener = $inlineContext->getDelimiterStack()->getLastBracket();
if ($opener === null) {
return false;
}
if (! $opener->isActive()) {
// no matched opener; remove from emphasis stack
$inlineContext->getDelimiterStack()->removeDelimiter($opener);
if (! $opener->isImage() && ! $opener->isActive()) {
// no matched opener; remove from stack
$inlineContext->getDelimiterStack()->removeBracket();
return false;
}
@@ -70,21 +71,19 @@ final class CloseBracketParser implements InlineParserInterface, EnvironmentAwar
// Inline link?
if ($result = $this->tryParseInlineLinkAndTitle($cursor)) {
$link = $result;
} elseif ($link = $this->tryParseReference($cursor, $inlineContext->getReferenceMap(), $opener->getIndex(), $startPos)) {
} elseif ($link = $this->tryParseReference($cursor, $inlineContext->getReferenceMap(), $opener, $startPos)) {
$reference = $link;
$link = ['url' => $link->getDestination(), 'title' => $link->getTitle()];
} else {
// No match
$inlineContext->getDelimiterStack()->removeDelimiter($opener); // Remove this opener from stack
// No match; remove this opener from stack
$inlineContext->getDelimiterStack()->removeBracket();
$cursor->restoreState($previousState);
return false;
}
$isImage = $opener->getChar() === '!';
$inline = $this->createInline($link['url'], $link['title'], $isImage, $reference ?? null);
$opener->getInlineNode()->replaceWith($inline);
$inline = $this->createInline($link['url'], $link['title'], $opener->isImage(), $reference ?? null);
$opener->getNode()->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.
@@ -104,8 +103,9 @@ final class CloseBracketParser implements InlineParserInterface, EnvironmentAwar
// Process delimiters such as emphasis inside link/image
$delimiterStack = $inlineContext->getDelimiterStack();
$stackBottom = $opener->getPrevious();
$stackBottom = $opener->getPosition();
$delimiterStack->processDelimiters($stackBottom, $this->environment->getDelimiterProcessors());
$delimiterStack->removeBracket();
$delimiterStack->removeAll($stackBottom);
// Merge any adjacent Text nodes together
@@ -113,8 +113,8 @@ final class CloseBracketParser implements InlineParserInterface, EnvironmentAwar
// 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('[');
if (! $opener->isImage()) {
$inlineContext->getDelimiterStack()->deactivateLinkOpeners();
}
return true;
@@ -168,21 +168,23 @@ final class CloseBracketParser implements InlineParserInterface, EnvironmentAwar
return ['url' => $dest, 'title' => $title];
}
private function tryParseReference(Cursor $cursor, ReferenceMapInterface $referenceMap, ?int $openerIndex, int $startPos): ?ReferenceInterface
private function tryParseReference(Cursor $cursor, ReferenceMapInterface $referenceMap, Bracket $opener, 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 {
if ($n > 2) {
$start = $beforeLabel + 1;
$length = $n - 2;
} elseif (! $opener->hasNext()) {
// Empty or missing second label means to use the first label as the reference.
// The reference must not contain a bracket. If we know there's a bracket, we don't even bother checking it.
$start = $opener->getPosition();
$length = $startPos - $start;
} else {
$cursor->restoreState($savePos);
return null;
}
$referenceLabel = $cursor->getSubstring($start, $length);
@@ -16,7 +16,6 @@ declare(strict_types=1);
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;
@@ -36,8 +35,7 @@ final class OpenBracketParser implements InlineParserInterface
$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);
$inlineContext->getDelimiterStack()->addBracket($node, $inlineContext->getCursor()->getPosition(), false);
return true;
}
@@ -17,8 +17,9 @@ declare(strict_types=1);
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\AbstractBlock;
use League\CommonMark\Node\Block\Paragraph;
use League\CommonMark\Node\Block\TightBlockInterface;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
@@ -39,11 +40,14 @@ final class ListItemRenderer implements NodeRendererInterface, XmlNodeRendererIn
ListItem::assertInstanceOf($node);
$contents = $childRenderer->renderNodes($node->children());
if (\substr($contents, 0, 1) === '<' && ! $this->startsTaskListItem($node)) {
$inTightList = ($parent = $node->parent()) && $parent instanceof TightBlockInterface && $parent->isTight();
if ($this->needsBlockSeparator($node->firstChild(), $inTightList)) {
$contents = "\n" . $contents;
}
if (\substr($contents, -1, 1) === '>') {
if ($this->needsBlockSeparator($node->lastChild(), $inTightList)) {
$contents .= "\n";
}
@@ -65,10 +69,12 @@ final class ListItemRenderer implements NodeRendererInterface, XmlNodeRendererIn
return [];
}
private function startsTaskListItem(ListItem $block): bool
private function needsBlockSeparator(?Node $child, bool $inTightList): bool
{
$firstChild = $block->firstChild();
if ($child instanceof Paragraph && $inTightList) {
return false;
}
return $firstChild instanceof Paragraph && $firstChild->firstChild() instanceof TaskListItemMarker;
return $child instanceof AbstractBlock;
}
}
@@ -24,12 +24,19 @@ use League\CommonMark\Util\RegexHelper;
final class QuoteParser implements InlineParserInterface
{
/**
* @deprecated This constant is no longer used and will be removed in a future major release
*/
public const DOUBLE_QUOTES = [Quote::DOUBLE_QUOTE, Quote::DOUBLE_QUOTE_OPENER, Quote::DOUBLE_QUOTE_CLOSER];
/**
* @deprecated This constant is no longer used and will be removed in a future major release
*/
public const SINGLE_QUOTES = [Quote::SINGLE_QUOTE, Quote::SINGLE_QUOTE_OPENER, Quote::SINGLE_QUOTE_CLOSER];
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::oneOf(...[...self::DOUBLE_QUOTES, ...self::SINGLE_QUOTES]);
return InlineParserMatch::oneOf(Quote::SINGLE_QUOTE, Quote::DOUBLE_QUOTE);
}
/**
@@ -39,8 +46,7 @@ final class QuoteParser implements InlineParserInterface
{
$char = $inlineContext->getFullMatch();
$cursor = $inlineContext->getCursor();
$normalizedCharacter = $this->getNormalizedQuoteCharacter($char);
$index = $cursor->getPosition();
$charBefore = $cursor->peek(-1);
if ($charBefore === null) {
@@ -58,28 +64,15 @@ final class QuoteParser implements InlineParserInterface
$canOpen = $leftFlanking && ! $rightFlanking;
$canClose = $rightFlanking;
$node = new Quote($normalizedCharacter, ['delim' => true]);
$node = new Quote($char, ['delim' => true]);
$inlineContext->getContainer()->appendChild($node);
// Add entry to stack to this opener
$inlineContext->getDelimiterStack()->push(new Delimiter($normalizedCharacter, 1, $node, $canOpen, $canClose));
$inlineContext->getDelimiterStack()->push(new Delimiter($char, 1, $node, $canOpen, $canClose, $index));
return true;
}
private function getNormalizedQuoteCharacter(string $character): string
{
if (\in_array($character, self::DOUBLE_QUOTES, true)) {
return Quote::DOUBLE_QUOTE;
}
if (\in_array($character, self::SINGLE_QUOTES, true)) {
return Quote::SINGLE_QUOTE;
}
return $character;
}
/**
* @return bool[]
*/
@@ -14,10 +14,10 @@ declare(strict_types=1);
namespace League\CommonMark\Extension\Strikethrough;
use League\CommonMark\Delimiter\DelimiterInterface;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
use League\CommonMark\Delimiter\Processor\CacheableDelimiterProcessorInterface;
use League\CommonMark\Node\Inline\AbstractStringContainer;
final class StrikethroughDelimiterProcessor implements DelimiterProcessorInterface
final class StrikethroughDelimiterProcessor implements CacheableDelimiterProcessorInterface
{
public function getOpeningCharacter(): string
{
@@ -44,7 +44,8 @@ final class StrikethroughDelimiterProcessor implements DelimiterProcessorInterfa
return 0;
}
return \min($opener->getLength(), $closer->getLength());
// $opener and $closer are the same length so we just return one of them
return $opener->getLength();
}
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void
@@ -60,4 +61,9 @@ final class StrikethroughDelimiterProcessor implements DelimiterProcessorInterfa
$opener->insertAfter($strikethrough);
}
public function getCacheKey(DelimiterInterface $closer): string
{
return '~' . $closer->getLength();
}
}
@@ -41,6 +41,7 @@ final class TableExtension implements ConfigurableExtensionInterface
'center' => (clone $attributeArraySchema)->default(['align' => 'center']),
'right' => (clone $attributeArraySchema)->default(['align' => 'right']),
]),
'max_autocompleted_cells' => Expect::int()->min(0)->default(TableParser::DEFAULT_MAX_AUTOCOMPLETED_CELLS),
]));
}
@@ -52,7 +53,7 @@ final class TableExtension implements ConfigurableExtensionInterface
}
$environment
->addBlockStartParser(new TableStartParser())
->addBlockStartParser(new TableStartParser($environment->getConfiguration()->get('table/max_autocompleted_cells')))
->addRenderer(Table::class, $tableRenderer)
->addRenderer(TableSection::class, new TableSectionRenderer())
@@ -25,6 +25,11 @@ use League\CommonMark\Util\ArrayCollection;
final class TableParser extends AbstractBlockContinueParser implements BlockContinueParserWithInlinesInterface
{
/**
* @internal
*/
public const DEFAULT_MAX_AUTOCOMPLETED_CELLS = 10_000;
/** @psalm-readonly */
private Table $block;
@@ -54,6 +59,8 @@ final class TableParser extends AbstractBlockContinueParser implements BlockCont
/** @psalm-readonly-allow-private-mutation */
private bool $nextIsSeparatorLine = true;
private int $remainingAutocompletedCells;
/**
* @param array<int, string|null> $columns
* @param array<int, string> $headerCells
@@ -62,12 +69,13 @@ final class TableParser extends AbstractBlockContinueParser implements BlockCont
*
* @phpstan-param array<int, TableCell::ALIGN_*|null> $columns
*/
public function __construct(array $columns, array $headerCells)
public function __construct(array $columns, array $headerCells, int $remainingAutocompletedCells = self::DEFAULT_MAX_AUTOCOMPLETED_CELLS)
{
$this->block = new Table();
$this->bodyLines = new ArrayCollection();
$this->columns = $columns;
$this->headerCells = $headerCells;
$this->block = new Table();
$this->bodyLines = new ArrayCollection();
$this->columns = $columns;
$this->headerCells = $headerCells;
$this->remainingAutocompletedCells = $remainingAutocompletedCells;
}
public function canHaveLazyContinuationLines(): bool
@@ -121,6 +129,12 @@ final class TableParser extends AbstractBlockContinueParser implements BlockCont
// Body can not have more columns than head
for ($i = 0; $i < $headerColumns; $i++) {
// It can have less columns though, in which case we'll autocomplete the empty ones (up to some limit)
if (! isset($cells[$i]) && $this->remainingAutocompletedCells-- <= 0) {
// Too many cells were auto-completed, so we'll just stop here
return;
}
$cell = $cells[$i] ?? '';
$tableCell = $this->parseCell($cell, $i, $inlineParser);
$row->appendChild($tableCell);
@@ -138,14 +152,12 @@ final class TableParser extends AbstractBlockContinueParser implements BlockCont
private function parseCell(string $cell, int $column, InlineParserEngineInterface $inlineParser): TableCell
{
$tableCell = new TableCell();
$tableCell = new TableCell(TableCell::TYPE_DATA, $this->columns[$column] ?? null);
if ($column < \count($this->columns)) {
$tableCell->setAlign($this->columns[$column]);
if ($cell !== '') {
$inlineParser->parse(\trim($cell), $tableCell);
}
$inlineParser->parse(\trim($cell), $tableCell);
return $tableCell;
}
@@ -23,6 +23,13 @@ use League\CommonMark\Parser\MarkdownParserStateInterface;
final class TableStartParser implements BlockStartParserInterface
{
private int $maxAutocompletedCells;
public function __construct(int $maxAutocompletedCells = TableParser::DEFAULT_MAX_AUTOCOMPLETED_CELLS)
{
$this->maxAutocompletedCells = $maxAutocompletedCells;
}
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
$paragraph = $parserState->getParagraphContent();
@@ -35,8 +42,8 @@ final class TableStartParser implements BlockStartParserInterface
return BlockStart::none();
}
$lines = \explode("\n", $paragraph);
$lastLine = \array_pop($lines);
$lastLineBreak = \strrpos($paragraph, "\n");
$lastLine = $lastLineBreak === false ? $paragraph : \substr($paragraph, $lastLineBreak + 1);
$headerCells = TableParser::split($lastLine);
if (\count($headerCells) > \count($columns)) {
@@ -47,13 +54,13 @@ final class TableStartParser implements BlockStartParserInterface
$parsers = [];
if (\count($lines) > 0) {
if ($lastLineBreak !== false) {
$p = new ParagraphParser();
$p->addLine(\implode("\n", $lines));
$p->addLine(\substr($paragraph, 0, $lastLineBreak));
$parsers[] = $p;
}
$parsers[] = new TableParser($columns, $headerCells);
$parsers[] = new TableParser($columns, $headerCells, $this->maxAutocompletedCells);
return BlockStart::of(...$parsers)
->at($cursor)