Skip to content

Commit

Permalink
Fix list tightness and ending line numbers
Browse files Browse the repository at this point in the history
We fix issues with list tightness by using a different strategy:

1. Compare the end and start lines of adjoining elements
   to check tightness. (This required tweaking the end
   position of some block types to exclude trailing blank
   lines.)

2. Delay removal of link reference definitions until the
   entire document is parsed.

See commonmark/commonmark.js@df3ea1e
  • Loading branch information
colinodell committed Feb 2, 2024
1 parent b84552e commit d710631
Show file tree
Hide file tree
Showing 11 changed files with 89 additions and 50 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi
- Remove restrictive limitation on inline comments
- Unicode symbols now treated like punctuation (for purposes of flankingness)
- Trailing tabs on the last line of indented code blocks will be excluded
- `Paragraph`s only containing link reference definitions will be kept in the AST until the `Document` is finalized
- (These were previously removed immediately after parsing the `Paragraph`)

### Fixed

- Fixed list tightness not being determined properly in some edge cases
- Fixed incorrect ending line numbers for several block types in various scenarios

## [2.4.2] - 2024-02-02

Expand Down
2 changes: 1 addition & 1 deletion src/Extension/CommonMark/Node/Block/ListBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,6 @@ public function closeBlock(): void
}

$this->block->setLiteral(\implode("\n", $lines) . "\n");
$this->block->setEndLine($this->block->getStartLine() + \count($lines) - 1);
}
}
59 changes: 34 additions & 25 deletions src/Extension/CommonMark/Parser/Block/ListBlockParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -48,32 +44,45 @@ public function isContainer(): bool

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
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): BlockContinue|null

Check failure on line 50 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / PHPStan

Method League\CommonMark\Extension\CommonMark\Parser\Block\ListBlockParser::tryContinue() uses native union types but they're supported only on PHP 8.0 and later.

Check failure on line 50 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / Psalm

ParseError

