Skip to content

Commit 2bf5ddc

Browse files
committed
fix: [[collapsible]]内で空行による段落分けが機能しない問題を修正 (#33)
mergeParagraphs関数が全ての連続段落を結合していたため、空行で 区切られた段落まで1つの段落にマージされていた。 新しいmergeSplitParagraphs関数は、未認識ブロックトークン([[で 始まる段落)による人為的な分割のみをマージし、空行による正当な 段落分割は保持する。
1 parent c8c441d commit 2bf5ddc

File tree

4 files changed

+190
-59
lines changed

4 files changed

+190
-59
lines changed

packages/parser/src/parser/rules/block/collapsible.ts

Lines changed: 53 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@
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
*/
226220
export 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
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
{
2+
"elements": [
3+
{
4+
"element": "collapsible",
5+
"data": {
6+
"elements": [
7+
{
8+
"element": "container",
9+
"data": {
10+
"type": "paragraph",
11+
"attributes": {},
12+
"elements": [
13+
{
14+
"element": "text",
15+
"data": ""
16+
},
17+
{
18+
"element": "text",
19+
"data": ""
20+
},
21+
{
22+
"element": "text",
23+
"data": "1"
24+
}
25+
]
26+
}
27+
},
28+
{
29+
"element": "container",
30+
"data": {
31+
"type": "paragraph",
32+
"attributes": {},
33+
"elements": [
34+
{
35+
"element": "text",
36+
"data": ""
37+
},
38+
{
39+
"element": "text",
40+
"data": ""
41+
},
42+
{
43+
"element": "text",
44+
"data": "2"
45+
}
46+
]
47+
}
48+
},
49+
{
50+
"element": "container",
51+
"data": {
52+
"type": "paragraph",
53+
"attributes": {},
54+
"elements": [
55+
{
56+
"element": "text",
57+
"data": ""
58+
},
59+
{
60+
"element": "text",
61+
"data": ""
62+
},
63+
{
64+
"element": "text",
65+
"data": "3"
66+
},
67+
{
68+
"element": "line-break"
69+
},
70+
{
71+
"element": "text",
72+
"data": ""
73+
},
74+
{
75+
"element": "text",
76+
"data": ""
77+
},
78+
{
79+
"element": "text",
80+
"data": "4"
81+
},
82+
{
83+
"element": "line-break"
84+
},
85+
{
86+
"element": "text",
87+
"data": ""
88+
},
89+
{
90+
"element": "text",
91+
"data": ""
92+
},
93+
{
94+
"element": "text",
95+
"data": "5"
96+
}
97+
]
98+
}
99+
}
100+
],
101+
"attributes": {},
102+
"start-open": false,
103+
"show-text": null,
104+
"hide-text": null,
105+
"show-top": true,
106+
"show-bottom": false
107+
}
108+
},
109+
{
110+
"element": "footnote-block",
111+
"data": {
112+
"title": null,
113+
"hide": false
114+
}
115+
}
116+
]
117+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[[collapsible]]
2+
文章1
3+
4+
文章2
5+
6+
文章3
7+
文章4
8+
文章5
9+
[[/collapsible]]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<div class="collapsible-block">
2+
<div class="collapsible-block-folded"><a class="collapsible-block-link" href="javascript:;">+&nbsp;show&nbsp;block</a></div>
3+
<div class="collapsible-block-unfolded" style="display:none">
4+
<div class="collapsible-block-unfolded-link"><a class="collapsible-block-link" href="javascript:;">–&nbsp;hide&nbsp;block</a></div>
5+
<div class="collapsible-block-content">
6+
<p>文章1</p>
7+
<p>文章2</p>
8+
<p>文章3<br />文章4<br />文章5</p>
9+
</div>
10+
</div>
11+
</div>

0 commit comments

Comments
 (0)