aontu 0.55.0 → 0.57.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 (128) 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 +4 -2
  6. package/dist/aontu.js +5 -2
  7. package/dist/aontu.js.map +1 -1
  8. package/dist/cli.d.ts +10 -3
  9. package/dist/cli.js +311 -53
  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/escape.d.ts +5 -0
  15. package/dist/escape.js +455 -0
  16. package/dist/escape.js.map +1 -0
  17. package/dist/format.d.ts +26 -0
  18. package/dist/format.js +1489 -0
  19. package/dist/format.js.map +1 -0
  20. package/dist/hints.js +39 -1
  21. package/dist/hints.js.map +1 -1
  22. package/dist/jsonschema.d.ts +1 -0
  23. package/dist/jsonschema.js +3 -2
  24. package/dist/jsonschema.js.map +1 -1
  25. package/dist/lang.js +84 -6
  26. package/dist/lang.js.map +1 -1
  27. package/dist/lsp.d.ts +2 -1
  28. package/dist/lsp.js +4 -4
  29. package/dist/lsp.js.map +1 -1
  30. package/dist/mcp-server.js +2 -2
  31. package/dist/mcp-server.js.map +1 -1
  32. package/dist/mcp.js +6 -4
  33. package/dist/mcp.js.map +1 -1
  34. package/dist/mod-tool.js +8 -7
  35. package/dist/mod-tool.js.map +1 -1
  36. package/dist/mod.js +6 -6
  37. package/dist/mod.js.map +1 -1
  38. package/dist/patch.d.ts +1 -0
  39. package/dist/patch.js +1 -0
  40. package/dist/patch.js.map +1 -1
  41. package/dist/query.d.ts +1 -0
  42. package/dist/query.js +4 -3
  43. package/dist/query.js.map +1 -1
  44. package/dist/reach.d.ts +1 -0
  45. package/dist/reach.js +3 -2
  46. package/dist/reach.js.map +1 -1
  47. package/dist/relation.d.ts +1 -0
  48. package/dist/relation.js +3 -2
  49. package/dist/relation.js.map +1 -1
  50. package/dist/sigdecl.js +1 -1
  51. package/dist/sigdecl.js.map +1 -1
  52. package/dist/std.js +2 -1
  53. package/dist/std.js.map +1 -1
  54. package/dist/subsume.d.ts +1 -0
  55. package/dist/subsume.js +3 -2
  56. package/dist/subsume.js.map +1 -1
  57. package/dist/trim.d.ts +1 -0
  58. package/dist/trim.js +3 -2
  59. package/dist/trim.js.map +1 -1
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/dist/type.d.ts +1 -0
  62. package/dist/type.js.map +1 -1
  63. package/dist/utility.d.ts +8 -2
  64. package/dist/utility.js +9 -1
  65. package/dist/utility.js.map +1 -1
  66. package/dist/val/EachFuncVal.js +1 -2
  67. package/dist/val/EachFuncVal.js.map +1 -1
  68. package/dist/val/EmitFuncVal.d.ts +23 -0
  69. package/dist/val/EmitFuncVal.js +261 -0
  70. package/dist/val/EmitFuncVal.js.map +1 -0
  71. package/dist/val/FilterFuncVal.js +1 -2
  72. package/dist/val/FilterFuncVal.js.map +1 -1
  73. package/dist/val/FuncBaseVal.d.ts +1 -0
  74. package/dist/val/FuncBaseVal.js +16 -0
  75. package/dist/val/FuncBaseVal.js.map +1 -1
  76. package/dist/val/PackFuncVal.js +1 -2
  77. package/dist/val/PackFuncVal.js.map +1 -1
  78. package/dist/val/PlaceVal.d.ts +3 -1
  79. package/dist/val/PlaceVal.js +8 -6
  80. package/dist/val/PlaceVal.js.map +1 -1
  81. package/dist/val/StrFuncVal.d.ts +32 -0
  82. package/dist/val/StrFuncVal.js +292 -0
  83. package/dist/val/StrFuncVal.js.map +1 -0
  84. package/dist/vet.d.ts +1 -0
  85. package/dist/vet.js +1 -1
  86. package/dist/vet.js.map +1 -1
  87. package/dist/view.d.ts +2 -1
  88. package/dist/view.js +379 -8
  89. package/dist/view.js.map +1 -1
  90. package/grammar/aontu.abnf +156 -0
  91. package/grammar/aontu.gbnf +4 -3
  92. package/grammar/aontu.lark +4 -3
  93. package/grammar/aontu.tmLanguage.json +1 -1
  94. package/package.json +2 -1
  95. package/skill/grammar-card.md +5 -2
  96. package/src/agentsmd.ts +15 -3
  97. package/src/aontu.ts +8 -1
  98. package/src/cli.ts +365 -58
  99. package/src/diff.ts +7 -2
  100. package/src/escape.ts +371 -0
  101. package/src/format.ts +1728 -0
  102. package/src/hints.ts +53 -1
  103. package/src/jsonschema.ts +7 -1
  104. package/src/lang.ts +93 -6
  105. package/src/lsp.ts +8 -6
  106. package/src/mcp-server.ts +3 -2
  107. package/src/mcp.ts +6 -4
  108. package/src/mod-tool.ts +9 -8
  109. package/src/mod.ts +6 -6
  110. package/src/patch.ts +7 -0
  111. package/src/query.ts +8 -4
  112. package/src/reach.ts +7 -2
  113. package/src/relation.ts +7 -2
  114. package/src/sigdecl.ts +1 -1
  115. package/src/std.ts +2 -1
  116. package/src/subsume.ts +7 -2
  117. package/src/trim.ts +7 -2
  118. package/src/type.ts +8 -0
  119. package/src/utility.ts +27 -2
  120. package/src/val/EachFuncVal.ts +1 -3
  121. package/src/val/EmitFuncVal.ts +401 -0
  122. package/src/val/FilterFuncVal.ts +1 -3
  123. package/src/val/FuncBaseVal.ts +18 -0
  124. package/src/val/PackFuncVal.ts +1 -3
  125. package/src/val/PlaceVal.ts +8 -6
  126. package/src/val/StrFuncVal.ts +334 -0
  127. package/src/vet.ts +9 -3
  128. package/src/view.ts +442 -9
