@plurnk/plurnk-contracts 1.16.4 → 1.17.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 -29
- package/SPEC.md +605 -518
- package/dist/conformance/agui-v1.json +3 -3
- package/dist/schema/CapabilityProjection.json +2 -4
- package/dist/schema/ClientStatement.json +5 -52
- package/dist/schema/FunctionalityDefinitionState.json +2 -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 +9 -5
- package/dist/schema/PlurnkStatement.json +86 -134
- package/dist/schema/ProposalProjection.json +2 -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 +16 -14
- package/dist/src/ApplicationPort.d.ts.map +1 -1
- package/dist/src/AstBuilder.d.ts +3 -2
- package/dist/src/AstBuilder.d.ts.map +1 -1
- package/dist/src/AstBuilder.js +340 -187
- package/dist/src/AstBuilder.js.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/PlurnkErrorStrategy.d.ts.map +1 -1
- package/dist/src/PlurnkErrorStrategy.js +58 -139
- package/dist/src/PlurnkErrorStrategy.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/PlurnkParser.d.ts +9 -5
- package/dist/src/PlurnkParser.d.ts.map +1 -1
- package/dist/src/PlurnkParser.js +175 -180
- package/dist/src/PlurnkParser.js.map +1 -1
- package/dist/src/RecordingListener.d.ts +1 -1
- package/dist/src/RecordingListener.d.ts.map +1 -1
- package/dist/src/RecordingListener.js +24 -6
- package/dist/src/RecordingListener.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/generated/plurnkLexer.d.ts +95 -106
- package/dist/src/generated/plurnkLexer.d.ts.map +1 -1
- package/dist/src/generated/plurnkLexer.js +674 -828
- package/dist/src/generated/plurnkLexer.js.map +1 -1
- package/dist/src/generated/plurnkParser.d.ts +111 -152
- package/dist/src/generated/plurnkParser.d.ts.map +1 -1
- package/dist/src/generated/plurnkParser.js +756 -1082
- package/dist/src/generated/plurnkParser.js.map +1 -1
- package/dist/src/generated/plurnkParserVisitor.d.ts +15 -36
- package/dist/src/generated/plurnkParserVisitor.d.ts.map +1 -1
- package/dist/src/generated/plurnkParserVisitor.js +12 -30
- package/dist/src/generated/plurnkParserVisitor.js.map +1 -1
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/types.d.ts +3 -7
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.generated.d.ts +147 -80
- package/dist/src/types.generated.d.ts.map +1 -1
- package/dist/src/types.js +3 -3
- package/dist/src/types.js.map +1 -1
- package/package.json +4 -8
- package/plurnk.md +103 -119
- package/dist/plurnk.gemma.gbnf +0 -141
- package/dist/plurnk.qwen.gbnf +0 -130
package/dist/src/AstBuilder.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import { ParserRuleContext, TerminalNode } from "antlr4ng";
|
|
3
3
|
import * as xpath from "xpath";
|
|
4
4
|
import { JSONPathEnvironment } from "json-p3/dist/json-p3.esm.js";
|
|
5
|
-
import { BodyContext, LineMarkerContext, MetadataContext,
|
|
5
|
+
import { BodyContext, LineMarkerContext, MetadataContext, DispositionStatementContext, SendStatementContext, TargetContext, } from "./generated/plurnkParser.js";
|
|
6
6
|
import { plurnkLexer } from "./generated/plurnkLexer.js";
|
|
7
7
|
import PlurnkParseError from "./PlurnkParseError.js";
|
|
8
8
|
import PathSyntax from "./PathSyntax.js";
|
|
9
9
|
import PlanValue from "./PlanValue.js";
|
|
10
|
+
import TurnDisposition from "./TurnDisposition.js";
|
|
10
11
|
export default class AstBuilder {
|
|
11
|
-
// {§misplaced-
|
|
12
|
+
// {§misplaced-aside-advisory} — advisories raised while building one statement; the
|
|
12
13
|
// parser drains them right after the statement so the model sees WHAT it did on the first try.
|
|
13
14
|
static #advisories = [];
|
|
14
15
|
static takeAdvisories() {
|
|
@@ -17,32 +18,165 @@ export default class AstBuilder {
|
|
|
17
18
|
return taken;
|
|
18
19
|
}
|
|
19
20
|
// A body that is solely an HTML comment can never be a matcher. Preserve it
|
|
20
|
-
// as the operation
|
|
21
|
-
static #
|
|
21
|
+
// as the operation aside and report only that deterministic normalization.
|
|
22
|
+
static #asideBody(op, aside, raw, position) {
|
|
22
23
|
if (raw === null)
|
|
23
|
-
return {
|
|
24
|
+
return { aside, raw };
|
|
24
25
|
const comment = /^\s*<!--([\s\S]*?)-->\s*$/u.exec(raw);
|
|
25
26
|
if (comment === null)
|
|
26
|
-
return {
|
|
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
|
|
28
|
-
return {
|
|
27
|
+
return { aside, raw };
|
|
28
|
+
AstBuilder.#advisories.push(new PlurnkParseError(position.line, position.column, "parser", `The ${op} body contained only an HTML comment; it was applied as the operation aside.`, "warning"));
|
|
29
|
+
return { aside: aside ?? (comment[1] ?? "").trim(), raw: null };
|
|
30
|
+
}
|
|
31
|
+
// {§matcher-option} — `pattern` is the language's key inside `[metadata]`: lifted into the
|
|
32
|
+
// statement's `matcher`, classified exactly as a body matcher was ({§matcher-prefix-claims}).
|
|
33
|
+
// A block that carries only `pattern` leaves no metadata for the owner; beside other keys the
|
|
34
|
+
// block stays for the owner, whose reader skips the reserved key. The block's shape stays the
|
|
35
|
+
// owner's business ({§scheme-metadata-modifier}): a second block or malformed JSON lifts
|
|
36
|
+
// nothing and reaches the owner's 400 untouched; only a present `pattern` that is not a
|
|
37
|
+
// string, or a malformed matcher, is the language's own positioned diagnostic.
|
|
38
|
+
// {§naked-pattern} — one line of matcher text, its trailing aside split back out. A sigil
|
|
39
|
+
// (`/`, `//`, `$`, `~`, `&`, `^`) is a matcher wherever it stands; a sigil-less glob or literal
|
|
40
|
+
// is one only on the heading line of FIND, READ or KILL, where the text can mean nothing else.
|
|
41
|
+
// {§trailing-slots} — the slots after the matcher peel off the right end of the heading text,
|
|
42
|
+
// aside, scope and option block in any order, until what remains is the matcher.
|
|
43
|
+
static #bareMatcher(raw, op, inline, position, carried = { scope: false, metadata: false }) {
|
|
44
|
+
if (raw === null)
|
|
45
|
+
return null;
|
|
46
|
+
let text = raw.trim();
|
|
47
|
+
if (text === "" || text.includes("\n"))
|
|
48
|
+
return null;
|
|
49
|
+
let aside = null;
|
|
50
|
+
let scope = null;
|
|
51
|
+
let metadata = null;
|
|
52
|
+
// A slot the heading already carries is not peeled: a second one is trailing text.
|
|
53
|
+
let scopeFree = !carried.scope;
|
|
54
|
+
let metadataFree = !carried.metadata;
|
|
55
|
+
const scopeTail = op === "FIND" ? AstBuilder.#TAIL_POSITIONS : AstBuilder.#TAIL_TEXT_SCOPE;
|
|
56
|
+
for (;;) {
|
|
57
|
+
const trailingAside = /\s*<!--([\s\S]*?)-->\s*$/u.exec(text);
|
|
58
|
+
if (trailingAside !== null && aside === null) {
|
|
59
|
+
aside = (trailingAside[1] ?? "").trim();
|
|
60
|
+
text = text.slice(0, trailingAside.index).trim();
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const trailingScope = scopeTail.exec(text);
|
|
64
|
+
if (trailingScope !== null && scopeFree && trailingScope.index > 0) {
|
|
65
|
+
scope = trailingScope[1];
|
|
66
|
+
scopeFree = false;
|
|
67
|
+
text = text.slice(0, trailingScope.index).trim();
|
|
68
|
+
AstBuilder.#adviseTrailing(position, `\`${scope}\` after the pattern was read as the scope; the scope goes before the pattern.`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const trailingBlock = /\s*\[(\{[\s\S]*\})\]\s*$/u.exec(text);
|
|
72
|
+
if (trailingBlock !== null && metadataFree && trailingBlock.index > 0 && AstBuilder.#isJsonArrayOfObjects(trailingBlock[1])) {
|
|
73
|
+
metadata = trailingBlock[1];
|
|
74
|
+
metadataFree = false;
|
|
75
|
+
text = text.slice(0, trailingBlock.index).trim();
|
|
76
|
+
AstBuilder.#adviseTrailing(position, `\`[${metadata}]\` after the pattern was read as the option block; options go before the pattern.`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (text === "")
|
|
82
|
+
return null;
|
|
83
|
+
if (AstBuilder.#SIGIL.test(text))
|
|
84
|
+
return { text, aside, scope, metadata };
|
|
85
|
+
if (!inline || (op !== "FIND" && op !== "READ" && op !== "KILL"))
|
|
86
|
+
return null;
|
|
87
|
+
return { text, aside, scope, metadata };
|
|
88
|
+
}
|
|
89
|
+
static #adviseTrailing(position, message) {
|
|
90
|
+
if (position === undefined)
|
|
91
|
+
return;
|
|
92
|
+
AstBuilder.#advisories.push(new PlurnkParseError(position.line, position.column, "parser", message, "warning"));
|
|
93
|
+
}
|
|
94
|
+
static #isJsonArrayOfObjects(inner) {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(`[${inner}]`);
|
|
97
|
+
return Array.isArray(parsed) && parsed.every((element) => typeof element === "object" && element !== null && !Array.isArray(element));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
static #SIGIL = /^(\/|\$|~|&|\^)/u;
|
|
104
|
+
// The scope shapes the lexer admits, matched at the right end of the heading text.
|
|
105
|
+
static #TAIL_POSITIONS = /\s*(<-?[0-9]+(?:\.[0-9]+)?(?:(?:,\s?|-)-?[0-9]+(?:\.[0-9]+)?)*>)\s*$/u;
|
|
106
|
+
static #TAIL_TEXT_SCOPE = /\s*(<(?:-?[0-9]+(?:\.[0-9]+)?|@[0-9A-Za-z]{5}(?:[: ][1-9][0-9]*)?|@[0-9]{1,4})(?:(?:,\s?|-)(?:-?[0-9]+(?:\.[0-9]+)?|@[0-9A-Za-z]{5}(?:[: ][1-9][0-9]*)?|@[0-9]{1,4}))*>)\s*$/u;
|
|
107
|
+
// The body text that opened on the heading line itself, split from the lines beneath it.
|
|
108
|
+
static #splitInlineBody(ctx, position) {
|
|
109
|
+
const text = AstBuilder.#bodyTextOf(ctx);
|
|
110
|
+
if (text === null)
|
|
111
|
+
return { inline: null, below: null };
|
|
112
|
+
const body = AstBuilder.#findFirst(ctx, BodyContext);
|
|
113
|
+
if (body?.start === null || body?.start === undefined || body.start.line !== position.line)
|
|
114
|
+
return { inline: null, below: text };
|
|
115
|
+
const eol = text.search(/\r?\n/u);
|
|
116
|
+
if (eol === -1)
|
|
117
|
+
return { inline: text, below: null };
|
|
118
|
+
const below = text.slice(eol).replace(/^\r?\n/u, "");
|
|
119
|
+
return { inline: text.slice(0, eol), below: below === "" ? null : below };
|
|
120
|
+
}
|
|
121
|
+
static #liftMatcher(op, metadata, position, raw = null, inline = false, carriedScope = false) {
|
|
122
|
+
if (metadata === null || metadata.length !== 1) {
|
|
123
|
+
const bare = AstBuilder.#bareMatcher(raw, op, inline, position, { scope: carriedScope, metadata: metadata !== null });
|
|
124
|
+
return bare === null
|
|
125
|
+
? { matcher: null, metadata, aside: null, scope: null }
|
|
126
|
+
: {
|
|
127
|
+
matcher: AstBuilder.#parseMatcherBody(bare.text, position),
|
|
128
|
+
metadata: metadata ?? (bare.metadata === null ? null : [bare.metadata]),
|
|
129
|
+
aside: bare.aside,
|
|
130
|
+
scope: bare.scope,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
let parsed;
|
|
134
|
+
try {
|
|
135
|
+
parsed = JSON.parse(`[${metadata[0]}]`);
|
|
136
|
+
}
|
|
137
|
+
catch (cause) {
|
|
138
|
+
if (!(cause instanceof SyntaxError))
|
|
139
|
+
throw cause;
|
|
140
|
+
return { matcher: null, metadata, aside: null, scope: null };
|
|
141
|
+
}
|
|
142
|
+
const elements = parsed;
|
|
143
|
+
if (elements.some((element) => typeof element !== "object" || element === null || Array.isArray(element))) {
|
|
144
|
+
return { matcher: null, metadata, aside: null, scope: null };
|
|
145
|
+
}
|
|
146
|
+
const options = Object.assign({}, ...elements);
|
|
147
|
+
if (!Object.hasOwn(options, "pattern"))
|
|
148
|
+
return { matcher: null, metadata, aside: null, scope: null };
|
|
149
|
+
const pattern = options.pattern;
|
|
150
|
+
if (typeof pattern !== "string") {
|
|
151
|
+
throw new PlurnkParseError(position.line, position.column, "visitor", `${op} "pattern" must be a string matcher, e.g. [{"pattern": "/needle/i"}].`);
|
|
152
|
+
}
|
|
153
|
+
const matcher = AstBuilder.#parseMatcherBody(pattern, position);
|
|
154
|
+
const others = Object.keys(options).filter((key) => key !== "pattern");
|
|
155
|
+
return { matcher, metadata: others.length === 0 ? null : metadata, aside: null, scope: null };
|
|
156
|
+
}
|
|
157
|
+
// {§matcher-option} — a text or log operation's body is never a matcher; it is ignored with one
|
|
158
|
+
// advisory naming the option form, and the operation still runs (operator, 2026-09-12: warn, never
|
|
159
|
+
// strike, over a body the model was taught not to write).
|
|
160
|
+
static #adviseBody(op, raw, position) {
|
|
161
|
+
if (raw === null || raw.trim() === "")
|
|
162
|
+
return;
|
|
163
|
+
if (AstBuilder.#bareMatcher(raw, op, false) !== null)
|
|
164
|
+
return;
|
|
165
|
+
AstBuilder.#advisories.push(new PlurnkParseError(position.line, position.column, "parser", `${op} takes no body; the body was ignored. A pattern belongs on the opening fence line after the path.`, "warning"));
|
|
29
166
|
}
|
|
30
167
|
static #SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
31
168
|
// Compile-only RFC 9535 admission using the runtime's JSONPath engine. {§matcher-prefix-claims}
|
|
32
169
|
static #JSONPATH = new JSONPathEnvironment();
|
|
33
170
|
static #GRAPH_MATCHER = /^&[<>]?[^\s<>]\S*$/u;
|
|
34
171
|
static build(ctx) {
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return AstBuilder.#buildPlan(ctx);
|
|
172
|
+
// Disposition and SEND contexts can arrive without a statement wrapper.
|
|
173
|
+
if (ctx instanceof DispositionStatementContext)
|
|
174
|
+
return AstBuilder.#buildDisposition(ctx);
|
|
39
175
|
if (ctx instanceof SendStatementContext)
|
|
40
176
|
return AstBuilder.#buildSend(ctx);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (midSend)
|
|
45
|
-
return AstBuilder.#buildMidSend(midSend);
|
|
177
|
+
const send = ctx.sendStatement();
|
|
178
|
+
if (send)
|
|
179
|
+
return AstBuilder.#buildSend(send);
|
|
46
180
|
const find = ctx.findStatement();
|
|
47
181
|
if (find)
|
|
48
182
|
return AstBuilder.#buildFind(find);
|
|
@@ -73,32 +207,32 @@ export default class AstBuilder {
|
|
|
73
207
|
const kill = ctx.killStatement();
|
|
74
208
|
if (kill)
|
|
75
209
|
return AstBuilder.#buildKill(kill);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (plan)
|
|
81
|
-
return AstBuilder.#buildPlan(plan);
|
|
82
|
-
const send = ctx.sendStatement();
|
|
83
|
-
if (send)
|
|
84
|
-
return AstBuilder.#buildSend(send);
|
|
210
|
+
if ("dispositionStatement" in ctx) {
|
|
211
|
+
const disposition = ctx.dispositionStatement();
|
|
212
|
+
if (disposition)
|
|
213
|
+
return AstBuilder.#buildDisposition(disposition);
|
|
85
214
|
}
|
|
86
215
|
throw new Error("statement context has no recognized alternative");
|
|
87
216
|
}
|
|
88
217
|
static #buildFind(ctx) {
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
218
|
+
const position = AstBuilder.#positionOf(ctx);
|
|
219
|
+
const split = AstBuilder.#splitInlineBody(ctx, position);
|
|
220
|
+
const bodied = AstBuilder.#asideBody("FIND", AstBuilder.#asideOf(ctx), split.below, position);
|
|
221
|
+
return AstBuilder.#buildFindFrom(ctx, bodied.aside, split.inline, bodied.raw);
|
|
92
222
|
}
|
|
93
|
-
static #buildFindFrom(ctx,
|
|
223
|
+
static #buildFindFrom(ctx, aside, inline, below) {
|
|
94
224
|
const position = AstBuilder.#positionOf(ctx);
|
|
95
225
|
const slots = AstBuilder.#extractSlots(ctx.slotModifiers(), position);
|
|
226
|
+
AstBuilder.#adviseBody("FIND", below, position);
|
|
227
|
+
const lifted = AstBuilder.#liftMatcher("FIND", slots.metadata, position, inline ?? below, inline !== null, slots.lineMarker !== null);
|
|
96
228
|
return {
|
|
97
229
|
op: "FIND",
|
|
98
|
-
|
|
99
|
-
annotation,
|
|
230
|
+
aside: aside ?? lifted.aside,
|
|
100
231
|
...slots,
|
|
101
|
-
|
|
232
|
+
lineMarker: slots.lineMarker ?? (lifted.scope === null ? null : AstBuilder.#parseLineMarker(lifted.scope)),
|
|
233
|
+
metadata: lifted.metadata,
|
|
234
|
+
matcher: lifted.matcher,
|
|
235
|
+
body: null,
|
|
102
236
|
position,
|
|
103
237
|
};
|
|
104
238
|
}
|
|
@@ -113,34 +247,17 @@ export default class AstBuilder {
|
|
|
113
247
|
const look = ctx.lookStatement();
|
|
114
248
|
if (look)
|
|
115
249
|
return AstBuilder.#buildLook(look);
|
|
116
|
-
const buff = ctx.buffStatement();
|
|
117
|
-
if (buff)
|
|
118
|
-
return AstBuilder.#buildBuff(buff);
|
|
119
250
|
throw new Error("clientStatement context has no recognized alternative");
|
|
120
251
|
}
|
|
121
|
-
// LOOK
|
|
122
|
-
//
|
|
252
|
+
// LOOK is the client-tier matcher observation. It shares the tag slots and parses
|
|
253
|
+
// its matcher body directly for its client-owned lifecycle.
|
|
123
254
|
static #buildLook(ctx) {
|
|
124
255
|
const position = AstBuilder.#positionOf(ctx);
|
|
125
256
|
const slots = AstBuilder.#extractTextSlots(ctx.slotModifiers(), position);
|
|
126
257
|
const raw = AstBuilder.#bodyTextOf(ctx);
|
|
127
258
|
return {
|
|
128
259
|
op: "LOOK",
|
|
129
|
-
|
|
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),
|
|
260
|
+
aside: AstBuilder.#asideOf(ctx),
|
|
144
261
|
...slots,
|
|
145
262
|
body: raw !== null ? AstBuilder.#parseMatcherBody(raw, position) : null,
|
|
146
263
|
position,
|
|
@@ -149,33 +266,20 @@ export default class AstBuilder {
|
|
|
149
266
|
static #buildRead(ctx) {
|
|
150
267
|
const position = AstBuilder.#positionOf(ctx);
|
|
151
268
|
const slots = AstBuilder.#extractTextSlots(ctx.slotModifiers(), position);
|
|
152
|
-
const
|
|
153
|
-
const bodied = AstBuilder.#
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
}
|
|
269
|
+
const split = AstBuilder.#splitInlineBody(ctx, position);
|
|
270
|
+
const bodied = AstBuilder.#asideBody("READ", AstBuilder.#asideOf(ctx), split.below, position);
|
|
271
|
+
AstBuilder.#adviseBody("READ", bodied.raw, position);
|
|
272
|
+
const lifted = AstBuilder.#liftMatcher("READ", slots.metadata, position, split.inline ?? bodied.raw, split.inline !== null, slots.lineMarker !== null);
|
|
273
|
+
const aside = bodied.aside ?? lifted.aside;
|
|
274
|
+
// {§read-find-normalization} — a READ is never rewritten: a glob target is the runtime's
|
|
275
|
+
// fan-out over every matching path, with or without a matcher (core {§read-fan-out}).
|
|
174
276
|
return {
|
|
175
277
|
op: "READ",
|
|
176
|
-
|
|
177
|
-
annotation,
|
|
278
|
+
aside,
|
|
178
279
|
...slots,
|
|
280
|
+
lineMarker: slots.lineMarker ?? (lifted.scope === null ? null : AstBuilder.#parseTextLineMarker(lifted.scope, position)),
|
|
281
|
+
metadata: lifted.metadata,
|
|
282
|
+
matcher: lifted.matcher,
|
|
179
283
|
body: null,
|
|
180
284
|
position,
|
|
181
285
|
};
|
|
@@ -183,12 +287,18 @@ export default class AstBuilder {
|
|
|
183
287
|
static #buildEdit(ctx) {
|
|
184
288
|
const position = AstBuilder.#positionOf(ctx);
|
|
185
289
|
const slots = AstBuilder.#extractTextSlots(ctx.slotModifiers(), position);
|
|
290
|
+
// {§naked-pattern} — a sigil on the heading line is the matcher; the lines beneath are the
|
|
291
|
+
// replacement (none deletes each match). Any other heading-line text is the body it always was.
|
|
292
|
+
const split = AstBuilder.#splitInlineBody(ctx, position);
|
|
293
|
+
const lifted = AstBuilder.#liftMatcher("EDIT", slots.metadata, position, split.inline, true, slots.lineMarker !== null);
|
|
186
294
|
return {
|
|
187
295
|
op: "EDIT",
|
|
188
|
-
|
|
189
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
296
|
+
aside: AstBuilder.#asideOf(ctx) ?? lifted.aside,
|
|
190
297
|
...slots,
|
|
191
|
-
|
|
298
|
+
lineMarker: slots.lineMarker ?? (lifted.scope === null ? null : AstBuilder.#parseTextLineMarker(lifted.scope, position)),
|
|
299
|
+
metadata: lifted.metadata,
|
|
300
|
+
matcher: lifted.matcher,
|
|
301
|
+
body: lifted.matcher === null || split.inline === null ? AstBuilder.#bodyTextOf(ctx) : split.below,
|
|
192
302
|
position,
|
|
193
303
|
};
|
|
194
304
|
}
|
|
@@ -200,8 +310,7 @@ export default class AstBuilder {
|
|
|
200
310
|
throw new Error("COPY grammar did not produce two resource selections");
|
|
201
311
|
return {
|
|
202
312
|
op: "COPY",
|
|
203
|
-
|
|
204
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
313
|
+
aside: AstBuilder.#asideOf(ctx),
|
|
205
314
|
source: AstBuilder.#resourceSelectionFromCtx(selections[0], position),
|
|
206
315
|
destination: AstBuilder.#resourceSelectionFromCtx(selections[1], position),
|
|
207
316
|
position,
|
|
@@ -215,128 +324,96 @@ export default class AstBuilder {
|
|
|
215
324
|
throw new Error("MOVE grammar did not produce two resource selections");
|
|
216
325
|
return {
|
|
217
326
|
op: "MOVE",
|
|
218
|
-
|
|
219
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
327
|
+
aside: AstBuilder.#asideOf(ctx),
|
|
220
328
|
source: AstBuilder.#resourceSelectionFromCtx(selections[0], position),
|
|
221
329
|
destination: AstBuilder.#resourceSelectionFromCtx(selections[1], position),
|
|
222
330
|
position,
|
|
223
331
|
};
|
|
224
332
|
}
|
|
225
|
-
|
|
226
|
-
static #SEND_LABELS = Object.freeze({ NEXT: 102, WAIT: 202, TERM: 200, FAIL: 499 });
|
|
227
|
-
static #buildSend(ctx) {
|
|
333
|
+
static #buildDisposition(ctx) {
|
|
228
334
|
const position = AstBuilder.#positionOf(ctx);
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
335
|
+
const op = (ctx.start?.text ?? "").replace(/^`+[0-9]*/, "");
|
|
336
|
+
if (!TurnDisposition.isOp(op))
|
|
337
|
+
throw new Error(`Unknown disposition operation: ${op}`);
|
|
338
|
+
// {§one-line-turn} — an inventory written as a block on the heading line is the body when
|
|
339
|
+
// nothing sits beneath the heading, with one advisory naming where it belongs.
|
|
340
|
+
const below = AstBuilder.#bodyTextOf(ctx);
|
|
341
|
+
const inline = ctx.metadata()?.getText() ?? null;
|
|
342
|
+
if (inline !== null && (below === null || below.trim() === "")) {
|
|
343
|
+
AstBuilder.#advisories.push(new PlurnkParseError(position.line, position.column, "parser", `${op}'s inventory was read from the heading line; it belongs in the body.`, "warning"));
|
|
344
|
+
}
|
|
345
|
+
const raw = below !== null && below.trim() !== "" ? below : inline;
|
|
234
346
|
return {
|
|
235
|
-
op
|
|
236
|
-
|
|
237
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
238
|
-
status,
|
|
347
|
+
op,
|
|
348
|
+
aside: AstBuilder.#asideOf(ctx),
|
|
239
349
|
target: null,
|
|
240
350
|
metadata: null,
|
|
241
|
-
// A WAIT keeps its scope ({§send-wait-scope}); the dispatcher owns what it accepts.
|
|
242
351
|
lineMarker: AstBuilder.#lineMarkerFromCtx(ctx.lineMarker()),
|
|
243
|
-
body: raw
|
|
352
|
+
body: PlanValue.admit(raw ?? "", (message) => AstBuilder.#advisories.push(new PlurnkParseError(position.line, position.column, "visitor", message, "warning"))),
|
|
244
353
|
position,
|
|
245
354
|
};
|
|
246
355
|
}
|
|
247
356
|
// A mid-turn SEND is a message to its recipient path, or to the user when it names none.
|
|
248
|
-
static #
|
|
357
|
+
static #buildSend(ctx) {
|
|
249
358
|
const position = AstBuilder.#positionOf(ctx);
|
|
250
|
-
const slots = AstBuilder.#
|
|
359
|
+
const slots = AstBuilder.#extractSlots(ctx.resourceSelection(), position);
|
|
251
360
|
const raw = AstBuilder.#bodyTextOf(ctx);
|
|
252
361
|
return {
|
|
253
362
|
op: "SEND",
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
status: null,
|
|
257
|
-
target: slots.target,
|
|
258
|
-
metadata: slots.metadata,
|
|
259
|
-
lineMarker: null,
|
|
363
|
+
aside: AstBuilder.#asideOf(ctx),
|
|
364
|
+
...slots,
|
|
260
365
|
body: raw !== null ? AstBuilder.#parseSendBody(raw) : null,
|
|
261
366
|
position,
|
|
262
367
|
};
|
|
263
368
|
}
|
|
264
369
|
static #buildExec(ctx) {
|
|
265
370
|
const position = AstBuilder.#positionOf(ctx);
|
|
266
|
-
const slots = AstBuilder.#extractExecSlots(ctx.execModifiers(), position);
|
|
371
|
+
const slots = AstBuilder.#extractExecSlots(ctx.execModifiers(), position, AstBuilder.#executorOf(ctx) ?? "sh");
|
|
267
372
|
return {
|
|
268
373
|
op: "EXEC",
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
executor: AstBuilder.#executorOf(ctx, position),
|
|
374
|
+
aside: AstBuilder.#asideOf(ctx),
|
|
375
|
+
executor: AstBuilder.#executorOf(ctx),
|
|
272
376
|
...slots,
|
|
273
377
|
body: AstBuilder.#bodyTextOf(ctx),
|
|
274
378
|
position,
|
|
275
379
|
};
|
|
276
380
|
}
|
|
277
|
-
// {§
|
|
278
|
-
|
|
279
|
-
static #executorOf(ctx
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
return list[0]?.getText().slice(1, -1) ?? null;
|
|
381
|
+
// {§executor-case} — the AST carries the registered spelling; the tag may be written in any case.
|
|
382
|
+
static executorSpellings = new Map();
|
|
383
|
+
static #executorOf(ctx) {
|
|
384
|
+
const name = ctx.OPEN_EXEC().getText().replace(/^`+[0-9]*/, "");
|
|
385
|
+
if (name === "EXEC")
|
|
386
|
+
return null;
|
|
387
|
+
return AstBuilder.executorSpellings.get(name.toLowerCase()) ?? name;
|
|
286
388
|
}
|
|
287
389
|
static #buildBare(ctx) {
|
|
288
390
|
const position = AstBuilder.#positionOf(ctx);
|
|
391
|
+
const slots = AstBuilder.#extractBranchSlots(ctx.targetWithMetadata(), position);
|
|
289
392
|
return {
|
|
290
393
|
op: "BARE",
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
metadata: null,
|
|
394
|
+
aside: AstBuilder.#asideOf(ctx),
|
|
395
|
+
target: slots.target,
|
|
396
|
+
metadata: slots.metadata,
|
|
295
397
|
lineMarker: null,
|
|
296
398
|
body: AstBuilder.#requiredBodyTextOf(ctx),
|
|
297
399
|
position,
|
|
298
400
|
};
|
|
299
401
|
}
|
|
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
402
|
static #buildKill(ctx) {
|
|
330
403
|
const position = AstBuilder.#positionOf(ctx);
|
|
331
404
|
// {§kill-scope} — the scope names lines of a log body or of an entry; null kills the whole target.
|
|
332
405
|
const slots = AstBuilder.#extractTextSlots(ctx.slotModifiers(), position);
|
|
333
|
-
const
|
|
406
|
+
const split = AstBuilder.#splitInlineBody(ctx, position);
|
|
407
|
+
AstBuilder.#adviseBody("KILL", split.below, position);
|
|
408
|
+
const lifted = AstBuilder.#liftMatcher("KILL", slots.metadata, position, split.inline ?? split.below, split.inline !== null, slots.lineMarker !== null);
|
|
334
409
|
return {
|
|
335
410
|
op: "KILL",
|
|
336
|
-
|
|
337
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
411
|
+
aside: AstBuilder.#asideOf(ctx) ?? lifted.aside,
|
|
338
412
|
...slots,
|
|
339
|
-
|
|
413
|
+
lineMarker: slots.lineMarker ?? (lifted.scope === null ? null : AstBuilder.#parseTextLineMarker(lifted.scope, position)),
|
|
414
|
+
metadata: lifted.metadata,
|
|
415
|
+
matcher: lifted.matcher,
|
|
416
|
+
body: null,
|
|
340
417
|
position,
|
|
341
418
|
};
|
|
342
419
|
}
|
|
@@ -345,8 +422,7 @@ export default class AstBuilder {
|
|
|
345
422
|
const slots = AstBuilder.#extractBranchSlots(ctx.targetWithMetadata(), position);
|
|
346
423
|
return {
|
|
347
424
|
op: "WORK",
|
|
348
|
-
|
|
349
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
425
|
+
aside: AstBuilder.#asideOf(ctx),
|
|
350
426
|
...slots,
|
|
351
427
|
lineMarker: null,
|
|
352
428
|
body: AstBuilder.#requiredBodyTextOf(ctx),
|
|
@@ -358,8 +434,7 @@ export default class AstBuilder {
|
|
|
358
434
|
const slots = AstBuilder.#extractBranchSlots(ctx.targetWithMetadata(), position);
|
|
359
435
|
return {
|
|
360
436
|
op: "FORK",
|
|
361
|
-
|
|
362
|
-
annotation: AstBuilder.#annotationOf(ctx),
|
|
437
|
+
aside: AstBuilder.#asideOf(ctx),
|
|
363
438
|
...slots,
|
|
364
439
|
lineMarker: null,
|
|
365
440
|
body: AstBuilder.#requiredBodyTextOf(ctx),
|
|
@@ -372,22 +447,27 @@ export default class AstBuilder {
|
|
|
372
447
|
metadata: AstBuilder.#metadataFromCtx(ctx),
|
|
373
448
|
};
|
|
374
449
|
}
|
|
450
|
+
static #singleMarker(ctx, pos) {
|
|
451
|
+
const found = AstBuilder.#findAll(ctx, LineMarkerContext);
|
|
452
|
+
if (found.length > 1)
|
|
453
|
+
throw new PlurnkParseError(pos.line, pos.column, "visitor", "A resource selection takes at most one scope.");
|
|
454
|
+
return found[0] ?? null;
|
|
455
|
+
}
|
|
375
456
|
static #extractSlots(modCtx, pos) {
|
|
376
457
|
return {
|
|
377
458
|
target: AstBuilder.#targetFromCtx(AstBuilder.#findFirst(modCtx, TargetContext), pos),
|
|
378
459
|
metadata: AstBuilder.#metadataFromCtx(modCtx),
|
|
379
|
-
lineMarker: AstBuilder.#lineMarkerFromCtx(AstBuilder.#
|
|
460
|
+
lineMarker: AstBuilder.#lineMarkerFromCtx(AstBuilder.#singleMarker(modCtx, pos)),
|
|
380
461
|
};
|
|
381
462
|
}
|
|
382
463
|
static #extractTextSlots(modCtx, pos) {
|
|
383
464
|
return {
|
|
384
465
|
target: AstBuilder.#targetFromCtx(AstBuilder.#findFirst(modCtx, TargetContext), pos),
|
|
385
466
|
metadata: AstBuilder.#metadataFromCtx(modCtx),
|
|
386
|
-
lineMarker: AstBuilder.#textLineMarkerFromCtx(AstBuilder.#
|
|
467
|
+
lineMarker: AstBuilder.#textLineMarkerFromCtx(AstBuilder.#singleMarker(modCtx, pos)),
|
|
387
468
|
};
|
|
388
469
|
}
|
|
389
470
|
// 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
471
|
static #findToken(root, tokenType) {
|
|
392
472
|
if (root === null)
|
|
393
473
|
return null;
|
|
@@ -402,12 +482,12 @@ export default class AstBuilder {
|
|
|
402
482
|
}
|
|
403
483
|
return null;
|
|
404
484
|
}
|
|
405
|
-
static #extractExecSlots(modCtx, pos) {
|
|
485
|
+
static #extractExecSlots(modCtx, pos, executor) {
|
|
406
486
|
// {§exec-executor-slot} — every slot at most once; the grammar admits any order.
|
|
407
487
|
const once = (type, slot) => {
|
|
408
488
|
const found = AstBuilder.#findAll(modCtx, type);
|
|
409
489
|
if (found.length > 1) {
|
|
410
|
-
throw new PlurnkParseError(pos.line, pos.column, "visitor",
|
|
490
|
+
throw new PlurnkParseError(pos.line, pos.column, "visitor", `${executor} accepts ${slot} at most once`);
|
|
411
491
|
}
|
|
412
492
|
return found[0] ?? null;
|
|
413
493
|
};
|
|
@@ -444,6 +524,11 @@ export default class AstBuilder {
|
|
|
444
524
|
static #targetFromCtx(ctx, pos) {
|
|
445
525
|
if (ctx === null)
|
|
446
526
|
return null;
|
|
527
|
+
const nestedScope = ctx.lineMarker();
|
|
528
|
+
if (nestedScope !== null) {
|
|
529
|
+
const point = AstBuilder.#positionOf(nestedScope);
|
|
530
|
+
AstBuilder.#advisories.push(new PlurnkParseError(point.line, point.column, "parser", "The scope was inside the target slot; it was applied as the operation scope.", "warning"));
|
|
531
|
+
}
|
|
447
532
|
const text = ctx.TARGET_TEXT().map((token) => token.getText()).join("");
|
|
448
533
|
return AstBuilder.parsePath(text, pos);
|
|
449
534
|
}
|
|
@@ -457,10 +542,12 @@ export default class AstBuilder {
|
|
|
457
542
|
const target = AstBuilder.#targetFromCtx(AstBuilder.#findFirst(ctx, TargetContext), pos);
|
|
458
543
|
if (target === null)
|
|
459
544
|
throw new Error("resource selection grammar did not produce a target");
|
|
545
|
+
const lifted = AstBuilder.#liftMatcher("COPY/MOVE", AstBuilder.#metadataFromCtx(ctx), pos);
|
|
460
546
|
return {
|
|
461
547
|
target,
|
|
462
|
-
metadata:
|
|
463
|
-
lineMarker: AstBuilder.#textLineMarkerFromCtx(AstBuilder.#
|
|
548
|
+
metadata: lifted.metadata,
|
|
549
|
+
lineMarker: AstBuilder.#textLineMarkerFromCtx(AstBuilder.#singleMarker(ctx, pos)),
|
|
550
|
+
matcher: lifted.matcher,
|
|
464
551
|
};
|
|
465
552
|
}
|
|
466
553
|
static #lineMarkerFromCtx(ctx) {
|
|
@@ -473,32 +560,66 @@ export default class AstBuilder {
|
|
|
473
560
|
if (ctx === null)
|
|
474
561
|
return null;
|
|
475
562
|
const text = ctx.L_MARKER()?.getText() ?? "";
|
|
476
|
-
return AstBuilder.#parseTextLineMarker(text);
|
|
563
|
+
return AstBuilder.#parseTextLineMarker(text, AstBuilder.#positionOf(ctx));
|
|
477
564
|
}
|
|
478
|
-
static #parseTextLineMarker(text) {
|
|
565
|
+
static #parseTextLineMarker(text, position) {
|
|
479
566
|
if (!text.includes("@"))
|
|
480
567
|
return AstBuilder.#parseLineMarker(text);
|
|
481
|
-
const marks = text.slice(1, -1).split(/, ?/).map((component) =>
|
|
568
|
+
const marks = text.slice(1, -1).split(/, ?/).map((component) => {
|
|
569
|
+
// {§anchor-digits} — `@210` is the line number 210 with the anchor's sigil, not a hash.
|
|
570
|
+
if (/^@[0-9]{1,4}$/u.test(component)) {
|
|
571
|
+
if (position !== undefined) {
|
|
572
|
+
AstBuilder.#advisories.push(new PlurnkParseError(position.line, position.column, "parser", `\`${component}\` was read as line ${component.slice(1)}; an anchor is five characters (\`@abcde\`).`, "warning"));
|
|
573
|
+
}
|
|
574
|
+
return Number.parseInt(component.slice(1), 10);
|
|
575
|
+
}
|
|
576
|
+
// {§combined-anchor-tolerance} — `@abcde 42` / `@abcde:42` is the displayed prefix copied whole;
|
|
577
|
+
// the anchor is the coordinate and the number is dropped.
|
|
578
|
+
const combined = /^(@[0-9A-Za-z]{5})[: ][1-9][0-9]*$/u.exec(component);
|
|
579
|
+
if (combined !== null) {
|
|
580
|
+
if (position !== undefined) {
|
|
581
|
+
AstBuilder.#advisories.push(new PlurnkParseError(position.line, position.column, "parser", `\`${component}\` was read as the anchor \`${combined[1]}\`; a scope position takes the anchor without its displayed line number.`, "warning"));
|
|
582
|
+
}
|
|
583
|
+
return combined[1];
|
|
584
|
+
}
|
|
585
|
+
return component.startsWith("@") ? component : Number.parseFloat(component);
|
|
586
|
+
});
|
|
482
587
|
return { marks: marks };
|
|
483
588
|
}
|
|
484
589
|
static #positionOf(ctx) {
|
|
485
590
|
const start = ctx.start;
|
|
486
591
|
return { line: start?.line ?? 0, column: start?.column ?? 0 };
|
|
487
592
|
}
|
|
488
|
-
static #
|
|
489
|
-
const token = AstBuilder.#findToken(ctx, plurnkLexer.
|
|
490
|
-
|
|
593
|
+
static #asideOf(ctx) {
|
|
594
|
+
const token = AstBuilder.#findToken(ctx, plurnkLexer.ASIDE);
|
|
595
|
+
if (token === null)
|
|
596
|
+
return null;
|
|
597
|
+
const inner = token.endsWith("-->") ? token.slice("<!--".length, -"-->".length) : token.slice("<!--".length);
|
|
598
|
+
return inner.trim();
|
|
491
599
|
}
|
|
600
|
+
// {§closer-fallback} — without a real closer (the block ended at the next heading or at the end
|
|
601
|
+
// of the input) the body is cut back to its last bare fence line, which is the closer the model
|
|
602
|
+
// meant, and one terminating line ending goes with it. A synthetic SECTION_END carries no backtick.
|
|
492
603
|
static #bodyTextOf(ctx) {
|
|
493
|
-
|
|
604
|
+
const text = AstBuilder.#findFirst(ctx, BodyContext)?.getText() ?? null;
|
|
605
|
+
if (text === null)
|
|
606
|
+
return null;
|
|
607
|
+
const closer = AstBuilder.#findToken(ctx, plurnkLexer.SECTION_END);
|
|
608
|
+
if (closer !== null && closer.includes("`"))
|
|
609
|
+
return text;
|
|
610
|
+
const lines = text.split("\n");
|
|
611
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
612
|
+
if (/^[ \t]*`{3,}[0-9]*[ \t]*\r?$/u.test(lines[index] ?? "")) {
|
|
613
|
+
const kept = lines.slice(0, index).join("\n");
|
|
614
|
+
return kept === "" ? null : kept;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const trimmed = text.replace(/\r?\n$/u, "");
|
|
618
|
+
return trimmed === "" ? null : trimmed;
|
|
494
619
|
}
|
|
495
620
|
static #requiredBodyTextOf(ctx) {
|
|
496
621
|
return AstBuilder.#bodyTextOf(ctx) ?? "";
|
|
497
622
|
}
|
|
498
|
-
static #splitDelimiter(headingText, op) {
|
|
499
|
-
const marker = op === "PLAN" ? "## " : "### ";
|
|
500
|
-
return headingText.slice(marker.length + op.length);
|
|
501
|
-
}
|
|
502
623
|
static #isDigit(c) {
|
|
503
624
|
return c !== undefined && c >= "0" && c <= "9";
|
|
504
625
|
}
|
|
@@ -547,7 +668,12 @@ export default class AstBuilder {
|
|
|
547
668
|
return null;
|
|
548
669
|
const target = PathSyntax.unescapeTarget(raw);
|
|
549
670
|
if (!AstBuilder.#SCHEME_PATTERN.test(target)) {
|
|
550
|
-
|
|
671
|
+
// {§local-path-fragment} — `#channel` after a bare path is the channel, exactly as on a
|
|
672
|
+
// URL; a spelling that opens with `#` names no path, so it stays whole.
|
|
673
|
+
const hash = target.indexOf("#");
|
|
674
|
+
if (hash < 1)
|
|
675
|
+
return { kind: "local", raw: target };
|
|
676
|
+
return { kind: "local", raw: target.slice(0, hash), fragment: target.slice(hash + 1) };
|
|
551
677
|
}
|
|
552
678
|
const protectedTarget = AstBuilder.#protectPathBraces(target);
|
|
553
679
|
let url;
|
|
@@ -621,7 +747,7 @@ export default class AstBuilder {
|
|
|
621
747
|
const raw = body.replace(/(?:\r\n|\r|\n)$/u, "");
|
|
622
748
|
const lineCount = raw.split(/\r\n|\r|\n/u).length;
|
|
623
749
|
if (lineCount !== 1) {
|
|
624
|
-
throw new PlurnkParseError(pos.line, pos.column, "visitor", `Matcher
|
|
750
|
+
throw new PlurnkParseError(pos.line, pos.column, "visitor", `Matcher has ${lineCount} lines; expected 1.`);
|
|
625
751
|
}
|
|
626
752
|
if (raw.startsWith("//")) {
|
|
627
753
|
try {
|
|
@@ -632,12 +758,24 @@ export default class AstBuilder {
|
|
|
632
758
|
}
|
|
633
759
|
return { dialect: "xpath", raw };
|
|
634
760
|
}
|
|
761
|
+
// {§naked-pattern} — a matcher opening with `^` is a regex written without slashes or flags.
|
|
762
|
+
if (raw.startsWith("^")) {
|
|
763
|
+
const inline = AstBuilder.#liftInlineFlags(raw.slice(1), "", pos);
|
|
764
|
+
const pattern = `^${inline.pattern}`;
|
|
765
|
+
try {
|
|
766
|
+
new RegExp(pattern, inline.flags);
|
|
767
|
+
}
|
|
768
|
+
catch (e) {
|
|
769
|
+
throw new PlurnkParseError(pos.line, pos.column, "visitor", `pattern leads with \`^\` but is not a valid regex - ${AstBuilder.#detail(e)}`);
|
|
770
|
+
}
|
|
771
|
+
return { dialect: "regex", raw, pattern, flags: inline.flags };
|
|
772
|
+
}
|
|
635
773
|
if (raw.startsWith("/")) {
|
|
636
|
-
const regex = AstBuilder.#tryParseSlashRegex(raw);
|
|
774
|
+
const regex = AstBuilder.#tryParseSlashRegex(raw, pos);
|
|
637
775
|
if (regex.ok)
|
|
638
776
|
return { dialect: "regex", raw, pattern: regex.pattern, flags: regex.flags };
|
|
639
|
-
if (regex.reason === "
|
|
640
|
-
throw new PlurnkParseError(pos.line, pos.column, "visitor", "Regex matcher has trailing text after
|
|
777
|
+
if (regex.reason === "trailing") {
|
|
778
|
+
throw new PlurnkParseError(pos.line, pos.column, "visitor", "Regex matcher has trailing text after `/pattern/flags`.");
|
|
641
779
|
}
|
|
642
780
|
const slashRecovery = regex.reason === "invalid"
|
|
643
781
|
&& regex.detail.includes("Invalid flags supplied")
|
|
@@ -662,7 +800,7 @@ export default class AstBuilder {
|
|
|
662
800
|
return { dialect: "jsonpath", raw };
|
|
663
801
|
}
|
|
664
802
|
if (raw.startsWith("~"))
|
|
665
|
-
return { dialect: "
|
|
803
|
+
return { dialect: "fts", raw };
|
|
666
804
|
if (raw.startsWith("&")) {
|
|
667
805
|
if (!AstBuilder.#GRAPH_MATCHER.test(raw)) {
|
|
668
806
|
throw new PlurnkParseError(pos.line, pos.column, "visitor", "Malformed graph matcher; expected `&symbol`, `&<symbol`, or `&>symbol`.");
|
|
@@ -677,7 +815,18 @@ export default class AstBuilder {
|
|
|
677
815
|
// Splits an ECMAScript `/pattern/flags` literal. Backslash escapes and character
|
|
678
816
|
// classes keep a slash inside the pattern; the first unescaped slash outside a
|
|
679
817
|
// class closes it. The native constructor owns pattern and flag validity.
|
|
680
|
-
|
|
818
|
+
// {§inline-flag-tolerance} — a leading PCRE inline modifier such as `(?i)` is the pretrained
|
|
819
|
+
// spelling of a flag; ECMAScript refuses the group, so it is lifted into the flags with one
|
|
820
|
+
// advisory rather than refused.
|
|
821
|
+
static #liftInlineFlags(pattern, flags, pos) {
|
|
822
|
+
const inline = /^\(\?([ims]+)\)/u.exec(pattern);
|
|
823
|
+
if (inline === null)
|
|
824
|
+
return { pattern, flags };
|
|
825
|
+
const lifted = [...new Set([...flags, ...inline[1]])].join("");
|
|
826
|
+
AstBuilder.#advisories.push(new PlurnkParseError(pos.line, pos.column, "parser", `\`${inline[0]}\` was read as the \`${inline[1]}\` flag; an ECMAScript regex takes its flags after the closing \`/\`.`, "warning"));
|
|
827
|
+
return { pattern: pattern.slice(inline[0].length), flags: lifted };
|
|
828
|
+
}
|
|
829
|
+
static #tryParseSlashRegex(raw, pos) {
|
|
681
830
|
let i = 1;
|
|
682
831
|
let inClass = false;
|
|
683
832
|
while (i < raw.length) {
|
|
@@ -701,14 +850,18 @@ export default class AstBuilder {
|
|
|
701
850
|
}
|
|
702
851
|
if (i >= raw.length)
|
|
703
852
|
return { ok: false, reason: "unclosed" };
|
|
704
|
-
const
|
|
705
|
-
const
|
|
853
|
+
const authored = raw.slice(i + 1);
|
|
854
|
+
const trailing = /^([A-Za-z]*)[\t ]/u.exec(authored);
|
|
855
|
+
const inline = AstBuilder.#liftInlineFlags(raw.slice(1, i), trailing?.[1] ?? authored, pos);
|
|
856
|
+
const { pattern, flags } = inline;
|
|
706
857
|
try {
|
|
707
858
|
new RegExp(pattern, flags);
|
|
708
859
|
}
|
|
709
860
|
catch (e) {
|
|
710
861
|
return { ok: false, reason: "invalid", detail: AstBuilder.#detail(e), flags };
|
|
711
862
|
}
|
|
863
|
+
if (trailing !== null)
|
|
864
|
+
return { ok: false, reason: "trailing" };
|
|
712
865
|
return { ok: true, pattern, flags };
|
|
713
866
|
}
|
|
714
867
|
static #parseSendBody(raw) {
|