@jxsuite/schema 5.6.0 → 6.0.2

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.
@@ -0,0 +1,581 @@
1
+ /**
2
+ * Where a function body stands in a document, and what that says about its types (`spec.md` §23.3):
3
+ * the `state` it reads, the row of a map it is drawn in, and the event its handler is given.
4
+ *
5
+ * The two files a checker reads, one body wrapped alone (`virtual-types.ts`) and a whole `.jxn`
6
+ * document (`virtual-document.ts`), both ask here, so neither can say of the same body what the
7
+ * other does not.
8
+ *
9
+ * Every type written here is a lookup that answers `any` when it finds nothing: a project with no
10
+ * DOM library, an element the platform does not name, a list the document does not declare. A type
11
+ * that failed instead would be an error on a line the author did not write.
12
+ *
13
+ * @docs framework/concepts/type-checking
14
+ */
15
+ import { classifyFunctionEntries } from "../function-role";
16
+ import type { FunctionRole } from "../function-role";
17
+ import { isFunctionDef, isJsonObject, isNumber, isRef, isString } from "../guards";
18
+ import type { JsonObject, JsonValue, JxElement } from "../../types";
19
+ import { pointerInto, propertyName } from "./schema-types";
20
+ import { stateOf, typeOfEntry } from "./state-types";
21
+
22
+ /** An element whose `state` a body can read. */
23
+ export interface StateScope {
24
+ node: JsonObject;
25
+ /** The scope around it, or null at the document's root. */
26
+ parent: StateScope | null;
27
+ /** Where the element is. The root is `""`. */
28
+ pointer: string;
29
+ }
30
+
31
+ /** One map a body is drawn inside: what it repeats, and where that is read. */
32
+ export interface Row {
33
+ /** What the map's `items` holds: a list, or a `$ref` to one. */
34
+ items: JsonValue | undefined;
35
+ /** The map around this one, whose row `$map/item` names in `items`. */
36
+ parent: Row | null;
37
+ /** The scope `items` is read in. */
38
+ scope: StateScope;
39
+ }
40
+
41
+ /** What surrounds one value of a document. */
42
+ export interface Site {
43
+ row: Row | null;
44
+ scope: StateScope;
45
+ /** Whether an element here is in the SVG namespace. */
46
+ svg: boolean;
47
+ }
48
+
49
+ /** Members of a document that hold no element and no handler. */
50
+ const NOT_WALKED: ReadonlySet<string> = new Set(["$defs", "$script", "state", "style"]);
51
+
52
+ /** The interfaces a lookup reads, declared empty so that a project without the DOM library has them. */
53
+ export const EVENT_GLOBALS = [
54
+ "declare global {",
55
+ " interface HTMLElementEventMap {}",
56
+ " interface HTMLElementTagNameMap {}",
57
+ " interface SVGElementTagNameMap {}",
58
+ "}",
59
+ "",
60
+ ].join("\n");
61
+
62
+ /** The site of a document's root, which is a scope whether or not it has `state`. */
63
+ export function rootSite(doc: JsonObject): Site {
64
+ return { row: null, scope: { node: doc, parent: null, pointer: "" }, svg: doc.tagName === "svg" };
65
+ }
66
+
67
+ /**
68
+ * The site of a value one step down.
69
+ *
70
+ * @param site The site of `parent`.
71
+ * @param parent The value stepped from.
72
+ * @param key The member of `parent` stepped into.
73
+ * @param child The value there.
74
+ * @param pointer Where `child` is.
75
+ */
76
+ export function siteInto(
77
+ site: Site,
78
+ parent: JsonValue | undefined,
79
+ key: string | number,
80
+ child: JsonValue | undefined,
81
+ pointer: string,
82
+ ): Site {
83
+ // A map's element is drawn once for each item, and reads the row it is drawn for (§10.2).
84
+ const row =
85
+ key === "map" && isJsonObject(parent) && parent.$prototype === "Array"
86
+ ? { items: parent.items, parent: site.row, scope: site.scope }
87
+ : site.row;
88
+ if (!isJsonObject(child)) {
89
+ return { ...site, row };
90
+ }
91
+ const scope = stateOf(child) ? { node: child, parent: site.scope, pointer } : site.scope;
92
+ const tag = child.tagName;
93
+ // What `foreignObject` holds is HTML again, and it is not itself an element HTML names.
94
+ const svg = isString(tag) ? tag !== "foreignObject" && (tag === "svg" || site.svg) : site.svg;
95
+ return { row, scope, svg };
96
+ }
97
+
98
+ /**
99
+ * Visit every object of a document that can hold a handler or a scope, with what surrounds it.
100
+ *
101
+ * @param doc The document.
102
+ * @param visit Called for the root, then for each object below it, parents first.
103
+ */
104
+ export function eachSite(
105
+ doc: JsonObject,
106
+ visit: (node: JsonObject, pointer: string, site: Site) => void,
107
+ ): void {
108
+ const walk = (value: JsonValue | undefined, pointer: string, site: Site): void => {
109
+ if (Array.isArray(value)) {
110
+ for (const [index, item] of value.entries()) {
111
+ const at = pointerInto(pointer, index);
112
+ walk(item, at, siteInto(site, value, index, item, at));
113
+ }
114
+ return;
115
+ }
116
+ if (!isJsonObject(value)) {
117
+ return;
118
+ }
119
+ visit(value, pointer, site);
120
+ for (const [key, child] of Object.entries(value)) {
121
+ if (!NOT_WALKED.has(key)) {
122
+ const at = pointerInto(pointer, key);
123
+ walk(child, at, siteInto(site, value, key, child, at));
124
+ }
125
+ }
126
+ };
127
+ walk(doc, "", rootSite(doc));
128
+ }
129
+
130
+ /** The names a scope's body can read: its own, and every one around it. */
131
+ function visibleKeys(scope: StateScope | null): Set<string> {
132
+ const keys = new Set<string>();
133
+ for (let at = scope; at; at = at.parent) {
134
+ for (const key of Object.keys(stateOf(at.node) ?? {})) {
135
+ keys.add(key);
136
+ }
137
+ }
138
+ return keys;
139
+ }
140
+
141
+ /** What one scope's interface declares. */
142
+ export interface ScopeMembers {
143
+ /**
144
+ * Its own names that a scope around it declares too. An interface cannot extend one it disagrees
145
+ * with, so it extends that one without these (`EXCEPT`), and every other member is still the
146
+ * member it was declared as. Empty when nothing is shadowed, and the interface extends as is.
147
+ */
148
+ shadowed: string[];
149
+ /** Its own entries, each with the role its document reads it in. */
150
+ own: { entry: JsonValue | undefined; key: string; role: FunctionRole | undefined }[];
151
+ }
152
+
153
+ /** What a scope's interface declares, and how it stands to the one around it. */
154
+ export function membersOf(scope: StateScope): ScopeMembers {
155
+ // SAFETY: a scope is an element, or a document, which is one with `$` members beside it.
156
+ const roles = classifyFunctionEntries(scope.node as JxElement);
157
+ const own = Object.entries(stateOf(scope.node) ?? {}).map(([key, entry]) => ({
158
+ entry,
159
+ key,
160
+ role: roles.get(key),
161
+ }));
162
+ const around = visibleKeys(scope.parent);
163
+ return { own, shadowed: own.flatMap(({ key }) => (around.has(key) ? [key] : [])) };
164
+ }
165
+
166
+ /**
167
+ * The helper a shadowing scope's interface extends through: `T` without the members named `K`.
168
+ * Mapped over `keyof T`, so each member it keeps is the one `T` declares (a rename started on
169
+ * either side reaches both) and an index signature is kept as one, which `Omit` does not do.
170
+ */
171
+ export const EXCEPT =
172
+ "type $except<T, K extends PropertyKey> = { [P in keyof T as P extends K ? never : P]: T[P] };";
173
+
174
+ /** What a scope's interface extends: the one around it, less what this one declares over it. */
175
+ export function heritageOf(around: string, shadowed: readonly string[]): string {
176
+ const names = shadowed.map((key) => JSON.stringify(key)).join(" | ");
177
+ return shadowed.length === 0 ? around : `$except<${around}, ${names}>`;
178
+ }
179
+
180
+ /**
181
+ * What a path reads off a value's type, or `any` where the type does not say.
182
+ *
183
+ * @param base The type the path starts from.
184
+ * @param path The members to read, outermost first.
185
+ * @param object Whether `base` is an interface, which is never `null`.
186
+ */
187
+ function typeAlong(base: string, path: readonly string[], object = false): string {
188
+ let type = base;
189
+ for (const [index, key] of path.entries()) {
190
+ if (type === "any") {
191
+ break;
192
+ }
193
+ const from = object && index === 0 ? type : `NonNullable<${type}>`;
194
+ type = `(${from} extends { ${propertyName(key)}: infer V } ? V : any)`;
195
+ }
196
+ return type;
197
+ }
198
+
199
+ /** The segments of a JSON Pointer's path, unescaped. */
200
+ function segments(path: string): string[] {
201
+ return path.split("/").map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"));
202
+ }
203
+
204
+ /** The path a `$ref` names under a prefix, or null when it names something else. */
205
+ function pathUnder(ref: string, prefix: string): string[] | null {
206
+ if (ref === prefix) {
207
+ return [];
208
+ }
209
+ return ref.startsWith(`${prefix}/`) ? segments(ref.slice(prefix.length + 1)) : null;
210
+ }
211
+
212
+ /** The type of what a map repeats: the list its `items` holds or names. */
213
+ function listType(row: Row, nameOf: (scope: StateScope) => string): string {
214
+ const { items } = row;
215
+ if (Array.isArray(items)) {
216
+ return typeOfEntry(items);
217
+ }
218
+ if (!isRef(items)) {
219
+ return "any";
220
+ }
221
+ const inState = pathUnder(items.$ref, "#/state");
222
+ if (inState) {
223
+ return typeAlong(nameOf(row.scope), inState, true);
224
+ }
225
+ // A list read off the row of the map around this one.
226
+ const inRow = pathUnder(items.$ref, "$map/item") ?? pathUnder(items.$ref, "#/$map/item");
227
+ return inRow && row.parent ? typeAlong(itemType(row.parent, nameOf), inRow) : "any";
228
+ }
229
+
230
+ /**
231
+ * The type of one item of a map's list.
232
+ *
233
+ * @param row The map.
234
+ * @param nameOf What a scope's interface is called where this is written.
235
+ */
236
+ export function itemType(row: Row, nameOf: (scope: StateScope) => string): string {
237
+ const list = listType(row, nameOf);
238
+ return list === "any" ? "any" : `(NonNullable<${list}> extends readonly (infer I)[] ? I : any)`;
239
+ }
240
+
241
+ /** Whether a body in a scope can be told what `$map` is: no `state` around it has a `$map`. */
242
+ export function readsRow(scope: StateScope): boolean {
243
+ return !visibleKeys(scope).has("$map");
244
+ }
245
+
246
+ /**
247
+ * Events that do not bubble (UI Events, and the HTML Standard's element and media events), so a
248
+ * handler is given one only when it started on the handler's own element.
249
+ */
250
+ const NOT_BUBBLING: ReadonlySet<string> = new Set([
251
+ "blur",
252
+ "focus",
253
+ "mouseenter",
254
+ "mouseleave",
255
+ "pointerenter",
256
+ "pointerleave",
257
+ "load",
258
+ "error",
259
+ "scroll",
260
+ "scrollend",
261
+ "toggle",
262
+ "invalid",
263
+ "close",
264
+ "canplay",
265
+ "canplaythrough",
266
+ "durationchange",
267
+ "emptied",
268
+ "ended",
269
+ "loadeddata",
270
+ "loadedmetadata",
271
+ "loadstart",
272
+ "pause",
273
+ "play",
274
+ "playing",
275
+ "progress",
276
+ "ratechange",
277
+ "seeked",
278
+ "seeking",
279
+ "stalled",
280
+ "suspend",
281
+ "timeupdate",
282
+ "volumechange",
283
+ "waiting",
284
+ ]);
285
+
286
+ /** Whether nothing an element holds is an element, so an event on it started on it. */
287
+ function isLeaf(element: JsonObject): boolean {
288
+ const { children } = element;
289
+ const texts = children === undefined ? [] : Array.isArray(children) ? children : [children];
290
+ return (
291
+ element.innerHTML === undefined && texts.every((child) => isString(child) || isNumber(child))
292
+ );
293
+ }
294
+
295
+ /** Whether an event a handler on an element is given can only have started on that element. */
296
+ function startsHere(element: JsonObject, name: string): boolean {
297
+ // What a `select` holds are its choices, and a choice made is the control's own event.
298
+ const control = element.tagName === "select" && (name === "change" || name === "input");
299
+ return control || NOT_BUBBLING.has(name) || isLeaf(element);
300
+ }
301
+
302
+ /**
303
+ * The type of the event an `on*` handler on an element is given.
304
+ *
305
+ * It is the event the platform names for the binding, on the element the platform names for the
306
+ * tag: `any` where it names either one not at all, as for a custom element, whose events are its
307
+ * own. Its `target` is the element too where the two cannot differ: nothing inside it is an
308
+ * element, or the event does not bubble.
309
+ *
310
+ * @param element The element the handler is on.
311
+ * @param key The binding, such as `onclick`.
312
+ * @param site What surrounds the element.
313
+ */
314
+ export function eventType(element: JsonObject, key: string, site: Site): string {
315
+ const tag = element.tagName;
316
+ if (!isString(tag)) {
317
+ return "any";
318
+ }
319
+ const elements = site.svg ? "SVGElementTagNameMap" : "HTMLElementTagNameMap";
320
+ const name = key.slice(2);
321
+ const members = startsHere(element, name)
322
+ ? "readonly currentTarget: T; readonly target: T"
323
+ : "readonly currentTarget: T";
324
+ const named = `HTMLElementEventMap extends { ${propertyName(name)}: infer E }`;
325
+ return `(${elements} extends { ${propertyName(tag)}: infer T } ? ${named} ? E & { ${members} } : any : any)`;
326
+ }
327
+
328
+ /** How a body's event is written. */
329
+ export interface EventLines {
330
+ /** The lines that declare `event`, before the function. */
331
+ head: string;
332
+ /** The type of the parameter that receives it. */
333
+ parameter: string;
334
+ }
335
+
336
+ /**
337
+ * How a body's event is written.
338
+ *
339
+ * @param event Its type.
340
+ * @param params The names the body is called with, `state` first.
341
+ */
342
+ export function eventLines(event: string, params: readonly string[]): EventLines {
343
+ if (event === "any") {
344
+ return { head: "declare const event: any;\n", parameter: "any" };
345
+ }
346
+ // A parameter called `event` hides the declared one, so it cannot be typed by the name it hides.
347
+ return params[1] === "event"
348
+ ? { head: EVENT_GLOBALS, parameter: event }
349
+ : { head: `${EVENT_GLOBALS}declare const event: ${event};\n`, parameter: "typeof event" };
350
+ }
351
+
352
+ /** The scope that declares a name a `$ref` reads, from the innermost out. */
353
+ function declaringScope(scope: StateScope, key: string): StateScope | null {
354
+ for (let at: StateScope | null = scope; at; at = at.parent) {
355
+ if (Object.hasOwn(stateOf(at.node) ?? {}, key)) {
356
+ return at;
357
+ }
358
+ }
359
+ return null;
360
+ }
361
+
362
+ /** One `on*` binding that names a `state` function. */
363
+ interface Binding {
364
+ /** The function it names, as the pointer to its entry. */
365
+ entry: string;
366
+ key: string;
367
+ type: string;
368
+ }
369
+
370
+ /**
371
+ * The event each `state` function is given, for those the document binds as handlers and nothing
372
+ * else.
373
+ *
374
+ * A function a handler names by `$ref` receives that binding's event, and one bound in several
375
+ * places receives any of theirs. A function whose name is written anywhere else in the document (a
376
+ * body, a template, the script, another reference) may be called with something that is no event,
377
+ * so it is left out, and its parameters stay `any`.
378
+ *
379
+ * @param doc The document.
380
+ * @returns The type by the pointer to each function's entry.
381
+ */
382
+ export function boundEvents(doc: JsonObject): Map<string, string> {
383
+ const bindings: Binding[] = [];
384
+ const refs = new Set<JsonValue>();
385
+ eachSite(doc, (node, _pointer, site) => {
386
+ for (const [key, value] of Object.entries(node)) {
387
+ const path = key.startsWith("on") && isRef(value) ? pathUnder(value.$ref, "#/state") : null;
388
+ const [name, ...deeper] = path ?? [];
389
+ const declared = name === undefined ? null : declaringScope(site.scope, name);
390
+ if (name !== undefined && declared && deeper.length === 0) {
391
+ const entry = pointerInto(pointerInto(declared.pointer, "state"), name);
392
+ if (isFunctionDef(stateOf(declared.node)?.[name])) {
393
+ bindings.push({ entry, key: name, type: eventType(node, key, site) });
394
+ refs.add(value);
395
+ }
396
+ }
397
+ }
398
+ });
399
+ if (bindings.length === 0) {
400
+ return new Map();
401
+ }
402
+ // Every string the document holds, keys and the bindings themselves left out.
403
+ const strings: string[] = [];
404
+ const gather = (value: JsonValue | undefined): void => {
405
+ if (isString(value)) {
406
+ strings.push(value);
407
+ } else if (Array.isArray(value)) {
408
+ for (const item of value) {
409
+ gather(item);
410
+ }
411
+ } else if (isJsonObject(value) && !refs.has(value)) {
412
+ for (const item of Object.values(value)) {
413
+ gather(item);
414
+ }
415
+ }
416
+ };
417
+ gather(doc);
418
+ const said = strings.join("\n");
419
+ const types = new Map<string, Set<string>>();
420
+ for (const { entry, key, type } of bindings) {
421
+ const escaped = key.replaceAll(/[$()*+.?[\\\]^{|}]/gu, String.raw`\$&`);
422
+ if (!new RegExp(String.raw`(?<![\w$])${escaped}(?![\w$])`, "u").test(said)) {
423
+ types.set(entry, (types.get(entry) ?? new Set()).add(type));
424
+ }
425
+ }
426
+ return new Map([...types].map(([entry, each]) => [entry, [...each].join(" | ")]));
427
+ }
428
+
429
+ /** A string the runtime reads as a template (`spec.md` §6), and where it stands. */
430
+ export interface TemplateSite {
431
+ pointer: string;
432
+ site: Site;
433
+ text: string;
434
+ }
435
+
436
+ /** The handler an `on*` member binds, when a `$ref` is what binds it. */
437
+ export interface HandlerBinding {
438
+ element: JsonObject;
439
+ key: string;
440
+ }
441
+
442
+ /** A `$ref` into `state` or into the row of a map, and where it stands. */
443
+ export interface RefSite {
444
+ /** The handler it binds, or null when it is read as a value. */
445
+ binding: HandlerBinding | null;
446
+ /** The members it names, outermost first, each with where in the `$ref` it is written. */
447
+ path: RefSegment[];
448
+ /** Where the object holding `$ref` is. */
449
+ pointer: string;
450
+ /** What comes before the first member: `#/state/`, `#/`, or nothing. */
451
+ prefix: string;
452
+ ref: string;
453
+ site: Site;
454
+ }
455
+
456
+ /** One member a `$ref` names. */
457
+ export interface RefSegment {
458
+ /** One past its last character in the `$ref`. */
459
+ end: number;
460
+ /** The member, unescaped. */
461
+ key: string;
462
+ start: number;
463
+ }
464
+
465
+ /** What one object of a document says outside its functions that a checker reads. */
466
+ export interface Expressions {
467
+ refs: RefSite[];
468
+ templates: TemplateSite[];
469
+ }
470
+
471
+ /** The members a `$ref` names after a prefix, each with where it is written. */
472
+ function segmentsAfter(ref: string, prefix: string): RefSegment[] {
473
+ const path: RefSegment[] = [];
474
+ let start = prefix.length;
475
+ for (const part of ref.slice(prefix.length).split("/")) {
476
+ const [key = part] = segments(part);
477
+ path.push({ end: start + part.length, key, start });
478
+ start += part.length + 1;
479
+ }
480
+ return path;
481
+ }
482
+
483
+ /** Read a `$ref` as a path into `state` or a row, or null when it names something else. */
484
+ function readRef(ref: string): Pick<RefSite, "path" | "prefix"> | null {
485
+ // Both spellings of a row, as the runtime reads both.
486
+ const prefix = ["#/state/", "#/$map/", "$map/"].find((start) => ref.startsWith(start));
487
+ if (prefix === undefined) {
488
+ return null;
489
+ }
490
+ const before = prefix === "#/state/" ? prefix : prefix.slice(0, -"$map/".length);
491
+ const path = segmentsAfter(ref, before);
492
+ // A path with nothing between two slashes names no member, and is the runtime's to refuse.
493
+ return path.some(({ key }) => key === "") ? null : { path, prefix: before };
494
+ }
495
+
496
+ /**
497
+ * What one object of a document says outside its functions: each `${}` template it holds, and each
498
+ * `$ref` into `state` or the row of a map.
499
+ *
500
+ * A template is a string of an element: a member, an attribute, a `style` value, a child, a value
501
+ * handed down as `$props`, or an entry of its `state`. A string anywhere else is data, and is not
502
+ * read, because a `${` in data is text. A `$ref` is read wherever it is written, since nothing else
503
+ * is spelled that way, and one that names a file, a definition or the window is left out.
504
+ *
505
+ * @param node The object, as `eachSite` visits it.
506
+ * @param pointer Where it is.
507
+ * @param site What surrounds it.
508
+ * @returns Both, in the order the object holds them.
509
+ */
510
+ export function expressionsAt(node: JsonObject, pointer: string, site: Site): Expressions {
511
+ const found: Expressions = { refs: [], templates: [] };
512
+ // The root is an element whether or not it names a tag.
513
+ const element = pointer === "" || isString(node.tagName);
514
+ const template = (value: JsonValue | undefined, at: string): void => {
515
+ if (isString(value) && value.includes("${")) {
516
+ found.templates.push({ pointer: at, site, text: value });
517
+ }
518
+ };
519
+ const refAt = (value: JsonValue | undefined, at: string, binding: HandlerBinding | null) => {
520
+ const read = isRef(value) ? readRef(value.$ref) : null;
521
+ if (isRef(value) && read) {
522
+ found.refs.push({ ...read, binding, pointer: at, ref: value.$ref, site });
523
+ }
524
+ return isRef(value);
525
+ };
526
+ /** The strings an object holds directly, under names that are its own. */
527
+ const members = (value: JsonObject, at: string): void => {
528
+ for (const [name, item] of Object.entries(value)) {
529
+ if (!name.startsWith("$")) {
530
+ template(item, pointerInto(at, name));
531
+ }
532
+ }
533
+ };
534
+ /** A value `eachSite` does not walk: each `$ref` in it, and each string where those are templates. */
535
+ const within = (value: JsonValue | undefined, at: string, templates: boolean): void => {
536
+ if (refAt(value, at, null)) {
537
+ return;
538
+ }
539
+ if (templates) {
540
+ template(value, at);
541
+ }
542
+ if (Array.isArray(value) || isJsonObject(value)) {
543
+ for (const [key, item] of Object.entries(value)) {
544
+ within(item, pointerInto(at, key), templates);
545
+ }
546
+ }
547
+ };
548
+ for (const [key, value] of Object.entries(node)) {
549
+ const at = pointerInto(pointer, key);
550
+ const handler = key.startsWith("on");
551
+ if (refAt(value, at, element && handler ? { element: node, key } : null)) {
552
+ continue;
553
+ }
554
+ if (key === "state" && isJsonObject(value)) {
555
+ for (const [name, entry] of Object.entries(value)) {
556
+ const entryAt = pointerInto(at, name);
557
+ // An entry that is a string is a template, and so is what configures a prototype.
558
+ template(entry, entryAt);
559
+ if (isJsonObject(entry) && isString(entry.$prototype) && !isFunctionDef(entry)) {
560
+ members(entry, entryAt);
561
+ }
562
+ // Anything deeper is data, in which only a `$ref` is read.
563
+ within(entry, entryAt, false);
564
+ }
565
+ } else if (key === "style") {
566
+ within(value, at, element);
567
+ } else if (Array.isArray(value)) {
568
+ for (const [index, item] of value.entries()) {
569
+ const child = pointerInto(at, index);
570
+ if (!refAt(item, child, null) && element && key === "children") {
571
+ template(item, child);
572
+ }
573
+ }
574
+ } else if (element && (key === "attributes" || key === "$props") && isJsonObject(value)) {
575
+ members(value, at);
576
+ } else if (element && !handler && !key.startsWith("$") && key !== "tagName") {
577
+ template(value, at);
578
+ }
579
+ }
580
+ return found;
581
+ }
@@ -10,6 +10,12 @@ import type { JxDocument } from "../../types.ts";
10
10
  import { parseJxDocument } from "../parse.ts";
