@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,28 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
* Parser rule for the Wikidot `[[module Rate]]` block.
|
|
4
|
-
*
|
|
5
|
-
* Renders a page rating widget. This is a simple module with no attributes
|
|
6
|
-
* and no body content.
|
|
7
|
-
*
|
|
8
|
-
* @module
|
|
9
|
-
*/
|
|
10
|
-
|
|
1
|
+
import type { CustomRateModuleData, RateModuleData } from "@wdprlib/ast";
|
|
11
2
|
import type { ModuleRule } from "../types";
|
|
12
|
-
import type { RateModuleData } from "./types";
|
|
13
3
|
|
|
14
|
-
/**
|
|
15
|
-
* Module rule for `[[module Rate]]`.
|
|
16
|
-
*
|
|
17
|
-
* Simply produces a `{ module: "rate" }` AST node. The rendering application
|
|
18
|
-
* is responsible for displaying upvote/downvote buttons and the current rating.
|
|
19
|
-
*/
|
|
4
|
+
/** The main rating accepts no page, axis, policy, or permission attributes. */
|
|
20
5
|
export const rateModuleRule: ModuleRule = {
|
|
21
6
|
name: "module-rate",
|
|
22
7
|
acceptsNames: ["rate"],
|
|
23
8
|
hasBody: false,
|
|
9
|
+
parse(_ctx, _pos, args): RateModuleData {
|
|
10
|
+
return { module: "rate", ref: Object.keys(args).length === 0 ? { kind: "main" } : null };
|
|
11
|
+
},
|
|
12
|
+
};
|
|
24
13
|
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
/** Reference an exact, host-registered key on the displayed page. */
|
|
15
|
+
export const customRateModuleRule: ModuleRule = {
|
|
16
|
+
name: "module-custom-rate",
|
|
17
|
+
acceptsNames: ["customrate"],
|
|
18
|
+
hasBody: false,
|
|
19
|
+
parse(_ctx, _pos, args): CustomRateModuleData {
|
|
20
|
+
const key = args.key;
|
|
21
|
+
const valid =
|
|
22
|
+
key !== undefined && key !== "" && Object.keys(args).every((name) => name === "key");
|
|
23
|
+
return { module: "custom-rate", ref: valid ? { kind: "custom", axisKey: key } : null };
|
|
27
24
|
},
|
|
28
25
|
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Element, RatingRef, RatingState, SyntaxTree } from "@wdprlib/ast";
|
|
2
|
+
import { getModuleParseAst, type ModuleParseResult } from "../types";
|
|
3
|
+
import { mapElementChildren } from "../walk";
|
|
4
|
+
import type { RatingsFetcher } from "./types";
|
|
5
|
+
|
|
6
|
+
function refKey(ref: RatingRef): string {
|
|
7
|
+
return ref.kind === "main" ? "main" : `custom:${ref.axisKey}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Resolve once after includes and module expansion, using only the displayed page. */
|
|
11
|
+
export async function resolveRatings(
|
|
12
|
+
ast: SyntaxTree,
|
|
13
|
+
fetchRatings?: RatingsFetcher,
|
|
14
|
+
): Promise<SyntaxTree> {
|
|
15
|
+
const refs = new Map<string, RatingRef>();
|
|
16
|
+
mapDocument(ast, (element) => {
|
|
17
|
+
if (isRating(element) && element.data.ref) refs.set(refKey(element.data.ref), element.data.ref);
|
|
18
|
+
return element;
|
|
19
|
+
});
|
|
20
|
+
const states = new Map<string, RatingState>();
|
|
21
|
+
if (fetchRatings && refs.size > 0) {
|
|
22
|
+
for (const state of await fetchRatings([...refs.values()])) {
|
|
23
|
+
const key = refKey(state.ref);
|
|
24
|
+
if (refs.has(key)) states.set(key, state);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return mapDocument(ast, (element) => {
|
|
28
|
+
if (!isRating(element)) return element;
|
|
29
|
+
const { state: _previous, ...data } = element.data;
|
|
30
|
+
const state = data.ref ? states.get(refKey(data.ref)) : undefined;
|
|
31
|
+
return { ...element, data: state ? { ...data, state } : data };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Module-generated source cannot declare ratings, including in its footnote side channel. */
|
|
36
|
+
export function suppressModuleRatings(result: ModuleParseResult): ModuleParseResult {
|
|
37
|
+
const ast = mapDocument(getModuleParseAst(result), (element) =>
|
|
38
|
+
isRating(element) ? null : element,
|
|
39
|
+
);
|
|
40
|
+
return "ast" in result ? { ...result, ast } : ast;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isRating(element: Element): element is Extract<Element, { element: "module" }> & {
|
|
44
|
+
data: Extract<
|
|
45
|
+
Extract<Element, { element: "module" }>["data"],
|
|
46
|
+
{ module: "rate" | "custom-rate" }
|
|
47
|
+
>;
|
|
48
|
+
} {
|
|
49
|
+
return (
|
|
50
|
+
element.element === "module" &&
|
|
51
|
+
(element.data.module === "rate" || element.data.module === "custom-rate")
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function mapDocument(ast: SyntaxTree, transform: (element: Element) => Element | null): SyntaxTree {
|
|
56
|
+
const map = (elements: Element[]): Element[] =>
|
|
57
|
+
elements.flatMap((original) => {
|
|
58
|
+
const element = transform(original);
|
|
59
|
+
if (!element) return [];
|
|
60
|
+
if (element.element === "if") {
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
...element,
|
|
64
|
+
data: {
|
|
65
|
+
...element.data,
|
|
66
|
+
// oxlint-disable-next-line unicorn/no-thenable -- public AST branch name
|
|
67
|
+
then: map(element.data.then),
|
|
68
|
+
else: map(element.data.else),
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
if (element.element === "ifexpr") {
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
...element,
|
|
77
|
+
data: {
|
|
78
|
+
...element.data,
|
|
79
|
+
// oxlint-disable-next-line unicorn/no-thenable -- public AST branch name
|
|
80
|
+
then: map(element.data.then),
|
|
81
|
+
else: map(element.data.else),
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
if (element.element === "bibliography-block") {
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
...element,
|
|
90
|
+
data: {
|
|
91
|
+
...element.data,
|
|
92
|
+
entries: element.data.entries.map((entry) => ({
|
|
93
|
+
...entry,
|
|
94
|
+
key: map(entry.key),
|
|
95
|
+
value: map(entry.value),
|
|
96
|
+
})),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
return [mapElementChildren(element, map)];
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
...ast,
|
|
105
|
+
elements: map(ast.elements),
|
|
106
|
+
...(ast.footnotes ? { footnotes: ast.footnotes.map(map) } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Type definitions for the Rate module.
|
|
4
|
-
*
|
|
5
|
-
* The `[[module Rate]]` block renders a page rating widget (upvote/downvote buttons).
|
|
6
|
-
* It takes no attributes and has no body.
|
|
7
|
-
*
|
|
8
|
-
* @module
|
|
9
|
-
*/
|
|
1
|
+
import type { RatingRef, RatingState } from "@wdprlib/ast";
|
|
2
|
+
export type { RateModuleData, CustomRateModuleData } from "@wdprlib/ast";
|
|
10
3
|
|
|
11
4
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* is responsible for displaying the appropriate rating widget.
|
|
5
|
+
* Read authorized ratings for the displayed page. Omit unknown, disabled, or
|
|
6
|
+
* inaccessible references. The host owns registration, category policies,
|
|
7
|
+
* access checks, aggregates, and persistence; rendering must never create data.
|
|
16
8
|
*/
|
|
17
|
-
export
|
|
18
|
-
module: "rate";
|
|
19
|
-
}
|
|
9
|
+
export type RatingsFetcher = (refs: readonly RatingRef[]) => Promise<readonly RatingState[]>;
|
|
@@ -28,6 +28,7 @@ import { ModuleDocumentRegistry } from "./document";
|
|
|
28
28
|
import { collectStyles, mergeCollectedStyles } from "./styles";
|
|
29
29
|
import { createListPagesPager } from "../listpages/resolution/pager";
|
|
30
30
|
import type { ListPagesPaginationState } from "./data-maps";
|
|
31
|
+
import { suppressModuleRatings } from "../rate/resolve";
|
|
31
32
|
|
|
32
33
|
export type AsyncModuleParseFunction = (source: string) => Promise<ModuleParseResult>;
|
|
33
34
|
|
|
@@ -73,7 +74,7 @@ export async function resolveModulesWithAsyncParse(
|
|
|
73
74
|
const registry = new ModuleDocumentRegistry();
|
|
74
75
|
registry.register(ast);
|
|
75
76
|
const parse = async (source: string): Promise<SyntaxTree> =>
|
|
76
|
-
registry.register(await options.parse(source));
|
|
77
|
+
registry.register(suppressModuleRatings(await options.parse(source)));
|
|
77
78
|
|
|
78
79
|
const [listPagesData, listUsersData, tagCloudData] = await Promise.all([
|
|
79
80
|
buildListPagesDataMap(
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import { walkAndResolve } from "./resolution/walk-resolve";
|
|
32
32
|
import { collectStyles, mergeCollectedStyles } from "./resolution/styles";
|
|
33
33
|
import { containsSyntaxFootnoteBlock, ModuleDocumentRegistry } from "./resolution/document";
|
|
34
|
+
import { resolveRatings, suppressModuleRatings } from "./rate/resolve";
|
|
34
35
|
|
|
35
36
|
const MODULE_SECONDARY_INCLUDE_MAX_ITERATIONS = 5;
|
|
36
37
|
|
|
@@ -188,11 +189,9 @@ export async function resolveModules(
|
|
|
188
189
|
if (mergedStyles.length > 0) result.styles = mergedStyles;
|
|
189
190
|
|
|
190
191
|
options.onDiagnostics?.(registry.diagnostics);
|
|
191
|
-
return
|
|
192
|
-
result,
|
|
193
|
-
|
|
194
|
-
pageTags,
|
|
195
|
-
containsSyntaxFootnoteBlock(ast.elements),
|
|
192
|
+
return resolveRatings(
|
|
193
|
+
registry.finalize(result, finalElements, pageTags, containsSyntaxFootnoteBlock(ast.elements)),
|
|
194
|
+
dataProvider.fetchRatings,
|
|
196
195
|
);
|
|
197
196
|
}
|
|
198
197
|
|
|
@@ -203,9 +202,12 @@ function createModuleParseFunction(
|
|
|
203
202
|
): ParseFunction {
|
|
204
203
|
const transform = createModuleSourceTransform(options, dataProvider);
|
|
205
204
|
return (source: string) =>
|
|
206
|
-
registry.register(
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
registry.register(
|
|
206
|
+
suppressModuleRatings(options.parse(transform ? transform(source) : source)),
|
|
207
|
+
{
|
|
208
|
+
stripLegacyImplicitFootnoteBlock: true,
|
|
209
|
+
},
|
|
210
|
+
);
|
|
209
211
|
}
|
|
210
212
|
|
|
211
213
|
function createModuleSourceTransform(
|
|
@@ -17,6 +17,7 @@ import type { ListUsersDataFetcher } from "./listusers/types";
|
|
|
17
17
|
import type { TagCloudDataFetcher } from "./tagcloud/types";
|
|
18
18
|
import type { IfTagsResolver } from "./iftags/types";
|
|
19
19
|
import type { IncludeFetcher } from "./include/resolve/types";
|
|
20
|
+
import type { RatingsFetcher } from "./rate/types";
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Callback bag for supplying external data during module resolution.
|
|
@@ -29,6 +30,9 @@ import type { IncludeFetcher } from "./include/resolve/types";
|
|
|
29
30
|
* @group Module Resolution
|
|
30
31
|
*/
|
|
31
32
|
export interface DataProvider {
|
|
33
|
+
/** Read registered ratings for the displayed page; omitted references stay hidden. */
|
|
34
|
+
fetchRatings?: RatingsFetcher;
|
|
35
|
+
|
|
32
36
|
/**
|
|
33
37
|
* Fetch page data for `[[module ListPages]]` expansion.
|
|
34
38
|
*
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Token } from "../../../../lexer";
|
|
2
|
+
import type { ParseContext } from "../../types";
|
|
3
|
+
import { createOpaqueProbe, opaqueRuleEnd } from "../../opaque-probe";
|
|
4
|
+
import { codeBlockRule } from "../code";
|
|
5
|
+
import { mathBlockRule } from "../math";
|
|
6
|
+
import { linkTripleRule } from "../../inline/link-triple";
|
|
7
|
+
import { protectedInlineRegionEnd } from "../../inline/raw/end";
|
|
8
|
+
|
|
9
|
+
// These constructs are replaced before Note in Wikidot's processing order.
|
|
10
|
+
const opaqueRules = [codeBlockRule, mathBlockRule, linkTripleRule];
|
|
11
|
+
interface NoteBounds {
|
|
12
|
+
bodyStart: number;
|
|
13
|
+
close: number;
|
|
14
|
+
end: number;
|
|
15
|
+
}
|
|
16
|
+
const boundsCache = new WeakMap<readonly Token[], Map<number, NoteBounds | null>>();
|
|
17
|
+
const closeCache = new WeakMap<readonly Token[], Map<string, Map<number, number>>>();
|
|
18
|
+
|
|
19
|
+
function tagEnd(tokens: readonly Token[], pos: number, name: string, close = false): number {
|
|
20
|
+
return tokens[pos]?.type === (close ? "BLOCK_END_OPEN" : "BLOCK_OPEN") &&
|
|
21
|
+
tokens[pos + 1]?.value.toLowerCase() === name &&
|
|
22
|
+
tokens[pos + 2]?.type === "BLOCK_CLOSE"
|
|
23
|
+
? pos + 3
|
|
24
|
+
: pos;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findClose(probe: ParseContext, start: number, name: string): number {
|
|
28
|
+
let byName = closeCache.get(probe.tokens);
|
|
29
|
+
if (!byName) {
|
|
30
|
+
byName = new Map();
|
|
31
|
+
closeCache.set(probe.tokens, byName);
|
|
32
|
+
}
|
|
33
|
+
let cache = byName.get(name);
|
|
34
|
+
if (!cache) {
|
|
35
|
+
cache = new Map();
|
|
36
|
+
byName.set(name, cache);
|
|
37
|
+
}
|
|
38
|
+
if (cache.has(start)) return cache.get(start)!;
|
|
39
|
+
const visited: number[] = [];
|
|
40
|
+
let result = -1;
|
|
41
|
+
for (let pos = start; pos < probe.tokens.length; pos++) {
|
|
42
|
+
if (cache.has(pos)) {
|
|
43
|
+
result = cache.get(pos)!;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
visited.push(pos);
|
|
47
|
+
const protectedEnd = protectedInlineRegionEnd(probe.tokens, pos, probe.tokens.length);
|
|
48
|
+
if (protectedEnd > pos) {
|
|
49
|
+
pos = protectedEnd - 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (tagEnd(probe.tokens, pos, name, true) > pos) {
|
|
53
|
+
result = pos;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
const opaqueEnd = opaqueRuleEnd(probe, pos, opaqueRules, true);
|
|
57
|
+
if (opaqueEnd > pos) {
|
|
58
|
+
pos = opaqueEnd - 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// Footnote precedes Note and uses its first complete close, without nesting.
|
|
62
|
+
if (name === "note") {
|
|
63
|
+
const footnoteStart = tagEnd(probe.tokens, pos, "footnote");
|
|
64
|
+
if (footnoteStart > pos) {
|
|
65
|
+
const end = findClose(probe, footnoteStart, "footnote");
|
|
66
|
+
if (end >= 0) pos = end + 2;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Only visited positions are reusable: opaque interiors have different boundaries.
|
|
71
|
+
for (const pos of visited) cache.set(pos, result);
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function findNoteBounds(ctx: ParseContext, start: number = ctx.pos): NoteBounds | null {
|
|
76
|
+
const openEnd = tagEnd(ctx.tokens, start, "note");
|
|
77
|
+
if (openEnd === start || ctx.tokens[openEnd]?.type !== "NEWLINE") return null;
|
|
78
|
+
let cache = boundsCache.get(ctx.tokens);
|
|
79
|
+
if (!cache) {
|
|
80
|
+
cache = new Map();
|
|
81
|
+
boundsCache.set(ctx.tokens, cache);
|
|
82
|
+
}
|
|
83
|
+
if (cache.has(start)) return cache.get(start)!;
|
|
84
|
+
const close = findClose(createOpaqueProbe(ctx), openEnd + 1, "note");
|
|
85
|
+
const result = close < 0 ? null : { bodyStart: openEnd + 1, close, end: close + 3 };
|
|
86
|
+
cache.set(start, result);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { BlockRule } from "../../types";
|
|
2
|
+
import { parseBlocksUntil } from "../parsing/content";
|
|
3
|
+
import { findNoteBounds } from "./boundary";
|
|
4
|
+
|
|
5
|
+
const excludedBlockNames = new Set(["note"]);
|
|
6
|
+
|
|
7
|
+
export const noteRule: BlockRule = {
|
|
8
|
+
name: "note",
|
|
9
|
+
startTokens: ["BLOCK_OPEN"],
|
|
10
|
+
requiresLineStart: false,
|
|
11
|
+
parse(ctx) {
|
|
12
|
+
const bounds = findNoteBounds(ctx);
|
|
13
|
+
if (!bounds) return { success: false };
|
|
14
|
+
const body = parseBlocksUntil(
|
|
15
|
+
{
|
|
16
|
+
...ctx,
|
|
17
|
+
tokens: ctx.tokens.slice(bounds.bodyStart, bounds.close),
|
|
18
|
+
pos: 0,
|
|
19
|
+
scope: {
|
|
20
|
+
...ctx.scope,
|
|
21
|
+
inlineEnd: undefined,
|
|
22
|
+
tableFormatting: undefined,
|
|
23
|
+
blockCloseCondition: undefined,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
() => false,
|
|
27
|
+
{ excludedBlockNames },
|
|
28
|
+
);
|
|
29
|
+
return {
|
|
30
|
+
success: true,
|
|
31
|
+
consumed: bounds.end - ctx.pos,
|
|
32
|
+
elements: [
|
|
33
|
+
{ element: "container", data: { type: "note", attributes: {}, elements: body.elements } },
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { markUnparsedDivStart } from "../../../postprocess/divAdjacentParagraph";
|
|
2
|
+
import { isDivClose } from "../div/close";
|
|
1
3
|
/**
|
|
2
4
|
* Paragraph rule
|
|
3
5
|
*
|
|
@@ -8,6 +10,7 @@ import type { Element } from "@wdprlib/ast";
|
|
|
8
10
|
import type { BlockRule, ParseContext, RuleResult } from "../../types";
|
|
9
11
|
import { parseInlineContent } from "./content";
|
|
10
12
|
import { normalizeParagraphElements } from "./normalize";
|
|
13
|
+
import { isPreservedLeadingLineBreak } from "../../inline/parsing/preserved-line-break";
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* Paragraph is the fallback block rule.
|
|
@@ -23,18 +26,19 @@ export const paragraphRule: BlockRule = {
|
|
|
23
26
|
|
|
24
27
|
parse(ctx: ParseContext): RuleResult<Element> {
|
|
25
28
|
const result = parseInlineContent(ctx);
|
|
26
|
-
if (result.
|
|
29
|
+
if (result.consumed === 0) {
|
|
27
30
|
return { success: false };
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
const elements = normalizeParagraphElements(result.elements);
|
|
31
34
|
if (elements.length === 0) {
|
|
32
|
-
return { success:
|
|
35
|
+
return { success: true, elements: [], consumed: result.consumed };
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
const nextPos = ctx.pos + result.consumed;
|
|
36
39
|
const nextToken = ctx.tokens[nextPos];
|
|
37
40
|
if (nextToken?.type === "COLON" && nextToken.lineStart) {
|
|
41
|
+
if (isPreservedLeadingLineBreak(elements[0])) elements[0] = { element: "line-break" };
|
|
38
42
|
return {
|
|
39
43
|
success: true,
|
|
40
44
|
elements: [...elements, { element: "line-break" }],
|
|
@@ -42,9 +46,11 @@ export const paragraphRule: BlockRule = {
|
|
|
42
46
|
};
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
const wrapped = wrapParagraphElements(elements);
|
|
50
|
+
if (isDivClose(ctx)) markUnparsedDivStart(wrapped);
|
|
45
51
|
return {
|
|
46
52
|
success: true,
|
|
47
|
-
elements:
|
|
53
|
+
elements: wrapped,
|
|
48
54
|
consumed: result.consumed,
|
|
49
55
|
};
|
|
50
56
|
},
|
|
@@ -57,7 +63,8 @@ export function wrapParagraphElements(elements: Element[]): Element[] {
|
|
|
57
63
|
let bare = false;
|
|
58
64
|
const flush = (trimBreaks = false) => {
|
|
59
65
|
const content = trimBreaks ? normalizeParagraphElements(group) : group;
|
|
60
|
-
while (content[0]?.element === "line-break"
|
|
66
|
+
while (content[0]?.element === "line-break" && !isPreservedLeadingLineBreak(content[0]))
|
|
67
|
+
content.shift();
|
|
61
68
|
while (content.length) {
|
|
62
69
|
const last = content.at(-1)!;
|
|
63
70
|
if (last.element !== "text" || last.data.trim() !== "") break;
|
|
@@ -66,6 +73,7 @@ export function wrapParagraphElements(elements: Element[]): Element[] {
|
|
|
66
73
|
while (content[0]?.element === "text" && content[0].data.trim() === "") content.shift();
|
|
67
74
|
if (content[0]?.element === "text")
|
|
68
75
|
content[0] = { element: "text", data: content[0].data.trimStart() };
|
|
76
|
+
if (isPreservedLeadingLineBreak(content[0])) content[0] = { element: "line-break" };
|
|
69
77
|
if (content.length)
|
|
70
78
|
output.push(
|
|
71
79
|
...(bare || content.some((el) => el.element === "image")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Element } from "@wdprlib/ast";
|
|
2
2
|
import { processCloseSpanMarkers } from "./span-markers";
|
|
3
|
+
import { isPreservedLeadingLineBreak } from "../../inline/parsing/preserved-line-break";
|
|
3
4
|
|
|
4
5
|
type PreservedLineBreak = Extract<Element, { element: "line-break" }> & {
|
|
5
6
|
_preservedTrailingBreak?: boolean;
|
|
@@ -44,7 +45,11 @@ function removeTrailingWhitespaceText(elements: Element[]): void {
|
|
|
44
45
|
|
|
45
46
|
function removeLeadingLineBreaks(elements: Element[]): Element[] {
|
|
46
47
|
let first = 0;
|
|
47
|
-
while (
|
|
48
|
+
while (
|
|
49
|
+
first < elements.length &&
|
|
50
|
+
elements[first]?.element === "line-break" &&
|
|
51
|
+
!isPreservedLeadingLineBreak(elements[first])
|
|
52
|
+
) {
|
|
48
53
|
first++;
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -21,7 +21,7 @@ export function parseBlockItem(ctx: ParseContext): BlockItemResult {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const fallback = ctx.blockFallbackRule.parse(ctx);
|
|
24
|
-
if (fallback.success
|
|
24
|
+
if (fallback.success) {
|
|
25
25
|
return { elements: fallback.elements, consumed: fallback.consumed };
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -101,7 +101,7 @@ export function parseBlocksUntil(
|
|
|
101
101
|
blockCtx.scope = blockScope;
|
|
102
102
|
|
|
103
103
|
const result = parseBlockItem(blockCtx);
|
|
104
|
-
|
|
104
|
+
for (const element of result.elements) elements.push(element);
|
|
105
105
|
consumed += result.consumed;
|
|
106
106
|
pos += result.consumed;
|
|
107
107
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { stripAutomaticLineBreak } from "../../inline/parsing/automatic-line-break";
|
|
2
|
+
import { createAutomaticLineBreak } 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";
|
|
@@ -58,7 +60,7 @@ export function parseInlineContentUntil(
|
|
|
58
60
|
consumed += newlineResult.consumed;
|
|
59
61
|
pos += newlineResult.consumed;
|
|
60
62
|
if (newlineResult.addLineBreak) {
|
|
61
|
-
elements.push(
|
|
63
|
+
elements.push(createAutomaticLineBreak(token));
|
|
62
64
|
}
|
|
63
65
|
continue;
|
|
64
66
|
}
|
|
@@ -69,7 +71,7 @@ export function parseInlineContentUntil(
|
|
|
69
71
|
for (const rule of getCandidateBlockRules(blockRules, token)) {
|
|
70
72
|
const result = rule.parse(blockCtx);
|
|
71
73
|
if (result.success) {
|
|
72
|
-
|
|
74
|
+
for (const element of result.elements) elements.push(element);
|
|
73
75
|
consumed += result.consumed;
|
|
74
76
|
pos += result.consumed;
|
|
75
77
|
matched = true;
|
|
@@ -84,7 +86,8 @@ export function parseInlineContentUntil(
|
|
|
84
86
|
for (const rule of getCandidateInlineRules(inlineRules, token.type)) {
|
|
85
87
|
const result = rule.parse(inlineCtx);
|
|
86
88
|
if (result.success) {
|
|
87
|
-
elements
|
|
89
|
+
stripAutomaticLineBreak(elements, result.stripLeadingLineBreak);
|
|
90
|
+
for (const element of result.elements) elements.push(element);
|
|
88
91
|
consumed += result.consumed;
|
|
89
92
|
pos += result.consumed;
|
|
90
93
|
matched = true;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stripAutomaticLineBreak } from "../../../inline/parsing/automatic-line-break";
|
|
1
2
|
import { protectedInlineRegionEnd } from "../../../inline/raw/end";
|
|
2
3
|
import type { Element, TableCell } from "@wdprlib/ast";
|
|
3
4
|
import type { ParseContext } from "../../../types";
|
|
@@ -85,7 +86,8 @@ export function parseTableCell(
|
|
|
85
86
|
for (const rule of getCandidateInlineRules(inlineRules, token.type)) {
|
|
86
87
|
const result = rule.parse(inlineCtx);
|
|
87
88
|
if (result.success) {
|
|
88
|
-
children
|
|
89
|
+
stripAutomaticLineBreak(children, result.stripLeadingLineBreak);
|
|
90
|
+
for (const element of result.elements) children.push(element);
|
|
89
91
|
consumed += result.consumed;
|
|
90
92
|
pos += result.consumed;
|
|
91
93
|
matched = true;
|
|
@@ -69,7 +69,7 @@ export function parseCellContent(
|
|
|
69
69
|
for (const rule of getCandidateInlineRules(ctx.inlineRules, token.type)) {
|
|
70
70
|
const result = rule.parse(inlineCtx);
|
|
71
71
|
if (result.success) {
|
|
72
|
-
content.addInlineElements(result.elements);
|
|
72
|
+
content.addInlineElements(result.elements, result.stripLeadingLineBreak);
|
|
73
73
|
consumed += result.consumed;
|
|
74
74
|
pos += result.consumed;
|
|
75
75
|
matched = true;
|
|
@@ -1,8 +1,10 @@
|
|
|
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
|
|
|
3
5
|
export interface CellContentAccumulator {
|
|
4
6
|
addInline(element: Element): void;
|
|
5
|
-
addInlineElements(elements: Element[]): void;
|
|
7
|
+
addInlineElements(elements: Element[], stripLeadingLineBreak?: Token): void;
|
|
6
8
|
addBlockElements(elements: Element[]): void;
|
|
7
9
|
addParagraphBreak(): void;
|
|
8
10
|
closeInlineSegmentBeforeBlock(): void;
|
|
@@ -40,7 +42,8 @@ export function createCellContentAccumulator(): CellContentAccumulator {
|
|
|
40
42
|
addInline(element: Element) {
|
|
41
43
|
currentSegment.push(element);
|
|
42
44
|
},
|
|
43
|
-
addInlineElements(nextElements: Element[]) {
|
|
45
|
+
addInlineElements(nextElements: Element[], stripLeadingLineBreak?: Token) {
|
|
46
|
+
stripAutomaticLineBreak(currentSegment, stripLeadingLineBreak);
|
|
44
47
|
currentSegment.push(...nextElements);
|
|
45
48
|
},
|
|
46
49
|
addBlockElements(blockElements: Element[]) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createAutomaticLineBreak } from "../../inline/parsing/automatic-line-break";
|
|
1
2
|
import type { ParseContext } from "../../types";
|
|
2
3
|
import type { CellContentAccumulator } from "./cell-content/segments";
|
|
3
4
|
|
|
@@ -40,7 +41,7 @@ export function consumeCellContentNewline(
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
if (!content.isEmpty()) {
|
|
43
|
-
content.addInline(
|
|
44
|
+
content.addInline(createAutomaticLineBreak(ctx.tokens[startPos]!));
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
return { consumed };
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { Element } from "@wdprlib/ast";
|
|
2
|
-
import type { TokenType } from "../../../lexer";
|
|
2
|
+
import type { Token, TokenType } from "../../../lexer";
|
|
3
3
|
import type { ParseContext } from "./parse-context";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Result of a rule attempt.
|
|
7
7
|
*/
|
|
8
|
-
export type RuleResult<T> =
|
|
8
|
+
export type RuleResult<T> =
|
|
9
|
+
| { success: true; elements: T[]; consumed: number; stripLeadingLineBreak?: Token }
|
|
10
|
+
| { success: false };
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Block rule interface.
|
|
@@ -10,6 +10,8 @@ import type { ParseContext } from "./parse-context";
|
|
|
10
10
|
export interface ScopeContext {
|
|
11
11
|
/** Exclusive token boundary inherited by nested inline rules. */
|
|
12
12
|
readonly inlineEnd?: number;
|
|
13
|
+
/** Keep bare addresses as text inside an existing anchor. */
|
|
14
|
+
readonly suppressEmailLinks?: boolean;
|
|
13
15
|
/** Closing delimiters paired across cells of the current pipe table. */
|
|
14
16
|
readonly tableFormatting?: { end: number; suppressedClosers: Set<number> };
|
|
15
17
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Token } from "../../../../lexer";
|
|
1
2
|
import type { Element } from "@wdprlib/ast";
|
|
2
3
|
import type { ParseContext } from "../../types";
|
|
3
4
|
import { inlineRules } from "../index";
|
|
@@ -6,6 +7,7 @@ import { getCandidateInlineRules } from "../utils";
|
|
|
6
7
|
export interface AnchorChildResult {
|
|
7
8
|
elements: Element[];
|
|
8
9
|
consumed: number;
|
|
10
|
+
stripLeadingLineBreak?: Token;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export function parseAnchorChild(ctx: ParseContext, pos: number): AnchorChildResult {
|
|
@@ -14,11 +16,15 @@ export function parseAnchorChild(ctx: ParseContext, pos: number): AnchorChildRes
|
|
|
14
16
|
return { elements: [], consumed: 0 };
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
const inlineCtx: ParseContext = {
|
|
19
|
+
const inlineCtx: ParseContext = {
|
|
20
|
+
...ctx,
|
|
21
|
+
pos,
|
|
22
|
+
scope: { ...ctx.scope, suppressEmailLinks: true },
|
|
23
|
+
};
|
|
18
24
|
for (const rule of getCandidateInlineRules(inlineRules, token.type)) {
|
|
19
25
|
const result = rule.parse(inlineCtx);
|
|
20
26
|
if (result.success) {
|
|
21
|
-
return
|
|
27
|
+
return result;
|
|
22
28
|
}
|
|
23
29
|
}
|
|
24
30
|
|
|
@@ -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 { parseAnchorChild } from "./child";
|
|
@@ -50,7 +51,8 @@ export function parseAnchorContent(
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
const child = parseAnchorChild(ctx, pos);
|
|
53
|
-
children
|
|
54
|
+
stripAutomaticLineBreak(children, child.stripLeadingLineBreak);
|
|
55
|
+
for (const element of child.elements) children.push(element);
|
|
54
56
|
pos += child.consumed;
|
|
55
57
|
consumed += child.consumed;
|
|
56
58
|
}
|