Skip to content

Commit a537762

Browse files
committed
Sign Newline Ordered List Rule
1 parent b9a964f commit a537762

File tree

6 files changed

+106
-56
lines changed

6 files changed

+106
-56
lines changed

src/Converter/HtmlToDjot.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,8 +488,11 @@ protected function processDefinitionList(DOMElement $node): string
488488
$ddCount = 0;
489489
} elseif ($tag === 'dd') {
490490
// Definition: indented content after blank line
491-
if ($lastWasTerm || $ddCount > 0) {
491+
if ($lastWasTerm) {
492492
$output .= "\n";
493+
} elseif ($ddCount > 0) {
494+
// Multiple dd elements need `: +` continuation marker
495+
$output .= ": +\n\n";
493496
}
494497
$content = trim($this->processChildren($child));
495498
// Indent definition content

src/Parser/BlockParser.php

Lines changed: 42 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1776,6 +1776,11 @@ protected function tryParseDjotDefinitionList(Node $parent, array $lines, int $s
17761776

17771777
$termContent = $termMatch[1];
17781778

1779+
// Check for continuation marker `: +` - not a new term, breaks term collection
1780+
if ($termContent === '+') {
1781+
break;
1782+
}
1783+
17791784
// Special case: if term starts with code fence, term is empty and fence is part of definition
17801785
$termStartsWithCodeFence = preg_match('/^(`{3,}|~{3,})/', $termContent, $fenceMatch);
17811786

@@ -1847,9 +1852,9 @@ protected function tryParseDjotDefinitionList(Node $parent, array $lines, int $s
18471852
}
18481853

18491854
// Now collect definition content (after blank line, 2-space indent)
1850-
// When multiple terms share definitions, each paragraph block becomes a separate dd
1855+
// Use `: +` marker to create additional dd elements for the same term
18511856
$defLines = [];
1852-
$multipleTerms = count($terms) > 1;
1857+
$allDefBlocks = [];
18531858

18541859
// If term started with code fence, add it to definition content
18551860
if ($codeFenceInfo !== null) {
@@ -1866,6 +1871,17 @@ protected function tryParseDjotDefinitionList(Node $parent, array $lines, int $s
18661871
continue;
18671872
}
18681873

1874+
// Check for continuation marker `: +` - creates new dd for same term
1875+
if ($defLine === ': +') {
1876+
if ($defLines !== []) {
1877+
$allDefBlocks[] = $defLines;
1878+
$defLines = [];
1879+
}
1880+
$i++;
1881+
1882+
continue;
1883+
}
1884+
18691885
// Check for next term (space is syntax delimiter, not tab)
18701886
if (preg_match('/^: +/', $defLine)) {
18711887
break;
@@ -1880,22 +1896,35 @@ protected function tryParseDjotDefinitionList(Node $parent, array $lines, int $s
18801896
}
18811897
}
18821898

1899+
// Add final block
1900+
if ($defLines !== []) {
1901+
$allDefBlocks[] = $defLines;
1902+
}
1903+
18831904
// Create definition node(s)
1884-
if ($multipleTerms && $defLines !== []) {
1885-
// Split by blank lines - each block becomes a separate dd
1886-
$blocks = $this->splitByBlankLines($defLines);
1887-
foreach ($blocks as $block) {
1905+
if ($allDefBlocks !== []) {
1906+
foreach ($allDefBlocks as $block) {
18881907
$def = new DefinitionDescription();
18891908
$defAttributes = [];
18901909

1910+
// Skip leading/trailing blank lines
1911+
while ($block !== [] && $block[0] === '') {
1912+
array_shift($block);
1913+
}
1914+
while ($block !== [] && end($block) === '') {
1915+
array_pop($block);
1916+
}
1917+
18911918
// Check if last line is a standalone attribute block for the dd
18921919
$blockCount = count($block);
18931920
if ($blockCount > 0 && preg_match('/^\{([^{}]+)\}\s*$/', $block[$blockCount - 1], $attrMatch)) {
18941921
$defAttributes = AttributeParser::parse($attrMatch[1]);
18951922
array_pop($block);
18961923
}
18971924

1898-
$this->parseBlocks($def, $block, 0);
1925+
if ($block !== []) {
1926+
$this->parseBlocks($def, $block, 0);
1927+
}
18991928

19001929
// Apply definition attributes
19011930
if ($defAttributes !== []) {
@@ -1906,43 +1935,8 @@ protected function tryParseDjotDefinitionList(Node $parent, array $lines, int $s
19061935
$defList->appendChild($def);
19071936
}
19081937
} else {
1909-
// Single term: all content goes in one dd
1910-
$def = new DefinitionDescription();
1911-
$defAttributes = [];
1912-
if ($defLines !== []) {
1913-
// Skip leading blank lines using index (avoid O(n) array_shift)
1914-
$defStart = 0;
1915-
$defLineCount = count($defLines);
1916-
while ($defStart < $defLineCount && $defLines[$defStart] === '') {
1917-
$defStart++;
1918-
}
1919-
// Remove trailing blank lines (but preserve potential attribute line)
1920-
$defEnd = $defLineCount;
1921-
while ($defEnd > $defStart + 1 && $defLines[$defEnd - 1] === '') {
1922-
$defEnd--;
1923-
}
1924-
1925-
// Check if last line is a standalone attribute block for the dd
1926-
if ($defEnd > $defStart && preg_match('/^\{([^{}]+)\}\s*$/', $defLines[$defEnd - 1], $attrMatch)) {
1927-
$defAttributes = AttributeParser::parse($attrMatch[1]);
1928-
$defEnd--;
1929-
// Remove any trailing blank lines before the attribute
1930-
while ($defEnd > $defStart && $defLines[$defEnd - 1] === '') {
1931-
$defEnd--;
1932-
}
1933-
}
1934-
1935-
if ($defEnd > $defStart) {
1936-
$this->parseBlocks($def, array_slice($defLines, $defStart, $defEnd - $defStart), 0);
1937-
}
1938-
}
1939-
// Apply definition attributes
1940-
if ($defAttributes !== []) {
1941-
foreach ($defAttributes as $key => $value) {
1942-
$def->setAttribute($key, $value);
1943-
}
1944-
}
1945-
$defList->appendChild($def);
1938+
// Term with no definition content - create empty dd
1939+
$defList->appendChild(new DefinitionDescription());
19461940
}
19471941
}
19481942

@@ -2533,9 +2527,10 @@ protected function startsNewBlockSignificant(string $line): bool
25332527
// Fenced divs: :{3,}
25342528
return isset($line[1], $line[2]) && $line[1] === ':' && $line[2] === ':';
25352529
default:
2536-
// Ordered lists: digit or letter followed by . or )
2537-
if (ctype_digit($first) || ctype_alpha($first)) {
2538-
return preg_match('/^(\d+|[a-zA-Z])[.)]\s/', $line) === 1;
2530+
// Only 1. or 1) can interrupt paragraphs (CommonMark rule)
2531+
// Prevents "1985. That year..." from becoming a list
2532+
if ($first === '1') {
2533+
return preg_match('/^1[.)]\s/', $line) === 1;
25392534
}
25402535

25412536
return false;

tests/TestCase/Converter/HtmlToDjotTest.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,14 +253,16 @@ public function testDefinitionListMultipleTerms(): void
253253

254254
public function testDefinitionListMultipleDefinitions(): void
255255
{
256-
// Multiple dd elements under multiple terms become separate indented paragraphs
256+
// Multiple dd elements use `: +` continuation marker
257257
$html = '<dl><dt>color</dt><dt>colour</dt><dd>The visual property.</dd><dd>Used in design.</dd></dl>';
258258
$result = $this->converter->convert($html);
259259

260260
$this->assertStringContainsString(': color', $result);
261261
$this->assertStringContainsString(': colour', $result);
262-
// Each dd becomes a separate indented block separated by blank line
263-
$this->assertStringContainsString(" The visual property.\n\n Used in design.", $result);
262+
// Each dd is separated by `: +` marker
263+
$this->assertStringContainsString(': +', $result);
264+
$this->assertStringContainsString('The visual property.', $result);
265+
$this->assertStringContainsString('Used in design.', $result);
264266
}
265267

266268
// ==================== Spans with Attributes ====================

tests/TestCase/DjotConverterTest.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -555,16 +555,16 @@ public function testDefinitionListMultipleTermsWithBlankLines(): void
555555

556556
public function testDefinitionListMultipleTermsMultipleDefinitions(): void
557557
{
558-
// When multiple terms share definitions, each paragraph block becomes a separate dd
559-
$djot = ": color\n: colour\n\n The visual property of objects.\n\n Used in art and design.";
558+
// Use `: +` marker to create multiple dd elements for same term(s)
559+
$djot = ": color\n: colour\n\n The visual property of objects.\n\n: +\n\n Used in art and design.";
560560

561561
$result = $this->converter->convert($djot);
562562

563563
$this->assertStringContainsString('<dt>color</dt>', $result);
564564
$this->assertStringContainsString('<dt>colour</dt>', $result);
565565
$this->assertStringContainsString('The visual property', $result);
566566
$this->assertStringContainsString('Used in art and design', $result);
567-
// Multiple terms with blank-line-separated paragraphs = multiple dd elements
567+
// `: +` marker creates separate dd elements
568568
$this->assertSame(2, substr_count($result, '<dd>'));
569569
}
570570

@@ -601,7 +601,8 @@ public function testDefinitionListDdAttribute(): void
601601
public function testDefinitionListAllAttributes(): void
602602
{
603603
// DD attributes come AFTER content (consistent with list items)
604-
$djot = "{.vocabulary}\n: color\n{.american}\n: colour\n{.british}\n\n The visual property.\n {.primary}\n\n Used in design.\n {.secondary}";
604+
// Use `: +` to create multiple dd elements with separate attributes
605+
$djot = "{.vocabulary}\n: color\n{.american}\n: colour\n{.british}\n\n The visual property.\n {.primary}\n\n: +\n\n Used in design.\n {.secondary}";
605606

606607
$result = $this->converter->convert($djot);
607608

tests/TestCase/Renderer/MarkdownRendererTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,8 @@ public function testDefinitionList(): void
277277

278278
public function testDefinitionListMultipleTermsMultipleDefinitions(): void
279279
{
280-
$djot = ": color\n: colour\n\n The visual property.\n\n Used in design.";
280+
// Use `: +` marker to create multiple definitions
281+
$djot = ": color\n: colour\n\n The visual property.\n\n: +\n\n Used in design.";
281282
$document = $this->converter->parse($djot);
282283
$result = $this->renderer->render($document);
283284

tests/TestCase/SignificantNewlinesTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,4 +291,52 @@ public function testHeadingInterruptsParagraphInSignificantNewlinesMode(): void
291291
$this->assertInstanceOf(Paragraph::class, $children[0]);
292292
$this->assertInstanceOf(Heading::class, $children[1]);
293293
}
294+
295+
// ==================== Sign Newline Ordered List Rule ====================
296+
297+
public function testOnlyOneCanInterruptParagraph(): void
298+
{
299+
// Only "1." can interrupt a paragraph (CommonMark rule)
300+
$parser = new BlockParser(significantNewlines: true);
301+
$doc = $parser->parse("Steps:\n1. First step");
302+
303+
$children = $doc->getChildren();
304+
$this->assertCount(2, $children);
305+
$this->assertInstanceOf(Paragraph::class, $children[0]);
306+
$this->assertInstanceOf(ListBlock::class, $children[1]);
307+
}
308+
309+
public function testYearDoesNotBecomeList(): void
310+
{
311+
// "1985." should NOT interrupt - prevents years becoming lists
312+
$parser = new BlockParser(significantNewlines: true);
313+
$doc = $parser->parse("My favorite year was\n1985. It was great.");
314+
315+
$children = $doc->getChildren();
316+
$this->assertCount(1, $children);
317+
$this->assertInstanceOf(Paragraph::class, $children[0]);
318+
}
319+
320+
public function testHighNumberedListDoesNotInterrupt(): void
321+
{
322+
// "5." should NOT interrupt paragraphs
323+
$parser = new BlockParser(significantNewlines: true);
324+
$doc = $parser->parse("Continue from step\n5. Do this thing");
325+
326+
$children = $doc->getChildren();
327+
$this->assertCount(1, $children);
328+
$this->assertInstanceOf(Paragraph::class, $children[0]);
329+
}
330+
331+
public function testHighNumberedListAfterBlankLine(): void
332+
{
333+
// With blank line, any number can start a list
334+
$parser = new BlockParser(significantNewlines: true);
335+
$doc = $parser->parse("Continue from step\n\n5. Do this thing");
336+
337+
$children = $doc->getChildren();
338+
$this->assertCount(2, $children);
339+
$this->assertInstanceOf(Paragraph::class, $children[0]);
340+
$this->assertInstanceOf(ListBlock::class, $children[1]);
341+
}
294342
}

0 commit comments

Comments
 (0)