diff --git a/docs/guide/syntax.md b/docs/guide/syntax.md index bc22957b..8110f343 100644 --- a/docs/guide/syntax.md +++ b/docs/guide/syntax.md @@ -575,6 +575,57 @@ With `nestedListsWithoutBlankLine` mode enabled, nested lists can appear immedia See the [Parser Options guide](/guide/parser-options#nested-lists-without-blank-line-mode) for more on `nestedListsWithoutBlankLine` mode. +#### Compact List Blocks + +::: warning djot-php divergence +By default djot-php keeps a list **tight** when an item carries a sub-block. +Canonical djot renders such a list **loose**. This changes only the tight/loose +rendering, never the block structure, so it does not affect the uniformity +principle — but the HTML differs from djot.js for these inputs. +::: + +In standard djot, a block inside a list item needs a blank line before it, and +that blank line makes the whole list **loose** (every item gets `

` wrappers). +So any list whose items carry a sub-block is forced loose. djot-php avoids this: +the blank line still **starts** the block, but no longer **loosens** the list +when the indented content opens a block (sub-list, block quote, fenced code, +fenced div, heading, table). The item stays tight, with its lead text inline. + +```djot +- Deploy the service + + > Needs the new env vars set first +- Run migrations +``` + +renders a **tight** list (lead text inline, the quote attached to its item): + +```html +

+``` + +::: tip Still loose, correctly +Only blank-then-*block* is affected. A genuine **second prose paragraph** in an +item, and a **blank line between items**, still make the list loose: + +```djot +- First point. + + A full second paragraph of explanation. +- Second point. +``` +::: + ### Definition Lists Terms are prefixed with `: ` and definitions are indented below. diff --git a/src/Parser/BlockParser.php b/src/Parser/BlockParser.php index d3b83a7f..c44ff91f 100644 --- a/src/Parser/BlockParser.php +++ b/src/Parser/BlockParser.php @@ -1903,13 +1903,21 @@ protected function tryParseList(Node $parent, array $lines, int $start): ?int // Content after blank line with indentation belongs to previous item $lastItem = $this->listParser->getLastListItem($list); if ($lastItem !== null) { - // Check if the first indented content is a list marker or regular text - // Blank line followed by indented TEXT = loose list (multiple paragraphs) - // Blank line followed by indented LIST MARKER = tight nesting + // A blank line before indented content does not loosen the list + // when that content OPENS A BLOCK (sub-list, block quote, fenced + // code, fenced div, heading, table, thematic break). Only a + // genuine second prose paragraph (blank + indented plain text) + // makes the list loose. This keeps items tight while still + // requiring the blank line djot needs to start the block, so + // block recognition and uniformity are unchanged -- only the + // tight/loose RENDERING differs from canonical djot. $trimmedCurrent = ltrim($currentLine); - $firstContentIsListMarker = $this->listParser->parseListItemMarker($trimmedCurrent) !== null; - if (!$firstContentIsListMarker) { - // Indented text after blank = loose list + $firstContentOpensBlock = + $this->listParser->parseListItemMarker($trimmedCurrent) !== null + || $this->startsNewBlockSignificant($trimmedCurrent); + if (!$firstContentOpensBlock) { + // Indented plain text after a blank line = a second + // paragraph in the item => loose list. $list->setTight(false); } diff --git a/tests/TestCase/CompactListBlocksTest.php b/tests/TestCase/CompactListBlocksTest.php new file mode 100644 index 00000000..a8459904 --- /dev/null +++ b/tests/TestCase/CompactListBlocksTest.php @@ -0,0 +1,77 @@ + ceremony). + * + * This is a deliberate divergence from canonical djot, which renders such lists + * loose; it changes only the tight/loose RENDERING, never the block structure. + */ +class CompactListBlocksTest extends TestCase +{ + protected DjotConverter $converter; + + protected function setUp(): void + { + $this->converter = new DjotConverter(); + $this->converter->getHtmlRenderer()->setSoftBreakMode(SoftBreakMode::Newline); + } + + public function testBlockquoteAfterBlankStaysTight(): void + { + $html = $this->converter->convert("- item\n\n > note\n- next"); + // Lead text inline (tight), not wrapped in

. + $this->assertStringContainsString("

  • \nitem\n
    ", $html); + $this->assertStringNotContainsString('

    item

    ', $html); + } + + public function testFencedCodeAfterBlankStaysTight(): void + { + $html = $this->converter->convert("- run\n\n ```\n make\n ```\n- next"); + $this->assertStringContainsString("
  • \nrun\n
    ", $html);
    +        $this->assertStringNotContainsString('

    run

    ', $html); + } + + public function testHeadingAfterBlankStaysTight(): void + { + $html = $this->converter->convert("- section\n\n # Title\n- next"); + $this->assertStringContainsString("
  • \nsection\n", $html); + $this->assertStringNotContainsString('

    section

    ', $html); + } + + public function testSublistAfterBlankStaysTight(): void + { + $html = $this->converter->convert("- parent\n\n - child\n- next"); + $this->assertStringNotContainsString('

    parent

    ', $html); + } + + public function testSecondProseParagraphStillLoosens(): void + { + // A real second paragraph in the item is still a loose list. + $html = $this->converter->convert("- item\n\n second paragraph\n- next"); + $this->assertStringContainsString('

    item

    ', $html); + $this->assertStringContainsString('

    second paragraph

    ', $html); + } + + public function testBlankLineBetweenItemsStillLoosens(): void + { + $html = $this->converter->convert("- a\n\n- b"); + $this->assertStringContainsString('

    a

    ', $html); + $this->assertStringContainsString('

    b

    ', $html); + } +} diff --git a/tests/TestCase/Converter/HtmlToDjotTest.php b/tests/TestCase/Converter/HtmlToDjotTest.php index 8babff03..c0708836 100644 --- a/tests/TestCase/Converter/HtmlToDjotTest.php +++ b/tests/TestCase/Converter/HtmlToDjotTest.php @@ -722,7 +722,9 @@ public function testListItemWithBlockquoteKeepsNestedBlockquote(): void $this->assertSame("- One\n\n > Quote\n", $result); $htmlBack = (new DjotConverter())->convert($result); - $this->assertStringContainsString("
  • \n

    One

    \n
    ", $htmlBack); + // Compact-list-blocks: a blank line before a sub-block no longer loosens + // the item, so the lead text renders inline (tight), not wrapped in

    . + $this->assertStringContainsString("

  • \nOne\n
    ", $htmlBack); $this->assertStringContainsString('

    Quote

    ', $htmlBack); }