@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/src/jxn/parse.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * replaced, and its own `toJson` is not used, because it turns `x (ref)"p"` into `"p"`, silently.
10
10
  */
11
11
  import { InvalidKdlError, getLocation, parse } from "@bgotink/kdl";
12
- import type { Document, Entry, Node } from "@bgotink/kdl";
12
+ import type { Document, Entry, Identifier, Node, Value } from "@bgotink/kdl";
13
13
  import type { JsonObject, JsonValue, JxDocument } from "../../types.ts";
14
14
  import { isJsonObject, isNumber, isString } from "../guards.ts";
15
15
  import { normalizeObject } from "../parse.ts";
@@ -21,17 +21,48 @@ import { EVENT_HANDLER_NAMES } from "./event-handlers.ts";
21
21
  /** Bytes KDL's parser admits that a person should never have in a text file. */
22
22
  const RAW_CONTROL = /(?![\t\n\v\f\r])\p{Cc}/u;
23
23
 
24
- /** The annotations the grammar gives meaning to. Any other is a mistake, and not a thing to ignore. */
25
- const ANNOTATIONS = new Set(["array", "fn", "object", "on", "ref"]);
24
+ /**
25
+ * The annotations the grammar gives meaning to. Any other is a mistake, and not a thing to ignore.
26
+ *
27
+ * Exported for an editor's grammar, which paints the same set and is tested against this one.
28
+ */
29
+ export const JXN_ANNOTATIONS: ReadonlySet<string> = new Set([
30
+ "array",
31
+ "export",
32
+ "fn",
33
+ "object",
34
+ "on",
35
+ "ref",
36
+ "server",
37
+ ]);
26
38
 
27
39
  /** A child node name that means something other than an element. */
28
40
  const RESERVED_CHILDREN = new Set(["attributes", "script", "state", "style"]);
29
41
 
42
+ /**
43
+ * Where single tokens are, by JSON Pointer (`spec.md` §23.3).
44
+ *
45
+ * A span says where a whole node or entry is, which is what a save needs. A type checker needs the
46
+ * token itself: a string with its quotes, so each character of a body can be put back where it was
47
+ * written, and a name alone, so renaming it in an editor rewrites the name and nothing beside it.
48
+ *
49
+ * A pointer here is built from a key as the file spells it, and a document's keys are NFC. A key
50
+ * the two disagree about is not found, which costs that name its mapping and never misplaces one.
51
+ */
52
+ export interface JxnTokens {
53
+ /** The name of a keyed member: a node's name, or a property's. */
54
+ names: Map<string, JxnSpan>;
55
+ /** The one value a pointer holds, as written. */
56
+ values: Map<string, JxnSpan>;
57
+ }
58
+
30
59
  /** What one read of a file is building. */
31
60
  export interface Reader {
32
61
  /** Where each thing came from, by JSON Pointer, when that was asked for. */
33
62
  spans: Map<string, JxnSpan> | null;
34
63
  text: string;
64
+ /** Where single tokens are, when that was asked for. */
65
+ tokens?: JxnTokens | undefined;
35
66
  }
36
67
 
37
68
  /** Set a key, refusing a repeat and a key that would be a prototype rather than data. */
@@ -58,8 +89,8 @@ export function failure(reader: Reader, at: Entry | Node, message: string): JxnE
58
89
  return new JxnError(message, start === undefined ? null : locate(reader.text, start));
59
90
  }
60
91
 
61
- /** Where a node or entry is, from the locations the parser stored. */
62
- function spanOf(at: Entry | Node): JxnSpan | null {
92
+ /** Where a node, an entry or one token of either is, from the locations the parser stored. */
93
+ function spanOf(at: Entry | Identifier | Node | Value): JxnSpan | null {
63
94
  const location = getLocation(at);
64
95
  return location ? { end: location.end.offset, start: location.start.offset } : null;
65
96
  }
@@ -72,6 +103,35 @@ export function mark(reader: Reader, pointer: string, at: Entry | Node): void {
72
103
  }
73
104
  }
74
105
 
