diff --git a/packages/parser/src/parser/parse/context.ts b/packages/parser/src/parser/parse/context.ts
index 8a466be1..c0ba29fe 100644
--- a/packages/parser/src/parser/parse/context.ts
+++ b/packages/parser/src/parser/parse/context.ts
@@ -1,6 +1,7 @@
import { DEFAULT_SETTINGS, type PageRef } from "@wdprlib/ast";
import type { Token } from "../../lexer";
import { blockFallbackRule, blockRules, inlineRules, type ParseContext } from "../rules";
+import { splitSpansAtDivBoundaries } from "../rules/inline/span/block-split";
import type { ParserOptions } from "./options";
export function createParseContext(
@@ -8,7 +9,7 @@ export function createParseContext(
options: ParserOptions = {},
deferInclude?: (location: PageRef) => boolean,
): ParseContext {
- return {
+ const ctx: ParseContext = {
tokens,
pos: 0,
version: options.version ?? "wikidot",
@@ -29,4 +30,6 @@ export function createParseContext(
footnoteBlockParsed: false,
},
};
+ ctx.tokens = splitSpansAtDivBoundaries(ctx);
+ return ctx;
}
diff --git a/packages/parser/src/parser/rules/block/html/body.ts b/packages/parser/src/parser/rules/block/html/body.ts
index e7110ab5..b6450692 100644
--- a/packages/parser/src/parser/rules/block/html/body.ts
+++ b/packages/parser/src/parser/rules/block/html/body.ts
@@ -98,7 +98,7 @@ export function consumeHtmlClose(ctx: ParseContext, startPos: number): number {
return consumed;
}
-function isHtmlCloseAt(ctx: ParseContext, pos: number): boolean {
+export function isHtmlCloseAt(ctx: ParseContext, pos: number): boolean {
if (ctx.tokens[pos]?.type !== "BLOCK_END_OPEN") {
return false;
}
diff --git a/packages/parser/src/parser/rules/contracts/scope.ts b/packages/parser/src/parser/rules/contracts/scope.ts
index ed0d4861..1ee40ad6 100644
--- a/packages/parser/src/parser/rules/contracts/scope.ts
+++ b/packages/parser/src/parser/rules/contracts/scope.ts
@@ -34,4 +34,6 @@ export interface ScopeContext {
* the original primitive semantics while avoiding rollback-unsafe shared state.
*/
readonly footnoteBlockParsed: boolean;
+ /** Number of enclosing footnote bodies. */
+ readonly footnoteDepth?: number;
}
diff --git a/packages/parser/src/parser/rules/inline/footnote/boundary.ts b/packages/parser/src/parser/rules/inline/footnote/boundary.ts
index 83304243..082f275d 100644
--- a/packages/parser/src/parser/rules/inline/footnote/boundary.ts
+++ b/packages/parser/src/parser/rules/inline/footnote/boundary.ts
@@ -54,3 +54,39 @@ export function findFootnoteEnd(ctx: ParseContext, start: number): number {
}
return ctx.tokens.length;
}
+
+/**
+ * Map each footnote opener to the end of its close in one pass, using the same
+ * protected regions and nesting as {@link findFootnoteEnd}.
+ */
+export function matchFootnoteCloses(ctx: ParseContext): Map ` で包むかどうかです。
+実機は ` ` で包まずに ` ` で包みます。
+この差は span を含まない `[[div]]` の直後でも同じように出ます。
+
+入れ子の span が両方とも div をまたぐ場合、実機は外側の span だけを
+開き直し、WDPR は両方を開き直します。この入力は本 fixture に含めません。
diff --git a/tests/fixtures/span/div-boundary/expected-diagnostics.json b/tests/fixtures/span/div-boundary/expected-diagnostics.json
new file mode 100644
index 00000000..fe51488c
--- /dev/null
+++ b/tests/fixtures/span/div-boundary/expected-diagnostics.json
@@ -0,0 +1 @@
+[]
diff --git a/tests/fixtures/span/div-boundary/expected.json b/tests/fixtures/span/div-boundary/expected.json
new file mode 100644
index 00000000..05475e36
--- /dev/null
+++ b/tests/fixtures/span/div-boundary/expected.json
@@ -0,0 +1,403 @@
+{
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "div",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "paragraph",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "span",
+ "attributes": {
+ "class": "k"
+ },
+ "elements": [
+ {
+ "element": "text",
+ "data": "a"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "element": "container",
+ "data": {
+ "type": "paragraph",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "span",
+ "attributes": {
+ "class": "k"
+ },
+ "elements": [
+ {
+ "element": "line-break"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "element": "container",
+ "data": {
+ "type": "div",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "paragraph",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "span",
+ "attributes": {
+ "style": "color:red"
+ },
+ "elements": [
+ {
+ "element": "text",
+ "data": "b"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "element": "container",
+ "data": {
+ "type": "paragraph",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "span",
+ "attributes": {
+ "style": "color:red"
+ },
+ "elements": [
+ {
+ "element": "line-break"
+ },
+ {
+ "element": "text",
+ "data": "c"
+ },
+ {
+ "element": "text",
+ "data": " "
+ },
+ {
+ "element": "container",
+ "data": {
+ "type": "bold",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "text",
+ "data": "d"
+ }
+ ]
+ }
+ },
+ {
+ "element": "line-break"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "element": "container",
+ "data": {
+ "type": "paragraph",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "span",
+ "attributes": {
+ "class": "k"
+ },
+ "elements": [
+ {
+ "element": "text",
+ "data": "e"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "element": "container",
+ "data": {
+ "type": "div",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "paragraph",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "span",
+ "attributes": {
+ "class": "k"
+ },
+ "elements": [
+ {
+ "element": "text",
+ "data": "f"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "element": "container",
+ "data": {
+ "type": "paragraph",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "span",
+ "attributes": {
+ "class": "k"
+ },
+ "elements": [
+ {
+ "element": "line-break"
+ },
+ {
+ "element": "text",
+ "data": "g"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "element": "container",
+ "data": {
+ "type": "div",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "div",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "paragraph",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "span",
+ "attributes": {
+ "class": "k"
+ },
+ "elements": [
+ {
+ "element": "text",
+ "data": "h"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "element": "container",
+ "data": {
+ "type": "paragraph",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "span",
+ "attributes": {
+ "class": "k"
+ },
+ "elements": [
+ {
+ "element": "line-break"
+ },
+ {
+ "element": "text",
+ "data": "i"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "element": "container",
+ "data": {
+ "type": "paragraph",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "span",
+ "attributes": {
+ "class": "k"
+ },
+ "elements": [
+ {
+ "element": "line-break"
+ },
+ {
+ "element": "text",
+ "data": "j"
+ },
+ {
+ "element": "line-break"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "element": "container",
+ "data": {
+ "type": "div",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "paragraph",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "text",
+ "data": "k"
+ },
+ {
+ "element": "text",
+ "data": " "
+ },
+ {
+ "element": "container",
+ "data": {
+ "type": "span",
+ "attributes": {
+ "class": "k"
+ },
+ "elements": [
+ {
+ "element": "text",
+ "data": "l"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "element": "container",
+ "data": {
+ "type": "paragraph",
+ "attributes": {},
+ "elements": [
+ {
+ "element": "container",
+ "data": {
+ "type": "span",
+ "attributes": {
+ "class": "k"
+ },
+ "elements": [
+ {
+ "element": "text",
+ "data": "m"
+ }
+ ]
+ }
+ },
+ {
+ "element": "text",
+ "data": " "
+ },
+ {
+ "element": "text",
+ "data": "n"
+ }
+ ]
+ }
+ },
+ {
+ "element": "footnote-block",
+ "data": {
+ "title": null,
+ "hide": false
+ }
+ }
+ ]
+}
diff --git a/tests/fixtures/span/div-boundary/input.ftml b/tests/fixtures/span/div-boundary/input.ftml
new file mode 100644
index 00000000..4bd83258
--- /dev/null
+++ b/tests/fixtures/span/div-boundary/input.ftml
@@ -0,0 +1,28 @@
+[[div]]
+[[span class="k"]]a
+[[/div]]
+[[/span]]
+
+[[div]]
+[[span style="color:red"]]b
+[[/div]]
+c **d**
+[[/span]]
+
+[[span class="k"]]e
+[[div]]
+f
+[[/div]]
+g[[/span]]
+
+[[div]]
+[[div]]
+[[span class="k"]]h
+[[/div]]
+i
+[[/div]]
+j
+[[/span]]
+
+[[div]]
+k [[span class="k"]]l [[/div]] m[[/span]] n
\ No newline at end of file
diff --git a/tests/fixtures/span/div-boundary/output.html b/tests/fixtures/span/div-boundary/output.html
new file mode 100644
index 00000000..50e4e314
--- /dev/null
+++ b/tests/fixtures/span/div-boundary/output.html
@@ -0,0 +1 @@
+ a b e f h k l m n a b e f h k l
` から始めますが、WDPR は `
c d
g
i
j
+
+c d
+
+g
+
+i
+j
+
a
e
f
g
a")).toBe(true);
+ expect(html.match(/ a b b b b a b
c
[[html]]
c