aontu 0.55.0 → 0.56.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.
Files changed (84) hide show
  1. package/README.md +3 -3
  2. package/dist/agentsmd.d.ts +1 -0
  3. package/dist/agentsmd.js +10 -3
  4. package/dist/agentsmd.js.map +1 -1
  5. package/dist/aontu.d.ts +3 -2
  6. package/dist/aontu.js +5 -2
  7. package/dist/aontu.js.map +1 -1
  8. package/dist/cli.d.ts +5 -2
  9. package/dist/cli.js +236 -49
  10. package/dist/cli.js.map +1 -1
  11. package/dist/diff.d.ts +1 -0
  12. package/dist/diff.js +3 -2
  13. package/dist/diff.js.map +1 -1
  14. package/dist/format.d.ts +17 -0
  15. package/dist/format.js +1000 -0
  16. package/dist/format.js.map +1 -0
  17. package/dist/hints.js +4 -0
  18. package/dist/hints.js.map +1 -1
  19. package/dist/jsonschema.d.ts +1 -0
  20. package/dist/jsonschema.js +3 -2
  21. package/dist/jsonschema.js.map +1 -1
  22. package/dist/lang.js +64 -5
  23. package/dist/lang.js.map +1 -1
  24. package/dist/lsp.d.ts +2 -1
  25. package/dist/lsp.js +1 -1
  26. package/dist/lsp.js.map +1 -1
  27. package/dist/mcp.js +6 -4
  28. package/dist/mcp.js.map +1 -1
  29. package/dist/patch.d.ts +1 -0
  30. package/dist/patch.js +1 -0
  31. package/dist/patch.js.map +1 -1
  32. package/dist/query.d.ts +1 -0
  33. package/dist/query.js +4 -3
  34. package/dist/query.js.map +1 -1
  35. package/dist/reach.d.ts +1 -0
  36. package/dist/reach.js +3 -2
  37. package/dist/reach.js.map +1 -1
  38. package/dist/relation.d.ts +1 -0
  39. package/dist/relation.js +3 -2
  40. package/dist/relation.js.map +1 -1
  41. package/dist/std.js +2 -1
  42. package/dist/std.js.map +1 -1
  43. package/dist/subsume.d.ts +1 -0
  44. package/dist/subsume.js +3 -2
  45. package/dist/subsume.js.map +1 -1
  46. package/dist/trim.d.ts +1 -0
  47. package/dist/trim.js +3 -2
  48. package/dist/trim.js.map +1 -1
  49. package/dist/tsconfig.tsbuildinfo +1 -1
  50. package/dist/type.d.ts +1 -0
  51. package/dist/type.js.map +1 -1
  52. package/dist/utility.d.ts +8 -2
  53. package/dist/utility.js +9 -1
  54. package/dist/utility.js.map +1 -1
  55. package/dist/vet.d.ts +1 -0
  56. package/dist/vet.js +1 -1
  57. package/dist/vet.js.map +1 -1
  58. package/dist/view.d.ts +2 -1
  59. package/dist/view.js +379 -8
  60. package/dist/view.js.map +1 -1
  61. package/grammar/aontu.abnf +155 -0
  62. package/package.json +2 -1
  63. package/skill/grammar-card.md +5 -2
  64. package/src/agentsmd.ts +15 -3
  65. package/src/aontu.ts +7 -1
  66. package/src/cli.ts +267 -53
  67. package/src/diff.ts +7 -2
  68. package/src/format.ts +1146 -0
  69. package/src/hints.ts +5 -0
  70. package/src/jsonschema.ts +7 -1
  71. package/src/lang.ts +69 -5
  72. package/src/lsp.ts +5 -3
  73. package/src/mcp.ts +6 -4
  74. package/src/patch.ts +7 -0
  75. package/src/query.ts +8 -4
  76. package/src/reach.ts +7 -2
  77. package/src/relation.ts +7 -2
  78. package/src/std.ts +2 -1
  79. package/src/subsume.ts +7 -2
  80. package/src/trim.ts +7 -2
  81. package/src/type.ts +8 -0
  82. package/src/utility.ts +27 -2
  83. package/src/vet.ts +9 -3
  84. package/src/view.ts +442 -9
