Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/guide/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<p>` 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
<ul>
<li>
Deploy the service
<blockquote>
<p>Needs the new env vars set first</p>
</blockquote>
</li>
<li>
Run migrations
</li>
</ul>
```

::: 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.
Expand Down
20 changes: 14 additions & 6 deletions src/Parser/BlockParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
77 changes: 77 additions & 0 deletions tests/TestCase/CompactListBlocksTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace Djot\Test\TestCase;

use Djot\DjotConverter;
use Djot\Renderer\SoftBreakMode;
use PHPUnit\Framework\TestCase;

/**
* Compact list blocks (always on).
*
* A blank line is still required to START a block inside a list item (djot's
* block-recognition and the uniformity principle are unchanged). But that blank
* line no longer forces the list LOOSE when the indented content opens a block
* (sub-list, block quote, fenced code, fenced div, heading, table). Only a
* genuine second prose paragraph, or a blank line between items, makes the list
* loose. Result: an item can carry a sub-block while staying tight (lead text
* inline, no <p> 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 <p>.
$this->assertStringContainsString("<li>\nitem\n<blockquote>", $html);
$this->assertStringNotContainsString('<p>item</p>', $html);
}

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

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

public function testSublistAfterBlankStaysTight(): void
{
$html = $this->converter->convert("- parent\n\n - child\n- next");
$this->assertStringNotContainsString('<p>parent</p>', $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('<p>item</p>', $html);
$this->assertStringContainsString('<p>second paragraph</p>', $html);
}

public function testBlankLineBetweenItemsStillLoosens(): void
{
$html = $this->converter->convert("- a\n\n- b");
$this->assertStringContainsString('<p>a</p>', $html);
$this->assertStringContainsString('<p>b</p>', $html);
}
}
4 changes: 3 additions & 1 deletion tests/TestCase/Converter/HtmlToDjotTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,9 @@ public function testListItemWithBlockquoteKeepsNestedBlockquote(): void

$this->assertSame("- One\n\n > Quote\n", $result);
$htmlBack = (new DjotConverter())->convert($result);
$this->assertStringContainsString("<li>\n<p>One</p>\n<blockquote>", $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 <p>.
$this->assertStringContainsString("<li>\nOne\n<blockquote>", $htmlBack);
$this->assertStringContainsString('<p>Quote</p>', $htmlBack);
}

Expand Down
Loading