Skip to content

Commit 72200ec

Browse files
authored
Add nestedListsWithoutBlankLine lever; make blocksInterruptParagraphs interrupt consistently in lists (#207)
* feat: add nestedListsWithoutBlankLine flag to BlockParser * feat: granular list nesting via allowsImmediateNestedBlock gate * feat: add withNestedListsWithoutBlankLine converter factory * docs: clarify DjotConverter docblocks for nestedListsWithoutBlankLine * test: composition and regression guards for granular list nesting * docs: document nestedListsWithoutBlankLine and expanded blocksInterruptParagraphs * docs: lead optional-modes docs with granular list-nesting levers * docs: make nestedBlocksInLists deprecation coherent; track lone-marker todo * fix: apply lone-marker lookahead to in-list interruption; define significantNewlines via granular levers * docs: align parser-options/api with lone-marker lookahead and significantNewlines rewiring
1 parent a46d0a6 commit 72200ec

10 files changed

Lines changed: 557 additions & 74 deletions

docs/guide/parser-options.md

Lines changed: 113 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ $converter = new DjotConverter();
1616
$converter = new DjotConverter(
1717
xhtml: true,
1818
blocksInterruptParagraphs: true,
19-
nestedBlocksInLists: true,
19+
nestedListsWithoutBlankLine: true,
2020
);
2121
```
2222

@@ -36,7 +36,7 @@ use Djot\Renderer\HtmlRenderer;
3636

3737
// Full control via create()
3838
$converter = DjotConverter::create(
39-
new BlockParser(blocksInterruptParagraphs: true, nestedBlocksInLists: true),
39+
new BlockParser(blocksInterruptParagraphs: true, nestedListsWithoutBlankLine: true),
4040
new HtmlRenderer(xhtml: true),
4141
);
4242
```
@@ -105,7 +105,7 @@ Note: Use `\` at end of line for hard breaks (always renders as `<br>`) regardle
105105

106106
::: warning Deprecated
107107
`significantNewlines` is now a convenience shorthand equal to enabling both
108-
`blocksInterruptParagraphs` and `nestedBlocksInLists` together. It is
108+
`blocksInterruptParagraphs` and `nestedListsWithoutBlankLine` together. It is
109109
deprecated in favor of the two granular levers. Migrate as shown below.
110110

111111
```php
@@ -115,7 +115,7 @@ $converter = DjotConverter::withSignificantNewlines();
115115
// Equivalent (preferred):
116116
$converter = new DjotConverter(
117117
blocksInterruptParagraphs: true,
118-
nestedBlocksInLists: true,
118+
nestedListsWithoutBlankLine: true,
119119
);
120120
```
121121
:::
@@ -282,7 +282,7 @@ use Djot\Renderer\SoftBreakMode;
282282
// Significant newlines with safe mode for user-generated content
283283
$converter = new DjotConverter(
284284
safeMode: new SafeMode(),
285-
significantNewlines: true, // deprecated: prefer blocksInterruptParagraphs + nestedBlocksInLists
285+
significantNewlines: true, // deprecated: prefer blocksInterruptParagraphs + nestedListsWithoutBlankLine
286286
softBreakMode: SoftBreakMode::Break, // Optional: visible line breaks
287287
);
288288
```
@@ -361,13 +361,13 @@ Output:
361361
| Nested blocks in list items without a blank line | No | **Yes** | Yes |
362362
| Block elements interrupt top-level paragraphs | No | No | Yes |
363363

364-
Note: `significantNewlines` is deprecated; it enables both `blocksInterruptParagraphs` and `nestedBlocksInLists`. Prefer setting the two granular levers directly.
364+
Note: `significantNewlines` is deprecated; it enables both `blocksInterruptParagraphs` and `nestedListsWithoutBlankLine`. Prefer setting the two granular levers directly. (`nestedBlocksInLists` is a separate, also-deprecated broad lever - it is no longer what `significantNewlines` sets.)
365365

366366
## Block Interrupts Paragraphs Mode
367367

368-
`blocksInterruptParagraphs` is the complementary counterpart to [Nested Blocks in Lists](#nested-blocks-in-lists-mode) mode. It allows top-level block elements lists, blockquotes, headings, tables, thematic breaks, and code/div/comment fences to interrupt a paragraph without a preceding blank line. It does **not** enable nesting inside list items (that is `nestedBlocksInLists`).
368+
`blocksInterruptParagraphs` allows top-level block elements - lists, blockquotes, headings, tables, thematic breaks, and code/div/comment fences - to interrupt a paragraph without a preceding blank line. It **also** interrupts a list item's lead paragraph, so an indented non-list block (blockquote, heading, fenced code, or thematic break) nests inside the open item without a blank line. It does **not** nest a sublist inside a list item - that requires [Nested Lists Without Blank Line](#nested-lists-without-blank-line-mode) mode.
369369

370-
Use it when you want markdown-like top-level interruption but otherwise spec-compliant djot: indented content inside a list item still requires a blank line.
370+
Use it when you want markdown-like top-level interruption and non-list nesting in list items, but otherwise spec-compliant djot: a sublist under a list item still requires a blank line.
371371

372372
### Enabling Block Interrupts Paragraphs Mode
373373

@@ -408,19 +408,41 @@ two
408408
</ul>
409409
```
410410

411-
But indented content inside a list item does **not** nest (this is what differs from significant newlines mode):
411+
A non-list block also nests inside a list item without a blank line. For example, a blockquote under the item:
412412

413413
```php
414414
$converter = DjotConverter::withBlocksInterruptParagraphs();
415-
$result = $converter->convert("- a\n - b");
415+
$result = $converter->convert("- Item\n > a\n > b");
416416
```
417417

418418
Output:
419419
```html
420420
<ul>
421421
<li>
422-
a
423-
- b
422+
Item
423+
<blockquote>
424+
<p>a
425+
b</p>
426+
</blockquote>
427+
</li>
428+
</ul>
429+
```
430+
431+
In-list interruption uses the **same** lone-marker lookahead as the top-level path: an ambiguous single-line marker (`>` comparison, `|`, a lone bullet) stays literal, while unambiguous openers (`#`, fenced code, `:::`, `---`) and real multi-line blocks nest. So `- Item\n > quote` (a single `>` line) keeps `> quote` as literal text, exactly as `Foo\n> quote` does at the top level.
432+
433+
A sublist does **not** nest with this flag alone (that requires [Nested Lists Without Blank Line](#nested-lists-without-blank-line-mode) mode):
434+
435+
```php
436+
$converter = DjotConverter::withBlocksInterruptParagraphs();
437+
$result = $converter->convert("- Item\n - sub");
438+
```
439+
440+
Output:
441+
```html
442+
<ul>
443+
<li>
444+
Item
445+
- sub
424446
</li>
425447
</ul>
426448
```
@@ -430,6 +452,83 @@ a
430452
| Behavior | Default | `blocksInterruptParagraphs` | `significantNewlines` |
431453
|----------------------------------------------------------------|---------|-----------------------------|-----------------------|
432454
| Block elements interrupt top-level paragraphs | No | **Yes** | Yes |
433-
| Nested blocks in list items without a blank line | No | No | Yes |
455+
| Non-list block nests in list item without a blank line | No | **Yes** | Yes |
456+
| Sublist nests in list item without a blank line | No | No | Yes |
457+
458+
The two granular levers are independent. `blocksInterruptParagraphs` alone does not nest sublists; `nestedListsWithoutBlankLine` alone does not interrupt top-level paragraphs or nest non-list blocks. `significantNewlines` enables both simultaneously.
459+
460+
## Nested Lists Without Blank Line Mode
461+
462+
`nestedListsWithoutBlankLine` lets a sublist nest inside a list item without a blank line, while leaving everything else at the spec default. It nests **only** sublists: a non-list block (blockquote, heading, thematic break) under the item stays literal, and top-level paragraphs are **not** interrupted.
463+
464+
Use it when you want markdown-like nested lists but otherwise spec-compliant djot: top-level paragraphs still require a blank line before any block, and only sublists nest in list items.
465+
466+
### Enabling Nested Lists Without Blank Line Mode
467+
468+
```php
469+
use Djot\DjotConverter;
470+
use Djot\Parser\BlockParser;
471+
472+
// Method 1: Factory method
473+
$converter = DjotConverter::withNestedListsWithoutBlankLine();
474+
475+
// Method 2: Constructor parameter
476+
$converter = new DjotConverter(nestedListsWithoutBlankLine: true);
477+
478+
// Method 3: Directly on the parser
479+
$parser = new BlockParser(nestedListsWithoutBlankLine: true);
480+
$parser->setNestedListsWithoutBlankLine(true);
481+
```
482+
483+
### Behavior
484+
485+
A sublist nests inside the open list item, even without a blank line:
486+
487+
```php
488+
$converter = DjotConverter::withNestedListsWithoutBlankLine();
489+
$result = $converter->convert("- Item\n - Nested one\n - Nested two");
490+
```
491+
492+
Output:
493+
```html
494+
<ul>
495+
<li>
496+
Item
497+
<ul>
498+
<li>
499+
Nested one
500+
</li>
501+
<li>
502+
Nested two
503+
</li>
504+
</ul>
505+
</li>
506+
</ul>
507+
```
508+
509+
But a non-list block (such as a blockquote) under the item stays literal - it does **not** nest with this flag (that is `blocksInterruptParagraphs`):
510+
511+
```php
512+
$converter = DjotConverter::withNestedListsWithoutBlankLine();
513+
$result = $converter->convert("- Item\n > quote");
514+
```
515+
516+
Output:
517+
```html
518+
<ul>
519+
<li>
520+
Item
521+
&gt; quote
522+
</li>
523+
</ul>
524+
```
525+
526+
### nestedListsWithoutBlankLine vs blocksInterruptParagraphs
527+
528+
| Behavior | Default | `nestedListsWithoutBlankLine` | `blocksInterruptParagraphs` | both |
529+
|--------------------------------------------------------|---------|-------------------------------|-----------------------------|------|
530+
| Sublist nests in list item without a blank line | No | **Yes** | No | Yes |
531+
| Non-list block nests in list item without a blank line | No | No | **Yes** | Yes |
532+
| Block elements interrupt top-level paragraphs | No | No | Yes | Yes |
434533

435-
The two granular levers are independent. `blocksInterruptParagraphs` alone does not nest list items; `nestedBlocksInLists` alone does not interrupt top-level paragraphs. `significantNewlines` enables both simultaneously.
534+
Note: `blocksInterruptParagraphs` + `nestedListsWithoutBlankLine` together equal the deprecated `significantNewlines`; the deprecated `nestedBlocksInLists` enables the two in-list rows (sublist + non-list block) without top-level interruption.

docs/guide/syntax.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,15 +533,15 @@ A list is loose if *any* item is separated by a blank line. One blank line makes
533533
- Nested item B
534534
```
535535

536-
With `significantNewlines` mode enabled, nested lists can appear immediately without a blank line:
536+
With `nestedListsWithoutBlankLine` mode enabled, nested lists can appear immediately without a blank line:
537537

538538
```djot
539539
- Parent item
540540
- Nested item A
541541
- Nested item B
542542
```
543543

544-
See the [API Reference](/reference/api#significant-newlines-mode) for more on `significantNewlines` mode.
544+
See the [Parser Options guide](/guide/parser-options#nested-lists-without-blank-line-mode) for more on `nestedListsWithoutBlankLine` mode.
545545

546546
### Definition Lists
547547

docs/reference/api.md

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public function __construct(
2727
?RendererInterface $renderer = null,
2828
bool $nestedBlocksInLists = false,
2929
bool $blocksInterruptParagraphs = false,
30+
bool $nestedListsWithoutBlankLine = false,
3031
)
3132
```
3233

@@ -35,19 +36,20 @@ public function __construct(
3536
- `$strict`: When `true`, throws `ParseException` on parse errors (see [Error Handling](#error-handling)).
3637
- `$safeMode`: When `true` or a `SafeMode` instance, enables XSS protection (see [Safe Mode](#safe-mode)).
3738
- `$profile`: A `Profile` instance for feature restriction (see [Profiles](/guide/profiles)).
38-
- `$significantNewlines`: **Deprecated.** Convenience shorthand for `blocksInterruptParagraphs: true, nestedBlocksInLists: true`. Prefer the two granular levers. See [Significant Newlines Mode](#significant-newlines-mode).
39+
- `$significantNewlines`: **Deprecated.** Convenience shorthand for `blocksInterruptParagraphs: true, nestedListsWithoutBlankLine: true`. Prefer the two granular levers. See [Significant Newlines Mode](#significant-newlines-mode).
3940
- `$softBreakMode`: Override how soft breaks are rendered. When `null`, uses the renderer's default (newline for HTML).
4041
- `$roundTripMode`: When `true`, adds round-trip metadata for Djot→HTML→Djot workflows (HTML renderer only).
41-
- `$parser`: Optional pre-configured parser. When provided, inline parser constructor flags such as `warnings`, `strict`, `significantNewlines` (deprecated), `nestedBlocksInLists`, and `blocksInterruptParagraphs` are ignored.
42+
- `$parser`: Optional pre-configured parser. When provided, inline parser constructor flags such as `warnings`, `strict`, `significantNewlines` (deprecated), `nestedBlocksInLists`, `blocksInterruptParagraphs`, and `nestedListsWithoutBlankLine` are ignored.
4243
- `$renderer`: Optional pre-configured renderer. When provided, inline renderer constructor flags such as `xhtml`, `safeMode`, `softBreakMode`, and `roundTripMode` are ignored.
43-
- `$nestedBlocksInLists`: When `true`, indentation alone introduces nested blocks inside list items without a blank line, while top-level paragraph interruption stays spec-compliant (see [Nested Blocks in Lists Mode](/guide/parser-options#nested-blocks-in-lists-mode)). Implied by `$significantNewlines`.
44-
- `$blocksInterruptParagraphs`: When `true`, top-level block elements (lists, blockquotes, headings, tables, thematic breaks, and code/div/comment fences) can interrupt a paragraph without a preceding blank line, while list-item nesting stays spec-compliant (see [Block Interrupts Paragraphs Mode](/guide/parser-options#block-interrupts-paragraphs-mode)). Implied by `$significantNewlines`.
44+
- `$nestedBlocksInLists`: **Deprecated.** When `true`, indentation alone introduces nested blocks of **any** type inside list items without a blank line (broad, eager - no lone-marker lookahead), while top-level paragraph interruption stays spec-compliant (see [Nested Blocks in Lists Mode](/guide/parser-options#nested-blocks-in-lists-mode)). Prefer `$blocksInterruptParagraphs` + `$nestedListsWithoutBlankLine`. No longer implied by `$significantNewlines`.
45+
- `$blocksInterruptParagraphs`: When `true`, top-level block elements (lists, blockquotes, headings, tables, thematic breaks, and code/div/comment fences) can interrupt a paragraph without a preceding blank line. It **also** interrupts a list item's lead paragraph, so an indented non-list block nests inside the item without a blank line, using the **same** lone-marker lookahead as the top level: unambiguous openers (`#`, fenced code, `:::`, `---`) and real multi-line blocks nest, while a single ambiguous marker line (`>`, `|`) stays literal. It does not nest sublists (see [Block Interrupts Paragraphs Mode](/guide/parser-options#block-interrupts-paragraphs-mode)). Implied by `$significantNewlines`.
46+
- `$nestedListsWithoutBlankLine`: When `true`, a sublist nests inside a list item without a blank line. Only sublists nest; non-list blocks under the item stay literal and top-level paragraph interruption is unaffected (see [Nested Lists Without Blank Line Mode](/guide/parser-options#nested-lists-without-blank-line-mode)). Implied by `$significantNewlines`.
4547

4648
### Factory Methods
4749

4850
#### withSignificantNewlines()
4951

50-
> **Deprecated.** Prefer using `new DjotConverter(blocksInterruptParagraphs: true, nestedBlocksInLists: true)` or the two dedicated factory methods. See [Significant Newlines Mode](/guide/parser-options#significant-newlines-mode).
52+
> **Deprecated.** Prefer using `new DjotConverter(blocksInterruptParagraphs: true, nestedListsWithoutBlankLine: true)` or the two dedicated factory methods. See [Significant Newlines Mode](/guide/parser-options#significant-newlines-mode).
5153
5254
```php
5355
public static function withSignificantNewlines(
@@ -61,7 +63,7 @@ public static function withSignificantNewlines(
6163
): self
6264
```
6365

64-
Creates a converter with significant newlines mode enabled (equivalent to `blocksInterruptParagraphs: true` + `nestedBlocksInLists: true`). See [Significant Newlines Mode](/guide/parser-options#significant-newlines-mode).
66+
Creates a converter with significant newlines mode enabled (equivalent to `blocksInterruptParagraphs: true` + `nestedListsWithoutBlankLine: true`). See [Significant Newlines Mode](/guide/parser-options#significant-newlines-mode).
6567

6668
#### withNestedBlocksInLists()
6769

@@ -93,7 +95,23 @@ public static function withBlocksInterruptParagraphs(
9395
): self
9496
```
9597

96-
Creates a converter that allows top-level block elements (lists, blockquotes, headings, tables, thematic breaks, and code/div/comment fences) to interrupt a paragraph without a preceding blank line, while list-item nesting stays spec-compliant. See [Block Interrupts Paragraphs Mode](/guide/parser-options#block-interrupts-paragraphs-mode).
98+
Creates a converter that allows top-level block elements (lists, blockquotes, headings, tables, thematic breaks, and code/div/comment fences) to interrupt a paragraph without a preceding blank line, and nests indented non-list blocks inside list items without a blank line. Sublist nesting stays spec-compliant. See [Block Interrupts Paragraphs Mode](/guide/parser-options#block-interrupts-paragraphs-mode).
99+
100+
#### withNestedListsWithoutBlankLine()
101+
102+
```php
103+
public static function withNestedListsWithoutBlankLine(
104+
bool $xhtml = false,
105+
bool $warnings = false,
106+
bool $strict = false,
107+
bool|SafeMode|null $safeMode = null,
108+
?Profile $profile = null,
109+
?SoftBreakMode $softBreakMode = null,
110+
bool $roundTripMode = false,
111+
): self
112+
```
113+
114+
Creates a converter that nests a sublist inside a list item without requiring a blank line. Only sublists nest; non-list blocks under the item stay literal and top-level paragraph interruption stays at the spec default. See [Nested Lists Without Blank Line Mode](/guide/parser-options#nested-lists-without-blank-line-mode).
97115

98116
### Methods
99117

@@ -540,6 +558,7 @@ $parser = new BlockParser(
540558
significantNewlines: false,
541559
nestedBlocksInLists: false,
542560
blocksInterruptParagraphs: false,
561+
nestedListsWithoutBlankLine: false,
543562
);
544563
$document = $parser->parse($djotString);
545564

@@ -556,10 +575,16 @@ $isEnabled = $parser->getSignificantNewlines();
556575
$parser->setNestedBlocksInLists(true);
557576
$isEnabled = $parser->getNestedBlocksInLists();
558577

559-
// Enable/disable top-level paragraph interruption only
578+
// Enable/disable top-level paragraph interruption and non-list
579+
// block nesting in list items
560580
// (significant newlines mode enables this implicitly)
561581
$parser->setBlocksInterruptParagraphs(true);
562582
$isEnabled = $parser->getBlocksInterruptParagraphs();
583+
584+
// Enable/disable sublist nesting in list items only
585+
// (significant newlines mode enables this implicitly)
586+
$parser->setNestedListsWithoutBlankLine(true);
587+
$isEnabled = $parser->getNestedListsWithoutBlankLine();
563588
```
564589

565590
#### Custom Block Patterns
@@ -913,7 +938,7 @@ $node->addClass(string $class): void
913938
## Significant Newlines Mode
914939

915940
> **Deprecated.** `significantNewlines` is a convenience shorthand for
916-
> `blocksInterruptParagraphs: true` + `nestedBlocksInLists: true`.
941+
> `blocksInterruptParagraphs: true` + `nestedListsWithoutBlankLine: true`.
917942
> Prefer the two granular levers or their factory methods.
918943
919944
An optional parsing mode for chat messages, comments, and quick notes where markdown-like behavior is more intuitive.
@@ -930,7 +955,7 @@ $converter = new DjotConverter(significantNewlines: true);
930955
// Preferred: use the two granular levers
931956
$converter = new DjotConverter(
932957
blocksInterruptParagraphs: true,
933-
nestedBlocksInLists: true,
958+
nestedListsWithoutBlankLine: true,
934959
);
935960

936961
// Via parser directly (deprecated)

0 commit comments

Comments
 (0)