@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.
Files changed (124) hide show
  1. package/dist/index.cjs +295 -118
  2. package/dist/index.js +272 -95
  3. package/package.json +5 -3
  4. package/src/index.ts +163 -0
  5. package/src/lexer/index.ts +20 -0
  6. package/src/lexer/lexer.ts +687 -0
  7. package/src/lexer/tokens.ts +141 -0
  8. package/src/parser/constants.ts +173 -0
  9. package/src/parser/depth.ts +251 -0
  10. package/src/parser/index.ts +18 -0
  11. package/src/parser/parse.ts +315 -0
  12. package/src/parser/postprocess/divAdjacentParagraph.ts +76 -0
  13. package/src/parser/postprocess/index.ts +15 -0
  14. package/src/parser/postprocess/spanStrip.ts +697 -0
  15. package/src/parser/preprocess/expr.ts +265 -0
  16. package/src/parser/preprocess/index.ts +38 -0
  17. package/src/parser/preprocess/typography.ts +67 -0
  18. package/src/parser/preprocess/utils.ts +250 -0
  19. package/src/parser/preprocess/whitespace.ts +111 -0
  20. package/src/parser/rules/block/align.ts +282 -0
  21. package/src/parser/rules/block/bibliography.ts +359 -0
  22. package/src/parser/rules/block/block-list.ts +689 -0
  23. package/src/parser/rules/block/blockquote.ts +238 -0
  24. package/src/parser/rules/block/center.ts +87 -0
  25. package/src/parser/rules/block/clear-float.ts +75 -0
  26. package/src/parser/rules/block/code.ts +187 -0
  27. package/src/parser/rules/block/collapsible.ts +337 -0
  28. package/src/parser/rules/block/comment.ts +73 -0
  29. package/src/parser/rules/block/content-separator.ts +79 -0
  30. package/src/parser/rules/block/definition-list.ts +270 -0
  31. package/src/parser/rules/block/div.ts +400 -0
  32. package/src/parser/rules/block/embed-block.ts +153 -0
  33. package/src/parser/rules/block/footnoteblock.ts +200 -0
  34. package/src/parser/rules/block/heading.ts +142 -0
  35. package/src/parser/rules/block/horizontal-rule.ts +61 -0
  36. package/src/parser/rules/block/html.ts +222 -0
  37. package/src/parser/rules/block/iframe.ts +239 -0
  38. package/src/parser/rules/block/iftags.ts +150 -0
  39. package/src/parser/rules/block/include.ts +179 -0
  40. package/src/parser/rules/block/index.ts +127 -0
  41. package/src/parser/rules/block/list.ts +244 -0
  42. package/src/parser/rules/block/math.ts +183 -0
  43. package/src/parser/rules/block/module/backlinks/index.ts +31 -0
  44. package/src/parser/rules/block/module/backlinks/types.ts +21 -0
  45. package/src/parser/rules/block/module/categories/index.ts +34 -0
  46. package/src/parser/rules/block/module/categories/types.ts +21 -0
  47. package/src/parser/rules/block/module/css/index.ts +37 -0
  48. package/src/parser/rules/block/module/iftags/condition.ts +109 -0
  49. package/src/parser/rules/block/module/iftags/index.ts +26 -0
  50. package/src/parser/rules/block/module/iftags/preprocess.ts +140 -0
  51. package/src/parser/rules/block/module/iftags/resolve.ts +73 -0
  52. package/src/parser/rules/block/module/iftags/types.ts +63 -0
  53. package/src/parser/rules/block/module/include/index.ts +20 -0
  54. package/src/parser/rules/block/module/include/resolve.ts +556 -0
  55. package/src/parser/rules/block/module/index.ts +122 -0
  56. package/src/parser/rules/block/module/join/index.ts +34 -0
  57. package/src/parser/rules/block/module/join/types.ts +23 -0
  58. package/src/parser/rules/block/module/listpages/compiler.ts +453 -0
  59. package/src/parser/rules/block/module/listpages/extract.ts +410 -0
  60. package/src/parser/rules/block/module/listpages/index.ts +83 -0
  61. package/src/parser/rules/block/module/listpages/normalize.ts +390 -0
  62. package/src/parser/rules/block/module/listpages/parser.ts +106 -0
  63. package/src/parser/rules/block/module/listpages/resolve.ts +130 -0
  64. package/src/parser/rules/block/module/listpages/types.ts +513 -0
  65. package/src/parser/rules/block/module/listpages/url-resolver.ts +186 -0
  66. package/src/parser/rules/block/module/listusers/compiler.ts +77 -0
  67. package/src/parser/rules/block/module/listusers/extract.ts +45 -0
  68. package/src/parser/rules/block/module/listusers/index.ts +36 -0
  69. package/src/parser/rules/block/module/listusers/parser.ts +54 -0
  70. package/src/parser/rules/block/module/listusers/resolve.ts +58 -0
  71. package/src/parser/rules/block/module/listusers/types.ts +93 -0
  72. package/src/parser/rules/block/module/mapping.ts +61 -0
  73. package/src/parser/rules/block/module/page-tree/index.ts +38 -0
  74. package/src/parser/rules/block/module/page-tree/types.ts +29 -0
  75. package/src/parser/rules/block/module/rate/index.ts +28 -0
  76. package/src/parser/rules/block/module/rate/types.ts +19 -0
  77. package/src/parser/rules/block/module/resolve.ts +411 -0
  78. package/src/parser/rules/block/module/types-common.ts +59 -0
  79. package/src/parser/rules/block/module/types.ts +61 -0
  80. package/src/parser/rules/block/module/utils.ts +43 -0
  81. package/src/parser/rules/block/module/walk.ts +380 -0
  82. package/src/parser/rules/block/module.ts +164 -0
  83. package/src/parser/rules/block/orphan-li.ts +177 -0
  84. package/src/parser/rules/block/paragraph.ts +157 -0
  85. package/src/parser/rules/block/table-block.ts +726 -0
  86. package/src/parser/rules/block/table.ts +441 -0
  87. package/src/parser/rules/block/tabview.ts +331 -0
  88. package/src/parser/rules/block/toc.ts +129 -0
  89. package/src/parser/rules/block/utils.ts +615 -0
  90. package/src/parser/rules/index.ts +49 -0
  91. package/src/parser/rules/inline/anchor-name.ts +154 -0
  92. package/src/parser/rules/inline/anchor.ts +327 -0
  93. package/src/parser/rules/inline/bibcite.ts +153 -0
  94. package/src/parser/rules/inline/bold.ts +86 -0
  95. package/src/parser/rules/inline/color.ts +140 -0
  96. package/src/parser/rules/inline/comment.ts +90 -0
  97. package/src/parser/rules/inline/equation-ref.ts +115 -0
  98. package/src/parser/rules/inline/expr.ts +526 -0
  99. package/src/parser/rules/inline/footnote.ts +223 -0
  100. package/src/parser/rules/inline/guillemet.ts +64 -0
  101. package/src/parser/rules/inline/html.ts +132 -0
  102. package/src/parser/rules/inline/image.ts +328 -0
  103. package/src/parser/rules/inline/index.ts +150 -0
  104. package/src/parser/rules/inline/italic.ts +74 -0
  105. package/src/parser/rules/inline/line-break.ts +326 -0
  106. package/src/parser/rules/inline/link-anchor.ts +147 -0
  107. package/src/parser/rules/inline/link-single.ts +164 -0
  108. package/src/parser/rules/inline/link-star.ts +134 -0
  109. package/src/parser/rules/inline/link-triple.ts +267 -0
  110. package/src/parser/rules/inline/math-inline.ts +126 -0
  111. package/src/parser/rules/inline/monospace.ts +78 -0
  112. package/src/parser/rules/inline/raw.ts +262 -0
  113. package/src/parser/rules/inline/size.ts +244 -0
  114. package/src/parser/rules/inline/span.ts +424 -0
  115. package/src/parser/rules/inline/strikethrough.ts +115 -0
  116. package/src/parser/rules/inline/subscript.ts +84 -0
  117. package/src/parser/rules/inline/superscript.ts +84 -0
  118. package/src/parser/rules/inline/text.ts +84 -0
  119. package/src/parser/rules/inline/underline.ts +127 -0
  120. package/src/parser/rules/inline/user.ts +147 -0
  121. package/src/parser/rules/inline/utils.ts +344 -0
  122. package/src/parser/rules/types.ts +252 -0
  123. package/src/parser/rules/utils.ts +155 -0
  124. 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
+ }