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.
- package/README.md +3 -3
- package/dist/agentsmd.d.ts +1 -0
- package/dist/agentsmd.js +10 -3
- package/dist/agentsmd.js.map +1 -1
- package/dist/aontu.d.ts +3 -2
- package/dist/aontu.js +5 -2
- package/dist/aontu.js.map +1 -1
- package/dist/cli.d.ts +5 -2
- package/dist/cli.js +236 -49
- package/dist/cli.js.map +1 -1
- package/dist/diff.d.ts +1 -0
- package/dist/diff.js +3 -2
- package/dist/diff.js.map +1 -1
- package/dist/format.d.ts +17 -0
- package/dist/format.js +1000 -0
- package/dist/format.js.map +1 -0
- package/dist/hints.js +4 -0
- package/dist/hints.js.map +1 -1
- package/dist/jsonschema.d.ts +1 -0
- package/dist/jsonschema.js +3 -2
- package/dist/jsonschema.js.map +1 -1
- package/dist/lang.js +64 -5
- package/dist/lang.js.map +1 -1
- package/dist/lsp.d.ts +2 -1
- package/dist/lsp.js +1 -1
- package/dist/lsp.js.map +1 -1
- package/dist/mcp.js +6 -4
- package/dist/mcp.js.map +1 -1
- package/dist/patch.d.ts +1 -0
- package/dist/patch.js +1 -0
- package/dist/patch.js.map +1 -1
- package/dist/query.d.ts +1 -0
- package/dist/query.js +4 -3
- package/dist/query.js.map +1 -1
- package/dist/reach.d.ts +1 -0
- package/dist/reach.js +3 -2
- package/dist/reach.js.map +1 -1
- package/dist/relation.d.ts +1 -0
- package/dist/relation.js +3 -2
- package/dist/relation.js.map +1 -1
- package/dist/std.js +2 -1
- package/dist/std.js.map +1 -1
- package/dist/subsume.d.ts +1 -0
- package/dist/subsume.js +3 -2
- package/dist/subsume.js.map +1 -1
- package/dist/trim.d.ts +1 -0
- package/dist/trim.js +3 -2
- package/dist/trim.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/type.d.ts +1 -0
- package/dist/type.js.map +1 -1
- package/dist/utility.d.ts +8 -2
- package/dist/utility.js +9 -1
- package/dist/utility.js.map +1 -1
- package/dist/vet.d.ts +1 -0
- package/dist/vet.js +1 -1
- package/dist/vet.js.map +1 -1
- package/dist/view.d.ts +2 -1
- package/dist/view.js +379 -8
- package/dist/view.js.map +1 -1
- package/grammar/aontu.abnf +155 -0
- package/package.json +2 -1
- package/skill/grammar-card.md +5 -2
- package/src/agentsmd.ts +15 -3
- package/src/aontu.ts +7 -1
- package/src/cli.ts +267 -53
- package/src/diff.ts +7 -2
- package/src/format.ts +1146 -0
- package/src/hints.ts +5 -0
- package/src/jsonschema.ts +7 -1
- package/src/lang.ts +69 -5
- package/src/lsp.ts +5 -3
- package/src/mcp.ts +6 -4
- package/src/patch.ts +7 -0
- package/src/query.ts +8 -4
- package/src/reach.ts +7 -2
- package/src/relation.ts +7 -2
- package/src/std.ts +2 -1
- package/src/subsume.ts +7 -2
- package/src/trim.ts +7 -2
- package/src/type.ts +8 -0
- package/src/utility.ts +27 -2
- package/src/vet.ts +9 -3
- 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
|