11
11
  import { parseJxn } from "./parse.ts";
12
12
 
13
+ /**
14
+ * The language id an editor knows Jx Notation by. It is the same in each, so that a setting or a
15
+ * theme written against one reads in another.
16
+ */
17
+ export const JXN_LANGUAGE_ID = "jxn";
18
+
13
19
  /**
14
20
  * Whether a path names a `.jxn` file.
15
21
  *
@@ -71,6 +71,7 @@ export const EVENT_HANDLER_NAMES: ReadonlySet<string> = new Set([
71
71
  "oncontrollerchange",
72
72
  "oncookiechange",
73
73
  "oncopy",
74
+ "oncsrcchange",
74
75
  "oncuechange",
75
76
  "oncurrententrychange",
76
77
  "oncurrentscreenchange",
@@ -122,6 +123,9 @@ export const EVENT_HANDLER_NAMES: ReadonlySet<string> = new Set([
122
123
  "onhashchange",
123
124
  "onicecandidate",
124
125
  "onicecandidateerror",
126
+ "onicecandidatepairadd",
127
+ "onicecandidatepairnominate",
128
+ "onicecandidatepairremove",
125
129
  "oniceconnectionstatechange",
126
130
  "onicegatheringstatechange",
127
131
  "oninput",
@@ -258,6 +262,7 @@ export const EVENT_HANDLER_NAMES: ReadonlySet<string> = new Set([
258
262
  "onsqueeze",
259
263
  "onsqueezeend",
260
264
  "onsqueezestart",
265
+ "onssrcchange",
261
266
  "onstalled",
262
267
  "onstart",
263
268
  "onstartstreaming",
package/src/jxn/index.ts CHANGED
@@ -6,13 +6,25 @@
6
6
  * @docs framework/concepts/jxn-files
7
7
  */
