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.
- 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 +4 -2
- package/dist/aontu.js +5 -2
- package/dist/aontu.js.map +1 -1
- package/dist/cli.d.ts +10 -3
- package/dist/cli.js +311 -53
- 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/escape.d.ts +5 -0
- package/dist/escape.js +455 -0
- package/dist/escape.js.map +1 -0
- package/dist/format.d.ts +26 -0
- package/dist/format.js +1489 -0
- package/dist/format.js.map +1 -0
- package/dist/hints.js +39 -1
- 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 +84 -6
- package/dist/lang.js.map +1 -1
- package/dist/lsp.d.ts +2 -1
- package/dist/lsp.js +4 -4
- package/dist/lsp.js.map +1 -1
- package/dist/mcp-server.js +2 -2
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp.js +6 -4
- package/dist/mcp.js.map +1 -1
- package/dist/mod-tool.js +8 -7
- package/dist/mod-tool.js.map +1 -1
- package/dist/mod.js +6 -6
- package/dist/mod.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/sigdecl.js +1 -1
- package/dist/sigdecl.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/val/EachFuncVal.js +1 -2
- package/dist/val/EachFuncVal.js.map +1 -1
- package/dist/val/EmitFuncVal.d.ts +23 -0
- package/dist/val/EmitFuncVal.js +261 -0
- package/dist/val/EmitFuncVal.js.map +1 -0
- package/dist/val/FilterFuncVal.js +1 -2
- package/dist/val/FilterFuncVal.js.map +1 -1
- package/dist/val/FuncBaseVal.d.ts +1 -0
- package/dist/val/FuncBaseVal.js +16 -0
- package/dist/val/FuncBaseVal.js.map +1 -1
- package/dist/val/PackFuncVal.js +1 -2
- package/dist/val/PackFuncVal.js.map +1 -1
- package/dist/val/PlaceVal.d.ts +3 -1
- package/dist/val/PlaceVal.js +8 -6
- package/dist/val/PlaceVal.js.map +1 -1
- package/dist/val/StrFuncVal.d.ts +32 -0
- package/dist/val/StrFuncVal.js +292 -0
- package/dist/val/StrFuncVal.js.map +1 -0
- 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 +156 -0
- package/grammar/aontu.gbnf +4 -3
- package/grammar/aontu.lark +4 -3
- package/grammar/aontu.tmLanguage.json +1 -1
- package/package.json +2 -1
- package/skill/grammar-card.md +5 -2
- package/src/agentsmd.ts +15 -3
- package/src/aontu.ts +8 -1
- package/src/cli.ts +365 -58
- package/src/diff.ts +7 -2
- package/src/escape.ts +371 -0
- package/src/format.ts +1728 -0
- package/src/hints.ts +53 -1
- package/src/jsonschema.ts +7 -1
- package/src/lang.ts +93 -6
- package/src/lsp.ts +8 -6
- package/src/mcp-server.ts +3 -2
- package/src/mcp.ts +6 -4
- package/src/mod-tool.ts +9 -8
- package/src/mod.ts +6 -6
- 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/sigdecl.ts +1 -1
- 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/val/EachFuncVal.ts +1 -3
- package/src/val/EmitFuncVal.ts +401 -0
- package/src/val/FilterFuncVal.ts +1 -3
- package/src/val/FuncBaseVal.ts +18 -0
- package/src/val/PackFuncVal.ts +1 -3
- package/src/val/PlaceVal.ts +8 -6
- package/src/val/StrFuncVal.ts +334 -0
- package/src/vet.ts +9 -3
- 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
|