@jxsuite/schema 5.6.0 → 6.0.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/README.md +1 -0
- package/package.json +5 -1
- package/schema.json +15 -0
- package/src/document-scripts.ts +304 -0
- package/src/dynamic-tag.ts +570 -0
- package/src/guards.ts +179 -4
- package/src/jxn/complete.ts +582 -0
- package/src/jxn/document-scopes.ts +581 -0
- package/src/jxn/document.ts +6 -0
- package/src/jxn/index.ts +14 -2
- package/src/jxn/outline.ts +452 -0
- package/src/jxn/parse.ts +234 -13
- package/src/jxn/schema-types.ts +299 -0
- package/src/jxn/schema-walk.ts +221 -0
- package/src/jxn/serialize.ts +113 -5
- package/src/jxn/state-types.ts +271 -0
- package/src/jxn/string-pieces.ts +240 -0
- package/src/jxn/virtual-document.ts +1077 -0
- package/src/jxn/virtual-types.ts +94 -169
- package/src/schema.ts +22 -0
- package/types.ts +16 -0
package/src/guards.ts
CHANGED
|
@@ -293,8 +293,152 @@ export function maskCode(body: string): string {
|
|
|
293
293
|
const masked = new Map<string, string>();
|
|
294
294
|
const MASKED_LIMIT = 2048;
|
|
295
295
|
|
|
296
|
-
/**
|
|
297
|
-
|
|
296
|
+
/**
|
|
297
|
+
* One `${…}` of a template string: `start` is the offset of its `$`, `end` is one past its closing
|
|
298
|
+
* `}`, and `expr` is the expression between the braces, exactly as written.
|
|
299
|
+
*/
|
|
300
|
+
export interface TemplateExpression {
|
|
301
|
+
readonly start: number;
|
|
302
|
+
readonly end: number;
|
|
303
|
+
readonly expr: string;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Each top-level `${…}` of a template string (`spec.md` §8.4), in order.
|
|
308
|
+
*
|
|
309
|
+
* A template string is the inside of a JavaScript template literal, and its expressions are code,
|
|
310
|
+
* so where one ends is a question about code: a `}` inside a string, a comment, an object literal
|
|
311
|
+
* or a nested template literal does not close it. This is the {@link maskCode} scan started inside
|
|
312
|
+
* a template, so every reader of a template agrees with the engine that evaluates it. A pattern
|
|
313
|
+
* such as `[^}]+` cuts `${f(x, { a: 1 })}` at the object's brace, and a brace count alone ends `${a
|
|
314
|
+
* ? "}" : ""}` inside the string.
|
|
315
|
+
*
|
|
316
|
+
* Two things this scan does that {@link maskCode} does not, because an expression is short and the
|
|
317
|
+
* cost of misreading one is a binding shown or stored wrong:
|
|
318
|
+
*
|
|
319
|
+
* - A regular expression literal is recognised where an operand may start, so a quote or a brace
|
|
320
|
+
* inside `/"/g` is not read as code.
|
|
321
|
+
* - An expression the scan cannot close (an unterminated string) is closed by brace depth instead,
|
|
322
|
+
* and one with no closing brace at all is not reported: its `${` is literal text, and the scan
|
|
323
|
+
* goes on after it, so a complete expression later in the string is still found.
|
|
324
|
+
*
|
|
325
|
+
* The result is shared and must not be changed.
|
|
326
|
+
*
|
|
327
|
+
* @param template The template string, without the backticks of a literal.
|
|
328
|
+
*/
|
|
329
|
+
export function templateExpressions(template: string): readonly TemplateExpression[] {
|
|
330
|
+
if (!template.includes("${")) {
|
|
331
|
+
return NO_EXPRESSIONS;
|
|
332
|
+
}
|
|
333
|
+
const known = scanned.get(template);
|
|
334
|
+
if (known !== undefined) {
|
|
335
|
+
return known;
|
|
336
|
+
}
|
|
337
|
+
const found: TemplateExpression[] = [];
|
|
338
|
+
let from = 0;
|
|
339
|
+
while (from < template.length) {
|
|
340
|
+
const spans: number[] = [];
|
|
341
|
+
scanCode(template.slice(from), spans);
|
|
342
|
+
let unclosed = -1;
|
|
343
|
+
for (let i = 0; i < spans.length; i += 2) {
|
|
344
|
+
const start = from + (spans[i] ?? 0);
|
|
345
|
+
const end = spans[i + 1] ?? -1;
|
|
346
|
+
if (end === -1) {
|
|
347
|
+
unclosed = start;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
found.push(expressionAt(template, start, from + end));
|
|
351
|
+
}
|
|
352
|
+
if (unclosed === -1) {
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
// The scan lost its place (a quote it could not pair): brace depth closes this one expression.
|
|
356
|
+
const end = closeByBraceDepth(template, unclosed);
|
|
357
|
+
if (end === -1) {
|
|
358
|
+
// Never closed, so this `${` is text; a complete expression may still follow it.
|
|
359
|
+
from = unclosed + 2;
|
|
360
|
+
} else {
|
|
361
|
+
found.push(expressionAt(template, unclosed, end));
|
|
362
|
+
from = end;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (scanned.size >= MASKED_LIMIT) {
|
|
366
|
+
scanned.clear();
|
|
367
|
+
}
|
|
368
|
+
scanned.set(template, found);
|
|
369
|
+
return found;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const NO_EXPRESSIONS: readonly TemplateExpression[] = [];
|
|
373
|
+
|
|
374
|
+
/** Templates already scanned: a renderer asks about the same template on every evaluation. */
|
|
375
|
+
const scanned = new Map<string, readonly TemplateExpression[]>();
|
|
376
|
+
|
|
377
|
+
function expressionAt(template: string, start: number, end: number): TemplateExpression {
|
|
378
|
+
return { end, expr: template.slice(start + 2, end - 1), start };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** One past the `}` that balances the `${` at `start` by brace depth alone, or -1 for none. */
|
|
382
|
+
function closeByBraceDepth(template: string, start: number): number {
|
|
383
|
+
let depth = 0;
|
|
384
|
+
for (let at = start + 1; at < template.length; at += 1) {
|
|
385
|
+
const char = template.charAt(at);
|
|
386
|
+
if (char === "{") {
|
|
387
|
+
depth += 1;
|
|
388
|
+
} else if (char === "}") {
|
|
389
|
+
depth -= 1;
|
|
390
|
+
if (depth === 0) {
|
|
391
|
+
return at + 1;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return -1;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** The characters after which a `/` starts a regular expression literal, not a division. */
|
|
399
|
+
const BEFORE_OPERAND = new Set("(,=:[!&|?{;+-*%<>~^");
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Whether `before`, the operator character read at `at`, is the second half of `++` or `--`. A `/`
|
|
403
|
+
* after one of those divides (`a++ / 2`): read as a regular expression it ran to the next `/` on
|
|
404
|
+
* the line, which in `"${a++ / 2} and ${b / 3}"` is in the NEXT expression, and the two were
|
|
405
|
+
* reported as one.
|
|
406
|
+
*/
|
|
407
|
+
function endsUpdateOperator(body: string, before: string, at: number): boolean {
|
|
408
|
+
return (before === "+" || before === "-") && body.charAt(at - 1) === before;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* One past the end of the regular expression literal opening at `at`, or -1 when the `/` there is
|
|
413
|
+
* not one: it does not close on its line. A `/` inside a character class does not close it.
|
|
414
|
+
*/
|
|
415
|
+
function regexLiteralEnd(body: string, at: number): number {
|
|
416
|
+
let inClass = false;
|
|
417
|
+
for (let i = at + 1; i < body.length; i += 1) {
|
|
418
|
+
const char = body.charAt(i);
|
|
419
|
+
if (char === "\n") {
|
|
420
|
+
return -1;
|
|
421
|
+
}
|
|
422
|
+
if (char === "\\") {
|
|
423
|
+
i += 1;
|
|
424
|
+
} else if (char === "[") {
|
|
425
|
+
inClass = true;
|
|
426
|
+
} else if (char === "]") {
|
|
427
|
+
inClass = false;
|
|
428
|
+
} else if (char === "/" && !inClass) {
|
|
429
|
+
return i + 1;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return -1;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* The scan {@link maskCode} caches. With `spans` it reads `body` as a template string instead of
|
|
437
|
+
* code (it starts inside the template), recognises regular expression literals, and records each
|
|
438
|
+
* top-level substitution for {@link templateExpressions} as a pair of offsets: its `$`, then one
|
|
439
|
+
* past its `}`, which is -1 while it is still open.
|
|
440
|
+
*/
|
|
441
|
+
function scanCode(body: string, spans?: number[]): string {
|
|
298
442
|
/* UTF-16 units, as `at` counts them. Code points (`[...body]`) made an astral character in a
|
|
299
443
|
string one element for two offsets, so the result came back short and every offset after it,
|
|
300
444
|
which is what a caller splices the body at, was off by one. */
|
|
@@ -306,7 +450,11 @@ function scanCode(body: string): string {
|
|
|
306
450
|
};
|
|
307
451
|
/** The brace depth of each template substitution still open, innermost last. */
|
|
308
452
|
const substitutions: number[] = [];
|
|
309
|
-
let inTemplate =
|
|
453
|
+
let inTemplate = spans !== undefined;
|
|
454
|
+
/** The last character of code read that is not white space: what a `/` follows. */
|
|
455
|
+
let before = "";
|
|
456
|
+
/** Where `before` was read, when it is an operator or part of a name; stale otherwise. */
|
|
457
|
+
let beforeAt = -1;
|
|
310
458
|
let at = 0;
|
|
311
459
|
while (at < body.length) {
|
|
312
460
|
const char = body.charAt(at);
|
|
@@ -315,12 +463,18 @@ function scanCode(body: string): string {
|
|
|
315
463
|
blank(at);
|
|
316
464
|
blank(at + 1);
|
|
317
465
|
at += 2;
|
|
318
|
-
} else if (char === "`") {
|
|
466
|
+
} else if (char === "`" && !(spans && substitutions.length === 0)) {
|
|
467
|
+
// A template STRING has no closing backtick: one in its own text is text.
|
|
319
468
|
inTemplate = false;
|
|
469
|
+
before = char;
|
|
320
470
|
at += 1;
|
|
321
471
|
} else if (char === "$" && body.charAt(at + 1) === "{") {
|
|
472
|
+
if (substitutions.length === 0) {
|
|
473
|
+
spans?.push(at, -1);
|
|
474
|
+
}
|
|
322
475
|
substitutions.push(0);
|
|
323
476
|
inTemplate = false;
|
|
477
|
+
before = "{";
|
|
324
478
|
at += 2;
|
|
325
479
|
} else {
|
|
326
480
|
blank(at);
|
|
@@ -347,10 +501,24 @@ function scanCode(body: string): string {
|
|
|
347
501
|
blank(at);
|
|
348
502
|
at += 1;
|
|
349
503
|
}
|
|
504
|
+
before = char;
|
|
350
505
|
at += 1;
|
|
351
506
|
} else if (char === "`") {
|
|
352
507
|
inTemplate = true;
|
|
353
508
|
at += 1;
|
|
509
|
+
} else if (
|
|
510
|
+
spans &&
|
|
511
|
+
char === "/" &&
|
|
512
|
+
BEFORE_OPERAND.has(before) &&
|
|
513
|
+
!endsUpdateOperator(body, before, beforeAt) &&
|
|
514
|
+
regexLiteralEnd(body, at) > 0
|
|
515
|
+
) {
|
|
516
|
+
const stop = regexLiteralEnd(body, at);
|
|
517
|
+
for (at += 1; at < stop - 1; at += 1) {
|
|
518
|
+
blank(at);
|
|
519
|
+
}
|
|
520
|
+
before = "/";
|
|
521
|
+
at = stop;
|
|
354
522
|
} else {
|
|
355
523
|
const open = substitutions.length - 1;
|
|
356
524
|
if (open >= 0 && char === "{") {
|
|
@@ -359,10 +527,17 @@ function scanCode(body: string): string {
|
|
|
359
527
|
if (substitutions[open] === 0) {
|
|
360
528
|
substitutions.pop();
|
|
361
529
|
inTemplate = true;
|
|
530
|
+
if (spans && substitutions.length === 0) {
|
|
531
|
+
spans[spans.length - 1] = at + 1;
|
|
532
|
+
}
|
|
362
533
|
} else {
|
|
363
534
|
substitutions[open] = (substitutions[open] ?? 1) - 1;
|
|
364
535
|
}
|
|
365
536
|
}
|
|
537
|
+
if (char.trim() !== "") {
|
|
538
|
+
before = char;
|
|
539
|
+
beforeAt = at;
|
|
540
|
+
}
|
|
366
541
|
at += 1;
|
|
367
542
|
}
|
|
368
543
|
}
|