@wdprlib/parser 5.3.0 → 6.0.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 +2429 -1199
- package/dist/index.d.cts +95 -8
- package/dist/index.d.ts +95 -8
- package/dist/index.js +2385 -1152
- package/package.json +3 -2
- package/src/build-info.generated.ts +2 -2
- package/src/index.ts +3 -0
- 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/module/index.ts +3 -2
- package/src/parser/rules/block/module/listpages/extraction/listpages.ts +5 -0
- package/src/parser/rules/block/module/listpages/extraction/query.ts +1 -0
- package/src/parser/rules/block/module/listpages/extraction/template.ts +16 -0
- package/src/parser/rules/block/module/listpages/extraction/variables.ts +1 -0
- package/src/parser/rules/block/module/listpages/index.ts +1 -0
- package/src/parser/rules/block/module/listpages/normalization/order-parent.ts +9 -1
- package/src/parser/rules/block/module/listpages/normalize.ts +1 -0
- package/src/parser/rules/block/module/listpages/parser.ts +1 -0
- package/src/parser/rules/block/module/listpages/resolve.ts +4 -2
- package/src/parser/rules/block/module/listpages/template/excerpt.ts +63 -0
- package/src/parser/rules/block/module/listpages/template/format/content.ts +0 -32
- package/src/parser/rules/block/module/listpages/template/format/index.ts +1 -1
- package/src/parser/rules/block/module/listpages/template/getters/index.ts +14 -0
- package/src/parser/rules/block/module/listpages/template/getters/parameterized.ts +3 -1
- package/src/parser/rules/block/module/listpages/template/getters/registered.ts +52 -0
- package/src/parser/rules/block/module/listpages/template/getters/simple.ts +15 -5
- package/src/parser/rules/block/module/listpages/template/literal.ts +6 -0
- package/src/parser/rules/block/module/listpages/template/syntax.ts +10 -1
- package/src/parser/rules/block/module/listpages/types/data-requirements.ts +6 -0
- package/src/parser/rules/block/module/listpages/types/external-data.ts +36 -2
- package/src/parser/rules/block/module/listpages/types/normalized-query.ts +14 -4
- package/src/parser/rules/block/module/listpages/types/query.ts +2 -0
- package/src/parser/rules/block/module/listpages/types/variables.ts +5 -0
- package/src/parser/rules/block/module/listpages/url-resolution/fields.ts +2 -0
- package/src/parser/rules/block/module/listusers/resolve.ts +2 -1
- package/src/parser/rules/block/module/mapping.ts +2 -1
- package/src/parser/rules/block/module/rate/index.ts +16 -19
- package/src/parser/rules/block/module/rate/resolve.ts +108 -0
- package/src/parser/rules/block/module/rate/types.ts +6 -16
- package/src/parser/rules/block/module/resolution/resolve-async.ts +2 -1
- package/src/parser/rules/block/module/resolve.ts +10 -8
- package/src/parser/rules/block/module/types-common.ts +4 -0
- 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 +46 -7
- 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 +32 -5
- 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/pipeline/process.ts +13 -0
- package/src/pipeline/types.ts +19 -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,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,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createAutomaticLineBreak } from "../parsing/automatic-line-break";
|
|
1
2
|
import type { Element } from "@wdprlib/ast";
|
|
2
3
|
import type { TokenType } from "../../../../lexer";
|
|
3
4
|
import type { InlineRule, ParseContext, RuleResult } from "../../types";
|
|
@@ -27,7 +28,7 @@ export const newlineLineBreakRule: InlineRule = {
|
|
|
27
28
|
|
|
28
29
|
return {
|
|
29
30
|
success: true,
|
|
30
|
-
elements: [
|
|
31
|
+
elements: [createAutomaticLineBreak(currentTok)],
|
|
31
32
|
consumed: 1,
|
|
32
33
|
};
|
|
33
34
|
},
|
|
@@ -53,6 +54,12 @@ function isValidBlockStartAfterNewline(ctx: ParseContext, tokenPos: number): boo
|
|
|
53
54
|
return false;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
if (token.type === "LIST_BULLET" || token.type === "LIST_NUMBER") {
|
|
58
|
+
// リスト構文はマーカー直後の空白が必須(list/line.tsと同じ規則)。
|
|
59
|
+
// 空白なしの行(例: 行頭の `*http://...`)はリストにならないため<br>を抑制しない
|
|
60
|
+
return ctx.tokens[tokenPos + 1]?.type === "WHITESPACE";
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
if (token.type !== "HEADING_MARKER") {
|
|
57
64
|
return true;
|
|
58
65
|
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import { startsWithUrlScheme } from "../../../../lexer/url-schemes";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether `url` is a valid target for a single-bracket link (`[url label]`).
|
|
5
|
+
*
|
|
6
|
+
* Matches Text_Wiki's described-URL rule (regexLiberal): a site-relative path
|
|
7
|
+
* (`/…`) or any recognized URL scheme (http/https/ftp/gopher/news/mms/mailto).
|
|
8
|
+
* Kept in sync with the autolink rule so bracket-described links and bare
|
|
9
|
+
* auto-links accept the same schemes.
|
|
10
|
+
*/
|
|
1
11
|
export function isDirectBracketUrl(url: string): boolean {
|
|
2
|
-
return (
|
|
3
|
-
url !== "" && (url.startsWith("/") || url.startsWith("http://") || url.startsWith("https://"))
|
|
4
|
-
);
|
|
12
|
+
return url !== "" && (url.startsWith("/") || startsWithUrlScheme(url, 0));
|
|
5
13
|
}
|
|
@@ -4,8 +4,10 @@ import { normalizeAnchor } from "./anchor";
|
|
|
4
4
|
import { isDirectBracketUrl } from "./direct-url";
|
|
5
5
|
import { collectBracketLinkParts } from "./parts";
|
|
6
6
|
import { parseBracketLinkPrefix } from "./prefix";
|
|
7
|
+
import { isBracketEmail, wikipediaPage } from "./special-target";
|
|
7
8
|
|
|
8
9
|
export interface ParsedBracketLink {
|
|
10
|
+
interwiki?: boolean;
|
|
9
11
|
link: string;
|
|
10
12
|
labelText: string;
|
|
11
13
|
target: AnchorTarget | null;
|
|
@@ -20,19 +22,22 @@ export function parseSingleBracketLink(ctx: ParseContext): ParsedBracketLink | n
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
const link = parts.first.trim();
|
|
23
|
-
|
|
25
|
+
const email = isBracketEmail(link);
|
|
26
|
+
const wikiPage = wikipediaPage(link);
|
|
27
|
+
if (!email && wikiPage === null && !isDirectBracketUrl(link)) {
|
|
24
28
|
return null;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
const labelText = parts.label.trim();
|
|
31
|
+
const labelText = parts.label.trim() || wikiPage;
|
|
28
32
|
if (!labelText) {
|
|
29
33
|
return null;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
return {
|
|
33
|
-
link,
|
|
37
|
+
link: email ? `mailto:${link}` : link,
|
|
38
|
+
interwiki: wikiPage !== null,
|
|
34
39
|
labelText,
|
|
35
|
-
target: prefix.target,
|
|
40
|
+
target: wikiPage !== null ? "new-tab" : prefix.target,
|
|
36
41
|
consumed: 1 + prefix.consumed + parts.consumed,
|
|
37
42
|
};
|
|
38
43
|
}
|
|
@@ -16,49 +16,27 @@ export function collectBracketLinkParts(
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
let pos = startPos;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
while (pos < ctx.tokens.length) {
|
|
23
|
-
const token = ctx.tokens[pos];
|
|
24
|
-
if (
|
|
25
|
-
!token ||
|
|
26
|
-
token.type === "WHITESPACE" ||
|
|
27
|
-
token.type === "BRACKET_CLOSE" ||
|
|
28
|
-
token.type === "NEWLINE" ||
|
|
29
|
-
token.type === "EOF"
|
|
30
|
-
) {
|
|
31
|
-
break;
|
|
32
|
-
}
|
|
33
|
-
first += token.value;
|
|
34
|
-
pos++;
|
|
35
|
-
consumed++;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
39
|
-
pos++;
|
|
40
|
-
consumed++;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
let label = "";
|
|
44
|
-
while (pos < ctx.tokens.length) {
|
|
19
|
+
const end = Math.min(ctx.scope.inlineEnd ?? ctx.tokens.length, ctx.tokens.length);
|
|
20
|
+
const values: string[] = [];
|
|
21
|
+
while (pos < end) {
|
|
45
22
|
const token = ctx.tokens[pos];
|
|
46
23
|
if (
|
|
47
24
|
!token ||
|
|
48
25
|
token.type === "BRACKET_CLOSE" ||
|
|
49
26
|
token.type === "NEWLINE" ||
|
|
50
27
|
token.type === "EOF"
|
|
51
|
-
)
|
|
28
|
+
)
|
|
52
29
|
break;
|
|
53
|
-
|
|
54
|
-
label += token.value;
|
|
30
|
+
values.push(token.value);
|
|
55
31
|
pos++;
|
|
56
|
-
consumed++;
|
|
57
32
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
33
|
+
if (pos >= end || ctx.tokens[pos]?.type !== "BRACKET_CLOSE") return null;
|
|
34
|
+
// Compact TEXT tokens can contain both the target and the label separator.
|
|
35
|
+
const content = values.join("");
|
|
36
|
+
const separator = content.search(/[ \t]/);
|
|
37
|
+
return {
|
|
38
|
+
first: separator === -1 ? content : content.slice(0, separator),
|
|
39
|
+
label: separator === -1 ? "" : content.slice(separator).trimStart(),
|
|
40
|
+
consumed: pos - startPos + 1,
|
|
41
|
+
};
|
|
64
42
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Text_Wiki Email.php described-email grammar; no URI header characters. */
|
|
2
|
+
export function isBracketEmail(target: string): boolean {
|
|
3
|
+
return /^[_a-z0-9-]+(?:\.[_a-z0-9-]+)*@[a-z0-9-]+(?:\.[a-z0-9-]+)+$/i.test(target);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Single-bracket Interwiki.php grammar, restricted to the verified Wikipedia mapping. */
|
|
7
|
+
export function wikipediaPage(target: string): string | null {
|
|
8
|
+
return /^wikipedia:((?!:)[A-Za-z0-9_/=&~#.:;+-]+)$/.exec(target)?.[1] ?? null;
|
|
9
|
+
}
|
|
@@ -8,15 +8,17 @@
|
|
|
8
8
|
* Supported URL formats:
|
|
9
9
|
* - Absolute URLs: `[https://example.com/ Label]`
|
|
10
10
|
* - Relative paths: `[/some-page Label]`
|
|
11
|
+
* - Email addresses: `[support@example.com Label]`
|
|
12
|
+
* - Wikipedia links: `[wikipedia:Article]` or `[wikipedia:Article Label]`
|
|
11
13
|
*
|
|
12
14
|
* An optional `*` prefix on the URL opens the link in a new tab:
|
|
13
15
|
* `[*https://example.com/ Opens in new tab]`.
|
|
14
16
|
*
|
|
15
17
|
* Unlike triple-bracket links (`[[[page]]]`), single-bracket links
|
|
16
|
-
* require
|
|
17
|
-
* The label text is required.
|
|
18
|
+
* require an external target or site-relative path.
|
|
19
|
+
* The label text is required except for Wikipedia links.
|
|
18
20
|
*
|
|
19
|
-
* Produces a `"link"` AST element with `type: "direct"`.
|
|
21
|
+
* Produces a `"link"` AST element with `type: "direct"` or `"interwiki"`.
|
|
20
22
|
*
|
|
21
23
|
* @module
|
|
22
24
|
*/
|
|
@@ -33,8 +35,8 @@ import { parseSingleBracketLink } from "./link-bracket/parsed";
|
|
|
33
35
|
*
|
|
34
36
|
* Fails if:
|
|
35
37
|
* - No closing `]` is found on the same line
|
|
36
|
-
* - The
|
|
37
|
-
* - The label
|
|
38
|
+
* - The target is not a supported URL, email address, or Wikipedia target
|
|
39
|
+
* - The label is empty for a non-Wikipedia target
|
|
38
40
|
*/
|
|
39
41
|
export const linkSingleRule: InlineRule = {
|
|
40
42
|
name: "linkSingle",
|
|
@@ -44,7 +46,7 @@ export const linkSingleRule: InlineRule = {
|
|
|
44
46
|
* Attempts to parse a single-bracket link at the current position.
|
|
45
47
|
*
|
|
46
48
|
* @param ctx - Parse context with token stream and current position
|
|
47
|
-
* @returns A successful result with a `"link"` element of type `"direct"`,
|
|
49
|
+
* @returns A successful result with a `"link"` element of type `"direct"` or `"interwiki"`,
|
|
48
50
|
* or `{ success: false }`
|
|
49
51
|
*/
|
|
50
52
|
parse(ctx: ParseContext): RuleResult<Element> {
|
|
@@ -59,7 +61,7 @@ export const linkSingleRule: InlineRule = {
|
|
|
59
61
|
{
|
|
60
62
|
element: "link",
|
|
61
63
|
data: {
|
|
62
|
-
type: "direct",
|
|
64
|
+
type: parsed.interwiki ? "interwiki" : "direct",
|
|
63
65
|
link: parsed.link,
|
|
64
66
|
extra: null,
|
|
65
67
|
label: linkLabel,
|
|
@@ -36,6 +36,7 @@ export const linkTripleRule: InlineRule = {
|
|
|
36
36
|
const normalized = normalizeTripleLinkTarget(trimmedTarget);
|
|
37
37
|
const { linkType, link } = determineLinkTypeAndLocation(normalized.target);
|
|
38
38
|
const label = buildTripleLinkLabel({
|
|
39
|
+
isPage: linkType === "page",
|
|
39
40
|
foundPipe: parts.foundPipe,
|
|
40
41
|
labelText: parts.labelText,
|
|
41
42
|
finalTarget: normalized.target,
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import type { LinkLabel } from "@wdprlib/ast";
|
|
2
2
|
|
|
3
3
|
export function buildTripleLinkLabel(args: {
|
|
4
|
+
isPage: boolean;
|
|
4
5
|
foundPipe: boolean;
|
|
5
6
|
labelText: string;
|
|
6
7
|
finalTarget: string;
|
|
7
8
|
originalTarget: string;
|
|
8
9
|
}): LinkLabel {
|
|
10
|
+
if (args.isPage && args.foundPipe && !args.labelText.trim()) return "page";
|
|
9
11
|
return {
|
|
10
|
-
text: getTripleLinkDisplayText(
|
|
12
|
+
text: getTripleLinkDisplayText({
|
|
13
|
+
...args,
|
|
14
|
+
originalTarget:
|
|
15
|
+
args.isPage && !args.foundPipe ? args.originalTarget.split("#")[0]! : args.originalTarget,
|
|
16
|
+
}),
|
|
11
17
|
};
|
|
12
18
|
}
|
|
13
19
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Element } from "@wdprlib/ast";
|
|
2
|
+
import type { Token } from "../../../../lexer";
|
|
3
|
+
import type { ParseContext } from "../../types";
|
|
4
|
+
|
|
5
|
+
// Suppressed newlines must not cause an earlier, unrelated break to be removed.
|
|
6
|
+
const origins = new WeakMap<Element, Token>();
|
|
7
|
+
|
|
8
|
+
export function createAutomaticLineBreak(token: Token): Element {
|
|
9
|
+
const element: Element = { element: "line-break" };
|
|
10
|
+
origins.set(element, token);
|
|
11
|
+
return element;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function precedingSingleNewline(ctx: ParseContext): Token | undefined {
|
|
15
|
+
let pos = ctx.pos - 1;
|
|
16
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") pos--;
|
|
17
|
+
const newline = ctx.tokens[pos];
|
|
18
|
+
if (newline?.type !== "NEWLINE") return undefined;
|
|
19
|
+
pos--;
|
|
20
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") pos--;
|
|
21
|
+
if (ctx.tokens[pos]?.type === "NEWLINE") return undefined;
|
|
22
|
+
return newline;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function stripAutomaticLineBreak(elements: Element[], token: Token | undefined): void {
|
|
26
|
+
if (!token) return;
|
|
27
|
+
let index = elements.length - 1;
|
|
28
|
+
while (index >= 0) {
|
|
29
|
+
const element = elements[index];
|
|
30
|
+
if (element?.element !== "text" || !/^[ \t]*$/.test(element.data)) break;
|
|
31
|
+
index--;
|
|
32
|
+
}
|
|
33
|
+
const element = elements[index];
|
|
34
|
+
if (element && origins.get(element) === token) elements.length = index;
|
|
35
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { findNoteBounds } from "../../block/note/boundary";
|
|
1
2
|
import type { ParseContext } from "../../types";
|
|
2
3
|
import { INDENT_ACCEPTING_BLOCK_NAMES, KNOWN_BLOCK_NAMES } from "../../../constants";
|
|
3
4
|
import { parseBlockName } from "../../common";
|
|
@@ -27,6 +28,7 @@ export function isUnknownBlockToken(ctx: ParseContext, tokenPos: number): boolea
|
|
|
27
28
|
}
|
|
28
29
|
return true;
|
|
29
30
|
}
|
|
31
|
+
if (nameResult.name === "note") return findNoteBounds(ctx, tokenPos) === null;
|
|
30
32
|
return !KNOWN_BLOCK_NAMES.has(nameResult.name);
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -32,11 +32,21 @@ export function isParagraphBreakingBlockStart(
|
|
|
32
32
|
!isAnchorName(ctx, nextPos) &&
|
|
33
33
|
!isInvalidBlockOpen(ctx, nextPos) &&
|
|
34
34
|
!isInvalidHeading(ctx, nextPos) &&
|
|
35
|
+
!isInvalidListMarker(ctx, nextPos) &&
|
|
35
36
|
!isExcludedBlockStart(ctx, nextPos) &&
|
|
36
37
|
!isUnknownBlockStart(ctx, nextPos)
|
|
37
38
|
);
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
function isInvalidListMarker(ctx: ParseContext, markerPos: number): boolean {
|
|
42
|
+
const marker = ctx.tokens[markerPos];
|
|
43
|
+
if (marker?.type !== "LIST_BULLET" && marker?.type !== "LIST_NUMBER") return false;
|
|
44
|
+
|
|
45
|
+
// リスト構文はマーカー直後の空白が必須(list/line.tsと同じ規則)。
|
|
46
|
+
// 空白なしの行(例: 行頭の `*http://...`)はリストにならないため段落境界にもしない
|
|
47
|
+
return ctx.tokens[markerPos + 1]?.type !== "WHITESPACE";
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
function isOrphanCloseSpan(ctx: ParseContext, blockEndOpenPos: number): boolean {
|
|
41
51
|
const token = ctx.tokens[blockEndOpenPos];
|
|
42
52
|
if (token?.type !== "BLOCK_END_OPEN") return false;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { stripAutomaticLineBreak } from "./automatic-line-break";
|
|
2
|
+
import { parseButtonSyntax } from "../button/syntax";
|
|
3
|
+
import { parseSocialSyntax } from "../social/syntax";
|
|
4
|
+
import { parseDateSyntax } from "../date/syntax";
|
|
5
|
+
import { emailRegionEnd } from "../email/candidates";
|
|
1
6
|
import { protectedInlineRegionEnd } from "../raw/end";
|
|
2
7
|
import type { Element } from "@wdprlib/ast";
|
|
3
8
|
import type { ParseContext } from "../../types";
|
|
@@ -7,7 +12,10 @@ import {
|
|
|
7
12
|
type InlineEndType,
|
|
8
13
|
} from "./plain-text";
|
|
9
14
|
import { getParagraphNewlineBoundary } from "./paragraph-boundary";
|
|
10
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
createPreservedLeadingLineBreak,
|
|
17
|
+
createPreservedTrailingLineBreak,
|
|
18
|
+
} from "./preserved-line-break";
|
|
11
19
|
import { getCandidateInlineRules } from "./rules";
|
|
12
20
|
import { parseSimpleInlineToken } from "./simple-token";
|
|
13
21
|
|
|
@@ -21,6 +29,7 @@ export interface InlineParseResult {
|
|
|
21
29
|
|
|
22
30
|
/**
|
|
23
31
|
* Parse inline content until a specific token type.
|
|
32
|
+
* EOF mode preserves literal newlines within the inherited inline boundary.
|
|
24
33
|
*
|
|
25
34
|
* When endType is "PARAGRAPH_BREAK", handles NEWLINEs and stops at:
|
|
26
35
|
* - Double NEWLINE (paragraph break)
|
|
@@ -31,13 +40,21 @@ export function parseInlineUntil(ctx: ParseContext, endType: InlineEndType): Inl
|
|
|
31
40
|
const nodes: Element[] = [];
|
|
32
41
|
let consumed = 0;
|
|
33
42
|
let pos = ctx.pos;
|
|
43
|
+
let consumedEmptyRaw = false;
|
|
34
44
|
|
|
35
45
|
const paragraphMode = endType === "PARAGRAPH_BREAK";
|
|
36
|
-
const
|
|
46
|
+
const preserveNewlines = endType === "EOF";
|
|
47
|
+
const multiline = paragraphMode || preserveNewlines || FORMATTING_CLOSE_TOKENS.has(endType);
|
|
37
48
|
let inlineEnd = ctx.scope.inlineEnd ?? ctx.tokens.length;
|
|
38
49
|
if (!multiline) {
|
|
39
50
|
for (let end = ctx.pos; end < inlineEnd; end++) {
|
|
40
|
-
const protectedEnd =
|
|
51
|
+
const protectedEnd = Math.max(
|
|
52
|
+
parseButtonSyntax(ctx, end, inlineEnd)?.end ?? end,
|
|
53
|
+
parseSocialSyntax(ctx, end, inlineEnd)?.end ?? end,
|
|
54
|
+
parseDateSyntax(ctx, end, inlineEnd)?.end ?? end,
|
|
55
|
+
emailRegionEnd(ctx.tokens, end, inlineEnd),
|
|
56
|
+
protectedInlineRegionEnd(ctx.tokens, end, inlineEnd),
|
|
57
|
+
);
|
|
41
58
|
if (protectedEnd > end) {
|
|
42
59
|
end = protectedEnd - 1;
|
|
43
60
|
continue;
|
|
@@ -79,6 +96,13 @@ export function parseInlineUntil(ctx: ParseContext, endType: InlineEndType): Inl
|
|
|
79
96
|
break;
|
|
80
97
|
}
|
|
81
98
|
|
|
99
|
+
if (preserveNewlines && token.type === "NEWLINE") {
|
|
100
|
+
nodes.push({ element: "text", data: token.value });
|
|
101
|
+
pos++;
|
|
102
|
+
consumed++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
82
106
|
if (multiline && token.type === "NEWLINE" && !ctx.scope.tableFormatting) {
|
|
83
107
|
const boundary = getParagraphNewlineBoundary(ctx, pos, nodes.length > 0);
|
|
84
108
|
if (boundary.shouldBreak) {
|
|
@@ -97,11 +121,12 @@ export function parseInlineUntil(ctx: ParseContext, endType: InlineEndType): Inl
|
|
|
97
121
|
continue;
|
|
98
122
|
}
|
|
99
123
|
|
|
100
|
-
|
|
124
|
+
const hasEmail = emailRegionEnd(ctx.tokens, pos, inlineEnd) > pos;
|
|
125
|
+
if (token.type === endType && !hasEmail) {
|
|
101
126
|
break;
|
|
102
127
|
}
|
|
103
128
|
|
|
104
|
-
if (canCollectLongPlainTextRuns) {
|
|
129
|
+
if (canCollectLongPlainTextRuns && !hasEmail) {
|
|
105
130
|
const plainTextRun = collectLongPlainTextRun(ctx, pos, endType);
|
|
106
131
|
if (plainTextRun) {
|
|
107
132
|
nodes.push({ element: "text", data: plainTextRun.value });
|
|
@@ -112,7 +137,7 @@ export function parseInlineUntil(ctx: ParseContext, endType: InlineEndType): Inl
|
|
|
112
137
|
}
|
|
113
138
|
|
|
114
139
|
const simpleToken = parseSimpleInlineToken(token, ctx.tokens[pos + 1]);
|
|
115
|
-
if (simpleToken) {
|
|
140
|
+
if (simpleToken && !hasEmail) {
|
|
116
141
|
nodes.push(simpleToken.element);
|
|
117
142
|
consumed += simpleToken.consumed;
|
|
118
143
|
pos += simpleToken.consumed;
|
|
@@ -125,6 +150,10 @@ export function parseInlineUntil(ctx: ParseContext, endType: InlineEndType): Inl
|
|
|
125
150
|
for (const rule of getCandidateInlineRules(inlineRules, token.type)) {
|
|
126
151
|
const result = rule.parse(inlineCtx);
|
|
127
152
|
if (result.success) {
|
|
153
|
+
stripAutomaticLineBreak(nodes, result.stripLeadingLineBreak);
|
|
154
|
+
if (rule.name === "raw" && result.elements.length === 0 && nodes.length === 0) {
|
|
155
|
+
consumedEmptyRaw = true;
|
|
156
|
+
}
|
|
128
157
|
if (rule.name === "comment") {
|
|
129
158
|
let after = pos + result.consumed;
|
|
130
159
|
while (ctx.tokens[after]?.type === "WHITESPACE") after++;
|
|
@@ -139,7 +168,17 @@ export function parseInlineUntil(ctx: ParseContext, endType: InlineEndType): Inl
|
|
|
139
168
|
if (nodes.at(-1)?.element === "line-break") nodes.pop();
|
|
140
169
|
}
|
|
141
170
|
}
|
|
142
|
-
|
|
171
|
+
for (const element of result.elements) {
|
|
172
|
+
nodes.push(
|
|
173
|
+
paragraphMode &&
|
|
174
|
+
consumedEmptyRaw &&
|
|
175
|
+
nodes.length === 0 &&
|
|
176
|
+
token.type === "NEWLINE" &&
|
|
177
|
+
element.element === "line-break"
|
|
178
|
+
? createPreservedLeadingLineBreak()
|
|
179
|
+
: element,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
143
182
|
consumed += result.consumed;
|
|
144
183
|
pos += result.consumed;
|
|
145
184
|
matched = true;
|