@jxsuite/schema 5.5.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/class-schema.json +2 -0
- package/defs/class-def.schema.ts +4 -0
- package/defs/function-def.schema.ts +4 -2
- package/package.json +7 -1
- package/schema.json +26 -2
- package/src/component-meta.ts +10 -1
- package/src/document-scripts.ts +304 -0
- package/src/dynamic-tag.ts +570 -0
- package/src/function-role.ts +182 -0
- package/src/guards.ts +190 -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 +205 -0
- package/src/schema.ts +22 -0
- package/types.ts +16 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One verdict per `$prototype: "Function"` entry: is it a **computed** value, read reactively, or a
|
|
3
|
+
* **callable**, invoked by an event, a call site or the host?
|
|
4
|
+
*
|
|
5
|
+
* Every tier reads this one classifier (the interpreter's `buildScope`, both compiled targets and
|
|
6
|
+
* the build-time scope), so a document means the same thing in Studio and in production. It reads
|
|
7
|
+
* the document, never an imported function's arity or source: a compiler has only the document, and
|
|
8
|
+
* `fn.toString()` of a bundled import is not a stable input (spec.md §5.3 4b, 4d).
|
|
9
|
+
*
|
|
10
|
+
* @docs framework/concepts/functions
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
bodyReturnsValue,
|
|
14
|
+
hasStructuredBody,
|
|
15
|
+
isFunctionDef,
|
|
16
|
+
isJsonObject,
|
|
17
|
+
isRef,
|
|
18
|
+
isString,
|
|
19
|
+
paramNames,
|
|
20
|
+
} from "./guards";
|
|
21
|
+
import type { JsonValue, JxElement, JxFunctionDef } from "../types";
|
|
22
|
+
|
|
23
|
+
/** What a Function entry is to the tiers that read it. */
|
|
24
|
+
export type FunctionRole = "computed" | "callable";
|
|
25
|
+
|
|
26
|
+
/** Lifecycle hooks the generated element calls itself, so they are never `$ref`'d (spec.md §16.4). */
|
|
27
|
+
export const LIFECYCLE_KEYS: ReadonlySet<string> = new Set(["onMount", "onUnmount", "onAdopted"]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The state keys a document uses as a _callable_, at any depth: bound to an `on*` event (a
|
|
31
|
+
* definition's root-level handlers included, §16.1), invoked by an `$expression` `call` node
|
|
32
|
+
* (§19.4c), called as `state.key(…)` from a template string or another function's body, or named as
|
|
33
|
+
* a lifecycle hook (§16.4). `$defs` holds JSON Schema, not usage, and is skipped.
|
|
34
|
+
*
|
|
35
|
+
* Anything not used as a callable has its return value read reactively. Defaulting the other way
|
|
36
|
+
* round would turn a called helper into a value and break its call site.
|
|
37
|
+
*/
|
|
38
|
+
function collectCallableKeys(doc: JxElement): Set<string> {
|
|
39
|
+
const keys = new Set<string>();
|
|
40
|
+
const seen = new Set<object>();
|
|
41
|
+
|
|
42
|
+
const addStateRef = (ref: string): void => {
|
|
43
|
+
if (ref.startsWith("#/state/")) {
|
|
44
|
+
/* SAFETY: `String.prototype.split` always returns at least one element (the whole string
|
|
45
|
+
when the separator is absent), so index 0 is present. */
|
|
46
|
+
keys.add(ref.slice("#/state/".length).split("/")[0] as string);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const visit = (node: JsonValue | undefined): void => {
|
|
51
|
+
if (isString(node)) {
|
|
52
|
+
// `${state.formatDate(d)}` in a template, or `state.helper(x)` in another function's body.
|
|
53
|
+
for (const call of node.matchAll(/\bstate\.([A-Za-z_$][\w$]*)\s*\(/gu)) {
|
|
54
|
+
/* SAFETY: the pattern's one capture group is not optional, so every match fills index 1
|
|
55
|
+
with the called member's name. */
|
|
56
|
+
keys.add(call[1] as string);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(node)) {
|
|
61
|
+
if (!seen.has(node)) {
|
|
62
|
+
seen.add(node);
|
|
63
|
+
for (const item of node) {
|
|
64
|
+
visit(item);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!isJsonObject(node) || seen.has(node)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
seen.add(node);
|
|
73
|
+
// An `$expression` `call` node names its callee in `target` (spec.md §19.4c).
|
|
74
|
+
if (node.operator === "call" && isRef(node.target)) {
|
|
75
|
+
addStateRef(node.target.$ref);
|
|
76
|
+
}
|
|
77
|
+
for (const [key, value] of Object.entries(node)) {
|
|
78
|
+
if (key === "$defs") {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (key.startsWith("on") && isRef(value)) {
|
|
82
|
+
addStateRef(value.$ref);
|
|
83
|
+
}
|
|
84
|
+
visit(value);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// SAFETY: a Jx element is JSON data; its type names what it holds.
|
|
89
|
+
visit(doc as JsonValue);
|
|
90
|
+
for (const hook of LIFECYCLE_KEYS) {
|
|
91
|
+
if (isJsonObject(doc.state) && hook in doc.state) {
|
|
92
|
+
keys.add(hook);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return keys;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Does a Function declaration name both a `body` and a `$src`? The two are mutually exclusive
|
|
100
|
+
* (spec.md §5.3 4d), and the test is presence, as the `FunctionDef` schema's `not` is, so an empty
|
|
101
|
+
* `body: ""` beside a `$src` is refused too and `jx validate` agrees with every tier.
|
|
102
|
+
*
|
|
103
|
+
* @param def A Function declaration, a `state` entry or a handler written inline on an `on*` key.
|
|
104
|
+
*/
|
|
105
|
+
export function declaresBodyAndSrc(def: JxFunctionDef): boolean {
|
|
106
|
+
return "body" in def && "$src" in def;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Does a Function declaration list any parameter of its own, by `parameters` or the legacy
|
|
111
|
+
* `arguments`? An inline handler that does not is rewritten to read `state` as the handler's alias,
|
|
112
|
+
* and one that does is built with {@link handlerParamNames}.
|
|
113
|
+
*
|
|
114
|
+
* @param def A Function declaration.
|
|
115
|
+
*/
|
|
116
|
+
export function declaresParameters(def: JxFunctionDef): boolean {
|
|
117
|
+
return paramNames(def.parameters ?? def.arguments).length > 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The formal parameters a string `body` is built with, in every tier (spec.md §5.3 4d): `state`
|
|
122
|
+
* first, then the other declared names in declared order. Binding is by position, as ECMAScript's
|
|
123
|
+
* is, so a name that is not `state` receives whatever the call site passes in that position. An
|
|
124
|
+
* event binding calls a handler `(state, event)`, so the first other name is the event and a later
|
|
125
|
+
* one is `undefined` from an event (a call with too few arguments), filled by a call site that
|
|
126
|
+
* passes it. A declared `state` is moved to the front rather than repeated, since a duplicate
|
|
127
|
+
* parameter is a sloppy-mode quirk in which the later one wins and `state` would be `undefined`.
|
|
128
|
+
*
|
|
129
|
+
* @param def A Function declaration, a `state` entry or a handler written inline on an `on*` key.
|
|
130
|
+
*/
|
|
131
|
+
export function handlerParamNames(def: JxFunctionDef): string[] {
|
|
132
|
+
const list = ["state"];
|
|
133
|
+
for (const name of paramNames(def.parameters ?? def.arguments)) {
|
|
134
|
+
if (name !== "state") {
|
|
135
|
+
list.push(name);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return list;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The rules in order: the first that applies decides. */
|
|
142
|
+
function functionEntryRole(
|
|
143
|
+
key: string,
|
|
144
|
+
def: JxFunctionDef,
|
|
145
|
+
callableKeys: () => Set<string>,
|
|
146
|
+
): FunctionRole {
|
|
147
|
+
if (hasStructuredBody(def)) {
|
|
148
|
+
return "callable";
|
|
149
|
+
}
|
|
150
|
+
// Declared parameters or arguments make an entry callable wherever it is used (§19.4c).
|
|
151
|
+
if (paramNames(def.parameters ?? def.arguments).length > 0) {
|
|
152
|
+
return "callable";
|
|
153
|
+
}
|
|
154
|
+
if (isString(def.body)) {
|
|
155
|
+
return bodyReturnsValue(def.body) ? "computed" : "callable";
|
|
156
|
+
}
|
|
157
|
+
if (def.$src) {
|
|
158
|
+
return callableKeys().has(key) ? "callable" : "computed";
|
|
159
|
+
}
|
|
160
|
+
return "callable";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Classify every Function entry in a document's `state`. Other state shapes get no verdict. The
|
|
165
|
+
* document is the one a tier builds, after the project-state merge, so a project-global entry one
|
|
166
|
+
* page calls and another only reads is a function on the first and a computed on the second.
|
|
167
|
+
*
|
|
168
|
+
* The walk for usage runs at most once, and only when a bodyless `$src` entry reaches it.
|
|
169
|
+
*
|
|
170
|
+
* @param doc The document (or just `{ state }`) being built.
|
|
171
|
+
*/
|
|
172
|
+
export function classifyFunctionEntries(doc: JxElement): Map<string, FunctionRole> {
|
|
173
|
+
const roles = new Map<string, FunctionRole>();
|
|
174
|
+
let callable: Set<string> | null = null;
|
|
175
|
+
const callableKeys = (): Set<string> => (callable ??= collectCallableKeys(doc));
|
|
176
|
+
for (const [key, def] of Object.entries(doc.state ?? {})) {
|
|
177
|
+
if (isFunctionDef(def)) {
|
|
178
|
+
roles.set(key, functionEntryRole(key, def, callableKeys));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return roles;
|
|
182
|
+
}
|
package/src/guards.ts
CHANGED
|
@@ -245,6 +245,17 @@ export function bodyReturnsValue(body: string): boolean {
|
|
|
245
245
|
return /\breturn\b[^\S\n]*(?![;}\s]|$)/.test(maskCode(body));
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Whether an object is a function entry, whose `body` is code (`spec.md` §23.1): a `Function`
|
|
250
|
+
* entry, or a class member, which a `.class.json` names by its `role` alone. A field is a member
|
|
251
|
+
* with no body. Any other `body` may be data, so it is never read as code.
|
|
252
|
+
*
|
|
253
|
+
* @param value An object in a document or class definition.
|
|
254
|
+
*/
|
|
255
|
+
export function isFunctionEntry(value: JsonObject): boolean {
|
|
256
|
+
return value.$prototype === "Function" || (isString(value.role) && value.role !== "field");
|
|
257
|
+
}
|
|
258
|
+
|
|
248
259
|
/**
|
|
249
260
|
* A function body with its comments and the contents of its strings blanked, so a pattern reads
|
|
250
261
|
* only code (`spec.md` §23.1). The result is the same length, every newline kept, so an offset in
|
|
@@ -282,8 +293,152 @@ export function maskCode(body: string): string {
|
|
|
282
293
|
const masked = new Map<string, string>();
|
|
283
294
|
const MASKED_LIMIT = 2048;
|
|
284
295
|
|
|
285
|
-
/**
|
|
286
|
-
|
|
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 {
|
|
287
442
|
/* UTF-16 units, as `at` counts them. Code points (`[...body]`) made an astral character in a
|
|
288
443
|
string one element for two offsets, so the result came back short and every offset after it,
|
|
289
444
|
which is what a caller splices the body at, was off by one. */
|
|
@@ -295,7 +450,11 @@ function scanCode(body: string): string {
|
|
|
295
450
|
};
|
|
296
451
|
/** The brace depth of each template substitution still open, innermost last. */
|
|
297
452
|
const substitutions: number[] = [];
|
|
298
|
-
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;
|
|
299
458
|
let at = 0;
|
|
300
459
|
while (at < body.length) {
|
|
301
460
|
const char = body.charAt(at);
|
|
@@ -304,12 +463,18 @@ function scanCode(body: string): string {
|
|
|
304
463
|
blank(at);
|
|
305
464
|
blank(at + 1);
|
|
306
465
|
at += 2;
|
|
307
|
-
} 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.
|
|
308
468
|
inTemplate = false;
|
|
469
|
+
before = char;
|
|
309
470
|
at += 1;
|
|
310
471
|
} else if (char === "$" && body.charAt(at + 1) === "{") {
|
|
472
|
+
if (substitutions.length === 0) {
|
|
473
|
+
spans?.push(at, -1);
|
|
474
|
+
}
|
|
311
475
|
substitutions.push(0);
|
|
312
476
|
inTemplate = false;
|
|
477
|
+
before = "{";
|
|
313
478
|
at += 2;
|
|
314
479
|
} else {
|
|
315
480
|
blank(at);
|
|
@@ -336,10 +501,24 @@ function scanCode(body: string): string {
|
|
|
336
501
|
blank(at);
|
|
337
502
|
at += 1;
|
|
338
503
|
}
|
|
504
|
+
before = char;
|
|
339
505
|
at += 1;
|
|
340
506
|
} else if (char === "`") {
|
|
341
507
|
inTemplate = true;
|
|
342
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;
|
|
343
522
|
} else {
|
|
344
523
|
const open = substitutions.length - 1;
|
|
345
524
|
if (open >= 0 && char === "{") {
|
|
@@ -348,10 +527,17 @@ function scanCode(body: string): string {
|
|
|
348
527
|
if (substitutions[open] === 0) {
|
|
349
528
|
substitutions.pop();
|
|
350
529
|
inTemplate = true;
|
|
530
|
+
if (spans && substitutions.length === 0) {
|
|
531
|
+
spans[spans.length - 1] = at + 1;
|
|
532
|
+
}
|
|
351
533
|
} else {
|
|
352
534
|
substitutions[open] = (substitutions[open] ?? 1) - 1;
|
|
353
535
|
}
|
|
354
536
|
}
|
|
537
|
+
if (char.trim() !== "") {
|
|
538
|
+
before = char;
|
|
539
|
+
beforeAt = at;
|
|
540
|
+
}
|
|
355
541
|
at += 1;
|
|
356
542
|
}
|
|
357
543
|
}
|