Skip to content

Commit 9a08ce8

Browse files
committed
fix: expr/edge-casesテストを修正
- 連続+演算子を正しくパース(1 + + 2 → 3) - エラーメッセージをWikidot互換に変更(too many values in the stack) - ifexprエラー時にエラーメッセージを出力(elseブランチではなく) - 空式で段落を分割するポストプロセス処理を追加
1 parent dc4b585 commit 9a08ce8

File tree

5 files changed

+99
-22
lines changed

5 files changed

+99
-22
lines changed

packages/parser/src/parser/postprocess/spanStrip.ts

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
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
*/
241314
export 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
}

packages/render/src/elements/expr.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,17 @@ export function renderIf(ctx: RenderContext, data: IfCondData): void {
2828

2929
/**
3030
* Render #ifexpr - evaluates expression and branches based on result
31-
* On error, selects else branch (Wikidot-compatible)
31+
* On error, outputs Wikidot-compatible error message
3232
*/
3333
export function renderIfExpr(ctx: RenderContext, data: IfExprData): void {
3434
const result = evaluateExpression(data.expression);
35-
// ifexpr: error or 0 selects else branch
36-
const isTrue = result.success && result.value !== 0;
37-
const elements = isTrue ? data.then : data.else;
35+
if (!result.success) {
36+
// ifexpr: error outputs error message (Wikidot-compatible)
37+
ctx.pushEscaped(`run-time error: ${result.error}`);
38+
return;
39+
}
40+
// 0 selects else branch, non-zero selects then branch
41+
const elements = result.value !== 0 ? data.then : data.else;
3842
renderBranchElements(ctx, elements);
3943
}
4044

packages/render/src/utils/expr-eval.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,8 @@ class ExprParser {
238238
parse(): number {
239239
const result = this.parseOr();
240240
if (this.current().kind !== "EOF") {
241-
throw new Error("Unexpected token");
241+
// Wikidot-compatible error message when extra values remain
242+
throw new Error("too many values in the stack");
242243
}
243244
return result;
244245
}
@@ -386,7 +387,7 @@ class ExprParser {
386387
}
387388
if (kind === "PLUS") {
388389
this.advance();
389-
return this.parseUnary();
390+
return +this.parseUnary();
390391
}
391392

392393
return this.parsePrimary();

tests/fixtures/expr/edge-cases/expected.json

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,16 @@
3838
"data": {
3939
"expression": "1 + + 2"
4040
}
41-
},
42-
{
43-
"element": "line-break"
44-
},
45-
{
46-
"element": "expr",
47-
"data": {
48-
"expression": ""
49-
}
50-
},
51-
{
52-
"element": "line-break"
53-
},
41+
}
42+
]
43+
}
44+
},
45+
{
46+
"element": "container",
47+
"data": {
48+
"type": "paragraph",
49+
"attributes": {},
50+
"elements": [
5451
{
5552
"element": "expr",
5653
"data": {

tests/integration/fixture-render.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const EXCLUDED_FIXTURES = new Set<string>([
1919
"module/listusers/fail", // 同上
2020
"module/pagetree", // PageTreeは動的コンテンツ(resolver未実装)
2121
"table/fail-paragraph", // リンク解釈・段落内改行処理の問題(別issueで対応)
22-
"expr/edge-cases", // エラーメッセージがWikidotと異なる(スタックベース vs 再帰下降)
22+
// "expr/edge-cases", // エラーメッセージがWikidotと異なる(スタックベース vs 再帰下降)
2323
"misc/bibliography", // bibliography機能(bibcite/bibitems)が未実装
2424
]);
2525

0 commit comments

Comments
 (0)