From 1c47366b53a82b1fe36cdd9979a959dd69867810 Mon Sep 17 00:00:00 2001 From: r74tech Date: Fri, 5 Jun 2026 17:07:01 +0900 Subject: [PATCH] =?UTF-8?q?fix(parser):=20[[include]]=20=E3=83=87=E3=82=A3?= =?UTF-8?q?=E3=83=AC=E3=82=AF=E3=83=86=E3=82=A3=E3=83=96=E3=81=AE=E7=B5=82?= =?UTF-8?q?=E7=AB=AF=20]]=20=E7=9B=B4=E5=BE=8C=E3=81=AE=E4=BD=99=E5=88=86?= =?UTF-8?q?=E3=81=AA=20]=20=E3=82=92=E8=A8=B1=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 最後の引数値が `]` で終わる multi-line include (例: `[[include foo\n|key=val|bar=--]|]]` の改行省略形である `[[include foo\n|key=val|bar=--]]]`) で、scanIncludeDirectives が 「行末 blank ではない」と判定して terminator を見つけられず、 include 全体を生テキストとして残していた。 isRestOfLineBlank が行末判定で `]` も whitespace と同様に skip する よう修正。終端 `]]` で directive を close し、余分な `]` は include 外の raw text として残る。 --- .../rules/block/module/include/resolve.ts | 17 +++++++++++--- tests/unit/module/include/resolve.test.ts | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/parser/src/parser/rules/block/module/include/resolve.ts b/packages/parser/src/parser/rules/block/module/include/resolve.ts index 5056169..694a703 100644 --- a/packages/parser/src/parser/rules/block/module/include/resolve.ts +++ b/packages/parser/src/parser/rules/block/module/include/resolve.ts @@ -172,15 +172,26 @@ interface IncludeDirectiveMatch { /** * Returns `true` when everything between `pos` and the next newline (or - * end of string) is whitespace — i.e. `pos` sits at the end of its line. + * end of string) is whitespace or stray `]` characters — i.e. `pos` sits + * at the end of its line, optionally trailed by extra `]` that spilled + * over from the directive's last attribute value. + * + * Wikidot wikitext occasionally contains directives whose final attribute + * value ends in `]`, e.g. `[[include foo |key=--]|]]`. When the closing + * `]]` is written without a separator (`[[include foo |key=--]]]`), the + * first `]]` after the value is the directive terminator and the + * remaining `]` belongs outside the directive. Without tolerating those + * trailing `]`, the scanner would fail to close the directive at line + * end and drop the whole include to raw text. */ function isRestOfLineBlank(source: string, pos: number): boolean { for (let i = pos; i < source.length; i++) { const ch = source[i]; if (ch === "\n") return true; - if (ch !== " " && ch !== "\t" && ch !== "\r") return false; + if (ch === " " || ch === "\t" || ch === "\r" || ch === "]") continue; + return false; } - return true; // reached EOF with only whitespace + return true; // reached EOF with only whitespace / trailing `]` } /** diff --git a/tests/unit/module/include/resolve.test.ts b/tests/unit/module/include/resolve.test.ts index 7dde7a8..bd5aad0 100644 --- a/tests/unit/module/include/resolve.test.ts +++ b/tests/unit/module/include/resolve.test.ts @@ -300,6 +300,29 @@ describe("resolveIncludes", () => { const source = "[[include tmpl |cap=[[[a]]]]]\n[[include tmpl |cap=[[[b]]]]]"; expect(resolveIncludes(source, fetcher)).toBe("<<[[[a]]]>>\n<<[[[b]]]>>"); }); + + test("multi-line directive closes when the terminating ]] is trailed by stray ]", () => { + // When a final attribute value ends in `]` and the directive's + // closing `]]` follows without a separator (`--]]]`), the first + // `]]` terminates the directive (capturing `--` as the value); + // the trailing `]` outside the directive is preserved as raw text. + const source = "[[include tmpl\n|cap= --]]]"; + expect(resolveIncludes(source, fetcher)).toBe("<<-->>]"); + }); + + test("multi-line directive with stray ]] tail and following content", () => { + // The trailing `]` after the directive close must remain in the + // output (not be swallowed by the directive). + const source = "[[include tmpl\n|cap= --]]]\n\nafter"; + expect(resolveIncludes(source, fetcher)).toBe("<<-->>]\n\nafter"); + }); + + test("multi-line directive with multiple stray ] characters", () => { + // `]]]]]` at the end: the first `]]` drives depth to zero; the + // remaining `]]]` is plain text outside the directive. + const source = "[[include tmpl\n|cap= x]]]]]"; + expect(resolveIncludes(source, fetcher)).toBe("<>]]]"); + }); }); describe("default-value idiom (duplicate key)", () => {