@wdprlib/parser 4.3.0 → 5.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/README.md +26 -38
- package/dist/index.cjs +1035 -249
- package/dist/index.d.cts +203 -158
- package/dist/index.d.ts +203 -158
- package/dist/index.js +930 -139
- package/package.json +2 -2
- package/src/index.ts +13 -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/preprocess/expr/index.ts +26 -0
- package/src/parser/preprocess/utils/raw-regions.ts +16 -7
- 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 +4 -1
- package/src/parser/rules/block/module/listpages/index.ts +2 -0
- 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/listpages/selectors.ts +64 -0
- package/src/parser/rules/block/module/listpages/types/external-data.ts +22 -0
- 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 +172 -0
- package/src/pipeline/types.ts +63 -0
|
@@ -1,41 +1,161 @@
|
|
|
1
1
|
import type { Element } from "@wdprlib/ast";
|
|
2
|
-
import { STYLE_SLOT_PREFIX } from "@wdprlib/ast";
|
|
3
|
-
import { mapElementChildren } from "../walk";
|
|
2
|
+
import { STYLE_ANCHOR_PREFIX, STYLE_SLOT_PREFIX } from "@wdprlib/ast";
|
|
3
|
+
import { mapElementChildren, walkElements } from "../walk";
|
|
4
4
|
import type { IfTagsData } from "../iftags/resolve";
|
|
5
5
|
|
|
6
6
|
type IfTagsDataWithStyleSlot = IfTagsData & {
|
|
7
|
-
_styleSlot
|
|
7
|
+
_styleSlot?: number;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
export type ResolvedStyleSlots = ReadonlyMap<number, readonly string[]>;
|
|
11
|
+
|
|
12
|
+
export interface StyleCollectionResult {
|
|
13
|
+
elements: Element[];
|
|
14
|
+
styles: string[];
|
|
15
|
+
anchoredStyles: string[];
|
|
16
|
+
anchors: Element[];
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
/**
|
|
11
20
|
* Collect and remove style elements from the AST.
|
|
12
21
|
*
|
|
13
22
|
* Unresolved `if-tags` elements receive style-slot placeholders so render-time
|
|
14
23
|
* iftags evaluation can preserve style order relative to already collected CSS.
|
|
15
24
|
*/
|
|
16
|
-
export function collectStyles(
|
|
25
|
+
export function collectStyles(
|
|
26
|
+
elements: Element[],
|
|
27
|
+
ignoredAnchors: WeakSet<Element> = new WeakSet(),
|
|
28
|
+
): StyleCollectionResult {
|
|
17
29
|
const styles: string[] = [];
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
30
|
+
const anchoredStyles: string[] = [];
|
|
31
|
+
const anchors: Element[] = [];
|
|
32
|
+
const usedSlots = collectExistingStyleSlots(elements);
|
|
33
|
+
const ctx = { nextSlotId: 0, usedSlots };
|
|
34
|
+
const filtered = collectStylesFromElements(
|
|
35
|
+
elements,
|
|
36
|
+
styles,
|
|
37
|
+
anchoredStyles,
|
|
38
|
+
anchors,
|
|
39
|
+
ignoredAnchors,
|
|
40
|
+
ctx,
|
|
41
|
+
);
|
|
42
|
+
return { elements: filtered, styles, anchoredStyles, anchors };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Preserve styles collected by earlier resolution passes while keeping
|
|
47
|
+
* unresolved IfTags style-slot markers idempotent across repeated passes.
|
|
48
|
+
*/
|
|
49
|
+
export function mergeCollectedStyles(
|
|
50
|
+
existing: readonly string[] | undefined,
|
|
51
|
+
collected: readonly string[],
|
|
52
|
+
resolvedSlots: ResolvedStyleSlots = new Map(),
|
|
53
|
+
anchoredStyles: readonly string[] = [],
|
|
54
|
+
): string[] {
|
|
55
|
+
const unanchoredPrevious = [...(existing ?? [])];
|
|
56
|
+
for (const style of anchoredStyles) removeFirst(unanchoredPrevious, style);
|
|
57
|
+
for (const style of collected) {
|
|
58
|
+
if (isStyleSlotMarker(style)) removeFirst(unanchoredPrevious, style);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const merged = unanchoredPrevious.filter((style) => !isStyleSlotMarker(style));
|
|
62
|
+
for (const style of collected) {
|
|
63
|
+
if (isStyleSlotMarker(style)) appendStyleSlot(merged, style, resolvedSlots);
|
|
64
|
+
else merged.push(style);
|
|
65
|
+
}
|
|
66
|
+
return merged;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isStyleSlotMarker(style: string): boolean {
|
|
70
|
+
return style.startsWith(STYLE_SLOT_PREFIX);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createStyleSlotMarker(slotId: number): string {
|
|
74
|
+
return `${STYLE_SLOT_PREFIX}${slotId}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getStyleSlotId(data: IfTagsData): number | undefined {
|
|
78
|
+
const slotId = (data as IfTagsDataWithStyleSlot)._styleSlot;
|
|
79
|
+
return Number.isSafeInteger(slotId) && slotId! >= 0 ? slotId : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function removeFirst(values: string[], value: string): void {
|
|
83
|
+
const index = values.indexOf(value);
|
|
84
|
+
if (index >= 0) values.splice(index, 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function appendStyleSlot(
|
|
88
|
+
target: string[],
|
|
89
|
+
marker: string,
|
|
90
|
+
resolvedSlots: ResolvedStyleSlots,
|
|
91
|
+
): void {
|
|
92
|
+
const slotId = Number(marker.slice(STYLE_SLOT_PREFIX.length));
|
|
93
|
+
const replacement = resolvedSlots.get(slotId);
|
|
94
|
+
if (replacement) target.push(...replacement);
|
|
95
|
+
else target.push(marker);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function collectExistingStyleSlots(elements: Element[]): Set<number> {
|
|
99
|
+
const slots = new Set<number>();
|
|
100
|
+
walkElements(elements, (element) => {
|
|
101
|
+
if (element.element !== "if-tags") return;
|
|
102
|
+
const slotId = getStyleSlotId(element.data);
|
|
103
|
+
if (slotId !== undefined) slots.add(slotId);
|
|
104
|
+
});
|
|
105
|
+
return slots;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function allocateStyleSlot(ctx: StyleCollectionContext): number {
|
|
109
|
+
while (ctx.usedSlots.has(ctx.nextSlotId)) ctx.nextSlotId++;
|
|
110
|
+
const slotId = ctx.nextSlotId++;
|
|
111
|
+
ctx.usedSlots.add(slotId);
|
|
112
|
+
return slotId;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface StyleCollectionContext {
|
|
116
|
+
nextSlotId: number;
|
|
117
|
+
usedSlots: Set<number>;
|
|
21
118
|
}
|
|
22
119
|
|
|
23
120
|
function collectStylesFromElements(
|
|
24
121
|
elements: Element[],
|
|
25
122
|
styles: string[],
|
|
26
|
-
|
|
123
|
+
anchoredStyles: string[],
|
|
124
|
+
anchors: Element[],
|
|
125
|
+
ignoredAnchors: WeakSet<Element>,
|
|
126
|
+
ctx: StyleCollectionContext,
|
|
27
127
|
): Element[] {
|
|
28
128
|
const result: Element[] = [];
|
|
29
129
|
|
|
30
130
|
for (const element of elements) {
|
|
31
131
|
if (element.element === "style") {
|
|
32
|
-
|
|
132
|
+
const css = element.data as string;
|
|
133
|
+
if (css.startsWith(STYLE_SLOT_PREFIX)) {
|
|
134
|
+
styles.push(css);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (css.startsWith(STYLE_ANCHOR_PREFIX)) {
|
|
138
|
+
anchors.push(element);
|
|
139
|
+
if (ignoredAnchors.has(element)) {
|
|
140
|
+
result.push(element);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const anchoredCss = css.slice(STYLE_ANCHOR_PREFIX.length);
|
|
144
|
+
styles.push(anchoredCss);
|
|
145
|
+
anchoredStyles.push(anchoredCss);
|
|
146
|
+
result.push(element);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
styles.push(css);
|
|
150
|
+
const anchor: Element = { element: "style", data: `${STYLE_ANCHOR_PREFIX}${css}` };
|
|
151
|
+
anchors.push(anchor);
|
|
152
|
+
result.push(anchor);
|
|
33
153
|
continue;
|
|
34
154
|
}
|
|
35
155
|
|
|
36
156
|
if (element.element === "if-tags") {
|
|
37
|
-
const slotId = ctx
|
|
38
|
-
styles.push(
|
|
157
|
+
const slotId = getStyleSlotId(element.data) ?? allocateStyleSlot(ctx);
|
|
158
|
+
styles.push(createStyleSlotMarker(slotId));
|
|
39
159
|
const data: IfTagsDataWithStyleSlot = { ...(element.data as IfTagsData), _styleSlot: slotId };
|
|
40
160
|
result.push({
|
|
41
161
|
element: "if-tags",
|
|
@@ -45,7 +165,9 @@ function collectStylesFromElements(
|
|
|
45
165
|
}
|
|
46
166
|
|
|
47
167
|
result.push(
|
|
48
|
-
mapElementChildren(element, (children) =>
|
|
168
|
+
mapElementChildren(element, (children) =>
|
|
169
|
+
collectStylesFromElements(children, styles, anchoredStyles, anchors, ignoredAnchors, ctx),
|
|
170
|
+
),
|
|
49
171
|
);
|
|
50
172
|
}
|
|
51
173
|
|
|
@@ -3,6 +3,7 @@ import { mapElementChildrenWithState } from "../walk";
|
|
|
3
3
|
import { isIfTagsElement, resolveIfTags, type IfTagsData } from "../iftags/resolve";
|
|
4
4
|
import type { ListPagesContext, ListUsersContext, TagCloudContext } from "./contexts";
|
|
5
5
|
import { countDynamicModules, resolveDynamicModuleElement } from "./dynamic-modules";
|
|
6
|
+
import { collectStyles, createStyleSlotMarker, getStyleSlotId } from "./styles";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Resolution context passed through AST traversal.
|
|
@@ -21,6 +22,8 @@ export interface WalkContext {
|
|
|
21
22
|
listPagesIdCounter: number;
|
|
22
23
|
listUsersIdCounter: number;
|
|
23
24
|
tagCloudIdCounter: number;
|
|
25
|
+
resolvedStyleSlots: Map<number, string[]>;
|
|
26
|
+
routedStyleAnchors: WeakSet<Element>;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export interface WalkResult {
|
|
@@ -56,6 +59,7 @@ export function walkAndResolve(elements: Element[], ctx: WalkContext): WalkResul
|
|
|
56
59
|
if (isIfTagsElement(element)) {
|
|
57
60
|
const ifTagsData = element.data as IfTagsData;
|
|
58
61
|
const resolveResult = resolveIfTags(ifTagsData, ctx.pageTags);
|
|
62
|
+
const styleSlotId = getStyleSlotId(ifTagsData);
|
|
59
63
|
|
|
60
64
|
if (resolveResult.evaluated) {
|
|
61
65
|
if (resolveResult.matched) {
|
|
@@ -65,11 +69,25 @@ export function walkAndResolve(elements: Element[], ctx: WalkContext): WalkResul
|
|
|
65
69
|
listUsersIdCounter: listUsersId,
|
|
66
70
|
tagCloudIdCounter: tagCloudId,
|
|
67
71
|
});
|
|
68
|
-
|
|
72
|
+
if (styleSlotId === undefined) {
|
|
73
|
+
result.push(...childResult.elements);
|
|
74
|
+
} else {
|
|
75
|
+
const collected = collectStyles(childResult.elements);
|
|
76
|
+
ctx.resolvedStyleSlots.set(styleSlotId, collected.styles);
|
|
77
|
+
for (const anchor of collected.anchors) ctx.routedStyleAnchors.add(anchor);
|
|
78
|
+
result.push(
|
|
79
|
+
{ element: "style", data: createStyleSlotMarker(styleSlotId) },
|
|
80
|
+
...collected.elements,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
69
83
|
listPagesId = childResult.nextListPagesId;
|
|
70
84
|
listUsersId = childResult.nextListUsersId;
|
|
71
85
|
tagCloudId = childResult.nextTagCloudId;
|
|
72
86
|
} else {
|
|
87
|
+
if (styleSlotId !== undefined) {
|
|
88
|
+
ctx.resolvedStyleSlots.set(styleSlotId, []);
|
|
89
|
+
result.push({ element: "style", data: createStyleSlotMarker(styleSlotId) });
|
|
90
|
+
}
|
|
73
91
|
const counts = countDynamicModules(ifTagsData.elements);
|
|
74
92
|
listPagesId += counts.listPagesId;
|
|
75
93
|
listUsersId += counts.listUsersId;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* @module
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import type { SyntaxTree } from "@wdprlib/ast";
|
|
19
|
+
import type { Diagnostic, Element, SyntaxTree } from "@wdprlib/ast";
|
|
20
20
|
import type { DataProvider } from "./types-common";
|
|
21
21
|
import { resolveIncludes } from "./include";
|
|
22
22
|
import type { ListPagesDataRequirement, CompiledTemplate } from "./listpages/types";
|
|
@@ -29,7 +29,8 @@ import {
|
|
|
29
29
|
buildTagCloudContext,
|
|
30
30
|
} from "./resolution/contexts";
|
|
31
31
|
import { walkAndResolve } from "./resolution/walk-resolve";
|
|
32
|
-
import { collectStyles } from "./resolution/styles";
|
|
32
|
+
import { collectStyles, mergeCollectedStyles } from "./resolution/styles";
|
|
33
|
+
import { containsSyntaxFootnoteBlock, ModuleDocumentRegistry } from "./resolution/document";
|
|
33
34
|
|
|
34
35
|
const MODULE_SECONDARY_INCLUDE_MAX_ITERATIONS = 5;
|
|
35
36
|
|
|
@@ -110,6 +111,9 @@ export interface ResolveOptions {
|
|
|
110
111
|
* state outside wdpr.
|
|
111
112
|
*/
|
|
112
113
|
transformModuleSource?: ModuleSourceTransform;
|
|
114
|
+
|
|
115
|
+
/** Receives diagnostics emitted by ListPages/ListUsers secondary parses. */
|
|
116
|
+
onDiagnostics?: (diagnostics: Diagnostic[]) => void;
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
/**
|
|
@@ -131,7 +135,9 @@ export async function resolveModules(
|
|
|
131
135
|
dataProvider: DataProvider,
|
|
132
136
|
options: ResolveOptions,
|
|
133
137
|
): Promise<SyntaxTree> {
|
|
134
|
-
const
|
|
138
|
+
const registry = new ModuleDocumentRegistry();
|
|
139
|
+
registry.register(ast);
|
|
140
|
+
const parse = createModuleParseFunction(options, dataProvider, registry);
|
|
135
141
|
const listPagesCtx = await buildListPagesContext(
|
|
136
142
|
dataProvider,
|
|
137
143
|
options.requirements.listPages ?? [],
|
|
@@ -147,6 +153,8 @@ export async function resolveModules(
|
|
|
147
153
|
);
|
|
148
154
|
const tagCloudCtx = await buildTagCloudContext(dataProvider, options.requirements.tagCloud ?? []);
|
|
149
155
|
const pageTags = dataProvider.getPageTags?.() ?? null;
|
|
156
|
+
const resolvedStyleSlots = new Map<number, string[]>();
|
|
157
|
+
const routedStyleAnchors = new WeakSet<Element>();
|
|
150
158
|
|
|
151
159
|
// Resolve AST
|
|
152
160
|
const resolvedElements = walkAndResolve(ast.elements, {
|
|
@@ -160,33 +168,44 @@ export async function resolveModules(
|
|
|
160
168
|
listPagesIdCounter: 0,
|
|
161
169
|
listUsersIdCounter: 0,
|
|
162
170
|
tagCloudIdCounter: 0,
|
|
171
|
+
resolvedStyleSlots,
|
|
172
|
+
routedStyleAnchors,
|
|
163
173
|
});
|
|
164
174
|
|
|
165
175
|
// Collect style elements from resolved AST
|
|
166
|
-
const {
|
|
176
|
+
const {
|
|
177
|
+
elements: finalElements,
|
|
178
|
+
styles,
|
|
179
|
+
anchoredStyles,
|
|
180
|
+
} = collectStyles(resolvedElements.elements, routedStyleAnchors);
|
|
167
181
|
|
|
168
182
|
const result: SyntaxTree = {
|
|
169
183
|
...ast,
|
|
170
184
|
elements: finalElements,
|
|
171
185
|
};
|
|
172
186
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
187
|
+
const mergedStyles = mergeCollectedStyles(ast.styles, styles, resolvedStyleSlots, anchoredStyles);
|
|
188
|
+
if (mergedStyles.length > 0) result.styles = mergedStyles;
|
|
176
189
|
|
|
177
|
-
|
|
190
|
+
options.onDiagnostics?.(registry.diagnostics);
|
|
191
|
+
return registry.finalize(
|
|
192
|
+
result,
|
|
193
|
+
finalElements,
|
|
194
|
+
pageTags,
|
|
195
|
+
containsSyntaxFootnoteBlock(ast.elements),
|
|
196
|
+
);
|
|
178
197
|
}
|
|
179
198
|
|
|
180
199
|
function createModuleParseFunction(
|
|
181
200
|
options: ResolveOptions,
|
|
182
201
|
dataProvider: DataProvider,
|
|
202
|
+
registry: ModuleDocumentRegistry,
|
|
183
203
|
): ParseFunction {
|
|
184
204
|
const transform = createModuleSourceTransform(options, dataProvider);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return (source: string) => options.parse(transform(source));
|
|
205
|
+
return (source: string) =>
|
|
206
|
+
registry.register(options.parse(transform ? transform(source) : source), {
|
|
207
|
+
stripLegacyImplicitFootnoteBlock: true,
|
|
208
|
+
});
|
|
190
209
|
}
|
|
191
210
|
|
|
192
211
|
function createModuleSourceTransform(
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type { Element, Module } from "@wdprlib/ast";
|
|
13
|
+
import type { Element, Module, ParseResult, SyntaxTree } from "@wdprlib/ast";
|
|
14
14
|
import type { ParseContext } from "../../types";
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -23,7 +23,12 @@ import type { ParseContext } from "../../types";
|
|
|
23
23
|
* @param input - Wikitext string to parse
|
|
24
24
|
* @returns Object containing the parsed elements
|
|
25
25
|
*/
|
|
26
|
-
export type
|
|
26
|
+
export type ModuleParseResult = SyntaxTree | ParseResult;
|
|
27
|
+
export type ParseFunction = (input: string) => ModuleParseResult;
|
|
28
|
+
|
|
29
|
+
export function getModuleParseAst(result: ModuleParseResult): SyntaxTree {
|
|
30
|
+
return "ast" in result ? result.ast : result;
|
|
31
|
+
}
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
34
|
* Definition of a module rule that handles a specific Wikidot module type.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_SETTINGS,
|
|
3
|
+
type Diagnostic,
|
|
4
|
+
type PageRef,
|
|
5
|
+
type WikitextPageContext,
|
|
6
|
+
} from "@wdprlib/ast";
|
|
7
|
+
import { parse } from "../parser";
|
|
8
|
+
import { extractDataRequirements } from "../parser/rules/block/module/listpages/extract";
|
|
9
|
+
import {
|
|
10
|
+
resolveIncludesAsyncWithTrace,
|
|
11
|
+
type AsyncIncludeFetcher,
|
|
12
|
+
type IncludeDependency,
|
|
13
|
+
} from "../parser/rules/block/module/include";
|
|
14
|
+
import type { DataProvider } from "../parser/rules/block/module/types-common";
|
|
15
|
+
import { resolveModulesWithAsyncParse } from "../parser/rules/block/module/resolution/resolve-async";
|
|
16
|
+
import type {
|
|
17
|
+
ProcessedWikitextDocument,
|
|
18
|
+
ProcessWikitextCallbackContext,
|
|
19
|
+
ProcessWikitextOptions,
|
|
20
|
+
} from "./types";
|
|
21
|
+
|
|
22
|
+
const DEFAULT_MODULE_MAX_PASSES = 5;
|
|
23
|
+
const PIPELINE_DIAGNOSTIC_POSITION = {
|
|
24
|
+
start: { line: 1, column: 1, offset: 0 },
|
|
25
|
+
end: { line: 1, column: 1, offset: 0 },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export async function processWikitext<TPage extends WikitextPageContext>(
|
|
29
|
+
source: string,
|
|
30
|
+
options: ProcessWikitextOptions<TPage>,
|
|
31
|
+
): Promise<ProcessedWikitextDocument<TPage>> {
|
|
32
|
+
const settings = options.settings ?? DEFAULT_SETTINGS;
|
|
33
|
+
const callbackContext: ProcessWikitextCallbackContext<TPage> = {
|
|
34
|
+
page: options.page,
|
|
35
|
+
settings,
|
|
36
|
+
};
|
|
37
|
+
const dependencies: IncludeDependency[] = [];
|
|
38
|
+
const diagnostics: Diagnostic[] = [];
|
|
39
|
+
const fetchInclude = createRequestIncludeFetcher(
|
|
40
|
+
options.dataProvider?.fetchInclude
|
|
41
|
+
? (pageRef) => options.dataProvider!.fetchInclude!(pageRef, callbackContext)
|
|
42
|
+
: undefined,
|
|
43
|
+
);
|
|
44
|
+
const resolveSource = async (input: string): Promise<string> => {
|
|
45
|
+
if (!fetchInclude) return input;
|
|
46
|
+
const resolution = await resolveIncludesAsyncWithTrace(input, fetchInclude, {
|
|
47
|
+
maxIterations: options.includeMaxIterations,
|
|
48
|
+
settings,
|
|
49
|
+
});
|
|
50
|
+
dependencies.push(...resolution.dependencies);
|
|
51
|
+
if (resolution.reachedMaxIterations) {
|
|
52
|
+
diagnostics.push(
|
|
53
|
+
createLimitDiagnostic(
|
|
54
|
+
"include-resolution-limit",
|
|
55
|
+
`Include expansion stopped after ${options.includeMaxIterations ?? 10} iterations.`,
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return resolution.source;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const expandedSource = await resolveSource(source);
|
|
63
|
+
const initial = parse(expandedSource, {
|
|
64
|
+
settings,
|
|
65
|
+
pageTags: options.page.tags,
|
|
66
|
+
appendImplicitFootnoteBlock: false,
|
|
67
|
+
});
|
|
68
|
+
diagnostics.push(...initial.diagnostics);
|
|
69
|
+
const dataProvider = createModuleDataProvider(options, callbackContext);
|
|
70
|
+
const parseFragment = async (fragmentSource: string) =>
|
|
71
|
+
parse(await resolveSource(fragmentSource), {
|
|
72
|
+
settings,
|
|
73
|
+
pageTags: options.page.tags,
|
|
74
|
+
appendImplicitFootnoteBlock: false,
|
|
75
|
+
});
|
|
76
|
+
let ast = initial.ast;
|
|
77
|
+
|
|
78
|
+
for (let pass = 0; pass < DEFAULT_MODULE_MAX_PASSES; pass++) {
|
|
79
|
+
const extraction = extractDataRequirements(ast);
|
|
80
|
+
if (pass > 0 && !hasResolvableRequirements(extraction.requirements, options.dataProvider)) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resolved = await resolveModulesWithAsyncParse(ast, dataProvider, {
|
|
85
|
+
parse: parseFragment,
|
|
86
|
+
compiledListPagesTemplates: extraction.compiledListPagesTemplates,
|
|
87
|
+
compiledListUsersTemplates: extraction.compiledListUsersTemplates,
|
|
88
|
+
requirements: extraction.requirements,
|
|
89
|
+
urlPath: options.page.urlPath,
|
|
90
|
+
pageTags: options.page.tags,
|
|
91
|
+
});
|
|
92
|
+
ast = resolved.ast;
|
|
93
|
+
diagnostics.push(...resolved.diagnostics);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (hasResolvableRequirements(extractDataRequirements(ast).requirements, options.dataProvider)) {
|
|
97
|
+
diagnostics.push(
|
|
98
|
+
createLimitDiagnostic(
|
|
99
|
+
"module-resolution-limit",
|
|
100
|
+
`Module resolution stopped after ${DEFAULT_MODULE_MAX_PASSES} passes.`,
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
ast,
|
|
107
|
+
page: options.page,
|
|
108
|
+
settings,
|
|
109
|
+
diagnostics,
|
|
110
|
+
dependencies,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hasResolvableRequirements(
|
|
115
|
+
requirements: ReturnType<typeof extractDataRequirements>["requirements"],
|
|
116
|
+
provider:
|
|
117
|
+
| {
|
|
118
|
+
fetchListPages?: unknown;
|
|
119
|
+
fetchListUsers?: unknown;
|
|
120
|
+
fetchTagCloud?: unknown;
|
|
121
|
+
}
|
|
122
|
+
| undefined,
|
|
123
|
+
): boolean {
|
|
124
|
+
return Boolean(
|
|
125
|
+
(provider?.fetchListPages && requirements.listPages.length > 0) ||
|
|
126
|
+
(provider?.fetchListUsers && requirements.listUsers.length > 0) ||
|
|
127
|
+
(provider?.fetchTagCloud && requirements.tagCloud.length > 0),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createLimitDiagnostic(code: string, message: string): Diagnostic {
|
|
132
|
+
return {
|
|
133
|
+
severity: "warning",
|
|
134
|
+
code,
|
|
135
|
+
message,
|
|
136
|
+
position: PIPELINE_DIAGNOSTIC_POSITION,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function createModuleDataProvider<TPage extends WikitextPageContext>(
|
|
141
|
+
options: ProcessWikitextOptions<TPage>,
|
|
142
|
+
context: ProcessWikitextCallbackContext<TPage>,
|
|
143
|
+
): DataProvider {
|
|
144
|
+
const provider = options.dataProvider;
|
|
145
|
+
return {
|
|
146
|
+
fetchListPages: provider?.fetchListPages
|
|
147
|
+
? (query, requirement) => provider.fetchListPages!(query, requirement, context)
|
|
148
|
+
: undefined,
|
|
149
|
+
fetchListUsers: provider?.fetchListUsers
|
|
150
|
+
? (requirement) => provider.fetchListUsers!(requirement, context)
|
|
151
|
+
: undefined,
|
|
152
|
+
fetchTagCloud: provider?.fetchTagCloud
|
|
153
|
+
? (requirement) => provider.fetchTagCloud!(requirement, context)
|
|
154
|
+
: undefined,
|
|
155
|
+
getPageTags: () => options.page.tags,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function createRequestIncludeFetcher(
|
|
160
|
+
fetcher: AsyncIncludeFetcher | undefined,
|
|
161
|
+
): AsyncIncludeFetcher | undefined {
|
|
162
|
+
if (!fetcher) return undefined;
|
|
163
|
+
const cache = new Map<string, Promise<string | null>>();
|
|
164
|
+
return (pageRef: PageRef): Promise<string | null> => {
|
|
165
|
+
const key = `${pageRef.site ?? ""}:${pageRef.page.toLowerCase()}`;
|
|
166
|
+
const cached = cache.get(key);
|
|
167
|
+
if (cached) return cached;
|
|
168
|
+
const result = fetcher(pageRef).catch(() => null);
|
|
169
|
+
cache.set(key, result);
|
|
170
|
+
return result;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Diagnostic,
|
|
3
|
+
PageRef,
|
|
4
|
+
SyntaxTree,
|
|
5
|
+
WikitextPageContext,
|
|
6
|
+
WikitextSettings,
|
|
7
|
+
} from "@wdprlib/ast";
|
|
8
|
+
import type { IncludeDependency } from "../parser/rules/block/module/include";
|
|
9
|
+
import type {
|
|
10
|
+
ListPagesDataRequirement,
|
|
11
|
+
ListPagesExternalData,
|
|
12
|
+
NormalizedListPagesQuery,
|
|
13
|
+
} from "../parser/rules/block/module/listpages/types";
|
|
14
|
+
import type {
|
|
15
|
+
ListUsersDataRequirement,
|
|
16
|
+
ListUsersExternalData,
|
|
17
|
+
} from "../parser/rules/block/module/listusers/types";
|
|
18
|
+
import type {
|
|
19
|
+
TagCloudDataRequirement,
|
|
20
|
+
TagCloudExternalData,
|
|
21
|
+
} from "../parser/rules/block/module/tagcloud/types";
|
|
22
|
+
|
|
23
|
+
export interface ProcessWikitextCallbackContext<TPage extends WikitextPageContext> {
|
|
24
|
+
page: TPage;
|
|
25
|
+
settings: WikitextSettings;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ProcessWikitextDataProvider<TPage extends WikitextPageContext> {
|
|
29
|
+
fetchInclude?: (
|
|
30
|
+
pageRef: PageRef,
|
|
31
|
+
context: ProcessWikitextCallbackContext<TPage>,
|
|
32
|
+
) => Promise<string | null>;
|
|
33
|
+
fetchListPages?: (
|
|
34
|
+
query: NormalizedListPagesQuery,
|
|
35
|
+
requirement: ListPagesDataRequirement,
|
|
36
|
+
context: ProcessWikitextCallbackContext<TPage>,
|
|
37
|
+
) => Promise<ListPagesExternalData | null | undefined>;
|
|
38
|
+
fetchListUsers?: (
|
|
39
|
+
requirement: ListUsersDataRequirement,
|
|
40
|
+
context: ProcessWikitextCallbackContext<TPage>,
|
|
41
|
+
) => Promise<ListUsersExternalData | null | undefined>;
|
|
42
|
+
fetchTagCloud?: (
|
|
43
|
+
requirement: TagCloudDataRequirement,
|
|
44
|
+
context: ProcessWikitextCallbackContext<TPage>,
|
|
45
|
+
) => Promise<TagCloudExternalData | null | undefined>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ProcessWikitextOptions<TPage extends WikitextPageContext> {
|
|
49
|
+
page: TPage;
|
|
50
|
+
settings?: WikitextSettings;
|
|
51
|
+
dataProvider?: ProcessWikitextDataProvider<TPage>;
|
|
52
|
+
includeMaxIterations?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ProcessedWikitextDocument<
|
|
56
|
+
TPage extends WikitextPageContext = WikitextPageContext,
|
|
57
|
+
> {
|
|
58
|
+
ast: SyntaxTree;
|
|
59
|
+
page: TPage;
|
|
60
|
+
settings: WikitextSettings;
|
|
61
|
+
diagnostics: Diagnostic[];
|
|
62
|
+
dependencies: IncludeDependency[];
|
|
63
|
+
}
|