@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,390 @@
1
+ /**
2
+ *
3
+ * Query normalization for the ListPages module.
4
+ *
5
+ * Converts the raw string-based `ListPagesQuery` (where fields like `tags`,
6
+ * `category`, and `order` are plain strings) into a `NormalizedListPagesQuery`
7
+ * with type-safe structured objects. This makes it straightforward for external
8
+ * applications to build database queries from the normalized representation
9
+ * without having to re-parse Wikidot's query syntax.
10
+ *
11
+ * Based on Wikidot official documentation:
12
+ * https://www.wikidot.com/doc-modules:listpages-module
13
+ *
14
+ * @module
15
+ */
16
+
17
+ import type {
18
+ ListPagesQuery,
19
+ NormalizedListPagesQuery,
20
+ NormalizedTags,
21
+ NormalizedCategory,
22
+ NormalizedOrder,
23
+ NormalizedParent,
24
+ NormalizedDateSelector,
25
+ NormalizedNumericSelector,
26
+ OrderField,
27
+ OrderDirection,
28
+ DateComparisonOp,
29
+ NumericComparisonOp,
30
+ } from "./types";
31
+
32
+ /**
33
+ * Pattern for splitting multi-value attribute strings.
34
+ * Wikidot allows commas, semicolons, and whitespace as separators between values.
35
+ */
36
+ const TOKEN_SEPARATOR = /[,;\s]+/;
37
+
38
+ /**
39
+ * Parse tags string into structured format
40
+ *
41
+ * Syntax:
42
+ * - `+tag`: AND condition (must have this tag)
43
+ * - `-tag`: NOT condition (must not have this tag)
44
+ * - `tag`: OR condition (any of these tags)
45
+ * - `=`: same visible tags as current page
46
+ * - `==`: exact same tags as current page
47
+ * - `-`: pages with no tags
48
+ */
49
+ export function parseTags(value: string): NormalizedTags {
50
+ const result: NormalizedTags = {
51
+ all: [],
52
+ any: [],
53
+ none: [],
54
+ special: null,
55
+ };
56
+
57
+ const trimmed = value.trim();
58
+ if (!trimmed) return result;
59
+
60
+ // Check for special selectors
61
+ if (trimmed === "-") {
62
+ result.special = "none";
63
+ return result;
64
+ }
65
+ if (trimmed === "==") {
66
+ result.special = "same-all";
67
+ return result;
68
+ }
69
+
70
+ const tokens = trimmed.split(TOKEN_SEPARATOR).filter(Boolean);
71
+
72
+ for (const token of tokens) {
73
+ if (token === "=") {
74
+ result.special = "same-visible";
75
+ } else if (token.startsWith("+")) {
76
+ const tag = token.slice(1);
77
+ if (tag) result.all.push(tag);
78
+ } else if (token.startsWith("-")) {
79
+ const tag = token.slice(1);
80
+ if (tag) result.none.push(tag);
81
+ } else {
82
+ result.any.push(token);
83
+ }
84
+ }
85
+
86
+ return result;
87
+ }
88
+
89
+ /**
90
+ * Parse category string into structured format
91
+ *
92
+ * Syntax:
93
+ * - `*`: all categories
94
+ * - `.`: current category
95
+ * - `-category`: exclude category
96
+ * - `category`: include category
97
+ * - Multiple categories separated by comma, semicolon, or whitespace
98
+ */
99
+ export function parseCategory(value: string): NormalizedCategory {
100
+ const result: NormalizedCategory = {
101
+ include: [],
102
+ exclude: [],
103
+ all: false,
104
+ current: false,
105
+ };
106
+
107
+ const trimmed = value.trim().toLowerCase();
108
+ if (!trimmed) return result;
109
+
110
+ if (trimmed === "*") {
111
+ result.all = true;
112
+ return result;
113
+ }
114
+
115
+ const tokens = trimmed.split(TOKEN_SEPARATOR).filter(Boolean);
116
+
117
+ for (const token of tokens) {
118
+ if (token === "*") {
119
+ result.all = true;
120
+ } else if (token === ".") {
121
+ result.current = true;
122
+ } else if (token.startsWith("-")) {
123
+ const cat = token.slice(1);
124
+ if (cat) result.exclude.push(cat);
125
+ } else {
126
+ result.include.push(token);
127
+ }
128
+ }
129
+
130
+ return result;
131
+ }
132
+
133
+ /**
134
+ * Mapping from Wikidot's order field names (both camelCase PHP-style and
135
+ * snake_case documentation-style) to normalized `OrderField` values.
136
+ */
137
+ const ORDER_FIELD_MAP: Record<string, OrderField> = {
138
+ // camelCase format (Wikidot PHP style)
139
+ datecreated: "created_at",
140
+ dateedited: "updated_at",
141
+ title: "title",
142
+ fullname: "fullname",
143
+ rating: "rating",
144
+ votes: "votes",
145
+ revisions: "revisions",
146
+ comments: "comments",
147
+ pagelength: "size",
148
+ size: "size",
149
+ random: "random",
150
+ // snake_case format (documentation style)
151
+ created_at: "created_at",
152
+ updated_at: "updated_at",
153
+ };
154
+
155
+ /**
156
+ * Parse order string into structured format
157
+ *
158
+ * Supports both formats:
159
+ * - camelCase: `dateCreatedDesc`, `titleAsc`, `ratingDesc`
160
+ * - Space-separated: `created_at desc`, `title asc`
161
+ *
162
+ * Default: { field: "created_at", direction: "desc" }
163
+ */
164
+ export function parseOrder(value: string): NormalizedOrder {
165
+ const defaultOrder: NormalizedOrder = { field: "created_at", direction: "desc" };
166
+ const trimmed = value.trim().toLowerCase();
167
+ if (!trimmed) return defaultOrder;
168
+
169
+ // Try space-separated format: "created_at desc"
170
+ const spaceParts = trimmed.split(/\s+/);
171
+ if (spaceParts.length >= 2 && spaceParts[0] && spaceParts[1]) {
172
+ const field = ORDER_FIELD_MAP[spaceParts[0]];
173
+ const direction = spaceParts[1] === "asc" ? "asc" : "desc";
174
+ if (field) {
175
+ return { field, direction };
176
+ }
177
+ }
178
+
179
+ // Try camelCase format: "dateCreatedDesc"
180
+ let direction: OrderDirection = "desc";
181
+ let fieldPart = trimmed;
182
+
183
+ if (trimmed.endsWith("desc")) {
184
+ direction = "desc";
185
+ fieldPart = trimmed.slice(0, -4);
186
+ } else if (trimmed.endsWith("asc")) {
187
+ direction = "asc";
188
+ fieldPart = trimmed.slice(0, -3);
189
+ }
190
+
191
+ const field = ORDER_FIELD_MAP[fieldPart];
192
+ if (field) {
193
+ return { field, direction };
194
+ }
195
+
196
+ // Try single word (field only, use default direction)
197
+ const singleField = ORDER_FIELD_MAP[trimmed];
198
+ if (singleField) {
199
+ return { field: singleField, direction: "desc" };
200
+ }
201
+
202
+ return defaultOrder;
203
+ }
204
+
205
+ /**
206
+ * Parse parent string into structured format
207
+ *
208
+ * Syntax:
209
+ * - `-`: orphan pages (no parent)
210
+ * - `=`: sibling pages (same parent as current)
211
+ * - `-=`: pages with different parent
212
+ * - `.`: children of current page
213
+ * - `page-name`: children of specific page
214
+ *
215
+ * Returns undefined for empty/whitespace-only input.
216
+ */
217
+ export function parseParent(value: string): NormalizedParent | undefined {
218
+ const trimmed = value.trim();
219
+ if (!trimmed) return undefined;
220
+
221
+ switch (trimmed) {
222
+ case "-":
223
+ return { type: "none" };
224
+ case "=":
225
+ return { type: "same" };
226
+ case "-=":
227
+ return { type: "different" };
228
+ case ".":
229
+ return { type: "children" };
230
+ default:
231
+ return { type: "page", name: trimmed };
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Date comparison operators, ordered longest-first so longer operators
237
+ * (like `<=`) are matched before shorter ones (like `<`).
238
+ */
239
+ const DATE_COMPARISON_OPS: DateComparisonOp[] = ["<=", ">=", "<>", "<", ">", "="];
240
+
241
+ /**
242
+ * Pattern for relative date expressions like "last 7 days", "last 2 weeks", "last month".
243
+ * The count is optional and defaults to 1 (e.g., "last month" = "last 1 month").
244
+ */
245
+ const RELATIVE_DATE_PATTERN = /^last\s+(?:(\d+)\s+)?(day|week|month)s?$/i;
246
+
247
+ /**
248
+ * Parse date selector string into structured format
249
+ *
250
+ * Syntax:
251
+ * - `yyyy`: year only
252
+ * - `yyyy.mm`: year and month
253
+ * - `>=yyyy.mm.dd`: comparison with date
254
+ * - `last 7 days`: relative date
255
+ */
256
+ export function parseDateSelector(value: string): NormalizedDateSelector | undefined {
257
+ const trimmed = value.trim();
258
+ if (!trimmed) return undefined;
259
+
260
+ // Check relative date format: "last 7 days"
261
+ const relativeMatch = trimmed.match(RELATIVE_DATE_PATTERN);
262
+ if (relativeMatch && relativeMatch[2]) {
263
+ const count = relativeMatch[1] ? parseInt(relativeMatch[1], 10) : 1;
264
+ // Validate count is at least 1
265
+ if (count < 1) return undefined;
266
+ const unit = relativeMatch[2].toLowerCase() as "day" | "week" | "month";
267
+ return { type: "relative", unit, count };
268
+ }
269
+
270
+ // Check comparison operators
271
+ for (const op of DATE_COMPARISON_OPS) {
272
+ if (trimmed.startsWith(op)) {
273
+ const date = trimmed.slice(op.length).trim();
274
+ if (date) {
275
+ return { type: "comparison", op, date };
276
+ }
277
+ }
278
+ }
279
+
280
+ // Check year format: "2024"
281
+ if (/^\d{4}$/.test(trimmed)) {
282
+ return { type: "year", year: parseInt(trimmed, 10) };
283
+ }
284
+
285
+ // Check year.month format: "2024.03"
286
+ const monthMatch = trimmed.match(/^(\d{4})\.(\d{1,2})$/);
287
+ if (monthMatch && monthMatch[1] && monthMatch[2]) {
288
+ const month = parseInt(monthMatch[2], 10);
289
+ // Validate month is 1-12
290
+ if (month < 1 || month > 12) return undefined;
291
+ return {
292
+ type: "month",
293
+ year: parseInt(monthMatch[1], 10),
294
+ month,
295
+ };
296
+ }
297
+
298
+ return undefined;
299
+ }
300
+
301
+ /**
302
+ * Numeric comparison operators, ordered longest-first for correct prefix matching.
303
+ */
304
+ const NUMERIC_COMPARISON_OPS: NumericComparisonOp[] = ["<=", ">=", "<", ">", "="];
305
+
306
+ /**
307
+ * Parse numeric selector string into structured format
308
+ *
309
+ * Syntax:
310
+ * - `5`: equals 5
311
+ * - `>=10`: greater than or equal to 10
312
+ * - `<0`: less than 0
313
+ *
314
+ * Returns undefined for non-numeric or infinite values.
315
+ */
316
+ export function parseNumericSelector(value: string): NormalizedNumericSelector | undefined {
317
+ const trimmed = value.trim();
318
+ if (!trimmed) return undefined;
319
+
320
+ // Check comparison operators
321
+ for (const op of NUMERIC_COMPARISON_OPS) {
322
+ if (trimmed.startsWith(op)) {
323
+ const numStr = trimmed.slice(op.length).trim();
324
+ const num = parseFloat(numStr);
325
+ // Strict validation: must be finite number and entire string must be numeric
326
+ if (Number.isFinite(num) && /^-?\d+(\.\d+)?$/.test(numStr)) {
327
+ return { op, value: num };
328
+ }
329
+ return undefined;
330
+ }
331
+ }
332
+
333
+ // Plain number (equals) - strict validation
334
+ const num = parseFloat(trimmed);
335
+ if (Number.isFinite(num) && /^-?\d+(\.\d+)?$/.test(trimmed)) {
336
+ return { op: "=", value: num };
337
+ }
338
+
339
+ return undefined;
340
+ }
341
+
342
+ /**
343
+ * Normalize a ListPagesQuery into structured types
344
+ *
345
+ * @param query - Raw query with string fields
346
+ * @returns Normalized query with structured types
347
+ */
348
+ export function normalizeQuery(query: ListPagesQuery): NormalizedListPagesQuery {
349
+ const result: NormalizedListPagesQuery = {};
350
+
351
+ // Pass through simple fields
352
+ if (query.pagetype) result.pagetype = query.pagetype;
353
+ if (query.linkTo) result.linkTo = query.linkTo;
354
+ if (query.createdBy) result.createdBy = query.createdBy;
355
+ if (query.name) result.name = query.name;
356
+ if (query.fullname) result.fullname = query.fullname;
357
+ if (query.range) result.range = query.range;
358
+ if (query.dataFormFields) result.dataFormFields = query.dataFormFields;
359
+ if (query.offset !== undefined) result.offset = query.offset;
360
+ if (query.limit !== undefined) result.limit = query.limit;
361
+ if (query.perPage !== undefined) result.perPage = query.perPage;
362
+ if (query.reverse !== undefined) result.reverse = query.reverse;
363
+
364
+ // Parse complex fields
365
+ if (query.tags) result.tags = parseTags(query.tags);
366
+ if (query.category) result.category = parseCategory(query.category);
367
+ if (query.order) result.order = parseOrder(query.order);
368
+ if (query.parent) {
369
+ const parent = parseParent(query.parent);
370
+ if (parent) result.parent = parent;
371
+ }
372
+ if (query.createdAt) {
373
+ const createdAt = parseDateSelector(query.createdAt);
374
+ if (createdAt) result.createdAt = createdAt;
375
+ }
376
+ if (query.updatedAt) {
377
+ const updatedAt = parseDateSelector(query.updatedAt);
378
+ if (updatedAt) result.updatedAt = updatedAt;
379
+ }
380
+ if (query.rating) {
381
+ const rating = parseNumericSelector(query.rating);
382
+ if (rating) result.rating = rating;
383
+ }
384
+ if (query.votes) {
385
+ const votes = parseNumericSelector(query.votes);
386
+ if (votes) result.votes = votes;
387
+ }
388
+
389
+ return result;
390
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ *
3
+ * Parser rule for the Wikidot `[[module ListPages ...]]` block.
4
+ *
5
+ * Parses the module's attributes into a structured `list-pages` Module AST node.
6
+ * Handles both hyphenated (`link-to`) and concatenated (`linkto`) attribute name
7
+ * formats, as Wikidot normalizes both to lowercase. The raw attribute values are
8
+ * preserved in the `attributes` field for `@URL` resolution by external applications.
9
+ *
10
+ * @module
11
+ */
12
+
13
+ import type { Module } from "@wdprlib/ast";
14
+ import type { ModuleRule } from "../types";
15
+ import { parseBool, parseInt32 } from "../utils";
16
+
17
+ /**
18
+ * Module rule for `[[module ListPages ...]]`.
19
+ *
20
+ * ListPages is the most complex Wikidot module. It queries pages by various
21
+ * criteria (tags, category, parent, date, rating, etc.) and renders each matching
22
+ * page using a template specified in the module body. The template uses
23
+ * `%%variable%%` syntax to reference page data.
24
+ *
25
+ * This rule only handles parsing; data fetching and template rendering are handled
26
+ * by the extract/resolve pipeline.
27
+ */
28
+ export const listPagesModuleRule: ModuleRule = {
29
+ name: "module-listpages",
30
+ acceptsNames: ["listpages"],
31
+ hasBody: true,
32
+
33
+ parse(_ctx, _pos, args, body): Module {
34
+ // Extract known attributes, pass rest as additional attributes
35
+ // Note: attribute names are normalized to lowercase by parseAttributesRaw
36
+ const {
37
+ category,
38
+ tags,
39
+ parent,
40
+ rating,
41
+ votes,
42
+ name,
43
+ fullname,
44
+ range,
45
+ pagetype,
46
+ offset,
47
+ limit,
48
+ order,
49
+ reverse,
50
+ separate,
51
+ wrapper,
52
+ rss,
53
+ } = args;
54
+ // Hyphenated attributes (stored with hyphens in lowercase)
55
+ const linkTo = args["link-to"] ?? args.linkto;
56
+ const createdBy = args["created-by"] ?? args.createdby;
57
+ const createdAt = args["created-at"] ?? args.createdat;
58
+ const updatedAt = args["updated-at"] ?? args.updatedat;
59
+ const perPage = args["per-page"] ?? args.perpage;
60
+ const prependLine = args["prepend-line"] ?? args.prependline;
61
+ const appendLine = args["append-line"] ?? args.appendline;
62
+ const rssDescription = args["rss-description"] ?? args.rssdescription;
63
+ const rssHome = args["rss-home"] ?? args.rsshome;
64
+ const rssLimit = args["rss-limit"] ?? args.rsslimit;
65
+ const rssOnly = args["rss-only"] ?? args.rssonly;
66
+ const urlAttrPrefix = args["url-attr-prefix"] ?? args.urlattrprefix;
67
+
68
+ // Store all raw arguments for @URL resolution by external apps
69
+ const rawArgs: Record<string, string> = { ...args };
70
+
71
+ return {
72
+ module: "list-pages",
73
+ category,
74
+ tags,
75
+ parent,
76
+ "link-to": linkTo,
77
+ "created-by": createdBy,
78
+ "created-at": createdAt,
79
+ "updated-at": updatedAt,
80
+ rating,
81
+ votes,
82
+ name,
83
+ fullname,
84
+ range,
85
+ pagetype,
86
+ offset: parseInt32(offset),
87
+ limit: parseInt32(limit),
88
+ "per-page": parseInt32(perPage),
89
+ order,
90
+ reverse: parseBool(reverse, false),
91
+ separate: parseBool(separate, true),
92
+ wrapper: parseBool(wrapper, true),
93
+ "prepend-line": prependLine,
94
+ "append-line": appendLine,
95
+ rss,
96
+ "rss-description": rssDescription,
97
+ "rss-home": rssHome,
98
+ "rss-limit": parseInt32(rssLimit),
99
+ "rss-only": parseBool(rssOnly, false),
100
+ "url-attr-prefix": urlAttrPrefix,
101
+ body,
102
+ // All raw arguments for @URL resolution
103
+ attributes: rawArgs,
104
+ };
105
+ },
106
+ };
@@ -0,0 +1,130 @@
1
+ /**
2
+ *
3
+ * ListPages module resolution (phase 3 of the ListPages lifecycle).
4
+ *
5
+ * After the application has fetched page data based on the extracted requirements,
6
+ * this module substitutes that data into the pre-compiled templates and re-parses
7
+ * the resulting wikitext to produce final AST elements.
8
+ *
9
+ * For each page in the fetched data:
10
+ * 1. Build a `VariableContext` with page data, index, total count, and site info
11
+ * 2. Execute the compiled template to produce a wikitext string
12
+ * 3. Re-parse the wikitext string into AST elements
13
+ * 4. Optionally wrap each item in a `div.list-pages-item` (when `separate=true`)
14
+ *
15
+ * The final result may also include prepend/append lines and be wrapped in a
16
+ * `div.list-pages-box` (when `wrapper=true`).
17
+ *
18
+ * @module
19
+ */
20
+
21
+ import type { Element, Module } from "@wdprlib/ast";
22
+ import type { ListPagesExternalData, CompiledTemplate, VariableContext } from "./types";
23
+ import type { ParseFunction } from "../types";
24
+ export type { ParseFunction };
25
+
26
+ /**
27
+ * Narrowed type for the list-pages variant of the Module discriminated union.
28
+ */
29
+ export type ListPagesModuleData = Extract<Module, { module: "list-pages" }>;
30
+
31
+ /**
32
+ * Type guard to check if a Module is a list-pages module.
33
+ *
34
+ * @param module - A Module discriminated union value
35
+ * @returns true if the module is a list-pages module
36
+ */
37
+ export function isListPagesModule(module: Module): module is ListPagesModuleData {
38
+ return module.module === "list-pages";
39
+ }
40
+
41
+ /**
42
+ * Resolve a single ListPages module
43
+ *
44
+ * @param module - ListPages module data from AST
45
+ * @param data - External data fetched for this module
46
+ * @param compiledTemplate - Pre-compiled template function
47
+ * @param parse - Parser function for re-parsing templates
48
+ * @returns Resolved elements
49
+ */
50
+ export function resolveListPages(
51
+ module: ListPagesModuleData,
52
+ data: ListPagesExternalData,
53
+ compiledTemplate: CompiledTemplate,
54
+ parse: ParseFunction,
55
+ ): Element[] {
56
+ const items: Element[] = [];
57
+
58
+ // Process each page
59
+ for (let i = 0; i < data.pages.length; i++) {
60
+ const page = data.pages[i];
61
+ if (!page) continue;
62
+
63
+ const ctx: VariableContext = {
64
+ page,
65
+ index: i + 1,
66
+ total: data.totalCount,
67
+ limit: module.limit,
68
+ site: data.site,
69
+ };
70
+
71
+ // Execute compiled template
72
+ const substituted = compiledTemplate(ctx);
73
+
74
+ // Re-parse as wikitext
75
+ const itemAst = parse(substituted);
76
+
77
+ if (module.separate) {
78
+ // Wrap each item in div
79
+ const wrapper: Element = {
80
+ element: "container",
81
+ data: {
82
+ type: "div",
83
+ attributes: { class: "list-pages-item" },
84
+ elements: itemAst.elements,
85
+ },
86
+ };
87
+ items.push(wrapper);
88
+ } else {
89
+ items.push(...itemAst.elements);
90
+ }
91
+ }
92
+
93
+ // Handle empty results
94
+ if (items.length === 0) {
95
+ return [];
96
+ }
97
+
98
+ // Build final result
99
+ const result: Element[] = [];
100
+
101
+ // Prepend line (only when separate=false)
102
+ if (module["prepend-line"] && !module.separate) {
103
+ const prependAst = parse(module["prepend-line"]);
104
+ result.push(...prependAst.elements);
105
+ }
106
+
107
+ result.push(...items);
108
+
109
+ // Append line (only when separate=false)
110
+ if (module["append-line"] && !module.separate) {
111
+ const appendAst = parse(module["append-line"]);
112
+ result.push(...appendAst.elements);
113
+ }
114
+
115
+ // Wrap everything if wrapper=true
116
+ if (module.wrapper) {
117
+ return [
118
+ {
119
+ element: "container",
120
+ data: {
121
+ type: "div",
122
+ attributes: { class: "list-pages-box" },
123
+ elements: result,
124
+ },
125
+ },
126
+ ];
127
+ }
128
+
129
+ return result;
130
+ }