106
+ /** Record where a pointer's name was written. */
107
+ function markName(reader: Reader, pointer: string, at: Identifier | null): void {
108
+ const span = reader.tokens && at ? spanOf(at) : null;
109
+ if (span) {
110
+ reader.tokens?.names.set(pointer, span);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Record where a pointer's one value was written.
116
+ *
117
+ * The parser's location for an argument starts at its annotation, so `(ref)"#/state/a"` would be a
118
+ * token that begins five characters before the string does. What it kept of how the value was
119
+ * spelled is the token itself, and the token ends where the location does.
120
+ */
121
+ function markValue(reader: Reader, pointer: string, at: Entry | undefined): void {
122
+ const span = reader.tokens && at ? spanOf(at.value) : null;
123
+ if (span) {
124
+ const written = at?.value.representation;
125
+ const start = written === undefined ? span.start : span.end - written.length;
126
+ reader.tokens?.values.set(pointer, { end: span.end, start });
127
+ }
128
+ }
129
+
130
+ /** The entries of a node that carry no name, in order. */
131
+ function positionalEntries(node: Node): Entry[] {
132
+ return node.entries.filter((entry) => !entry.name);
133
+ }
134
+
75
135
  /** A JSON Pointer token. */
76
136
  export function token(key: string): string {
77
137
  return key.replaceAll("~", "~0").replaceAll("/", "~1");
@@ -80,7 +140,7 @@ export function token(key: string): string {
80
140
  /** A node's annotation, checked. */
81
141
  export function annotationOf(reader: Reader, node: Node): string | null {
82
142
  const tag = node.getTag();
83
- if (tag !== null && !ANNOTATIONS.has(tag)) {
143
+ if (tag !== null && !JXN_ANNOTATIONS.has(tag)) {
84
144
  throw failure(reader, node, `(${tag}) is not an annotation Jx Notation has`);
85
145
  }
86
146
  if (tag === "ref") {
@@ -140,7 +200,9 @@ function readFunction(reader: Reader, node: Node, pointer: string): JsonObject {
140
200
  const kids = childrenOf(node);
141
201
  for (const entry of properties(node)) {
142
202
  setKey(fn, nameOf(entry), valueOf(reader, entry), reader, entry);
203
+ markValue(reader, `${pointer}/${token(nameOf(entry))}`, entry);
143
204
  }
205
+ const written = reader.tokens ? positionalEntries(node) : [];
144
206
  if (kids.length > 0) {
145
207
  if (values.length > 0) {
146
208
  setKey(fn, "parameters", values, reader, node);
@@ -153,28 +215,150 @@ function readFunction(reader: Reader, node: Node, pointer: string): JsonObject {
153
215
  setKey(fn, "parameters", parameters, reader, node);
154
216
  }
155
217
  setKey(fn, "body", body, reader, node);
218
+ markValue(reader, `${pointer}/body`, written.pop());
219
+ }
220
+ for (const [index, entry] of written.entries()) {
221
+ markValue(reader, `${pointer}/parameters/${index}`, entry);
222
+ }
223
+ mark(reader, pointer, node);
224
+ return fn;
225
+ }
226
+
227
+ /**
228
+ * An export of the document's client module (`spec.md` §23.2): a Function whose `$src` is
229
+ * `#client`. The node's name is the export unless `$export` says another.
230
+ */
231
+ function readExport(reader: Reader, node: Node, pointer: string): JsonObject {
232
+ const fn: JsonObject = { $prototype: "Function", $src: "#client" };
233
+ if (positionals(reader, node).length > 0 || childrenOf(node).length > 0) {
234
+ throw failure(
235
+ reader,
236
+ node,
237
+ "(export) names a function the document's script exports, and holds no body of its own",
238
+ );
239
+ }
240
+ for (const entry of properties(node)) {
241
+ setKey(fn, nameOf(entry), valueOf(reader, entry), reader, entry);
242
+ markValue(reader, `${pointer}/${token(nameOf(entry))}`, entry);
243
+ }
244
+ if (!Object.hasOwn(fn, "$export")) {
245
+ fn.$export = node.getName().normalize("NFC");
156
246
  }
157
247
  mark(reader, pointer, node);
158
248
  return fn;
159
249
  }
160
250
 
161
- /** The items of a block made of `-` nodes. */
162
- function readItems(reader: Reader, kids: readonly Node[], pointer: string): JsonValue[] {
251
+ /**
252
+ * A function of the document's server module (`spec.md` §23.2, §11.4): a `timing: "server"` entry
253
+ * whose `$src` is `#server`, exported under the node's name, with the node's named values as its
254
+ * `arguments`.
255
+ */
256
+ function readServer(reader: Reader, node: Node, pointer: string): JsonObject {
257
+ if (positionals(reader, node).length > 0 || childrenOf(node).length > 0) {
258
+ throw failure(
259
+ reader,
260
+ node,
261
+ "(server) names a function the document's server script exports. Its arguments are written name=value",
262
+ );
263
+ }
264
+ const entry: JsonObject = {
265
+ $export: node.getName().normalize("NFC"),
266
+ $src: "#server",
267
+ timing: "server",
268
+ };
269
+ const named = properties(node);
270
+ if (named.length > 0) {
271
+ const args: JsonObject = {};
272
+ for (const argument of named) {
273
+ const key = nameOf(argument);
274
+ setKey(args, key, valueOf(reader, argument), reader, argument);
275
+ mark(reader, `${pointer}/arguments/${token(key)}`, argument);
276
+ markValue(reader, `${pointer}/arguments/${token(key)}`, argument);
277
+ }
278
+ entry.arguments = args;
279
+ }
280
+ mark(reader, pointer, node);
281
+ return entry;
282
+ }
283
+
284
+ /** The `$script` objects made from `script` nodes, which a second `script` node may add a side to. */
285
+ const SCRIPT_BLOCKS = new WeakSet<JsonObject>();
286
+
287
+ /**
288
+ * A `script` node of the root element: one of the document's two modules (`spec.md` §23.2).
289
+ *
290
+ * @param reader The read.
291
+ * @param node The `script` node.
292
+ * @param element The root element, which gains `$script.client` or `$script.server`.
293
+ */
294
+ function readScript(reader: Reader, node: Node, element: JsonObject): void {
295
+ const named: JsonObject = {};
296
+ for (const entry of properties(node)) {
297
+ const key = nameOf(entry);
298
+ setKey(named, key, valueOf(reader, entry), reader, entry);
299
+ if (key !== "timing" || named[key] !== "server") {
300
+ throw failure(reader, entry, "A script takes timing=server, and nothing else");
301
+ }
302
+ }
303
+ const values = positionals(reader, node);
304
+ const [text] = values;
305
+ if (values.length !== 1 || !isString(text) || childrenOf(node).length > 0) {
306
+ throw failure(reader, node, "A script holds one string, which is the text of its module");
307
+ }
308
+ const side = Object.hasOwn(named, "timing") ? "server" : "client";
309
+ let scripts = element.$script;
310
+ if (!isJsonObject(scripts) || !SCRIPT_BLOCKS.has(scripts)) {
311
+ scripts = {};
312
+ SCRIPT_BLOCKS.add(scripts);
313
+ setKey(element, "$script", scripts, reader, node);
314
+ }
315
+ setKey(scripts, side, text, reader, node);
316
+ mark(reader, `/$script/${side}`, node);
317
+ markValue(reader, `/$script/${side}`, positionalEntries(node)[0]);
318
+ }
319
+
320
+ /** Record where each value of a list written on one line is, under the index it has in the list. */
321
+ function markItems(reader: Reader, pointer: string, node: Node): void {
322
+ for (const [index, entry] of (reader.tokens ? positionalEntries(node) : []).entries()) {
323
+ markValue(reader, `${pointer}/${index}`, entry);
324
+ }
325
+ }
326
+
327
+ /**
328
+ * The items of a block made of `-` nodes.
329
+ *
330
+ * @param reader The file being read.
331
+ * @param kids The block.
332
+ * @param pointer Where the list is.
333
+ * @param before How many items the list holds ahead of the block, written on the node's own line.
334
+ */
335
+ function readItems(
336
+ reader: Reader,
337
+ kids: readonly Node[],
338
+ pointer: string,
339
+ before = 0,
340
+ ): JsonValue[] {
163
341
  return kids.map((kid, index) => {
164
342
  if (kid.getName() !== "-") {
165
343
  throw failure(reader, kid, `A list holds only "-" items, and this is "${kid.getName()}"`);
166
344
  }
167
- return readValue(reader, kid, `${pointer}/${index}`);
345
+ return readValue(reader, kid, `${pointer}/${before + index}`);
168
346
  });
169
347
  }
170
348
 
171
- /** A JSON block: JSON-in-KDL, plus `(ref)` and `(fn)`. */
349
+ /** A JSON block: JSON-in-KDL, plus `(ref)`, `(fn)`, `(export)` and `(server)`. */
172
350
  export function readValue(reader: Reader, node: Node, pointer: string): JsonValue {
173
351
  const tag = annotationOf(reader, node);
174
352
  mark(reader, pointer, node);
175
353
  if (tag === "fn") {
176
354
  return readFunction(reader, node, pointer);
177
355
  }
356
+ if (tag === "export") {
357
+ return readExport(reader, node, pointer);
358
+ }
359
+ if (tag === "server") {
360
+ return readServer(reader, node, pointer);
361
+ }
178
362
  const values = positionals(reader, node);
179
363
  const props = properties(node);
180
364
  const kids = childrenOf(node);
@@ -182,19 +366,22 @@ export function readValue(reader: Reader, node: Node, pointer: string): JsonValu
182
366
  if (props.length > 0) {
183
367
  throw failure(reader, node, "(array) holds items, and these are named");
184
368
  }
185
- return [...values, ...readItems(reader, kids, pointer)];
369
+ markItems(reader, pointer, node);
370
+ return [...values, ...readItems(reader, kids, pointer, values.length)];
186
371
  }
187
372
  if (tag !== "object") {
188
373
  if (values.length > 1) {
189
374
  if (props.length > 0 || kids.length > 0) {
190
375
  throw failure(reader, node, "A list of values cannot also have names or a block");
191
376
  }
377
+ markItems(reader, pointer, node);
192
378
  return values;
193
379
  }
194
380
  if (values.length === 1) {
195
381
  if (props.length > 0 || kids.length > 0) {
196
382
  throw failure(reader, node, "A value cannot also have names or a block");
197
383
  }
384
+ markValue(reader, pointer, positionalEntries(node)[0]);
198
385
  return values[0] ?? null;
199
386
  }
200
387
  if (kids.length > 0 && props.length === 0 && kids.every((kid) => kid.getName() === "-")) {
@@ -208,10 +395,13 @@ export function readValue(reader: Reader, node: Node, pointer: string): JsonValu
208
395
  const key = nameOf(entry);
209
396
  setKey(object, key, valueOf(reader, entry), reader, entry);
210
397
  mark(reader, `${pointer}/${token(key)}`, entry);
398
+ markName(reader, `${pointer}/${token(key)}`, entry.name);
399
+ markValue(reader, `${pointer}/${token(key)}`, entry);
211
400
  }
212
401
  for (const kid of kids) {
213
402
  const key = kid.getName();
214
403
  setKey(object, key, readValue(reader, kid, `${pointer}/${token(key)}`), reader, kid);
404
+ markName(reader, `${pointer}/${token(key)}`, kid.name);
215
405
  }
216
406
  return object;
217
407
  }
@@ -227,6 +417,7 @@ export function readStyle(reader: Reader, node: Node, pointer: string): JsonObje
227
417
  const key = nameOf(entry);
228
418
  setKey(style, key, valueOf(reader, entry), reader, entry);
229
419
  mark(reader, `${pointer}/${token(key)}`, entry);
420
+ markValue(reader, `${pointer}/${token(key)}`, entry);
230
421
  }
231
422
  for (const kid of childrenOf(node)) {
232
423
  const key = kid.getName();
@@ -257,6 +448,7 @@ export function readStyle(reader: Reader, node: Node, pointer: string): JsonObje
257
448
  }
258
449
  setKey(style, key, values[0] ?? null, reader, kid);
259
450
  mark(reader, child, kid);
451
+ markValue(reader, child, positionalEntries(kid)[0]);
260
452
  } else {
261
453
  setKey(style, key, readStyle(reader, kid, child), reader, kid);
262
454
  }
@@ -282,7 +474,9 @@ function readArrayRow(reader: Reader, node: Node, pointer: string): JsonObject {
282
474
  }
283
475
  for (const entry of properties(node)) {
284
476
  const key = nameOf(entry);
285
- setKey(row, key.startsWith(".") ? key.slice(1) : key, valueOf(reader, entry), reader, entry);
477
+ const member = key.startsWith(".") ? key.slice(1) : key;
478
+ setKey(row, member, valueOf(reader, entry), reader, entry);
479
+ markValue(reader, `${pointer}/${token(member)}`, entry);
286
480
  }
287
481
  if (childrenOf(node).length > 0) {
288
482
  setKey(row, "map", readOnlyElement(reader, node, `${pointer}/map`), reader, node);
@@ -299,6 +493,7 @@ function readChild(reader: Reader, node: Node, pointer: string): JsonValue {
299
493
  throw failure(reader, node, `"-" is one piece of text or one (ref)`);
300
494
  }
301
495
  mark(reader, pointer, node);
496
+ markValue(reader, pointer, positionalEntries(node)[0]);
302
497
  return values[0] ?? null;
303
498
  }
304
499
  if (name === "$array") {
@@ -355,6 +550,7 @@ function readElement(reader: Reader, node: Node, pointer: string): JsonObject {
355
550
  }
356
551
  if (text !== undefined) {
357
552
  setKey(element, "textContent", text, reader, node);
553
+ markValue(reader, `${pointer}/textContent`, positionalEntries(node)[1]);
358
554
  }
359
555
  } else {
360
556
  if (name.startsWith("$")) {
@@ -367,6 +563,7 @@ function readElement(reader: Reader, node: Node, pointer: string): JsonObject {
367
563
  if (values[0] !== undefined) {
368
564
  setKey(element, "textContent", values[0], reader, node);
369
565
  mark(reader, `${pointer}/textContent`, node);
566
+ markValue(reader, `${pointer}/textContent`, positionalEntries(node)[0]);
370
567
  }
371
568
  }
372
569
  readElementEntries(reader, node, element, pointer);
@@ -409,9 +606,11 @@ function readElementEntries(
409
606
  const member = key.startsWith(".") ? key.slice(1) : key;
410
607
  setKey(element, member, value, reader, entry);
411
608
  mark(reader, `${pointer}/${token(member)}`, entry);
609
+ markValue(reader, `${pointer}/${token(member)}`, entry);
412
610
  } else if (key.startsWith("on") && entryTag === "ref") {
413
611
  setKey(element, key, value, reader, entry);
414
612
  mark(reader, `${pointer}/${token(key)}`, entry);
613
+ markValue(reader, `${pointer}/${token(key)}`, entry);
415
614
  } else if (key.startsWith("on") && EVENT_HANDLER_NAMES.has(key)) {
416
615
  throw failure(
417
616
  reader,
@@ -421,6 +620,7 @@ function readElementEntries(
421
620
  } else {
422
621
  setKey(attributesOf(element), key, value, reader, entry);
423
622
  mark(reader, `${pointer}/attributes/${token(key)}`, entry);
623
+ markValue(reader, `${pointer}/attributes/${token(key)}`, entry);
424
624
  }
425
625
  }
426
626
  }
@@ -447,7 +647,17 @@ function readElementChild(
447
647
  // `(on)` has done its job by saying this is a handler, and what it holds is a JSON block.
448
648
  setKey(element, name, readValue(reader, kid, member(name)), reader, kid);
449
649
  } else if (name === "script") {
450
- throw failure(reader, kid, `"script" is reserved. A <script> element is $element "script"`);
650
+ if (pointer !== "") {
651
+ throw failure(
652
+ reader,
653
+ kid,
654
+ `"script" is the document's module, and is written in its root element. A <script> element is $element "script"`,
655
+ );
656
+ }
657
+ if (tag !== null) {
658
+ throw failure(reader, kid, `(${tag}) cannot annotate a script`);
659
+ }
660
+ readScript(reader, kid, element);
451
661
  } else if (name === "state") {
452
662
  setKey(element, "state", readValue(reader, kid, member("state")), reader, kid);
453
663
  } else if (name === "style") {
@@ -590,3 +800,14 @@ export function parseJxnWithSpans(text: string) {
590
800
  const spans = new Map<string, JxnSpan>();
591
801
  return { document: readDocument({ spans, text }), spans };
592
802
  }
803
+
804
+ /**
805
+ * Read a `.jxn` document, and where the single tokens a type checker reads were written.
806
+ *
807
+ * @param text The file.
808
+ * @throws {JxnError} When it is not one.
809
+ */
810
+ export function parseJxnWithTokens(text: string) {
811
+ const tokens: JxnTokens = { names: new Map(), values: new Map() };
812
+ return { document: readDocument({ spans: null, text, tokens }), tokens };
813
+ }