@tikoci/centrs 0.1.9-next.22 → 0.1.9-next.23
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/GLOSSARY.txt +24 -0
- package/commands/execute/examples.md +19 -0
- package/commands/explain/README.md +428 -128
- package/commands/explain/examples.md +260 -1
- package/docs/CONSTITUTION.md +4 -0
- package/docs/MATRIX.md +23 -11
- package/package.json +4 -1
- package/src/explain/args.ts +69 -8
- package/src/explain/blocks.ts +3 -4
- package/src/explain/brace-slots.ts +5 -0
- package/src/explain/catalog.ts +13 -6
- package/src/explain/defects.ts +1 -0
- package/src/explain/group-faults.ts +927 -0
- package/src/explain/highlight-projection.ts +10 -8
- package/src/explain/operators.ts +20 -19
- package/src/explain/pathresolve.ts +102 -20
- package/src/explain/quoted-string.ts +41 -6
- package/src/explain/run-tokens.ts +223 -0
- package/src/explain/scope-brace.ts +103 -6
- package/src/explain/segment.ts +41 -3
- package/src/explain/spaced-args.ts +116 -0
- package/src/explain/symbols.ts +17 -1
- package/src/explain/transport.ts +13 -0
- package/src/explain/verbsplit.ts +14 -188
- package/src/explain/write.ts +8 -1
- package/src/explain.ts +378 -18
- package/src/offline-gate.ts +30 -9
- package/src/protocols/btest-session.ts +30 -10
- package/src/protocols/mac-telnet-console.ts +16 -0
package/src/explain/transport.ts
CHANGED
|
@@ -111,6 +111,17 @@ export interface ExplainTransportInput {
|
|
|
111
111
|
* centrs surface should be offered for them and none is rendered.
|
|
112
112
|
*/
|
|
113
113
|
rejected?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Why offline cannot pair the operands into a request, when two adjacent
|
|
116
|
+
* words may be an argument and its value with the `=` left out (#350).
|
|
117
|
+
*
|
|
118
|
+
* Also distinct from an unread argument list: the bytes are no evaluation,
|
|
119
|
+
* and nothing is wrong with them, but which word is an argument name is
|
|
120
|
+
* per-command schema. The reading before #350 classified these `unknown`
|
|
121
|
+
* through its positionals, and this keeps that fail-closed answer rather
|
|
122
|
+
* than letting the withdrawal turn it into an `execute` line.
|
|
123
|
+
*/
|
|
124
|
+
unpaired?: string;
|
|
114
125
|
}
|
|
115
126
|
|
|
116
127
|
export interface ExplainTransportOptions {
|
|
@@ -440,6 +451,8 @@ export function classifyExplainTransport(
|
|
|
440
451
|
// syntax error, which is the false confident reading in a second place.
|
|
441
452
|
if (input.rejected !== undefined)
|
|
442
453
|
return unknown(input.rejected, options.evidenceId);
|
|
454
|
+
if (input.unpaired !== undefined)
|
|
455
|
+
return unknown(input.unpaired, options.evidenceId);
|
|
443
456
|
|
|
444
457
|
// Root scripting directives have no RouterOS REST path. They always ride the
|
|
445
458
|
// raw execute surface, regardless of whether their operands are literal.
|
package/src/explain/verbsplit.ts
CHANGED
|
@@ -88,6 +88,12 @@ import {
|
|
|
88
88
|
type Span,
|
|
89
89
|
type StatementAnalysis,
|
|
90
90
|
} from "./pathresolve.ts";
|
|
91
|
+
import {
|
|
92
|
+
asciiWordSpans,
|
|
93
|
+
isAsciiWhitespace,
|
|
94
|
+
type RunToken,
|
|
95
|
+
runTokens,
|
|
96
|
+
} from "./run-tokens.ts";
|
|
91
97
|
import {
|
|
92
98
|
documentRange,
|
|
93
99
|
type MaskedRange,
|
|
@@ -96,20 +102,6 @@ import {
|
|
|
96
102
|
} from "./segment.ts";
|
|
97
103
|
import { SUBMENU_DIRECTIVES, VERBS } from "./verbs.ts";
|
|
98
104
|
|
|
99
|
-
const BARE_WORD = /^[A-Za-z][A-Za-z0-9._-]*$/;
|
|
100
|
-
/**
|
|
101
|
-
* A word that ends the leading run: it carries `=` (V2) or opens a group /
|
|
102
|
-
* string / variable (V3). `:` is deliberately NOT here — RouterOS uses it only
|
|
103
|
-
* to start a scripting directive, not as a mid-statement opener, and a
|
|
104
|
-
* `:`-prefixed token is already rejected by `BARE_WORD` below, so it ends the
|
|
105
|
-
* run regardless.
|
|
106
|
-
*/
|
|
107
|
-
const RUN_TERMINATOR = /^[[({"$]/;
|
|
108
|
-
|
|
109
|
-
function isAsciiWhitespace(char: string | undefined): boolean {
|
|
110
|
-
return char === " " || char === "\t" || char === "\r" || char === "\n";
|
|
111
|
-
}
|
|
112
|
-
|
|
113
105
|
function trimAscii(text: string): string {
|
|
114
106
|
let start = 0;
|
|
115
107
|
let end = text.length;
|
|
@@ -125,78 +117,14 @@ function trimAsciiStart(text: string): string {
|
|
|
125
117
|
return text.slice(start);
|
|
126
118
|
}
|
|
127
119
|
|
|
128
|
-
/**
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
chars: Span[];
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* The words of a statement, WITH their offsets (#202c).
|
|
141
|
-
*
|
|
142
|
-
* The one word scanner this module has. It replaced a
|
|
143
|
-
* `replace(/\\\r?\n/g,"").split(whitespace)` pair whose output could not be
|
|
144
|
-
* mapped back to the text it came from — and a second scanner written beside it
|
|
145
|
-
* for the offsets would be free to disagree with the boundary rule, which is the
|
|
146
|
-
* failure #202b hit three times in argument handling.
|
|
147
|
-
*
|
|
148
|
-
* H5 from the Q1 segmenter: a `\<newline>` is removed before RouterOS parses the
|
|
149
|
-
* continued line, so it JOINS the words either side of it rather than separating
|
|
150
|
-
* them (`na\<nl>me` is the single word `name`) and contributes no character to
|
|
151
|
-
* the name. That is why a word's `end - start` is not its `name.length`: the span
|
|
152
|
-
* covers the continuation bytes, the name does not. A caller taking an interior
|
|
153
|
-
* offset from a name is therefore wrong on continued statements, and
|
|
154
|
-
* {@link describeStatement} only ever publishes the run's END.
|
|
155
|
-
*/
|
|
156
|
-
function* asciiWordSpans(text: string): Generator<Word> {
|
|
157
|
-
// Yielded ONE WORD AT A TIME rather than returned as a list. Both readers
|
|
158
|
-
// stop at the first word the leading run cannot take, and a statement's text
|
|
159
|
-
// carries its whole nested `do={…}` subtree — so building a word (and a span
|
|
160
|
-
// per character of it) for the rest of that subtree was work no caller ever
|
|
161
|
-
// looked at, once per enclosing level (#322). A word is complete when the
|
|
162
|
-
// next one starts, which is why the yield happens on the following boundary
|
|
163
|
-
// rather than at the point the word is opened.
|
|
164
|
-
let current: Word | null = null;
|
|
165
|
-
let i = 0;
|
|
166
|
-
while (i < text.length) {
|
|
167
|
-
const c = text[i] as string;
|
|
168
|
-
if (
|
|
169
|
-
c === "\\" &&
|
|
170
|
-
(text[i + 1] === "\n" || (text[i + 1] === "\r" && text[i + 2] === "\n"))
|
|
171
|
-
) {
|
|
172
|
-
// Inside a word this joins; between words it is whitespace that opens
|
|
173
|
-
// none — matching the removal-then-split the rule replaced.
|
|
174
|
-
i += text[i + 1] === "\r" ? 3 : 2;
|
|
175
|
-
if (current !== null) current.end = i;
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
if (isAsciiWhitespace(c)) {
|
|
179
|
-
if (current !== null) yield current;
|
|
180
|
-
current = null;
|
|
181
|
-
i++;
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
if (current === null) {
|
|
185
|
-
current = {
|
|
186
|
-
name: c,
|
|
187
|
-
start: i,
|
|
188
|
-
end: i + 1,
|
|
189
|
-
chars: [{ start: i, end: i + 1 }],
|
|
190
|
-
};
|
|
191
|
-
} else {
|
|
192
|
-
current.name += c;
|
|
193
|
-
current.end = i + 1;
|
|
194
|
-
current.chars.push({ start: i, end: i + 1 });
|
|
195
|
-
}
|
|
196
|
-
i++;
|
|
197
|
-
}
|
|
198
|
-
if (current !== null) yield current;
|
|
199
|
-
}
|
|
120
|
+
/** The run scanner lives in the leaf `run-tokens.ts` so `segment.ts` can share it (#496). */
|
|
121
|
+
export {
|
|
122
|
+
type LocatedRunToken,
|
|
123
|
+
locatedRunTokens,
|
|
124
|
+
type RunToken,
|
|
125
|
+
runTokens,
|
|
126
|
+
type Sep,
|
|
127
|
+
} from "./run-tokens.ts";
|
|
200
128
|
|
|
201
129
|
/**
|
|
202
130
|
* The frozen vocabulary lives in the leaf `verbs.ts` because `pathresolve.ts`
|
|
@@ -205,108 +133,6 @@ function* asciiWordSpans(text: string): Generator<Word> {
|
|
|
205
133
|
*/
|
|
206
134
|
export { SUBMENU_DIRECTIVES, VERBS } from "./verbs.ts";
|
|
207
135
|
|
|
208
|
-
/** Separator that precedes a run token (V1). */
|
|
209
|
-
export type Sep = "start" | "slash" | "space";
|
|
210
|
-
|
|
211
|
-
/** One token of the leading path run, with the punctuation that introduced it. */
|
|
212
|
-
export interface RunToken {
|
|
213
|
-
name: string;
|
|
214
|
-
sep: Sep;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** A run token mapped back to the statement bytes that spell it. */
|
|
218
|
-
export interface LocatedRunToken extends RunToken {
|
|
219
|
-
/** Name bytes, split where a `\<newline>` continuation interrupts them. */
|
|
220
|
-
nameSpans: Span[];
|
|
221
|
-
/** One valid `/` immediately before this token, if present. */
|
|
222
|
-
slashBefore?: Span;
|
|
223
|
-
/** One trailing `/` after this token, if present. */
|
|
224
|
-
slashAfter?: Span;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function coalesceChars(chars: readonly Span[]): Span[] {
|
|
228
|
-
const out: Span[] = [];
|
|
229
|
-
for (const char of chars) {
|
|
230
|
-
const last = out.at(-1);
|
|
231
|
-
if (last !== undefined && last.end === char.start) last.end = char.end;
|
|
232
|
-
else out.push({ start: char.start, end: char.end });
|
|
233
|
-
}
|
|
234
|
-
return out;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* The leading path-token run WITH separators preserved (V1). Same stopping
|
|
239
|
-
* rules as `pathresolve`'s run (V2/V3), so the two stay comparable: a slash-
|
|
240
|
-
* joined word expands to several `slash`-separated tokens; a space-separated
|
|
241
|
-
* word is one `space` token. Stops at the first `=`/group/string/var word, and
|
|
242
|
-
* at any word carrying a non-`BARE_WORD` segment (a variable segment truncates
|
|
243
|
-
* the run before the verb).
|
|
244
|
-
*/
|
|
245
|
-
export function locatedRunTokens(text: string): LocatedRunToken[] {
|
|
246
|
-
const out: LocatedRunToken[] = [];
|
|
247
|
-
for (const word of asciiWordSpans(text)) {
|
|
248
|
-
if (word.name.includes("=") || RUN_TERMINATOR.test(word.name)) break;
|
|
249
|
-
const parts: {
|
|
250
|
-
name: string;
|
|
251
|
-
start: number;
|
|
252
|
-
end: number;
|
|
253
|
-
slashBefore?: Span;
|
|
254
|
-
}[] = [];
|
|
255
|
-
let start = 0;
|
|
256
|
-
for (let i = 0; i <= word.name.length; i++) {
|
|
257
|
-
if (i < word.name.length && word.name[i] !== "/") continue;
|
|
258
|
-
if (start < i) {
|
|
259
|
-
const slashRunStart = start;
|
|
260
|
-
let slashCount = 0;
|
|
261
|
-
while (
|
|
262
|
-
slashRunStart - slashCount - 1 >= 0 &&
|
|
263
|
-
word.name[slashRunStart - slashCount - 1] === "/"
|
|
264
|
-
)
|
|
265
|
-
slashCount++;
|
|
266
|
-
const slashIndex = slashCount === 1 ? slashRunStart - 1 : -1;
|
|
267
|
-
parts.push({
|
|
268
|
-
name: word.name.slice(start, i),
|
|
269
|
-
start,
|
|
270
|
-
end: i,
|
|
271
|
-
...(slashIndex < 0 ? {} : { slashBefore: word.chars[slashIndex] }),
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
start = i + 1;
|
|
275
|
-
}
|
|
276
|
-
if (parts.length === 0) continue;
|
|
277
|
-
if (!parts.every((p) => BARE_WORD.test(p.name))) break;
|
|
278
|
-
for (let i = 0; i < parts.length; i++) {
|
|
279
|
-
const part = parts[i] as (typeof parts)[number];
|
|
280
|
-
const leadingSlash = word.name.startsWith("/") && i === 0;
|
|
281
|
-
const trailingSlash =
|
|
282
|
-
i === parts.length - 1 &&
|
|
283
|
-
word.name.endsWith("/") &&
|
|
284
|
-
!word.name.endsWith("//")
|
|
285
|
-
? word.chars.at(-1)
|
|
286
|
-
: undefined;
|
|
287
|
-
out.push({
|
|
288
|
-
name: part.name,
|
|
289
|
-
sep:
|
|
290
|
-
out.length === 0
|
|
291
|
-
? "start"
|
|
292
|
-
: i > 0 || leadingSlash
|
|
293
|
-
? "slash"
|
|
294
|
-
: "space",
|
|
295
|
-
nameSpans: coalesceChars(word.chars.slice(part.start, part.end)),
|
|
296
|
-
...(part.slashBefore === undefined
|
|
297
|
-
? {}
|
|
298
|
-
: { slashBefore: part.slashBefore }),
|
|
299
|
-
...(trailingSlash === undefined ? {} : { slashAfter: trailingSlash }),
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return out;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export function runTokens(text: string): RunToken[] {
|
|
307
|
-
return locatedRunTokens(text).map(({ name, sep }) => ({ name, sep }));
|
|
308
|
-
}
|
|
309
|
-
|
|
310
136
|
/** True when the statement is a scripting directive (`:x`, or a bare `x do={…}`). */
|
|
311
137
|
export function isDirective(text: string, range?: MaskedRange): boolean {
|
|
312
138
|
const t = trimAscii(text);
|
package/src/explain/write.ts
CHANGED
|
@@ -694,7 +694,14 @@ function collect(
|
|
|
694
694
|
});
|
|
695
695
|
continue;
|
|
696
696
|
}
|
|
697
|
-
|
|
697
|
+
// An ABSOLUTE `maybeNav` statement (#211) was `isNav` before R14, so it
|
|
698
|
+
// keeps this branch. `isConfirmedNav` cannot drop it, because its path is
|
|
699
|
+
// in neither table. A relative one never took this branch (#235), and
|
|
700
|
+
// still does not.
|
|
701
|
+
if (
|
|
702
|
+
(statement.isNav || (statement.maybeNav && t.startsWith("/"))) &&
|
|
703
|
+
!isCommandShaped(described, catalogAt)
|
|
704
|
+
) {
|
|
698
705
|
if (
|
|
699
706
|
isConfirmedNav(described) &&
|
|
700
707
|
!isDanglingBarePath(described, lastNonEmpty, index)
|