@wdprlib/ast 2.0.0 → 2.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 +446 -1
- package/dist/index.d.cts +84 -13
- package/dist/index.d.ts +84 -13
- package/dist/index.js +446 -1
- package/package.json +4 -2
- package/src/constants.ts +12 -0
- package/src/css.ts +83 -0
- package/src/diagnostic.ts +104 -0
- package/src/element.ts +1309 -0
- package/src/expr-eval.ts +586 -0
- package/src/index.ts +123 -0
- package/src/position.ts +73 -0
- package/src/settings.ts +157 -0
package/src/expr-eval.ts
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expression evaluator for Wikidot [[#expr]] and [[#ifexpr]]
|
|
3
|
+
*
|
|
4
|
+
* Supported:
|
|
5
|
+
* - Operators: +, -, *, /, % (modulo), ^ (power)
|
|
6
|
+
* - Comparison: <, >, <=, >=, =, !=
|
|
7
|
+
* - Logic: and, or, not
|
|
8
|
+
* - Functions: abs(), min(), max(), floor(), ceil(), round()
|
|
9
|
+
* - Parentheses for grouping
|
|
10
|
+
* - Negative numbers
|
|
11
|
+
*
|
|
12
|
+
* Expression limit: 256 characters (enforced by parser)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Set of string values considered falsy by Wikidot's `#if` construct.
|
|
17
|
+
* Case-insensitive after lowercasing and trimming.
|
|
18
|
+
*/
|
|
19
|
+
const FALSE_VALUES = new Set(["false", "null", "", "0"]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Determine whether a string value is truthy for Wikidot's `#if` construct.
|
|
23
|
+
*
|
|
24
|
+
* The value is lowercased and trimmed before checking against the set of
|
|
25
|
+
* known falsy strings (`"false"`, `"null"`, `""`, `"0"`).
|
|
26
|
+
*
|
|
27
|
+
* @param value - The condition string to check.
|
|
28
|
+
* @returns `true` if the value is not in the falsy set.
|
|
29
|
+
*/
|
|
30
|
+
export function isTruthy(value: string): boolean {
|
|
31
|
+
return !FALSE_VALUES.has(value.toLowerCase().trim());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Maximum allowed expression length (enforced by the parser, checked here as a safety net). */
|
|
35
|
+
const MAX_EXPRESSION_LENGTH = 256;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Determine whether a number is truthy for logical operators (`and`, `or`, `not`).
|
|
39
|
+
*
|
|
40
|
+
* 0 and `NaN` are falsy; all other finite and infinite values are truthy.
|
|
41
|
+
*
|
|
42
|
+
* @param n - The number to check.
|
|
43
|
+
* @returns `true` if the number is non-zero and not `NaN`.
|
|
44
|
+
*/
|
|
45
|
+
function isTruthyNum(n: number): boolean {
|
|
46
|
+
return n !== 0 && !Number.isNaN(n);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Result of evaluating a mathematical expression.
|
|
51
|
+
* Either a successful numeric value or an error message string.
|
|
52
|
+
*/
|
|
53
|
+
export type ExprResult = { success: true; value: number } | { success: false; error: string };
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format a numeric expression result for display.
|
|
57
|
+
*
|
|
58
|
+
* Uses JavaScript's default `String(n)` so the full precision of the
|
|
59
|
+
* computed value is preserved (e.g. `1/3` becomes `"0.3333333333333333"`,
|
|
60
|
+
* matching the `Number` → `String` conversion rather than truncating to
|
|
61
|
+
* a fixed number of decimals). Used by both the inline renderer and the
|
|
62
|
+
* opener preprocess so the same expression produces the same string
|
|
63
|
+
* regardless of where it appears in the source.
|
|
64
|
+
*/
|
|
65
|
+
export function formatExprValue(n: number): string {
|
|
66
|
+
return String(n);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Evaluate a mathematical expression string and return the result.
|
|
71
|
+
*
|
|
72
|
+
* The expression is tokenized, parsed with a recursive descent parser,
|
|
73
|
+
* and evaluated in a single pass. Errors produce Wikidot-compatible
|
|
74
|
+
* messages (e.g., `"division by zero"`, `"too many values in the stack"`).
|
|
75
|
+
*
|
|
76
|
+
* NaN and Infinity results are treated as division-by-zero errors.
|
|
77
|
+
*
|
|
78
|
+
* @param expr - The expression string to evaluate.
|
|
79
|
+
* @returns A success result with a numeric value, or an error result with a message.
|
|
80
|
+
*/
|
|
81
|
+
export function evaluateExpression(expr: string): ExprResult {
|
|
82
|
+
try {
|
|
83
|
+
// Enforce length limit
|
|
84
|
+
if (expr.length > MAX_EXPRESSION_LENGTH) {
|
|
85
|
+
return { success: false, error: "expression too long" };
|
|
86
|
+
}
|
|
87
|
+
if (expr.trim() === "") {
|
|
88
|
+
return { success: false, error: "empty expression" };
|
|
89
|
+
}
|
|
90
|
+
const tokens = tokenize(expr);
|
|
91
|
+
if (tokens.length <= 1) {
|
|
92
|
+
// Only EOF token
|
|
93
|
+
return { success: false, error: "empty expression" };
|
|
94
|
+
}
|
|
95
|
+
const parser = new ExprParser(tokens);
|
|
96
|
+
const result = parser.parse();
|
|
97
|
+
// Treat NaN and Infinity as division by zero (Wikidot-compatible)
|
|
98
|
+
if (!Number.isFinite(result)) {
|
|
99
|
+
return { success: false, error: "division by zero" };
|
|
100
|
+
}
|
|
101
|
+
return { success: true, value: result };
|
|
102
|
+
} catch (e) {
|
|
103
|
+
const msg = e instanceof Error ? e.message : "unknown error";
|
|
104
|
+
return { success: false, error: msg };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Discriminant for expression tokens. */
|
|
109
|
+
type TokenKind =
|
|
110
|
+
| "NUMBER"
|
|
111
|
+
| "IDENTIFIER"
|
|
112
|
+
| "PLUS"
|
|
113
|
+
| "MINUS"
|
|
114
|
+
| "STAR"
|
|
115
|
+
| "SLASH"
|
|
116
|
+
| "PERCENT"
|
|
117
|
+
| "CARET"
|
|
118
|
+
| "LPAREN"
|
|
119
|
+
| "RPAREN"
|
|
120
|
+
| "COMMA"
|
|
121
|
+
| "LT"
|
|
122
|
+
| "GT"
|
|
123
|
+
| "LE"
|
|
124
|
+
| "GE"
|
|
125
|
+
| "EQ"
|
|
126
|
+
| "NE"
|
|
127
|
+
| "BANG"
|
|
128
|
+
| "EOF";
|
|
129
|
+
|
|
130
|
+
/** A single token produced by the expression tokenizer. */
|
|
131
|
+
interface ExprToken {
|
|
132
|
+
/** The type of this token. */
|
|
133
|
+
kind: TokenKind;
|
|
134
|
+
/** Numeric value for NUMBER tokens; string representation for all others. */
|
|
135
|
+
value: string | number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Tokenize a mathematical expression string into a sequence of tokens.
|
|
140
|
+
*
|
|
141
|
+
* Handles numbers (including decimals), identifiers (function names and
|
|
142
|
+
* keywords like `and`/`or`/`not`), two-character operators (`<=`, `>=`,
|
|
143
|
+
* `!=`, `<>`), and single-character operators. Unknown characters throw
|
|
144
|
+
* an error.
|
|
145
|
+
*
|
|
146
|
+
* @param expr - The expression string to tokenize.
|
|
147
|
+
* @returns Array of tokens, always ending with an EOF token.
|
|
148
|
+
* @throws {Error} On encountering an unknown character or invalid number.
|
|
149
|
+
*/
|
|
150
|
+
function tokenize(expr: string): ExprToken[] {
|
|
151
|
+
const tokens: ExprToken[] = [];
|
|
152
|
+
let i = 0;
|
|
153
|
+
|
|
154
|
+
while (i < expr.length) {
|
|
155
|
+
const ch = expr[i]!;
|
|
156
|
+
|
|
157
|
+
// Skip whitespace
|
|
158
|
+
if (/\s/.test(ch)) {
|
|
159
|
+
i++;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Number (including decimals)
|
|
164
|
+
if (/\d/.test(ch) || (ch === "." && /\d/.test(expr[i + 1] ?? ""))) {
|
|
165
|
+
let numStr = "";
|
|
166
|
+
let hasDot = false;
|
|
167
|
+
while (i < expr.length) {
|
|
168
|
+
const c = expr[i]!;
|
|
169
|
+
if (c === ".") {
|
|
170
|
+
if (hasDot) break; // Only one decimal point allowed
|
|
171
|
+
hasDot = true;
|
|
172
|
+
} else if (!/\d/.test(c)) {
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
numStr += c;
|
|
176
|
+
i++;
|
|
177
|
+
}
|
|
178
|
+
const num = parseFloat(numStr);
|
|
179
|
+
if (!Number.isFinite(num)) {
|
|
180
|
+
throw new Error("Invalid number");
|
|
181
|
+
}
|
|
182
|
+
tokens.push({ kind: "NUMBER", value: num });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Identifier (function names, keywords like and/or/not)
|
|
187
|
+
if (/[a-zA-Z_]/.test(ch)) {
|
|
188
|
+
let id = "";
|
|
189
|
+
while (i < expr.length) {
|
|
190
|
+
const c = expr[i]!;
|
|
191
|
+
if (!/[a-zA-Z0-9_]/.test(c)) break;
|
|
192
|
+
id += c;
|
|
193
|
+
i++;
|
|
194
|
+
}
|
|
195
|
+
tokens.push({ kind: "IDENTIFIER", value: id.toLowerCase() });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Two-character operators
|
|
200
|
+
if (ch === "<" && expr[i + 1] === "=") {
|
|
201
|
+
tokens.push({ kind: "LE", value: "<=" });
|
|
202
|
+
i += 2;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (ch === ">" && expr[i + 1] === "=") {
|
|
206
|
+
tokens.push({ kind: "GE", value: ">=" });
|
|
207
|
+
i += 2;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (ch === "!" && expr[i + 1] === "=") {
|
|
211
|
+
tokens.push({ kind: "NE", value: "!=" });
|
|
212
|
+
i += 2;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (ch === "<" && expr[i + 1] === ">") {
|
|
216
|
+
tokens.push({ kind: "NE", value: "<>" });
|
|
217
|
+
i += 2;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Unary logical NOT (Wikidot's `!` operator). The two-character `!=`
|
|
222
|
+
// has already been handled above, so a bare `!` here is always
|
|
223
|
+
// unary not.
|
|
224
|
+
if (ch === "!") {
|
|
225
|
+
tokens.push({ kind: "BANG", value: "!" });
|
|
226
|
+
i++;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Single-character operators
|
|
231
|
+
switch (ch) {
|
|
232
|
+
case "+":
|
|
233
|
+
tokens.push({ kind: "PLUS", value: "+" });
|
|
234
|
+
break;
|
|
235
|
+
case "-":
|
|
236
|
+
tokens.push({ kind: "MINUS", value: "-" });
|
|
237
|
+
break;
|
|
238
|
+
case "*":
|
|
239
|
+
tokens.push({ kind: "STAR", value: "*" });
|
|
240
|
+
break;
|
|
241
|
+
case "/":
|
|
242
|
+
tokens.push({ kind: "SLASH", value: "/" });
|
|
243
|
+
break;
|
|
244
|
+
case "%":
|
|
245
|
+
tokens.push({ kind: "PERCENT", value: "%" });
|
|
246
|
+
break;
|
|
247
|
+
case "^":
|
|
248
|
+
tokens.push({ kind: "CARET", value: "^" });
|
|
249
|
+
break;
|
|
250
|
+
case "(":
|
|
251
|
+
tokens.push({ kind: "LPAREN", value: "(" });
|
|
252
|
+
break;
|
|
253
|
+
case ")":
|
|
254
|
+
tokens.push({ kind: "RPAREN", value: ")" });
|
|
255
|
+
break;
|
|
256
|
+
case ",":
|
|
257
|
+
tokens.push({ kind: "COMMA", value: "," });
|
|
258
|
+
break;
|
|
259
|
+
case "<":
|
|
260
|
+
tokens.push({ kind: "LT", value: "<" });
|
|
261
|
+
break;
|
|
262
|
+
case ">":
|
|
263
|
+
tokens.push({ kind: "GT", value: ">" });
|
|
264
|
+
break;
|
|
265
|
+
case "=":
|
|
266
|
+
tokens.push({ kind: "EQ", value: "=" });
|
|
267
|
+
break;
|
|
268
|
+
default:
|
|
269
|
+
// Unknown character is an error
|
|
270
|
+
throw new Error(`Unknown character: ${ch}`);
|
|
271
|
+
}
|
|
272
|
+
i++;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
tokens.push({ kind: "EOF", value: "" });
|
|
276
|
+
return tokens;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Recursive descent parser for expressions
|
|
281
|
+
* Precedence (low to high):
|
|
282
|
+
* or
|
|
283
|
+
* and
|
|
284
|
+
* not (unary)
|
|
285
|
+
* comparison (<, >, <=, >=, =, !=)
|
|
286
|
+
* addition (+, -)
|
|
287
|
+
* multiplication (*, /, %)
|
|
288
|
+
* power (^)
|
|
289
|
+
* unary (-, +)
|
|
290
|
+
* primary (number, parentheses, function call)
|
|
291
|
+
*/
|
|
292
|
+
class ExprParser {
|
|
293
|
+
private pos = 0;
|
|
294
|
+
|
|
295
|
+
constructor(private tokens: ExprToken[]) {}
|
|
296
|
+
|
|
297
|
+
parse(): number {
|
|
298
|
+
const result = this.parseOr();
|
|
299
|
+
if (this.current().kind !== "EOF") {
|
|
300
|
+
// Wikidot-compatible error message when extra values remain
|
|
301
|
+
throw new Error("too many values in the stack");
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private current(): ExprToken {
|
|
307
|
+
return this.tokens[this.pos] ?? { kind: "EOF", value: "" };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private advance(): ExprToken {
|
|
311
|
+
const token = this.current();
|
|
312
|
+
this.pos++;
|
|
313
|
+
return token;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private parseOr(): number {
|
|
317
|
+
let left = this.parseAnd();
|
|
318
|
+
|
|
319
|
+
while (this.current().kind === "IDENTIFIER" && this.current().value === "or") {
|
|
320
|
+
this.advance();
|
|
321
|
+
const right = this.parseAnd();
|
|
322
|
+
// Treat 0 and NaN as falsy
|
|
323
|
+
left = isTruthyNum(left) || isTruthyNum(right) ? 1 : 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return left;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private parseAnd(): number {
|
|
330
|
+
let left = this.parseNot();
|
|
331
|
+
|
|
332
|
+
while (this.current().kind === "IDENTIFIER" && this.current().value === "and") {
|
|
333
|
+
this.advance();
|
|
334
|
+
const right = this.parseNot();
|
|
335
|
+
// Treat 0 and NaN as falsy
|
|
336
|
+
left = isTruthyNum(left) && isTruthyNum(right) ? 1 : 0;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return left;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private parseNot(): number {
|
|
343
|
+
const cur = this.current();
|
|
344
|
+
if ((cur.kind === "IDENTIFIER" && cur.value === "not") || cur.kind === "BANG") {
|
|
345
|
+
this.advance();
|
|
346
|
+
const value = this.parseNot();
|
|
347
|
+
// Treat 0 and NaN as falsy
|
|
348
|
+
return isTruthyNum(value) ? 0 : 1;
|
|
349
|
+
}
|
|
350
|
+
return this.parseComparison();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private parseComparison(): number {
|
|
354
|
+
let left = this.parseAddition();
|
|
355
|
+
|
|
356
|
+
const kind = this.current().kind;
|
|
357
|
+
if (
|
|
358
|
+
kind === "LT" ||
|
|
359
|
+
kind === "GT" ||
|
|
360
|
+
kind === "LE" ||
|
|
361
|
+
kind === "GE" ||
|
|
362
|
+
kind === "EQ" ||
|
|
363
|
+
kind === "NE"
|
|
364
|
+
) {
|
|
365
|
+
this.advance();
|
|
366
|
+
const right = this.parseAddition();
|
|
367
|
+
|
|
368
|
+
switch (kind) {
|
|
369
|
+
case "LT":
|
|
370
|
+
return left < right ? 1 : 0;
|
|
371
|
+
case "GT":
|
|
372
|
+
return left > right ? 1 : 0;
|
|
373
|
+
case "LE":
|
|
374
|
+
return left <= right ? 1 : 0;
|
|
375
|
+
case "GE":
|
|
376
|
+
return left >= right ? 1 : 0;
|
|
377
|
+
case "EQ":
|
|
378
|
+
return left === right ? 1 : 0;
|
|
379
|
+
case "NE":
|
|
380
|
+
return left !== right ? 1 : 0;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return left;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private parseAddition(): number {
|
|
388
|
+
let left = this.parseMultiplication();
|
|
389
|
+
|
|
390
|
+
while (true) {
|
|
391
|
+
const kind = this.current().kind;
|
|
392
|
+
if (kind === "PLUS") {
|
|
393
|
+
this.advance();
|
|
394
|
+
left = left + this.parseMultiplication();
|
|
395
|
+
} else if (kind === "MINUS") {
|
|
396
|
+
this.advance();
|
|
397
|
+
left = left - this.parseMultiplication();
|
|
398
|
+
} else {
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return left;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private parseMultiplication(): number {
|
|
407
|
+
let left = this.parsePower();
|
|
408
|
+
|
|
409
|
+
while (true) {
|
|
410
|
+
const kind = this.current().kind;
|
|
411
|
+
if (kind === "STAR") {
|
|
412
|
+
this.advance();
|
|
413
|
+
left = left * this.parsePower();
|
|
414
|
+
} else if (kind === "SLASH") {
|
|
415
|
+
this.advance();
|
|
416
|
+
left = left / this.parsePower();
|
|
417
|
+
} else if (kind === "PERCENT") {
|
|
418
|
+
this.advance();
|
|
419
|
+
left = left % this.parsePower();
|
|
420
|
+
} else {
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return left;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private parsePower(): number {
|
|
429
|
+
const left = this.parseUnary();
|
|
430
|
+
|
|
431
|
+
if (this.current().kind === "CARET") {
|
|
432
|
+
this.advance();
|
|
433
|
+
// Right-associative
|
|
434
|
+
const right = this.parsePower();
|
|
435
|
+
return Math.pow(left, right);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return left;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private parseUnary(): number {
|
|
442
|
+
const kind = this.current().kind;
|
|
443
|
+
|
|
444
|
+
if (kind === "MINUS") {
|
|
445
|
+
this.advance();
|
|
446
|
+
return -this.parseUnary();
|
|
447
|
+
}
|
|
448
|
+
if (kind === "PLUS") {
|
|
449
|
+
this.advance();
|
|
450
|
+
return +this.parseUnary();
|
|
451
|
+
}
|
|
452
|
+
// `!` here applies when the parser descends past the top-level
|
|
453
|
+
// `parseNot` (e.g. on the RHS of a comparison: `a != !(b)`).
|
|
454
|
+
if (kind === "BANG") {
|
|
455
|
+
this.advance();
|
|
456
|
+
const value = this.parseUnary();
|
|
457
|
+
return isTruthyNum(value) ? 0 : 1;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return this.parsePrimary();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private parsePrimary(): number {
|
|
464
|
+
const token = this.current();
|
|
465
|
+
|
|
466
|
+
if (token.kind === "NUMBER") {
|
|
467
|
+
this.advance();
|
|
468
|
+
return token.value as number;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (token.kind === "LPAREN") {
|
|
472
|
+
this.advance();
|
|
473
|
+
const value = this.parseOr();
|
|
474
|
+
if (this.current().kind !== "RPAREN") {
|
|
475
|
+
throw new Error("Expected )");
|
|
476
|
+
}
|
|
477
|
+
this.advance();
|
|
478
|
+
return value;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (token.kind === "IDENTIFIER") {
|
|
482
|
+
const name = token.value as string;
|
|
483
|
+
this.advance();
|
|
484
|
+
|
|
485
|
+
// Function call
|
|
486
|
+
if (this.current().kind === "LPAREN") {
|
|
487
|
+
return this.parseFunctionCall(name);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Wikidot accepts `true` / `false` as boolean literals inside
|
|
491
|
+
// `[[#expr]]` / `[[#ifexpr]]` (lowercased above by the tokenizer).
|
|
492
|
+
if (name === "true") return 1;
|
|
493
|
+
if (name === "false") return 0;
|
|
494
|
+
|
|
495
|
+
throw new Error(`undefined constant "${name}"`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
throw new Error("Expected expression");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private parseFunctionCall(name: string): number {
|
|
502
|
+
if (this.current().kind !== "LPAREN") {
|
|
503
|
+
throw new Error("Expected (");
|
|
504
|
+
}
|
|
505
|
+
this.advance();
|
|
506
|
+
|
|
507
|
+
const args: number[] = [];
|
|
508
|
+
|
|
509
|
+
if (this.current().kind !== "RPAREN") {
|
|
510
|
+
args.push(this.parseOr());
|
|
511
|
+
|
|
512
|
+
while (this.current().kind === "COMMA") {
|
|
513
|
+
this.advance();
|
|
514
|
+
args.push(this.parseOr());
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (this.current().kind !== "RPAREN") {
|
|
519
|
+
throw new Error("Expected )");
|
|
520
|
+
}
|
|
521
|
+
this.advance();
|
|
522
|
+
|
|
523
|
+
return this.callFunction(name, args);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private callFunction(name: string, args: number[]): number {
|
|
527
|
+
switch (name) {
|
|
528
|
+
case "abs":
|
|
529
|
+
this.checkArgs(name, args, 1);
|
|
530
|
+
return Math.abs(args[0]!);
|
|
531
|
+
case "min":
|
|
532
|
+
this.checkArgsMin(name, args, 1);
|
|
533
|
+
return Math.min(...args);
|
|
534
|
+
case "max":
|
|
535
|
+
this.checkArgsMin(name, args, 1);
|
|
536
|
+
return Math.max(...args);
|
|
537
|
+
case "floor":
|
|
538
|
+
this.checkArgs(name, args, 1);
|
|
539
|
+
return Math.floor(args[0]!);
|
|
540
|
+
case "ceil":
|
|
541
|
+
this.checkArgs(name, args, 1);
|
|
542
|
+
return Math.ceil(args[0]!);
|
|
543
|
+
case "round":
|
|
544
|
+
this.checkArgs(name, args, 1);
|
|
545
|
+
return Math.round(args[0]!);
|
|
546
|
+
case "sqrt":
|
|
547
|
+
this.checkArgs(name, args, 1);
|
|
548
|
+
return Math.sqrt(args[0]!);
|
|
549
|
+
case "sin":
|
|
550
|
+
this.checkArgs(name, args, 1);
|
|
551
|
+
return Math.sin(args[0]!);
|
|
552
|
+
case "cos":
|
|
553
|
+
this.checkArgs(name, args, 1);
|
|
554
|
+
return Math.cos(args[0]!);
|
|
555
|
+
case "tan":
|
|
556
|
+
this.checkArgs(name, args, 1);
|
|
557
|
+
return Math.tan(args[0]!);
|
|
558
|
+
case "ln":
|
|
559
|
+
this.checkArgs(name, args, 1);
|
|
560
|
+
return Math.log(args[0]!);
|
|
561
|
+
case "log":
|
|
562
|
+
this.checkArgs(name, args, 1);
|
|
563
|
+
return Math.log10(args[0]!);
|
|
564
|
+
case "exp":
|
|
565
|
+
this.checkArgs(name, args, 1);
|
|
566
|
+
return Math.exp(args[0]!);
|
|
567
|
+
case "pow":
|
|
568
|
+
this.checkArgs(name, args, 2);
|
|
569
|
+
return Math.pow(args[0]!, args[1]!);
|
|
570
|
+
default:
|
|
571
|
+
throw new Error(`undefined function "${name}"`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private checkArgs(name: string, args: number[], expected: number): void {
|
|
576
|
+
if (args.length !== expected) {
|
|
577
|
+
throw new Error(`${name}() expects ${expected} argument(s), got ${args.length}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private checkArgsMin(name: string, args: number[], min: number): void {
|
|
582
|
+
if (args.length < min) {
|
|
583
|
+
throw new Error(`${name}() expects at least ${min} argument(s), got ${args.length}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST type definitions for the Wikidot markup parser.
|
|
3
|
+
*
|
|
4
|
+
* This package provides the TypeScript types that describe the abstract
|
|
5
|
+
* syntax tree (AST) produced by `@wdprlib/parser` and consumed by
|
|
6
|
+
* `@wdprlib/render`. It also exports factory helpers for constructing
|
|
7
|
+
* common node types and context-dependent settings for controlling
|
|
8
|
+
* parser/renderer behaviour.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type { Position, Point } from "./position";
|
|
14
|
+
export { createPoint, createPosition } from "./position";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Identifies the source markup dialect.
|
|
18
|
+
*
|
|
19
|
+
* Currently only `"wikidot"` is supported. Included in {@link SyntaxTree}
|
|
20
|
+
* so consumers can branch on the dialect if other formats are added later.
|
|
21
|
+
*
|
|
22
|
+
* @group Core
|
|
23
|
+
*/
|
|
24
|
+
export type Version = "wikidot";
|
|
25
|
+
|
|
26
|
+
// Element types (output AST)
|
|
27
|
+
export type {
|
|
28
|
+
Element,
|
|
29
|
+
ElementName,
|
|
30
|
+
ElementData,
|
|
31
|
+
ElementOf,
|
|
32
|
+
ElementDataMap,
|
|
33
|
+
SyntaxTree,
|
|
34
|
+
ContainerType,
|
|
35
|
+
StringContainerType,
|
|
36
|
+
ContainerData,
|
|
37
|
+
AttributeMap,
|
|
38
|
+
VariableMap,
|
|
39
|
+
Alignment,
|
|
40
|
+
LinkType,
|
|
41
|
+
LinkLocation,
|
|
42
|
+
LinkLabel,
|
|
43
|
+
PageRef,
|
|
44
|
+
ImageSource,
|
|
45
|
+
FloatAlignment,
|
|
46
|
+
ListType,
|
|
47
|
+
ListItem,
|
|
48
|
+
ListData,
|
|
49
|
+
CodeBlockData,
|
|
50
|
+
TabData,
|
|
51
|
+
TableCell,
|
|
52
|
+
TableRow,
|
|
53
|
+
TableData,
|
|
54
|
+
DefinitionListItem,
|
|
55
|
+
Module,
|
|
56
|
+
CollapsibleData,
|
|
57
|
+
ClearFloat,
|
|
58
|
+
AnchorTarget,
|
|
59
|
+
HeaderType,
|
|
60
|
+
AlignType,
|
|
61
|
+
HeadingLevel,
|
|
62
|
+
Heading,
|
|
63
|
+
DateItem,
|
|
64
|
+
Embed,
|
|
65
|
+
EmbedBlockData,
|
|
66
|
+
TocEntry,
|
|
67
|
+
AnchorData,
|
|
68
|
+
LinkData,
|
|
69
|
+
ImageData,
|
|
70
|
+
TableOfContentsData,
|
|
71
|
+
FootnoteBlockData,
|
|
72
|
+
BibliographyCiteData,
|
|
73
|
+
BibliographyBlockData,
|
|
74
|
+
UserData,
|
|
75
|
+
DateData,
|
|
76
|
+
ColorData,
|
|
77
|
+
MathData,
|
|
78
|
+
MathInlineData,
|
|
79
|
+
HtmlData,
|
|
80
|
+
IframeData,
|
|
81
|
+
IncludeData,
|
|
82
|
+
IfTagsData,
|
|
83
|
+
ExprData,
|
|
84
|
+
IfCondData,
|
|
85
|
+
IfExprData,
|
|
86
|
+
} from "./element";
|
|
87
|
+
export {
|
|
88
|
+
text,
|
|
89
|
+
container,
|
|
90
|
+
paragraph,
|
|
91
|
+
bold,
|
|
92
|
+
italics,
|
|
93
|
+
heading,
|
|
94
|
+
lineBreak,
|
|
95
|
+
horizontalRule,
|
|
96
|
+
link,
|
|
97
|
+
list,
|
|
98
|
+
listItemElements,
|
|
99
|
+
listItemSubList,
|
|
100
|
+
isStringContainerType,
|
|
101
|
+
isHeaderType,
|
|
102
|
+
isAlignType,
|
|
103
|
+
isContainerTypeParagraphSafe,
|
|
104
|
+
isParagraphSafe,
|
|
105
|
+
} from "./element";
|
|
106
|
+
|
|
107
|
+
// Diagnostics
|
|
108
|
+
export type { Diagnostic, DiagnosticSeverity, ParseResult } from "./diagnostic";
|
|
109
|
+
|
|
110
|
+
// Constants
|
|
111
|
+
export { STYLE_SLOT_PREFIX } from "./constants";
|
|
112
|
+
|
|
113
|
+
// CSS value definitions
|
|
114
|
+
export type { CssLengthUnit } from "./css";
|
|
115
|
+
export { CSS_LENGTH_UNITS } from "./css";
|
|
116
|
+
|
|
117
|
+
// Wikitext settings
|
|
118
|
+
export type { WikitextMode, WikitextSettings } from "./settings";
|
|
119
|
+
|
|
120
|
+
// Expression evaluator (shared by parser preprocess and render).
|
|
121
|
+
export { evaluateExpression, isTruthy, formatExprValue } from "./expr-eval";
|
|
122
|
+
export type { ExprResult } from "./expr-eval";
|
|
123
|
+
export { createSettings, DEFAULT_SETTINGS } from "./settings";
|