@wdprlib/parser 3.1.2 → 3.2.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 +295 -118
- package/dist/index.js +272 -95
- package/package.json +5 -3
- package/src/index.ts +163 -0
- package/src/lexer/index.ts +20 -0
- package/src/lexer/lexer.ts +687 -0
- package/src/lexer/tokens.ts +141 -0
- package/src/parser/constants.ts +173 -0
- package/src/parser/depth.ts +251 -0
- package/src/parser/index.ts +18 -0
- package/src/parser/parse.ts +315 -0
- package/src/parser/postprocess/divAdjacentParagraph.ts +76 -0
- package/src/parser/postprocess/index.ts +15 -0
- package/src/parser/postprocess/spanStrip.ts +697 -0
- package/src/parser/preprocess/expr.ts +265 -0
- package/src/parser/preprocess/index.ts +38 -0
- package/src/parser/preprocess/typography.ts +67 -0
- package/src/parser/preprocess/utils.ts +250 -0
- package/src/parser/preprocess/whitespace.ts +111 -0
- package/src/parser/rules/block/align.ts +282 -0
- package/src/parser/rules/block/bibliography.ts +359 -0
- package/src/parser/rules/block/block-list.ts +689 -0
- package/src/parser/rules/block/blockquote.ts +238 -0
- package/src/parser/rules/block/center.ts +87 -0
- package/src/parser/rules/block/clear-float.ts +75 -0
- package/src/parser/rules/block/code.ts +187 -0
- package/src/parser/rules/block/collapsible.ts +337 -0
- package/src/parser/rules/block/comment.ts +73 -0
- package/src/parser/rules/block/content-separator.ts +79 -0
- package/src/parser/rules/block/definition-list.ts +270 -0
- package/src/parser/rules/block/div.ts +400 -0
- package/src/parser/rules/block/embed-block.ts +153 -0
- package/src/parser/rules/block/footnoteblock.ts +200 -0
- package/src/parser/rules/block/heading.ts +142 -0
- package/src/parser/rules/block/horizontal-rule.ts +61 -0
- package/src/parser/rules/block/html.ts +222 -0
- package/src/parser/rules/block/iframe.ts +239 -0
- package/src/parser/rules/block/iftags.ts +150 -0
- package/src/parser/rules/block/include.ts +179 -0
- package/src/parser/rules/block/index.ts +127 -0
- package/src/parser/rules/block/list.ts +244 -0
- package/src/parser/rules/block/math.ts +183 -0
- package/src/parser/rules/block/module/backlinks/index.ts +31 -0
- package/src/parser/rules/block/module/backlinks/types.ts +21 -0
- package/src/parser/rules/block/module/categories/index.ts +34 -0
- package/src/parser/rules/block/module/categories/types.ts +21 -0
- package/src/parser/rules/block/module/css/index.ts +37 -0
- package/src/parser/rules/block/module/iftags/condition.ts +109 -0
- package/src/parser/rules/block/module/iftags/index.ts +26 -0
- package/src/parser/rules/block/module/iftags/preprocess.ts +140 -0
- package/src/parser/rules/block/module/iftags/resolve.ts +73 -0
- package/src/parser/rules/block/module/iftags/types.ts +63 -0
- package/src/parser/rules/block/module/include/index.ts +20 -0
- package/src/parser/rules/block/module/include/resolve.ts +556 -0
- package/src/parser/rules/block/module/index.ts +122 -0
- package/src/parser/rules/block/module/join/index.ts +34 -0
- package/src/parser/rules/block/module/join/types.ts +23 -0
- package/src/parser/rules/block/module/listpages/compiler.ts +453 -0
- package/src/parser/rules/block/module/listpages/extract.ts +410 -0
- package/src/parser/rules/block/module/listpages/index.ts +83 -0
- package/src/parser/rules/block/module/listpages/normalize.ts +390 -0
- package/src/parser/rules/block/module/listpages/parser.ts +106 -0
- package/src/parser/rules/block/module/listpages/resolve.ts +130 -0
- package/src/parser/rules/block/module/listpages/types.ts +513 -0
- package/src/parser/rules/block/module/listpages/url-resolver.ts +186 -0
- package/src/parser/rules/block/module/listusers/compiler.ts +77 -0
- package/src/parser/rules/block/module/listusers/extract.ts +45 -0
- package/src/parser/rules/block/module/listusers/index.ts +36 -0
- package/src/parser/rules/block/module/listusers/parser.ts +54 -0
- package/src/parser/rules/block/module/listusers/resolve.ts +58 -0
- package/src/parser/rules/block/module/listusers/types.ts +93 -0
- package/src/parser/rules/block/module/mapping.ts +61 -0
- package/src/parser/rules/block/module/page-tree/index.ts +38 -0
- package/src/parser/rules/block/module/page-tree/types.ts +29 -0
- package/src/parser/rules/block/module/rate/index.ts +28 -0
- package/src/parser/rules/block/module/rate/types.ts +19 -0
- package/src/parser/rules/block/module/resolve.ts +411 -0
- package/src/parser/rules/block/module/types-common.ts +59 -0
- package/src/parser/rules/block/module/types.ts +61 -0
- package/src/parser/rules/block/module/utils.ts +43 -0
- package/src/parser/rules/block/module/walk.ts +380 -0
- package/src/parser/rules/block/module.ts +164 -0
- package/src/parser/rules/block/orphan-li.ts +177 -0
- package/src/parser/rules/block/paragraph.ts +157 -0
- package/src/parser/rules/block/table-block.ts +726 -0
- package/src/parser/rules/block/table.ts +441 -0
- package/src/parser/rules/block/tabview.ts +331 -0
- package/src/parser/rules/block/toc.ts +129 -0
- package/src/parser/rules/block/utils.ts +615 -0
- package/src/parser/rules/index.ts +49 -0
- package/src/parser/rules/inline/anchor-name.ts +154 -0
- package/src/parser/rules/inline/anchor.ts +327 -0
- package/src/parser/rules/inline/bibcite.ts +153 -0
- package/src/parser/rules/inline/bold.ts +86 -0
- package/src/parser/rules/inline/color.ts +140 -0
- package/src/parser/rules/inline/comment.ts +90 -0
- package/src/parser/rules/inline/equation-ref.ts +115 -0
- package/src/parser/rules/inline/expr.ts +526 -0
- package/src/parser/rules/inline/footnote.ts +223 -0
- package/src/parser/rules/inline/guillemet.ts +64 -0
- package/src/parser/rules/inline/html.ts +132 -0
- package/src/parser/rules/inline/image.ts +328 -0
- package/src/parser/rules/inline/index.ts +150 -0
- package/src/parser/rules/inline/italic.ts +74 -0
- package/src/parser/rules/inline/line-break.ts +326 -0
- package/src/parser/rules/inline/link-anchor.ts +147 -0
- package/src/parser/rules/inline/link-single.ts +164 -0
- package/src/parser/rules/inline/link-star.ts +134 -0
- package/src/parser/rules/inline/link-triple.ts +267 -0
- package/src/parser/rules/inline/math-inline.ts +126 -0
- package/src/parser/rules/inline/monospace.ts +78 -0
- package/src/parser/rules/inline/raw.ts +262 -0
- package/src/parser/rules/inline/size.ts +244 -0
- package/src/parser/rules/inline/span.ts +424 -0
- package/src/parser/rules/inline/strikethrough.ts +115 -0
- package/src/parser/rules/inline/subscript.ts +84 -0
- package/src/parser/rules/inline/superscript.ts +84 -0
- package/src/parser/rules/inline/text.ts +84 -0
- package/src/parser/rules/inline/underline.ts +127 -0
- package/src/parser/rules/inline/user.ts +147 -0
- package/src/parser/rules/inline/utils.ts +344 -0
- package/src/parser/rules/types.ts +252 -0
- package/src/parser/rules/utils.ts +155 -0
- package/src/parser/toc.ts +130 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified resolver that walks a parsed AST and expands dynamic modules.
|
|
3
|
+
*
|
|
4
|
+
* Handles three module families in a single traversal:
|
|
5
|
+
*
|
|
6
|
+
* - **ListPages** — fetches page data via {@link DataProvider.fetchListPages},
|
|
7
|
+
* resolves `@URL` parameters from the page path (HPC support), and
|
|
8
|
+
* expands `%%variable%%` templates.
|
|
9
|
+
* - **ListUsers** — fetches user data via {@link DataProvider.fetchListUsers}
|
|
10
|
+
* and expands `%%variable%%` templates.
|
|
11
|
+
* - **IfTags** — evaluates tag conditions against the current page's tags
|
|
12
|
+
* (from {@link DataProvider.getPageTags}) and keeps or discards content.
|
|
13
|
+
*
|
|
14
|
+
* The main entry point is {@link resolveModules}.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { Element, SyntaxTree } from "@wdprlib/ast";
|
|
20
|
+
import { STYLE_SLOT_PREFIX } from "@wdprlib/ast";
|
|
21
|
+
import type { DataProvider } from "./types-common";
|
|
22
|
+
import { walkElements, mapElementChildren, mapElementChildrenWithState } from "./walk";
|
|
23
|
+
import type {
|
|
24
|
+
ListPagesDataRequirement,
|
|
25
|
+
ListPagesExternalData,
|
|
26
|
+
CompiledTemplate,
|
|
27
|
+
} from "./listpages/types";
|
|
28
|
+
import type {
|
|
29
|
+
ListUsersDataRequirement,
|
|
30
|
+
ListUsersExternalData,
|
|
31
|
+
ListUsersCompiledTemplate,
|
|
32
|
+
} from "./listusers/types";
|
|
33
|
+
import { isListPagesModule, resolveListPages, type ParseFunction } from "./listpages/resolve";
|
|
34
|
+
import { isListUsersModule, resolveListUsers } from "./listusers/resolve";
|
|
35
|
+
import { isIfTagsElement, resolveIfTags, type IfTagsData } from "./iftags/resolve";
|
|
36
|
+
import { parseUrlParams, resolveAndNormalizeQuery } from "./listpages/url-resolver";
|
|
37
|
+
|
|
38
|
+
// Re-export from listpages/resolve for external use
|
|
39
|
+
export type { ParseFunction } from "./listpages/resolve";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Configuration for {@link resolveModules}.
|
|
43
|
+
*
|
|
44
|
+
* Callers must supply pre-extracted requirements and pre-compiled
|
|
45
|
+
* templates (obtained from `extractDataRequirements()` and
|
|
46
|
+
* `compileTemplate()` / `compileListUsersTemplate()`).
|
|
47
|
+
*
|
|
48
|
+
* @group Module Resolution
|
|
49
|
+
*/
|
|
50
|
+
export interface ResolveOptions {
|
|
51
|
+
/** Parser function used to re-parse expanded template markup into AST nodes */
|
|
52
|
+
parse: ParseFunction;
|
|
53
|
+
|
|
54
|
+
/** Pre-compiled ListPages body templates, keyed by requirement ID */
|
|
55
|
+
compiledListPagesTemplates: Map<number, CompiledTemplate>;
|
|
56
|
+
|
|
57
|
+
/** Pre-compiled ListUsers body templates, keyed by requirement ID */
|
|
58
|
+
compiledListUsersTemplates?: Map<number, ListUsersCompiledTemplate>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Data requirements grouped by module type.
|
|
62
|
+
* Obtained from `extractDataRequirements()`.
|
|
63
|
+
*/
|
|
64
|
+
requirements: {
|
|
65
|
+
listPages?: ListPagesDataRequirement[];
|
|
66
|
+
listUsers?: ListUsersDataRequirement[];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* URL path for `@URL` parameter resolution (HPC / pagination support).
|
|
71
|
+
*
|
|
72
|
+
* Wikidot encodes pagination state in the URL path as key/value pairs
|
|
73
|
+
* after the page name, e.g. `"/scp-001/offset/10/page2_limit/5"`.
|
|
74
|
+
* When provided, `@URL` references in ListPages queries are replaced
|
|
75
|
+
* with the corresponding values from this path.
|
|
76
|
+
*/
|
|
77
|
+
urlPath?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Context for ListPages resolution (internal)
|
|
82
|
+
*/
|
|
83
|
+
interface ListPagesContext {
|
|
84
|
+
dataMap: Map<number, ListPagesExternalData>;
|
|
85
|
+
compiledTemplates: Map<number, CompiledTemplate>;
|
|
86
|
+
parse: ParseFunction;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Context for ListUsers resolution (internal)
|
|
91
|
+
*/
|
|
92
|
+
interface ListUsersContext {
|
|
93
|
+
dataMap: Map<number, ListUsersExternalData>;
|
|
94
|
+
compiledTemplates: Map<number, ListUsersCompiledTemplate>;
|
|
95
|
+
parse: ParseFunction;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve all modules in the AST
|
|
100
|
+
*
|
|
101
|
+
* Fetches data for each module using the provided callback,
|
|
102
|
+
* then expands the modules with the fetched data.
|
|
103
|
+
*
|
|
104
|
+
* Handles:
|
|
105
|
+
* - ListPages: fetches page data and expands templates
|
|
106
|
+
* - IfTags: evaluates tag conditions and includes/excludes content
|
|
107
|
+
*
|
|
108
|
+
* @param ast - Parsed AST
|
|
109
|
+
* @param dataProvider - Callback provider to fetch data for each module
|
|
110
|
+
* @param options - Resolution options including requirements
|
|
111
|
+
*/
|
|
112
|
+
export async function resolveModules(
|
|
113
|
+
ast: SyntaxTree,
|
|
114
|
+
dataProvider: DataProvider,
|
|
115
|
+
options: ResolveOptions,
|
|
116
|
+
): Promise<SyntaxTree> {
|
|
117
|
+
// Build ListPages context if requirements provided
|
|
118
|
+
let listPagesCtx: ListPagesContext | null = null;
|
|
119
|
+
const listPagesReqs = options.requirements.listPages ?? [];
|
|
120
|
+
|
|
121
|
+
if (listPagesReqs.length > 0 && dataProvider.fetchListPages) {
|
|
122
|
+
const dataMap = new Map<number, ListPagesExternalData>();
|
|
123
|
+
|
|
124
|
+
// Parse URL parameters once for all modules
|
|
125
|
+
const urlParams = parseUrlParams(options.urlPath ?? "");
|
|
126
|
+
|
|
127
|
+
for (const req of listPagesReqs) {
|
|
128
|
+
// Resolve @URL parameters and normalize query
|
|
129
|
+
const normalizedQuery = resolveAndNormalizeQuery(req, urlParams);
|
|
130
|
+
const data = await dataProvider.fetchListPages(normalizedQuery, req);
|
|
131
|
+
if (data) {
|
|
132
|
+
dataMap.set(req.id, data);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (dataMap.size > 0) {
|
|
137
|
+
listPagesCtx = {
|
|
138
|
+
dataMap,
|
|
139
|
+
compiledTemplates: options.compiledListPagesTemplates,
|
|
140
|
+
parse: options.parse,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Build ListUsers context if requirements provided
|
|
146
|
+
let listUsersCtx: ListUsersContext | null = null;
|
|
147
|
+
const listUsersReqs = options.requirements.listUsers ?? [];
|
|
148
|
+
|
|
149
|
+
if (listUsersReqs.length > 0 && dataProvider.fetchListUsers) {
|
|
150
|
+
const dataMap = new Map<number, ListUsersExternalData>();
|
|
151
|
+
|
|
152
|
+
for (const req of listUsersReqs) {
|
|
153
|
+
const data = await dataProvider.fetchListUsers(req);
|
|
154
|
+
if (data) {
|
|
155
|
+
dataMap.set(req.id, data);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (dataMap.size > 0) {
|
|
160
|
+
listUsersCtx = {
|
|
161
|
+
dataMap,
|
|
162
|
+
compiledTemplates: options.compiledListUsersTemplates ?? new Map(),
|
|
163
|
+
parse: options.parse,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Get page tags if callback provided
|
|
169
|
+
const pageTags = dataProvider.getPageTags?.() ?? null;
|
|
170
|
+
|
|
171
|
+
// Resolve AST
|
|
172
|
+
const resolvedElements = walkAndResolve(ast.elements, {
|
|
173
|
+
listPages: listPagesCtx,
|
|
174
|
+
listUsers: listUsersCtx,
|
|
175
|
+
fetchListPagesProvided: dataProvider.fetchListPages !== undefined,
|
|
176
|
+
fetchListUsersProvided: dataProvider.fetchListUsers !== undefined,
|
|
177
|
+
pageTags,
|
|
178
|
+
listPagesIdCounter: 0,
|
|
179
|
+
listUsersIdCounter: 0,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Collect style elements from resolved AST
|
|
183
|
+
const { elements: finalElements, styles } = collectStyles(resolvedElements.elements);
|
|
184
|
+
|
|
185
|
+
const result: SyntaxTree = {
|
|
186
|
+
...ast,
|
|
187
|
+
elements: finalElements,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
if (styles.length > 0) {
|
|
191
|
+
result.styles = styles;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Resolution context passed through AST traversal
|
|
199
|
+
*/
|
|
200
|
+
interface WalkContext {
|
|
201
|
+
listPages: ListPagesContext | null;
|
|
202
|
+
listUsers: ListUsersContext | null;
|
|
203
|
+
/** Whether fetchListPages callback was provided (even if no data returned) */
|
|
204
|
+
fetchListPagesProvided: boolean;
|
|
205
|
+
/** Whether fetchListUsers callback was provided (even if no data returned) */
|
|
206
|
+
fetchListUsersProvided: boolean;
|
|
207
|
+
pageTags: string[] | null;
|
|
208
|
+
listPagesIdCounter: number;
|
|
209
|
+
listUsersIdCounter: number;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
interface WalkResult {
|
|
213
|
+
elements: Element[];
|
|
214
|
+
nextListPagesId: number;
|
|
215
|
+
nextListUsersId: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Walk AST and resolve modules/iftags
|
|
220
|
+
*/
|
|
221
|
+
function walkAndResolve(elements: Element[], ctx: WalkContext): WalkResult {
|
|
222
|
+
const result: Element[] = [];
|
|
223
|
+
let listPagesId = ctx.listPagesIdCounter;
|
|
224
|
+
let listUsersId = ctx.listUsersIdCounter;
|
|
225
|
+
|
|
226
|
+
for (const element of elements) {
|
|
227
|
+
// ListPages module
|
|
228
|
+
if (element.element === "module" && isListPagesModule(element.data)) {
|
|
229
|
+
if (ctx.listPages) {
|
|
230
|
+
const moduleData = ctx.listPages.dataMap.get(listPagesId);
|
|
231
|
+
const template = ctx.listPages.compiledTemplates.get(listPagesId);
|
|
232
|
+
|
|
233
|
+
if (moduleData && template) {
|
|
234
|
+
const resolved = resolveListPages(
|
|
235
|
+
element.data,
|
|
236
|
+
moduleData,
|
|
237
|
+
template,
|
|
238
|
+
ctx.listPages.parse,
|
|
239
|
+
);
|
|
240
|
+
result.push(...resolved);
|
|
241
|
+
}
|
|
242
|
+
} else if (!ctx.fetchListPagesProvided) {
|
|
243
|
+
result.push(element);
|
|
244
|
+
}
|
|
245
|
+
listPagesId++;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ListUsers module
|
|
250
|
+
if (element.element === "module" && isListUsersModule(element.data)) {
|
|
251
|
+
if (ctx.listUsers) {
|
|
252
|
+
const moduleData = ctx.listUsers.dataMap.get(listUsersId);
|
|
253
|
+
const template = ctx.listUsers.compiledTemplates.get(listUsersId);
|
|
254
|
+
|
|
255
|
+
if (moduleData && template) {
|
|
256
|
+
const resolved = resolveListUsers(
|
|
257
|
+
element.data,
|
|
258
|
+
moduleData,
|
|
259
|
+
template,
|
|
260
|
+
ctx.listUsers.parse,
|
|
261
|
+
);
|
|
262
|
+
result.push(...resolved);
|
|
263
|
+
}
|
|
264
|
+
} else if (!ctx.fetchListUsersProvided) {
|
|
265
|
+
result.push(element);
|
|
266
|
+
}
|
|
267
|
+
listUsersId++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// IfTags
|
|
272
|
+
if (isIfTagsElement(element)) {
|
|
273
|
+
const ifTagsData = element.data as IfTagsData;
|
|
274
|
+
const resolveResult = resolveIfTags(ifTagsData, ctx.pageTags);
|
|
275
|
+
|
|
276
|
+
if (resolveResult.evaluated) {
|
|
277
|
+
if (resolveResult.matched) {
|
|
278
|
+
const childResult = walkAndResolve(ifTagsData.elements, {
|
|
279
|
+
...ctx,
|
|
280
|
+
listPagesIdCounter: listPagesId,
|
|
281
|
+
listUsersIdCounter: listUsersId,
|
|
282
|
+
});
|
|
283
|
+
result.push(...childResult.elements);
|
|
284
|
+
listPagesId = childResult.nextListPagesId;
|
|
285
|
+
listUsersId = childResult.nextListUsersId;
|
|
286
|
+
} else {
|
|
287
|
+
const counts = countModulesInElements(ifTagsData.elements);
|
|
288
|
+
listPagesId += counts.listPages;
|
|
289
|
+
listUsersId += counts.listUsers;
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
const childResult = walkAndResolve(ifTagsData.elements, {
|
|
293
|
+
...ctx,
|
|
294
|
+
listPagesIdCounter: listPagesId,
|
|
295
|
+
listUsersIdCounter: listUsersId,
|
|
296
|
+
});
|
|
297
|
+
result.push({
|
|
298
|
+
element: "if-tags",
|
|
299
|
+
data: {
|
|
300
|
+
...ifTagsData,
|
|
301
|
+
elements: childResult.elements,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
listPagesId = childResult.nextListPagesId;
|
|
305
|
+
listUsersId = childResult.nextListUsersId;
|
|
306
|
+
}
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Recurse into child elements (list, table, definition-list, tab-view, generic)
|
|
311
|
+
const mapped = mapElementChildrenWithState(
|
|
312
|
+
element,
|
|
313
|
+
{ listPagesId, listUsersId },
|
|
314
|
+
(children, state) => {
|
|
315
|
+
const childResult = walkAndResolve(children, {
|
|
316
|
+
...ctx,
|
|
317
|
+
listPagesIdCounter: state.listPagesId,
|
|
318
|
+
listUsersIdCounter: state.listUsersId,
|
|
319
|
+
});
|
|
320
|
+
return {
|
|
321
|
+
elements: childResult.elements,
|
|
322
|
+
state: {
|
|
323
|
+
listPagesId: childResult.nextListPagesId,
|
|
324
|
+
listUsersId: childResult.nextListUsersId,
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
result.push(mapped.element);
|
|
330
|
+
listPagesId = mapped.state.listPagesId;
|
|
331
|
+
listUsersId = mapped.state.listUsersId;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { elements: result, nextListPagesId: listPagesId, nextListUsersId: listUsersId };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Count resolved modules in elements without resolving them
|
|
339
|
+
*
|
|
340
|
+
* Used to advance ID counters without expensive resolution when
|
|
341
|
+
* IfTags condition doesn't match (elements are skipped but IDs must sync)
|
|
342
|
+
*/
|
|
343
|
+
function countModulesInElements(elements: Element[]): { listPages: number; listUsers: number } {
|
|
344
|
+
let listPages = 0;
|
|
345
|
+
let listUsers = 0;
|
|
346
|
+
walkElements(elements, (element) => {
|
|
347
|
+
if (element.element === "module") {
|
|
348
|
+
if (isListPagesModule(element.data)) {
|
|
349
|
+
listPages++;
|
|
350
|
+
} else if (isListUsersModule(element.data)) {
|
|
351
|
+
listUsers++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
return { listPages, listUsers };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Collect and remove style elements from the AST.
|
|
360
|
+
*
|
|
361
|
+
* Walks the element tree recursively, extracting style elements
|
|
362
|
+
* from any depth and returning them separately. Unresolved `if-tags`
|
|
363
|
+
* elements are skipped — their internal styles remain in the AST and
|
|
364
|
+
* are rendered inline at render time when the condition is evaluated.
|
|
365
|
+
*
|
|
366
|
+
* The order of collected styles reflects their appearance order in the AST.
|
|
367
|
+
*/
|
|
368
|
+
|
|
369
|
+
function collectStyles(elements: Element[]): { elements: Element[]; styles: string[] } {
|
|
370
|
+
const styles: string[] = [];
|
|
371
|
+
const ctx = { nextSlotId: 0 };
|
|
372
|
+
const filtered = collectStylesFromElements(elements, styles, ctx);
|
|
373
|
+
return { elements: filtered, styles };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function collectStylesFromElements(
|
|
377
|
+
elements: Element[],
|
|
378
|
+
styles: string[],
|
|
379
|
+
ctx: { nextSlotId: number },
|
|
380
|
+
): Element[] {
|
|
381
|
+
const result: Element[] = [];
|
|
382
|
+
|
|
383
|
+
for (const element of elements) {
|
|
384
|
+
if (element.element === "style") {
|
|
385
|
+
styles.push(element.data as string);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Unresolved iftags: insert a style-slot placeholder to preserve
|
|
390
|
+
// source-order of styles relative to other collected styles.
|
|
391
|
+
// The slot ID is attached to the element data so the renderer can
|
|
392
|
+
// collect styles into the correct slot at render time.
|
|
393
|
+
if (element.element === "if-tags") {
|
|
394
|
+
const slotId = ctx.nextSlotId++;
|
|
395
|
+
styles.push(`${STYLE_SLOT_PREFIX}${slotId}`);
|
|
396
|
+
result.push({
|
|
397
|
+
element: "if-tags",
|
|
398
|
+
data: { ...(element.data as IfTagsData), _styleSlot: slotId },
|
|
399
|
+
} as unknown as Element);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Recurse into children using mapElementChildren
|
|
404
|
+
const mapped = mapElementChildren(element, (children) =>
|
|
405
|
+
collectStylesFromElements(children, styles, ctx),
|
|
406
|
+
);
|
|
407
|
+
result.push(mapped);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared callback interface for module resolution.
|
|
3
|
+
*
|
|
4
|
+
* `DataProvider` is the single object that callers pass to
|
|
5
|
+
* `resolveModules()` to supply external data. Each property is an
|
|
6
|
+
* optional async callback; if omitted the corresponding module type is
|
|
7
|
+
* left unresolved in the AST.
|
|
8
|
+
*
|
|
9
|
+
* Include resolution uses a separate API (`resolveIncludes()`)
|
|
10
|
+
* because it operates on raw wikitext before parsing, not on AST nodes.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ListPagesDataFetcher } from "./listpages/types";
|
|
16
|
+
import type { ListUsersDataFetcher } from "./listusers/types";
|
|
17
|
+
import type { IfTagsResolver } from "./iftags/types";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Callback bag for supplying external data during module resolution.
|
|
21
|
+
*
|
|
22
|
+
* Pass an instance to `resolveModules()`. Every callback is optional:
|
|
23
|
+
* when a callback is missing the corresponding module node is kept as-is
|
|
24
|
+
* in the output AST — useful when you only need to resolve a subset of
|
|
25
|
+
* modules (e.g. only `[[iftags]]` on the client side).
|
|
26
|
+
*
|
|
27
|
+
* @group Module Resolution
|
|
28
|
+
*/
|
|
29
|
+
export interface DataProvider {
|
|
30
|
+
/**
|
|
31
|
+
* Fetch page data for `[[module ListPages]]` expansion.
|
|
32
|
+
*
|
|
33
|
+
* Called once per ListPages instance in the AST with the normalised
|
|
34
|
+
* query parameters extracted from the module's wikitext attributes.
|
|
35
|
+
*
|
|
36
|
+
* @security The query fields originate from **untrusted user input**.
|
|
37
|
+
* When building database queries from the returned requirement:
|
|
38
|
+
* - **Never** interpolate `req.query` / `req.rawAttributes` into SQL
|
|
39
|
+
* - **Always** use parameterised queries or prepared statements
|
|
40
|
+
* - For `order` (ORDER BY), validate against a whitelist of column names
|
|
41
|
+
*/
|
|
42
|
+
fetchListPages?: ListPagesDataFetcher;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fetch user data for `[[module ListUsers]]` expansion.
|
|
46
|
+
*
|
|
47
|
+
* Called once per ListUsers instance with the parsed query parameters.
|
|
48
|
+
*/
|
|
49
|
+
fetchListUsers?: ListUsersDataFetcher;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Return the current page's tags for `[[iftags]]` evaluation.
|
|
53
|
+
*
|
|
54
|
+
* If provided, `[[iftags]]` blocks are evaluated and either kept or
|
|
55
|
+
* discarded based on whether the page's tags satisfy the condition.
|
|
56
|
+
* If omitted, `[[iftags]]` blocks pass through unresolved.
|
|
57
|
+
*/
|
|
58
|
+
getPageTags?: IfTagsResolver;
|
|
59
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Core type definitions for the Wikidot module system.
|
|
4
|
+
*
|
|
5
|
+
* Wikidot modules are block-level constructs invoked with `[[module Name ...]]`
|
|
6
|
+
* syntax. Each module type (ListPages, CSS, Rate, etc.) is implemented as a
|
|
7
|
+
* `ModuleRule` that defines how to parse the module's attributes and body into
|
|
8
|
+
* an AST node.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Element, Module } from "@wdprlib/ast";
|
|
14
|
+
import type { ParseContext } from "../../types";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parser function type for re-parsing substituted template output as wikitext.
|
|
18
|
+
*
|
|
19
|
+
* Used by ListPages and ListUsers modules during the resolution phase. After
|
|
20
|
+
* template variables are substituted with actual data, the resulting string
|
|
21
|
+
* needs to be parsed as wikitext to produce AST elements.
|
|
22
|
+
*
|
|
23
|
+
* @param input - Wikitext string to parse
|
|
24
|
+
* @returns Object containing the parsed elements
|
|
25
|
+
*/
|
|
26
|
+
export type ParseFunction = (input: string) => { elements: Element[] };
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Definition of a module rule that handles a specific Wikidot module type.
|
|
30
|
+
*
|
|
31
|
+
* Each module rule declares which module names it handles (e.g., "listpages",
|
|
32
|
+
* "css"), whether the module accepts a body (content between `[[module Name]]`
|
|
33
|
+
* and `[[/module]]`), and a parse function that produces the AST representation.
|
|
34
|
+
*
|
|
35
|
+
* The parse function's return type determines how the result is handled:
|
|
36
|
+
* - `Module`: Wrapped as `{ element: "module", data: Module }` in the AST
|
|
37
|
+
* - `Element`: Used directly as an AST element (e.g., CSS module returns a `style` element)
|
|
38
|
+
*/
|
|
39
|
+
export interface ModuleRule {
|
|
40
|
+
/** Internal identifier for the rule (e.g., "module-listpages") */
|
|
41
|
+
name: string;
|
|
42
|
+
/** Module names this rule handles, matched case-insensitively (e.g., ["listpages"]) */
|
|
43
|
+
acceptsNames: string[];
|
|
44
|
+
/** Whether this module accepts a body (content between `[[module]]` and `[[/module]]`) */
|
|
45
|
+
hasBody: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Parse the module's attributes and optional body into an AST node.
|
|
48
|
+
*
|
|
49
|
+
* @param ctx - Current parse context (token stream, settings, etc.)
|
|
50
|
+
* @param pos - Current token position
|
|
51
|
+
* @param args - Key-value attributes from the module's opening tag
|
|
52
|
+
* @param body - Raw text content between opening and closing tags (only if `hasBody` is true)
|
|
53
|
+
* @returns A Module data object or a direct Element
|
|
54
|
+
*/
|
|
55
|
+
parse: (
|
|
56
|
+
ctx: ParseContext,
|
|
57
|
+
pos: number,
|
|
58
|
+
args: Record<string, string>,
|
|
59
|
+
body?: string,
|
|
60
|
+
) => Module | Element;
|
|
61
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Utility functions for parsing Wikidot module attribute values.
|
|
4
|
+
*
|
|
5
|
+
* Wikidot modules accept attributes as string key-value pairs. These utilities
|
|
6
|
+
* convert common attribute types (booleans, integers) from their string
|
|
7
|
+
* representation to proper TypeScript types, following Wikidot's conventions
|
|
8
|
+
* for truthy/falsy values.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse a boolean value from a Wikidot attribute string.
|
|
15
|
+
*
|
|
16
|
+
* Wikidot accepts both "yes"/"no" and "true"/"false" as boolean attribute values.
|
|
17
|
+
* If the value does not match any recognized boolean string, the default is returned.
|
|
18
|
+
*
|
|
19
|
+
* @param value - The attribute string value, or undefined if the attribute was not specified
|
|
20
|
+
* @param defaultValue - Value to return when the attribute is undefined or unrecognized
|
|
21
|
+
* @returns The parsed boolean value
|
|
22
|
+
*/
|
|
23
|
+
export function parseBool(value: string | undefined, defaultValue: boolean): boolean {
|
|
24
|
+
if (value === undefined) return defaultValue;
|
|
25
|
+
if (value === "yes" || value === "true") return true;
|
|
26
|
+
if (value === "no" || value === "false") return false;
|
|
27
|
+
return defaultValue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse a 32-bit integer value from a Wikidot attribute string.
|
|
32
|
+
*
|
|
33
|
+
* Uses base-10 parsing. Returns undefined for non-numeric strings or
|
|
34
|
+
* when the attribute is not specified.
|
|
35
|
+
*
|
|
36
|
+
* @param value - The attribute string value, or undefined if not specified
|
|
37
|
+
* @returns The parsed integer, or undefined if parsing fails
|
|
38
|
+
*/
|
|
39
|
+
export function parseInt32(value: string | undefined): number | undefined {
|
|
40
|
+
if (value === undefined) return undefined;
|
|
41
|
+
const num = Number.parseInt(value, 10);
|
|
42
|
+
return Number.isNaN(num) ? undefined : num;
|
|
43
|
+
}
|