Skip to content

Commit 2cbf3b1

Browse files
committed
fix: imageテストを修正(段落分割版)
- float center imageの行全体を削除 - アライメント付き画像で段落を分割 - インライン画像を含む段落は<p>タグ省略 - file3形式のパースを修正 - _closeSpan判定を修正
1 parent 1cf1d1a commit 2cbf3b1

File tree

6 files changed

+302
-170
lines changed

6 files changed

+302
-170
lines changed

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

Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,15 @@ function processCloseSpanMarkers(elements: Element[]): Element[] {
2121
if (!elem) continue;
2222

2323
// Check for closeSpan marker
24+
// _closeSpan is on data directly, not data.attributes
2425
if (
2526
elem.element === "container" &&
2627
elem.data &&
2728
typeof elem.data === "object" &&
2829
"type" in elem.data &&
2930
elem.data.type === "span" &&
30-
"attributes" in elem.data &&
31-
typeof elem.data.attributes === "object" &&
32-
elem.data.attributes &&
33-
"_closeSpan" in elem.data.attributes
31+
"_closeSpan" in elem.data &&
32+
(elem.data as any)._closeSpan === true
3433
) {
3534
// Wrap all preceding content in a span
3635
if (result.length > 0) {
@@ -80,6 +79,50 @@ export const paragraphRule: BlockRule = {
8079
// Process closeSpan markers (for split spans)
8180
let elements = processCloseSpanMarkers(result.elements);
8281

82+
// Split paragraph at aligned images (they become block-level elements)
83+
// This also removes float center images (invalid in Wikidot)
84+
const splitResult = splitAtAlignedImages(elements);
85+
const hasAlignedImages = splitResult.some((part) => part.type === "image");
86+
87+
if (splitResult.length > 1 || hasAlignedImages) {
88+
// Return multiple elements: paragraphs and standalone images
89+
const outputElements: Element[] = [];
90+
for (const part of splitResult) {
91+
if (part.type === "image") {
92+
outputElements.push(part.element);
93+
} else if (part.elements.length > 0) {
94+
// Clean up paragraph elements
95+
const cleaned = cleanParagraphElements(part.elements);
96+
if (cleaned.length > 0) {
97+
outputElements.push({
98+
element: "container",
99+
data: {
100+
type: "paragraph",
101+
attributes: {},
102+
elements: cleaned,
103+
},
104+
});
105+
}
106+
}
107+
}
108+
if (outputElements.length === 0) {
109+
return { success: false };
110+
}
111+
return {
112+
success: true,
113+
elements: outputElements,
114+
consumed: result.consumed,
115+
};
116+
}
117+
118+
// Rebuild elements from splitResult (may have float center removed)
119+
elements = [];
120+
for (const part of splitResult) {
121+
if (part.type === "text") {
122+
elements.push(...part.elements);
123+
}
124+
}
125+
83126
// Remove trailing line-breaks (they shouldn't appear at end of paragraph)
84127
// Exception: line-breaks flagged by preserveTrailingLineBreak context are kept
85128
while (elements.length > 0 && elements[elements.length - 1]?.element === "line-break") {
@@ -155,3 +198,95 @@ function parseInlineContent(ctx: ParseContext): {
155198
// The parser will stop at double NEWLINE (paragraph break)
156199
return parseInlineUntil(ctx, "PARAGRAPH_BREAK" as any);
157200
}
201+
202+
type SplitPart = { type: "text"; elements: Element[] } | { type: "image"; element: Element };
203+
204+
/**
205+
* Split elements at aligned images
206+
* Aligned images become block-level elements, splitting the paragraph
207+
* Float center images are removed entirely (invalid in Wikidot)
208+
*/
209+
function splitAtAlignedImages(elements: Element[]): SplitPart[] {
210+
const parts: SplitPart[] = [];
211+
let currentText: Element[] = [];
212+
213+
for (let i = 0; i < elements.length; i++) {
214+
const elem = elements[i];
215+
if (!elem) continue;
216+
217+
if (isAlignedImage(elem)) {
218+
const imageData = (elem as any).data;
219+
// Float center is invalid - skip the image AND preceding text on same line
220+
if (imageData?.alignment?.float && imageData.alignment.align === "center") {
221+
// Remove text preceding the image on the same line (back to last line-break)
222+
while (currentText.length > 0) {
223+
const last = currentText[currentText.length - 1];
224+
if (last?.element === "line-break") {
225+
break;
226+
}
227+
currentText.pop();
228+
}
229+
// Also skip line-break after the image if present
230+
if (elements[i + 1]?.element === "line-break") {
231+
i++;
232+
}
233+
continue;
234+
}
235+
236+
// Save current text as a part
237+
if (currentText.length > 0) {
238+
parts.push({ type: "text", elements: [...currentText] });
239+
currentText = [];
240+
}
241+
// Add image as standalone element
242+
parts.push({ type: "image", element: elem });
243+
} else {
244+
currentText.push(elem);
245+
}
246+
}
247+
248+
// Add remaining text
249+
if (currentText.length > 0) {
250+
parts.push({ type: "text", elements: currentText });
251+
}
252+
253+
return parts;
254+
}
255+
256+
/**
257+
* Check if element is an aligned image (has alignment property)
258+
*/
259+
function isAlignedImage(elem: Element): boolean {
260+
if (elem.element !== "image") return false;
261+
const data = (elem as any).data;
262+
return data?.alignment != null;
263+
}
264+
265+
/**
266+
* Clean up paragraph elements (remove trailing/leading line-breaks)
267+
*/
268+
function cleanParagraphElements(elements: Element[]): Element[] {
269+
let result = [...elements];
270+
271+
// Remove trailing line-breaks
272+
while (result.length > 0 && result[result.length - 1]?.element === "line-break") {
273+
result.pop();
274+
}
275+
276+
// Remove trailing whitespace
277+
while (result.length > 0) {
278+
const last = result[result.length - 1];
279+
if (last?.element === "text" && typeof last.data === "string" && last.data.trim() === "") {
280+
result.pop();
281+
} else {
282+
break;
283+
}
284+
}
285+
286+
// Remove leading line-breaks
287+
while (result.length > 0 && result[0]?.element === "line-break") {
288+
result.shift();
289+
}
290+
291+
return result;
292+
}

packages/parser/src/parser/rules/inline/image.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,23 +86,35 @@ function parseImageSource(src: string): ImageSource {
8686
}
8787

8888
// File references - determine type based on format
89-
// file3: site:page/file
90-
// file2: page/file
91-
// file1: file
89+
// file3: site:page/file or site/page/file (2+ slashes)
90+
// file2: page/file (1 slash)
91+
// file1: file (no slash)
9292
const colonIdx = src.indexOf(":");
9393
const slashIdx = src.indexOf("/");
9494

9595
if (colonIdx > 0 && slashIdx > colonIdx) {
96-
// site:page/file format
96+
// site:page/file format (colon-based)
9797
const site = src.substring(0, colonIdx);
9898
const rest = src.substring(colonIdx + 1);
9999
const lastSlash = rest.lastIndexOf("/");
100100
const page = rest.substring(0, lastSlash);
101101
const file = rest.substring(lastSlash + 1);
102102
return { type: "file3", data: { site, page, file } };
103103
}
104+
105+
// Count slashes to determine format
106+
const slashes = src.split("/").length - 1;
107+
if (slashes >= 2) {
108+
// site/page/file format (2+ slashes = file3)
109+
const firstSlash = src.indexOf("/");
110+
const lastSlash = src.lastIndexOf("/");
111+
const site = src.substring(0, firstSlash);
112+
const page = src.substring(firstSlash + 1, lastSlash);
113+
const file = src.substring(lastSlash + 1);
114+
return { type: "file3", data: { site, page, file } };
115+
}
104116
if (slashIdx > 0) {
105-
// page/file format
117+
// page/file format (1 slash = file2)
106118
const page = src.substring(0, slashIdx);
107119
const file = src.substring(slashIdx + 1);
108120
return { type: "file2", data: { page, file } };

packages/render/src/elements/container.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
1-
import type { ContainerData } from "@wdprlib/ast";
1+
import type { ContainerData, Element } from "@wdprlib/ast";
22
import { isStringContainerType, isHeaderType, isAlignType } from "@wdprlib/ast";
33
import type { RenderContext } from "../context";
44
import { escapeAttr, sanitizeAttributes } from "../escape";
55
import { renderElements } from "../render";
66

7+
/**
8+
* Check if elements contain an inline image (image without alignment)
9+
* Wikidot skips <p> tags for paragraphs containing inline images
10+
*/
11+
function hasInlineImage(elements: Element[]): boolean {
12+
for (const elem of elements) {
13+
if (elem.element === "image") {
14+
const data = (elem as any).data;
15+
// Inline image = no alignment (alignment is null or undefined)
16+
if (data?.alignment == null) {
17+
return true;
18+
}
19+
}
20+
}
21+
return false;
22+
}
23+
724
/** Render a container element */
825
export function renderContainer(ctx: RenderContext, data: ContainerData): void {
926
const { type, attributes, elements } = data;
@@ -53,9 +70,14 @@ function renderStringContainer(
5370
): void {
5471
switch (type) {
5572
case "paragraph":
56-
ctx.push(`<p${renderAttrs(attributes)}>`);
57-
renderElements(ctx, elements);
58-
ctx.push("</p>");
73+
// Wikidot: paragraphs containing inline images (no alignment) skip <p> tags
74+
if (hasInlineImage(elements)) {
75+
renderElements(ctx, elements);
76+
} else {
77+
ctx.push(`<p${renderAttrs(attributes)}>`);
78+
renderElements(ctx, elements);
79+
ctx.push("</p>");
80+
}
5981
break;
6082
case "bold":
6183
ctx.push(`<strong${renderAttrs(attributes)}>`);

0 commit comments

Comments
 (0)