@milehimikey/em 1.7.2 → 1.8.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/.claude/skills/event-modeling/SKILL.md +92 -65
- package/.claude/skills/event-modeling/reference/conform.md +45 -20
- package/.claude/skills/event-modeling/reference/em-dsl.md +120 -55
- package/.claude/skills/event-modeling/reference/extract.md +5 -4
- package/.claude/skills/event-modeling/reference/implement.md +34 -22
- package/.claude/skills/event-modeling/templates/model-readme.md +8 -5
- package/.claude/skills/event-modeling/templates/slice.md +19 -5
- package/README.md +6 -4
- package/dist/catalog/docJoin.d.ts +28 -3
- package/dist/catalog/docJoin.js +82 -13
- package/dist/catalog/docJoin.js.map +1 -1
- package/dist/catalog/docModelConsistencyValidate.d.ts +8 -0
- package/dist/catalog/docModelConsistencyValidate.js +335 -0
- package/dist/catalog/docModelConsistencyValidate.js.map +1 -0
- package/dist/catalog/noteBindingValidate.d.ts +8 -0
- package/dist/catalog/noteBindingValidate.js +133 -0
- package/dist/catalog/noteBindingValidate.js.map +1 -0
- package/dist/catalog/pages.js +3 -3
- package/dist/catalog/pages.js.map +1 -1
- package/dist/catalog/sliceDoc.d.ts +14 -0
- package/dist/catalog/sliceDoc.js +22 -0
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/catalog/sliceReadyValidate.d.ts +18 -0
- package/dist/catalog/sliceReadyValidate.js +25 -1
- package/dist/catalog/sliceReadyValidate.js.map +1 -1
- package/dist/cli/agentsMd.d.ts +15 -0
- package/dist/cli/agentsMd.js +72 -0
- package/dist/cli/agentsMd.js.map +1 -0
- package/dist/cli/conformScope.d.ts +86 -0
- package/dist/cli/conformScope.js +156 -0
- package/dist/cli/conformScope.js.map +1 -0
- package/dist/cli/contract.d.ts +7 -0
- package/dist/cli/contract.js +27 -0
- package/dist/cli/contract.js.map +1 -0
- package/dist/cli/coverage.d.ts +69 -0
- package/dist/cli/coverage.js +157 -0
- package/dist/cli/coverage.js.map +1 -0
- package/dist/cli/markImplemented.d.ts +34 -0
- package/dist/cli/markImplemented.js +180 -0
- package/dist/cli/markImplemented.js.map +1 -0
- package/dist/cli/migrateReactionShape.d.ts +62 -0
- package/dist/cli/migrateReactionShape.js +283 -0
- package/dist/cli/migrateReactionShape.js.map +1 -0
- package/dist/cli/sliceIndex.d.ts +49 -0
- package/dist/cli/sliceIndex.js +115 -0
- package/dist/cli/sliceIndex.js.map +1 -0
- package/dist/cli/sliceNew.d.ts +18 -0
- package/dist/cli/sliceNew.js +53 -0
- package/dist/cli/sliceNew.js.map +1 -0
- package/dist/cli/stateFile.d.ts +74 -0
- package/dist/cli/stateFile.js +191 -0
- package/dist/cli/stateFile.js.map +1 -0
- package/dist/cli.js +535 -43
- package/dist/cli.js.map +1 -1
- package/dist/emit/coverageJson.d.ts +8 -0
- package/dist/emit/coverageJson.js +30 -0
- package/dist/emit/coverageJson.js.map +1 -0
- package/dist/emit/dot.js +41 -10
- package/dist/emit/dot.js.map +1 -1
- package/dist/emit/json.d.ts +72 -4
- package/dist/emit/json.js +73 -4
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/validateJson.d.ts +52 -0
- package/dist/emit/validateJson.js +120 -0
- package/dist/emit/validateJson.js.map +1 -0
- package/dist/mcp/main.d.ts +2 -0
- package/dist/mcp/main.js +20 -0
- package/dist/mcp/main.js.map +1 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +260 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/model/model.d.ts +30 -1
- package/dist/model/model.js +26 -0
- package/dist/model/model.js.map +1 -1
- package/dist/model/rules.d.ts +114 -0
- package/dist/model/rules.js +116 -4
- package/dist/model/rules.js.map +1 -1
- package/dist/model/validate.js +83 -7
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +41 -0
- package/dist/parser/lexer.d.ts +16 -0
- package/dist/parser/lexer.js +44 -2
- package/dist/parser/lexer.js.map +1 -1
- package/dist/parser/parser.js +340 -20
- package/dist/parser/parser.js.map +1 -1
- package/dist/render/drawNotes.js +8 -1
- package/dist/render/drawNotes.js.map +1 -1
- package/dist/render/render.js +85 -17
- package/dist/render/render.js.map +1 -1
- package/dist/render/serve.d.ts +4 -0
- package/dist/render/serve.js +23 -239
- package/dist/render/serve.js.map +1 -1
- package/dist/render/sliceStatus.d.ts +1 -1
- package/dist/render/sliceStatus.js +35 -6
- package/dist/render/sliceStatus.js.map +1 -1
- package/dist/render/viewerHtml.d.ts +1 -0
- package/dist/render/viewerHtml.js +5 -0
- package/dist/render/viewerHtml.js.map +1 -0
- package/dist/render/watch.d.ts +10 -0
- package/dist/render/watch.js +29 -0
- package/dist/render/watch.js.map +1 -1
- package/dist/templates.d.ts +19 -0
- package/dist/templates.js +169 -0
- package/dist/templates.js.map +1 -1
- package/dist/util/markers.d.ts +13 -0
- package/dist/util/markers.js +30 -0
- package/dist/util/markers.js.map +1 -0
- package/dist/util/walkDir.d.ts +10 -1
- package/dist/util/walkDir.js +11 -7
- package/dist/util/walkDir.js.map +1 -1
- package/package.json +12 -3
- package/.claude/skills/event-modeling/templates/live.html +0 -117
package/dist/parser/ast.d.ts
CHANGED
|
@@ -6,6 +6,38 @@ export interface Field {
|
|
|
6
6
|
name: string;
|
|
7
7
|
/** Optional type annotation after a `:`. */
|
|
8
8
|
type?: string;
|
|
9
|
+
/** Trailing `tag` clause on the field line (event fields only): marks this field as an
|
|
10
|
+
* identity tag — the tag key defaults to the field's own name. Merged with any
|
|
11
|
+
* element-level `tag` clauses (`ElementNode.tags`) at export time via `model/model.ts`'s
|
|
12
|
+
* `collectTags`, rather than normalized into that array here — keeps the field's own
|
|
13
|
+
* `tag` flag visible right next to the field it marks. */
|
|
14
|
+
tag?: boolean;
|
|
15
|
+
/** Trailing `renamed from "Old1", "Old2"` clause on the field line (event/command fields
|
|
16
|
+
* only, MIL-68): the prior name(s) this field was known as, most-recent-first, for
|
|
17
|
+
* payload-conversion codegen at handling time. `undefined` when absent — a declared
|
|
18
|
+
* type's fields can never carry this clause (it can't parse there), so they always export
|
|
19
|
+
* it as `null` (see `emit/json.ts`'s `fieldExport`). Purely export/codegen metadata —
|
|
20
|
+
* `em diff` still reports a rename as remove+add (see `ElementNode.renamedFrom`). */
|
|
21
|
+
renamedFrom?: string[];
|
|
22
|
+
}
|
|
23
|
+
/** Kind of an element-level `tag` clause (`ElementNode.tags`) — the inline field `tag` (identity)
|
|
24
|
+
* isn't one of these; it lives on `Field.tag` instead and is merged in only at export/validate
|
|
25
|
+
* time (see `model/model.ts`'s `collectTags`). */
|
|
26
|
+
export type TagClauseKind = "composite" | "external";
|
|
27
|
+
/** An element-level `tag` clause — `tag <key> from a, b` (composite) or
|
|
28
|
+
* `tag <key> external "text"` (external). Events only (MIL-66). Declared either as a
|
|
29
|
+
* trailing clause on the element (same family as `note`/`issue`, via `extractClauses`) or as
|
|
30
|
+
* one or more standalone `tag ...` lines immediately following the event inside a slice body.
|
|
31
|
+
* Multiple accumulate, in declaration order. */
|
|
32
|
+
export interface TagClause {
|
|
33
|
+
/** The declared tag key (bare identifier, unquoted). */
|
|
34
|
+
key: string;
|
|
35
|
+
kind: TagClauseKind;
|
|
36
|
+
/** composite only: the ≥2 field names forming this tag key, in declaration order. */
|
|
37
|
+
fields?: string[];
|
|
38
|
+
/** external only: free-text documentation of how this tag would be computed — never parsed. */
|
|
39
|
+
description?: string;
|
|
40
|
+
line: number;
|
|
9
41
|
}
|
|
10
42
|
export interface ElementNode {
|
|
11
43
|
kind: ElementKind;
|
|
@@ -32,6 +64,15 @@ export interface ElementNode {
|
|
|
32
64
|
* surface (an event as a contract, a view as a public read API's response shape), as
|
|
33
65
|
* opposed to an internal-only fact or read model. */
|
|
34
66
|
public?: boolean;
|
|
67
|
+
/** Element-level `tag` clauses (composite/external) — events only. `undefined`/absent when
|
|
68
|
+
* none are declared; inline field identity tags live on `Field.tag` instead, not here. */
|
|
69
|
+
tags?: TagClause[];
|
|
70
|
+
/** `renamed from "Old1", "Old2"` on the element's own name — event or command only (MIL-68):
|
|
71
|
+
* the prior name(s) this element was known as, most-recent-first. Codegen/export metadata
|
|
72
|
+
* for payload conversion — `em diff` deliberately does NOT infer a rename from this (stays
|
|
73
|
+
* remove+add, no-inference philosophy); this is purely additive metadata a consumer opts
|
|
74
|
+
* into reading. `undefined` when absent. */
|
|
75
|
+
renamedFrom?: string[];
|
|
35
76
|
line: number;
|
|
36
77
|
}
|
|
37
78
|
export interface SliceNode {
|
package/dist/parser/lexer.d.ts
CHANGED
|
@@ -17,5 +17,21 @@ export declare function stripComment(line: string): string;
|
|
|
17
17
|
* `\"`/`\\` escapes inside. Text that merely starts with `"` but whose string
|
|
18
18
|
* never actually closes at the end of `s` is left untouched, unquoted. */
|
|
19
19
|
export declare function unquote(s: string): string;
|
|
20
|
+
/** Split a comma-separated list, honouring quotes — a comma inside a quoted span is literal, not
|
|
21
|
+
* a separator — but unlike `splitQuotedList` below, returns each segment's RAW text untrimmed
|
|
22
|
+
* and unquoted, with the same "one entry per separator, including a trailing empty one" contract
|
|
23
|
+
* as native `String.split(sep)` (for input with no quotes at all, output is character-for-
|
|
24
|
+
* character identical to `s.split(sep)`). Used for field specs: `parseInlineFields` needs one
|
|
25
|
+
* comma-separated span per field, but a field-level clause may itself contain a quoted list with
|
|
26
|
+
* embedded commas (MIL-68's `renamed from "Old1", "Old2"`), which a naive `.split(",")` would
|
|
27
|
+
* break apart mid-string.
|
|
28
|
+
*
|
|
29
|
+
* A `"` only opens a span when `isQuoteOpener` says it's grammar-anchored (start of `s`, after
|
|
30
|
+
* a `QUOTE_OPENER_KEYWORDS` keyword, or after a comma) — same MIL-122 discipline as
|
|
31
|
+
* `scanUnquoted` above. Two independently-stray inch marks in a field list
|
|
32
|
+
* (`{ size24: 24", size32: 32" }`) must NOT pair with each other and swallow the comma between
|
|
33
|
+
* them; only a genuinely anchored `"` (e.g. right after `renamed from`, or after a from-list
|
|
34
|
+
* comma) opens a span at all. */
|
|
35
|
+
export declare function splitTopLevel(s: string, sep: string): string[];
|
|
20
36
|
/** Split a comma-separated list, honouring quotes, returning trimmed unquoted items. */
|
|
21
37
|
export declare function splitQuotedList(s: string): string[];
|
package/dist/parser/lexer.js
CHANGED
|
@@ -30,8 +30,13 @@ export function matchQuote(s, openIdx) {
|
|
|
30
30
|
}
|
|
31
31
|
/** Keywords after which a `"` is grammar-legitimate as the start of a quoted
|
|
32
32
|
* string: a declaration's name (`model "Name"`, `slice "Name"`, `type "Name"`)
|
|
33
|
-
* or a clause's value (`note`/`issue`/`divergence`/`from`/`source "..."`).
|
|
34
|
-
|
|
33
|
+
* or a clause's value (`note`/`issue`/`divergence`/`from`/`source`/`external "..."`).
|
|
34
|
+
* `external` (MIL-66) is here so a `#`/`{`/`}` inside a `tag X external "..."` description is
|
|
35
|
+
* inert string content, never mistaken for a comment or block boundary (same MIL-122 discipline
|
|
36
|
+
* every other quoted clause value gets). `renamed from "..."` (MIL-68) needs no entry of its
|
|
37
|
+
* own: it incidentally ends in the same `from` this list already carries, so a `#`/`{`/`}`
|
|
38
|
+
* inside a rename item's quotes is already inert here — relied on deliberately, not by luck. */
|
|
39
|
+
const QUOTE_OPENER_KEYWORDS = /(?:^|\s)(?:model|slice|type|note|issue|divergence|from|source|external)$/i;
|
|
35
40
|
/** True if the `"` at `s[i]` is grammar-legitimate as the START of a quoted
|
|
36
41
|
* string — the very first character of `s`, immediately after one of
|
|
37
42
|
* `QUOTE_OPENER_KEYWORDS` (`keyword "value"`), or immediately after a
|
|
@@ -125,6 +130,43 @@ export function unquote(s) {
|
|
|
125
130
|
}
|
|
126
131
|
return t;
|
|
127
132
|
}
|
|
133
|
+
/** Split a comma-separated list, honouring quotes — a comma inside a quoted span is literal, not
|
|
134
|
+
* a separator — but unlike `splitQuotedList` below, returns each segment's RAW text untrimmed
|
|
135
|
+
* and unquoted, with the same "one entry per separator, including a trailing empty one" contract
|
|
136
|
+
* as native `String.split(sep)` (for input with no quotes at all, output is character-for-
|
|
137
|
+
* character identical to `s.split(sep)`). Used for field specs: `parseInlineFields` needs one
|
|
138
|
+
* comma-separated span per field, but a field-level clause may itself contain a quoted list with
|
|
139
|
+
* embedded commas (MIL-68's `renamed from "Old1", "Old2"`), which a naive `.split(",")` would
|
|
140
|
+
* break apart mid-string.
|
|
141
|
+
*
|
|
142
|
+
* A `"` only opens a span when `isQuoteOpener` says it's grammar-anchored (start of `s`, after
|
|
143
|
+
* a `QUOTE_OPENER_KEYWORDS` keyword, or after a comma) — same MIL-122 discipline as
|
|
144
|
+
* `scanUnquoted` above. Two independently-stray inch marks in a field list
|
|
145
|
+
* (`{ size24: 24", size32: 32" }`) must NOT pair with each other and swallow the comma between
|
|
146
|
+
* them; only a genuinely anchored `"` (e.g. right after `renamed from`, or after a from-list
|
|
147
|
+
* comma) opens a span at all. */
|
|
148
|
+
export function splitTopLevel(s, sep) {
|
|
149
|
+
const items = [];
|
|
150
|
+
let cur = "";
|
|
151
|
+
for (let i = 0; i < s.length; i++) {
|
|
152
|
+
const c = s[i];
|
|
153
|
+
if (c === '"' && isQuoteOpener(s, i)) {
|
|
154
|
+
const close = matchQuote(s, i);
|
|
155
|
+
const end = close >= 0 ? close : s.length - 1;
|
|
156
|
+
cur += s.slice(i, end + 1);
|
|
157
|
+
i = end;
|
|
158
|
+
}
|
|
159
|
+
else if (c === sep) {
|
|
160
|
+
items.push(cur);
|
|
161
|
+
cur = "";
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
cur += c;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
items.push(cur);
|
|
168
|
+
return items;
|
|
169
|
+
}
|
|
128
170
|
/** Split a comma-separated list, honouring quotes, returning trimmed unquoted items. */
|
|
129
171
|
export function splitQuotedList(s) {
|
|
130
172
|
const items = [];
|
package/dist/parser/lexer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lexer.js","sourceRoot":"","sources":["../../src/parser/lexer.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,iFAAiF;AACjF,+EAA+E;AAC/E,iFAAiF;AACjF,2EAA2E;AAE3E;;;;0CAI0C;AAC1C,SAAS,UAAU,CAAC,CAAS,EAAE,CAAS;IACtC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACvD,CAAC;AAED;6EAC6E;AAC7E,MAAM,UAAU,UAAU,CAAC,CAAS,EAAE,OAAe;IACnD,KAAK,IAAI,CAAC,GAAG,OAAO,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,GAAI,CAAC;QACzC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAClB,CAAC,GAAG,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YACrB,SAAS;QACX,CAAC;QACD,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG;YAAE,OAAO,CAAC,CAAC;QAC3B,CAAC,EAAE,CAAC;IACN,CAAC;IACD,OAAO,CAAC,CAAC,CAAC;AACZ,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"lexer.js","sourceRoot":"","sources":["../../src/parser/lexer.ts"],"names":[],"mappings":"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"}
|
package/dist/parser/parser.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// arrow <From Element> -> <To Element>
|
|
17
17
|
// type "Name" { field: Type, ... }
|
|
18
18
|
import { AUTOMATION_KINDS, } from "./ast.js";
|
|
19
|
-
import { decodeQuoted, hasUnterminatedQuote, indexOfUnquoted, lastIndexOfUnquoted, matchQuote, splitQuotedList, stripComment, unquote, } from "./lexer.js";
|
|
19
|
+
import { decodeQuoted, hasUnterminatedQuote, indexOfUnquoted, lastIndexOfUnquoted, matchQuote, splitQuotedList, splitTopLevel, stripComment, unquote, } from "./lexer.js";
|
|
20
20
|
export class ParseError extends Error {
|
|
21
21
|
line;
|
|
22
22
|
constructor(message, line) {
|
|
@@ -89,21 +89,54 @@ export function parse(source) {
|
|
|
89
89
|
}
|
|
90
90
|
throw new ParseError("unexpected '}'", lineNo);
|
|
91
91
|
}
|
|
92
|
-
// Inside an open field block: each line is a field declaration
|
|
92
|
+
// Inside an open field block: each line is a field declaration — except a line that is
|
|
93
|
+
// itself an element-level `tag <key> from …`/`tag <key> external "…` clause, which reads
|
|
94
|
+
// as a standalone `tag` line's canonical position (after the event's closing `}`, MIL-66)
|
|
95
|
+
// and would otherwise be silently swallowed as junk fields (e.g. a bare identifier list
|
|
96
|
+
// parsed as one-field-per-name) with no diagnostic at all.
|
|
93
97
|
if (currentElement) {
|
|
94
|
-
|
|
98
|
+
if (isTagClauseLineShape(line)) {
|
|
99
|
+
throw new ParseError(currentElement.kind === "event"
|
|
100
|
+
? "a `tag` clause belongs after the event's closing '}', not inside its field block " +
|
|
101
|
+
"— move this line below the '}'"
|
|
102
|
+
: "`tag` is only valid on event — identity/composite/external tags describe an " +
|
|
103
|
+
`event's DCB tag, not a ${currentElement.kind} field`, lineNo);
|
|
104
|
+
}
|
|
105
|
+
for (const f of parseInlineFields(line, lineNo, currentElement.kind))
|
|
95
106
|
(currentElement.fields ??= []).push(f);
|
|
96
107
|
continue;
|
|
97
108
|
}
|
|
98
|
-
// Inside an open `type` block: each line is a field declaration too
|
|
109
|
+
// Inside an open `type` block: each line is a field declaration too, with the same guard —
|
|
110
|
+
// `tag` is events-only, so a `type` block never has a legal position for it at all.
|
|
99
111
|
if (currentTypeDecl) {
|
|
100
|
-
|
|
112
|
+
if (isTagClauseLineShape(line)) {
|
|
113
|
+
throw new ParseError("`tag` is only valid on event — identity/composite/external tags describe an event's " +
|
|
114
|
+
"DCB tag, not a type field", lineNo);
|
|
115
|
+
}
|
|
116
|
+
currentTypeDecl.fields.push(...parseInlineFields(line, lineNo, "type-decl"));
|
|
101
117
|
continue;
|
|
102
118
|
}
|
|
103
119
|
const [keyword, ...rest] = splitFirstWord(line);
|
|
104
120
|
const remainder = rest.join(" ").trim();
|
|
105
|
-
// Inside a slice: only element declarations are allowed
|
|
121
|
+
// Inside a slice: only element declarations are allowed, plus a standalone `tag ...` line
|
|
122
|
+
// (MIL-66's canonical form), which attaches to the most recently declared element rather
|
|
123
|
+
// than opening one of its own. Checked before the element-keyword gate below since `tag`
|
|
124
|
+
// is not itself an ELEMENT_KEYWORD.
|
|
106
125
|
if (currentSlice) {
|
|
126
|
+
if (keyword === "tag") {
|
|
127
|
+
const target = currentSlice.elements[currentSlice.elements.length - 1];
|
|
128
|
+
if (!target) {
|
|
129
|
+
throw new ParseError("a standalone `tag` line must follow an event declared earlier in this slice", lineNo);
|
|
130
|
+
}
|
|
131
|
+
if (target.kind !== "event") {
|
|
132
|
+
throw new ParseError(`a standalone \`tag\` line must follow an event — "${target.name}" is a ${target.kind}`, lineNo);
|
|
133
|
+
}
|
|
134
|
+
const leftover = extractTagClauses(target, `tag ${remainder}`, lineNo);
|
|
135
|
+
if (leftover) {
|
|
136
|
+
throw new ParseError(`unrecognized trailing text in 'tag' clause: '${leftover}'`, lineNo);
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
107
140
|
if (!ELEMENT_KEYWORDS.has(keyword)) {
|
|
108
141
|
throw new ParseError(`'${keyword}' is not valid inside a slice (expected ui/command/view/event/automation/processor/saga/translation or '}')`, lineNo);
|
|
109
142
|
}
|
|
@@ -118,7 +151,7 @@ export function parse(source) {
|
|
|
118
151
|
const after = remainder.slice(braceAt + 1);
|
|
119
152
|
const closeAt = indexOfUnquoted(after, "}");
|
|
120
153
|
const inner = closeAt >= 0 ? after.slice(0, closeAt) : after;
|
|
121
|
-
el.fields.push(...parseInlineFields(inner));
|
|
154
|
+
el.fields.push(...parseInlineFields(inner, lineNo, el.kind));
|
|
122
155
|
if (closeAt < 0) {
|
|
123
156
|
currentElement = el; // block stays open across lines
|
|
124
157
|
}
|
|
@@ -188,7 +221,7 @@ export function parse(source) {
|
|
|
188
221
|
const after = remainder.slice(braceAt + 1);
|
|
189
222
|
const closeAt = indexOfUnquoted(after, "}");
|
|
190
223
|
const inner = closeAt >= 0 ? after.slice(0, closeAt) : after;
|
|
191
|
-
decl.fields.push(...parseInlineFields(inner));
|
|
224
|
+
decl.fields.push(...parseInlineFields(inner, lineNo, "type-decl"));
|
|
192
225
|
if (closeAt < 0) {
|
|
193
226
|
currentTypeDecl = decl; // block stays open across lines
|
|
194
227
|
}
|
|
@@ -226,6 +259,39 @@ function parseElement(kind, raw, line) {
|
|
|
226
259
|
throw new ParseError(`${kind} requires a name`, line);
|
|
227
260
|
return node;
|
|
228
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Pulls a `keyword "A", "B", ...` clause out of `rest`: `keywordPattern` (a regex source
|
|
264
|
+
* fragment, e.g. `renamed\\s+from`) must be immediately followed by an opening quote, then a
|
|
265
|
+
* quoted item, optionally followed by `,` + another quoted item, repeated for as many items as
|
|
266
|
+
* are actually there. Unlike `extractQuotedClause`, this is deliberately NOT greedy to
|
|
267
|
+
* end-of-line: it stops consuming as soon as the text after a closing quote isn't `, "` — so a
|
|
268
|
+
* trailing `@Tag`/`public`/`{ … }` on the same line is left in `rest` for its own clause to
|
|
269
|
+
* find (MIL-68's `renamed from "Old" @Payment { … }` needs the list to end cleanly before
|
|
270
|
+
* `@Payment`). `keywordLabel` is the human-readable name used in the unterminated-string error.
|
|
271
|
+
* Returns `null` if `keywordPattern` isn't immediately followed by a quote (clause absent).
|
|
272
|
+
*/
|
|
273
|
+
function extractQuotedListClause(rest, keywordPattern, keywordLabel, line) {
|
|
274
|
+
const kwMatch = rest.match(new RegExp(`(?:^|\\s)${keywordPattern}\\s+(?=")`));
|
|
275
|
+
if (!kwMatch || kwMatch.index === undefined)
|
|
276
|
+
return null;
|
|
277
|
+
let idx = kwMatch.index + kwMatch[0].length;
|
|
278
|
+
const values = [];
|
|
279
|
+
for (;;) {
|
|
280
|
+
const close = matchQuote(rest, idx);
|
|
281
|
+
if (close < 0)
|
|
282
|
+
throw new ParseError(`unterminated string literal in '${keywordLabel}' clause`, line);
|
|
283
|
+
values.push(decodeQuoted(rest.slice(idx + 1, close)));
|
|
284
|
+
idx = close + 1;
|
|
285
|
+
const commaMatch = rest.slice(idx).match(/^\s*,\s*(?=")/);
|
|
286
|
+
if (!commaMatch)
|
|
287
|
+
break;
|
|
288
|
+
idx += commaMatch[0].length;
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
values,
|
|
292
|
+
rest: (rest.slice(0, kwMatch.index) + rest.slice(idx)).trim(),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
229
295
|
/**
|
|
230
296
|
* Pulls a `keyword "value"` clause out of `rest`, case-insensitively, honouring
|
|
231
297
|
* `\"`/`\\` escapes inside the string (decoded in the returned `value`) and a
|
|
@@ -288,6 +354,28 @@ function extractClauses(node, raw, line) {
|
|
|
288
354
|
node.divergence = divergenceClause.value;
|
|
289
355
|
rest = divergenceClause.rest;
|
|
290
356
|
}
|
|
357
|
+
// Element-level `tag` clause(s) (event only, MIL-66): `tag <key> from a, b` (composite) or
|
|
358
|
+
// `tag <key> external "text"` (external). Extracted before `from` below — though the two
|
|
359
|
+
// never actually collide (this `from` is an unquoted bare-identifier list, the view/reaction
|
|
360
|
+
// `from` below requires a quote immediately after — see `extractTagClauses`) — so both belong
|
|
361
|
+
// in this same note/issue/divergence "extract before `from`" cluster. May match more than
|
|
362
|
+
// once: `tag` clauses accumulate, unlike every other clause here.
|
|
363
|
+
rest = extractTagClauses(node, rest, line);
|
|
364
|
+
// `renamed from "Old1", "Old2"` clause (event or command only, MIL-68): the prior name(s)
|
|
365
|
+
// this element was known as, for payload-conversion codegen. Extracted here, before the
|
|
366
|
+
// plain `from` clause below, because `renamed from` itself ends in the word `from` — the
|
|
367
|
+
// general clause's `from\s+(?=")` regex would otherwise match the `from "..."` inside this
|
|
368
|
+
// one's own text. Non-greedy (see `extractQuotedListClause`), so a trailing `@Context`/
|
|
369
|
+
// `public`/`{ … }` on the same header line is untouched. `em diff` deliberately does not
|
|
370
|
+
// read this — it stays remove+add, no-inference (see docs/dsl.md).
|
|
371
|
+
const renamedFromClause = extractQuotedListClause(rest, "renamed\\s+from", "renamed from", line);
|
|
372
|
+
if (renamedFromClause) {
|
|
373
|
+
if (node.kind !== "event" && node.kind !== "command")
|
|
374
|
+
throw new ParseError("`renamed from` is only valid on event or command — only elements with a wire/API " +
|
|
375
|
+
"identity can be renamed", line);
|
|
376
|
+
node.renamedFrom = renamedFromClause.values;
|
|
377
|
+
rest = renamedFromClause.rest;
|
|
378
|
+
}
|
|
291
379
|
// `from "A", "B"` clause (views and reactions). The keyword is case-sensitive
|
|
292
380
|
// and its operand must open with a quote, so a capitalized `From` — or a bare
|
|
293
381
|
// lowercase `from` — inside an element name (`event Widget Removed From
|
|
@@ -355,27 +443,259 @@ function extractClauses(node, raw, line) {
|
|
|
355
443
|
}
|
|
356
444
|
return rest;
|
|
357
445
|
}
|
|
358
|
-
/**
|
|
359
|
-
|
|
446
|
+
/** Bare-identifier grammar shared by every element-level `tag` clause: the composite/external
|
|
447
|
+
* tag key itself, and each field name in a composite tag's field list. */
|
|
448
|
+
const TAG_IDENT = "[A-Za-z_][A-Za-z0-9_]*";
|
|
449
|
+
/** Matches an element-level composite tag clause, `tag <key> from a, b[, c …]`, anywhere in a
|
|
450
|
+
* clause-text string — group 1 is the key, group 2 the raw (still comma-joined) field list.
|
|
451
|
+
* The key is constrained to `TAG_IDENT`, not `\S+`: a quoted or punctuated key (`tag "x" from
|
|
452
|
+
* a, b`, `tag pro-duct from a, b`) must never silently match with the junk folded into the
|
|
453
|
+
* exported key — see `TAG_CLAUSE_LEFTOVER_RE` below. Shared between `extractTagClauses` (which
|
|
454
|
+
* matches anywhere via the `(?:^|\s)` alternation) and `isTagClauseLineShape` (which requires
|
|
455
|
+
* the match to start at index 0) so both recognize the exact same clause shape. */
|
|
456
|
+
const TAG_COMPOSITE_RE = new RegExp(`(?:^|\\s)tag\\s+(${TAG_IDENT})\\s+from\\s+(${TAG_IDENT}(?:\\s*,\\s*${TAG_IDENT})*)`);
|
|
457
|
+
/** Matches an element-level external tag clause, `tag <key> external "…`, anywhere in a
|
|
458
|
+
* clause-text string, up to (not including) the opening quote — same key constraint and
|
|
459
|
+
* sharing rationale as `TAG_COMPOSITE_RE`. */
|
|
460
|
+
const TAG_EXTERNAL_RE = new RegExp(`(?:^|\\s)tag\\s+(${TAG_IDENT})\\s+external\\s+(?=")`);
|
|
461
|
+
/** Catches a malformed tag key — quoted, punctuated, or otherwise not a bare identifier — that
|
|
462
|
+
* `TAG_COMPOSITE_RE`/`TAG_EXTERNAL_RE` deliberately never match, so it would otherwise sit
|
|
463
|
+
* unmentioned in a clause's leftover text (silently folding into the element's free-text name,
|
|
464
|
+
* or surfacing only as a confusing downstream "unrecognized trailing text"). Checked once,
|
|
465
|
+
* after `extractTagClauses`'s extraction loop has taken everything a well-formed clause could
|
|
466
|
+
* give it: anything still shaped like `tag <junk> from …`/`tag <junk> external …` at that point
|
|
467
|
+
* can only be a malformed key. */
|
|
468
|
+
const TAG_CLAUSE_LEFTOVER_RE = /(?:^|\s)tag\s+\S+\s+(?:from\b|external\b)/;
|
|
469
|
+
/** True when `line` — already trimmed, the full text of one field-block line — OPENS with an
|
|
470
|
+
* element-level `tag <key> from …`/`tag <key> external "…` clause shape. Used to reject a
|
|
471
|
+
* standalone element-level tag line written INSIDE an open element/type field block (MIL-66's
|
|
472
|
+
* canonical standalone form only follows the event's closing `}`, at the slice level, or
|
|
473
|
+
* trails an inline block on the same line) instead of silently swallowing it as junk fields.
|
|
474
|
+
* Anchored to the START of `line` — unlike `extractTagClauses`'s `(?:^|\s)` match-anywhere,
|
|
475
|
+
* which is right for scanning trailing clause text — so a field merely NAMED `tag` (bare
|
|
476
|
+
* `tag`, `tag: UUID`) or a field like `tagline: string` never trips this: neither has the
|
|
477
|
+
* clause shape starting at position 0. */
|
|
478
|
+
function isTagClauseLineShape(line) {
|
|
479
|
+
const c = line.match(TAG_COMPOSITE_RE);
|
|
480
|
+
if (c && c.index === 0)
|
|
481
|
+
return true;
|
|
482
|
+
const e = line.match(TAG_EXTERNAL_RE);
|
|
483
|
+
return !!e && e.index === 0;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Pulls every element-level `tag <key> from a, b` (composite) or `tag <key> external "text"`
|
|
487
|
+
* (external) clause out of `raw`, in a loop — `tag` clauses accumulate, unlike every other
|
|
488
|
+
* clause `extractClauses` handles. Applies each to `node.tags` and returns whatever text is
|
|
489
|
+
* left. Events only (event or nothing — same posture as the `public` kind check above); a
|
|
490
|
+
* non-event `node` throws as soon as a `tag` clause is actually found, so an element that
|
|
491
|
+
* merely has "tag" nowhere in its trailing text pays no penalty.
|
|
492
|
+
*
|
|
493
|
+
* Shared by `extractClauses` (the trailing-clause position, on the header line, after an
|
|
494
|
+
* inline `{ … }` block, or on a multi-line block's closing `}` line) and the standalone
|
|
495
|
+
* `tag ...` line handler in `parse()` (which prepends the literal `"tag "` back on before
|
|
496
|
+
* calling this, so both entry points share one matcher/error-message implementation).
|
|
497
|
+
*
|
|
498
|
+
* The composite field list is a bounded bare-identifier grammar (`a`, `a, b`, `a, b, c`, …),
|
|
499
|
+
* not "everything to end of line" the way the quoted `from`/`note`/etc. clauses are — so a
|
|
500
|
+
* `tag` clause never has to be the line's last token; a trailing `public`/`@Context`/etc.
|
|
501
|
+
* simply isn't valid identifier-list syntax and is left for those clauses to find afterward.
|
|
502
|
+
*/
|
|
503
|
+
function extractTagClauses(node, raw, line) {
|
|
504
|
+
let rest = raw;
|
|
505
|
+
for (;;) {
|
|
506
|
+
const cMatch = rest.match(TAG_COMPOSITE_RE);
|
|
507
|
+
const eMatch = rest.match(TAG_EXTERNAL_RE);
|
|
508
|
+
let which = null;
|
|
509
|
+
if (cMatch && eMatch)
|
|
510
|
+
which = (cMatch.index ?? 0) <= (eMatch.index ?? 0) ? "composite" : "external";
|
|
511
|
+
else if (cMatch)
|
|
512
|
+
which = "composite";
|
|
513
|
+
else if (eMatch)
|
|
514
|
+
which = "external";
|
|
515
|
+
if (!which)
|
|
516
|
+
break;
|
|
517
|
+
if (node.kind !== "event") {
|
|
518
|
+
throw new ParseError("`tag` is only valid on event — identity/composite/external tags describe an event's " +
|
|
519
|
+
"DCB tag, not a command/view/ui", line);
|
|
520
|
+
}
|
|
521
|
+
if (which === "composite") {
|
|
522
|
+
const m = cMatch;
|
|
523
|
+
const key = m[1];
|
|
524
|
+
const fields = m[2].split(",").map((s) => s.trim());
|
|
525
|
+
if (fields.length < 2)
|
|
526
|
+
throw new ParseError(`tag "${key}": a composite tag needs at least 2 fields (got ${fields.length})`, line);
|
|
527
|
+
(node.tags ??= []).push({ key, kind: "composite", fields, line });
|
|
528
|
+
rest = (rest.slice(0, m.index) + rest.slice((m.index ?? 0) + m[0].length)).trim();
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
const m = eMatch;
|
|
532
|
+
const key = m[1];
|
|
533
|
+
const openIdx = (m.index ?? 0) + m[0].length;
|
|
534
|
+
const closeIdx = matchQuote(rest, openIdx);
|
|
535
|
+
if (closeIdx < 0)
|
|
536
|
+
throw new ParseError(`unterminated string literal in tag "${key}" external clause`, line);
|
|
537
|
+
const description = decodeQuoted(rest.slice(openIdx + 1, closeIdx));
|
|
538
|
+
(node.tags ??= []).push({ key, kind: "external", description, line });
|
|
539
|
+
rest = (rest.slice(0, m.index) + rest.slice(closeIdx + 1)).trim();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (TAG_CLAUSE_LEFTOVER_RE.test(rest)) {
|
|
543
|
+
throw new ParseError("a `tag` clause's key must be a bare identifier (letters, digits, underscore) — " +
|
|
544
|
+
"quoted or punctuated keys aren't supported", line);
|
|
545
|
+
}
|
|
546
|
+
return rest;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Pulls recognized trailing clauses off one field spec's raw text (a single comma-separated
|
|
550
|
+
* entry from a `{ … }` block's inner text, BEFORE the `name`/`name: Type` split — a clause
|
|
551
|
+
* trails the type, or the bare name for a typeless field). Returns the leftover text (still to
|
|
552
|
+
* be split on `:`) plus whatever clauses were found.
|
|
553
|
+
*
|
|
554
|
+
* The reusable field-level counterpart to `extractClauses` (element-level) — kept a separate
|
|
555
|
+
* step from the name/type split below so a second field clause slots in as a new case here
|
|
556
|
+
* instead of a restructure. `context` is the enclosing block's element kind (or `"type-decl"`);
|
|
557
|
+
* a clause legal only on certain kinds throws a ParseError naming the actual context, mirroring
|
|
558
|
+
* `extractClauses`'s per-clause kind checks.
|
|
559
|
+
*
|
|
560
|
+
* Anchored as a genuinely TRAILING word requiring non-blank content before it (`/^(.*\S)\s+tag$/`)
|
|
561
|
+
* so a field literally NAMED `tag` (bare `tag`, or `tag: UUID`) is a field declaration, never a
|
|
562
|
+
* clause — the text "tag" alone can't satisfy "something, then whitespace, then tag" against
|
|
563
|
+
* itself. Same discipline `extractClauses` uses for `public`/`again` against a same-named
|
|
564
|
+
* element.
|
|
565
|
+
*
|
|
566
|
+
* Runs as a fixpoint loop over both clauses so `tag` and `renamed from` may trail a field in
|
|
567
|
+
* EITHER order (`paymentId: UUID renamed from "id" tag` and `paymentId: UUID tag renamed from
|
|
568
|
+
* "id"` both work, MIL-68): each pass tries whichever clause hasn't been found yet, and stops
|
|
569
|
+
* once a pass finds nothing new. At most two passes ever do real work (one clause can only
|
|
570
|
+
* unblock the other once), so this never loops meaningfully longer than a fixed two-clause
|
|
571
|
+
* chain would.
|
|
572
|
+
*/
|
|
573
|
+
function extractFieldClauses(raw, line, context) {
|
|
574
|
+
const clauses = {};
|
|
575
|
+
let rest = raw;
|
|
576
|
+
for (;;) {
|
|
577
|
+
let progressed = false;
|
|
578
|
+
if (clauses.tag === undefined) {
|
|
579
|
+
const tagMatch = rest.match(/^(.*\S)\s+tag$/);
|
|
580
|
+
if (tagMatch) {
|
|
581
|
+
if (context !== "event") {
|
|
582
|
+
throw new ParseError("`tag` is only valid on an event field — identity/composite/external tags describe " +
|
|
583
|
+
`an event's DCB tag, not a ${context === "type-decl" ? "type" : context} field`, line);
|
|
584
|
+
}
|
|
585
|
+
clauses.tag = true;
|
|
586
|
+
rest = tagMatch[1];
|
|
587
|
+
progressed = true;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (clauses.renamedFrom === undefined) {
|
|
591
|
+
const renamedFromClause = extractQuotedListClause(rest, "renamed\\s+from", "renamed from", line);
|
|
592
|
+
if (renamedFromClause) {
|
|
593
|
+
if (context !== "event" && context !== "command") {
|
|
594
|
+
throw new ParseError("`renamed from` is only valid on an event or command field — only elements with a " +
|
|
595
|
+
`wire/API identity can be renamed, not a ${context === "type-decl" ? "type" : context} field`, line);
|
|
596
|
+
}
|
|
597
|
+
clauses.renamedFrom = renamedFromClause.values;
|
|
598
|
+
rest = renamedFromClause.rest;
|
|
599
|
+
progressed = true;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (!progressed)
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
return { rest, clauses };
|
|
606
|
+
}
|
|
607
|
+
/** Parse one field spec: `name` or `name: Type`, plus any trailing field-level clauses
|
|
608
|
+
* (`extractFieldClauses`). Returns null for blanks. */
|
|
609
|
+
function parseFieldSpec(raw, line, context) {
|
|
360
610
|
const s = raw.trim();
|
|
361
611
|
if (!s)
|
|
362
612
|
return null;
|
|
363
|
-
const
|
|
613
|
+
const { rest, clauses } = extractFieldClauses(s, line, context);
|
|
614
|
+
const spec = rest.trim();
|
|
615
|
+
if (!spec)
|
|
616
|
+
return null;
|
|
617
|
+
const colon = spec.indexOf(":");
|
|
618
|
+
let field;
|
|
364
619
|
if (colon >= 0) {
|
|
365
|
-
const name = unquote(
|
|
366
|
-
const type = unquote(
|
|
367
|
-
|
|
620
|
+
const name = unquote(spec.slice(0, colon).trim());
|
|
621
|
+
const type = unquote(spec.slice(colon + 1).trim());
|
|
622
|
+
field = name ? { name, ...(type ? { type } : {}) } : null;
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
const name = unquote(spec);
|
|
626
|
+
field = name ? { name } : null;
|
|
627
|
+
}
|
|
628
|
+
if (field && clauses.tag)
|
|
629
|
+
field.tag = true;
|
|
630
|
+
if (field && clauses.renamedFrom)
|
|
631
|
+
field.renamedFrom = clauses.renamedFrom;
|
|
632
|
+
return field;
|
|
633
|
+
}
|
|
634
|
+
/** Matches a fragment ending in an in-progress `renamed from` list: `renamed from` followed by
|
|
635
|
+
* one or more quoted items, comma-separated, the LAST one flush against the fragment's end —
|
|
636
|
+
* exactly the shape `splitTopLevel` leaves on the fragment that owns the list once it's split
|
|
637
|
+
* the top-level commas BETWEEN list items apart (see `mergeRenamedFromContinuations`). */
|
|
638
|
+
const RENAMED_FROM_TAIL = /(?:^|\s)renamed\s+from\s+"(?:[^"\\]|\\.)*"(?:\s*,\s*"(?:[^"\\]|\\.)*")*$/;
|
|
639
|
+
/** Matches a fragment that, trimmed, STARTS with a quoted string — capturing that leading
|
|
640
|
+
* quoted span so the caller can inspect what (if anything) follows it (see
|
|
641
|
+
* `mergeRenamedFromContinuations`). */
|
|
642
|
+
const LEADING_QUOTED_STRING = /^"(?:[^"\\]|\\.)*"/;
|
|
643
|
+
/**
|
|
644
|
+
* Re-merges `splitTopLevel`'s fragments across a field-level `renamed from "Old1", "Old2"`
|
|
645
|
+
* list's own internal commas (MIL-68, hazard 4). `splitTopLevel` only treats a comma as
|
|
646
|
+
* literal WHILE INSIDE a single quoted span — the commas BETWEEN two list items are
|
|
647
|
+
* themselves top-level separators, so `{ a: X renamed from "A", "B", b: Y }` splits into
|
|
648
|
+
* THREE raw fragments (`a: X renamed from "A"`, `"B"`, `b: Y`), not two fields. Worse, a
|
|
649
|
+
* continuation fragment need not be JUST the quoted string — `{ paymentId: UUID renamed from
|
|
650
|
+
* "id", "pid" tag }` splits into `paymentId: UUID renamed from "id"` and `"pid" tag`, and that
|
|
651
|
+
* second fragment must fold in WHOLE (quote and trailing ` tag` both) or the ` tag` survives as
|
|
652
|
+
* a fabricated field of its own — a phantom identity tag flowing into the export with no
|
|
653
|
+
* diagnostic.
|
|
654
|
+
*
|
|
655
|
+
* A fragment whose trimmed text STARTS with a quoted string is folded back into the PREVIOUS
|
|
656
|
+
* fragment in full (`prev + ", " + frag`) whenever both (a) that previous fragment's own
|
|
657
|
+
* trailing text is itself an in-progress `renamed from` list, and (b) the text right after the
|
|
658
|
+
* fragment's leading quote is not a `:` (allowing leading whitespace) — otherwise it's left
|
|
659
|
+
* alone. Condition (b) preserves the documented escape hatch: a quoted field name immediately
|
|
660
|
+
* followed by `: Type` (`{ a: X renamed from "A", "B": Type }`) is an ordinary field with a
|
|
661
|
+
* quoted name, not a list continuation, unchanged from pre-MIL-68 behavior. Anything else
|
|
662
|
+
* trailing the quote (a bare quoted field, or trailing clause text like ` tag`) is swept into
|
|
663
|
+
* the list fragment and left for `extractFieldClauses`'s fixpoint loop to sort out — this makes
|
|
664
|
+
* the ambiguous case always resolve to "continuation of the rename list", never "next field
|
|
665
|
+
* (or fields)"; see docs/dsl.md for the two escape hatches (multi-line field blocks, or giving
|
|
666
|
+
* the quoted-name field a type).
|
|
667
|
+
*/
|
|
668
|
+
function mergeRenamedFromContinuations(fragments) {
|
|
669
|
+
const merged = [];
|
|
670
|
+
for (const raw of fragments) {
|
|
671
|
+
const trimmed = raw.trim();
|
|
672
|
+
const prev = merged.length > 0 ? merged[merged.length - 1] : undefined;
|
|
673
|
+
const leadingQuote = trimmed.match(LEADING_QUOTED_STRING);
|
|
674
|
+
const afterQuote = leadingQuote ? trimmed.slice(leadingQuote[0].length).trimStart() : undefined;
|
|
675
|
+
if (prev !== undefined &&
|
|
676
|
+
leadingQuote !== null &&
|
|
677
|
+
!afterQuote.startsWith(":") &&
|
|
678
|
+
RENAMED_FROM_TAIL.test(prev)) {
|
|
679
|
+
merged[merged.length - 1] = `${prev}, ${trimmed}`;
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
merged.push(trimmed);
|
|
683
|
+
}
|
|
368
684
|
}
|
|
369
|
-
|
|
370
|
-
return name ? { name } : null;
|
|
685
|
+
return merged;
|
|
371
686
|
}
|
|
372
687
|
/** Parse comma-separated field specs from the inner text of a `{ … }` block (or a single
|
|
373
688
|
* line inside an already-open one). Shared by element field blocks and `type` blocks —
|
|
374
|
-
* every field-bearing `{ … }` block in the grammar splits and parses fields the same way.
|
|
375
|
-
|
|
689
|
+
* every field-bearing `{ … }` block in the grammar splits and parses fields the same way.
|
|
690
|
+
* The split is quote-aware (`splitTopLevel`, identical to native `.split(",")` when `inner`
|
|
691
|
+
* has no quotes at all) so a field clause carrying a quoted, comma-bearing list (MIL-68's
|
|
692
|
+
* `renamed from "Old1", "Old2"`) isn't broken apart mid-string; `mergeRenamedFromContinuations`
|
|
693
|
+
* then re-joins the fragments the split still leaves apart BETWEEN list items. */
|
|
694
|
+
function parseInlineFields(inner, line, context) {
|
|
376
695
|
const fields = [];
|
|
377
|
-
|
|
378
|
-
|
|
696
|
+
const fragments = mergeRenamedFromContinuations(splitTopLevel(inner, ","));
|
|
697
|
+
for (const spec of fragments) {
|
|
698
|
+
const f = parseFieldSpec(spec, line, context);
|
|
379
699
|
if (f)
|
|
380
700
|
fields.push(f);
|
|
381
701
|
}
|