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)", () => {