Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions packages/parser/src/parser/rules/block/module/include/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `]`
}

/**
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/module/include/resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("<<x>>]]]");
});
});

describe("default-value idiom (duplicate key)", () => {
Expand Down
Loading