@@ -137,69 +137,8 @@ function consumeCloseTag(ctx: ParseContext, pos: number): number {
137137 return closeConsumed ;
138138}
139139
140- /**
141- * Merges consecutive paragraph containers that were split by unrecognised
142- * block tokens back into the preceding paragraph.
143- *
144- * When a `[[collapsible]]` token appears inside a collapsible body (the rule
145- * is filtered out to prevent nesting), the paragraph parser treats the
146- * `BLOCK_OPEN` or `BLOCK_END_OPEN` as a paragraph boundary, splitting content
147- * that Wikidot keeps in a single paragraph. This function detects those
148- * artificial splits — paragraphs whose first text element is `"[["` or
149- * `"[[/"` — and merges them back, inserting a line-break between runs.
150- *
151- * Paragraphs separated by blank lines (double newline) do NOT start with
152- * block-open text and are therefore left as separate paragraphs.
153- */
154- function mergeSplitParagraphs ( elements : Element [ ] ) : Element [ ] {
155- const result : Element [ ] = [ ] ;
156-
157- for ( const elem of elements ) {
158- if (
159- elem . element !== "container" ||
160- ! elem . data ||
161- typeof elem . data !== "object" ||
162- ! ( "type" in elem . data ) ||
163- elem . data . type !== "paragraph" ||
164- ! ( "elements" in elem . data ) ||
165- ! Array . isArray ( elem . data . elements )
166- ) {
167- result . push ( elem ) ;
168- continue ;
169- }
170-
171- // Check if this paragraph starts with "[[" or "[[/" (unrecognised block token)
172- const firstElem = elem . data . elements [ 0 ] ;
173- const startsWithBlockOpen =
174- firstElem ?. element === "text" &&
175- typeof firstElem . data === "string" &&
176- ( firstElem . data === "[[" || firstElem . data === "[[/" ) ;
177-
178- if ( ! startsWithBlockOpen ) {
179- result . push ( elem ) ;
180- continue ;
181- }
182-
183- // Try to merge into the previous paragraph
184- const prev = result [ result . length - 1 ] ;
185- if (
186- prev ?. element === "container" &&
187- prev . data &&
188- typeof prev . data === "object" &&
189- "type" in prev . data &&
190- prev . data . type === "paragraph" &&
191- "elements" in prev . data &&
192- Array . isArray ( prev . data . elements )
193- ) {
194- prev . data . elements . push ( { element : "line-break" } ) ;
195- prev . data . elements . push ( ...elem . data . elements ) ;
196- } else {
197- result . push ( elem ) ;
198- }
199- }
200-
201- return result ;
202- }
140+ /** Block names excluded from rule dispatch and paragraph-boundary detection. */
141+ const EXCLUDED_BLOCKS = new Set ( [ "collapsible" ] ) ;
203142
204143/**
205144 * Block rule for `[[collapsible ...]]...[[/collapsible]]`.
@@ -208,7 +147,7 @@ function mergeSplitParagraphs(elements: Element[]): Element[] {
208147 * 1. Match BLOCK_OPEN + name "collapsible".
209148 * 2. Parse multiline attributes (show, hide, folded, hideLocation, etc.).
210149 * 3. If a NEWLINE follows the opening tag, parse body as block content
211- * with the collapsible rule itself removed (to prevent nesting).
150+ * with the collapsible rule itself excluded (to prevent nesting).
212151 * Otherwise, parse inline content until close tag or end of line
213152 * (inline form).
214153 * 4. Consume the `[[/collapsible]]` closing tag.
@@ -299,24 +238,22 @@ export const collapsibleRule: BlockRule = {
299238 }
300239 } else {
301240 // Block form: parse content recursively until [[/collapsible]]
302- // Collapsible cannot be nested in Wikidot - nested [[collapsible]] becomes plain text
303- const bodyCtx : ParseContext = {
304- ...ctx ,
305- pos,
306- blockRules : ctx . blockRules . filter ( ( r ) => r . name !== "collapsible" ) ,
307- } ;
241+ // Collapsible cannot be nested in Wikidot - nested [[collapsible]] becomes plain text.
242+ // excludedBlockNames removes the collapsible rule from dispatch AND prevents
243+ // [[collapsible]] / [[/collapsible]] tokens from triggering paragraph splits.
244+ const bodyCtx : ParseContext = { ...ctx , pos } ;
308245
309246 const closeCondition = ( checkCtx : ParseContext ) : boolean => {
310247 return isCollapsibleClose ( checkCtx , checkCtx . pos ) ;
311248 } ;
312249
313- const bodyResult = parseBlocksUntil ( bodyCtx , closeCondition ) ;
250+ const bodyResult = parseBlocksUntil ( bodyCtx , closeCondition , {
251+ excludedBlockNames : EXCLUDED_BLOCKS ,
252+ } ) ;
314253 consumed += bodyResult . consumed ;
315254 pos += bodyResult . consumed ;
316255
317- // Merge paragraphs that were artificially split by unrecognised
318- // [[collapsible]] tokens (nested collapsible is treated as plain text)
319- bodyElements = mergeSplitParagraphs ( bodyResult . elements ) ;
256+ bodyElements = bodyResult . elements ;
320257 }
321258
322259 // Check for missing close tag
0 commit comments