@plurnk/plurnk-contracts 1.16.5 → 1.18.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 +13 -62
- package/SPEC.md +633 -529
- package/dist/conformance/agui-v1.json +3 -50
- package/dist/schema/CapabilityDescriptor.json +6 -1
- package/dist/schema/CapabilityProjection.json +2 -4
- package/dist/schema/CapabilitySelector.json +6 -1
- package/dist/schema/ClientStatement.json +5 -52
- package/dist/schema/FunctionalityDefinitionState.json +10 -2
- package/dist/schema/LineMarker.json +1 -1
- package/dist/schema/LoopPolicy.json +2 -3
- package/dist/schema/MatcherBody.json +6 -6
- package/dist/schema/McpServerDefinition.json +17 -0
- package/dist/schema/ModelCatalogPage.json +10 -1
- package/dist/schema/ModelRoute.json +9 -0
- package/dist/schema/Notice.json +1 -1
- package/dist/schema/ParsedPath.json +3 -2
- package/dist/schema/Plan.json +10 -6
- package/dist/schema/PlurnkStatement.json +95 -143
- package/dist/schema/ProposalProjection.json +6 -1
- package/dist/schema/ResourceSelection.json +51 -27
- package/dist/schema/SkillDefinition.json +3 -3
- package/dist/src/AcpPlanValue.d.ts +0 -1
- package/dist/src/AcpPlanValue.d.ts.map +1 -1
- package/dist/src/AcpPlanValue.js +14 -13
- package/dist/src/AcpPlanValue.js.map +1 -1
- package/dist/src/ApplicationPort.d.ts +17 -14
- package/dist/src/ApplicationPort.d.ts.map +1 -1
- package/dist/src/JsonDocument.d.ts +2 -0
- package/dist/src/JsonDocument.d.ts.map +1 -0
- package/dist/src/JsonDocument.js +14 -0
- package/dist/src/JsonDocument.js.map +1 -0
- package/dist/src/LoopLifecycle.d.ts +3 -0
- package/dist/src/LoopLifecycle.d.ts.map +1 -0
- package/dist/src/LoopLifecycle.js +14 -0
- package/dist/src/LoopLifecycle.js.map +1 -0
- package/dist/src/PlanValue.d.ts +1 -1
- package/dist/src/PlanValue.d.ts.map +1 -1
- package/dist/src/PlanValue.js +10 -6
- package/dist/src/PlanValue.js.map +1 -1
- package/dist/src/PlurnkParseError.d.ts +3 -1
- package/dist/src/PlurnkParseError.d.ts.map +1 -1
- package/dist/src/PlurnkParseError.js +4 -1
- package/dist/src/PlurnkParseError.js.map +1 -1
- package/dist/src/TurnDisposition.d.ts +11 -0
- package/dist/src/TurnDisposition.d.ts.map +1 -0
- package/dist/src/TurnDisposition.js +32 -0
- package/dist/src/TurnDisposition.js.map +1 -0
- package/dist/src/Validator.js +1 -1
- package/dist/src/Validator.js.map +1 -1
- package/dist/src/index.d.ts +5 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/types.d.ts +13 -8
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.generated.d.ts +172 -86
- package/dist/src/types.generated.d.ts.map +1 -1
- package/dist/src/types.js +9 -4
- package/dist/src/types.js.map +1 -1
- package/package.json +5 -23
- package/plurnk.md +106 -119
- package/bin/plurnk-contracts.js +0 -43
- package/dist/plurnk.gemma.gbnf +0 -141
- package/dist/plurnk.qwen.gbnf +0 -130
- package/dist/src/AstBuilder.d.ts +0 -20
- package/dist/src/AstBuilder.d.ts.map +0 -1
- package/dist/src/AstBuilder.js +0 -723
- package/dist/src/AstBuilder.js.map +0 -1
- package/dist/src/PlurnkErrorStrategy.d.ts +0 -11
- package/dist/src/PlurnkErrorStrategy.d.ts.map +0 -1
- package/dist/src/PlurnkErrorStrategy.js +0 -358
- package/dist/src/PlurnkErrorStrategy.js.map +0 -1
- package/dist/src/PlurnkParser.d.ts +0 -11
- package/dist/src/PlurnkParser.d.ts.map +0 -1
- package/dist/src/PlurnkParser.js +0 -335
- package/dist/src/PlurnkParser.js.map +0 -1
- package/dist/src/RecordingListener.d.ts +0 -9
- package/dist/src/RecordingListener.d.ts.map +0 -1
- package/dist/src/RecordingListener.js +0 -19
- package/dist/src/RecordingListener.js.map +0 -1
- package/dist/src/generated/plurnkLexer.d.ts +0 -174
- package/dist/src/generated/plurnkLexer.d.ts.map +0 -1
- package/dist/src/generated/plurnkLexer.js +0 -1215
- package/dist/src/generated/plurnkLexer.js.map +0 -1
- package/dist/src/generated/plurnkParser.d.ts +0 -477
- package/dist/src/generated/plurnkParser.d.ts.map +0 -1
- package/dist/src/generated/plurnkParser.js +0 -3298
- package/dist/src/generated/plurnkParser.js.map +0 -1
- package/dist/src/generated/plurnkParserVisitor.d.ts +0 -284
- package/dist/src/generated/plurnkParserVisitor.d.ts.map +0 -1
- package/dist/src/generated/plurnkParserVisitor.js +0 -245
- package/dist/src/generated/plurnkParserVisitor.js.map +0 -1
package/dist/src/AstBuilder.js
DELETED
|
@@ -1,723 +0,0 @@
|
|
|
1
|
-
/// <reference path="./json-p3-esm.d.ts" />
|
|
2
|
-
import { ParserRuleContext, TerminalNode } from "antlr4ng";
|
|
3
|
-
import * as xpath from "xpath";
|
|
4
|
-
import { JSONPathEnvironment } from "json-p3/dist/json-p3.esm.js";
|
|
5
|
-
import { BodyContext, LineMarkerContext, MetadataContext, MidSendContext, PlanStatementContext, SendStatementContext, TargetContext, } from "./generated/plurnkParser.js";
|
|
6
|
-
import { plurnkLexer } from "./generated/plurnkLexer.js";
|
|
7
|
-
import PlurnkParseError from "./PlurnkParseError.js";
|
|
8
|
-
import PathSyntax from "./PathSyntax.js";
|
|
9
|
-
import PlanValue from "./PlanValue.js";
|
|
10
|
-
export default class AstBuilder {
|
|
11
|
-
// {§misplaced-annotation-advisory} — advisories raised while building one statement; the
|
|
12
|
-
// parser drains them right after the statement so the model sees WHAT it did on the first try.
|
|
13
|
-
static #advisories = [];
|
|
14
|
-
static takeAdvisories() {
|
|
15
|
-
const taken = AstBuilder.#advisories;
|
|
16
|
-
AstBuilder.#advisories = [];
|
|
17
|
-
return taken;
|
|
18
|
-
}
|
|
19
|
-
// A body that is solely an HTML comment can never be a matcher. Preserve it
|
|
20
|
-
// as the operation annotation and report only that deterministic normalization.
|
|
21
|
-
static #annotationBody(op, delimiter, annotation, raw, position) {
|
|
22
|
-
if (raw === null)
|
|
23
|
-
return { annotation, raw };
|
|
24
|
-
const comment = /^\s*<!--([\s\S]*?)-->\s*$/u.exec(raw);
|
|
25
|
-
if (comment === null)
|
|
26
|
-
return { annotation, raw };
|
|
27
|
-
AstBuilder.#advisories.push(new PlurnkParseError(position.line, position.column, "parser", `The ${op} body contained only an HTML comment; it was applied as the operation annotation.`, "warning"));
|
|
28
|
-
return { annotation: annotation ?? (comment[1] ?? "").trim(), raw: null };
|
|
29
|
-
}
|
|
30
|
-
static #SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
31
|
-
// Compile-only RFC 9535 admission using the runtime's JSONPath engine. {§matcher-prefix-claims}
|
|
32
|
-
static #JSONPATH = new JSONPathEnvironment();
|
|
33
|
-
static #GRAPH_MATCHER = /^&[<>]?[^\s<>]\S*$/u;
|
|
34
|
-
static build(ctx) {
|
|
35
|
-
// The strict turn root attaches the leading PLAN and the terminal SEND as direct
|
|
36
|
-
// children (not wrapped in `statement`), so dispatch those by type first.
|
|
37
|
-
if (ctx instanceof PlanStatementContext)
|
|
38
|
-
return AstBuilder.#buildPlan(ctx);
|
|
39
|
-
if (ctx instanceof SendStatementContext)
|
|
40
|
-
return AstBuilder.#buildSend(ctx);
|
|
41
|
-
if (ctx instanceof MidSendContext)
|
|
42
|
-
return AstBuilder.#buildMidSend(ctx);
|
|
43
|
-
const midSend = ctx.midSend();
|
|
44
|
-
if (midSend)
|
|
45
|
-
return AstBuilder.#buildMidSend(midSend);
|
|
46
|
-
const find = ctx.findStatement();
|
|
47
|
-
if (find)
|
|
48
|
-
return AstBuilder.#buildFind(find);
|
|
49
|
-
const read = ctx.readStatement();
|
|
50
|
-
if (read)
|
|
51
|
-
return AstBuilder.#buildRead(read);
|
|
52
|
-
const edit = ctx.editStatement();
|
|
53
|
-
if (edit)
|
|
54
|
-
return AstBuilder.#buildEdit(edit);
|
|
55
|
-
const copy = ctx.copyStatement();
|
|
56
|
-
if (copy)
|
|
57
|
-
return AstBuilder.#buildCopy(copy);
|
|
58
|
-
const move = ctx.moveStatement();
|
|
59
|
-
if (move)
|
|
60
|
-
return AstBuilder.#buildMove(move);
|
|
61
|
-
const exec = ctx.execStatement();
|
|
62
|
-
if (exec)
|
|
63
|
-
return AstBuilder.#buildExec(exec);
|
|
64
|
-
const bare = ctx.bareStatement();
|
|
65
|
-
if (bare)
|
|
66
|
-
return AstBuilder.#buildBare(bare);
|
|
67
|
-
const work = ctx.workStatement();
|
|
68
|
-
if (work)
|
|
69
|
-
return AstBuilder.#buildWork(work);
|
|
70
|
-
const fork = ctx.forkStatement();
|
|
71
|
-
if (fork)
|
|
72
|
-
return AstBuilder.#buildFork(fork);
|
|
73
|
-
const kill = ctx.killStatement();
|
|
74
|
-
if (kill)
|
|
75
|
-
return AstBuilder.#buildKill(kill);
|
|
76
|
-
// `midStatement` has no planStatement alternative (PLAN is never a mid-op); only the
|
|
77
|
-
// full `statement` rule does.
|
|
78
|
-
if ("planStatement" in ctx) {
|
|
79
|
-
const plan = ctx.planStatement();
|
|
80
|
-
if (plan)
|
|
81
|
-
return AstBuilder.#buildPlan(plan);
|
|
82
|
-
const send = ctx.sendStatement();
|
|
83
|
-
if (send)
|
|
84
|
-
return AstBuilder.#buildSend(send);
|
|
85
|
-
}
|
|
86
|
-
throw new Error("statement context has no recognized alternative");
|
|
87
|
-
}
|
|
88
|
-
static #buildFind(ctx) {
|
|
89
|
-
const positionForBody = AstBuilder.#positionOf(ctx);
|
|
90
|
-
const bodied = AstBuilder.#annotationBody("FIND", AstBuilder.#splitDelimiter(ctx.OPEN_FIND().getText(), "FIND"), AstBuilder.#annotationOf(ctx), AstBuilder.#bodyTextOf(ctx), positionForBody);
|
|
91
|
-
return AstBuilder.#buildFindFrom(ctx, bodied.annotation, bodied.raw);
|
|
92
|
-
}
|
|
93
|
-
static #buildFindFrom(ctx, annotation, raw) {
|
|
94
|
-
const position = AstBuilder.#positionOf(ctx);
|
|
95
|
-
const slots = AstBuilder.#extractSlots(ctx.slotModifiers(), position);
|
|
96
|
-
return {
|
|
97
|
-
op: "FIND",
|
|
98
|
-
delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_FIND().getText(), "FIND"),
|
|
99
|
-
annotation,
|
|
100
|
-
...slots,
|
|
101
|
-
body: raw !== null ? AstBuilder.#parseMatcherBody(raw, position) : null,
|
|
102
|
-
position,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
// Client-tier dispatch (parseClient). A `clientStatement` is either a protocol `statement`
|
|
106
|
-
// (delegated to build, returning a PlurnkStatement — which IS a ClientStatement) or one of
|
|
107
|
-
// the two client-only ops. Kept separate from build() so the protocol return type stays the
|
|
108
|
-
// closed PlurnkStatement and client ops never leak into it.
|
|
109
|
-
static buildClient(ctx) {
|
|
110
|
-
const statement = ctx.statement();
|
|
111
|
-
if (statement)
|
|
112
|
-
return AstBuilder.build(statement);
|
|
113
|
-
const look = ctx.lookStatement();
|
|
114
|
-
if (look)
|
|
115
|
-
return AstBuilder.#buildLook(look);
|
|
116
|
-
const buff = ctx.buffStatement();
|
|
117
|
-
if (buff)
|
|
118
|
-
return AstBuilder.#buildBuff(buff);
|
|
119
|
-
throw new Error("clientStatement context has no recognized alternative");
|
|
120
|
-
}
|
|
121
|
-
// LOOK / BUFF are client-tier matcher observations. They share the tag slots
|
|
122
|
-
// and parse matcher bodies directly for their client-owned lifecycles.
|
|
123
|
-
static #buildLook(ctx) {
|
|
124
|
-
const position = AstBuilder.#positionOf(ctx);
|
|
125
|
-
const slots = AstBuilder.#extractTextSlots(ctx.slotModifiers(), position);
|
|
126
|
-
const raw = AstBuilder.#bodyTextOf(ctx);
|
|
127
|
-
return {
|
|
128
|
-
op: "LOOK",
|
|
129
|
-
delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_LOOK().getText(), "LOOK"),
|
|
130
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
131
|
-
...slots,
|
|
132
|
-
body: raw !== null ? AstBuilder.#parseMatcherBody(raw, position) : null,
|
|
133
|
-
position,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
static #buildBuff(ctx) {
|
|
137
|
-
const position = AstBuilder.#positionOf(ctx);
|
|
138
|
-
const slots = AstBuilder.#extractSlots(ctx.slotModifiers(), position);
|
|
139
|
-
const raw = AstBuilder.#bodyTextOf(ctx);
|
|
140
|
-
return {
|
|
141
|
-
op: "BUFF",
|
|
142
|
-
delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_BUFF().getText(), "BUFF"),
|
|
143
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
144
|
-
...slots,
|
|
145
|
-
body: raw !== null ? AstBuilder.#parseMatcherBody(raw, position) : null,
|
|
146
|
-
position,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
static #buildRead(ctx) {
|
|
150
|
-
const position = AstBuilder.#positionOf(ctx);
|
|
151
|
-
const slots = AstBuilder.#extractTextSlots(ctx.slotModifiers(), position);
|
|
152
|
-
const delimiter = AstBuilder.#splitDelimiter(ctx.OPEN_READ().getText(), "READ");
|
|
153
|
-
const bodied = AstBuilder.#annotationBody("READ", delimiter, AstBuilder.#annotationOf(ctx), AstBuilder.#bodyTextOf(ctx), position);
|
|
154
|
-
const annotation = bodied.annotation;
|
|
155
|
-
const raw = bodied.raw;
|
|
156
|
-
const targetPath = slots.target?.kind === "url"
|
|
157
|
-
? slots.target.pathname
|
|
158
|
-
: slots.target?.raw;
|
|
159
|
-
const hasMatcher = raw !== null && raw.trim() !== "";
|
|
160
|
-
if (hasMatcher || (targetPath !== undefined && PathSyntax.hasGlob(targetPath))) {
|
|
161
|
-
if (slots.lineMarker?.marks.some((mark) => typeof mark === "string") === true) {
|
|
162
|
-
throw new PlurnkParseError(position.line, position.column, "visitor", "line anchors require an exact READ target; FIND result positions are numeric");
|
|
163
|
-
}
|
|
164
|
-
const findSlots = slots;
|
|
165
|
-
return {
|
|
166
|
-
op: "FIND",
|
|
167
|
-
delimiter,
|
|
168
|
-
annotation,
|
|
169
|
-
...findSlots,
|
|
170
|
-
body: hasMatcher ? AstBuilder.#parseMatcherBody(raw, position) : null,
|
|
171
|
-
position,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
return {
|
|
175
|
-
op: "READ",
|
|
176
|
-
delimiter,
|
|
177
|
-
annotation,
|
|
178
|
-
...slots,
|
|
179
|
-
body: null,
|
|
180
|
-
position,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
static #buildEdit(ctx) {
|
|
184
|
-
const position = AstBuilder.#positionOf(ctx);
|
|
185
|
-
const slots = AstBuilder.#extractTextSlots(ctx.slotModifiers(), position);
|
|
186
|
-
return {
|
|
187
|
-
op: "EDIT",
|
|
188
|
-
delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_EDIT().getText(), "EDIT"),
|
|
189
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
190
|
-
...slots,
|
|
191
|
-
body: AstBuilder.#bodyTextOf(ctx),
|
|
192
|
-
position,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
static #buildCopy(ctx) {
|
|
196
|
-
const position = AstBuilder.#positionOf(ctx);
|
|
197
|
-
const modifier = ctx.transferModifiers();
|
|
198
|
-
const selections = modifier.resourceSelection();
|
|
199
|
-
if (selections.length !== 2)
|
|
200
|
-
throw new Error("COPY grammar did not produce two resource selections");
|
|
201
|
-
return {
|
|
202
|
-
op: "COPY",
|
|
203
|
-
delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_COPY().getText(), "COPY"),
|
|
204
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
205
|
-
source: AstBuilder.#resourceSelectionFromCtx(selections[0], position),
|
|
206
|
-
destination: AstBuilder.#resourceSelectionFromCtx(selections[1], position),
|
|
207
|
-
position,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
static #buildMove(ctx) {
|
|
211
|
-
const position = AstBuilder.#positionOf(ctx);
|
|
212
|
-
const modifier = ctx.transferModifiers();
|
|
213
|
-
const selections = modifier.resourceSelection();
|
|
214
|
-
if (selections.length !== 2)
|
|
215
|
-
throw new Error("MOVE grammar did not produce two resource selections");
|
|
216
|
-
return {
|
|
217
|
-
op: "MOVE",
|
|
218
|
-
delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_MOVE().getText(), "MOVE"),
|
|
219
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
220
|
-
source: AstBuilder.#resourceSelectionFromCtx(selections[0], position),
|
|
221
|
-
destination: AstBuilder.#resourceSelectionFromCtx(selections[1], position),
|
|
222
|
-
position,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
// {§send-label} — a disposition label makes the SEND terminal and names no recipient.
|
|
226
|
-
static #SEND_LABELS = Object.freeze({ NEXT: 102, WAIT: 202, TERM: 200, FAIL: 499 });
|
|
227
|
-
static #buildSend(ctx) {
|
|
228
|
-
const position = AstBuilder.#positionOf(ctx);
|
|
229
|
-
const label = ctx.SEND_LABEL().getText().slice(1, -1);
|
|
230
|
-
const status = AstBuilder.#SEND_LABELS[label];
|
|
231
|
-
if (status === undefined)
|
|
232
|
-
throw new Error(`the lexer admitted an unknown SEND label: ${label}`);
|
|
233
|
-
const raw = AstBuilder.#bodyTextOf(ctx);
|
|
234
|
-
return {
|
|
235
|
-
op: "SEND",
|
|
236
|
-
delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_SEND().getText(), "SEND"),
|
|
237
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
238
|
-
status,
|
|
239
|
-
target: null,
|
|
240
|
-
metadata: null,
|
|
241
|
-
// A WAIT keeps its scope ({§send-wait-scope}); the dispatcher owns what it accepts.
|
|
242
|
-
lineMarker: AstBuilder.#lineMarkerFromCtx(ctx.lineMarker()),
|
|
243
|
-
body: raw !== null ? AstBuilder.#parseSendBody(raw) : null,
|
|
244
|
-
position,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
// A mid-turn SEND is a message to its recipient path, or to the user when it names none.
|
|
248
|
-
static #buildMidSend(ctx) {
|
|
249
|
-
const position = AstBuilder.#positionOf(ctx);
|
|
250
|
-
const slots = AstBuilder.#extractBranchSlots(ctx.targetWithMetadata(), position);
|
|
251
|
-
const raw = AstBuilder.#bodyTextOf(ctx);
|
|
252
|
-
return {
|
|
253
|
-
op: "SEND",
|
|
254
|
-
delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_SEND().getText(), "SEND"),
|
|
255
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
256
|
-
status: null,
|
|
257
|
-
target: slots.target,
|
|
258
|
-
metadata: slots.metadata,
|
|
259
|
-
lineMarker: null,
|
|
260
|
-
body: raw !== null ? AstBuilder.#parseSendBody(raw) : null,
|
|
261
|
-
position,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
static #buildExec(ctx) {
|
|
265
|
-
const position = AstBuilder.#positionOf(ctx);
|
|
266
|
-
const slots = AstBuilder.#extractExecSlots(ctx.execModifiers(), position);
|
|
267
|
-
return {
|
|
268
|
-
op: "EXEC",
|
|
269
|
-
delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_EXEC().getText(), "EXEC"),
|
|
270
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
271
|
-
executor: AstBuilder.#executorOf(ctx, position),
|
|
272
|
-
...slots,
|
|
273
|
-
body: AstBuilder.#bodyTextOf(ctx),
|
|
274
|
-
position,
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
// {§exec-executor-slot} — the executor may lead the heading or trail the path (once, unambiguous:
|
|
278
|
-
// no other slot after a path uses `[...]`); two executors are the one rejected shape.
|
|
279
|
-
static #executorOf(ctx, pos) {
|
|
280
|
-
const found = ctx.EXECUTOR();
|
|
281
|
-
const list = Array.isArray(found) ? found : found === null || found === undefined ? [] : [found];
|
|
282
|
-
if (list.length > 1) {
|
|
283
|
-
throw new PlurnkParseError(pos.line, pos.column, "visitor", "`### EXEC0` accepts one `[executor]`");
|
|
284
|
-
}
|
|
285
|
-
return list[0]?.getText().slice(1, -1) ?? null;
|
|
286
|
-
}
|
|
287
|
-
static #buildBare(ctx) {
|
|
288
|
-
const position = AstBuilder.#positionOf(ctx);
|
|
289
|
-
return {
|
|
290
|
-
op: "BARE",
|
|
291
|
-
delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_BARE().getText(), "BARE"),
|
|
292
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
293
|
-
target: null,
|
|
294
|
-
metadata: null,
|
|
295
|
-
lineMarker: null,
|
|
296
|
-
body: AstBuilder.#requiredBodyTextOf(ctx),
|
|
297
|
-
position,
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
static #buildPlan(ctx) {
|
|
301
|
-
const position = AstBuilder.#positionOf(ctx);
|
|
302
|
-
const slots = AstBuilder.#extractSlots(ctx.slotModifiers(), position);
|
|
303
|
-
const rejected = [
|
|
304
|
-
slots.target !== null ? "(path)" : null,
|
|
305
|
-
slots.metadata !== null ? "{metadata}" : null,
|
|
306
|
-
slots.lineMarker !== null ? "<scope>" : null,
|
|
307
|
-
].filter((slot) => slot !== null);
|
|
308
|
-
if (rejected.length > 0) {
|
|
309
|
-
throw new PlurnkParseError(position.line, position.column, "visitor", `PLAN does not accept ${AstBuilder.#joinTerms(rejected)}.`);
|
|
310
|
-
}
|
|
311
|
-
return {
|
|
312
|
-
op: "PLAN",
|
|
313
|
-
delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_PLAN().getText(), "PLAN"),
|
|
314
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
315
|
-
target: null,
|
|
316
|
-
metadata: null,
|
|
317
|
-
lineMarker: null,
|
|
318
|
-
body: PlanValue.admit(AstBuilder.#requiredBodyTextOf(ctx)),
|
|
319
|
-
position,
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
static #joinTerms(terms) {
|
|
323
|
-
if (terms.length < 2)
|
|
324
|
-
return terms[0] ?? "";
|
|
325
|
-
if (terms.length === 2)
|
|
326
|
-
return `${terms[0]} and ${terms[1]}`;
|
|
327
|
-
return `${terms.slice(0, -1).join(", ")}, and ${terms.at(-1)}`;
|
|
328
|
-
}
|
|
329
|
-
static #buildKill(ctx) {
|
|
330
|
-
const position = AstBuilder.#positionOf(ctx);
|
|
331
|
-
// {§kill-scope} — the scope names lines of a log body or of an entry; null kills the whole target.
|
|
332
|
-
const slots = AstBuilder.#extractTextSlots(ctx.slotModifiers(), position);
|
|
333
|
-
const raw = AstBuilder.#bodyTextOf(ctx);
|
|
334
|
-
return {
|
|
335
|
-
op: "KILL",
|
|
336
|
-
delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_KILL().getText(), "KILL"),
|
|
337
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
338
|
-
...slots,
|
|
339
|
-
body: raw !== null ? AstBuilder.#parseMatcherBody(raw, position) : null,
|
|
340
|
-
position,
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
static #buildWork(ctx) {
|
|
344
|
-
const position = AstBuilder.#positionOf(ctx);
|
|
345
|
-
const slots = AstBuilder.#extractBranchSlots(ctx.targetWithMetadata(), position);
|
|
346
|
-
return {
|
|
347
|
-
op: "WORK",
|
|
348
|
-
delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_WORK().getText(), "WORK"),
|
|
349
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
350
|
-
...slots,
|
|
351
|
-
lineMarker: null,
|
|
352
|
-
body: AstBuilder.#requiredBodyTextOf(ctx),
|
|
353
|
-
position,
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
static #buildFork(ctx) {
|
|
357
|
-
const position = AstBuilder.#positionOf(ctx);
|
|
358
|
-
const slots = AstBuilder.#extractBranchSlots(ctx.targetWithMetadata(), position);
|
|
359
|
-
return {
|
|
360
|
-
op: "FORK",
|
|
361
|
-
delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_FORK().getText(), "FORK"),
|
|
362
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
363
|
-
...slots,
|
|
364
|
-
lineMarker: null,
|
|
365
|
-
body: AstBuilder.#requiredBodyTextOf(ctx),
|
|
366
|
-
position,
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
static #extractBranchSlots(ctx, pos) {
|
|
370
|
-
return {
|
|
371
|
-
target: AstBuilder.#targetFromCtx(AstBuilder.#findFirst(ctx, TargetContext), pos),
|
|
372
|
-
metadata: AstBuilder.#metadataFromCtx(ctx),
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
static #extractSlots(modCtx, pos) {
|
|
376
|
-
return {
|
|
377
|
-
target: AstBuilder.#targetFromCtx(AstBuilder.#findFirst(modCtx, TargetContext), pos),
|
|
378
|
-
metadata: AstBuilder.#metadataFromCtx(modCtx),
|
|
379
|
-
lineMarker: AstBuilder.#lineMarkerFromCtx(AstBuilder.#findFirst(modCtx, LineMarkerContext)),
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
static #extractTextSlots(modCtx, pos) {
|
|
383
|
-
return {
|
|
384
|
-
target: AstBuilder.#targetFromCtx(AstBuilder.#findFirst(modCtx, TargetContext), pos),
|
|
385
|
-
metadata: AstBuilder.#metadataFromCtx(modCtx),
|
|
386
|
-
lineMarker: AstBuilder.#textLineMarkerFromCtx(AstBuilder.#findFirst(modCtx, LineMarkerContext)),
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
// Depth-first search for the first terminal of `tokenType`; returns its text or null.
|
|
390
|
-
// Lets the SEND/KILL signal read work regardless of which signal rule wrapped it.
|
|
391
|
-
static #findToken(root, tokenType) {
|
|
392
|
-
if (root === null)
|
|
393
|
-
return null;
|
|
394
|
-
for (const child of root.children ?? []) {
|
|
395
|
-
if (child instanceof TerminalNode && child.symbol.type === tokenType)
|
|
396
|
-
return child.getText();
|
|
397
|
-
if (child instanceof ParserRuleContext) {
|
|
398
|
-
const found = AstBuilder.#findToken(child, tokenType);
|
|
399
|
-
if (found !== null)
|
|
400
|
-
return found;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
return null;
|
|
404
|
-
}
|
|
405
|
-
static #extractExecSlots(modCtx, pos) {
|
|
406
|
-
// {§exec-executor-slot} — every slot at most once; the grammar admits any order.
|
|
407
|
-
const once = (type, slot) => {
|
|
408
|
-
const found = AstBuilder.#findAll(modCtx, type);
|
|
409
|
-
if (found.length > 1) {
|
|
410
|
-
throw new PlurnkParseError(pos.line, pos.column, "visitor", `\`### EXEC0\` accepts ${slot} at most once`);
|
|
411
|
-
}
|
|
412
|
-
return found[0] ?? null;
|
|
413
|
-
};
|
|
414
|
-
return {
|
|
415
|
-
target: AstBuilder.#targetFromCtx(once(TargetContext, "one `(program)` path"), pos),
|
|
416
|
-
metadata: AstBuilder.#metadataFromCtx(modCtx),
|
|
417
|
-
lineMarker: AstBuilder.#lineMarkerFromCtx(once(LineMarkerContext, "one `<scope>`")),
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
static #findAll(root, type) {
|
|
421
|
-
if (root === null)
|
|
422
|
-
return [];
|
|
423
|
-
if (root instanceof type)
|
|
424
|
-
return [root];
|
|
425
|
-
return (root.children ?? []).flatMap((child) => child instanceof ParserRuleContext ? AstBuilder.#findAll(child, type) : []);
|
|
426
|
-
}
|
|
427
|
-
static #findFirst(root, type) {
|
|
428
|
-
if (root === null)
|
|
429
|
-
return null;
|
|
430
|
-
if (root instanceof type)
|
|
431
|
-
return root;
|
|
432
|
-
const children = root.children;
|
|
433
|
-
if (!children)
|
|
434
|
-
return null;
|
|
435
|
-
for (const child of children) {
|
|
436
|
-
if (child instanceof ParserRuleContext) {
|
|
437
|
-
const found = AstBuilder.#findFirst(child, type);
|
|
438
|
-
if (found !== null)
|
|
439
|
-
return found;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return null;
|
|
443
|
-
}
|
|
444
|
-
static #targetFromCtx(ctx, pos) {
|
|
445
|
-
if (ctx === null)
|
|
446
|
-
return null;
|
|
447
|
-
const text = ctx.TARGET_TEXT().map((token) => token.getText()).join("");
|
|
448
|
-
return AstBuilder.parsePath(text, pos);
|
|
449
|
-
}
|
|
450
|
-
static #metadataFromCtx(ctx) {
|
|
451
|
-
const blocks = AstBuilder.#findAll(ctx, MetadataContext);
|
|
452
|
-
return blocks.length === 0
|
|
453
|
-
? null
|
|
454
|
-
: blocks.map((block) => block.METADATA_TEXT().map((token) => token.getText()).join(""));
|
|
455
|
-
}
|
|
456
|
-
static #resourceSelectionFromCtx(ctx, pos) {
|
|
457
|
-
const target = AstBuilder.#targetFromCtx(AstBuilder.#findFirst(ctx, TargetContext), pos);
|
|
458
|
-
if (target === null)
|
|
459
|
-
throw new Error("resource selection grammar did not produce a target");
|
|
460
|
-
return {
|
|
461
|
-
target,
|
|
462
|
-
metadata: AstBuilder.#metadataFromCtx(ctx),
|
|
463
|
-
lineMarker: AstBuilder.#textLineMarkerFromCtx(AstBuilder.#findFirst(ctx, LineMarkerContext)),
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
static #lineMarkerFromCtx(ctx) {
|
|
467
|
-
if (ctx === null)
|
|
468
|
-
return null;
|
|
469
|
-
const text = ctx.L_MARKER()?.getText() ?? "";
|
|
470
|
-
return AstBuilder.#parseLineMarker(text);
|
|
471
|
-
}
|
|
472
|
-
static #textLineMarkerFromCtx(ctx) {
|
|
473
|
-
if (ctx === null)
|
|
474
|
-
return null;
|
|
475
|
-
const text = ctx.L_MARKER()?.getText() ?? "";
|
|
476
|
-
return AstBuilder.#parseTextLineMarker(text);
|
|
477
|
-
}
|
|
478
|
-
static #parseTextLineMarker(text) {
|
|
479
|
-
if (!text.includes("@"))
|
|
480
|
-
return AstBuilder.#parseLineMarker(text);
|
|
481
|
-
const marks = text.slice(1, -1).split(/, ?/).map((component) => component.startsWith("@") ? component : Number.parseFloat(component));
|
|
482
|
-
return { marks: marks };
|
|
483
|
-
}
|
|
484
|
-
static #positionOf(ctx) {
|
|
485
|
-
const start = ctx.start;
|
|
486
|
-
return { line: start?.line ?? 0, column: start?.column ?? 0 };
|
|
487
|
-
}
|
|
488
|
-
static #annotationOf(ctx) {
|
|
489
|
-
const token = AstBuilder.#findToken(ctx, plurnkLexer.ANNOTATION);
|
|
490
|
-
return token === null ? null : token.slice("<!--".length, -"-->".length).trim();
|
|
491
|
-
}
|
|
492
|
-
static #bodyTextOf(ctx) {
|
|
493
|
-
return AstBuilder.#findFirst(ctx, BodyContext)?.getText() ?? null;
|
|
494
|
-
}
|
|
495
|
-
static #requiredBodyTextOf(ctx) {
|
|
496
|
-
return AstBuilder.#bodyTextOf(ctx) ?? "";
|
|
497
|
-
}
|
|
498
|
-
static #splitDelimiter(headingText, op) {
|
|
499
|
-
const marker = op === "PLAN" ? "## " : "### ";
|
|
500
|
-
return headingText.slice(marker.length + op.length);
|
|
501
|
-
}
|
|
502
|
-
static #isDigit(c) {
|
|
503
|
-
return c !== undefined && c >= "0" && c <= "9";
|
|
504
|
-
}
|
|
505
|
-
// Scans `<scope>` into ordered numeric components. Separators are `,`
|
|
506
|
-
// (with an optional space) or `-`; a `-` immediately starting a component is its
|
|
507
|
-
// sign, not a separator. The operation owner assigns roles. {§scope-marker-forms}
|
|
508
|
-
static #parseLineMarker(text) {
|
|
509
|
-
const inner = text.slice(1, -1);
|
|
510
|
-
const marks = [];
|
|
511
|
-
let i = 0;
|
|
512
|
-
while (i < inner.length) {
|
|
513
|
-
let j = i;
|
|
514
|
-
if (inner[j] === "-")
|
|
515
|
-
j++;
|
|
516
|
-
while (AstBuilder.#isDigit(inner[j]))
|
|
517
|
-
j++;
|
|
518
|
-
if (inner[j] === "." && AstBuilder.#isDigit(inner[j + 1])) {
|
|
519
|
-
j++;
|
|
520
|
-
while (AstBuilder.#isDigit(inner[j]))
|
|
521
|
-
j++;
|
|
522
|
-
}
|
|
523
|
-
marks.push(Number.parseFloat(inner.slice(i, j)));
|
|
524
|
-
i = j;
|
|
525
|
-
if (inner[i] === ",") {
|
|
526
|
-
i++;
|
|
527
|
-
if (inner[i] === " ")
|
|
528
|
-
i++;
|
|
529
|
-
}
|
|
530
|
-
else if (inner[i] === "-") {
|
|
531
|
-
i++;
|
|
532
|
-
}
|
|
533
|
-
else {
|
|
534
|
-
break;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
// L_MARKER always matches at least one number, so marks is non-empty.
|
|
538
|
-
return { marks: marks };
|
|
539
|
-
}
|
|
540
|
-
/**
|
|
541
|
-
* Apply target-slot decomposition without round-tripping through a statement.
|
|
542
|
-
* Returns null for empty input and throws PlurnkParseError when a scheme URL
|
|
543
|
-
* fails WHATWG admission. {§path-syntax}
|
|
544
|
-
*/
|
|
545
|
-
static parsePath(raw, pos = { line: 0, column: 0 }) {
|
|
546
|
-
if (raw.length === 0)
|
|
547
|
-
return null;
|
|
548
|
-
const target = PathSyntax.unescapeTarget(raw);
|
|
549
|
-
if (!AstBuilder.#SCHEME_PATTERN.test(target)) {
|
|
550
|
-
return { kind: "local", raw: target };
|
|
551
|
-
}
|
|
552
|
-
const protectedTarget = AstBuilder.#protectPathBraces(target);
|
|
553
|
-
let url;
|
|
554
|
-
try {
|
|
555
|
-
url = new URL(protectedTarget.value);
|
|
556
|
-
}
|
|
557
|
-
catch {
|
|
558
|
-
throw new PlurnkParseError(pos.line, pos.column, "visitor", "invalid URI in path");
|
|
559
|
-
}
|
|
560
|
-
// Uniform WHATWG decomposition — no per-scheme authority allowlist. `://`
|
|
561
|
-
// introduces an authority for every scheme; an authority-less reference
|
|
562
|
-
// writes the empty-authority form `scheme:///path` (host parses empty).
|
|
563
|
-
// Whether a given scheme should carry an authority is a runtime concern,
|
|
564
|
-
// not the grammar's; the parser just reports what the standard parsed.
|
|
565
|
-
const parsed = {
|
|
566
|
-
kind: "url",
|
|
567
|
-
raw: target,
|
|
568
|
-
scheme: url.protocol.replace(/:$/, ""),
|
|
569
|
-
username: url.username || null,
|
|
570
|
-
password: url.password || null,
|
|
571
|
-
hostname: url.hostname || null,
|
|
572
|
-
port: url.port ? Number.parseInt(url.port, 10) : null,
|
|
573
|
-
pathname: protectedTarget.restore(url.pathname),
|
|
574
|
-
query: AstBuilder.#queryFrom(url),
|
|
575
|
-
fragment: url.hash ? url.hash.slice(1) : null,
|
|
576
|
-
};
|
|
577
|
-
return parsed;
|
|
578
|
-
}
|
|
579
|
-
// WHATWG percent-encodes raw braces. Protect only authored path braces while
|
|
580
|
-
// decomposing so brace globs remain distinguishable from authored `%7B`/`%7D`
|
|
581
|
-
// literal path data. Query and fragment spelling remain untouched.
|
|
582
|
-
static #protectPathBraces(target) {
|
|
583
|
-
const authorityStart = target.indexOf("://") + 3;
|
|
584
|
-
const pathStart = target.indexOf("/", authorityStart);
|
|
585
|
-
if (pathStart < 0)
|
|
586
|
-
return { value: target, restore: (pathname) => pathname };
|
|
587
|
-
const queryStart = target.indexOf("?", pathStart);
|
|
588
|
-
const fragmentStart = target.indexOf("#", pathStart);
|
|
589
|
-
const endings = [queryStart, fragmentStart].filter((index) => index >= 0);
|
|
590
|
-
const pathEnd = endings.length === 0 ? target.length : Math.min(...endings);
|
|
591
|
-
const rawPath = target.slice(pathStart, pathEnd);
|
|
592
|
-
if (!/[{}]/u.test(rawPath))
|
|
593
|
-
return { value: target, restore: (pathname) => pathname };
|
|
594
|
-
const sentinels = [];
|
|
595
|
-
for (let codePoint = 0xF0000; sentinels.length < 2; codePoint += 1) {
|
|
596
|
-
const character = String.fromCodePoint(codePoint);
|
|
597
|
-
const encoded = encodeURIComponent(character);
|
|
598
|
-
if (!target.includes(character) && !target.toUpperCase().includes(encoded))
|
|
599
|
-
sentinels.push(encoded);
|
|
600
|
-
}
|
|
601
|
-
const [open, close] = sentinels;
|
|
602
|
-
const protectedPath = rawPath.replaceAll("{", open).replaceAll("}", close);
|
|
603
|
-
return {
|
|
604
|
-
value: target.slice(0, pathStart) + protectedPath + target.slice(pathEnd),
|
|
605
|
-
restore: (pathname) => pathname.replaceAll(open, "{").replaceAll(close, "}"),
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
static #queryFrom(url) {
|
|
609
|
-
const queryStart = url.href.indexOf("?");
|
|
610
|
-
if (queryStart === -1)
|
|
611
|
-
return null;
|
|
612
|
-
const fragmentStart = url.href.indexOf("#", queryStart);
|
|
613
|
-
return url.href.slice(queryStart + 1, fragmentStart === -1 ? undefined : fragmentStart);
|
|
614
|
-
}
|
|
615
|
-
// The leading prefix claims its dialect; failed claimed syntax never falls back
|
|
616
|
-
// to glob. XPath's `//` is classified before regex `/`. {§matcher-prefix-claims}
|
|
617
|
-
static #parseMatcherBody(body, pos) {
|
|
618
|
-
// At statement EOF ANTLR retains one ordinary terminating line ending in
|
|
619
|
-
// BODY_TEXT; before a following heading the lexer consumes that same EOL as
|
|
620
|
-
// SECTION_END. Normalize the equivalent surfaces before enforcing one line.
|
|
621
|
-
const raw = body.replace(/(?:\r\n|\r|\n)$/u, "");
|
|
622
|
-
const lineCount = raw.split(/\r\n|\r|\n/u).length;
|
|
623
|
-
if (lineCount !== 1) {
|
|
624
|
-
throw new PlurnkParseError(pos.line, pos.column, "visitor", `Matcher body has ${lineCount} lines; expected 1.`);
|
|
625
|
-
}
|
|
626
|
-
if (raw.startsWith("//")) {
|
|
627
|
-
try {
|
|
628
|
-
xpath.parse(raw);
|
|
629
|
-
}
|
|
630
|
-
catch (e) {
|
|
631
|
-
throw new PlurnkParseError(pos.line, pos.column, "visitor", `pattern leads with \`//\` but is not a valid xpath selector - ${AstBuilder.#detail(e)}`);
|
|
632
|
-
}
|
|
633
|
-
return { dialect: "xpath", raw };
|
|
634
|
-
}
|
|
635
|
-
if (raw.startsWith("/")) {
|
|
636
|
-
const regex = AstBuilder.#tryParseSlashRegex(raw);
|
|
637
|
-
if (regex.ok)
|
|
638
|
-
return { dialect: "regex", raw, pattern: regex.pattern, flags: regex.flags };
|
|
639
|
-
if (regex.reason === "invalid" && /^[\t ]/u.test(regex.flags)) {
|
|
640
|
-
throw new PlurnkParseError(pos.line, pos.column, "visitor", "Regex matcher has trailing text after its closing `/`; operation modifiers precede the line ending, and the matcher occupies the next line: `### FIND0 (path) <scope>` above, `/pattern/` below.");
|
|
641
|
-
}
|
|
642
|
-
const slashRecovery = regex.reason === "invalid"
|
|
643
|
-
&& regex.detail.includes("Invalid flags supplied")
|
|
644
|
-
? " - use only ECMAScript flags after the closing `/`; escape a literal `/` inside the pattern as `\\/`"
|
|
645
|
-
: "";
|
|
646
|
-
// Quote the offending matcher so a multi-op emission's failure is
|
|
647
|
-
// unambiguous about WHICH body failed (a correct sibling regex must
|
|
648
|
-
// not take the blame for a broken one).
|
|
649
|
-
const excerpt = raw.length > 80 ? `${raw.slice(0, 80)}…` : raw;
|
|
650
|
-
throw new PlurnkParseError(pos.line, pos.column, "visitor", regex.reason === "unclosed"
|
|
651
|
-
? `regex matcher must use \`/pattern/flags\`; this matcher has no closing \`/\`: \`${excerpt}\``
|
|
652
|
-
: `pattern leads with \`/\` but is not a valid \`/pattern/flags\` regex - ${regex.detail}${slashRecovery}: \`${excerpt}\``);
|
|
653
|
-
}
|
|
654
|
-
if (raw.startsWith("$")) {
|
|
655
|
-
// Compile-only RFC 9535 admission through the shared json-p3 engine.
|
|
656
|
-
try {
|
|
657
|
-
AstBuilder.#JSONPATH.compile(raw);
|
|
658
|
-
}
|
|
659
|
-
catch (e) {
|
|
660
|
-
throw new PlurnkParseError(pos.line, pos.column, "visitor", `pattern leads with \`$\` but is not a valid jsonpath - ${AstBuilder.#detail(e)}`);
|
|
661
|
-
}
|
|
662
|
-
return { dialect: "jsonpath", raw };
|
|
663
|
-
}
|
|
664
|
-
if (raw.startsWith("~"))
|
|
665
|
-
return { dialect: "semantic", raw };
|
|
666
|
-
if (raw.startsWith("&")) {
|
|
667
|
-
if (!AstBuilder.#GRAPH_MATCHER.test(raw)) {
|
|
668
|
-
throw new PlurnkParseError(pos.line, pos.column, "visitor", "Malformed graph matcher; expected `&symbol`, `&<symbol`, or `&>symbol`.");
|
|
669
|
-
}
|
|
670
|
-
return { dialect: "graph", raw };
|
|
671
|
-
}
|
|
672
|
-
return { dialect: "glob", raw };
|
|
673
|
-
}
|
|
674
|
-
static #detail(e) {
|
|
675
|
-
return e instanceof Error ? e.message : String(e);
|
|
676
|
-
}
|
|
677
|
-
// Splits an ECMAScript `/pattern/flags` literal. Backslash escapes and character
|
|
678
|
-
// classes keep a slash inside the pattern; the first unescaped slash outside a
|
|
679
|
-
// class closes it. The native constructor owns pattern and flag validity.
|
|
680
|
-
static #tryParseSlashRegex(raw) {
|
|
681
|
-
let i = 1;
|
|
682
|
-
let inClass = false;
|
|
683
|
-
while (i < raw.length) {
|
|
684
|
-
if (raw[i] === "\\") {
|
|
685
|
-
i += 2;
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
if (raw[i] === "[") {
|
|
689
|
-
inClass = true;
|
|
690
|
-
i++;
|
|
691
|
-
continue;
|
|
692
|
-
}
|
|
693
|
-
if (raw[i] === "]" && inClass) {
|
|
694
|
-
inClass = false;
|
|
695
|
-
i++;
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
698
|
-
if (raw[i] === "/" && !inClass)
|
|
699
|
-
break;
|
|
700
|
-
i++;
|
|
701
|
-
}
|
|
702
|
-
if (i >= raw.length)
|
|
703
|
-
return { ok: false, reason: "unclosed" };
|
|
704
|
-
const pattern = raw.slice(1, i);
|
|
705
|
-
const flags = raw.slice(i + 1);
|
|
706
|
-
try {
|
|
707
|
-
new RegExp(pattern, flags);
|
|
708
|
-
}
|
|
709
|
-
catch (e) {
|
|
710
|
-
return { ok: false, reason: "invalid", detail: AstBuilder.#detail(e), flags };
|
|
711
|
-
}
|
|
712
|
-
return { ok: true, pattern, flags };
|
|
713
|
-
}
|
|
714
|
-
static #parseSendBody(raw) {
|
|
715
|
-
let json = null;
|
|
716
|
-
try {
|
|
717
|
-
json = JSON.parse(raw);
|
|
718
|
-
}
|
|
719
|
-
catch { /* best-effort */ }
|
|
720
|
-
return { raw, json };
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
//# sourceMappingURL=AstBuilder.js.map
|