@wdprlib/ast 2.0.0 → 2.1.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 +391 -0
- package/dist/index.d.cts +46 -1
- package/dist/index.d.ts +46 -1
- package/dist/index.js +391 -0
- package/package.json +4 -2
- package/src/constants.ts +12 -0
- package/src/diagnostic.ts +104 -0
- package/src/element.ts +1287 -0
- package/src/expr-eval.ts +586 -0
- package/src/index.ts +119 -0
- package/src/position.ts +73 -0
- package/src/settings.ts +157 -0
package/dist/index.js
CHANGED
|
@@ -193,6 +193,394 @@ function isParagraphSafe(element) {
|
|
|
193
193
|
}
|
|
194
194
|
// packages/ast/src/constants.ts
|
|
195
195
|
var STYLE_SLOT_PREFIX = "\x00__IFTAGS_SLOT__";
|
|
196
|
+
// packages/ast/src/expr-eval.ts
|
|
197
|
+
var FALSE_VALUES = new Set(["false", "null", "", "0"]);
|
|
198
|
+
function isTruthy(value) {
|
|
199
|
+
return !FALSE_VALUES.has(value.toLowerCase().trim());
|
|
200
|
+
}
|
|
201
|
+
var MAX_EXPRESSION_LENGTH = 256;
|
|
202
|
+
function isTruthyNum(n) {
|
|
203
|
+
return n !== 0 && !Number.isNaN(n);
|
|
204
|
+
}
|
|
205
|
+
function formatExprValue(n) {
|
|
206
|
+
return String(n);
|
|
207
|
+
}
|
|
208
|
+
function evaluateExpression(expr) {
|
|
209
|
+
try {
|
|
210
|
+
if (expr.length > MAX_EXPRESSION_LENGTH) {
|
|
211
|
+
return { success: false, error: "expression too long" };
|
|
212
|
+
}
|
|
213
|
+
if (expr.trim() === "") {
|
|
214
|
+
return { success: false, error: "empty expression" };
|
|
215
|
+
}
|
|
216
|
+
const tokens = tokenize(expr);
|
|
217
|
+
if (tokens.length <= 1) {
|
|
218
|
+
return { success: false, error: "empty expression" };
|
|
219
|
+
}
|
|
220
|
+
const parser = new ExprParser(tokens);
|
|
221
|
+
const result = parser.parse();
|
|
222
|
+
if (!Number.isFinite(result)) {
|
|
223
|
+
return { success: false, error: "division by zero" };
|
|
224
|
+
}
|
|
225
|
+
return { success: true, value: result };
|
|
226
|
+
} catch (e) {
|
|
227
|
+
const msg = e instanceof Error ? e.message : "unknown error";
|
|
228
|
+
return { success: false, error: msg };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function tokenize(expr) {
|
|
232
|
+
const tokens = [];
|
|
233
|
+
let i = 0;
|
|
234
|
+
while (i < expr.length) {
|
|
235
|
+
const ch = expr[i];
|
|
236
|
+
if (/\s/.test(ch)) {
|
|
237
|
+
i++;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (/\d/.test(ch) || ch === "." && /\d/.test(expr[i + 1] ?? "")) {
|
|
241
|
+
let numStr = "";
|
|
242
|
+
let hasDot = false;
|
|
243
|
+
while (i < expr.length) {
|
|
244
|
+
const c = expr[i];
|
|
245
|
+
if (c === ".") {
|
|
246
|
+
if (hasDot)
|
|
247
|
+
break;
|
|
248
|
+
hasDot = true;
|
|
249
|
+
} else if (!/\d/.test(c)) {
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
numStr += c;
|
|
253
|
+
i++;
|
|
254
|
+
}
|
|
255
|
+
const num = parseFloat(numStr);
|
|
256
|
+
if (!Number.isFinite(num)) {
|
|
257
|
+
throw new Error("Invalid number");
|
|
258
|
+
}
|
|
259
|
+
tokens.push({ kind: "NUMBER", value: num });
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (/[a-zA-Z_]/.test(ch)) {
|
|
263
|
+
let id = "";
|
|
264
|
+
while (i < expr.length) {
|
|
265
|
+
const c = expr[i];
|
|
266
|
+
if (!/[a-zA-Z0-9_]/.test(c))
|
|
267
|
+
break;
|
|
268
|
+
id += c;
|
|
269
|
+
i++;
|
|
270
|
+
}
|
|
271
|
+
tokens.push({ kind: "IDENTIFIER", value: id.toLowerCase() });
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (ch === "<" && expr[i + 1] === "=") {
|
|
275
|
+
tokens.push({ kind: "LE", value: "<=" });
|
|
276
|
+
i += 2;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (ch === ">" && expr[i + 1] === "=") {
|
|
280
|
+
tokens.push({ kind: "GE", value: ">=" });
|
|
281
|
+
i += 2;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (ch === "!" && expr[i + 1] === "=") {
|
|
285
|
+
tokens.push({ kind: "NE", value: "!=" });
|
|
286
|
+
i += 2;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (ch === "<" && expr[i + 1] === ">") {
|
|
290
|
+
tokens.push({ kind: "NE", value: "<>" });
|
|
291
|
+
i += 2;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (ch === "!") {
|
|
295
|
+
tokens.push({ kind: "BANG", value: "!" });
|
|
296
|
+
i++;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
switch (ch) {
|
|
300
|
+
case "+":
|
|
301
|
+
tokens.push({ kind: "PLUS", value: "+" });
|
|
302
|
+
break;
|
|
303
|
+
case "-":
|
|
304
|
+
tokens.push({ kind: "MINUS", value: "-" });
|
|
305
|
+
break;
|
|
306
|
+
case "*":
|
|
307
|
+
tokens.push({ kind: "STAR", value: "*" });
|
|
308
|
+
break;
|
|
309
|
+
case "/":
|
|
310
|
+
tokens.push({ kind: "SLASH", value: "/" });
|
|
311
|
+
break;
|
|
312
|
+
case "%":
|
|
313
|
+
tokens.push({ kind: "PERCENT", value: "%" });
|
|
314
|
+
break;
|
|
315
|
+
case "^":
|
|
316
|
+
tokens.push({ kind: "CARET", value: "^" });
|
|
317
|
+
break;
|
|
318
|
+
case "(":
|
|
319
|
+
tokens.push({ kind: "LPAREN", value: "(" });
|
|
320
|
+
break;
|
|
321
|
+
case ")":
|
|
322
|
+
tokens.push({ kind: "RPAREN", value: ")" });
|
|
323
|
+
break;
|
|
324
|
+
case ",":
|
|
325
|
+
tokens.push({ kind: "COMMA", value: "," });
|
|
326
|
+
break;
|
|
327
|
+
case "<":
|
|
328
|
+
tokens.push({ kind: "LT", value: "<" });
|
|
329
|
+
break;
|
|
330
|
+
case ">":
|
|
331
|
+
tokens.push({ kind: "GT", value: ">" });
|
|
332
|
+
break;
|
|
333
|
+
case "=":
|
|
334
|
+
tokens.push({ kind: "EQ", value: "=" });
|
|
335
|
+
break;
|
|
336
|
+
default:
|
|
337
|
+
throw new Error(`Unknown character: ${ch}`);
|
|
338
|
+
}
|
|
339
|
+
i++;
|
|
340
|
+
}
|
|
341
|
+
tokens.push({ kind: "EOF", value: "" });
|
|
342
|
+
return tokens;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
class ExprParser {
|
|
346
|
+
tokens;
|
|
347
|
+
pos = 0;
|
|
348
|
+
constructor(tokens) {
|
|
349
|
+
this.tokens = tokens;
|
|
350
|
+
}
|
|
351
|
+
parse() {
|
|
352
|
+
const result = this.parseOr();
|
|
353
|
+
if (this.current().kind !== "EOF") {
|
|
354
|
+
throw new Error("too many values in the stack");
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
current() {
|
|
359
|
+
return this.tokens[this.pos] ?? { kind: "EOF", value: "" };
|
|
360
|
+
}
|
|
361
|
+
advance() {
|
|
362
|
+
const token = this.current();
|
|
363
|
+
this.pos++;
|
|
364
|
+
return token;
|
|
365
|
+
}
|
|
366
|
+
parseOr() {
|
|
367
|
+
let left = this.parseAnd();
|
|
368
|
+
while (this.current().kind === "IDENTIFIER" && this.current().value === "or") {
|
|
369
|
+
this.advance();
|
|
370
|
+
const right = this.parseAnd();
|
|
371
|
+
left = isTruthyNum(left) || isTruthyNum(right) ? 1 : 0;
|
|
372
|
+
}
|
|
373
|
+
return left;
|
|
374
|
+
}
|
|
375
|
+
parseAnd() {
|
|
376
|
+
let left = this.parseNot();
|
|
377
|
+
while (this.current().kind === "IDENTIFIER" && this.current().value === "and") {
|
|
378
|
+
this.advance();
|
|
379
|
+
const right = this.parseNot();
|
|
380
|
+
left = isTruthyNum(left) && isTruthyNum(right) ? 1 : 0;
|
|
381
|
+
}
|
|
382
|
+
return left;
|
|
383
|
+
}
|
|
384
|
+
parseNot() {
|
|
385
|
+
const cur = this.current();
|
|
386
|
+
if (cur.kind === "IDENTIFIER" && cur.value === "not" || cur.kind === "BANG") {
|
|
387
|
+
this.advance();
|
|
388
|
+
const value = this.parseNot();
|
|
389
|
+
return isTruthyNum(value) ? 0 : 1;
|
|
390
|
+
}
|
|
391
|
+
return this.parseComparison();
|
|
392
|
+
}
|
|
393
|
+
parseComparison() {
|
|
394
|
+
let left = this.parseAddition();
|
|
395
|
+
const kind = this.current().kind;
|
|
396
|
+
if (kind === "LT" || kind === "GT" || kind === "LE" || kind === "GE" || kind === "EQ" || kind === "NE") {
|
|
397
|
+
this.advance();
|
|
398
|
+
const right = this.parseAddition();
|
|
399
|
+
switch (kind) {
|
|
400
|
+
case "LT":
|
|
401
|
+
return left < right ? 1 : 0;
|
|
402
|
+
case "GT":
|
|
403
|
+
return left > right ? 1 : 0;
|
|
404
|
+
case "LE":
|
|
405
|
+
return left <= right ? 1 : 0;
|
|
406
|
+
case "GE":
|
|
407
|
+
return left >= right ? 1 : 0;
|
|
408
|
+
case "EQ":
|
|
409
|
+
return left === right ? 1 : 0;
|
|
410
|
+
case "NE":
|
|
411
|
+
return left !== right ? 1 : 0;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return left;
|
|
415
|
+
}
|
|
416
|
+
parseAddition() {
|
|
417
|
+
let left = this.parseMultiplication();
|
|
418
|
+
while (true) {
|
|
419
|
+
const kind = this.current().kind;
|
|
420
|
+
if (kind === "PLUS") {
|
|
421
|
+
this.advance();
|
|
422
|
+
left = left + this.parseMultiplication();
|
|
423
|
+
} else if (kind === "MINUS") {
|
|
424
|
+
this.advance();
|
|
425
|
+
left = left - this.parseMultiplication();
|
|
426
|
+
} else {
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return left;
|
|
431
|
+
}
|
|
432
|
+
parseMultiplication() {
|
|
433
|
+
let left = this.parsePower();
|
|
434
|
+
while (true) {
|
|
435
|
+
const kind = this.current().kind;
|
|
436
|
+
if (kind === "STAR") {
|
|
437
|
+
this.advance();
|
|
438
|
+
left = left * this.parsePower();
|
|
439
|
+
} else if (kind === "SLASH") {
|
|
440
|
+
this.advance();
|
|
441
|
+
left = left / this.parsePower();
|
|
442
|
+
} else if (kind === "PERCENT") {
|
|
443
|
+
this.advance();
|
|
444
|
+
left = left % this.parsePower();
|
|
445
|
+
} else {
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return left;
|
|
450
|
+
}
|
|
451
|
+
parsePower() {
|
|
452
|
+
const left = this.parseUnary();
|
|
453
|
+
if (this.current().kind === "CARET") {
|
|
454
|
+
this.advance();
|
|
455
|
+
const right = this.parsePower();
|
|
456
|
+
return Math.pow(left, right);
|
|
457
|
+
}
|
|
458
|
+
return left;
|
|
459
|
+
}
|
|
460
|
+
parseUnary() {
|
|
461
|
+
const kind = this.current().kind;
|
|
462
|
+
if (kind === "MINUS") {
|
|
463
|
+
this.advance();
|
|
464
|
+
return -this.parseUnary();
|
|
465
|
+
}
|
|
466
|
+
if (kind === "PLUS") {
|
|
467
|
+
this.advance();
|
|
468
|
+
return +this.parseUnary();
|
|
469
|
+
}
|
|
470
|
+
if (kind === "BANG") {
|
|
471
|
+
this.advance();
|
|
472
|
+
const value = this.parseUnary();
|
|
473
|
+
return isTruthyNum(value) ? 0 : 1;
|
|
474
|
+
}
|
|
475
|
+
return this.parsePrimary();
|
|
476
|
+
}
|
|
477
|
+
parsePrimary() {
|
|
478
|
+
const token = this.current();
|
|
479
|
+
if (token.kind === "NUMBER") {
|
|
480
|
+
this.advance();
|
|
481
|
+
return token.value;
|
|
482
|
+
}
|
|
483
|
+
if (token.kind === "LPAREN") {
|
|
484
|
+
this.advance();
|
|
485
|
+
const value = this.parseOr();
|
|
486
|
+
if (this.current().kind !== "RPAREN") {
|
|
487
|
+
throw new Error("Expected )");
|
|
488
|
+
}
|
|
489
|
+
this.advance();
|
|
490
|
+
return value;
|
|
491
|
+
}
|
|
492
|
+
if (token.kind === "IDENTIFIER") {
|
|
493
|
+
const name = token.value;
|
|
494
|
+
this.advance();
|
|
495
|
+
if (this.current().kind === "LPAREN") {
|
|
496
|
+
return this.parseFunctionCall(name);
|
|
497
|
+
}
|
|
498
|
+
if (name === "true")
|
|
499
|
+
return 1;
|
|
500
|
+
if (name === "false")
|
|
501
|
+
return 0;
|
|
502
|
+
throw new Error(`undefined constant "${name}"`);
|
|
503
|
+
}
|
|
504
|
+
throw new Error("Expected expression");
|
|
505
|
+
}
|
|
506
|
+
parseFunctionCall(name) {
|
|
507
|
+
if (this.current().kind !== "LPAREN") {
|
|
508
|
+
throw new Error("Expected (");
|
|
509
|
+
}
|
|
510
|
+
this.advance();
|
|
511
|
+
const args = [];
|
|
512
|
+
if (this.current().kind !== "RPAREN") {
|
|
513
|
+
args.push(this.parseOr());
|
|
514
|
+
while (this.current().kind === "COMMA") {
|
|
515
|
+
this.advance();
|
|
516
|
+
args.push(this.parseOr());
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (this.current().kind !== "RPAREN") {
|
|
520
|
+
throw new Error("Expected )");
|
|
521
|
+
}
|
|
522
|
+
this.advance();
|
|
523
|
+
return this.callFunction(name, args);
|
|
524
|
+
}
|
|
525
|
+
callFunction(name, args) {
|
|
526
|
+
switch (name) {
|
|
527
|
+
case "abs":
|
|
528
|
+
this.checkArgs(name, args, 1);
|
|
529
|
+
return Math.abs(args[0]);
|
|
530
|
+
case "min":
|
|
531
|
+
this.checkArgsMin(name, args, 1);
|
|
532
|
+
return Math.min(...args);
|
|
533
|
+
case "max":
|
|
534
|
+
this.checkArgsMin(name, args, 1);
|
|
535
|
+
return Math.max(...args);
|
|
536
|
+
case "floor":
|
|
537
|
+
this.checkArgs(name, args, 1);
|
|
538
|
+
return Math.floor(args[0]);
|
|
539
|
+
case "ceil":
|
|
540
|
+
this.checkArgs(name, args, 1);
|
|
541
|
+
return Math.ceil(args[0]);
|
|
542
|
+
case "round":
|
|
543
|
+
this.checkArgs(name, args, 1);
|
|
544
|
+
return Math.round(args[0]);
|
|
545
|
+
case "sqrt":
|
|
546
|
+
this.checkArgs(name, args, 1);
|
|
547
|
+
return Math.sqrt(args[0]);
|
|
548
|
+
case "sin":
|
|
549
|
+
this.checkArgs(name, args, 1);
|
|
550
|
+
return Math.sin(args[0]);
|
|
551
|
+
case "cos":
|
|
552
|
+
this.checkArgs(name, args, 1);
|
|
553
|
+
return Math.cos(args[0]);
|
|
554
|
+
case "tan":
|
|
555
|
+
this.checkArgs(name, args, 1);
|
|
556
|
+
return Math.tan(args[0]);
|
|
557
|
+
case "ln":
|
|
558
|
+
this.checkArgs(name, args, 1);
|
|
559
|
+
return Math.log(args[0]);
|
|
560
|
+
case "log":
|
|
561
|
+
this.checkArgs(name, args, 1);
|
|
562
|
+
return Math.log10(args[0]);
|
|
563
|
+
case "exp":
|
|
564
|
+
this.checkArgs(name, args, 1);
|
|
565
|
+
return Math.exp(args[0]);
|
|
566
|
+
case "pow":
|
|
567
|
+
this.checkArgs(name, args, 2);
|
|
568
|
+
return Math.pow(args[0], args[1]);
|
|
569
|
+
default:
|
|
570
|
+
throw new Error(`undefined function "${name}"`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
checkArgs(name, args, expected) {
|
|
574
|
+
if (args.length !== expected) {
|
|
575
|
+
throw new Error(`${name}() expects ${expected} argument(s), got ${args.length}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
checkArgsMin(name, args, min) {
|
|
579
|
+
if (args.length < min) {
|
|
580
|
+
throw new Error(`${name}() expects at least ${min} argument(s), got ${args.length}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
196
584
|
// packages/ast/src/settings.ts
|
|
197
585
|
function createSettings(mode) {
|
|
198
586
|
switch (mode) {
|
|
@@ -236,6 +624,7 @@ export {
|
|
|
236
624
|
link,
|
|
237
625
|
lineBreak,
|
|
238
626
|
italics,
|
|
627
|
+
isTruthy,
|
|
239
628
|
isStringContainerType,
|
|
240
629
|
isParagraphSafe,
|
|
241
630
|
isHeaderType,
|
|
@@ -243,6 +632,8 @@ export {
|
|
|
243
632
|
isAlignType,
|
|
244
633
|
horizontalRule,
|
|
245
634
|
heading,
|
|
635
|
+
formatExprValue,
|
|
636
|
+
evaluateExpression,
|
|
246
637
|
createSettings,
|
|
247
638
|
createPosition,
|
|
248
639
|
createPoint,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wdprlib/ast",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "AST types for Wikidot markup",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ast",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"directory": "packages/ast"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
-
"dist"
|
|
18
|
+
"dist",
|
|
19
|
+
"src"
|
|
19
20
|
],
|
|
20
21
|
"type": "module",
|
|
21
22
|
"sideEffects": false,
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
"types": "./dist/index.d.ts",
|
|
25
26
|
"exports": {
|
|
26
27
|
".": {
|
|
28
|
+
"bun": "./src/index.ts",
|
|
27
29
|
"import": {
|
|
28
30
|
"types": "./dist/index.d.ts",
|
|
29
31
|
"default": "./dist/index.js"
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel prefix for style slot placeholders in {@link SyntaxTree.styles}.
|
|
3
|
+
*
|
|
4
|
+
* When the resolver encounters an unresolved `[[iftags]]` block containing
|
|
5
|
+
* `[[module CSS]]`, it inserts a sentinel string (`STYLE_SLOT_PREFIX + slotId`)
|
|
6
|
+
* into the styles array to preserve source order. At render time the sentinel
|
|
7
|
+
* is replaced with the actual CSS collected from the iftags block (if the
|
|
8
|
+
* condition matches).
|
|
9
|
+
*
|
|
10
|
+
* A null-byte prefix ensures no collision with valid CSS content.
|
|
11
|
+
*/
|
|
12
|
+
export const STYLE_SLOT_PREFIX = "\0__IFTAGS_SLOT__";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic types for reporting parse-time issues.
|
|
3
|
+
*
|
|
4
|
+
* When the parser encounters syntactically questionable or invalid markup
|
|
5
|
+
* (e.g. an unclosed `[[div]]` block), it records a {@link Diagnostic} rather
|
|
6
|
+
* than throwing an error. The parser is lenient: it always produces an AST,
|
|
7
|
+
* even when diagnostics are present.
|
|
8
|
+
*
|
|
9
|
+
* Diagnostics are returned alongside the AST via {@link ParseResult}.
|
|
10
|
+
*
|
|
11
|
+
* @since 2.0.0
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Position } from "./position";
|
|
16
|
+
import type { SyntaxTree } from "./element";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Severity level of a diagnostic.
|
|
20
|
+
*
|
|
21
|
+
* - `"error"` — the markup is structurally broken (e.g. inline `[[div]]`
|
|
22
|
+
* without a newline after `]]`).
|
|
23
|
+
* - `"warning"` — the markup is likely unintentional but the parser can
|
|
24
|
+
* recover (e.g. a missing `[[/div]]` close tag).
|
|
25
|
+
* - `"info"` — informational hints (e.g. deprecated syntax).
|
|
26
|
+
*
|
|
27
|
+
* @since 2.0.0
|
|
28
|
+
* @group Diagnostics
|
|
29
|
+
*/
|
|
30
|
+
export type DiagnosticSeverity = "error" | "warning" | "info";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A single diagnostic emitted during parsing.
|
|
34
|
+
*
|
|
35
|
+
* Each diagnostic pinpoints a source location via {@link Position} and
|
|
36
|
+
* carries a machine-readable {@link Diagnostic.code | code} string for
|
|
37
|
+
* programmatic filtering (e.g. `"unclosed-block"`, `"inline-block-element"`).
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* import { parse } from "@wdprlib/parser";
|
|
42
|
+
*
|
|
43
|
+
* const { ast, diagnostics } = parse("[[div]]\nHello");
|
|
44
|
+
* for (const d of diagnostics) {
|
|
45
|
+
* console.log(`[${d.severity}] ${d.message} (line ${d.position.start.line})`);
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @since 2.0.0
|
|
50
|
+
* @group Diagnostics
|
|
51
|
+
*/
|
|
52
|
+
export interface Diagnostic {
|
|
53
|
+
/** How severe the issue is. */
|
|
54
|
+
severity: DiagnosticSeverity;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Machine-readable identifier for the diagnostic kind.
|
|
58
|
+
*
|
|
59
|
+
* Current codes:
|
|
60
|
+
* - `"unclosed-block"` — a block element has no matching close tag.
|
|
61
|
+
* - `"inline-block-element"` — a block element (e.g. `[[div]]`) is used
|
|
62
|
+
* inline without the required trailing newline.
|
|
63
|
+
*/
|
|
64
|
+
code: string;
|
|
65
|
+
|
|
66
|
+
/** Human-readable description of the issue. */
|
|
67
|
+
message: string;
|
|
68
|
+
|
|
69
|
+
/** Source range where the issue was detected. */
|
|
70
|
+
position: Position;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* An optional related source range that provides additional context
|
|
74
|
+
* (e.g. the opening tag position when reporting a missing close tag).
|
|
75
|
+
*/
|
|
76
|
+
relatedPosition?: Position;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The result of parsing a Wikidot markup string.
|
|
81
|
+
*
|
|
82
|
+
* Contains both the parsed AST and any diagnostics emitted during parsing.
|
|
83
|
+
* The AST is always produced, even when diagnostics are present — the parser
|
|
84
|
+
* is lenient and recovers from errors.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* import { parse } from "@wdprlib/parser";
|
|
89
|
+
*
|
|
90
|
+
* const result = parse("**bold** and //italic//");
|
|
91
|
+
* console.log(result.ast.elements); // AST nodes
|
|
92
|
+
* console.log(result.diagnostics); // [] (no issues)
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* @since 2.0.0
|
|
96
|
+
* @group Diagnostics
|
|
97
|
+
*/
|
|
98
|
+
export interface ParseResult {
|
|
99
|
+
/** The parsed syntax tree. */
|
|
100
|
+
ast: SyntaxTree;
|
|
101
|
+
|
|
102
|
+
/** Diagnostics emitted during parsing (empty when the input is clean). */
|
|
103
|
+
diagnostics: Diagnostic[];
|
|
104
|
+
}
|