package/dist/format.js ADDED
@@ -0,0 +1,1000 @@
1
+ "use strict";
2
+ /* Copyright (c) 2026 Richard Rodger, MIT License */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.format = format;
5
+ exports.unifiedDiff = unifiedDiff;
6
+ // THE SOURCE FORMATTER (docs/design/FMT.0.md): `aontu fmt`, in the
7
+ // tradition of gofmt. One agreed form for Aontu source, so that layout
8
+ // is never argued about and a diff shows only what changed.
9
+ //
10
+ // It reads the token stream the parser reads -- the lex subscriber the
11
+ // parser stack exposes -- so it sees what the value tree throws away:
12
+ // comments, blank lines, the quote a string used, the spelling of a
13
+ // number. From that stream it builds a layout tree, decides the shape
14
+ // of every container by the rules of the note's §3, and emits. Before
15
+ // returning it re-parses what it wrote and compares the two parse
16
+ // trees: a formatter that cannot prove its output is the same document
17
+ // refuses rather than return it.
18
+ //
19
+ // This is the syntactic tier only (P1): whitespace, commas, quotes,
20
+ // bare keys, chains and pair elements, none of which changes the parse
21
+ // tree. The lawful tier -- the repeat-the-prefix rewrite that rests on
22
+ // the meet -- is P2, and lands behind its own local check.
23
+ //
24
+ // The Go twin is go/format.go, function for function; the shared
25
+ // behaviour is test/spec/fmt.tsv, executed by both spec runners.
26
+ const aontu_1 = require("./aontu");
27
+ const vet_1 = require("./vet");
28
+ // The packing budget (§3.1). It decides which of two legal spellings
29
+ // to use, one line or several, and nothing else: the formatter never
30
+ // breaks a line, so a value wider than this stays as wide as it is.
31
+ const BUDGET = 80;
32
+ // THE DEPTH BUDGET. The layout is recursive, as the tree it reads is,
33
+ // and the canonical port's stack is finite: past the evaluation budget
34
+ // of 1000 levels -- the depth at which unification itself refuses --
35
+ // the formatter stops reading and refuses, so a pathological document
36
+ // is a finding rather than a crash.
37
+ const MAX_DEPTH = 1000;
38
+ // EVERY INCLUDE RESOLVES TO NOTHING. The formatter reads the file it is
39
+ // given and no other (§3.13), so `@"..."` is answered from memory with
40
+ // an empty source: the directive parses, the include is a token like
41
+ // any other, and no capability is needed because no file is read.
42
+ const stubResolver = ((spec) => ({
43
+ ...spec, kind: 'aon', full: '__fmt__.aon', src: '', found: true, search: [],
44
+ }));
45
+ // ONE ENGINE, ONE SUBSCRIBER. The parser's subscriber list is
46
+ // append-only, so the subscription is made once and writes to
47
+ // whichever sink the current parse installed; the sink is cleared
48
+ // before the parse returns, so the check's re-parse collects nothing.
49
+ let ENGINE;
50
+ let SINK;
51
+ function engine() {
52
+ if (undefined === ENGINE) {
53
+ ENGINE = new aontu_1.Aontu({ resolver: stubResolver });
54
+ ENGINE.lang.jsonic.sub({
55
+ lex: (tkn) => {
56
+ // Spaces carry nothing the layout needs, and the end token
57
+ // arrives once per nested parse -- the stub's empty includes
58
+ // among them -- so both are dropped here rather than skipped
59
+ // everywhere below.
60
+ if (undefined !== SINK && '#SP' !== tkn.name && '#ZZ' !== tkn.name) {
61
+ SINK.push({ name: tkn.name, src: tkn.src, val: tkn.val, sI: tkn.sI });
62
+ }
63
+ },
64
+ });
65
+ }
66
+ return ENGINE;
67
+ }
68
+ // One parse, with the token stream collected when a sink is given. The
69
+ // failure shape is the one every verb reports (`view`'s load).
70
+ function parseDoc(src, path, sink) {
71
+ const aontu = engine();
72
+ const ctx = aontu.ctx({ collect: true });
73
+ SINK = sink;
74
+ let parsed;
75
+ try {
76
+ parsed = aontu.parse(src, undefined === path ? undefined : { path }, ctx);
77
+ }
78
+ finally {
79
+ SINK = undefined;
80
+ }
81
+ if (0 < ctx.err.length) {
82
+ return { errors: [(0, vet_1.failureFinding)(ctx, path, parsed)] };
83
+ }
84
+ return { root: parsed };
85
+ }
86
+ const BINARY = { '#E&': true, '#E|': true, '#E+': true };
87
+ const PREFIX = { '#E*': true, '#E-': true };
88
+ const KEYISH = { '#TX': true, '#ST': true, '#NR': true, '#VL': true };
89
+ const CLOSER = { '#CB': true, '#CS': true, '#E)': true };
90
+ // The parts of one atom: a reference is `$`, dots and segments lexed
91
+ // one by one, and a bare word with a dot in it is the same run; what
92
+ // was adjacent in the source stays glued.
93
+ const GLUE = {
94
+ '#TX': true, '#ST': true, '#NR': true, '#VL': true, '#E.': true, '#E$': true,
95
+ };
96
+ const BARE = /^[A-Za-z_][A-Za-z0-9_]*$/;
97
+ // A single-quoted string becomes double-quoted unless it holds a double
98
+ // quote, which the swap would have to escape (§3.9). The body is copied
99
+ // as written: the escapes are the same under both quotes.
100
+ function normStr(src) {
101
+ if ("'" === src[0]) {
102
+ const body = src.slice(1, -1);
103
+ return body.includes('"') ? src : '"' + body + '"';
104
+ }
105
+ return src;
106
+ }
107
+ function atomText(tok) {
108
+ return '#ST' === tok.name ? normStr(tok.src) : tok.src;
109
+ }
110
+ // A quoted key whose text is a legal bare key is written bare; the
111
+ // keywords are legal keys too (`string: 1` is the key `string`), so no
112
+ // word is reserved. Anything else keeps its spelling.
113
+ function keyText(tok) {
114
+ if ('#ST' === tok.name) {
115
+ return BARE.test(tok.val) ? tok.val : normStr(tok.src);
116
+ }
117
+ return tok.src;
118
+ }
119
+ function newlines(src) {
120
+ return src.split('\n').length - 1;
121
+ }
122
+ class Reader {
123
+ constructor(toks) {
124
+ this.i = 0;
125
+ this.depth = 0;
126
+ // Past the depth budget: the reader answers '' for every token from
127
+ // here on, so every loop unwinds, and the document is refused.
128
+ this.deep = false;
129
+ this.T = toks;
130
+ }
131
+ // The name of the token k ahead, or '' past the end.
132
+ name(k) {
133
+ const t = this.T[this.i + k];
134
+ return this.deep || undefined === t ? '' : t.name;
135
+ }
136
+ // The offset of the next token that is not a line run or a comment.
137
+ significant() {
138
+ let k = 0;
139
+ while ('#LN' === this.name(k) || '#CM' === this.name(k)) {
140
+ k++;
141
+ }
142
+ return k;
143
+ }
144
+ // A key followed by a colon, the optional marker allowed between.
145
+ atKey() {
146
+ return KEYISH[this.name(0)] && ('#CL' === this.name(1) ||
147
+ ('#QM' === this.name(1) && '#CL' === this.name(2)));
148
+ }
149
+ // The entries of a container up to its closer, or of the document up
150
+ // to its end. Comments attach by the rules of §3.7: on the line of
151
+ // the entry that precedes them, or of the opener, they trail it;
152
+ // alone on a line they stand as entries and precede what follows.
153
+ body(close, opened) {
154
+ const body = [];
155
+ let open;
156
+ let last;
157
+ let opener = opened;
158
+ // Nothing since the opener or the last comma: a comma here is an
159
+ // empty element, which the parser reads as nil in a list.
160
+ let gap = true;
161
+ for (;;) {
162
+ const n = this.name(0);
163
+ // The closer, or the end: the parser accepts a container the
164
+ // source never closed (`a: {` is `{"a":{}}`).
165
+ if ('' === n || n === close) {
166
+ break;
167
+ }
168
+ if ('#LN' === n) {
169
+ if (1 < newlines(this.T[this.i].src) && 0 < body.length &&
170
+ 'blank' !== body[body.length - 1].t) {
171
+ body.push({ t: 'blank' });
172
+ }
173
+ last = undefined;
174
+ opener = false;
175
+ this.i++;
176
+ continue;
177
+ }
178
+ if ('#CA' === n) {
179
+ if (gap && '#CS' === close) {
180
+ const nil = { t: 'atom', text: 'nil' };
181
+ body.push(nil);
182
+ last = nil;
183
+ }
184
+ gap = true;
185
+ this.i++;
186
+ continue;
187
+ }
188
+ if ('#CM' === n) {
189
+ const text = this.T[this.i].src;
190
+ if (undefined !== last) {
191
+ last.trail = text;
192
+ }
193
+ else if (opener) {
194
+ open = text;
195
+ }
196
+ else {
197
+ body.push({ t: 'comment', text });
198
+ }
199
+ this.i++;
200
+ continue;
201
+ }
202
+ if (CLOSER[n]) {
203
+ // A closer that is not this container's: the parser ignores a
204
+ // stray one at the root (`a: 1 }` is `{"a":1}`), and so does
205
+ // this.
206
+ this.i++;
207
+ continue;
208
+ }
209
+ const e = this.entry();
210
+ body.push(e);
211
+ last = e;
212
+ opener = false;
213
+ gap = false;
214
+ }
215
+ return { body, open };
216
+ }
217
+ // One entry: an include, a spread, a pair, or -- as a list element or
218
+ // at the root -- a value.
219
+ entry() {
220
+ const n = this.name(0);
221
+ if ('#OD_multisource' === n) {
222
+ const text = '@' + normStr(this.T[this.i + 1].src);
223
+ this.i += 2;
224
+ return { t: 'include', text };
225
+ }
226
+ if ('#E&' === n && '#CL' === this.name(1)) {
227
+ this.i += 2;
228
+ return { t: 'spread', value: this.value() };
229
+ }
230
+ if (this.atKey()) {
231
+ const tok = this.T[this.i];
232
+ const opt = '#QM' === this.name(1);
233
+ this.i += opt ? 3 : 2;
234
+ return { t: 'pair', key: keyText(tok), opt, value: this.value() };
235
+ }
236
+ return this.value();
237
+ }
238
+ // A value: operands and operators up to whatever ends it -- a
239
+ // separator, a closer, the end, or a line run that no operator
240
+ // continues past.
241
+ value() {
242
+ if (MAX_DEPTH < ++this.depth) {
243
+ this.deep = true;
244
+ }
245
+ const v = this.valueAt();
246
+ this.depth--;
247
+ return v;
248
+ }
249
+ valueAt() {
250
+ const items = [];
251
+ for (;;) {
252
+ const n = this.name(0);
253
+ if ('' === n || '#CA' === n || CLOSER[n]) {
254
+ break;
255
+ }
256
+ // An operand directly after an operand is the next element of a
257
+ // list, `[1 -2]`, `[{a:1} {b:2}]`: this value is complete.
258
+ if (!this.open(items) && !BINARY[n] && '#LN' !== n && '#CM' !== n) {
259
+ break;
260
+ }
261
+ if ('#E&' === n && '#CL' === this.name(1)) {
262
+ if (0 === items.length) {
263
+ // A chain through a spread, `a: &: integer`. The braces are
264
+ // the agreed spelling (X-7), so it is read as the map it is.
265
+ this.i += 2;
266
+ return { t: 'map', body: [{ t: 'spread', value: this.value() }] };
267
+ }
268
+ // A sibling spread in a list, `[1 &: 2]`: this value is complete.
269
+ break;
270
+ }
271
+ if ('#LN' === n) {
272
+ // A break the author put before the value, after an operator
273
+ // (`a: 1 &\n 2`) or before one (`a: 1\n | 2`), or after a
274
+ // comment inside the value; anything else ends the value.
275
+ if (this.open(items) || BINARY[this.name(this.significant())]) {
276
+ this.i++;
277
+ continue;
278
+ }
279
+ break;
280
+ }
281
+ if ('#CM' === n) {
282
+ // A comment inside the value: after the colon, after an
283
+ // operator, or on a line the value continues past. Otherwise
284
+ // it trails the statement and the caller attaches it.
285
+ if (this.open(items) || BINARY[this.name(this.significant())]) {
286
+ items.push({ t: 'note', text: this.T[this.i].src });
287
+ this.i++;
288
+ continue;
289
+ }
290
+ break;
291
+ }
292
+ if (BINARY[n]) {
293
+ items.push({
294
+ t: 'op', text: this.T[this.i].src,
295
+ brk: '#LN' === this.name(-1) || '#LN' === this.name(1),
296
+ });
297
+ this.i++;
298
+ continue;
299
+ }
300
+ if (PREFIX[n]) {
301
+ items.push({ t: 'prefix', text: this.T[this.i].src });
302
+ this.i++;
303
+ continue;
304
+ }
305
+ if ('#E(' === n) {
306
+ this.i++;
307
+ const inner = this.seq();
308
+ this.i++;
309
+ items.push({ t: 'paren', inner });
310
+ continue;
311
+ }
312
+ if ('#TX' === n && '#E(' === this.name(1)) {
313
+ const name = this.T[this.i].src;
314
+ this.i += 2;
315
+ const args = this.seq();
316
+ this.i++;
317
+ items.push({ t: 'call', name, args });
318
+ continue;
319
+ }
320
+ if ('#OB' === n) {
321
+ this.i++;
322
+ const m = this.body('#CB', true);
323
+ this.i++;
324
+ items.push({ t: 'map', body: m.body, open: m.open });
325
+ continue;
326
+ }
327
+ if ('#OS' === n) {
328
+ this.i++;
329
+ const l = this.body('#CS', true);
330
+ this.i++;
331
+ items.push({ t: 'list', body: l.body, open: l.open });
332
+ continue;
333
+ }
334
+ if ('#OD_multisource' === n) {
335
+ items.push({ t: 'include', text: '@' + normStr(this.T[this.i + 1].src) });
336
+ this.i += 2;
337
+ continue;
338
+ }
339
+ if (this.atKey()) {
340
+ // A pair in value position is a chain, `a: b: 1`, and it is
341
+ // the whole of the value.
342
+ items.push(this.entry());
343
+ break;
344
+ }
345
+ items.push(this.atom());
346
+ }
347
+ if (1 === items.length && 'op' !== items[0].t && 'prefix' !== items[0].t &&
348
+ 'note' !== items[0].t) {
349
+ return items[0];
350
+ }
351
+ return { t: 'expr', items };
352
+ }
353
+ // Whether the expression so far wants an operand: nothing yet, or an
354
+ // operator, a prefix or a comment last.
355
+ open(items) {
356
+ if (0 === items.length) {
357
+ return true;
358
+ }
359
+ const t = items[items.length - 1].t;
360
+ return 'op' === t || 'prefix' === t || 'note' === t;
361
+ }
362
+ // The token under the cursor, and the parts glued to it.
363
+ atom() {
364
+ let text = atomText(this.T[this.i]);
365
+ this.i++;
366
+ while (GLUE[this.name(0)] &&
367
+ this.T[this.i - 1].sI + this.T[this.i - 1].src.length === this.T[this.i].sI) {
368
+ text += atomText(this.T[this.i]);
369
+ this.i++;
370
+ }
371
+ return { t: 'atom', text };
372
+ }
373
+ // A call's arguments, or a parenthesis's contents, up to the closing
374
+ // parenthesis: values separated by commas, with a comment among them
375
+ // kept as a note.
376
+ seq() {
377
+ const out = [];
378
+ let gap = true;
379
+ for (;;) {
380
+ const n = this.name(0);
381
+ if ('' === n || CLOSER[n]) {
382
+ break;
383
+ }
384
+ if ('#LN' === n) {
385
+ this.i++;
386
+ continue;
387
+ }
388
+ if ('#CA' === n) {
389
+ if (gap) {
390
+ out.push({ t: 'atom', text: 'nil' });
391
+ }
392
+ gap = true;
393
+ this.i++;
394
+ continue;
395
+ }
396
+ if ('#CM' === n) {
397
+ out.push({ t: 'note', text: this.T[this.i].src });
398
+ this.i++;
399
+ continue;
400
+ }
401
+ out.push(this.value());
402
+ gap = false;
403
+ }
404
+ return out;
405
+ }
406
+ }
407
+ // THE ROOT MAP HAS NO BRACES (§3.12). A document written as one braced
408
+ // map is its entries; the comments on the braces' lines become entries
409
+ // of their own, where nothing is lost.
410
+ function unwrap(root) {
411
+ const entries = root.filter((n) => 'comment' !== n.t && 'blank' !== n.t);
412
+ if (1 !== entries.length || 'map' !== entries[0].t) {
413
+ return root;
414
+ }
415
+ const m = entries[0];
416
+ const out = [];
417
+ for (const n of root) {
418
+ if (n !== m) {
419
+ out.push(n);
420
+ continue;
421
+ }
422
+ if (undefined !== m.open) {
423
+ out.push({ t: 'comment', text: m.open });
424
+ }
425
+ out.push(...m.body);
426
+ if (undefined !== m.trail) {
427
+ out.push({ t: 'comment', text: m.trail });
428
+ }
429
+ }
430
+ return out;
431
+ }
432
+ // ---------------------------------------------------------------------
433
+ // The layout
434
+ // D1: a one-pair map in value position is written as a chain, and a
435
+ // one-pair map as a list element as a pair element. A map whose only
436
+ // entry is a spread keeps its braces (X-7), and one holding a comment
437
+ // keeps them too, because the comment needs the lines. A trailing
438
+ // comment on the map's line joins the pair's own.
439
+ function chain(node) {
440
+ if ('map' !== node.t || undefined !== node.open || 1 !== node.body.length ||
441
+ 'pair' !== node.body[0].t) {
442
+ return node;
443
+ }
444
+ const p = node.body[0];
445
+ if (undefined === node.trail) {
446
+ return p;
447
+ }
448
+ return { ...p, trail: undefined === p.trail ? node.trail : p.trail + ' ' + node.trail };
449
+ }
450
+ function width(s) {
451
+ return Array.from(s).length;
452
+ }
453
+ function pairHead(node, tight) {
454
+ return node.key + (node.opt ? '?' : '') + (tight ? ':' : ': ');
455
+ }
456
+ // The one-line spelling of a node, or undefined where it has none: a
457
+ // comment, a blank line, a break the author kept, a string that spans
458
+ // lines. `tight` is the inline form of a pair, `a:1`, used inside a
459
+ // container; a statement's pair is `a: 1`.
460
+ function inline(node, tight) {
461
+ if (undefined !== node.trail) {
462
+ return undefined;
463
+ }
464
+ switch (node.t) {
465
+ case 'atom':
466
+ case 'include':
467
+ return node.text.includes('\n') ? undefined : node.text;
468
+ case 'pair': {
469
+ const v = inline(chain(node.value), tight);
470
+ return undefined === v ? undefined : pairHead(node, tight) + v;
471
+ }
472
+ case 'spread': {
473
+ // `{ &: integer }`, padded inside braces too: the marker reads as
474
+ // a marker and not as a key.
475
+ const v = inline(node.value, tight);
476
+ return undefined === v ? undefined : '&: ' + v;
477
+ }
478
+ case 'map':
479
+ case 'list': {
480
+ if (undefined !== node.open) {
481
+ return undefined;
482
+ }
483
+ const parts = [];
484
+ for (const e of node.body) {
485
+ const s = inline('list' === node.t ? chain(e) : e, true);
486
+ if (undefined === s) {
487
+ return undefined;
488
+ }
489
+ parts.push(s);
490
+ }
491
+ if ('list' === node.t) {
492
+ return '[' + parts.join(' ') + ']';
493
+ }
494
+ return 0 === parts.length ? '{}' : '{ ' + parts.join(' ') + ' }';
495
+ }
496
+ case 'call': {
497
+ const a = inlineSeq(node.args);
498
+ return undefined === a ? undefined : node.name + '(' + a + ')';
499
+ }
500
+ case 'paren': {
501
+ const a = inlineSeq(node.inner);
502
+ return undefined === a ? undefined : '(' + a + ')';
503
+ }
504
+ case 'expr':
505
+ return inlineExpr(node.items);
506
+ default:
507
+ // comment, blank: never on a line with anything else.
508
+ return undefined;
509
+ }
510
+ }
511
+ function inlineSeq(items) {
512
+ const parts = [];
513
+ for (const it of items) {
514
+ const s = inline(it, true);
515
+ if (undefined === s) {
516
+ return undefined;
517
+ }
518
+ parts.push(s);
519
+ }
520
+ return parts.join(', ');
521
+ }
522
+ // Binary operators spaced, prefixes tight (§3.11). An operand is
523
+ // never directly after an operand: the reader ends a value there.
524
+ function inlineExpr(items) {
525
+ let out = '';
526
+ for (const it of items) {
527
+ if ('note' === it.t || ('op' === it.t && it.brk)) {
528
+ return undefined;
529
+ }
530
+ if ('op' === it.t) {
531
+ out += ' ' + it.text + ' ';
532
+ continue;
533
+ }
534
+ if ('prefix' === it.t) {
535
+ out += it.text;
536
+ continue;
537
+ }
538
+ const s = inline(it, true);
539
+ if (undefined === s) {
540
+ return undefined;
541
+ }
542
+ out += s;
543
+ }
544
+ return out;
545
+ }
546
+ class Writer {
547
+ constructor() {
548
+ this.lines = [];
549
+ this.line = '';
550
+ this.started = false;
551
+ }
552
+ // A new line at an indentation, after a blank one when asked.
553
+ open(indent, blank) {
554
+ if (this.started) {
555
+ this.lines.push(rtrim(this.line));
556
+ if (blank) {
557
+ this.lines.push('');
558
+ }
559
+ }
560
+ this.line = ' '.repeat(indent);
561
+ this.started = true;
562
+ }
563
+ text(s) {
564
+ this.line += s;
565
+ }
566
+ // Nothing on the line yet but its indentation.
567
+ fresh() {
568
+ return '' === this.line.trim();
569
+ }
570
+ width() {
571
+ return width(this.line);
572
+ }
573
+ finish() {
574
+ if (!this.started) {
575
+ return '';
576
+ }
577
+ this.lines.push(rtrim(this.line));
578
+ return this.lines.join('\n') + '\n';
579
+ }
580
+ }
581
+ // A line never ends in a space: an operator the author left dangling
582
+ // (`a: 1 &`, which the parser accepts) would otherwise leave one.
583
+ function rtrim(s) {
584
+ return s.replace(/ +$/, '');
585
+ }
586
+ // The entries of a body, one per line at the indentation, with the
587
+ // blank lines the author kept between them (§3.8) -- never at the
588
+ // start or the end.
589
+ function emitBody(w, body, indent) {
590
+ let pending = false;
591
+ let count = 0;
592
+ for (const node of body) {
593
+ if ('blank' === node.t) {
594
+ pending = 0 < count;
595
+ continue;
596
+ }
597
+ w.open(indent, pending);
598
+ pending = false;
599
+ count++;
600
+ if ('comment' === node.t) {
601
+ w.text(node.text);
602
+ continue;
603
+ }
604
+ const e = chain(node);
605
+ emitValue(w, e, indent);
606
+ if (undefined !== e.trail) {
607
+ w.text(' ' + e.trail);
608
+ }
609
+ }
610
+ }
611
+ // A value onto the current line: its one-line spelling when there is
612
+ // one and it fits the budget, and otherwise its several-line form,
613
+ // which for a scalar is the same text, too wide and unbreakable.
614
+ function emitValue(w, node, indent) {
615
+ const s = inline(node, false);
616
+ if (undefined !== s && w.width() + width(s) <= BUDGET) {
617
+ w.text(s);
618
+ return;
619
+ }
620
+ switch (node.t) {
621
+ case 'pair': {
622
+ w.text(pairHead(node, false));
623
+ const v = chain(node.value);
624
+ emitValue(w, v, indent);
625
+ if (undefined !== v.trail) {
626
+ w.text(' ' + v.trail);
627
+ }
628
+ return;
629
+ }
630
+ case 'spread':
631
+ w.text('&: ');
632
+ emitValue(w, node.value, indent);
633
+ return;
634
+ case 'map':
635
+ emitBlock(w, '{', '}', node, indent);
636
+ return;
637
+ case 'list':
638
+ emitBlock(w, '[', ']', node, indent);
639
+ return;
640
+ case 'expr':
641
+ emitExpr(w, node.items, indent);
642
+ return;
643
+ case 'call':
644
+ case 'paren':
645
+ emitCall(w, node, indent);
646
+ return;
647
+ default:
648
+ w.text(node.text);
649
+ }
650
+ }
651
+ // A call, or a parenthesis, that has no one-line form or is too wide
652
+ // for the budget. Three shapes. A single container argument hugs the
653
+ // parentheses, `close({` ... `})`, and decides its own lines. Arguments
654
+ // that each have a one-line form stay on the one line however wide it
655
+ // is: the formatter never breaks a line. Otherwise -- an argument that
656
+ // is itself several lines, a comment among the arguments -- the
657
+ // parenthesis opens a block: one argument per line one level in, the
658
+ // closer alone at the opener's level.
659
+ function emitCall(w, node, indent) {
660
+ const items = 'call' === node.t ? node.args : node.inner;
661
+ const open = ('call' === node.t ? node.name : '') + '(';
662
+ if (1 === items.length && ('map' === items[0].t || 'list' === items[0].t)) {
663
+ w.text(open);
664
+ emitValue(w, items[0], indent);
665
+ w.text(')');
666
+ return;
667
+ }
668
+ const one = inlineSeq(items);
669
+ if (undefined !== one) {
670
+ w.text(open + one + ')');
671
+ return;
672
+ }
673
+ w.text(open);
674
+ let noted = false;
675
+ for (let k = 0; k < items.length; k++) {
676
+ const it = items[k];
677
+ if ('note' === it.t) {
678
+ // A comment among the arguments trails the line it was on -- the
679
+ // opener's, or an argument's -- and one that followed another
680
+ // comment keeps its own line.
681
+ if (noted) {
682
+ w.open(indent + 2, false);
683
+ w.text(it.text);
684
+ }
685
+ else {
686
+ w.text(' ' + it.text);
687
+ }
688
+ noted = true;
689
+ continue;
690
+ }
691
+ w.open(indent + 2, false);
692
+ emitValue(w, it, indent + 2);
693
+ if (items.slice(k + 1).some((x) => 'note' !== x.t)) {
694
+ w.text(',');
695
+ }
696
+ noted = false;
697
+ }
698
+ w.open(indent, false);
699
+ w.text(')');
700
+ }
701
+ // A container on several lines (§3.5): the opener ends its line, the
702
+ // entries are statements one level in, the closer stands alone. An
703
+ // empty container is inline whatever the budget says.
704
+ function emitBlock(w, open, close, node, indent) {
705
+ if (0 === node.body.length && undefined === node.open) {
706
+ w.text(open + close);
707
+ return;
708
+ }
709
+ w.text(open);
710
+ if (undefined !== node.open) {
711
+ w.text(' ' + node.open);
712
+ }
713
+ emitBody(w, node.body, indent + 2);
714
+ w.open(indent, false);
715
+ w.text(close);
716
+ }
717
+ // An expression that has no one-line form, or one too wide for the
718
+ // budget: the author's breaks are kept, each at its operator, which
719
+ // leads its continuation line (§3.11). The continuation is one level
720
+ // in when the expression follows a key on its line, and level with
721
+ // the first operand when the expression has the line to itself -- an
722
+ // argument of a block call, say -- so a disjunction of alternatives
723
+ // reads as the list it is. A container operand that does not fit from
724
+ // where it stands is a block whose closer lines up with the line that
725
+ // opened it.
726
+ function emitExpr(w, items, indent) {
727
+ const cont = w.fresh() ? indent : indent + 2;
728
+ // Whether the last item was an operand: a comment after one is a
729
+ // space away, and after an operator or the colon it is not. An
730
+ // operand is never directly after an operand (the reader ends a
731
+ // value there), so operands need no such check.
732
+ let operand = false;
733
+ let cur = indent;
734
+ for (const it of items) {
735
+ if ('op' === it.t) {
736
+ if (it.brk) {
737
+ cur = cont;
738
+ if (!w.fresh()) {
739
+ w.open(cur, false);
740
+ }
741
+ w.text(it.text + ' ');
742
+ }
743
+ else {
744
+ w.text(' ' + it.text + ' ');
745
+ }
746
+ operand = false;
747
+ continue;
748
+ }
749
+ if ('prefix' === it.t) {
750
+ w.text(it.text);
751
+ operand = false;
752
+ continue;
753
+ }
754
+ if ('note' === it.t) {
755
+ if (operand) {
756
+ w.text(' ');
757
+ }
758
+ w.text(it.text);
759
+ cur = cont;
760
+ w.open(cur, false);
761
+ operand = false;
762
+ continue;
763
+ }
764
+ emitValue(w, it, cur);
765
+ operand = true;
766
+ }
767
+ }
768
+ function emit(root) {
769
+ const w = new Writer();
770
+ emitBody(w, root, 0);
771
+ return w.finish();
772
+ }
773
+ // ---------------------------------------------------------------------
774
+ // The verb's library surface
775
+ function lf(text) {
776
+ return text.split('\r\n').join('\n');
777
+ }
778
+ // The check: the output parses, and to the same tree. Pre-unification
779
+ // canon is that tree, positions aside, and every rewrite of this tier
780
+ // leaves it unchanged (§7.3).
781
+ function sameDocument(root, after) {
782
+ const p = parseDoc(after, undefined, undefined);
783
+ return undefined === p.errors && root.canon === p.root.canon;
784
+ }
785
+ function depthFinding() {
786
+ return {
787
+ code: 'max_depth',
788
+ class: 'budget',
789
+ severity: 'error',
790
+ path: '$',
791
+ message: `The document nests more than ${MAX_DEPTH} levels deep, past what the formatter reads.`,
792
+ sites: [],
793
+ };
794
+ }
795
+ function checkFinding(path, expected, actual) {
796
+ return {
797
+ code: 'format_check',
798
+ class: 'internal',
799
+ severity: 'error',
800
+ path: '$',
801
+ message: 'The formatted text is not the same document, so nothing was written.',
802
+ note: 'a formatter defect: please report it with the source' +
803
+ (undefined === path ? '' : ' (' + path + ')'),
804
+ sites: [],
805
+ expected,
806
+ actual,
807
+ };
808
+ }
809
+ // Format one document. The text is the agreed form of the source;
810
+ // `changed` says whether it differs from what was given, which is
811
+ // what `--check` and `--list` report.
812
+ function format(src, opts, hooks) {
813
+ const text = lf(src);
814
+ const toks = [];
815
+ const parsed = parseDoc(text, opts?.path, toks);
816
+ if (undefined !== parsed.errors) {
817
+ return { verdict: 'error', errors: parsed.errors };
818
+ }
819
+ const reader = new Reader(toks);
820
+ const root = reader.body('', false).body;
821
+ if (reader.deep) {
822
+ return { verdict: 'error', errors: [depthFinding()] };
823
+ }
824
+ const out = emit(unwrap(root));
825
+ const same = hooks?.same ?? sameDocument;
826
+ if (!same(parsed.root, out)) {
827
+ return {
828
+ verdict: 'error',
829
+ errors: [checkFinding(opts?.path, parsed.root.canon, out)],
830
+ };
831
+ }
832
+ return { verdict: 'formatted', text: out, changed: out !== src };
833
+ }
834
+ // The lines of a text, with a marker on the last when the text does
835
+ // not end in a newline: such a line never equals its
836
+ // newline-terminated twin, which is how the diff reports the
837
+ // difference, and the marker is rendered as diff renders it. NUL,
838
+ // which no source line ends in.
839
+ const NO_NEWLINE = String.fromCharCode(0);
840
+ function textLines(text) {
841
+ if ('' === text) {
842
+ return [];
843
+ }
844
+ const lines = text.split('\n');
845
+ if ('' === lines[lines.length - 1]) {
846
+ lines.pop();
847
+ }
848
+ else {
849
+ lines[lines.length - 1] += NO_NEWLINE;
850
+ }
851
+ return lines;
852
+ }
853
+ // The longest chain of anchors in order on both sides: patience
854
+ // sorting over the right-hand positions, with the left already
855
+ // ascending.
856
+ function longestChain(pairs) {
857
+ const tails = [];
858
+ const prev = [];
859
+ for (let k = 0; k < pairs.length; k++) {
860
+ const j = pairs[k][1];
861
+ let lo = 0;
862
+ let hi = tails.length;
863
+ while (lo < hi) {
864
+ const mid = (lo + hi) >> 1;
865
+ if (pairs[tails[mid]][1] < j) {
866
+ lo = mid + 1;
867
+ }
868
+ else {
869
+ hi = mid;
870
+ }
871
+ }
872
+ prev[k] = 0 < lo ? tails[lo - 1] : -1;
873
+ tails[lo] = k;
874
+ }
875
+ const out = [];
876
+ let k = 0 === tails.length ? -1 : tails[tails.length - 1];
877
+ while (0 <= k) {
878
+ out.push(pairs[k]);
879
+ k = prev[k];
880
+ }
881
+ return out.reverse();
882
+ }
883
+ function patience(a, x0, x1, b, y0, y1, out) {
884
+ while (x0 < x1 && y0 < y1 && a[x0] === b[y0]) {
885
+ out.push({ op: ' ', text: a[x0] });
886
+ x0++;
887
+ y0++;
888
+ }
889
+ let tail = 0;
890
+ while (x0 < x1 - tail && y0 < y1 - tail && a[x1 - 1 - tail] === b[y1 - 1 - tail]) {
891
+ tail++;
892
+ }
893
+ x1 -= tail;
894
+ y1 -= tail;
895
+ const countA = new Map();
896
+ const countB = new Map();
897
+ const posB = new Map();
898
+ for (let x = x0; x < x1; x++) {
899
+ countA.set(a[x], (countA.get(a[x]) ?? 0) + 1);
900
+ }
901
+ for (let y = y0; y < y1; y++) {
902
+ countB.set(b[y], (countB.get(b[y]) ?? 0) + 1);
903
+ posB.set(b[y], y);
904
+ }
905
+ const pairs = [];
906
+ for (let x = x0; x < x1; x++) {
907
+ if (1 === countA.get(a[x]) && 1 === countB.get(a[x])) {
908
+ pairs.push([x, posB.get(a[x])]);
909
+ }
910
+ }
911
+ const anchors = longestChain(pairs);
912
+ if (0 === anchors.length) {
913
+ for (let x = x0; x < x1; x++) {
914
+ out.push({ op: '-', text: a[x] });
915
+ }
916
+ for (let y = y0; y < y1; y++) {
917
+ out.push({ op: '+', text: b[y] });
918
+ }
919
+ }
920
+ else {
921
+ let x = x0;
922
+ let y = y0;
923
+ for (const [ax, ay] of anchors) {
924
+ patience(a, x, ax, b, y, ay, out);
925
+ out.push({ op: ' ', text: a[ax] });
926
+ x = ax + 1;
927
+ y = ay + 1;
928
+ }
929
+ patience(a, x, x1, b, y, y1, out);
930
+ }
931
+ for (let k = 0; k < tail; k++) {
932
+ out.push({ op: ' ', text: a[x1 + k] });
933
+ }
934
+ }
935
+ // The diff in unified format, three lines of context, the file named
936
+ // on both sides. Empty when the texts are the same.
937
+ function unifiedDiff(name, before, after) {
938
+ const a = textLines(before);
939
+ const b = textLines(after);
940
+ const edits = [];
941
+ patience(a, 0, a.length, b, 0, b.length, edits);
942
+ // Hunks: changes closer than twice the context share one.
943
+ const hunks = [];
944
+ for (let k = 0; k < edits.length; k++) {
945
+ if (' ' === edits[k].op) {
946
+ continue;
947
+ }
948
+ const last = hunks[hunks.length - 1];
949
+ if (undefined !== last && k - last[1] <= 6) {
950
+ last[1] = k;
951
+ }
952
+ else {
953
+ hunks.push([k, k]);
954
+ }
955
+ }
956
+ if (0 === hunks.length) {
957
+ return '';
958
+ }
959
+ const out = ['--- a/' + name, '+++ b/' + name];
960
+ let ai = 0;
961
+ let bi = 0;
962
+ let next = 0;
963
+ for (const [s, e] of hunks) {
964
+ const from = Math.max(s - 3, 0);
965
+ const to = Math.min(e + 4, edits.length);
966
+ // Everything between two hunks is context -- a change would have
967
+ // opened a hunk -- so both sides advance together.
968
+ for (; next < from; next++) {
969
+ ai++;
970
+ bi++;
971
+ }
972
+ let alen = 0;
973
+ let blen = 0;
974
+ const lines = [];
975
+ for (let k = from; k < to; k++) {
976
+ const ed = edits[k];
977
+ if ('+' !== ed.op) {
978
+ alen++;
979
+ }
980
+ if ('-' !== ed.op) {
981
+ blen++;
982
+ }
983
+ if (ed.text.endsWith(NO_NEWLINE)) {
984
+ lines.push(ed.op + ed.text.slice(0, -1));
985
+ lines.push('\');
986
+ }
987
+ else {
988
+ lines.push(ed.op + ed.text);
989
+ }
990
+ }
991
+ out.push('@@ -' + (0 === alen ? ai : ai + 1) + ',' + alen +
992
+ ' +' + (0 === blen ? bi : bi + 1) + ',' + blen + ' @@');
993
+ out.push(...lines);
994
+ ai += alen;
995
+ bi += blen;
996
+ next = to;
997
+ }
998
+ return out.join('\n') + '\n';
999
+ }
1000
+ //# sourceMappingURL=format.js.map