@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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PageButtonData } from "@wdprlib/ast";
|
|
2
|
+
import { findRawTagClose } from "../parsing/raw-tag";
|
|
3
|
+
import type { ParseContext } from "../../types";
|
|
4
|
+
import { parseButtonAttributes } from "./attributes";
|
|
5
|
+
|
|
6
|
+
interface ButtonSyntax {
|
|
7
|
+
data: PageButtonData;
|
|
8
|
+
end: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Read a complete standalone button, including permitted separator newlines. */
|
|
12
|
+
export function parseButtonSyntax(
|
|
13
|
+
ctx: ParseContext,
|
|
14
|
+
start: number,
|
|
15
|
+
end: number,
|
|
16
|
+
): ButtonSyntax | null {
|
|
17
|
+
const tokens = ctx.tokens;
|
|
18
|
+
if (tokens[start]?.type !== "BLOCK_OPEN" || tokens[start + 1]?.value.toLowerCase() !== "button")
|
|
19
|
+
return null;
|
|
20
|
+
let pos = start + 2;
|
|
21
|
+
const skipSpace = () => {
|
|
22
|
+
const before = pos;
|
|
23
|
+
while (pos < end && /^\s+$/.test(tokens[pos]?.value ?? "")) pos++;
|
|
24
|
+
return pos > before;
|
|
25
|
+
};
|
|
26
|
+
if (!skipSpace()) return null;
|
|
27
|
+
let action = "";
|
|
28
|
+
while (
|
|
29
|
+
pos < end &&
|
|
30
|
+
tokens[pos]?.type !== "BLOCK_CLOSE" &&
|
|
31
|
+
!/^\s+$/.test(tokens[pos]?.value ?? "")
|
|
32
|
+
) {
|
|
33
|
+
const part = tokens[pos]?.value ?? "";
|
|
34
|
+
if (!/^[a-z0-9_-]+$/i.test(part)) return null;
|
|
35
|
+
action += part;
|
|
36
|
+
pos++;
|
|
37
|
+
}
|
|
38
|
+
if (!action) return null;
|
|
39
|
+
action = action.replaceAll("_", "-");
|
|
40
|
+
skipSpace();
|
|
41
|
+
const close = findRawTagClose(tokens, pos, end);
|
|
42
|
+
if (close === null) return null;
|
|
43
|
+
const attrs = parseButtonAttributes(
|
|
44
|
+
tokens
|
|
45
|
+
.slice(pos, close)
|
|
46
|
+
.map((token) => token.value)
|
|
47
|
+
.join(""),
|
|
48
|
+
);
|
|
49
|
+
const value = (key: string) => (attrs[key] && attrs[key] !== "0" ? attrs[key]! : null);
|
|
50
|
+
const attributes: Record<string, string> = {};
|
|
51
|
+
for (const name of ["class", "style"]) {
|
|
52
|
+
const attr = value(name);
|
|
53
|
+
if (attr !== null) attributes[name] = attr;
|
|
54
|
+
}
|
|
55
|
+
return { data: { action, text: value("text"), attributes }, end: close + 1 };
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { InlineRule } from "../../types";
|
|
2
|
+
import { parseDateSyntax } from "./syntax";
|
|
3
|
+
|
|
4
|
+
export const dateRule: InlineRule = {
|
|
5
|
+
name: "date",
|
|
6
|
+
startTokens: ["BLOCK_OPEN"],
|
|
7
|
+
parse(ctx) {
|
|
8
|
+
const parsed = parseDateSyntax(ctx, ctx.pos, ctx.scope.inlineEnd ?? ctx.tokens.length);
|
|
9
|
+
return parsed
|
|
10
|
+
? {
|
|
11
|
+
success: true,
|
|
12
|
+
consumed: parsed.end - ctx.pos,
|
|
13
|
+
elements: [{ element: "date", data: parsed.data }],
|
|
14
|
+
}
|
|
15
|
+
: { success: false };
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { DateData } from "@wdprlib/ast";
|
|
2
|
+
import type { ParseContext } from "../../types";
|
|
3
|
+
import { parseAttributesRaw } from "../../block/parsing/attributes";
|
|
4
|
+
|
|
5
|
+
interface DateSyntax {
|
|
6
|
+
data: DateData;
|
|
7
|
+
end: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Read a complete date without registering any parser side effects. */
|
|
11
|
+
export function parseDateSyntax(ctx: ParseContext, start: number, end: number): DateSyntax | null {
|
|
12
|
+
const tokens = ctx.tokens;
|
|
13
|
+
if (tokens[start]?.type !== "BLOCK_OPEN" || tokens[start + 1]?.value !== "date") return null;
|
|
14
|
+
let pos = start + 2;
|
|
15
|
+
const skipSpace = () => {
|
|
16
|
+
const before = pos;
|
|
17
|
+
while (pos < end && /^(?:\s+)$/.test(tokens[pos]?.value ?? "")) pos++;
|
|
18
|
+
return pos > before;
|
|
19
|
+
};
|
|
20
|
+
if (!skipSpace()) return null;
|
|
21
|
+
const value = tokens[pos]?.value ?? "";
|
|
22
|
+
if (pos >= end || !/^\d+$/.test(value)) return null;
|
|
23
|
+
const timestamp = Number(value);
|
|
24
|
+
if (!Number.isSafeInteger(timestamp) || timestamp > 8_640_000_000_000) return null;
|
|
25
|
+
pos++;
|
|
26
|
+
const separator = skipSpace();
|
|
27
|
+
if (tokens[pos]?.type !== "BLOCK_CLOSE" && !separator) return null;
|
|
28
|
+
let close = pos;
|
|
29
|
+
for (; close < end; close++) {
|
|
30
|
+
const token = tokens[close];
|
|
31
|
+
if (!token || token.type === "EOF" || token.type === "BLOCK_OPEN" || /[\r\n]/.test(token.value))
|
|
32
|
+
return null;
|
|
33
|
+
if (token.type === "BLOCK_CLOSE") break;
|
|
34
|
+
}
|
|
35
|
+
if (close >= end) return null;
|
|
36
|
+
const attributes = parseAttributesRaw(ctx, pos).attrs;
|
|
37
|
+
const format = attributes.format || null;
|
|
38
|
+
return {
|
|
39
|
+
data: {
|
|
40
|
+
value: { timestamp, timezone: "UTC" },
|
|
41
|
+
format,
|
|
42
|
+
hover: format?.split("|").slice(1).includes("agohover") ?? false,
|
|
43
|
+
},
|
|
44
|
+
end: close + 1,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Token, TokenType } from "../../../../lexer";
|
|
2
|
+
import { isLocalChar, scanEmail } from "./scan";
|
|
3
|
+
|
|
4
|
+
export interface EmailCandidate {
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
endToken: number;
|
|
8
|
+
address: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface EmailCache {
|
|
12
|
+
source: string;
|
|
13
|
+
offsets: number[];
|
|
14
|
+
candidates: Map<number, EmailCandidate | null>;
|
|
15
|
+
commentEnds: Map<number, number>;
|
|
16
|
+
unclosedComment: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const caches = new WeakMap<readonly Token[], EmailCache>();
|
|
20
|
+
export const EMAIL_START_TOKENS: TokenType[] = [
|
|
21
|
+
"TEXT",
|
|
22
|
+
"IDENTIFIER",
|
|
23
|
+
"UNDERSCORE",
|
|
24
|
+
"UNDERLINE_MARKER",
|
|
25
|
+
"STRIKE_MARKER",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function getCache(tokens: readonly Token[]): EmailCache {
|
|
29
|
+
let cache = caches.get(tokens);
|
|
30
|
+
if (cache) return cache;
|
|
31
|
+
const offsets = [0];
|
|
32
|
+
const values: string[] = [];
|
|
33
|
+
for (const token of tokens) {
|
|
34
|
+
values.push(token.value);
|
|
35
|
+
offsets.push(offsets.at(-1)! + token.value.length);
|
|
36
|
+
}
|
|
37
|
+
cache = {
|
|
38
|
+
source: values.join(""),
|
|
39
|
+
offsets,
|
|
40
|
+
candidates: new Map(),
|
|
41
|
+
commentEnds: new Map(),
|
|
42
|
+
unclosedComment: Infinity,
|
|
43
|
+
};
|
|
44
|
+
caches.set(tokens, cache);
|
|
45
|
+
return cache;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function commentEnd(cache: EmailCache, pos: number): number {
|
|
49
|
+
const opener = cache.source[pos] === "\n" ? pos + 1 : pos;
|
|
50
|
+
if (!cache.source.startsWith("[!--", opener)) return pos;
|
|
51
|
+
const cached = cache.commentEnds.get(pos);
|
|
52
|
+
if (cached !== undefined) return cached;
|
|
53
|
+
const close = opener >= cache.unclosedComment ? -1 : cache.source.indexOf("--]", opener + 4);
|
|
54
|
+
if (close === -1) cache.unclosedComment = Math.min(cache.unclosedComment, opener);
|
|
55
|
+
const end = close === -1 ? pos : close + 3;
|
|
56
|
+
cache.commentEnds.set(pos, end);
|
|
57
|
+
return end;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getEmailCandidate(tokens: readonly Token[], index: number): EmailCandidate | null {
|
|
61
|
+
const token = tokens[index];
|
|
62
|
+
if (!token || !EMAIL_START_TOKENS.includes(token.type)) return null;
|
|
63
|
+
const cache = getCache(tokens);
|
|
64
|
+
if (cache.candidates.has(index)) return cache.candidates.get(index)!;
|
|
65
|
+
const value = tokens[index]?.value ?? "";
|
|
66
|
+
// Compact TEXT may include a prefix before the last possible local-part run.
|
|
67
|
+
let suffix = value.length;
|
|
68
|
+
while (suffix > 0 && (isLocalChar(value.charCodeAt(suffix - 1)) || value[suffix - 1] === "."))
|
|
69
|
+
suffix--;
|
|
70
|
+
const doubled = value.lastIndexOf("..");
|
|
71
|
+
if (doubled >= suffix) suffix = doubled + 2;
|
|
72
|
+
while (value[suffix] === ".") suffix++;
|
|
73
|
+
if (suffix >= value.length || !isLocalChar(value.charCodeAt(suffix))) {
|
|
74
|
+
cache.candidates.set(index, null);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const start = cache.offsets[index]! + suffix;
|
|
78
|
+
const scanned = scanEmail(cache.source, start, (pos) => commentEnd(cache, pos));
|
|
79
|
+
if (scanned.end === undefined) {
|
|
80
|
+
cache.candidates.set(index, null);
|
|
81
|
+
let comment = 0;
|
|
82
|
+
// A failed local run cannot produce an address from one of its suffix tokens.
|
|
83
|
+
// Do not poison positions inside skipped comments: another parse scope may see them as raw text.
|
|
84
|
+
for (let i = index + 1; i < tokens.length && cache.offsets[i + 1]! <= scanned.localEnd; i++) {
|
|
85
|
+
const offset = cache.offsets[i]!;
|
|
86
|
+
while (scanned.comments[comment] && scanned.comments[comment]!.end <= offset) comment++;
|
|
87
|
+
const region = scanned.comments[comment];
|
|
88
|
+
if (!region || offset < region.start) cache.candidates.set(i, null);
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
let endToken = index;
|
|
93
|
+
while (cache.offsets[endToken + 1]! < scanned.end) endToken++;
|
|
94
|
+
const candidate = { start, end: scanned.end, endToken, address: scanned.address! };
|
|
95
|
+
cache.candidates.set(index, candidate);
|
|
96
|
+
return candidate;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface EmailGroup {
|
|
100
|
+
candidates: EmailCandidate[];
|
|
101
|
+
source: string;
|
|
102
|
+
start: number;
|
|
103
|
+
end: number;
|
|
104
|
+
endToken: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getEmailGroup(
|
|
108
|
+
tokens: readonly Token[],
|
|
109
|
+
index: number,
|
|
110
|
+
limit: number,
|
|
111
|
+
): EmailGroup | null {
|
|
112
|
+
const first = getEmailCandidate(tokens, index);
|
|
113
|
+
if (!first || first.endToken >= limit) return null;
|
|
114
|
+
const candidates = [first];
|
|
115
|
+
let last = first;
|
|
116
|
+
while (last.endToken > index) {
|
|
117
|
+
const next = getEmailCandidate(tokens, last.endToken);
|
|
118
|
+
if (!next || next.start < last.end || next.endToken >= limit) break;
|
|
119
|
+
candidates.push(next);
|
|
120
|
+
last = next;
|
|
121
|
+
}
|
|
122
|
+
const cache = getCache(tokens);
|
|
123
|
+
return {
|
|
124
|
+
candidates,
|
|
125
|
+
source: cache.source,
|
|
126
|
+
start: cache.offsets[index]!,
|
|
127
|
+
end: cache.offsets[last.endToken + 1]!,
|
|
128
|
+
endToken: last.endToken + 1,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function emailRegionEnd(tokens: readonly Token[], index: number, limit: number): number {
|
|
133
|
+
return getEmailGroup(tokens, index, limit)?.endToken ?? index;
|
|
134
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Element } from "@wdprlib/ast";
|
|
2
|
+
import type { InlineRule } from "../../types";
|
|
3
|
+
import { EMAIL_START_TOKENS, getEmailGroup } from "./candidates";
|
|
4
|
+
|
|
5
|
+
export const emailRule: InlineRule = {
|
|
6
|
+
name: "email",
|
|
7
|
+
startTokens: EMAIL_START_TOKENS,
|
|
8
|
+
parse(ctx) {
|
|
9
|
+
const group = getEmailGroup(ctx.tokens, ctx.pos, ctx.scope.inlineEnd ?? ctx.tokens.length);
|
|
10
|
+
if (!group) return { success: false };
|
|
11
|
+
const elements: Element[] = [];
|
|
12
|
+
let copied = group.start;
|
|
13
|
+
for (const candidate of group.candidates) {
|
|
14
|
+
if (copied < candidate.start)
|
|
15
|
+
elements.push({ element: "text", data: group.source.slice(copied, candidate.start) });
|
|
16
|
+
elements.push(
|
|
17
|
+
ctx.scope.suppressEmailLinks
|
|
18
|
+
? { element: "text", data: candidate.address }
|
|
19
|
+
: {
|
|
20
|
+
element: "link",
|
|
21
|
+
data: {
|
|
22
|
+
type: "direct",
|
|
23
|
+
link: `mailto:${candidate.address}`,
|
|
24
|
+
label: { text: candidate.address },
|
|
25
|
+
target: null,
|
|
26
|
+
extra: null,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
copied = candidate.end;
|
|
31
|
+
}
|
|
32
|
+
if (copied < group.end)
|
|
33
|
+
elements.push({ element: "text", data: group.source.slice(copied, group.end) });
|
|
34
|
+
return { success: true, elements, consumed: group.endToken - ctx.pos };
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export interface EmailScan {
|
|
2
|
+
end?: number;
|
|
3
|
+
address?: string;
|
|
4
|
+
localEnd: number;
|
|
5
|
+
comments: Array<{ start: number; end: number }>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isDomainChar(code: number): boolean {
|
|
9
|
+
return (
|
|
10
|
+
(code >= 48 && code <= 57) ||
|
|
11
|
+
(code >= 65 && code <= 90) ||
|
|
12
|
+
(code >= 97 && code <= 122) ||
|
|
13
|
+
code === 45
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isLocalChar(code: number): boolean {
|
|
18
|
+
return isDomainChar(code) || code === 95;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Read Email.php's grammar from one possible start, ignoring complete comments. */
|
|
22
|
+
export function scanEmail(
|
|
23
|
+
source: string,
|
|
24
|
+
start: number,
|
|
25
|
+
commentEnd: (pos: number) => number,
|
|
26
|
+
): EmailScan {
|
|
27
|
+
const comments: EmailScan["comments"] = [];
|
|
28
|
+
let pos = start;
|
|
29
|
+
const skipComments = () => {
|
|
30
|
+
let end = commentEnd(pos);
|
|
31
|
+
while (end > pos) {
|
|
32
|
+
comments.push({ start: pos, end });
|
|
33
|
+
pos = end;
|
|
34
|
+
end = commentEnd(pos);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
let localPart = false;
|
|
38
|
+
while (pos < source.length) {
|
|
39
|
+
skipComments();
|
|
40
|
+
if (isLocalChar(source.charCodeAt(pos))) {
|
|
41
|
+
localPart = true;
|
|
42
|
+
pos++;
|
|
43
|
+
} else if (source[pos] === "." && localPart) {
|
|
44
|
+
localPart = false;
|
|
45
|
+
pos++;
|
|
46
|
+
} else break;
|
|
47
|
+
}
|
|
48
|
+
const localEnd = pos;
|
|
49
|
+
if (!localPart || source[pos] !== "@") return { localEnd, comments };
|
|
50
|
+
pos++;
|
|
51
|
+
let domainPart = false;
|
|
52
|
+
let dots = 0;
|
|
53
|
+
let end = -1;
|
|
54
|
+
while (pos < source.length) {
|
|
55
|
+
skipComments();
|
|
56
|
+
if (isDomainChar(source.charCodeAt(pos))) {
|
|
57
|
+
domainPart = true;
|
|
58
|
+
pos++;
|
|
59
|
+
if (dots > 0) end = pos;
|
|
60
|
+
} else if (source[pos] === "." && domainPart) {
|
|
61
|
+
domainPart = false;
|
|
62
|
+
dots++;
|
|
63
|
+
pos++;
|
|
64
|
+
} else break;
|
|
65
|
+
}
|
|
66
|
+
if (end === -1) return { localEnd, comments };
|
|
67
|
+
const parts: string[] = [];
|
|
68
|
+
let copied = start;
|
|
69
|
+
for (const comment of comments) {
|
|
70
|
+
if (comment.start >= end) break;
|
|
71
|
+
parts.push(source.slice(copied, comment.start));
|
|
72
|
+
copied = comment.end;
|
|
73
|
+
}
|
|
74
|
+
parts.push(source.slice(copied, end));
|
|
75
|
+
return { end, address: parts.join(""), localEnd, comments };
|
|
76
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stripAutomaticLineBreak } from "../parsing/automatic-line-break";
|
|
1
2
|
import type { Element } from "@wdprlib/ast";
|
|
2
3
|
import type { ParseContext } from "../../types";
|
|
3
4
|
import { getCandidateInlineRules } from "../utils";
|
|
@@ -52,7 +53,8 @@ export function parseInlineBranch(ctx: ParseContext, startPos: number): BranchPa
|
|
|
52
53
|
}
|
|
53
54
|
const result = rule.parse(inlineCtx);
|
|
54
55
|
if (result.success) {
|
|
55
|
-
elements
|
|
56
|
+
stripAutomaticLineBreak(elements, result.stripLeadingLineBreak);
|
|
57
|
+
for (const element of result.elements) elements.push(element);
|
|
56
58
|
consumed += result.consumed;
|
|
57
59
|
pos += result.consumed;
|
|
58
60
|
matched = true;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createOpaqueProbe, opaqueRuleEnd } from "../../opaque-probe";
|
|
2
|
+
import type { ParseContext } from "../../types";
|
|
3
|
+
import { codeBlockRule } from "../../block/code";
|
|
4
|
+
import { htmlBlockRule } from "../../block/html";
|
|
5
|
+
import { mathBlockRule } from "../../block/math";
|
|
6
|
+
import { mathInlineRule } from "../math-inline";
|
|
7
|
+
import { linkTripleRule } from "../link-triple";
|
|
8
|
+
import { linkSingleRule } from "../link-single";
|
|
9
|
+
import { linkStarRule } from "../link-star";
|
|
10
|
+
import { linkAnchorRule } from "../link-anchor";
|
|
11
|
+
import { protectedInlineRegionEnd } from "../raw/end";
|
|
12
|
+
import { tryConsumeFootnoteClose } from "./close";
|
|
13
|
+
import { parseFootnoteOpen } from "./open";
|
|
14
|
+
|
|
15
|
+
const opaqueRules = [
|
|
16
|
+
codeBlockRule,
|
|
17
|
+
htmlBlockRule,
|
|
18
|
+
mathBlockRule,
|
|
19
|
+
mathInlineRule,
|
|
20
|
+
linkTripleRule,
|
|
21
|
+
linkSingleRule,
|
|
22
|
+
linkStarRule,
|
|
23
|
+
linkAnchorRule,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/** Locate the enclosing close without parsing code, HTML or math as footnote syntax. */
|
|
27
|
+
export function findFootnoteEnd(ctx: ParseContext, start: number): number {
|
|
28
|
+
const probe = createOpaqueProbe(ctx);
|
|
29
|
+
let depth = 0;
|
|
30
|
+
for (let pos = start; pos < ctx.tokens.length; pos++) {
|
|
31
|
+
if (ctx.tokens[pos]?.type === "EOF") return pos;
|
|
32
|
+
const protectedEnd = protectedInlineRegionEnd(ctx.tokens, pos, ctx.tokens.length);
|
|
33
|
+
if (protectedEnd > pos) {
|
|
34
|
+
pos = protectedEnd - 1;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
probe.pos = pos;
|
|
39
|
+
const close = tryConsumeFootnoteClose(probe, pos);
|
|
40
|
+
if (close) {
|
|
41
|
+
if (depth === 0) return pos;
|
|
42
|
+
depth--;
|
|
43
|
+
pos += close.consumed - 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const open = parseFootnoteOpen(probe);
|
|
47
|
+
if (open) {
|
|
48
|
+
depth++;
|
|
49
|
+
pos += open.consumed - 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const opaqueEnd = opaqueRuleEnd(probe, pos, opaqueRules);
|
|
53
|
+
if (opaqueEnd > pos) pos = opaqueEnd - 1;
|
|
54
|
+
}
|
|
55
|
+
return ctx.tokens.length;
|
|
56
|
+
}
|
|
@@ -1,54 +1,42 @@
|
|
|
1
1
|
import type { Element } from "@wdprlib/ast";
|
|
2
2
|
import type { ParseContext } from "../../types";
|
|
3
|
-
import {
|
|
3
|
+
import { parseBlocksUntil } from "../../block/parsing/content";
|
|
4
4
|
import { tryConsumeFootnoteClose } from "./close";
|
|
5
|
-
import {
|
|
5
|
+
import { findFootnoteEnd } from "./boundary";
|
|
6
6
|
|
|
7
7
|
export interface FootnoteContentResult {
|
|
8
|
-
|
|
8
|
+
elements: Element[];
|
|
9
9
|
consumed: number;
|
|
10
10
|
foundClose: boolean;
|
|
11
|
+
leadingParagraphBreak: boolean;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function parseFootnoteContent(ctx: ParseContext, startPos: number): FootnoteContentResult {
|
|
14
|
-
const
|
|
15
|
-
let
|
|
16
|
-
let pos = startPos;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const token = ctx.tokens[pos];
|
|
21
|
-
if (!token || token.type === "EOF") {
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const closeResult = tryConsumeFootnoteClose(ctx, pos);
|
|
26
|
-
if (closeResult) {
|
|
27
|
-
return {
|
|
28
|
-
paragraphs,
|
|
29
|
-
consumed: consumed + closeResult.consumed,
|
|
30
|
-
foundClose: true,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (token.type === "NEWLINE") {
|
|
35
|
-
const newlineResult = consumeFootnoteNewline(ctx, pos);
|
|
36
|
-
pos += newlineResult.consumed;
|
|
37
|
-
consumed += newlineResult.consumed;
|
|
38
|
-
if (newlineResult.paragraphBreak) {
|
|
39
|
-
currentParagraph++;
|
|
40
|
-
paragraphs[currentParagraph] = [];
|
|
41
|
-
} else {
|
|
42
|
-
paragraphs[currentParagraph]!.push({ element: "line-break" });
|
|
43
|
-
}
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const child = parseFootnoteChild(ctx, pos);
|
|
48
|
-
paragraphs[currentParagraph]!.push(...child.elements);
|
|
49
|
-
pos += child.consumed;
|
|
50
|
-
consumed += child.consumed;
|
|
15
|
+
const end = findFootnoteEnd(ctx, startPos);
|
|
16
|
+
let leadingNewlines = 0;
|
|
17
|
+
for (let pos = startPos; pos < end; pos++) {
|
|
18
|
+
const type = ctx.tokens[pos]?.type;
|
|
19
|
+
if (type === "NEWLINE") leadingNewlines++;
|
|
20
|
+
else if (type !== "WHITESPACE") break;
|
|
51
21
|
}
|
|
52
22
|
|
|
53
|
-
|
|
23
|
+
const bodyCtx: ParseContext = {
|
|
24
|
+
...ctx,
|
|
25
|
+
tokens: ctx.tokens.slice(startPos, end),
|
|
26
|
+
pos: 0,
|
|
27
|
+
scope: {
|
|
28
|
+
...ctx.scope,
|
|
29
|
+
inlineEnd: undefined,
|
|
30
|
+
tableFormatting: undefined,
|
|
31
|
+
blockCloseCondition: undefined,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const result = parseBlocksUntil(bodyCtx, () => false);
|
|
35
|
+
const close = tryConsumeFootnoteClose(ctx, end);
|
|
36
|
+
return {
|
|
37
|
+
elements: result.elements,
|
|
38
|
+
consumed: end - startPos + (close?.consumed ?? 0),
|
|
39
|
+
foundClose: close !== null,
|
|
40
|
+
leadingParagraphBreak: leadingNewlines >= 2,
|
|
41
|
+
};
|
|
54
42
|
}
|
|
@@ -1,38 +1,13 @@
|
|
|
1
1
|
import type { Element } from "@wdprlib/ast";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (i === 0) {
|
|
13
|
-
children.push(...para);
|
|
14
|
-
} else {
|
|
15
|
-
children.push({
|
|
16
|
-
element: "container",
|
|
17
|
-
data: {
|
|
18
|
-
type: "paragraph",
|
|
19
|
-
attributes: {},
|
|
20
|
-
elements: para,
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return children;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function trimLineBreaks(elements: Element[]): Element[] {
|
|
30
|
-
const result = [...elements];
|
|
31
|
-
while (result.length > 0 && result[0]?.element === "line-break") {
|
|
32
|
-
result.shift();
|
|
33
|
-
}
|
|
34
|
-
while (result.length > 0 && result[result.length - 1]?.element === "line-break") {
|
|
35
|
-
result.pop();
|
|
3
|
+
/** Wikidot leaves the initial footnote paragraph unwrapped unless a blank line precedes it. */
|
|
4
|
+
export function buildFootnoteChildren(
|
|
5
|
+
elements: Element[],
|
|
6
|
+
leadingParagraphBreak: boolean,
|
|
7
|
+
): Element[] {
|
|
8
|
+
const first = elements[0];
|
|
9
|
+
if (!leadingParagraphBreak && first?.element === "container" && first.data.type === "paragraph") {
|
|
10
|
+
return [...first.data.elements, ...elements.slice(1)];
|
|
36
11
|
}
|
|
37
|
-
return
|
|
12
|
+
return elements;
|
|
38
13
|
}
|
|
@@ -28,7 +28,10 @@ export const footnoteRule: InlineRule = {
|
|
|
28
28
|
|
|
29
29
|
const contentResult = parseFootnoteContent(ctx, openResult.bodyStart);
|
|
30
30
|
const consumed = openResult.consumed + contentResult.consumed;
|
|
31
|
-
const children = buildFootnoteChildren(
|
|
31
|
+
const children = buildFootnoteChildren(
|
|
32
|
+
contentResult.elements,
|
|
33
|
+
contentResult.leadingParagraphBreak,
|
|
34
|
+
);
|
|
32
35
|
|
|
33
36
|
if (!contentResult.foundClose) {
|
|
34
37
|
ctx.diagnostics.push({
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { emailRegionEnd } from "../email/candidates";
|
|
2
|
+
import { parseButtonSyntax } from "../button/syntax";
|
|
3
|
+
import { parseSocialSyntax } from "../social/syntax";
|
|
1
4
|
import { protectedInlineRegionEnd } from "../raw/end";
|
|
2
5
|
import type { TokenType } from "../../../../lexer";
|
|
3
6
|
import type { ParseContext } from "../../types";
|
|
@@ -20,6 +23,15 @@ export function findFormattingClose(
|
|
|
20
23
|
getParagraphNewlineBoundary(ctx, pos, true).shouldBreak
|
|
21
24
|
)
|
|
22
25
|
return null;
|
|
26
|
+
const inlineRegionEnd = Math.max(
|
|
27
|
+
emailRegionEnd(ctx.tokens, pos, end),
|
|
28
|
+
parseButtonSyntax(ctx, pos, end)?.end ?? pos,
|
|
29
|
+
parseSocialSyntax(ctx, pos, end)?.end ?? pos,
|
|
30
|
+
);
|
|
31
|
+
if (inlineRegionEnd > pos) {
|
|
32
|
+
pos = inlineRegionEnd - 1;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
23
35
|
if (token.type === marker && !table?.suppressedClosers.has(pos)) return pos;
|
|
24
36
|
const protectedEnd = protectedInlineRegionEnd(ctx.tokens, pos, end);
|
|
25
37
|
if (protectedEnd > pos) pos = protectedEnd - 1;
|
|
@@ -31,6 +31,8 @@ import { linkTripleRule } from "./link-triple";
|
|
|
31
31
|
import { linkSingleRule } from "./link-single";
|
|
32
32
|
import { linkAnchorRule } from "./link-anchor";
|
|
33
33
|
import { linkStarRule } from "./link-star";
|
|
34
|
+
import { autolinkRule } from "./autolink";
|
|
35
|
+
import { emailRule } from "./email";
|
|
34
36
|
import { colorRule } from "./color";
|
|
35
37
|
import {
|
|
36
38
|
backslashLineBreakRule,
|
|
@@ -41,6 +43,9 @@ import { commentRule } from "./comment";
|
|
|
41
43
|
import { htmlInlineRule } from "./html";
|
|
42
44
|
import { rawRule } from "./raw";
|
|
43
45
|
import { spanRule, closeSpanRule } from "./span";
|
|
46
|
+
import { buttonRule } from "./button";
|
|
47
|
+
import { socialRule } from "./social";
|
|
48
|
+
import { dateRule } from "./date";
|
|
44
49
|
import { sizeRule } from "./size";
|
|
45
50
|
import { footnoteRule } from "./footnote";
|
|
46
51
|
import { imageRule } from "./image";
|
|
@@ -65,6 +70,7 @@ export { linkTripleRule } from "./link-triple";
|
|
|
65
70
|
export { linkSingleRule } from "./link-single";
|
|
66
71
|
export { linkAnchorRule } from "./link-anchor";
|
|
67
72
|
export { linkStarRule } from "./link-star";
|
|
73
|
+
export { autolinkRule } from "./autolink";
|
|
68
74
|
export { colorRule } from "./color";
|
|
69
75
|
export {
|
|
70
76
|
backslashLineBreakRule,
|
|
@@ -75,6 +81,9 @@ export { commentRule } from "./comment";
|
|
|
75
81
|
export { htmlInlineRule } from "./html";
|
|
76
82
|
export { rawRule } from "./raw";
|
|
77
83
|
export { spanRule, closeSpanRule } from "./span";
|
|
84
|
+
export { buttonRule } from "./button";
|
|
85
|
+
export { socialRule } from "./social";
|
|
86
|
+
export { dateRule } from "./date";
|
|
78
87
|
export { sizeRule } from "./size";
|
|
79
88
|
export { footnoteRule } from "./footnote";
|
|
80
89
|
export { imageRule } from "./image";
|
|
@@ -112,6 +121,7 @@ export { textRule, fallbackRule } from "./text";
|
|
|
112
121
|
* separate last-resort handler.
|
|
113
122
|
*/
|
|
114
123
|
export const inlineRules: InlineRule[] = [
|
|
124
|
+
emailRule,
|
|
115
125
|
boldRule,
|
|
116
126
|
italicRule,
|
|
117
127
|
underlineRule,
|
|
@@ -123,6 +133,7 @@ export const inlineRules: InlineRule[] = [
|
|
|
123
133
|
linkSingleRule,
|
|
124
134
|
linkAnchorRule,
|
|
125
135
|
linkStarRule,
|
|
136
|
+
autolinkRule,
|
|
126
137
|
colorRule,
|
|
127
138
|
backslashLineBreakRule,
|
|
128
139
|
underscoreLineBreakRule,
|
|
@@ -132,6 +143,9 @@ export const inlineRules: InlineRule[] = [
|
|
|
132
143
|
rawRule,
|
|
133
144
|
imageRule,
|
|
134
145
|
embedBlockRule,
|
|
146
|
+
buttonRule,
|
|
147
|
+
socialRule,
|
|
148
|
+
dateRule,
|
|
135
149
|
sizeRule,
|
|
136
150
|
footnoteRule,
|
|
137
151
|
spanRule,
|