@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,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
|
+
}
|