package/dist/format.js ADDED
@@ -0,0 +1,1489 @@
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
+ // Two tiers. The syntactic (P1): whitespace, commas, quotes, bare
20
+ // keys, chains and pair elements, none of which changes the parse
21
+ // tree. The lawful (P2), over it: repeat the prefix, and merge what
22
+ // repeats -- rewrites that rest on the meet, each checked by the meet
23
+ // in isolation and kept only where the engine agrees.
24
+ //
25
+ // The Go twin is go/format.go, function for function; the shared
26
+ // behaviour is test/spec/fmt.tsv, executed by both spec runners.
27
+ const aontu_1 = require("./aontu");
28
+ const vet_1 = require("./vet");
29
+ // The packing budget (§3.1). It decides which of two legal spellings
30
+ // to use, one line or several, and nothing else: the formatter never
31
+ // breaks a line, so a value wider than this stays as wide as it is.
32
+ const BUDGET = 80;
33
+ // THE DEPTH BUDGET. The layout is recursive, as the tree it reads is,
34
+ // and the canonical port's stack is finite: past the evaluation budget
35
+ // of 1000 levels -- the depth at which unification itself refuses --
36
+ // the formatter stops reading and refuses, so a pathological document
37
+ // is a finding rather than a crash.
38
+ const MAX_DEPTH = 1000;
39
+ // EVERY INCLUDE RESOLVES TO NOTHING. The formatter reads the file it is
40
+ // given and no other (§3.13), so `@"..."` is answered from memory with
41
+ // an empty source: the directive parses, the include is a token like
42
+ // any other, and no capability is needed because no file is read.
43
+ const stubResolver = ((spec) => ({
44
+ ...spec, kind: 'aon', full: '__fmt__.aon', src: '', found: true, search: [],
45
+ }));
46
+ // ONE ENGINE, ONE SUBSCRIBER. The parser's subscriber list is
47
+ // append-only, so the subscription is made once and writes to
48
+ // whichever sink the current parse installed; the sink is cleared
49
+ // before the parse returns, so the check's re-parse collects nothing.
50
+ let ENGINE;
51
+ let SINK;
52
+ function engine() {
53
+ if (undefined === ENGINE) {
54
+ ENGINE = new aontu_1.Aontu({ resolver: stubResolver });
55
+ ENGINE.lang.jsonic.sub({
56
+ lex: (tkn) => {
57
+ // Spaces carry nothing the layout needs, and the end token
58
+ // arrives once per nested parse -- the stub's empty includes
59
+ // among them -- so both are dropped here rather than skipped
60
+ // everywhere below.
61
+ if (undefined !== SINK && '#SP' !== tkn.name && '#ZZ' !== tkn.name) {
62
+ SINK.push({ name: tkn.name, src: tkn.src, val: tkn.val, sI: tkn.sI });
63
+ }
64
+ },
65
+ });
66
+ }
67
+ return ENGINE;
68
+ }
69
+ // One parse, with the token stream collected when a sink is given. The
70
+ // failure shape is the one every verb reports (`view`'s load).
71
+ function parseDoc(src, path, sink) {
72
+ const aontu = engine();
73
+ const ctx = aontu.ctx({ collect: true });
74
+ SINK = sink;
75
+ let parsed;
76
+ try {
77
+ parsed = aontu.parse(src, undefined === path ? undefined : { path }, ctx);
78
+ }
79
+ finally {
80
+ SINK = undefined;
81
+ }
82
+ if (0 < ctx.err.length) {
83
+ return { errors: [(0, vet_1.failureFinding)(ctx, path, parsed)] };
84
+ }
85
+ return { root: parsed };
86
+ }
87
+ const BINARY = { '#E&': true, '#E|': true, '#E+': true };
88
+ const PREFIX = { '#E*': true, '#E-': true };
89
+ const KEYISH = { '#TX': true, '#ST': true, '#NR': true, '#VL': true };
90
+ const CLOSER = { '#CB': true, '#CS': true, '#E)': true };
91
+ // The parts of one atom: a reference is `$`, dots and segments lexed
92
+ // one by one, and a bare word with a dot in it is the same run; what
93
+ // was adjacent in the source stays glued.
94
+ const GLUE = {
95
+ '#TX': true, '#ST': true, '#NR': true, '#VL': true, '#E.': true, '#E$': true,
96
+ };
97
+ const BARE = /^[A-Za-z_][A-Za-z0-9_]*$/;
98
+ // A single-quoted string becomes double-quoted unless it holds a double
99
+ // quote, which the swap would have to escape (§3.9). The body is copied
100
+ // as written: the escapes are the same under both quotes.
101
+ function normStr(src) {
102
+ if ("'" === src[0]) {
103
+ const body = src.slice(1, -1);
104
+ return body.includes('"') ? src : '"' + body + '"';
105
+ }
106
+ return src;
107
+ }
108
+ function atomText(tok) {
109
+ return '#ST' === tok.name ? normStr(tok.src) : tok.src;
110
+ }
111
+ // A quoted key whose text is a legal bare key is written bare; the
112
+ // keywords are legal keys too (`string: 1` is the key `string`), so no
113
+ // word is reserved. Anything else keeps its spelling.
114
+ function keyText(tok) {
115
+ if ('#ST' === tok.name) {
116
+ return BARE.test(tok.val) ? tok.val : normStr(tok.src);
117
+ }
118
+ return tok.src;
119
+ }
120
+ function newlines(src) {
121
+ return src.split('\n').length - 1;
122
+ }
123
+ class Reader {
124
+ constructor(toks) {
125
+ this.i = 0;
126
+ this.depth = 0;
127
+ // Past the depth budget: the reader answers '' for every token from
128
+ // here on, so every loop unwinds, and the document is refused.
129
+ this.deep = false;
130
+ this.T = toks;
131
+ }
132
+ // The name of the token k ahead, or '' past the end.
133
+ name(k) {
134
+ const t = this.T[this.i + k];
135
+ return this.deep || undefined === t ? '' : t.name;
136
+ }
137
+ // The offset of the next token that is not a line run or a comment.
138
+ significant() {
139
+ let k = 0;
140
+ while ('#LN' === this.name(k) || '#CM' === this.name(k)) {
141
+ k++;
142
+ }
143
+ return k;
144
+ }
145
+ // A key followed by a colon, the optional marker allowed between.
146
+ atKey() {
147
+ return KEYISH[this.name(0)] && ('#CL' === this.name(1) ||
148
+ ('#QM' === this.name(1) && '#CL' === this.name(2)));
149
+ }
150
+ // The entries of a container up to its closer, or of the document up
151
+ // to its end. Comments attach by the rules of §3.7: on the line of
152
+ // the entry that precedes them, or of the opener, they trail it;
153
+ // alone on a line they stand as entries and precede what follows.
154
+ body(close, opened) {
155
+ const body = [];
156
+ let open;
157
+ let last;
158
+ let opener = opened;
159
+ // Nothing since the opener or the last comma: a comma here is an
160
+ // empty element, which the parser reads as nil in a list.
161
+ let gap = true;
162
+ for (;;) {
163
+ const n = this.name(0);
164
+ // The closer, or the end: the parser accepts a container the
165
+ // source never closed (`a: {` is `{"a":{}}`).
166
+ if ('' === n || n === close) {
167
+ break;
168
+ }
169
+ if ('#LN' === n) {
170
+ if (1 < newlines(this.T[this.i].src) && 0 < body.length &&
171
+ 'blank' !== body[body.length - 1].t) {
172
+ body.push({ t: 'blank' });
173
+ }
174
+ last = undefined;
175
+ opener = false;
176
+ this.i++;
177
+ continue;
178
+ }
179
+ if ('#CA' === n) {
180
+ if (gap && '#CS' === close) {
181
+ const nil = { t: 'atom', text: 'nil' };
182
+ body.push(nil);
183
+ last = nil;
184
+ }
185
+ gap = true;
186
+ this.i++;
187
+ continue;
188
+ }
189
+ if ('#CM' === n) {
190
+ const text = this.T[this.i].src;
191
+ if (undefined !== last) {
192
+ last.trail = text;
193
+ }
194
+ else if (opener) {
195
+ open = text;
196
+ }
197
+ else {
198
+ body.push({ t: 'comment', text });
199
+ }
200
+ this.i++;
201
+ continue;
202
+ }
203
+ if (CLOSER[n]) {
204
+ // A closer that is not this container's: the parser ignores a
205
+ // stray one at the root (`a: 1 }` is `{"a":1}`), and so does
206
+ // this.
207
+ this.i++;
208
+ continue;
209
+ }
210
+ const e = this.entry();
211
+ body.push(e);
212
+ last = e;
213
+ opener = false;
214
+ gap = false;
215
+ }
216
+ // A blank line before the closer is no paragraph break: nothing
217
+ // follows it, and the layout would drop it anyway.
218
+ while (0 < body.length && 'blank' === body[body.length - 1].t) {
219
+ body.pop();
220
+ }
221
+ return { body, open };
222
+ }
223
+ // One entry: an include, a spread, a pair, or -- as a list element or
224
+ // at the root -- a value.
225
+ entry() {
226
+ const n = this.name(0);
227
+ const at = this.T[this.i].sI;
228
+ if ('#OD_multisource' === n) {
229
+ const text = '@' + normStr(this.T[this.i + 1].src);
230
+ this.i += 2;
231
+ return { t: 'include', text, at };
232
+ }
233
+ if ('#E&' === n && '#CL' === this.name(1)) {
234
+ this.i += 2;
235
+ return { t: 'spread', value: this.value(), at };
236
+ }
237
+ if (this.atKey()) {
238
+ const tok = this.T[this.i];
239
+ const opt = '#QM' === this.name(1);
240
+ this.i += opt ? 3 : 2;
241
+ return { t: 'pair', key: keyText(tok), opt, value: this.value(), at };
242
+ }
243
+ return this.value();
244
+ }
245
+ // A value: operands and operators up to whatever ends it -- a
246
+ // separator, a closer, the end, or a line run that no operator
247
+ // continues past.
248
+ value() {
249
+ if (MAX_DEPTH < ++this.depth) {
250
+ this.deep = true;
251
+ }
252
+ const v = this.valueAt();
253
+ this.depth--;
254
+ return v;
255
+ }
256
+ valueAt() {
257
+ const items = [];
258
+ for (;;) {
259
+ const n = this.name(0);
260
+ if ('' === n || '#CA' === n || CLOSER[n]) {
261
+ break;
262
+ }
263
+ // An operand directly after an operand is the next element of a
264
+ // list, `[1 -2]`, `[{a:1} {b:2}]`: this value is complete.
265
+ if (!this.open(items) && !BINARY[n] && '#LN' !== n && '#CM' !== n) {
266
+ break;
267
+ }
268
+ const at = this.T[this.i].sI;
269
+ if ('#E&' === n && '#CL' === this.name(1)) {
270
+ if (0 === items.length) {
271
+ // A chain through a spread, `a: &: integer`. The braces are
272
+ // the agreed spelling (X-7), so it is read as the map it is.
273
+ this.i += 2;
274
+ return { t: 'map', body: [{ t: 'spread', value: this.value(), at }], at };
275
+ }
276
+ // A sibling spread in a list, `[1 &: 2]`: this value is complete.
277
+ break;
278
+ }
279
+ if ('#LN' === n) {
280
+ // A break the author put before the value, after an operator
281
+ // (`a: 1 &\n 2`) or before one (`a: 1\n | 2`), or after a
282
+ // comment inside the value; anything else ends the value.
283
+ if (this.open(items) || BINARY[this.name(this.significant())]) {
284
+ this.i++;
285
+ continue;
286
+ }
287
+ break;
288
+ }
289
+ if ('#CM' === n) {
290
+ // A comment inside the value: after the colon, after an
291
+ // operator, or on a line the value continues past. Otherwise
292
+ // it trails the statement and the caller attaches it.
293
+ if (this.open(items) || BINARY[this.name(this.significant())]) {
294
+ items.push({ t: 'note', text: this.T[this.i].src, at });
295
+ this.i++;
296
+ continue;
297
+ }
298
+ break;
299
+ }
300
+ if (BINARY[n]) {
301
+ items.push({
302
+ t: 'op', text: this.T[this.i].src,
303
+ brk: '#LN' === this.name(-1) || '#LN' === this.name(1), at,
304
+ });
305
+ this.i++;
306
+ continue;
307
+ }
308
+ if (PREFIX[n]) {
309
+ items.push({ t: 'prefix', text: this.T[this.i].src, at });
310
+ this.i++;
311
+ continue;
312
+ }
313
+ if ('#E(' === n) {
314
+ this.i++;
315
+ const inner = this.seq();
316
+ this.i++;
317
+ items.push({ t: 'paren', inner, at });
318
+ continue;
319
+ }
320
+ if ('#TX' === n && '#E(' === this.name(1)) {
321
+ const name = this.T[this.i].src;
322
+ this.i += 2;
323
+ const args = this.seq();
324
+ this.i++;
325
+ items.push({ t: 'call', name, args, at });
326
+ continue;
327
+ }
328
+ if ('#OB' === n) {
329
+ this.i++;
330
+ const m = this.body('#CB', true);
331
+ this.i++;
332
+ items.push({ t: 'map', body: m.body, open: m.open, at });
333
+ continue;
334
+ }
335
+ if ('#OS' === n) {
336
+ this.i++;
337
+ const l = this.body('#CS', true);
338
+ this.i++;
339
+ items.push({ t: 'list', body: l.body, open: l.open, at });
340
+ continue;
341
+ }
342
+ if ('#OD_multisource' === n) {
343
+ items.push({ t: 'include', text: '@' + normStr(this.T[this.i + 1].src), at });
344
+ this.i += 2;
345
+ continue;
346
+ }
347
+ if (this.atKey()) {
348
+ // A pair in value position is a chain, `a: b: 1`, and it is
349
+ // the whole of the value.
350
+ items.push(this.entry());
351
+ break;
352
+ }
353
+ items.push(this.atom());
354
+ }
355
+ if (1 === items.length && 'op' !== items[0].t && 'prefix' !== items[0].t &&
356
+ 'note' !== items[0].t) {
357
+ return items[0];
358
+ }
359
+ // An empty value, `a:`, is an expression with nothing in it.
360
+ return { t: 'expr', items, at: items[0]?.at };
361
+ }
362
+ // Whether the expression so far wants an operand: nothing yet, or an
363
+ // operator, a prefix or a comment last.
364
+ open(items) {
365
+ if (0 === items.length) {
366
+ return true;
367
+ }
368
+ const t = items[items.length - 1].t;
369
+ return 'op' === t || 'prefix' === t || 'note' === t;
370
+ }
371
+ // The token under the cursor, and the parts glued to it.
372
+ atom() {
373
+ const at = this.T[this.i].sI;
374
+ let text = atomText(this.T[this.i]);
375
+ this.i++;
376
+ while (GLUE[this.name(0)] &&
377
+ this.T[this.i - 1].sI + this.T[this.i - 1].src.length === this.T[this.i].sI) {
378
+ text += atomText(this.T[this.i]);
379
+ this.i++;
380
+ }
381
+ return { t: 'atom', text, at };
382
+ }
383
+ // A call's arguments, or a parenthesis's contents, up to the closing
384
+ // parenthesis: values separated by commas, with a comment among them
385
+ // kept as a note.
386
+ seq() {
387
+ const out = [];
388
+ let gap = true;
389
+ let comma = false;
390
+ for (;;) {
391
+ const n = this.name(0);
392
+ if ('' === n || CLOSER[n]) {
393
+ break;
394
+ }
395
+ if ('#LN' === n) {
396
+ this.i++;
397
+ continue;
398
+ }
399
+ if ('#CA' === n) {
400
+ if (gap) {
401
+ out.push({ t: 'atom', text: 'nil', sep: comma });
402
+ }
403
+ gap = true;
404
+ comma = true;
405
+ this.i++;
406
+ continue;
407
+ }
408
+ if ('#CM' === n) {
409
+ out.push({ t: 'note', text: this.T[this.i].src });
410
+ this.i++;
411
+ continue;
412
+ }
413
+ const v = this.value();
414
+ v.sep = comma;
415
+ out.push(v);
416
+ gap = false;
417
+ comma = false;
418
+ }
419
+ return out;
420
+ }
421
+ }
422
+ // THE ROOT MAP HAS NO BRACES (§3.12). A document written as one braced
423
+ // map is its entries; the comments on the braces' lines become entries
424
+ // of their own, where nothing is lost.
425
+ function unwrap(root) {
426
+ const entries = root.filter((n) => 'comment' !== n.t && 'blank' !== n.t);
427
+ if (1 !== entries.length || 'map' !== entries[0].t) {
428
+ return root;
429
+ }
430
+ const m = entries[0];
431
+ const out = [];
432
+ for (const n of root) {
433
+ if (n !== m) {
434
+ out.push(n);
435
+ continue;
436
+ }
437
+ if (undefined !== m.open) {
438
+ out.push({ t: 'comment', text: m.open });
439
+ }
440
+ out.push(...m.body);
441
+ if (undefined !== m.trail) {
442
+ out.push({ t: 'comment', text: m.trail });
443
+ }
444
+ }
445
+ return out;
446
+ }
447
+ // ---------------------------------------------------------------------
448
+ // The layout
449
+ // D1: a one-pair map in value position is written as a chain, and a
450
+ // one-pair map as a list element as a pair element. A map whose only
451
+ // entry is a spread keeps its braces (X-7), and one holding a comment
452
+ // keeps them too, because the comment needs the lines. A trailing
453
+ // comment on the map's line joins the pair's own.
454
+ function chain(node) {
455
+ if ('map' !== node.t || undefined !== node.open || 1 !== node.body.length ||
456
+ 'pair' !== node.body[0].t) {
457
+ return node;
458
+ }
459
+ const p = node.body[0];
460
+ if (undefined === node.trail) {
461
+ return p;
462
+ }
463
+ return { ...p, trail: undefined === p.trail ? node.trail : p.trail + ' ' + node.trail };
464
+ }
465
+ function width(s) {
466
+ return Array.from(s).length;
467
+ }
468
+ function pairHead(node, tight) {
469
+ return node.key + (node.opt ? '?' : '') + (tight ? ':' : ': ');
470
+ }
471
+ // The one-line spelling of a node, or undefined where it has none: a
472
+ // comment, a blank line, a break the author kept, a string that spans
473
+ // lines. `tight` is the inline form of a pair, `a:1`, used inside a
474
+ // container; a statement's pair is `a: 1`.
475
+ function inline(node, tight) {
476
+ if (undefined !== node.trail) {
477
+ return undefined;
478
+ }
479
+ switch (node.t) {
480
+ case 'atom':
481
+ case 'include':
482
+ return node.text.includes('\n') ? undefined : node.text;
483
+ case 'pair': {
484
+ const v = inline(chain(node.value), tight);
485
+ return undefined === v ? undefined : pairHead(node, tight) + v;
486
+ }
487
+ case 'spread': {
488
+ // `{ &: integer }`, padded inside braces too: the marker reads as
489
+ // a marker and not as a key.
490
+ const v = inline(node.value, tight);
491
+ return undefined === v ? undefined : '&: ' + v;
492
+ }
493
+ case 'map':
494
+ case 'list': {
495
+ if (undefined !== node.open) {
496
+ return undefined;
497
+ }
498
+ const parts = [];
499
+ for (const e of node.body) {
500
+ const s = inline('list' === node.t ? chain(e) : e, true);
501
+ if (undefined === s) {
502
+ return undefined;
503
+ }
504
+ parts.push(s);
505
+ }
506
+ if ('list' === node.t) {
507
+ return '[' + parts.join(' ') + ']';
508
+ }
509
+ return 0 === parts.length ? '{}' : '{ ' + parts.join(' ') + ' }';
510
+ }
511
+ case 'call': {
512
+ const a = inlineSeq(node.args);
513
+ return undefined === a ? undefined : node.name + '(' + a + ')';
514
+ }
515
+ case 'paren': {
516
+ const a = inlineSeq(node.inner);
517
+ return undefined === a ? undefined : '(' + a + ')';
518
+ }
519
+ case 'expr':
520
+ return inlineExpr(node.items);
521
+ default:
522
+ // comment, blank: never on a line with anything else.
523
+ return undefined;
524
+ }
525
+ }
526
+ // Arguments on one line, each after the separator the author wrote
527
+ // (§3.6): a comma stays a comma, and a space a space, because the
528
+ // parser reads `must((v) => 0 <= v, "…")` as a run of arguments too.
529
+ function inlineSeq(items) {
530
+ let out = '';
531
+ for (let k = 0; k < items.length; k++) {
532
+ const s = inline(items[k], true);
533
+ if (undefined === s) {
534
+ return undefined;
535
+ }
536
+ out += (0 === k ? '' : sepOf(items[k])) + s;
537
+ }
538
+ return out;
539
+ }
540
+ function sepOf(node) {
541
+ return node.sep ? ', ' : ' ';
542
+ }
543
+ // Binary operators spaced, prefixes tight (§3.11). An operand is
544
+ // never directly after an operand: the reader ends a value there.
545
+ function inlineExpr(items) {
546
+ let out = '';
547
+ for (const it of items) {
548
+ if ('note' === it.t || ('op' === it.t && it.brk)) {
549
+ return undefined;
550
+ }
551
+ if ('op' === it.t) {
552
+ out += ' ' + it.text + ' ';
553
+ continue;
554
+ }
555
+ if ('prefix' === it.t) {
556
+ out += it.text;
557
+ continue;
558
+ }
559
+ const s = inline(it, true);
560
+ if (undefined === s) {
561
+ return undefined;
562
+ }
563
+ out += s;
564
+ }
565
+ return out;
566
+ }
567
+ class Writer {
568
+ constructor() {
569
+ this.lines = [];
570
+ this.line = '';
571
+ this.started = false;
572
+ }
573
+ // A new line at an indentation, after a blank one when asked.
574
+ open(indent, blank) {
575
+ if (this.started) {
576
+ this.lines.push(rtrim(this.line));
577
+ if (blank) {
578
+ this.lines.push('');
579
+ }
580
+ }
581
+ this.line = ' '.repeat(indent);
582
+ this.started = true;
583
+ }
584
+ text(s) {
585
+ this.line += s;
586
+ }
587
+ // Nothing on the line yet but its indentation.
588
+ fresh() {
589
+ return '' === this.line.trim();
590
+ }
591
+ width() {
592
+ return width(this.line);
593
+ }
594
+ // Where the page is, and the lines written since, the current line
595
+ // included: the spelling of one statement, as it stands on the page.
596
+ mark() {
597
+ return this.lines.length;
598
+ }
599
+ since(mark) {
600
+ return this.lines.slice(mark).concat([this.line]).map(rtrim).join('\n') + '\n';
601
+ }
602
+ // The lines since a mark replaced by a text: the spelling before,
603
+ // where a rewrite did not pass its check.
604
+ replace(mark, text) {
605
+ const lines = text.split('\n');
606
+ lines.pop();
607
+ this.line = lines.pop();
608
+ this.lines.length = mark;
609
+ this.lines.push(...lines);
610
+ }
611
+ finish() {
612
+ if (!this.started) {
613
+ return '';
614
+ }
615
+ this.lines.push(rtrim(this.line));
616
+ return this.lines.join('\n') + '\n';
617
+ }
618
+ }
619
+ // A line never ends in a space: an operator the author left dangling
620
+ // (`a: 1 &`, which the parser accepts) would otherwise leave one.
621
+ function rtrim(s) {
622
+ return s.replace(/ +$/, '');
623
+ }
624
+ // The entries of a body, one per line at the indentation, with the
625
+ // blank lines the author kept between them (§3.8) -- never at the
626
+ // start or the end. In STATEMENT position (`stmt`: the root, and the
627
+ // body of a plain map that is itself the value of a statement) a pair
628
+ // is laid out by §3.4, which may repeat its key; anywhere else -- a
629
+ // list, an operand, an argument -- by §3.5 alone.
630
+ function emitBody(w, body, indent, stmt) {
631
+ let pending = false;
632
+ let count = 0;
633
+ for (const node of body) {
634
+ if ('blank' === node.t) {
635
+ pending = 0 < count;
636
+ continue;
637
+ }
638
+ w.open(indent, pending);
639
+ pending = false;
640
+ count++;
641
+ if ('comment' === node.t) {
642
+ w.text(node.text);
643
+ continue;
644
+ }
645
+ if (undefined !== stmt && 'pair' === node.t) {
646
+ emitStatement(w, node, indent, stmt, '');
647
+ continue;
648
+ }
649
+ const e = chain(node);
650
+ emitValue(w, e, indent);
651
+ if (undefined !== e.trail) {
652
+ w.text(' ' + e.trail);
653
+ }
654
+ }
655
+ }
656
+ // A value onto the current line: its one-line spelling when there is
657
+ // one and it fits the budget, and otherwise its several-line form,
658
+ // which for a scalar is the same text, too wide and unbreakable.
659
+ function emitValue(w, node, indent) {
660
+ const s = inline(node, false);
661
+ if (undefined !== s && w.width() + width(s) <= BUDGET) {
662
+ w.text(s);
663
+ return;
664
+ }
665
+ switch (node.t) {
666
+ case 'pair': {
667
+ w.text(pairHead(node, false));
668
+ const v = chain(node.value);
669
+ emitValue(w, v, indent);
670
+ if (undefined !== v.trail) {
671
+ w.text(' ' + v.trail);
672
+ }
673
+ return;
674
+ }
675
+ case 'spread':
676
+ w.text('&: ');
677
+ emitValue(w, node.value, indent);
678
+ return;
679
+ case 'map':
680
+ emitBlock(w, '{', '}', node, indent, undefined);
681
+ return;
682
+ case 'list':
683
+ emitBlock(w, '[', ']', node, indent, undefined);
684
+ return;
685
+ case 'expr':
686
+ emitExpr(w, node.items, indent);
687
+ return;
688
+ case 'call':
689
+ case 'paren':
690
+ emitCall(w, node, indent);
691
+ return;
692
+ default:
693
+ w.text(node.text);
694
+ }
695
+ }
696
+ // A call, or a parenthesis, that has no one-line form or is too wide
697
+ // for the budget. Three shapes. Arguments that are all FLAT -- none
698
+ // holds a container -- stay on the one line however wide it is: a
699
+ // scalar is no narrower on a line of its own, and the formatter never
700
+ // breaks a line. The last argument HUGS the parentheses, `hide({` ...
701
+ // `})`, `close($.E & {` ... `})`, when it is a container, or an
702
+ // expression the author did not break that ends in one, and the
703
+ // arguments before it fit on the opener's line: the container decides
704
+ // its own lines. Otherwise the parenthesis opens a block: one argument
705
+ // per line one level in, the closer alone at the opener's level. A
706
+ // call whose last argument hugs is hugged in turn, `type(close({` ...
707
+ // `}))`: the schema idiom.
708
+ function emitCall(w, node, indent) {
709
+ const items = 'call' === node.t ? node.args : node.inner;
710
+ const open = ('call' === node.t ? node.name : '') + '(';
711
+ const one = inlineSeq(items);
712
+ if (undefined !== one && !items.some(holdsContainer)) {
713
+ w.text(open + one + ')');
714
+ return;
715
+ }
716
+ const last = items[items.length - 1];
717
+ if (0 < items.length && hugs(last)) {
718
+ const head = inlineSeq(items.slice(0, -1));
719
+ const lead = '' === head ? '' : head + sepOf(last);
720
+ if (undefined !== head && ('' === head || w.width() + width(open + lead) <= BUDGET)) {
721
+ w.text(open + lead);
722
+ emitValue(w, last, indent);
723
+ w.text(')');
724
+ return;
725
+ }
726
+ }
727
+ w.text(open);
728
+ let noted = false;
729
+ for (let k = 0; k < items.length; k++) {
730
+ const it = items[k];
731
+ if ('note' === it.t) {
732
+ // A comment among the arguments trails the line it was on -- the
733
+ // opener's, or an argument's -- and one that followed another
734
+ // comment keeps its own line.
735
+ if (noted) {
736
+ w.open(indent + 2, false);
737
+ w.text(it.text);
738
+ }
739
+ else {
740
+ w.text(' ' + it.text);
741
+ }
742
+ noted = true;
743
+ continue;
744
+ }
745
+ w.open(indent + 2, false);
746
+ emitValue(w, it, indent + 2);
747
+ const next = items.slice(k + 1).find((x) => 'note' !== x.t);
748
+ if (undefined !== next && next.sep) {
749
+ w.text(',');
750
+ }
751
+ noted = false;
752
+ }
753
+ w.open(indent, false);
754
+ w.text(')');
755
+ }
756
+ // Whether a node holds a container anywhere: the argument has a
757
+ // several-line form of its own.
758
+ function holdsContainer(node) {
759
+ switch (node.t) {
760
+ case 'map':
761
+ case 'list':
762
+ return true;
763
+ case 'call':
764
+ return node.args.some(holdsContainer);
765
+ case 'paren':
766
+ return node.inner.some(holdsContainer);
767
+ case 'expr':
768
+ return node.items.some(holdsContainer);
769
+ default:
770
+ return false;
771
+ }
772
+ }
773
+ // Whether a last argument hugs the parentheses: a container; an
774
+ // expression with no break and no comment whose last operand is one;
775
+ // a call whose own last argument does.
776
+ function hugs(node) {
777
+ if ('map' === node.t || 'list' === node.t) {
778
+ return true;
779
+ }
780
+ if ('call' === node.t) {
781
+ return 0 < node.args.length && hugs(node.args[node.args.length - 1]);
782
+ }
783
+ return 'expr' === node.t &&
784
+ node.items.every((it) => 'note' !== it.t && !('op' === it.t && it.brk)) &&
785
+ hugs(node.items[node.items.length - 1]);
786
+ }
787
+ // A container on several lines (§3.5): the opener ends its line, the
788
+ // entries are statements one level in, the closer stands alone. An
789
+ // empty container is inline whatever the budget says.
790
+ function emitBlock(w, open, close, node, indent, stmt) {
791
+ if (0 === node.body.length && undefined === node.open) {
792
+ w.text(open + close);
793
+ return;
794
+ }
795
+ w.text(open);
796
+ if (undefined !== node.open) {
797
+ w.text(' ' + node.open);
798
+ }
799
+ emitBody(w, node.body, indent + 2, stmt);
800
+ w.open(indent, false);
801
+ w.text(close);
802
+ }
803
+ // An expression that has no one-line form, or one too wide for the
804
+ // budget: the author's breaks are kept, each at its operator, which
805
+ // leads its continuation line (§3.11). The continuation is one level
806
+ // in when the expression follows a key on its line, and level with
807
+ // the first operand when the expression has the line to itself -- an
808
+ // argument of a block call, say -- so a disjunction of alternatives
809
+ // reads as the list it is. A container operand that does not fit from
810
+ // where it stands is a block whose closer lines up with the line that
811
+ // opened it.
812
+ function emitExpr(w, items, indent) {
813
+ const cont = w.fresh() ? indent : indent + 2;
814
+ // Whether the last item was an operand: a comment after one is a
815
+ // space away, and after an operator or the colon it is not. An
816
+ // operand is never directly after an operand (the reader ends a
817
+ // value there), so operands need no such check.
818
+ let operand = false;
819
+ let cur = indent;
820
+ for (const it of items) {
821
+ if ('op' === it.t) {
822
+ if (it.brk) {
823
+ cur = cont;
824
+ if (!w.fresh()) {
825
+ w.open(cur, false);
826
+ }
827
+ w.text(it.text + ' ');
828
+ }
829
+ else {
830
+ w.text(' ' + it.text + ' ');
831
+ }
832
+ operand = false;
833
+ continue;
834
+ }
835
+ if ('prefix' === it.t) {
836
+ w.text(it.text);
837
+ operand = false;
838
+ continue;
839
+ }
840
+ if ('note' === it.t) {
841
+ if (operand) {
842
+ w.text(' ');
843
+ }
844
+ w.text(it.text);
845
+ cur = cont;
846
+ w.open(cur, false);
847
+ operand = false;
848
+ continue;
849
+ }
850
+ emitValue(w, it, cur);
851
+ operand = true;
852
+ }
853
+ }
854
+ // The entries of a plain map value: a braced map, or a chain, which is
855
+ // a one-entry map. A map with a comment on its opener keeps its braces
856
+ // (§3.7), so it is not plain here; nor is a map holding an include,
857
+ // which the local check cannot follow.
858
+ function plainEntries(v) {
859
+ if ('pair' === v.t) {
860
+ return [v];
861
+ }
862
+ if ('map' !== v.t || undefined !== v.open || v.body.some((e) => 'include' === e.t)) {
863
+ return undefined;
864
+ }
865
+ return v.body;
866
+ }
867
+ // The entries of a statement as they stand once it is merged into a
868
+ // wider map: its trailing comment sunk onto its last entry, so that it
869
+ // travels with the entry it stood beside. Undefined where the value is
870
+ // not a plain map, or the comment has no entry to sit on.
871
+ function members(p) {
872
+ const entries = plainEntries(p.value);
873
+ if (undefined === entries || undefined === p.trail) {
874
+ return entries;
875
+ }
876
+ const last = entries[entries.length - 1];
877
+ if (undefined === last || ('pair' !== last.t && 'spread' !== last.t)) {
878
+ return undefined;
879
+ }
880
+ const trail = undefined === last.trail ? p.trail : last.trail + ' ' + p.trail;
881
+ return entries.slice(0, -1).concat([{ ...last, trail }]);
882
+ }
883
+ // Adjacent statements naming one key, whose values are plain maps, are
884
+ // one map: their entries in order, with the comments and blank lines
885
+ // between the statements travelling with the statement they preceded.
886
+ // Only ADJACENT statements merge -- a `server:` line, something else,
887
+ // then another `server:` line stays as it is, because merging them
888
+ // would move a statement, and the formatter never reorders (§3.13).
889
+ // Nor do two statements merge into a map with two spreads: the engine
890
+ // keeps those as a conjunction, which is not the meet of the two maps.
891
+ // The tree is not changed: a merged statement is a new node that keeps
892
+ // the statements it replaces as its `orig`, its spelling before, and a
893
+ // statement merged somewhere below is copied the same way.
894
+ function mergeRuns(body) {
895
+ const out = [];
896
+ let i = 0;
897
+ while (i < body.length) {
898
+ const first = body[i];
899
+ const entries = 'pair' === first.t ? members(first) : undefined;
900
+ if (undefined === entries) {
901
+ out.push('pair' === first.t ? mergeDeep(first) : first);
902
+ i++;
903
+ continue;
904
+ }
905
+ const group = [first];
906
+ let merged = entries;
907
+ let carry = [];
908
+ let j = i + 1;
909
+ for (; j < body.length; j++) {
910
+ const n = body[j];
911
+ if ('comment' === n.t || 'blank' === n.t) {
912
+ carry.push(n);
913
+ continue;
914
+ }
915
+ const more = 'pair' === n.t && n.key === first.key && n.opt === first.opt
916
+ ? members(n) : undefined;
917
+ if (undefined === more || (spreads(merged) && spreads(more))) {
918
+ break;
919
+ }
920
+ group.push(...carry, n);
921
+ merged = merged.concat(carry, more);
922
+ carry = [];
923
+ }
924
+ if (1 === group.length) {
925
+ out.push(mergeDeep(first));
926
+ i++;
927
+ continue;
928
+ }
929
+ out.push({
930
+ t: 'pair', key: first.key, opt: first.opt,
931
+ value: { t: 'map', body: mergeRuns(merged) }, orig: group,
932
+ });
933
+ i = j - carry.length;
934
+ }
935
+ return out;
936
+ }
937
+ function spreads(entries) {
938
+ return entries.some((e) => 'spread' === e.t);
939
+ }
940
+ // The merge down a statement's plain-map spine: a chain's inner pair,
941
+ // or the entries of a map value, are statements of the map they are
942
+ // in. The statement itself where nothing below it merged.
943
+ function mergeDeep(p) {
944
+ const v = p.value;
945
+ const entries = plainEntries(v);
946
+ if (undefined === entries) {
947
+ return p;
948
+ }
949
+ const body = mergeRuns(entries);
950
+ if (body.length === entries.length && body.every((n, k) => n === entries[k])) {
951
+ return p;
952
+ }
953
+ return { ...p, value: 'pair' === v.t ? body[0] : { ...v, body }, orig: [p] };
954
+ }
955
+ function repeatLines(entries, prefix, indent) {
956
+ if (0 === entries.length || 'comment' === entries[entries.length - 1].t ||
957
+ 1 < entries.filter((e) => 'spread' === e.t).length) {
958
+ return undefined;
959
+ }
960
+ const out = [];
961
+ for (const e of entries) {
962
+ if ('blank' === e.t) {
963
+ out.push({ t: 'blank' });
964
+ continue;
965
+ }
966
+ if ('comment' === e.t) {
967
+ out.push({ t: 'comment', text: e.text });
968
+ continue;
969
+ }
970
+ const trail = undefined === e.trail ? '' : ' ' + e.trail;
971
+ if ('spread' === e.t) {
972
+ // The repeated spread entry is a one-entry map holding only a
973
+ // spread, so by D1's exception it keeps its braces.
974
+ const s = inline(e.value, true);
975
+ if (undefined === s || !fits(indent, prefix + '{ &: ' + s + ' }')) {
976
+ return undefined;
977
+ }
978
+ out.push({ t: 'text', text: prefix + '{ &: ' + s + ' }' + trail });
979
+ continue;
980
+ }
981
+ const head = prefix + pairHead(e, false);
982
+ const s = inline(chain(e.value), false);
983
+ if (undefined !== s && fits(indent, head + s)) {
984
+ out.push({ t: 'text', text: head + s + trail });
985
+ continue;
986
+ }
987
+ const sub = plainEntries(e.value);
988
+ if (undefined === sub) {
989
+ return undefined;
990
+ }
991
+ const lines = repeatLines(sub, head, indent);
992
+ if (undefined === lines) {
993
+ return undefined;
994
+ }
995
+ if ('' !== trail) {
996
+ lines[lines.length - 1].text += trail;
997
+ }
998
+ out.push(...lines);
999
+ }
1000
+ return out;
1001
+ }
1002
+ function fits(indent, text) {
1003
+ return indent + width(text) <= BUDGET;
1004
+ }
1005
+ // A pair in statement position, by §3.4. `prefix` is what stands
1006
+ // before it on its line: the heads of the chain it hangs from, not yet
1007
+ // written. Its value is laid out by §3.5 unless it is a plain map, and
1008
+ // then in this order: a chain, when the map holds exactly one pair
1009
+ // (D1); one line, when that fits the budget; the key repeated over the
1010
+ // entries, when every entry can be one line that way; a braced block
1011
+ // otherwise, whose entries are statements in turn. Whether the
1012
+ // statement was rewritten by this tier -- merged, or repeated -- is
1013
+ // returned, and the outermost such statement is checked: its spelling
1014
+ // on the page against what the syntactic tier writes for the
1015
+ // statements it came from, at the same indentation, which is what
1016
+ // stays on the page when the check fails.
1017
+ function emitStatement(w, p, indent, stmt, prefix) {
1018
+ const mark = w.mark();
1019
+ let rewritten = undefined !== p.orig;
1020
+ const entries = plainEntries(p.value);
1021
+ const head = prefix + pairHead(p, false);
1022
+ const s = undefined === entries ? undefined : inline(p.value, false);
1023
+ if (undefined === entries) {
1024
+ w.text(prefix);
1025
+ emitValue(w, p, indent);
1026
+ }
1027
+ else if (1 === entries.length && 'pair' === entries[0].t) {
1028
+ rewritten = emitStatement(w, entries[0], indent, { meet: stmt.meet, covered: true }, head)
1029
+ || rewritten;
1030
+ }
1031
+ else if (undefined !== s && fits(indent, head + s)) {
1032
+ w.text(head + s);
1033
+ }
1034
+ else {
1035
+ const lines = repeatLines(entries, head, indent);
1036
+ if (undefined !== lines) {
1037
+ let pending = false;
1038
+ let count = 0;
1039
+ for (const line of lines) {
1040
+ if ('blank' === line.t) {
1041
+ pending = 0 < count;
1042
+ continue;
1043
+ }
1044
+ if (0 < count) {
1045
+ w.open(indent, pending);
1046
+ }
1047
+ pending = false;
1048
+ count++;
1049
+ w.text(line.text);
1050
+ }
1051
+ rewritten = true;
1052
+ }
1053
+ else {
1054
+ w.text(head);
1055
+ emitBlock(w, '{', '}', p.value, indent, { meet: stmt.meet, covered: stmt.covered || rewritten });
1056
+ }
1057
+ }
1058
+ if (undefined !== p.trail) {
1059
+ w.text(' ' + p.trail);
1060
+ }
1061
+ if (rewritten && !stmt.covered) {
1062
+ const before = emitAt(p.orig ?? [p], indent);
1063
+ if (!stmt.meet(before, w.since(mark))) {
1064
+ w.replace(mark, before);
1065
+ }
1066
+ }
1067
+ return rewritten;
1068
+ }
1069
+ // The syntactic tier's spelling of some statements at an indentation:
1070
+ // a rewrite's spelling before.
1071
+ function emitAt(nodes, indent) {
1072
+ const w = new Writer();
1073
+ emitBody(w, nodes, indent, undefined);
1074
+ return w.finish();
1075
+ }
1076
+ // The document: by the syntactic tier alone, or with the lawful tier
1077
+ // over it when given its check.
1078
+ function emit(root, meet) {
1079
+ const w = new Writer();
1080
+ emitBody(w, undefined === meet ? root : mergeRuns(root), 0, undefined === meet ? undefined : { meet, covered: false });
1081
+ return w.finish();
1082
+ }
1083
+ // ---------------------------------------------------------------------
1084
+ // The lint (§4): what the formatter points at and never touches. Two
1085
+ // rules, both advice: the formatter never renames a key (§4.1) and
1086
+ // never introduces an alias (§4.2), and a rule with a mechanical fix
1087
+ // that keeps the document would belong to §3 instead (§4.3).
1088
+ // The shape width at which a repeat is worth an alias (§4.2): below
1089
+ // it, `{ a:1 }` twice is the shorter spelling. Measured over the use
1090
+ // cases when the lint landed (§7.10).
1091
+ const REPEAT_MIN_WIDTH = 40;
1092
+ function lintOf(root, text) {
1093
+ const out = [];
1094
+ const nodes = root.map(lintNode);
1095
+ for (const n of nodes) {
1096
+ keyCase(n, text, out);
1097
+ }
1098
+ repeats(nodes, text, out);
1099
+ out.sort((a, b) => a.line - b.line || a.col - b.col);
1100
+ return out;
1101
+ }
1102
+ // The tree the lint walks: a chain's inner pair as the one-entry map
1103
+ // it is, so that `a: {b: 1}` and `a: b: 1` -- one document to the
1104
+ // formatter -- are one shape to the lint.
1105
+ function lintNode(node) {
1106
+ if ('pair' === node.t && 'pair' === node.value.t) {
1107
+ return { ...node, value: { t: 'map', body: [node.value], at: node.value.at } };
1108
+ }
1109
+ return node;
1110
+ }
1111
+ function lintChildren(node) {
1112
+ switch (node.t) {
1113
+ case 'pair':
1114
+ case 'spread':
1115
+ return [lintNode(node).value];
1116
+ case 'map':
1117
+ case 'list':
1118
+ return node.body.map(lintNode);
1119
+ case 'call':
1120
+ return node.args;
1121
+ case 'paren':
1122
+ return node.inner;
1123
+ case 'expr':
1124
+ return node.items;
1125
+ default:
1126
+ return [];
1127
+ }
1128
+ }
1129
+ // Line and column, 1-based, of a source index.
1130
+ function lineCol(text, at) {
1131
+ const before = text.slice(0, at);
1132
+ return { line: before.split('\n').length, col: at - before.lastIndexOf('\n') };
1133
+ }
1134
+ // D4 (§4.1): keys are lower-case words, or CamelCase when a key is
1135
+ // several. A bare key holding `_`, or beginning with two capitals, is
1136
+ // reported with the spelling that would follow the form; a quoted key
1137
+ // is a deliberate spelling and a key of underscores alone names
1138
+ // nothing the rule can respell.
1139
+ function keyCase(node, text, out) {
1140
+ if ('pair' === node.t && BARE.test(node.key) && /[A-Za-z]/.test(node.key)) {
1141
+ const why = node.key.includes('_') ? 'holds an underscore'
1142
+ : /^[A-Z][A-Z]/.test(node.key) ? 'begins with capitals' : '';
1143
+ if ('' !== why) {
1144
+ out.push({
1145
+ rule: 'style/key-case', ...lineCol(text, node.at),
1146
+ message: `key ${node.key} ${why}; ${camel(node.key)} would follow the form`,
1147
+ });
1148
+ }
1149
+ }
1150
+ for (const child of lintChildren(node)) {
1151
+ keyCase(child, text, out);
1152
+ }
1153
+ }
1154
+ // The key as lower-case words or CamelCase: `credit_cents` is
1155
+ // `creditCents`, `HTTP_PORT` is `httpPort`, `HTTPServer` is
1156
+ // `httpServer`, `ID` is `id`.
1157
+ function camel(key) {
1158
+ const words = key.split('_').filter((w) => '' !== w)
1159
+ .map((w) => /^[A-Z]+$/.test(w) ? w.toLowerCase() : w);
1160
+ const head = words[0].replace(/^[A-Z]+(?=[A-Z][a-z])/, (run) => run.toLowerCase());
1161
+ return head.charAt(0).toLowerCase() + head.slice(1) +
1162
+ words.slice(1).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('');
1163
+ }
1164
+ // D3 (§4.2): a shape written twice can drift, and an alias names it
1165
+ // once. Every map or list whose shape recurs in the file, and whose
1166
+ // shape is REPEAT_MIN_WIDTH or wider, is reported once, at its first
1167
+ // site, with the count and the other sites; the naming is the
1168
+ // author's. A repeat inside a repeat is the outer one's: the walk does
1169
+ // not descend into a shape it reports.
1170
+ function repeats(nodes, text, out) {
1171
+ const counts = new Map();
1172
+ const tally = (node) => {
1173
+ if ('map' === node.t || 'list' === node.t) {
1174
+ const s = shape(node);
1175
+ counts.set(s, (counts.get(s) ?? 0) + 1);
1176
+ }
1177
+ lintChildren(node).forEach(tally);
1178
+ };
1179
+ nodes.forEach(tally);
1180
+ const sites = new Map();
1181
+ const visit = (node) => {
1182
+ if ('map' === node.t || 'list' === node.t) {
1183
+ const s = shape(node);
1184
+ if (2 <= counts.get(s) && REPEAT_MIN_WIDTH <= width(s)) {
1185
+ sites.set(s, (sites.get(s) ?? []).concat([node]));
1186
+ return;
1187
+ }
1188
+ }
1189
+ lintChildren(node).forEach(visit);
1190
+ };
1191
+ nodes.forEach(visit);
1192
+ for (const found of sites.values()) {
1193
+ if (2 <= found.length) {
1194
+ const [first, ...rest] = found.map((n) => lineCol(text, n.at));
1195
+ out.push({
1196
+ rule: 'style/repeat', ...first,
1197
+ message: `this ${found[0].t} is written ${found.length} times (again at ` +
1198
+ rest.map((p) => p.line + ':' + p.col).join(', ') +
1199
+ '); an alias would name it once',
1200
+ });
1201
+ }
1202
+ }
1203
+ }
1204
+ // A node's shape: its spelling with the layout, the comments and, for
1205
+ // a map, the order of its entries taken out, so that two spellings of
1206
+ // one value are one shape, as they are one canon.
1207
+ function shape(node) {
1208
+ switch (node.t) {
1209
+ case 'map':
1210
+ return '{' + node.body.filter(shaped).map((e) => shape(lintNode(e))).sort().join(' ') + '}';
1211
+ case 'list':
1212
+ return '[' + node.body.filter(shaped).map((e) => shape(lintNode(e))).join(' ') + ']';
1213
+ case 'pair':
1214
+ return node.key + (node.opt ? '?' : '') + ':' + shape(node.value);
1215
+ case 'spread':
1216
+ return '&:' + shape(node.value);
1217
+ case 'call':
1218
+ return node.name + '(' + node.args.filter(shaped).map(shape).join(',') + ')';
1219
+ case 'paren':
1220
+ return '(' + node.inner.filter(shaped).map(shape).join(',') + ')';
1221
+ case 'expr':
1222
+ return node.items.filter(shaped).map(shape).join('');
1223
+ default:
1224
+ return node.text;
1225
+ }
1226
+ }
1227
+ function shaped(node) {
1228
+ return 'comment' !== node.t && 'blank' !== node.t && 'note' !== node.t;
1229
+ }
1230
+ // ---------------------------------------------------------------------
1231
+ // The verb's library surface
1232
+ function lf(text) {
1233
+ return text.split('\r\n').join('\n');
1234
+ }
1235
+ // The check: the output parses, and to the same tree. Pre-unification
1236
+ // canon is that tree, positions aside, and every rewrite of the
1237
+ // syntactic tier leaves it unchanged (§7.3).
1238
+ function sameDocument(root, after) {
1239
+ const p = parseDoc(after, undefined, undefined);
1240
+ return undefined === p.errors && root.canon === p.root.canon;
1241
+ }
1242
+ // The check of a lawful rewrite: the spelling before and the spelling
1243
+ // after, evaluated in isolation, come to the same canon, the same
1244
+ // kinds of failure, and the same outcome of generation (§7.3). Local,
1245
+ // so it needs no include and no capability, and it applies whether or
1246
+ // not the document as a whole evaluates. The kinds, not the count: how
1247
+ // often one unresolved reference is reported depends on the order the
1248
+ // meet took. Generation too, because the engine generates from more
1249
+ // than the canon: a meet of maps with a nil member has refused a key
1250
+ // the same map written once generates.
1251
+ function sameByMeet(before, after) {
1252
+ return meetOf(before) === meetOf(after);
1253
+ }
1254
+ function meetOf(text) {
1255
+ const aontu = engine();
1256
+ const ctx = aontu.ctx({ collect: true });
1257
+ const v = aontu.unify(text, undefined, ctx);
1258
+ const gen = aontu.ctx({ collect: true });
1259
+ const out = aontu.generate(text, undefined, gen);
1260
+ const outcome = undefined !== out ? 'generated'
1261
+ : 0 < ctx.err.length ? kinds(ctx.err) : gen.err[0].why;
1262
+ return v.canon + '\n' + kinds(ctx.err) + '\n' + outcome;
1263
+ }
1264
+ function kinds(errs) {
1265
+ const whys = errs.map((e) => e.why);
1266
+ return whys.filter((x, i) => i === whys.indexOf(x)).sort().join(',');
1267
+ }
1268
+ function depthFinding() {
1269
+ return {
1270
+ code: 'max_depth',
1271
+ class: 'budget',
1272
+ severity: 'error',
1273
+ path: '$',
1274
+ message: `The document nests more than ${MAX_DEPTH} levels deep, past what the formatter reads.`,
1275
+ sites: [],
1276
+ };
1277
+ }
1278
+ function checkFinding(path, expected, actual) {
1279
+ return {
1280
+ code: 'format_check',
1281
+ class: 'internal',
1282
+ severity: 'error',
1283
+ path: '$',
1284
+ message: 'The formatted text is not the same document, so nothing was written.',
1285
+ note: 'a formatter defect: please report it with the source' +
1286
+ (undefined === path ? '' : ' (' + path + ')'),
1287
+ sites: [],
1288
+ expected,
1289
+ actual,
1290
+ };
1291
+ }
1292
+ // Format one document. The text is the agreed form of the source;
1293
+ // `changed` says whether it differs from what was given, which is
1294
+ // what `--check` and `--list` report.
1295
+ function format(src, opts, hooks) {
1296
+ const text = lf(src);
1297
+ const toks = [];
1298
+ const parsed = parseDoc(text, opts?.path, toks);
1299
+ if (undefined !== parsed.errors) {
1300
+ return { verdict: 'error', errors: parsed.errors };
1301
+ }
1302
+ const reader = new Reader(toks);
1303
+ const root = unwrap(reader.body('', false).body);
1304
+ if (reader.deep) {
1305
+ return { verdict: 'error', errors: [depthFinding()] };
1306
+ }
1307
+ // The syntactic tier first, checked against the parse tree; then the
1308
+ // lawful tier over it, each rewrite checked by the meet.
1309
+ const plain = emit(root, undefined);
1310
+ const same = hooks?.same ?? sameDocument;
1311
+ if (!same(parsed.root, plain)) {
1312
+ return {
1313
+ verdict: 'error',
1314
+ errors: [checkFinding(opts?.path, parsed.root.canon, plain)],
1315
+ };
1316
+ }
1317
+ const out = emit(root, hooks?.meet ?? sameByMeet);
1318
+ return {
1319
+ verdict: 'formatted', text: out, changed: out !== src,
1320
+ findings: opts?.lint ? lintOf(root, text) : [],
1321
+ };
1322
+ }
1323
+ // The lines of a text, with a marker on the last when the text does
1324
+ // not end in a newline: such a line never equals its
1325
+ // newline-terminated twin, which is how the diff reports the
1326
+ // difference, and the marker is rendered as diff renders it. NUL,
1327
+ // which no source line ends in.
1328
+ const NO_NEWLINE = String.fromCharCode(0);
1329
+ function textLines(text) {
1330
+ if ('' === text) {
1331
+ return [];
1332
+ }
1333
+ const lines = text.split('\n');
1334
+ if ('' === lines[lines.length - 1]) {
1335
+ lines.pop();
1336
+ }
1337
+ else {
1338
+ lines[lines.length - 1] += NO_NEWLINE;
1339
+ }
1340
+ return lines;
1341
+ }
1342
+ // The longest chain of anchors in order on both sides: patience
1343
+ // sorting over the right-hand positions, with the left already
1344
+ // ascending.
1345
+ function longestChain(pairs) {
1346
+ const tails = [];
1347
+ const prev = [];
1348
+ for (let k = 0; k < pairs.length; k++) {
1349
+ const j = pairs[k][1];
1350
+ let lo = 0;
1351
+ let hi = tails.length;
1352
+ while (lo < hi) {
1353
+ const mid = (lo + hi) >> 1;
1354
+ if (pairs[tails[mid]][1] < j) {
1355
+ lo = mid + 1;
1356
+ }
1357
+ else {
1358
+ hi = mid;
1359
+ }
1360
+ }
1361
+ prev[k] = 0 < lo ? tails[lo - 1] : -1;
1362
+ tails[lo] = k;
1363
+ }
1364
+ const out = [];
1365
+ let k = 0 === tails.length ? -1 : tails[tails.length - 1];
1366
+ while (0 <= k) {
1367
+ out.push(pairs[k]);
1368
+ k = prev[k];
1369
+ }
1370
+ return out.reverse();
1371
+ }
1372
+ function patience(a, x0, x1, b, y0, y1, out) {
1373
+ while (x0 < x1 && y0 < y1 && a[x0] === b[y0]) {
1374
+ out.push({ op: ' ', text: a[x0] });
1375
+ x0++;
1376
+ y0++;
1377
+ }
1378
+ let tail = 0;
1379
+ while (x0 < x1 - tail && y0 < y1 - tail && a[x1 - 1 - tail] === b[y1 - 1 - tail]) {
1380
+ tail++;
1381
+ }
1382
+ x1 -= tail;
1383
+ y1 -= tail;
1384
+ const countA = new Map();
1385
+ const countB = new Map();
1386
+ const posB = new Map();
1387
+ for (let x = x0; x < x1; x++) {
1388
+ countA.set(a[x], (countA.get(a[x]) ?? 0) + 1);
1389
+ }
1390
+ for (let y = y0; y < y1; y++) {
1391
+ countB.set(b[y], (countB.get(b[y]) ?? 0) + 1);
1392
+ posB.set(b[y], y);
1393
+ }
1394
+ const pairs = [];
1395
+ for (let x = x0; x < x1; x++) {
1396
+ if (1 === countA.get(a[x]) && 1 === countB.get(a[x])) {
1397
+ pairs.push([x, posB.get(a[x])]);
1398
+ }
1399
+ }
1400
+ const anchors = longestChain(pairs);
1401
+ if (0 === anchors.length) {
1402
+ for (let x = x0; x < x1; x++) {
1403
+ out.push({ op: '-', text: a[x] });
1404
+ }
1405
+ for (let y = y0; y < y1; y++) {
1406
+ out.push({ op: '+', text: b[y] });
1407
+ }
1408
+ }
1409
+ else {
1410
+ let x = x0;
1411
+ let y = y0;
1412
+ for (const [ax, ay] of anchors) {
1413
+ patience(a, x, ax, b, y, ay, out);
1414
+ out.push({ op: ' ', text: a[ax] });
1415
+ x = ax + 1;
1416
+ y = ay + 1;
1417
+ }
1418
+ patience(a, x, x1, b, y, y1, out);
1419
+ }
1420
+ for (let k = 0; k < tail; k++) {
1421
+ out.push({ op: ' ', text: a[x1 + k] });
1422
+ }
1423
+ }
1424
+ // The diff in unified format, three lines of context, the file named
1425
+ // on both sides. Empty when the texts are the same.
1426
+ function unifiedDiff(name, before, after) {
1427
+ const a = textLines(before);
1428
+ const b = textLines(after);
1429
+ const edits = [];
1430
+ patience(a, 0, a.length, b, 0, b.length, edits);
1431
+ // Hunks: changes closer than twice the context share one.
1432
+ const hunks = [];
1433
+ for (let k = 0; k < edits.length; k++) {
1434
+ if (' ' === edits[k].op) {
1435
+ continue;
1436
+ }
1437
+ const last = hunks[hunks.length - 1];
1438
+ if (undefined !== last && k - last[1] <= 6) {
1439
+ last[1] = k;
1440
+ }
1441
+ else {
1442
+ hunks.push([k, k]);
1443
+ }
1444
+ }
1445
+ if (0 === hunks.length) {
1446
+ return '';
1447
+ }
1448
+ const out = ['--- a/' + name, '+++ b/' + name];
1449
+ let ai = 0;
1450
+ let bi = 0;
1451
+ let next = 0;
1452
+ for (const [s, e] of hunks) {
1453
+ const from = Math.max(s - 3, 0);
1454
+ const to = Math.min(e + 4, edits.length);
1455
+ // Everything between two hunks is context -- a change would have
1456
+ // opened a hunk -- so both sides advance together.
1457
+ for (; next < from; next++) {
1458
+ ai++;
1459
+ bi++;
1460
+ }
1461
+ let alen = 0;
1462
+ let blen = 0;
1463
+ const lines = [];
1464
+ for (let k = from; k < to; k++) {
1465
+ const ed = edits[k];
1466
+ if ('+' !== ed.op) {
1467
+ alen++;
1468
+ }
1469
+ if ('-' !== ed.op) {
1470
+ blen++;
1471
+ }
1472
+ if (ed.text.endsWith(NO_NEWLINE)) {
1473
+ lines.push(ed.op + ed.text.slice(0, -1));
1474
+ lines.push('\');
1475
+ }
1476
+ else {
1477
+ lines.push(ed.op + ed.text);
1478
+ }
1479
+ }
1480
+ out.push('@@ -' + (0 === alen ? ai : ai + 1) + ',' + alen +
1481
+ ' +' + (0 === blen ? bi : bi + 1) + ',' + blen + ' @@');
1482
+ out.push(...lines);
1483
+ ai += alen;
1484
+ bi += blen;
1485
+ next = to;
1486
+ }
1487
+ return out.join('\n') + '\n';
1488
+ }
1489
+ //# sourceMappingURL=format.js.map