src/Extension/CommonMark/Parser/Block/ListBlockParser.php:50:99: ParseError: Union types are not supported in PHP < 8 (see https://psalm.dev/173)

Check failure on line 50 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / PHPStan

Method League\CommonMark\Extension\CommonMark\Parser\Block\ListBlockParser::tryContinue() uses native union types but they're supported only on PHP 8.0 and later.

Check failure on line 50 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / Psalm

ParseError

src/Extension/CommonMark/Parser/Block/ListBlockParser.php:50:99: ParseError: Union types are not supported in PHP < 8 (see https://psalm.dev/173)
{
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) {
// check for non-final list item ending with blank line:
if ($item->next() !== null && self::endsWithBlankLine($item)) {

Check failure on line 62 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / PHPStan

Parameter #1 $block of static method League\CommonMark\Extension\CommonMark\Parser\Block\ListBlockParser::endsWithBlankLine() expects League\CommonMark\Node\Block\AbstractBlock, League\CommonMark\Node\Node given.

Check failure on line 62 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / Psalm

ArgumentTypeCoercion

src/Extension/CommonMark/Parser/Block/ListBlockParser.php:62:67: ArgumentTypeCoercion: Argument 1 of League\CommonMark\Extension\CommonMark\Parser\Block\ListBlockParser::endsWithBlankLine expects League\CommonMark\Node\Block\AbstractBlock, but parent type League\CommonMark\Node\Node provided (see https://psalm.dev/193)

Check failure on line 62 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / PHPStan

Parameter #1 $block of static method League\CommonMark\Extension\CommonMark\Parser\Block\ListBlockParser::endsWithBlankLine() expects League\CommonMark\Node\Block\AbstractBlock, League\CommonMark\Node\Node given.

Check failure on line 62 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / Psalm

ArgumentTypeCoercion

src/Extension/CommonMark/Parser/Block/ListBlockParser.php:62:67: ArgumentTypeCoercion: Argument 1 of League\CommonMark\Extension\CommonMark\Parser\Block\ListBlockParser::endsWithBlankLine expects League\CommonMark\Node\Block\AbstractBlock, but parent type League\CommonMark\Node\Node provided (see https://psalm.dev/193)
$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) {
if ($subitem->next() && self::endsWithBlankLine($subitem)) {

Check failure on line 70 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / PHPStan

Parameter #1 $block of static method League\CommonMark\Extension\CommonMark\Parser\Block\ListBlockParser::endsWithBlankLine() expects League\CommonMark\Node\Block\AbstractBlock, League\CommonMark\Node\Node given.

Check failure on line 70 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / Psalm

ArgumentTypeCoercion

src/Extension/CommonMark/Parser/Block/ListBlockParser.php:70:65: ArgumentTypeCoercion: Argument 1 of League\CommonMark\Extension\CommonMark\Parser\Block\ListBlockParser::endsWithBlankLine expects League\CommonMark\Node\Block\AbstractBlock, but parent type League\CommonMark\Node\Node provided (see https://psalm.dev/193)

Check failure on line 70 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / PHPStan

Parameter #1 $block of static method League\CommonMark\Extension\CommonMark\Parser\Block\ListBlockParser::endsWithBlankLine() expects League\CommonMark\Node\Block\AbstractBlock, League\CommonMark\Node\Node given.

Check failure on line 70 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / Psalm

ArgumentTypeCoercion

src/Extension/CommonMark/Parser/Block/ListBlockParser.php:70:65: ArgumentTypeCoercion: Argument 1 of League\CommonMark\Extension\CommonMark\Parser\Block\ListBlockParser::endsWithBlankLine expects League\CommonMark\Node\Block\AbstractBlock, but parent type League\CommonMark\Node\Node provided (see https://psalm.dev/193)
$this->block->setTight(false);
break 2;
}

$subitem = $subitem->next();
}

$item = $item->next();
}

$this->block->setEndLine($this->block->lastChild()->getEndLine());

Check failure on line 81 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / PHPStan

Cannot call method getEndLine() on League\CommonMark\Node\Node|null.

Check failure on line 81 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / Psalm

PossiblyNullReference

src/Extension/CommonMark/Parser/Block/ListBlockParser.php:81:61: PossiblyNullReference: Cannot call method getEndLine on possibly null value (see https://psalm.dev/083)

Check failure on line 81 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Extension/CommonMark/Parser/Block/ListBlockParser.php:81:61: UndefinedMethod: Method League\CommonMark\Node\Node::getEndLine does not exist (see https://psalm.dev/022)

Check failure on line 81 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / PHPStan

Cannot call method getEndLine() on League\CommonMark\Node\Node|null.

Check failure on line 81 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / Psalm

PossiblyNullReference

src/Extension/CommonMark/Parser/Block/ListBlockParser.php:81:61: PossiblyNullReference: Cannot call method getEndLine on possibly null value (see https://psalm.dev/083)

Check failure on line 81 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Extension/CommonMark/Parser/Block/ListBlockParser.php:81:61: UndefinedMethod: Method League\CommonMark\Node\Node::getEndLine does not exist (see https://psalm.dev/022)
}

private static function endsWithBlankLine(AbstractBlock $block): bool
{
return $block->next() !== null && $block->getEndLine() !== $block->next()->getStartLine() - 1;

Check failure on line 86 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / PHPStan

Call to an undefined method League\CommonMark\Node\Node::getStartLine().

Check failure on line 86 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / Psalm

PossiblyNullReference

src/Extension/CommonMark/Parser/Block/ListBlockParser.php:86:84: PossiblyNullReference: Cannot call method getStartLine on possibly null value (see https://psalm.dev/083)

Check failure on line 86 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Extension/CommonMark/Parser/Block/ListBlockParser.php:86:84: UndefinedMethod: Method League\CommonMark\Node\Node::getStartLine does not exist (see https://psalm.dev/022)

Check failure on line 86 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / PHPStan

Call to an undefined method League\CommonMark\Node\Node::getStartLine().

Check failure on line 86 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / Psalm

PossiblyNullReference

src/Extension/CommonMark/Parser/Block/ListBlockParser.php:86:84: PossiblyNullReference: Cannot call method getStartLine on possibly null value (see https://psalm.dev/083)

Check failure on line 86 in src/Extension/CommonMark/Parser/Block/ListBlockParser.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Extension/CommonMark/Parser/Block/ListBlockParser.php:86:84: UndefinedMethod: Method League\CommonMark\Node\Node::getStartLine does not exist (see https://psalm.dev/022)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserSta
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);
Expand Down
30 changes: 11 additions & 19 deletions src/Extension/CommonMark/Parser/Block/ListItemParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@

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;
Expand All @@ -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);
Expand All @@ -47,18 +43,7 @@ public function isContainer(): bool

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
Expand All @@ -69,9 +54,6 @@ public function tryContinue(Cursor $cursor, BlockContinueParserInterface $active
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);
Expand All @@ -87,4 +69,14 @@ public function tryContinue(Cursor $cursor, BlockContinueParserInterface $active
// Note: We'll hit this case for lazy continuation lines, they will get added later.
return BlockContinue::none();
}

public function closeBlock(): void
{
if ($this->block->lastChild() !== null) {
$this->block->setEndLine($this->block->lastChild()->getEndLine());

Check failure on line 76 in src/Extension/CommonMark/Parser/Block/ListItemParser.php

View workflow job for this annotation

GitHub Actions / PHPStan

Call to an undefined method League\CommonMark\Node\Node::getEndLine().

Check failure on line 76 in src/Extension/CommonMark/Parser/Block/ListItemParser.php

View workflow job for this annotation

GitHub Actions / Psalm

PossiblyNullReference

src/Extension/CommonMark/Parser/Block/ListItemParser.php:76:65: PossiblyNullReference: Cannot call method getEndLine on possibly null value (see https://psalm.dev/083)

Check failure on line 76 in src/Extension/CommonMark/Parser/Block/ListItemParser.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Extension/CommonMark/Parser/Block/ListItemParser.php:76:65: UndefinedMethod: Method League\CommonMark\Node\Node::getEndLine does not exist (see https://psalm.dev/022)

Check failure on line 76 in src/Extension/CommonMark/Parser/Block/ListItemParser.php

View workflow job for this annotation

GitHub Actions / PHPStan

Call to an undefined method League\CommonMark\Node\Node::getEndLine().

Check failure on line 76 in src/Extension/CommonMark/Parser/Block/ListItemParser.php

View workflow job for this annotation

GitHub Actions / Psalm

PossiblyNullReference

src/Extension/CommonMark/Parser/Block/ListItemParser.php:76:65: PossiblyNullReference: Cannot call method getEndLine on possibly null value (see https://psalm.dev/083)

Check failure on line 76 in src/Extension/CommonMark/Parser/Block/ListItemParser.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedMethod

src/Extension/CommonMark/Parser/Block/ListItemParser.php:76:65: UndefinedMethod: Method League\CommonMark\Node\Node::getEndLine does not exist (see https://psalm.dev/022)
} else {
// Empty list item
$this->block->setEndLine($this->block->getStartLine());
}
}
}
2 changes: 2 additions & 0 deletions src/Node/Block/Paragraph.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@

class Paragraph extends AbstractBlock
{
/** @internal */
public bool $onlyContainsLinkReferenceDefinitions = false;
}
27 changes: 27 additions & 0 deletions src/Parser/Block/DocumentBlockParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Node\Block\Document;
use League\CommonMark\Node\Block\Paragraph;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Reference\ReferenceMapInterface;

Expand Down Expand Up @@ -50,4 +51,30 @@ public function tryContinue(Cursor $cursor, BlockContinueParserInterface $active
{
return BlockContinue::at($cursor);
}

public function closeBlock(): void
{
$this->removeLinkReferenceDefinitions();
}

private function removeLinkReferenceDefinitions(): void
{
$emptyNodes = [];

$walker = $this->document->walker();
while ($event = $walker->next()) {
$node = $event->getNode();
// TODO for v3: It would be great if we could find an alternate way to identify such paragraphs.
// Unfortunately, we can't simply check for empty paragraphs here because inlines haven't been processed yet,
// meaning all paragraphs will appear blank here, and we don't have a way to check the status of the reference parser
// which is attached to the (already-closed) paragraph parser.
if ($event->isEntering() && $node instanceof Paragraph && $node->onlyContainsLinkReferenceDefinitions) {
$emptyNodes[] = $node;
}
}

foreach ($emptyNodes as $node) {
$node->detach();
}
}
}
4 changes: 1 addition & 3 deletions src/Parser/Block/ParagraphParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,7 @@ public function addLine(string $line): void

public function closeBlock(): void
{
if ($this->referenceParser->hasReferences() && $this->referenceParser->getParagraphContent() === '') {
$this->block->detach();
}
$this->block->onlyContainsLinkReferenceDefinitions = $this->referenceParser->hasReferences() && $this->referenceParser->getParagraphContent() === '';
}

public function parseInlines(InlineParserEngineInterface $inlineParser): void
Expand Down
2 changes: 1 addition & 1 deletion src/Parser/MarkdownParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ private function parseLine(string $line): void
} else {
// finalize any blocks not matched
if ($unmatchedBlocks > 0) {
$this->closeBlockParsers($unmatchedBlocks, $this->lineNumber);
$this->closeBlockParsers($unmatchedBlocks, $this->lineNumber - 1);
}

if (! $blockParser->isContainer()) {
Expand Down
4 changes: 3 additions & 1 deletion tests/functional/CommonMarkJSRegressionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ public static function dataProvider(): \Generator
{
$tests = SpecReader::readFile(__DIR__ . '/../../vendor/commonmark/commonmark.js/test/regression.txt');
foreach ($tests as $example) {
// We can't currently render spec example 18 exactly how the upstream library does. We'll likely need to overhaul
// We can't currently render spec examples 18 or 24 exactly how the upstream library does. We'll likely need to overhaul
// our rendering approach in order to fix that, so we'll use this temporary workaround for now.
if ($example['number'] === 18) {
$example['output'] = \str_replace('</script></li>', "</script>\n</li>", $example['output']);
} elseif ($example['number'] === 24) {
$example['output'] = \str_replace("<pre>The following line is part of HTML block.\n\n</li>", "<pre>The following line is part of HTML block.\n</li>", $example['output']);
}

yield $example;
Expand Down

0 comments on commit d710631

Please sign in to comment.