8
8
  export { classifyJxn } from "./classify.ts";
9
- export { isJxnPath, isNativeDocumentPath, parseNativeDocument } from "./document.ts";
9
+ export {
10
+ isJxnPath,
11
+ isNativeDocumentPath,
12
+ JXN_LANGUAGE_ID,
13
+ parseNativeDocument,
14
+ } from "./document.ts";
10
15
  export type { JxnKind } from "./classify.ts";
11
16
  export { JxnError } from "./errors.ts";
12
17
  export type { JxnSpan } from "./errors.ts";
13
18
  export { countJxnComments, lintJxn } from "./lint.ts";
14
19
  export type { JxnWarning } from "./lint.ts";
15
- export { kdlProblem, parseJxn, parseJxnWithSpans } from "./parse.ts";
20
+ export {
21
+ JXN_ANNOTATIONS,
22
+ kdlProblem,
23
+ parseJxn,
24
+ parseJxnWithSpans,
25
+ parseJxnWithTokens,
26
+ } from "./parse.ts";
27
+ export type { JxnTokens } from "./parse.ts";
16
28
  export { reconcileJxn } from "./reconcile.ts";
17
29
  export type { JxnReader, JxnReading } from "./reconcile.ts";
18
30
  export { serializeJxn } from "./serialize.ts";