@@ -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 ;
0 commit comments