@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,513 @@
1
+ /**
2
+ *
3
+ * Type definitions for the ListPages module system.
4
+ *
5
+ * This file defines the complete type vocabulary for the ListPages lifecycle:
6
+ *
7
+ * - **Query types**: Raw and normalized representations of ListPages filter/sort parameters
8
+ * - **Variable types**: The `%%variable%%` names supported in ListPages templates
9
+ * - **Data requirement types**: What the parser tells the application it needs to fetch
10
+ * - **External data types**: What the application provides back (page data, user info, site context)
11
+ * - **Template types**: Compiled template function signatures and their execution context
12
+ * - **Normalized query types**: Structured representations of parsed query parameters
13
+ *
14
+ * Security note: Several fields contain untrusted user input from wikitext.
15
+ * See `ListPagesQuery` documentation for safe usage guidelines.
16
+ *
17
+ * @module
18
+ */
19
+
20
+ // =============================================================================
21
+ // Query Types
22
+ // =============================================================================
23
+
24
+ /**
25
+ * ListPages query parameters for page selection
26
+ *
27
+ * @security All string fields contain **untrusted user input** from wikitext.
28
+ * When using these values in database queries:
29
+ * - **NEVER** interpolate directly into SQL/NoSQL query strings
30
+ * - **ALWAYS** use parameterized queries or prepared statements
31
+ * - For ORDER BY clauses, use a whitelist of allowed column names
32
+ *
33
+ * @example Safe usage with SQL
34
+ * ```typescript
35
+ * // GOOD: Parameterized query
36
+ * db.query("SELECT * FROM pages WHERE category = ?", [query.category]);
37
+ *
38
+ * // BAD: String interpolation (SQL injection vulnerable!)
39
+ * db.query(`SELECT * FROM pages WHERE category = '${query.category}'`);
40
+ * ```
41
+ */
42
+ export interface ListPagesQuery {
43
+ /** Page type selector */
44
+ pagetype?: "normal" | "hidden" | "*";
45
+
46
+ /**
47
+ * Category selector
48
+ * @untrusted User input - use parameterized queries
49
+ */
50
+ category?: string;
51
+
52
+ /**
53
+ * Tag selector (e.g., "+fruit -admin")
54
+ * @untrusted User input - use parameterized queries
55
+ */
56
+ tags?: string;
57
+
58
+ /**
59
+ * Parent page selector
60
+ * @untrusted User input - use parameterized queries
61
+ */
62
+ parent?: string;
63
+
64
+ /**
65
+ * Link target selector
66
+ * @untrusted User input - use parameterized queries
67
+ */
68
+ linkTo?: string;
69
+
70
+ /**
71
+ * Created date selector
72
+ * @untrusted User input - use parameterized queries
73
+ */
74
+ createdAt?: string;
75
+
76
+ /**
77
+ * Updated date selector
78
+ * @untrusted User input - use parameterized queries
79
+ */
80
+ updatedAt?: string;
81
+
82
+ /**
83
+ * Author selector
84
+ * @untrusted User input - use parameterized queries
85
+ */
86
+ createdBy?: string;
87
+
88
+ /**
89
+ * Rating selector
90
+ * @untrusted User input - use parameterized queries
91
+ */
92
+ rating?: string;
93
+
94
+ /**
95
+ * Votes selector
96
+ * @untrusted User input - use parameterized queries
97
+ */
98
+ votes?: string;
99
+
100
+ /**
101
+ * Page name selector
102
+ * @untrusted User input - use parameterized queries
103
+ */
104
+ name?: string;
105
+
106
+ /**
107
+ * Full page name selector (category:name)
108
+ * @untrusted User input - use parameterized queries
109
+ */
110
+ fullname?: string;
111
+
112
+ /** Range selector relative to current page */
113
+ range?: "." | "before" | "after" | "others";
114
+
115
+ /**
116
+ * Data form field selectors
117
+ * @untrusted Both keys and values are user input
118
+ */
119
+ dataFormFields?: Record<string, string>;
120
+
121
+ /**
122
+ * Ordering specification
123
+ * @untrusted User input - use whitelist validation for ORDER BY
124
+ */
125
+ order?: string;
126
+
127
+ /** Pagination offset */
128
+ offset?: number;
129
+
130
+ /** Maximum number of results */
131
+ limit?: number;
132
+
133
+ /** Results per page */
134
+ perPage?: number;
135
+
136
+ /** Reverse order */
137
+ reverse?: boolean;
138
+ }
139
+
140
+ // =============================================================================
141
+ // Variable Types
142
+ // =============================================================================
143
+
144
+ /**
145
+ * All supported ListPages template variables
146
+ */
147
+ export type ListPagesVariable =
148
+ // Lifecycle - created
149
+ | "created_at"
150
+ | "created_by"
151
+ | "created_by_unix"
152
+ | "created_by_id"
153
+ | "created_by_linked"
154
+ // Lifecycle - updated
155
+ | "updated_at"
156
+ | "updated_by"
157
+ | "updated_by_unix"
158
+ | "updated_by_id"
159
+ | "updated_by_linked"
160
+ // Lifecycle - commented
161
+ | "commented_at"
162
+ | "commented_by"
163
+ | "commented_by_unix"
164
+ | "commented_by_id"
165
+ | "commented_by_linked"
166
+ // Structure - page
167
+ | "name"
168
+ | "category"
169
+ | "fullname"
170
+ | "title"
171
+ | "title_linked"
172
+ | "link"
173
+ // Structure - parent
174
+ | "parent_name"
175
+ | "parent_category"
176
+ | "parent_fullname"
177
+ | "parent_title"
178
+ | "parent_title_linked"
179
+ // Content
180
+ | "content"
181
+ | "content_n" // content{n}
182
+ | "preview"
183
+ | "preview_n" // preview(n)
184
+ | "summary"
185
+ | "first_paragraph"
186
+ // Tags
187
+ | "tags"
188
+ | "tags_linked"
189
+ | "_tags"
190
+ | "_tags_linked"
191
+ // Form data
192
+ | "form_data"
193
+ | "form_raw"
194
+ | "form_label"
195
+ | "form_hint"
196
+ // Metrics
197
+ | "children"
198
+ | "comments"
199
+ | "size"
200
+ | "rating"
201
+ | "rating_votes"
202
+ | "rating_percent"
203
+ | "revisions"
204
+ // Pagination
205
+ | "index"
206
+ | "total"
207
+ | "limit"
208
+ | "total_or_limit"
209
+ // Site context
210
+ | "site_title"
211
+ | "site_name"
212
+ | "site_domain";
213
+
214
+ // =============================================================================
215
+ // Data Requirement Types
216
+ // =============================================================================
217
+
218
+ /**
219
+ * Data requirement for a single ListPages module
220
+ */
221
+ export interface ListPagesDataRequirement {
222
+ /** Unique identifier for this module instance */
223
+ id: number;
224
+
225
+ /** Query parameters */
226
+ query: ListPagesQuery;
227
+
228
+ /** Variables used in the template */
229
+ neededVariables: ListPagesVariable[];
230
+
231
+ /** Indices needed for content{n} */
232
+ contentSectionIndices?: number[];
233
+
234
+ /** Lengths needed for preview(n) */
235
+ previewLengths?: number[];
236
+
237
+ /** Field names needed for form_data{field} etc */
238
+ formFields?: string[];
239
+
240
+ /** Prefix for tags_linked|prefix */
241
+ tagsLinkPrefix?: string;
242
+
243
+ /** Prefix for _tags_linked|prefix */
244
+ hiddenTagsLinkPrefix?: string;
245
+
246
+ /**
247
+ * URL attribute prefix for multiple ListPages modules
248
+ * When set, URL parameters are prefixed (e.g., "page2" -> "/page2_limit/1")
249
+ */
250
+ urlAttrPrefix?: string;
251
+
252
+ /**
253
+ * Raw attribute values before URL resolution
254
+ * Contains original string values that may include "@URL" or "@URL|default" format
255
+ * External applications should use these to resolve URL parameters
256
+ */
257
+ rawAttributes: Record<string, string>;
258
+ }
259
+
260
+ /**
261
+ * All data requirements from parsing
262
+ */
263
+ export interface DataRequirements {
264
+ listPages: ListPagesDataRequirement[];
265
+ listUsers: import("../listusers/types").ListUsersDataRequirement[];
266
+ }
267
+
268
+ // =============================================================================
269
+ // External Data Types
270
+ // =============================================================================
271
+
272
+ /**
273
+ * User information
274
+ */
275
+ export interface UserInfo {
276
+ id: number;
277
+ name: string;
278
+ unixName: string;
279
+ }
280
+
281
+ /**
282
+ * Page data provided by external source
283
+ */
284
+ export interface PageData {
285
+ // Identity
286
+ name: string;
287
+ category: string;
288
+ fullname: string;
289
+ title: string;
290
+
291
+ // Lifecycle - created
292
+ createdAt: Date;
293
+ createdBy?: UserInfo;
294
+
295
+ // Lifecycle - updated
296
+ updatedAt: Date;
297
+ updatedBy?: UserInfo;
298
+
299
+ // Lifecycle - commented
300
+ commentedAt?: Date;
301
+ commentedBy?: UserInfo;
302
+
303
+ // Parent
304
+ parentName?: string;
305
+ parentCategory?: string;
306
+ parentFullname?: string;
307
+ parentTitle?: string;
308
+
309
+ // Content (====で区切られた形式。wdparserが%%content{n}%%解決時に分割する)
310
+ content?: string;
311
+
312
+ // Tags
313
+ tags: string[];
314
+ hiddenTags: string[]; // Starting with _
315
+
316
+ // Form data
317
+ formData?: Record<string, string>;
318
+ formRaw?: Record<string, string>;
319
+ formLabel?: Record<string, string>;
320
+ formHint?: Record<string, string>;
321
+
322
+ // Metrics
323
+ children: number;
324
+ comments: number;
325
+ size: number;
326
+ rating: number;
327
+ ratingVotes: number;
328
+ ratingPercent?: number;
329
+ revisions: number;
330
+ }
331
+
332
+ /**
333
+ * Site context information
334
+ */
335
+ export interface SiteContext {
336
+ title: string;
337
+ name: string;
338
+ domain: string;
339
+ }
340
+
341
+ /**
342
+ * External data for a single ListPages module
343
+ */
344
+ export interface ListPagesExternalData {
345
+ pages: PageData[];
346
+ totalCount: number;
347
+ site: SiteContext;
348
+ }
349
+
350
+ /**
351
+ * Callback to fetch data for a ListPages module
352
+ *
353
+ * Called by resolveModules for each ListPages module in the AST.
354
+ * Receives a normalized query with all `@URL` parameters resolved.
355
+ * Return null/undefined to skip the module (outputs nothing).
356
+ *
357
+ * @param query - Normalized query with structured types (tags, category, order, etc.)
358
+ * @param requirement - Original data requirement (for accessing id, neededVariables, etc.)
359
+ */
360
+ export type ListPagesDataFetcher = (
361
+ query: NormalizedListPagesQuery,
362
+ requirement: ListPagesDataRequirement,
363
+ ) => ListPagesExternalData | null | undefined | Promise<ListPagesExternalData | null | undefined>;
364
+
365
+ // Note: DataProvider is in ../types-common.ts to avoid circular dependency
366
+
367
+ // =============================================================================
368
+ // Compiled Template Types
369
+ // =============================================================================
370
+
371
+ /**
372
+ * Context passed to compiled template
373
+ */
374
+ export interface VariableContext {
375
+ page: PageData;
376
+ index: number;
377
+ total: number;
378
+ limit?: number;
379
+ site: SiteContext;
380
+ }
381
+
382
+ /**
383
+ * Compiled template function
384
+ */
385
+ export type CompiledTemplate = (ctx: VariableContext) => string;
386
+
387
+ // =============================================================================
388
+ // Normalized Query Types
389
+ // =============================================================================
390
+
391
+ /**
392
+ * Normalized tags selector
393
+ */
394
+ export interface NormalizedTags {
395
+ /** AND conditions - pages must have ALL of these tags (+tag) */
396
+ all: string[];
397
+ /** OR conditions - pages must have ANY of these tags (no prefix) */
398
+ any: string[];
399
+ /** NOT conditions - pages must NOT have these tags (-tag) */
400
+ none: string[];
401
+ /** Special selector */
402
+ special: "same-visible" | "same-all" | "none" | null;
403
+ }
404
+
405
+ /**
406
+ * Normalized category selector
407
+ */
408
+ export interface NormalizedCategory {
409
+ /** Categories to include */
410
+ include: string[];
411
+ /** Categories to exclude (-category) */
412
+ exclude: string[];
413
+ /** Select all categories (*) */
414
+ all: boolean;
415
+ /** Select current category (.) */
416
+ current: boolean;
417
+ }
418
+
419
+ /**
420
+ * Order field options
421
+ */
422
+ export type OrderField =
423
+ | "created_at"
424
+ | "updated_at"
425
+ | "title"
426
+ | "fullname"
427
+ | "rating"
428
+ | "votes"
429
+ | "revisions"
430
+ | "comments"
431
+ | "size"
432
+ | "random";
433
+
434
+ /**
435
+ * Order direction
436
+ */
437
+ export type OrderDirection = "asc" | "desc";
438
+
439
+ /**
440
+ * Normalized order specification
441
+ */
442
+ export interface NormalizedOrder {
443
+ field: OrderField;
444
+ direction: OrderDirection;
445
+ }
446
+
447
+ /**
448
+ * Normalized parent selector
449
+ */
450
+ export type NormalizedParent =
451
+ | { type: "none" } // "-": orphan pages
452
+ | { type: "same" } // "=": sibling pages
453
+ | { type: "different" } // "-=": different parent
454
+ | { type: "children" } // ".": children of current page
455
+ | { type: "page"; name: string }; // specific page name
456
+
457
+ /**
458
+ * Date comparison operators
459
+ */
460
+ export type DateComparisonOp = "=" | "<" | ">" | "<=" | ">=" | "<>";
461
+
462
+ /**
463
+ * Normalized date selector
464
+ */
465
+ export type NormalizedDateSelector =
466
+ | { type: "year"; year: number }
467
+ | { type: "month"; year: number; month: number }
468
+ | { type: "comparison"; op: DateComparisonOp; date: string }
469
+ | { type: "relative"; unit: "day" | "week" | "month"; count: number };
470
+
471
+ /**
472
+ * Numeric comparison operators
473
+ */
474
+ export type NumericComparisonOp = "=" | "<" | ">" | "<=" | ">=";
475
+
476
+ /**
477
+ * Normalized numeric selector (for rating/votes)
478
+ */
479
+ export interface NormalizedNumericSelector {
480
+ op: NumericComparisonOp;
481
+ value: number;
482
+ }
483
+
484
+ /**
485
+ * Fully normalized ListPages query
486
+ *
487
+ * All string fields are parsed and structured into type-safe objects.
488
+ * Use `normalizeQuery()` to convert from `ListPagesQuery`.
489
+ *
490
+ * Note: This is structural normalization, not full validation.
491
+ * Invalid inputs are either rejected (return undefined) or ignored.
492
+ */
493
+ export interface NormalizedListPagesQuery {
494
+ pagetype?: "normal" | "hidden" | "*";
495
+ category?: NormalizedCategory;
496
+ tags?: NormalizedTags;
497
+ parent?: NormalizedParent;
498
+ linkTo?: string;
499
+ createdAt?: NormalizedDateSelector;
500
+ updatedAt?: NormalizedDateSelector;
501
+ createdBy?: string;
502
+ rating?: NormalizedNumericSelector;
503
+ votes?: NormalizedNumericSelector;
504
+ name?: string;
505
+ fullname?: string;
506
+ range?: "." | "before" | "after" | "others";
507
+ dataFormFields?: Record<string, string>;
508
+ order?: NormalizedOrder;
509
+ offset?: number;
510
+ limit?: number;
511
+ perPage?: number;
512
+ reverse?: boolean;
513
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ *
3
+ * URL parameter resolver for the `@URL|default` format in ListPages modules.
4
+ *
5
+ * Wikidot's ListPages module supports a dynamic parameter syntax where attribute
6
+ * values can be set to `@URL` or `@URL|default`. At render time, the actual value
7
+ * is read from the page's URL path parameters. This enables "HPC" (Hyper Page
8
+ * Changer) style multi-page content where a single page definition can display
9
+ * different data based on URL parameters.
10
+ *
11
+ * URL parameters follow the pattern `/key/value/key/value/...` in the URL path.
12
+ * When a `url-attr-prefix` is set (e.g., `"page2"`), parameter names are prefixed
13
+ * (e.g., `page2_offset`, `page2_limit`), allowing multiple independent ListPages
14
+ * modules on the same page.
15
+ *
16
+ * @example
17
+ * Wikidot markup:
18
+ * ```
19
+ * [[module ListPages offset="@URL|0" limit="@URL|10" url-attr-prefix="p2"]]
20
+ * ```
21
+ * URL: `/my-page/p2_offset/20/p2_limit/5`
22
+ * Result: offset=20, limit=5
23
+ *
24
+ * @module
25
+ */
26
+
27
+ import type { ListPagesDataRequirement, ListPagesQuery, NormalizedListPagesQuery } from "./types";
28
+ import { normalizeQuery } from "./normalize";
29
+
30
+ /**
31
+ * Mapping of module attribute names to their corresponding `ListPagesQuery` keys
32
+ * and expected value types. Only fields listed here support `@URL` resolution.
33
+ */
34
+ const URL_RESOLVABLE_FIELDS: ReadonlyArray<{
35
+ attr: string;
36
+ queryKey: keyof ListPagesQuery;
37
+ type: "string" | "number" | "boolean";
38
+ }> = [
39
+ { attr: "offset", queryKey: "offset", type: "number" },
40
+ { attr: "limit", queryKey: "limit", type: "number" },
41
+ { attr: "per-page", queryKey: "perPage", type: "number" },
42
+ { attr: "order", queryKey: "order", type: "string" },
43
+ { attr: "tags", queryKey: "tags", type: "string" },
44
+ { attr: "category", queryKey: "category", type: "string" },
45
+ { attr: "parent", queryKey: "parent", type: "string" },
46
+ { attr: "range", queryKey: "range", type: "string" },
47
+ { attr: "name", queryKey: "name", type: "string" },
48
+ { attr: "fullname", queryKey: "fullname", type: "string" },
49
+ { attr: "created-at", queryKey: "createdAt", type: "string" },
50
+ { attr: "updated-at", queryKey: "updatedAt", type: "string" },
51
+ { attr: "created-by", queryKey: "createdBy", type: "string" },
52
+ { attr: "rating", queryKey: "rating", type: "string" },
53
+ { attr: "votes", queryKey: "votes", type: "string" },
54
+ { attr: "reverse", queryKey: "reverse", type: "boolean" },
55
+ ];
56
+
57
+ /**
58
+ * Parse URL path parameters like /offset/1/page2_limit/1
59
+ * Returns a map of parameter name -> value
60
+ */
61
+ export function parseUrlParams(url: string): Map<string, string> {
62
+ const params = new Map<string, string>();
63
+ const parts = url.split("/").filter(Boolean);
64
+
65
+ // Skip the page name (first part), parse key/value pairs
66
+ for (let i = 1; i < parts.length - 1; i += 2) {
67
+ const key = parts[i];
68
+ const value = parts[i + 1];
69
+ if (key && value) {
70
+ params.set(key, value);
71
+ }
72
+ }
73
+
74
+ return params;
75
+ }
76
+
77
+ /**
78
+ * Resolve @URL|default format with actual URL parameters
79
+ *
80
+ * @param rawValue - The raw attribute value (e.g., "@URL|0")
81
+ * @param paramName - The parameter name (e.g., "offset")
82
+ * @param urlParams - Parsed URL parameters
83
+ * @param prefix - Optional URL attribute prefix (e.g., "page2")
84
+ * @returns Resolved value
85
+ */
86
+ export function resolveUrlValue(
87
+ rawValue: string | undefined,
88
+ paramName: string,
89
+ urlParams: Map<string, string>,
90
+ prefix?: string,
91
+ ): string | undefined {
92
+ if (!rawValue) return undefined;
93
+
94
+ // Check for @URL or @URL|default format
95
+ if (!rawValue.startsWith("@URL")) {
96
+ return rawValue;
97
+ }
98
+
99
+ // Extract default value if present
100
+ const defaultValue = rawValue.includes("|") ? rawValue.split("|")[1] : undefined;
101
+
102
+ // Build the actual parameter name with prefix
103
+ const actualParamName = prefix ? `${prefix}_${paramName}` : paramName;
104
+
105
+ // Get from URL params or use default
106
+ return urlParams.get(actualParamName) ?? defaultValue;
107
+ }
108
+
109
+ /**
110
+ * Resolve all `@URL` parameters and build a ListPagesQuery
111
+ *
112
+ * Takes a ListPagesDataRequirement and URL parameters, resolves all `@URL|default`
113
+ * values, and returns a complete ListPagesQuery ready for database queries.
114
+ *
115
+ * @param requirement - The data requirement from AST extraction
116
+ * @param urlParams - Parsed URL parameters (from parseUrlParams)
117
+ * @returns Resolved ListPagesQuery
118
+ *
119
+ * @example
120
+ * ```typescript
121
+ * const urlParams = parseUrlParams("/page/scp-001/page2_offset/10/page2_limit/5");
122
+ * const query = resolveQuery(requirement, urlParams);
123
+ * // query.offset = 10, query.limit = 5 (if urlAttrPrefix = "page2")
124
+ * ```
125
+ */
126
+ export function resolveQuery(
127
+ requirement: ListPagesDataRequirement,
128
+ urlParams: Map<string, string>,
129
+ ): ListPagesQuery {
130
+ const { query, rawAttributes, urlAttrPrefix } = requirement;
131
+ const resolved: ListPagesQuery = { ...query };
132
+
133
+ for (const field of URL_RESOLVABLE_FIELDS) {
134
+ const rawValue = rawAttributes[field.attr];
135
+ if (!rawValue) continue;
136
+
137
+ const resolvedValue = resolveUrlValue(rawValue, field.attr, urlParams, urlAttrPrefix);
138
+ if (resolvedValue === undefined) continue;
139
+
140
+ // Convert to appropriate type
141
+ switch (field.type) {
142
+ case "number": {
143
+ const num = parseInt(resolvedValue, 10);
144
+ if (!Number.isNaN(num)) {
145
+ (resolved as Record<string, unknown>)[field.queryKey] = num;
146
+ }
147
+ break;
148
+ }
149
+ case "boolean":
150
+ (resolved as Record<string, unknown>)[field.queryKey] =
151
+ resolvedValue === "true" || resolvedValue === "yes" || resolvedValue === "1";
152
+ break;
153
+ case "string":
154
+ default:
155
+ (resolved as Record<string, unknown>)[field.queryKey] = resolvedValue;
156
+ break;
157
+ }
158
+ }
159
+
160
+ return resolved;
161
+ }
162
+
163
+ /**
164
+ * Resolve all `@URL` parameters and normalize the query
165
+ *
166
+ * Combines URL resolution with query normalization in a single call.
167
+ * This is the recommended way to process ListPages queries for HPC.
168
+ *
169
+ * @param requirement - The data requirement from AST extraction
170
+ * @param urlParams - Parsed URL parameters (from parseUrlParams)
171
+ * @returns Normalized query with all `@URL` values resolved
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * const urlParams = parseUrlParams(window.location.pathname);
176
+ * const normalizedQuery = resolveAndNormalizeQuery(requirement, urlParams);
177
+ * // Ready for database query building
178
+ * ```
179
+ */
180
+ export function resolveAndNormalizeQuery(
181
+ requirement: ListPagesDataRequirement,
182
+ urlParams: Map<string, string>,
183
+ ): NormalizedListPagesQuery {
184
+ const resolved = resolveQuery(requirement, urlParams);
185
+ return normalizeQuery(resolved);
186
+ }