@wdprlib/parser 4.3.0 → 4.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/README.md +26 -38
- package/dist/index.cjs +801 -134
- package/dist/index.d.cts +195 -158
- package/dist/index.d.ts +195 -158
- package/dist/index.js +783 -113
- package/package.json +2 -2
- package/src/index.ts +11 -0
- package/src/parser/parse/context.ts +1 -0
- package/src/parser/parse/options.ts +6 -0
- package/src/parser/parse/result.ts +3 -1
- package/src/parser/rules/block/module/include/index.ts +6 -1
- package/src/parser/rules/block/module/include/resolve/index.ts +23 -3
- package/src/parser/rules/block/module/include/resolve/iterate.ts +71 -0
- package/src/parser/rules/block/module/index.ts +2 -1
- package/src/parser/rules/block/module/listpages/resolution/items.ts +2 -2
- package/src/parser/rules/block/module/listpages/resolution/wrapper.ts +3 -3
- package/src/parser/rules/block/module/listusers/resolve.ts +2 -2
- package/src/parser/rules/block/module/resolution/document.ts +357 -0
- package/src/parser/rules/block/module/resolution/resolve-async.ts +269 -0
- package/src/parser/rules/block/module/resolution/styles.ts +134 -12
- package/src/parser/rules/block/module/resolution/walk-resolve.ts +19 -1
- package/src/parser/rules/block/module/resolve.ts +32 -13
- package/src/parser/rules/block/module/types.ts +7 -2
- package/src/parser/rules/contracts/parse-context.ts +1 -0
- package/src/pipeline/index.ts +7 -0
- package/src/pipeline/process.ts +105 -0
- package/src/pipeline/types.ts +63 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wdprlib/parser",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"description": "Parser for Wikidot markup",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ast",
|
|
@@ -41,6 +41,6 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@braintree/sanitize-url": "^7.1.1",
|
|
44
|
-
"@wdprlib/ast": "2.
|
|
44
|
+
"@wdprlib/ast": "2.4.0"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/src/index.ts
CHANGED
|
@@ -98,6 +98,15 @@ export { Lexer, tokenize, createToken } from "./lexer";
|
|
|
98
98
|
export type { ParserOptions } from "./parser";
|
|
99
99
|
export { Parser, parse } from "./parser";
|
|
100
100
|
|
|
101
|
+
// High-level parser pipeline
|
|
102
|
+
export { processWikitext } from "./pipeline";
|
|
103
|
+
export type {
|
|
104
|
+
ProcessedWikitextDocument,
|
|
105
|
+
ProcessWikitextCallbackContext,
|
|
106
|
+
ProcessWikitextDataProvider,
|
|
107
|
+
ProcessWikitextOptions,
|
|
108
|
+
} from "./pipeline";
|
|
109
|
+
|
|
101
110
|
// Modules (ListPages, ListUsers, IfTags, Include, etc.)
|
|
102
111
|
export type {
|
|
103
112
|
// ListPages query types
|
|
@@ -121,6 +130,7 @@ export type {
|
|
|
121
130
|
ExtractionResult,
|
|
122
131
|
// Resolution types
|
|
123
132
|
ParseFunction,
|
|
133
|
+
ModuleParseResult,
|
|
124
134
|
ModuleSourceTransform,
|
|
125
135
|
ResolveOptions,
|
|
126
136
|
// Include resolution
|
|
@@ -163,6 +173,7 @@ export {
|
|
|
163
173
|
extractIncludeReferences,
|
|
164
174
|
resolveIncludes,
|
|
165
175
|
resolveIncludesAsync,
|
|
176
|
+
resolveIncludesAsyncWithTrace,
|
|
166
177
|
resolveIncludesWithTrace,
|
|
167
178
|
// IfTags source-level preprocessing (run between include expansion and parse)
|
|
168
179
|
preprocessIftags,
|
|
@@ -10,6 +10,7 @@ export function createParseContext(tokens: Token[], options: ParserOptions = {})
|
|
|
10
10
|
version: options.version ?? "wikidot",
|
|
11
11
|
trackPositions: options.trackPositions ?? true,
|
|
12
12
|
settings: options.settings ?? DEFAULT_SETTINGS,
|
|
13
|
+
appendImplicitFootnoteBlock: options.appendImplicitFootnoteBlock ?? true,
|
|
13
14
|
footnotes: [],
|
|
14
15
|
tocEntries: [],
|
|
15
16
|
codeBlocks: [],
|
|
@@ -31,4 +31,10 @@ export interface ParserOptions {
|
|
|
31
31
|
* - `string[]`: every iftags block is evaluated against the given tags eagerly.
|
|
32
32
|
*/
|
|
33
33
|
pageTags?: string[] | null;
|
|
34
|
+
/**
|
|
35
|
+
* Append the implicit document-level footnote block when no explicit block
|
|
36
|
+
* exists. Defaults to true. Fragment pipelines can disable this and add one
|
|
37
|
+
* block after all fragments have been merged.
|
|
38
|
+
*/
|
|
39
|
+
appendImplicitFootnoteBlock?: boolean;
|
|
34
40
|
}
|
|
@@ -10,7 +10,9 @@ import { containsFootnoteBlock } from "./footnotes";
|
|
|
10
10
|
|
|
11
11
|
export function finalizeParseResult(ctx: ParseContext, children: Element[]): ParseResult {
|
|
12
12
|
const cleanedChildren = postprocessChildren(children);
|
|
13
|
-
appendImplicitFootnoteBlock
|
|
13
|
+
if (ctx.appendImplicitFootnoteBlock) {
|
|
14
|
+
appendImplicitFootnoteBlock(cleanedChildren);
|
|
15
|
+
}
|
|
14
16
|
|
|
15
17
|
return {
|
|
16
18
|
ast: buildSyntaxTree(ctx, cleanedChildren),
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
* @module
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
export {
|
|
19
|
+
export {
|
|
20
|
+
resolveIncludes,
|
|
21
|
+
resolveIncludesAsync,
|
|
22
|
+
resolveIncludesAsyncWithTrace,
|
|
23
|
+
resolveIncludesWithTrace,
|
|
24
|
+
} from "./resolve";
|
|
20
25
|
export { extractIncludeReferences } from "./references";
|
|
21
26
|
export type {
|
|
22
27
|
IncludeFetcher,
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import type { PageRef } from "@wdprlib/ast";
|
|
9
9
|
import { createCachedAsyncIncludeFetcher, createCachedIncludeFetcher } from "./cache";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
expandIterative,
|
|
12
|
+
expandIterativeAsyncWithTrace,
|
|
13
|
+
expandIterativeWithTrace,
|
|
14
|
+
} from "./iterate";
|
|
11
15
|
import type {
|
|
12
16
|
AsyncIncludeFetcher,
|
|
13
17
|
IncludeFetcher,
|
|
@@ -84,13 +88,29 @@ export async function resolveIncludesAsync(
|
|
|
84
88
|
fetcher: AsyncIncludeFetcher,
|
|
85
89
|
options?: ResolveIncludesOptions,
|
|
86
90
|
): Promise<string> {
|
|
91
|
+
return (await resolveIncludesAsyncWithTrace(source, fetcher, options)).source;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Async include expansion with dependency and iteration trace data.
|
|
96
|
+
*/
|
|
97
|
+
export async function resolveIncludesAsyncWithTrace(
|
|
98
|
+
source: string,
|
|
99
|
+
fetcher: AsyncIncludeFetcher,
|
|
100
|
+
options?: ResolveIncludesOptions,
|
|
101
|
+
): Promise<ResolveIncludesTraceResult> {
|
|
87
102
|
if (options?.settings && !options.settings.enablePageSyntax) {
|
|
88
|
-
return
|
|
103
|
+
return {
|
|
104
|
+
source,
|
|
105
|
+
dependencies: [],
|
|
106
|
+
iterations: [],
|
|
107
|
+
reachedMaxIterations: false,
|
|
108
|
+
};
|
|
89
109
|
}
|
|
90
110
|
|
|
91
111
|
const maxIterations = options?.maxIterations ?? 10;
|
|
92
112
|
const cachedFetcher = createCachedAsyncIncludeFetcher(fetcher, normalizePageKey);
|
|
93
|
-
return
|
|
113
|
+
return expandIterativeAsyncWithTrace(source, cachedFetcher, maxIterations);
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
/**
|
|
@@ -102,6 +102,41 @@ export async function expandIterativeAsync(
|
|
|
102
102
|
return current;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/** Async counterpart of {@link expandIterativeWithTrace}. */
|
|
106
|
+
export async function expandIterativeAsyncWithTrace(
|
|
107
|
+
source: string,
|
|
108
|
+
fetcher: AsyncIncludeFetcher,
|
|
109
|
+
maxIterations: number,
|
|
110
|
+
): Promise<ResolveIncludesTraceResult> {
|
|
111
|
+
let current = source;
|
|
112
|
+
const replacementCache = new Map<string, Promise<string>>();
|
|
113
|
+
const dependencies: IncludeDependency[] = [];
|
|
114
|
+
const iterations: ResolveIncludesTraceResult["iterations"] = [];
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
117
|
+
const expanded = await expandOneIterationAsyncWithTrace(current, fetcher, replacementCache, i);
|
|
118
|
+
if (expanded === null) break;
|
|
119
|
+
|
|
120
|
+
dependencies.push(...expanded.dependencies);
|
|
121
|
+
iterations.push({
|
|
122
|
+
iteration: i,
|
|
123
|
+
directives: expanded.references,
|
|
124
|
+
changed: expanded.source !== current,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (expanded.source === current) break;
|
|
128
|
+
current = expanded.source;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
source: current,
|
|
133
|
+
dependencies,
|
|
134
|
+
iterations,
|
|
135
|
+
reachedMaxIterations:
|
|
136
|
+
iterations.length === maxIterations && scanIncludeDirectives(current).length > 0,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
105
140
|
function expandOneIteration(
|
|
106
141
|
source: string,
|
|
107
142
|
fetcher: IncludeFetcher,
|
|
@@ -179,6 +214,42 @@ async function expandOneIterationAsync(
|
|
|
179
214
|
return parts.join("");
|
|
180
215
|
}
|
|
181
216
|
|
|
217
|
+
async function expandOneIterationAsyncWithTrace(
|
|
218
|
+
source: string,
|
|
219
|
+
fetcher: AsyncIncludeFetcher,
|
|
220
|
+
replacementCache: Map<string, Promise<string>>,
|
|
221
|
+
iteration: number,
|
|
222
|
+
): Promise<{
|
|
223
|
+
source: string;
|
|
224
|
+
references: IncludeReference[];
|
|
225
|
+
dependencies: IncludeDependency[];
|
|
226
|
+
} | null> {
|
|
227
|
+
if (!MAYBE_INCLUDE_PATTERN.test(source)) return null;
|
|
228
|
+
|
|
229
|
+
const directives = scanIncludeDirectives(source);
|
|
230
|
+
if (directives.length === 0) return null;
|
|
231
|
+
|
|
232
|
+
const references = directives.map(createIncludeReference);
|
|
233
|
+
const replacements = await Promise.all(
|
|
234
|
+
directives.map(({ inner }) => replaceCachedAsync(inner, fetcher, replacementCache)),
|
|
235
|
+
);
|
|
236
|
+
const parts: string[] = [];
|
|
237
|
+
let lastPos = 0;
|
|
238
|
+
|
|
239
|
+
for (let i = 0; i < directives.length; i++) {
|
|
240
|
+
const { start, end } = directives[i]!;
|
|
241
|
+
parts.push(source.slice(lastPos, start), replacements[i]!);
|
|
242
|
+
lastPos = end;
|
|
243
|
+
}
|
|
244
|
+
parts.push(source.slice(lastPos));
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
source: parts.join(""),
|
|
248
|
+
references,
|
|
249
|
+
dependencies: references.map((reference) => ({ ...reference, iteration })),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
182
253
|
function replaceCached(inner: string, fetcher: IncludeFetcher, cache: Map<string, string>): string {
|
|
183
254
|
const cached = cache.get(inner);
|
|
184
255
|
if (cached !== undefined) return cached;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
// Module rule types and registry
|
|
23
|
-
export type { ModuleRule } from "./types";
|
|
23
|
+
export type { ModuleParseResult, ModuleRule } from "./types";
|
|
24
24
|
export { MODULE_RULES, getModuleRuleByName } from "./mapping";
|
|
25
25
|
export { moduleRule } from "./rule";
|
|
26
26
|
|
|
@@ -106,6 +106,7 @@ export {
|
|
|
106
106
|
extractIncludeReferences,
|
|
107
107
|
resolveIncludes,
|
|
108
108
|
resolveIncludesAsync,
|
|
109
|
+
resolveIncludesAsyncWithTrace,
|
|
109
110
|
resolveIncludesWithTrace,
|
|
110
111
|
} from "./include";
|
|
111
112
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Element } from "@wdprlib/ast";
|
|
2
2
|
import type { CompiledTemplate, ListPagesExternalData, VariableContext } from "../types";
|
|
3
|
-
import type
|
|
3
|
+
import { getModuleParseAst, type ParseFunction } from "../../types";
|
|
4
4
|
import type { ListPagesModuleData } from "../resolve";
|
|
5
5
|
|
|
6
6
|
export function renderListPagesItems(
|
|
@@ -23,7 +23,7 @@ export function renderListPagesItems(
|
|
|
23
23
|
site: data.site,
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
const itemAst = parse(compiledTemplate(ctx));
|
|
26
|
+
const itemAst = getModuleParseAst(parse(compiledTemplate(ctx)));
|
|
27
27
|
|
|
28
28
|
if (module.separate) {
|
|
29
29
|
items.push({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Element } from "@wdprlib/ast";
|
|
2
|
-
import type
|
|
2
|
+
import { getModuleParseAst, type ParseFunction } from "../../types";
|
|
3
3
|
import type { ListPagesModuleData } from "../resolve";
|
|
4
4
|
|
|
5
5
|
export function wrapListPagesResult(
|
|
@@ -14,14 +14,14 @@ export function wrapListPagesResult(
|
|
|
14
14
|
const result: Element[] = [];
|
|
15
15
|
|
|
16
16
|
if (module["prepend-line"] && !module.separate) {
|
|
17
|
-
const prependAst = parse(module["prepend-line"]);
|
|
17
|
+
const prependAst = getModuleParseAst(parse(module["prepend-line"]));
|
|
18
18
|
result.push(...prependAst.elements);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
result.push(...items);
|
|
22
22
|
|
|
23
23
|
if (module["append-line"] && !module.separate) {
|
|
24
|
-
const appendAst = parse(module["append-line"]);
|
|
24
|
+
const appendAst = getModuleParseAst(parse(module["append-line"]));
|
|
25
25
|
result.push(...appendAst.elements);
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
ListUsersCompiledTemplate,
|
|
16
16
|
ListUsersVariableContext,
|
|
17
17
|
} from "./types";
|
|
18
|
-
import type
|
|
18
|
+
import { getModuleParseAst, type ParseFunction } from "../types";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Narrowed type for the list-users variant of the Module discriminated union.
|
|
@@ -53,6 +53,6 @@ export function resolveListUsers(
|
|
|
53
53
|
): Element[] {
|
|
54
54
|
const ctx: ListUsersVariableContext = { user: data.user };
|
|
55
55
|
const substituted = compiledTemplate(ctx);
|
|
56
|
-
const itemAst = parse(substituted);
|
|
56
|
+
const itemAst = getModuleParseAst(parse(substituted));
|
|
57
57
|
return itemAst.elements;
|
|
58
58
|
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import {
|
|
2
|
+
evaluateExpression,
|
|
3
|
+
isTruthy,
|
|
4
|
+
type Diagnostic,
|
|
5
|
+
type Element,
|
|
6
|
+
type ParseResult,
|
|
7
|
+
type SyntaxTree,
|
|
8
|
+
type TocEntry,
|
|
9
|
+
} from "@wdprlib/ast";
|
|
10
|
+
import { extractHeadingText } from "../../heading/toc-text";
|
|
11
|
+
import { buildTableOfContents } from "../../../../toc";
|
|
12
|
+
import { resolveIfTags } from "../iftags/resolve";
|
|
13
|
+
import {
|
|
14
|
+
getGenericElementChildren,
|
|
15
|
+
listElement,
|
|
16
|
+
withGenericElementChildren,
|
|
17
|
+
} from "../walk/children";
|
|
18
|
+
import type { ModuleParseResult } from "../types";
|
|
19
|
+
|
|
20
|
+
interface RegisterOptions {
|
|
21
|
+
stripLegacyImplicitFootnoteBlock?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Tracks parse-local side channels while module fragments are merged. */
|
|
25
|
+
export class ModuleDocumentRegistry {
|
|
26
|
+
private readonly footnotesByElement = new WeakMap<Element, Element[]>();
|
|
27
|
+
readonly diagnostics: Diagnostic[] = [];
|
|
28
|
+
|
|
29
|
+
register(result: ModuleParseResult, options: RegisterOptions = {}): SyntaxTree {
|
|
30
|
+
const parseResult = isParseResult(result) ? result : null;
|
|
31
|
+
const ast: SyntaxTree = parseResult ? parseResult.ast : (result as SyntaxTree);
|
|
32
|
+
const elements = options.stripLegacyImplicitFootnoteBlock
|
|
33
|
+
? stripTrailingDefaultFootnoteBlock(ast.elements)
|
|
34
|
+
: ast.elements;
|
|
35
|
+
const registeredAst = elements === ast.elements ? ast : { ...ast, elements };
|
|
36
|
+
|
|
37
|
+
if (parseResult) {
|
|
38
|
+
this.diagnostics.push(...parseResult.diagnostics);
|
|
39
|
+
}
|
|
40
|
+
this.registerFootnotes(registeredAst);
|
|
41
|
+
return registeredAst;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
finalize(
|
|
45
|
+
ast: SyntaxTree,
|
|
46
|
+
elements: Element[],
|
|
47
|
+
pageTags: string[] | null,
|
|
48
|
+
ensureFootnoteBlock = true,
|
|
49
|
+
): SyntaxTree {
|
|
50
|
+
const normalizedElements = normalizeFootnoteBlocks(elements, ensureFootnoteBlock);
|
|
51
|
+
const footnotes: Element[][] = [];
|
|
52
|
+
const htmlBlocks: string[] = [];
|
|
53
|
+
const codeBlocks: NonNullable<SyntaxTree["code-blocks"]> = [];
|
|
54
|
+
const tocEntries: TocEntry[] = [];
|
|
55
|
+
|
|
56
|
+
const collectElement = (element: Element): void => {
|
|
57
|
+
if (element.element === "footnote") {
|
|
58
|
+
const content = this.footnotesByElement.get(element);
|
|
59
|
+
if (content) footnotes.push(content);
|
|
60
|
+
} else if (element.element === "html") {
|
|
61
|
+
htmlBlocks.push(element.data.contents);
|
|
62
|
+
} else if (element.element === "code") {
|
|
63
|
+
codeBlocks.push(element.data);
|
|
64
|
+
} else if (isTocHeading(element)) {
|
|
65
|
+
tocEntries.push({
|
|
66
|
+
level: element.data.type.header.level,
|
|
67
|
+
text: extractHeadingText(element.data.elements),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
walkRenderOrder(normalizedElements, pageTags, collectElement);
|
|
72
|
+
|
|
73
|
+
const result: SyntaxTree = { ...ast, elements: normalizedElements };
|
|
74
|
+
setOptionalArray(result, "footnotes", footnotes);
|
|
75
|
+
setOptionalArray(result, "html-blocks", htmlBlocks);
|
|
76
|
+
setOptionalArray(result, "code-blocks", codeBlocks);
|
|
77
|
+
setOptionalArray(result, "table-of-contents", buildTableOfContents(tocEntries));
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private registerFootnotes(ast: SyntaxTree): void {
|
|
82
|
+
const footnoteElements: Element[] = [];
|
|
83
|
+
walkSyntaxOrder(ast.elements, (element) => {
|
|
84
|
+
if (element.element === "footnote") footnoteElements.push(element);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < footnoteElements.length; i++) {
|
|
88
|
+
const content = ast.footnotes?.[i];
|
|
89
|
+
if (content) this.footnotesByElement.set(footnoteElements[i]!, content);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isParseResult(result: ModuleParseResult): result is ParseResult {
|
|
95
|
+
return "ast" in result && "diagnostics" in result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function setOptionalArray<K extends keyof SyntaxTree>(
|
|
99
|
+
tree: SyntaxTree,
|
|
100
|
+
key: K,
|
|
101
|
+
value: NonNullable<SyntaxTree[K]>,
|
|
102
|
+
): void {
|
|
103
|
+
if ((value as unknown[]).length > 0) {
|
|
104
|
+
tree[key] = value;
|
|
105
|
+
} else {
|
|
106
|
+
delete tree[key];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function stripTrailingDefaultFootnoteBlock(elements: Element[]): Element[] {
|
|
111
|
+
const last = elements.at(-1);
|
|
112
|
+
if (last?.element !== "footnote-block" || last.data.title !== null || last.data.hide === true) {
|
|
113
|
+
return elements;
|
|
114
|
+
}
|
|
115
|
+
return elements.slice(0, -1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeFootnoteBlocks(elements: Element[], ensureFootnoteBlock: boolean): Element[] {
|
|
119
|
+
const state = { found: false };
|
|
120
|
+
const normalized = mapSyntaxElements(elements, (element) => {
|
|
121
|
+
if (element.element !== "footnote-block") return element;
|
|
122
|
+
if (state.found) return null;
|
|
123
|
+
state.found = true;
|
|
124
|
+
return element;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (!state.found && ensureFootnoteBlock) {
|
|
128
|
+
normalized.push({
|
|
129
|
+
element: "footnote-block",
|
|
130
|
+
data: { title: null, hide: false },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return normalized;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function containsSyntaxFootnoteBlock(elements: Element[]): boolean {
|
|
137
|
+
let found = false;
|
|
138
|
+
walkSyntaxOrder(elements, (element) => {
|
|
139
|
+
if (element.element === "footnote-block") found = true;
|
|
140
|
+
});
|
|
141
|
+
return found;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function mapSyntaxElements(
|
|
145
|
+
elements: Element[],
|
|
146
|
+
transform: (element: Element) => Element | null,
|
|
147
|
+
): Element[] {
|
|
148
|
+
const result: Element[] = [];
|
|
149
|
+
for (const original of elements) {
|
|
150
|
+
const element = transform(original);
|
|
151
|
+
if (!element) continue;
|
|
152
|
+
result.push(mapSyntaxChildren(element, (children) => mapSyntaxElements(children, transform)));
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function mapSyntaxChildren(
|
|
158
|
+
element: Element,
|
|
159
|
+
transform: (elements: Element[]) => Element[],
|
|
160
|
+
): Element {
|
|
161
|
+
if (element.element === "if") {
|
|
162
|
+
return {
|
|
163
|
+
...element,
|
|
164
|
+
data: {
|
|
165
|
+
...element.data,
|
|
166
|
+
// oxlint-disable-next-line unicorn/no-thenable -- `then` is part of the public AST schema
|
|
167
|
+
then: transform(element.data.then),
|
|
168
|
+
else: transform(element.data.else),
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (element.element === "ifexpr") {
|
|
173
|
+
return {
|
|
174
|
+
...element,
|
|
175
|
+
data: {
|
|
176
|
+
...element.data,
|
|
177
|
+
// oxlint-disable-next-line unicorn/no-thenable -- `then` is part of the public AST schema
|
|
178
|
+
then: transform(element.data.then),
|
|
179
|
+
else: transform(element.data.else),
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (element.element === "bibliography-block") {
|
|
184
|
+
return {
|
|
185
|
+
...element,
|
|
186
|
+
data: {
|
|
187
|
+
...element.data,
|
|
188
|
+
entries: element.data.entries.map((entry) => ({
|
|
189
|
+
...entry,
|
|
190
|
+
key: transform(entry.key),
|
|
191
|
+
value: transform(entry.value),
|
|
192
|
+
})),
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (element.element === "list") {
|
|
197
|
+
return {
|
|
198
|
+
...element,
|
|
199
|
+
data: {
|
|
200
|
+
...element.data,
|
|
201
|
+
items: element.data.items.map((item) =>
|
|
202
|
+
item["item-type"] === "elements"
|
|
203
|
+
? { ...item, elements: transform(item.elements) }
|
|
204
|
+
: mapSubList(item, transform),
|
|
205
|
+
),
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (element.element === "table") {
|
|
210
|
+
return {
|
|
211
|
+
...element,
|
|
212
|
+
data: {
|
|
213
|
+
...element.data,
|
|
214
|
+
rows: element.data.rows.map((row) => ({
|
|
215
|
+
...row,
|
|
216
|
+
cells: row.cells.map((cell) => ({ ...cell, elements: transform(cell.elements) })),
|
|
217
|
+
})),
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (element.element === "definition-list") {
|
|
222
|
+
return {
|
|
223
|
+
...element,
|
|
224
|
+
data: element.data.map((entry) => ({
|
|
225
|
+
...entry,
|
|
226
|
+
key: transform(entry.key),
|
|
227
|
+
value: transform(entry.value),
|
|
228
|
+
})),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (element.element === "tab-view") {
|
|
232
|
+
return {
|
|
233
|
+
...element,
|
|
234
|
+
data: element.data.map((tab) => ({ ...tab, elements: transform(tab.elements) })),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const children = getGenericElementChildren(element);
|
|
239
|
+
return children === null ? element : withGenericElementChildren(element, transform(children));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function mapSubList(
|
|
243
|
+
item: Extract<ElementOfListItem, { "item-type": "sub-list" }>,
|
|
244
|
+
transform: (elements: Element[]) => Element[],
|
|
245
|
+
): ElementOfListItem {
|
|
246
|
+
const transformed = transform([listElement(item.data)])[0];
|
|
247
|
+
return transformed?.element === "list" ? { ...item, data: transformed.data } : item;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
type ElementOfListItem = Extract<Element, { element: "list" }>["data"]["items"][number];
|
|
251
|
+
|
|
252
|
+
function walkSyntaxOrder(elements: Element[], callback: (element: Element) => void): void {
|
|
253
|
+
for (const element of elements) {
|
|
254
|
+
callback(element);
|
|
255
|
+
walkAllSyntaxChildren(element, (children) => walkSyntaxOrder(children, callback));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function walkRenderOrder(
|
|
260
|
+
elements: Element[],
|
|
261
|
+
pageTags: string[] | null,
|
|
262
|
+
callback: (element: Element) => void,
|
|
263
|
+
): void {
|
|
264
|
+
for (const element of elements) {
|
|
265
|
+
callback(element);
|
|
266
|
+
if (element.element === "if") {
|
|
267
|
+
walkRenderOrder(
|
|
268
|
+
isTruthy(element.data.condition) ? element.data.then : element.data.else,
|
|
269
|
+
pageTags,
|
|
270
|
+
callback,
|
|
271
|
+
);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (element.element === "ifexpr") {
|
|
275
|
+
const evaluated = evaluateExpression(element.data.expression);
|
|
276
|
+
if (evaluated.success) {
|
|
277
|
+
walkRenderOrder(
|
|
278
|
+
evaluated.value !== 0 ? element.data.then : element.data.else,
|
|
279
|
+
pageTags,
|
|
280
|
+
callback,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (element.element === "if-tags") {
|
|
286
|
+
const resolved = resolveIfTags(element.data, pageTags);
|
|
287
|
+
if (!resolved.evaluated || resolved.matched) {
|
|
288
|
+
walkRenderOrder(element.data.elements, pageTags, callback);
|
|
289
|
+
}
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (element.element === "bibliography-block") {
|
|
293
|
+
if (!element.data.hide) {
|
|
294
|
+
for (const entry of element.data.entries) {
|
|
295
|
+
walkRenderOrder(entry.value, pageTags, callback);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
walkBasicChildren(element, (children) => walkRenderOrder(children, pageTags, callback));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function walkAllSyntaxChildren(element: Element, callback: (elements: Element[]) => void): void {
|
|
305
|
+
if (element.element === "if" || element.element === "ifexpr") {
|
|
306
|
+
callback(element.data.then);
|
|
307
|
+
callback(element.data.else);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (element.element === "bibliography-block") {
|
|
311
|
+
for (const entry of element.data.entries) {
|
|
312
|
+
callback(entry.key);
|
|
313
|
+
callback(entry.value);
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
walkBasicChildren(element, callback);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function walkBasicChildren(element: Element, callback: (elements: Element[]) => void): void {
|
|
321
|
+
if (element.element === "list") {
|
|
322
|
+
for (const item of element.data.items) {
|
|
323
|
+
callback(item["item-type"] === "elements" ? item.elements : [listElement(item.data)]);
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (element.element === "table") {
|
|
328
|
+
for (const row of element.data.rows) {
|
|
329
|
+
for (const cell of row.cells) callback(cell.elements);
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (element.element === "definition-list") {
|
|
334
|
+
for (const entry of element.data) {
|
|
335
|
+
callback(entry.key);
|
|
336
|
+
callback(entry.value);
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (element.element === "tab-view") {
|
|
341
|
+
for (const tab of element.data) callback(tab.elements);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const children = getGenericElementChildren(element);
|
|
345
|
+
if (children !== null) callback(children);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function isTocHeading(element: Element): element is Extract<Element, { element: "container" }> & {
|
|
349
|
+
data: { type: { header: { level: number; "has-toc": true } } };
|
|
350
|
+
} {
|
|
351
|
+
return (
|
|
352
|
+
element.element === "container" &&
|
|
353
|
+
typeof element.data.type === "object" &&
|
|
354
|
+
"header" in element.data.type &&
|
|
355
|
+
element.data.type.header["has-toc"]
|
|
356
|
+
);
|
|
357
|
+
}
|