@wdprlib/parser 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2074 -1137
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2074 -1137
- package/package.json +3 -2
- package/src/build-info.generated.ts +2 -2
- package/src/lexer/lexer.ts +34 -5
- package/src/lexer/quoted-string.ts +4 -4
- package/src/lexer/syntax-actions.ts +6 -1
- package/src/lexer/text-actions.ts +15 -1
- package/src/lexer/url-schemes.ts +78 -0
- package/src/parser/constants.ts +4 -0
- package/src/parser/parse/block.ts +1 -1
- package/src/parser/postprocess/divAdjacentParagraph.ts +23 -21
- package/src/parser/postprocess/spanStrip/merge.ts +8 -2
- package/src/parser/preprocess/typography.ts +25 -3
- package/src/parser/preprocess/utils/raw-regions.ts +59 -14
- package/src/parser/preprocess/whitespace/index.ts +8 -1
- package/src/parser/rules/block/bibliography/entry-content.ts +1 -1
- package/src/parser/rules/block/block-list/bare-content.ts +3 -1
- package/src/parser/rules/block/block-list/bare-paragraph.ts +7 -2
- package/src/parser/rules/block/block-list/item-content.ts +7 -3
- package/src/parser/rules/block/block-list/li-content.ts +8 -3
- package/src/parser/rules/block/block-list/li-item.ts +1 -1
- package/src/parser/rules/block/blockquote/build.ts +1 -1
- package/src/parser/rules/block/code/boundary.ts +76 -0
- package/src/parser/rules/block/code/content.ts +11 -40
- package/src/parser/rules/block/code/index.ts +8 -31
- package/src/parser/rules/block/code/open.ts +46 -0
- package/src/parser/rules/block/definition-list/item-key.ts +1 -1
- package/src/parser/rules/block/definition-list/item-value.ts +1 -1
- package/src/parser/rules/block/div/failed.ts +2 -0
- package/src/parser/rules/block/index.ts +3 -0
- package/src/parser/rules/block/list/line.ts +6 -3
- package/src/parser/rules/block/math/boundary.ts +104 -0
- package/src/parser/rules/block/math/index.ts +17 -57
- package/src/parser/rules/block/note/boundary.ts +88 -0
- package/src/parser/rules/block/note/index.ts +37 -0
- package/src/parser/rules/block/paragraph/index.ts +12 -4
- package/src/parser/rules/block/paragraph/normalize.ts +6 -1
- package/src/parser/rules/block/parsing/block-item.ts +1 -1
- package/src/parser/rules/block/parsing/content.ts +1 -1
- package/src/parser/rules/block/parsing/inline-content.ts +6 -3
- package/src/parser/rules/block/table/pipe/cell.ts +3 -1
- package/src/parser/rules/block/table-block/cell-content/index.ts +1 -1
- package/src/parser/rules/block/table-block/cell-content/segments.ts +5 -2
- package/src/parser/rules/block/table-block/cell-newline.ts +2 -1
- package/src/parser/rules/contracts/rule.ts +4 -2
- package/src/parser/rules/contracts/scope.ts +2 -0
- package/src/parser/rules/inline/anchor/child.ts +8 -2
- package/src/parser/rules/inline/anchor/content.ts +3 -1
- package/src/parser/rules/inline/anchor/index.ts +4 -1
- package/src/parser/rules/inline/anchor/newline.ts +2 -1
- package/src/parser/rules/inline/autolink.ts +153 -0
- package/src/parser/rules/inline/button/attributes.ts +17 -0
- package/src/parser/rules/inline/button/index.ts +17 -0
- package/src/parser/rules/inline/button/syntax.ts +56 -0
- package/src/parser/rules/inline/date/index.ts +17 -0
- package/src/parser/rules/inline/date/syntax.ts +46 -0
- package/src/parser/rules/inline/email/candidates.ts +134 -0
- package/src/parser/rules/inline/email/index.ts +36 -0
- package/src/parser/rules/inline/email/scan.ts +76 -0
- package/src/parser/rules/inline/expr/branch.ts +3 -1
- package/src/parser/rules/inline/footnote/boundary.ts +56 -0
- package/src/parser/rules/inline/footnote/content.ts +29 -41
- package/src/parser/rules/inline/footnote/elements.ts +9 -34
- package/src/parser/rules/inline/footnote/index.ts +4 -1
- package/src/parser/rules/inline/formatting/close.ts +12 -0
- package/src/parser/rules/inline/index.ts +14 -0
- package/src/parser/rules/inline/line-break/newline.ts +8 -1
- package/src/parser/rules/inline/link-bracket/direct-url.ts +11 -3
- package/src/parser/rules/inline/link-bracket/parsed.ts +9 -4
- package/src/parser/rules/inline/link-bracket/parts.ts +14 -36
- package/src/parser/rules/inline/link-bracket/special-target.ts +9 -0
- package/src/parser/rules/inline/link-single.ts +9 -7
- package/src/parser/rules/inline/link-triple/index.ts +1 -0
- package/src/parser/rules/inline/link-triple/label.ts +7 -1
- package/src/parser/rules/inline/parsing/automatic-line-break.ts +35 -0
- package/src/parser/rules/inline/parsing/block-boundary.ts +2 -0
- package/src/parser/rules/inline/parsing/block-start-predicates.ts +10 -0
- package/src/parser/rules/inline/parsing/inline-content.ts +36 -6
- package/src/parser/rules/inline/parsing/plain-text.ts +7 -2
- package/src/parser/rules/inline/parsing/preserved-line-break.ts +13 -0
- package/src/parser/rules/inline/parsing/raw-tag.ts +19 -0
- package/src/parser/rules/inline/raw/angle.ts +2 -1
- package/src/parser/rules/inline/raw/end.ts +21 -1
- package/src/parser/rules/inline/size/content.ts +1 -1
- package/src/parser/rules/inline/size/value.ts +11 -0
- package/src/parser/rules/inline/social/index.ts +17 -0
- package/src/parser/rules/inline/social/syntax.ts +40 -0
- package/src/parser/rules/inline/span/content.ts +3 -1
- package/src/parser/rules/inline/span/newline.ts +2 -1
- package/src/parser/rules/opaque-probe.ts +58 -0
- package/src/parser/rules/block/math/content.ts +0 -54
- package/src/parser/rules/block/math/name.ts +0 -35
- package/src/parser/rules/inline/footnote/child.ts +0 -22
- package/src/parser/rules/inline/footnote/newline.ts +0 -27
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Token } from "../../../../lexer";
|
|
2
|
+
import { stripAutomaticLineBreak } from "../../inline/parsing/automatic-line-break";
|
|
1
3
|
import type { Element } from "@wdprlib/ast";
|
|
2
4
|
import type { ParseContext } from "../../types";
|
|
3
5
|
import { getCandidateInlineRules } from "../../inline/utils";
|
|
@@ -8,6 +10,7 @@ interface ListItemContentResult {
|
|
|
8
10
|
matched: boolean;
|
|
9
11
|
elements: Element[];
|
|
10
12
|
consumed: number;
|
|
13
|
+
stripLeadingLineBreak?: Token;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
const blockListExcludedRulesCache = new WeakMap<
|
|
@@ -26,7 +29,7 @@ export function parseListItemBlockContent(
|
|
|
26
29
|
for (const rule of getCandidateBlockRules(filteredBlockRules, token)) {
|
|
27
30
|
const result = rule.parse(blockCtx);
|
|
28
31
|
if (result.success) {
|
|
29
|
-
return { matched: true,
|
|
32
|
+
return { matched: true, ...result };
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -56,7 +59,7 @@ export function parseListItemInlineContent(
|
|
|
56
59
|
for (const rule of getCandidateInlineRules(ctx.inlineRules, tokenType)) {
|
|
57
60
|
const result = rule.parse(inlineCtx);
|
|
58
61
|
if (result.success) {
|
|
59
|
-
return { matched: true,
|
|
62
|
+
return { matched: true, ...result };
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -108,7 +111,8 @@ export function collectPostLiTrailingContent(
|
|
|
108
111
|
|
|
109
112
|
const inlineResult = parseListItemInlineContent(ctx, pos, token.type);
|
|
110
113
|
if (inlineResult.matched) {
|
|
111
|
-
elements
|
|
114
|
+
stripAutomaticLineBreak(elements, inlineResult.stripLeadingLineBreak);
|
|
115
|
+
for (const element of inlineResult.elements) elements.push(element);
|
|
112
116
|
consumed += inlineResult.consumed;
|
|
113
117
|
pos += inlineResult.consumed;
|
|
114
118
|
continue;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAutomaticLineBreak,
|
|
3
|
+
stripAutomaticLineBreak,
|
|
4
|
+
} from "../../inline/parsing/automatic-line-break";
|
|
1
5
|
import type { Element } from "@wdprlib/ast";
|
|
2
6
|
import type { ParseContext } from "../../types";
|
|
3
7
|
import { parseListItemBlockContent, parseListItemInlineContent } from "./item-content";
|
|
@@ -50,7 +54,7 @@ export function collectLiItemContent(
|
|
|
50
54
|
if (token.type === "NEWLINE") {
|
|
51
55
|
const newlineResult = consumeLiItemNewlines(ctx, pos, elements.length > 0);
|
|
52
56
|
if (newlineResult.addLineBreak) {
|
|
53
|
-
elements.push(
|
|
57
|
+
elements.push(createAutomaticLineBreak(token));
|
|
54
58
|
}
|
|
55
59
|
pos += newlineResult.consumed;
|
|
56
60
|
consumed += newlineResult.consumed;
|
|
@@ -59,7 +63,7 @@ export function collectLiItemContent(
|
|
|
59
63
|
|
|
60
64
|
const blockResult = parseListItemBlockContent(ctx, pos, token);
|
|
61
65
|
if (blockResult.matched) {
|
|
62
|
-
|
|
66
|
+
for (const element of blockResult.elements) elements.push(element);
|
|
63
67
|
consumed += blockResult.consumed;
|
|
64
68
|
pos += blockResult.consumed;
|
|
65
69
|
continue;
|
|
@@ -67,7 +71,8 @@ export function collectLiItemContent(
|
|
|
67
71
|
|
|
68
72
|
const inlineResult = parseListItemInlineContent(ctx, pos, token.type);
|
|
69
73
|
if (inlineResult.matched) {
|
|
70
|
-
elements
|
|
74
|
+
stripAutomaticLineBreak(elements, inlineResult.stripLeadingLineBreak);
|
|
75
|
+
for (const element of inlineResult.elements) elements.push(element);
|
|
71
76
|
consumed += inlineResult.consumed;
|
|
72
77
|
pos += inlineResult.consumed;
|
|
73
78
|
continue;
|
|
@@ -62,7 +62,7 @@ export function parseLiItem(
|
|
|
62
62
|
pos += closeConsumed;
|
|
63
63
|
|
|
64
64
|
const trailingResult = collectPostLiTrailingContent(ctx, pos, listType);
|
|
65
|
-
|
|
65
|
+
for (const element of trailingResult.elements) contentElements.push(element);
|
|
66
66
|
consumed += trailingResult.consumed;
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -88,7 +88,7 @@ function sliceLineTokens(ctx: ParseContext, lines: BlockquoteLine[]): Token[] {
|
|
|
88
88
|
for (const { start, end } of lines) {
|
|
89
89
|
for (let pos = start; pos < end; pos++) {
|
|
90
90
|
const token = ctx.tokens[pos];
|
|
91
|
-
if (token) {
|
|
91
|
+
if (token && token.type !== "BACKSLASH_BREAK") {
|
|
92
92
|
tokens.push(token);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Token } from "../../../../lexer";
|
|
2
|
+
import { rawRegionEnd } from "../../inline/raw/end";
|
|
3
|
+
import { findCodeOpen } from "./open";
|
|
4
|
+
|
|
5
|
+
interface CodeBodyBounds {
|
|
6
|
+
closeStart: number;
|
|
7
|
+
end: number;
|
|
8
|
+
foundClose: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const caches = new WeakMap<readonly Token[], Map<number, CodeBodyBounds>>();
|
|
12
|
+
|
|
13
|
+
function codeCloseEnd(tokens: readonly Token[], start: number): number {
|
|
14
|
+
if (tokens[start]?.type !== "BLOCK_END_OPEN" || tokens[start + 1]?.value.toLowerCase() !== "code")
|
|
15
|
+
return start;
|
|
16
|
+
let end = start + 2;
|
|
17
|
+
while (tokens[end]?.type === "WHITESPACE") end++;
|
|
18
|
+
return tokens[end]?.type === "BLOCK_CLOSE" ? end + 1 : start;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function nestedCodeBodyStart(tokens: readonly Token[], start: number): number {
|
|
22
|
+
if (tokens[start]?.type !== "BLOCK_OPEN" || tokens[start + 1]?.value.toLowerCase() !== "code")
|
|
23
|
+
return start;
|
|
24
|
+
for (let pos = start + 2; pos < tokens.length; pos++) {
|
|
25
|
+
const type = tokens[pos]?.type;
|
|
26
|
+
if (type === "NEWLINE" || type === "EOF" || type === "BLOCK_OPEN") return start;
|
|
27
|
+
if (type === "BLOCK_CLOSE") {
|
|
28
|
+
const open = findCodeOpen(tokens, start);
|
|
29
|
+
return open && !open.repaired ? open.bodyStart : start;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return start;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function findCodeBodyBounds(tokens: readonly Token[], start: number): CodeBodyBounds {
|
|
36
|
+
let cache = caches.get(tokens);
|
|
37
|
+
if (!cache) {
|
|
38
|
+
cache = new Map();
|
|
39
|
+
caches.set(tokens, cache);
|
|
40
|
+
}
|
|
41
|
+
const cached = cache.get(start);
|
|
42
|
+
if (cached) return cached;
|
|
43
|
+
const stack = [start];
|
|
44
|
+
let pos = start;
|
|
45
|
+
while (pos < tokens.length && tokens[pos]?.type !== "EOF") {
|
|
46
|
+
const rawEnd = rawRegionEnd(tokens, pos, tokens.length);
|
|
47
|
+
if (rawEnd > pos) {
|
|
48
|
+
pos = rawEnd;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const end = codeCloseEnd(tokens, pos);
|
|
52
|
+
if (end > pos) {
|
|
53
|
+
const result = { closeStart: pos, end, foundClose: true };
|
|
54
|
+
cache.set(stack.pop()!, result);
|
|
55
|
+
if (stack.length === 0) return result;
|
|
56
|
+
pos = end;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const bodyStart = nestedCodeBodyStart(tokens, pos);
|
|
60
|
+
if (bodyStart > pos) {
|
|
61
|
+
const child = cache.get(bodyStart);
|
|
62
|
+
if (child) {
|
|
63
|
+
pos = child.end;
|
|
64
|
+
if (!child.foundClose) break;
|
|
65
|
+
} else {
|
|
66
|
+
stack.push(bodyStart);
|
|
67
|
+
pos = bodyStart;
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
pos++;
|
|
72
|
+
}
|
|
73
|
+
const result = { closeStart: pos, end: pos, foundClose: false };
|
|
74
|
+
for (const bodyStart of stack) cache.set(bodyStart, result);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ParseContext } from "../../types";
|
|
2
|
-
import {
|
|
2
|
+
import { findCodeBodyBounds } from "./boundary";
|
|
3
3
|
|
|
4
4
|
export interface CodeContentResult {
|
|
5
5
|
contents: string;
|
|
@@ -12,43 +12,14 @@ export function collectCodeContent(
|
|
|
12
12
|
startPos: number,
|
|
13
13
|
closingSwallowed: boolean,
|
|
14
14
|
): CodeContentResult {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (token.type === "BLOCK_END_OPEN") {
|
|
27
|
-
const closeNameResult = parseBlockName(ctx, pos + 1);
|
|
28
|
-
if (closeNameResult?.name === "code") {
|
|
29
|
-
foundClose = true;
|
|
30
|
-
const closeConsumed = consumeCodeClose(ctx, pos, closeNameResult.consumed);
|
|
31
|
-
consumed += closeConsumed;
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
contentParts.push(token.value);
|
|
37
|
-
pos++;
|
|
38
|
-
consumed++;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return { contents: contentParts.join(""), consumed, foundClose };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function consumeCodeClose(ctx: ParseContext, startPos: number, closeNameConsumed: number): number {
|
|
45
|
-
let pos = startPos + 1 + closeNameConsumed;
|
|
46
|
-
let consumed = 1 + closeNameConsumed;
|
|
47
|
-
|
|
48
|
-
if (ctx.tokens[pos]?.type === "BLOCK_CLOSE") {
|
|
49
|
-
pos++;
|
|
50
|
-
consumed++;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return consumed;
|
|
15
|
+
if (closingSwallowed) return { contents: "", consumed: 0, foundClose: true };
|
|
16
|
+
const bounds = findCodeBodyBounds(ctx.tokens, startPos);
|
|
17
|
+
return {
|
|
18
|
+
contents: ctx.tokens
|
|
19
|
+
.slice(startPos, bounds.closeStart)
|
|
20
|
+
.map((token) => token.value)
|
|
21
|
+
.join(""),
|
|
22
|
+
consumed: bounds.end - startPos,
|
|
23
|
+
foundClose: bounds.foundClose,
|
|
24
|
+
};
|
|
54
25
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import type { Element, CodeBlockData } from "@wdprlib/ast";
|
|
13
13
|
import type { BlockRule, ParseContext, RuleResult } from "../../types";
|
|
14
14
|
import { currentToken } from "../../types";
|
|
15
|
-
import {
|
|
15
|
+
import { findCodeOpen } from "./open";
|
|
16
16
|
import { parseAttributesRaw } from "../utils";
|
|
17
17
|
import { repairSwallowedCodeClose } from "./attributes";
|
|
18
18
|
import { parseInlineUntil } from "../../inline/utils";
|
|
@@ -38,37 +38,14 @@ export const codeBlockRule: BlockRule = {
|
|
|
38
38
|
return { success: false };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const open = findCodeOpen(ctx.tokens, ctx.pos);
|
|
42
|
+
if (!open) return { success: false };
|
|
43
|
+
const attrResult = parseAttributesRaw(ctx, ctx.pos + 2);
|
|
44
|
+
if (open.repaired) repairSwallowedCodeClose(ctx, open.attributesEnd, attrResult.attrs);
|
|
45
|
+
let pos = open.bodyStart;
|
|
46
|
+
let consumed = pos - ctx.pos;
|
|
43
47
|
|
|
44
|
-
const
|
|
45
|
-
if (!nameResult || nameResult.name !== "code") {
|
|
46
|
-
return { success: false };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
pos += nameResult.consumed;
|
|
50
|
-
consumed += nameResult.consumed;
|
|
51
|
-
|
|
52
|
-
const attrResult = parseAttributesRaw(ctx, pos);
|
|
53
|
-
pos += attrResult.consumed;
|
|
54
|
-
consumed += attrResult.consumed;
|
|
55
|
-
|
|
56
|
-
let closingSwallowed = false;
|
|
57
|
-
if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
|
|
58
|
-
const repaired = repairSwallowedCodeClose(ctx, pos, attrResult.attrs);
|
|
59
|
-
if (!repaired) return { success: false };
|
|
60
|
-
closingSwallowed = repaired.closingSwallowed;
|
|
61
|
-
} else {
|
|
62
|
-
pos++;
|
|
63
|
-
consumed++;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
67
|
-
pos++;
|
|
68
|
-
consumed++;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const contentResult = collectCodeContent(ctx, pos, closingSwallowed);
|
|
48
|
+
const contentResult = collectCodeContent(ctx, pos, open.closingSwallowed);
|
|
72
49
|
let codeContent = contentResult.contents.replace(/\n$/, "");
|
|
73
50
|
consumed += contentResult.consumed;
|
|
74
51
|
pos += contentResult.consumed;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Token } from "../../../../lexer";
|
|
2
|
+
|
|
3
|
+
interface CodeOpenBounds {
|
|
4
|
+
attributesEnd: number;
|
|
5
|
+
bodyStart: number;
|
|
6
|
+
repaired: boolean;
|
|
7
|
+
closingSwallowed: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const attributeRanges = new WeakMap<readonly Token[], { start: number; end: number }>();
|
|
11
|
+
|
|
12
|
+
function findAttributesEnd(tokens: readonly Token[], start: number): number {
|
|
13
|
+
const cached = attributeRanges.get(tokens);
|
|
14
|
+
if (cached && start >= cached.start && start <= cached.end) return cached.end;
|
|
15
|
+
let end = start;
|
|
16
|
+
while (end < tokens.length) {
|
|
17
|
+
const type = tokens[end]?.type;
|
|
18
|
+
if (type === "BLOCK_CLOSE" || type === "NEWLINE" || type === "EOF") break;
|
|
19
|
+
end++;
|
|
20
|
+
}
|
|
21
|
+
attributeRanges.set(tokens, { start, end });
|
|
22
|
+
return end;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function findCodeOpen(tokens: readonly Token[], start: number): CodeOpenBounds | null {
|
|
26
|
+
const name = tokens[start + 1];
|
|
27
|
+
if (
|
|
28
|
+
tokens[start]?.type !== "BLOCK_OPEN" ||
|
|
29
|
+
(name?.type !== "IDENTIFIER" && name?.type !== "TEXT") ||
|
|
30
|
+
name.value.toLowerCase() !== "code" ||
|
|
31
|
+
tokens[start + 2]?.type === "UNDERSCORE"
|
|
32
|
+
)
|
|
33
|
+
return null;
|
|
34
|
+
const attributesEnd = findAttributesEnd(tokens, start + 2);
|
|
35
|
+
const repaired = tokens[attributesEnd]?.type !== "BLOCK_CLOSE";
|
|
36
|
+
const last = tokens[attributesEnd - 1];
|
|
37
|
+
if (repaired && (last?.type !== "QUOTED_STRING" || !last.value.includes("]]"))) return null;
|
|
38
|
+
let bodyStart = attributesEnd + (repaired ? 0 : 1);
|
|
39
|
+
if (tokens[bodyStart]?.type === "NEWLINE") bodyStart++;
|
|
40
|
+
return {
|
|
41
|
+
attributesEnd,
|
|
42
|
+
bodyStart,
|
|
43
|
+
repaired,
|
|
44
|
+
closingSwallowed: repaired && last!.value.includes("[[/code]]"),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -51,7 +51,7 @@ export function parseDefinitionItemKey(
|
|
|
51
51
|
const inlineCtx: ParseContext = { ...ctx, pos };
|
|
52
52
|
const result = parseInlineUntil(inlineCtx, "COLON");
|
|
53
53
|
if (result.elements.length > 0) {
|
|
54
|
-
|
|
54
|
+
for (const element of result.elements) key.push(element);
|
|
55
55
|
for (let i = 0; i < result.consumed; i++) {
|
|
56
56
|
const t = ctx.tokens[pos + i];
|
|
57
57
|
if (t) keyTokens.push(t.value);
|
|
@@ -34,7 +34,7 @@ export function parseDefinitionItemValue(
|
|
|
34
34
|
const inlineCtx: ParseContext = { ...ctx, pos };
|
|
35
35
|
const result = parseInlineUntil(inlineCtx, "NEWLINE");
|
|
36
36
|
if (result.elements.length > 0) {
|
|
37
|
-
|
|
37
|
+
for (const element of result.elements) value.push(element);
|
|
38
38
|
pos += result.consumed;
|
|
39
39
|
consumed += result.consumed;
|
|
40
40
|
} else {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { markUnparsedDivStart } from "../../../postprocess/divAdjacentParagraph";
|
|
1
2
|
import type { Element } from "@wdprlib/ast";
|
|
2
3
|
import type { ParseContext, RuleResult } from "../../types";
|
|
3
4
|
import { parseAttributes, parseBlockName } from "../utils";
|
|
@@ -100,6 +101,7 @@ export function consumeFailedDiv(ctx: ParseContext): RuleResult<Element> {
|
|
|
100
101
|
consumed++;
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
markUnparsedDivStart(elements);
|
|
103
105
|
return {
|
|
104
106
|
success: true,
|
|
105
107
|
elements: [
|
|
@@ -27,6 +27,7 @@ import { blockListRule } from "./block-list";
|
|
|
27
27
|
import { blockquoteRule } from "./blockquote";
|
|
28
28
|
import { definitionListRule } from "./definition-list";
|
|
29
29
|
import { paragraphRule } from "./paragraph";
|
|
30
|
+
import { noteRule } from "./note";
|
|
30
31
|
import { divRule } from "./div";
|
|
31
32
|
import { codeBlockRule } from "./code";
|
|
32
33
|
import { collapsibleRule } from "./collapsible";
|
|
@@ -58,6 +59,7 @@ export { blockListRule } from "./block-list";
|
|
|
58
59
|
export { blockquoteRule } from "./blockquote";
|
|
59
60
|
export { definitionListRule } from "./definition-list";
|
|
60
61
|
export { paragraphRule } from "./paragraph";
|
|
62
|
+
export { noteRule } from "./note";
|
|
61
63
|
export { divRule } from "./div";
|
|
62
64
|
export { codeBlockRule } from "./code";
|
|
63
65
|
export { collapsibleRule } from "./collapsible";
|
|
@@ -123,6 +125,7 @@ export const blockRules: BlockRule[] = [
|
|
|
123
125
|
iftagsRule,
|
|
124
126
|
bibliographyRule,
|
|
125
127
|
galleryRule,
|
|
128
|
+
noteRule,
|
|
126
129
|
divRule,
|
|
127
130
|
// paragraphRule is not included - used as fallback
|
|
128
131
|
];
|
|
@@ -51,10 +51,13 @@ export function parseNativeListLine(ctx: ParseContext, startPos: number): ListLi
|
|
|
51
51
|
pos++;
|
|
52
52
|
consumed++;
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
// Wikidotのリスト構文はマーカー直後の空白が必須(Text_Wiki List.php: `(\*|#) `)。
|
|
55
|
+
// 空白なしの行(例: 生URLの `*http://...`)はリストではなくインライン内容として扱う
|
|
56
|
+
if (ctx.tokens[pos]?.type !== "WHITESPACE") {
|
|
57
|
+
return { kind: "stop" };
|
|
57
58
|
}
|
|
59
|
+
pos++;
|
|
60
|
+
consumed++;
|
|
58
61
|
|
|
59
62
|
const inlineCtx: ParseContext = { ...ctx, pos };
|
|
60
63
|
const inlineResult = parseInlineUntil(inlineCtx, "NEWLINE");
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Token } from "../../../../lexer";
|
|
2
|
+
|
|
3
|
+
interface MathOpenBounds {
|
|
4
|
+
nameStart: number;
|
|
5
|
+
nameEnd: number;
|
|
6
|
+
bodyStart: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface MathBodyBounds {
|
|
10
|
+
closeStart: number;
|
|
11
|
+
end: number;
|
|
12
|
+
foundClose: boolean;
|
|
13
|
+
hasContent: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const nameRanges = new WeakMap<readonly Token[], { start: number; end: number }>();
|
|
17
|
+
const bodyCaches = new WeakMap<readonly Token[], Map<number, MathBodyBounds>>();
|
|
18
|
+
|
|
19
|
+
function findNameEnd(tokens: readonly Token[], start: number): number {
|
|
20
|
+
const cached = nameRanges.get(tokens);
|
|
21
|
+
if (cached && start >= cached.start && start <= cached.end) return cached.end;
|
|
22
|
+
let end = start;
|
|
23
|
+
while (end < tokens.length) {
|
|
24
|
+
const type = tokens[end]?.type;
|
|
25
|
+
if (type === "BLOCK_CLOSE" || type === "WHITESPACE" || type === "NEWLINE" || type === "EOF")
|
|
26
|
+
break;
|
|
27
|
+
end++;
|
|
28
|
+
}
|
|
29
|
+
nameRanges.set(tokens, { start, end });
|
|
30
|
+
return end;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function findMathOpen(tokens: readonly Token[], start: number): MathOpenBounds | null {
|
|
34
|
+
const name = tokens[start + 1];
|
|
35
|
+
if (
|
|
36
|
+
tokens[start]?.type !== "BLOCK_OPEN" ||
|
|
37
|
+
(name?.type !== "IDENTIFIER" && name?.type !== "TEXT") ||
|
|
38
|
+
name.value.toLowerCase() !== "math" ||
|
|
39
|
+
tokens[start + 2]?.type === "UNDERSCORE"
|
|
40
|
+
)
|
|
41
|
+
return null;
|
|
42
|
+
let pos = start + 2;
|
|
43
|
+
while (tokens[pos]?.type === "WHITESPACE") pos++;
|
|
44
|
+
const nameStart = pos;
|
|
45
|
+
if (tokens[pos]?.type === "IDENTIFIER" || tokens[pos]?.type === "TEXT") {
|
|
46
|
+
pos = findNameEnd(tokens, pos);
|
|
47
|
+
}
|
|
48
|
+
const nameEnd = pos;
|
|
49
|
+
while (tokens[pos]?.type === "WHITESPACE") pos++;
|
|
50
|
+
if (tokens[pos]?.type !== "BLOCK_CLOSE") return null;
|
|
51
|
+
pos++;
|
|
52
|
+
if (tokens[pos]?.type === "NEWLINE") pos++;
|
|
53
|
+
return { nameStart, nameEnd, bodyStart: pos };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mathCloseEnd(tokens: readonly Token[], start: number): number {
|
|
57
|
+
if (
|
|
58
|
+
tokens[start]?.type !== "BLOCK_END_OPEN" ||
|
|
59
|
+
tokens[start + 1]?.value.toLowerCase() !== "math" ||
|
|
60
|
+
tokens[start + 2]?.type !== "BLOCK_CLOSE"
|
|
61
|
+
)
|
|
62
|
+
return start;
|
|
63
|
+
const end = start + 3;
|
|
64
|
+
return tokens[end]?.type === "NEWLINE" ? end + 1 : end;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function findMathBodyBounds(tokens: readonly Token[], start: number): MathBodyBounds {
|
|
68
|
+
let cache = bodyCaches.get(tokens);
|
|
69
|
+
if (!cache) {
|
|
70
|
+
cache = new Map();
|
|
71
|
+
bodyCaches.set(tokens, cache);
|
|
72
|
+
}
|
|
73
|
+
const visited: number[] = [];
|
|
74
|
+
let result: MathBodyBounds = {
|
|
75
|
+
closeStart: tokens.length,
|
|
76
|
+
end: tokens.length,
|
|
77
|
+
foundClose: false,
|
|
78
|
+
hasContent: false,
|
|
79
|
+
};
|
|
80
|
+
for (let pos = start; pos < tokens.length; pos++) {
|
|
81
|
+
const cached = cache.get(pos);
|
|
82
|
+
if (cached) {
|
|
83
|
+
result = cached;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
const end = mathCloseEnd(tokens, pos);
|
|
87
|
+
if (end > pos) {
|
|
88
|
+
result = { closeStart: pos, end, foundClose: true, hasContent: false };
|
|
89
|
+
cache.set(pos, result);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
visited.push(pos);
|
|
93
|
+
}
|
|
94
|
+
// Empty-body detection must also be reusable without joining each candidate's suffix.
|
|
95
|
+
for (let i = visited.length - 1; i >= 0; i--) {
|
|
96
|
+
const pos = visited[i]!;
|
|
97
|
+
const token = tokens[pos]!;
|
|
98
|
+
if (!result.hasContent && (token.type === "BACKSLASH_BREAK" || token.value.trim() !== "")) {
|
|
99
|
+
result = { ...result, hasContent: true };
|
|
100
|
+
}
|
|
101
|
+
cache.set(pos, result);
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
@@ -11,9 +11,7 @@
|
|
|
11
11
|
import type { Element } from "@wdprlib/ast";
|
|
12
12
|
import type { BlockRule, ParseContext, RuleResult } from "../../types";
|
|
13
13
|
import { currentToken } from "../../types";
|
|
14
|
-
import {
|
|
15
|
-
import { collectMathContent, consumeMathClose } from "./content";
|
|
16
|
-
import { parseMathName } from "./name";
|
|
14
|
+
import { findMathOpen, findMathBodyBounds } from "./boundary";
|
|
17
15
|
|
|
18
16
|
/**
|
|
19
17
|
* Block rule for `[[math name]]...[[/math]]`.
|
|
@@ -28,79 +26,41 @@ export const mathBlockRule: BlockRule = {
|
|
|
28
26
|
|
|
29
27
|
parse(ctx: ParseContext): RuleResult<Element> {
|
|
30
28
|
const openToken = currentToken(ctx);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let pos = ctx.pos + 1;
|
|
36
|
-
let consumed = 1;
|
|
37
|
-
|
|
38
|
-
const nameResult = parseBlockName(ctx, pos);
|
|
39
|
-
if (!nameResult || nameResult.name !== "math") {
|
|
40
|
-
return { success: false };
|
|
41
|
-
}
|
|
42
|
-
pos += nameResult.consumed;
|
|
43
|
-
consumed += nameResult.consumed;
|
|
44
|
-
|
|
45
|
-
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
46
|
-
pos++;
|
|
47
|
-
consumed++;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const mathName = parseMathName(ctx, pos);
|
|
51
|
-
pos += mathName.consumed;
|
|
52
|
-
consumed += mathName.consumed;
|
|
53
|
-
|
|
54
|
-
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
55
|
-
pos++;
|
|
56
|
-
consumed++;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
|
|
60
|
-
return { success: false };
|
|
61
|
-
}
|
|
62
|
-
pos++;
|
|
63
|
-
consumed++;
|
|
64
|
-
|
|
65
|
-
if (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
66
|
-
pos++;
|
|
67
|
-
consumed++;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const contentResult = collectMathContent(ctx, pos);
|
|
71
|
-
const latexSource = contentResult.latexSource.trim();
|
|
72
|
-
consumed += contentResult.consumed;
|
|
73
|
-
pos += contentResult.consumed;
|
|
74
|
-
|
|
75
|
-
if (!contentResult.foundClose) {
|
|
29
|
+
const open = findMathOpen(ctx.tokens, ctx.pos);
|
|
30
|
+
if (!open) return { success: false };
|
|
31
|
+
const bounds = findMathBodyBounds(ctx.tokens, open.bodyStart);
|
|
32
|
+
if (!bounds.foundClose) {
|
|
76
33
|
ctx.diagnostics.push({
|
|
77
34
|
severity: "warning",
|
|
78
35
|
code: "unclosed-block",
|
|
79
36
|
message: "Missing closing tag [[/math]] for [[math]]",
|
|
80
37
|
position: openToken.position,
|
|
81
38
|
});
|
|
82
|
-
} else {
|
|
83
|
-
const closeConsumed = consumeMathClose(ctx, pos);
|
|
84
|
-
pos += closeConsumed;
|
|
85
|
-
consumed += closeConsumed;
|
|
86
39
|
}
|
|
87
|
-
|
|
88
|
-
if (!latexSource) {
|
|
40
|
+
if (!bounds.hasContent) {
|
|
89
41
|
return { success: false };
|
|
90
42
|
}
|
|
91
|
-
|
|
43
|
+
const latexSource = ctx.tokens
|
|
44
|
+
.slice(open.bodyStart, bounds.closeStart)
|
|
45
|
+
.map((token) => (token.type === "BACKSLASH_BREAK" ? "\\\n" : token.value))
|
|
46
|
+
.join("")
|
|
47
|
+
.trim();
|
|
48
|
+
const name = ctx.tokens
|
|
49
|
+
.slice(open.nameStart, open.nameEnd)
|
|
50
|
+
.map((token) => token.value)
|
|
51
|
+
.join("");
|
|
92
52
|
return {
|
|
93
53
|
success: true,
|
|
94
54
|
elements: [
|
|
95
55
|
{
|
|
96
56
|
element: "math",
|
|
97
57
|
data: {
|
|
98
|
-
name:
|
|
58
|
+
name: name || null,
|
|
99
59
|
"latex-source": latexSource,
|
|
100
60
|
},
|
|
101
61
|
},
|
|
102
62
|
],
|
|
103
|
-
consumed,
|
|
63
|
+
consumed: bounds.end - ctx.pos,
|
|
104
64
|
};
|
|
105
65
|
},
|
|
106
66
|
};
|