22 * Post-processing for parsed AST
33 *
44 * Handles span_ (paragraph strip) paragraph merging
5+ * Handles empty expr splitting paragraphs
56 */
6- import type { Element , ContainerData } from "@wdprlib/ast" ;
7+ import type { Element , ContainerData , ExprData } from "@wdprlib/ast" ;
78
89/**
910 * Check if an element is a container with specific type
@@ -226,6 +227,77 @@ function splitParagraphAtBlankLineSpans(para: Element): Element[] {
226227 return result . length > 0 ? result : [ para ] ;
227228}
228229
230+ /**
231+ * Check if an element is an empty expr (expression is empty string)
232+ */
233+ function isEmptyExpr ( el : Element ) : boolean {
234+ if ( el . element !== "expr" ) return false ;
235+ const data = el . data as ExprData ;
236+ return data . expression === "" ;
237+ }
238+
239+ /**
240+ * Split paragraph at empty expr elements
241+ * Empty expr acts as a paragraph break
242+ * Returns array of paragraphs (original may be split into multiple)
243+ */
244+ function splitParagraphAtEmptyExpr ( para : Element ) : Element [ ] {
245+ const data = getContainerData ( para ) ;
246+ if ( ! data || data . type !== "paragraph" ) return [ para ] ;
247+
248+ // Check if paragraph contains empty expr
249+ const hasEmptyExpr = data . elements . some ( isEmptyExpr ) ;
250+ if ( ! hasEmptyExpr ) return [ para ] ;
251+
252+ const result : Element [ ] = [ ] ;
253+ let currentElements : Element [ ] = [ ] ;
254+
255+ for ( let i = 0 ; i < data . elements . length ; i ++ ) {
256+ const child = data . elements [ i ] ;
257+ if ( ! child ) continue ;
258+
259+ if ( isEmptyExpr ( child ) ) {
260+ // Skip the empty expr and surrounding line-breaks
261+ // Check if prev element is line-break, remove it
262+ if ( currentElements . length > 0 && currentElements [ currentElements . length - 1 ] ?. element === "line-break" ) {
263+ currentElements . pop ( ) ;
264+ }
265+ // Save current paragraph if not empty
266+ if ( currentElements . length > 0 ) {
267+ result . push ( {
268+ element : "container" ,
269+ data : {
270+ type : "paragraph" ,
271+ attributes : { } ,
272+ elements : currentElements ,
273+ } ,
274+ } ) ;
275+ currentElements = [ ] ;
276+ }
277+ // Skip next line-break if present
278+ if ( i + 1 < data . elements . length && data . elements [ i + 1 ] ?. element === "line-break" ) {
279+ i ++ ;
280+ }
281+ } else {
282+ currentElements . push ( child ) ;
283+ }
284+ }
285+
286+ // Add remaining elements as final paragraph
287+ if ( currentElements . length > 0 ) {
288+ result . push ( {
289+ element : "container" ,
290+ data : {
291+ type : "paragraph" ,
292+ attributes : { } ,
293+ elements : currentElements ,
294+ } ,
295+ } ) ;
296+ }
297+
298+ return result . length > 0 ? result : [ ] ;
299+ }
300+
229301/**
230302 * Merge consecutive paragraphs that contain span_ (paragraph strip mode)
231303 * Wikidot behavior: span_ removes paragraph breaks around it
@@ -237,15 +309,18 @@ function splitParagraphAtBlankLineSpans(para: Element): Element[] {
237309 * outside the paragraph.
238310 *
239311 * Also splits paragraphs containing spans with _splitByBlankLine marker.
312+ * Also splits paragraphs at empty [[#expr ]] elements.
240313 */
241314export function mergeSpanStripParagraphs ( children : Element [ ] ) : Element [ ] {
242- // First pass: split paragraphs at _splitByBlankLine markers
315+ // First pass: split paragraphs at _splitByBlankLine markers and empty expr
243316 const expandedChildren : Element [ ] = [ ] ;
244317 for ( const child of children ) {
245318 if ( isContainer ( child , "paragraph" ) ) {
246319 const data = getContainerData ( child ) ;
247320 if ( data && data . elements . some ( isSplitSpan ) ) {
248321 expandedChildren . push ( ...splitParagraphAtBlankLineSpans ( child ) ) ;
322+ } else if ( data && data . elements . some ( isEmptyExpr ) ) {
323+ expandedChildren . push ( ...splitParagraphAtEmptyExpr ( child ) ) ;
249324 } else {
250325 expandedChildren . push ( child ) ;
251326 }
0 commit comments