1919 * emitted as `<br />` + literal text, matching Wikidot rendering.
2020 * - An inline form (`[[collapsible]]text[[/collapsible]]` on one line) is
2121 * supported but uncommon.
22- * - Consecutive paragraph containers in the body are merged back into a single
23- * paragraph via `mergeParagraphs`, because Wikidot does not split
24- * paragraphs at unrecognised block tokens inside a collapsible.
2522 *
2623 * @module
2724 */
@@ -141,66 +138,64 @@ function consumeCloseTag(ctx: ParseContext, pos: number): number {
141138}
142139
143140/**
144- * Merges consecutive paragraph container elements into a single paragraph.
141+ * Merges consecutive paragraph containers that were split by unrecognised
142+ * block tokens back into the preceding paragraph.
145143 *
146- * When the collapsible rule is disabled during body parsing (to prevent
147- * nesting), unrecognised `[[collapsible...]]` tokens cause the paragraph
148- * parser to split content into multiple paragraphs. Wikidot itself keeps
149- * this content as one paragraph, so this function re-joins them, inserting
150- * line-break elements between the merged runs.
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` as a paragraph boundary, splitting content that Wikidot keeps
147+ * in a single paragraph. This function detects those artificial splits —
148+ * paragraphs whose first text element is `"[["` — and merges them back,
149+ * inserting a line-break between runs.
151150 *
152- * Non-paragraph elements (divs, tables, etc.) act as merge boundaries and
153- * are emitted as-is.
154- *
155- * @param elements - The body elements produced by block parsing.
156- * @returns A new array with adjacent paragraphs merged.
151+ * Paragraphs separated by blank lines (double newline) do NOT start with
152+ * `"[["` and are therefore left as separate paragraphs.
157153 */
158- function mergeParagraphs ( elements : Element [ ] ) : Element [ ] {
154+ function mergeSplitParagraphs ( elements : Element [ ] ) : Element [ ] {
159155 const result : Element [ ] = [ ] ;
160- let mergedElements : Element [ ] = [ ] ;
161156
162157 for ( const elem of elements ) {
163158 if (
164- elem . element === "container" &&
165- elem . data &&
166- typeof elem . data === "object" &&
167- "type" in elem . data &&
168- elem . data . type === "paragraph"
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 )
169166 ) {
170- // Add line-break between merged paragraphs
171- if ( mergedElements . length > 0 ) {
172- mergedElements . push ( { element : "line-break" } ) ;
173- }
174- if ( "elements" in elem . data && Array . isArray ( elem . data . elements ) ) {
175- mergedElements . push ( ...elem . data . elements ) ;
176- }
177- } else {
178- // Non-paragraph element: flush merged paragraphs
179- if ( mergedElements . length > 0 ) {
180- result . push ( {
181- element : "container" ,
182- data : {
183- type : "paragraph" ,
184- attributes : { } ,
185- elements : mergedElements ,
186- } ,
187- } ) ;
188- mergedElements = [ ] ;
189- }
190167 result . push ( elem ) ;
168+ continue ;
191169 }
192- }
193170
194- // Flush remaining merged paragraphs
195- if ( mergedElements . length > 0 ) {
196- result . push ( {
197- element : "container" ,
198- data : {
199- type : "paragraph" ,
200- attributes : { } ,
201- elements : mergedElements ,
202- } ,
203- } ) ;
171+ // Check if this paragraph starts with "[[" (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 === "[[" ;
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+ }
204199 }
205200
206201 return result ;
@@ -216,11 +211,10 @@ function mergeParagraphs(elements: Element[]): Element[] {
216211 * with the collapsible rule itself removed (to prevent nesting).
217212 * Otherwise, parse inline content until close tag or end of line
218213 * (inline form).
219- * 4. Merge consecutive paragraphs in the body via `mergeParagraphs()`.
220- * 5. Consume the `[[/collapsible]]` closing tag.
221- * 6. Consume any orphaned `[[/collapsible]]` tags that follow, converting
214+ * 4. Consume the `[[/collapsible]]` closing tag.
215+ * 5. Consume any orphaned `[[/collapsible]]` tags that follow, converting
222216 * them to `<br />` + literal text.
223- * 7 . Derive `show-top` / `show-bottom` booleans from the `hideLocation`
217+ * 6 . Derive `show-top` / `show-bottom` booleans from the `hideLocation`
224218 * attribute.
225219 */
226220export const collapsibleRule : BlockRule = {
@@ -320,9 +314,9 @@ export const collapsibleRule: BlockRule = {
320314 consumed += bodyResult . consumed ;
321315 pos += bodyResult . consumed ;
322316
323- // Merge consecutive paragraphs into one (Wikidot doesn't split paragraphs
324- // at unrecognized [[block ]] tokens inside collapsible)
325- bodyElements = mergeParagraphs ( bodyResult . elements ) ;
317+ // Merge paragraphs that were artificially split by unrecognised
318+ // [[collapsible ]] tokens (nested collapsible is treated as plain text )
319+ bodyElements = mergeSplitParagraphs ( bodyResult . elements ) ;
326320 }
327321
328322 // Check for missing close tag
0 commit comments