@miragon/wardley-dsl 0.5.2 → 0.6.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 +25 -0
- package/dist/index.cjs +2213 -718
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -8
- package/dist/index.d.ts +43 -8
- package/dist/index.js +2213 -720
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -14,64 +14,165 @@ function parseCoords(line) {
|
|
|
14
14
|
if (Number.isNaN(a) || Number.isNaN(b)) return null;
|
|
15
15
|
return { a, b };
|
|
16
16
|
}
|
|
17
|
-
function
|
|
18
|
-
return line.replace(COORDS_RE, " ");
|
|
19
|
-
}
|
|
20
|
-
function splitAtCoords(line) {
|
|
17
|
+
function splitAtTuple(line) {
|
|
21
18
|
const m = COORDS_RE.exec(line);
|
|
22
19
|
if (!m) return null;
|
|
23
20
|
const a = Number(m[1]);
|
|
24
21
|
const b = Number(m[2]);
|
|
25
|
-
if (Number.isNaN(a) || Number.isNaN(b)) return null;
|
|
26
22
|
return {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
before: line.slice(0, m.index).trim(),
|
|
24
|
+
tuple: m[0],
|
|
25
|
+
coords: Number.isNaN(a) || Number.isNaN(b) ? null : { a, b },
|
|
26
|
+
after: line.slice(m.index + m[0].length)
|
|
30
27
|
};
|
|
31
28
|
}
|
|
29
|
+
function splitAtCoords(line) {
|
|
30
|
+
const split = splitAtTuple(line);
|
|
31
|
+
if (!split?.coords) return null;
|
|
32
|
+
return { name: split.before, coords: split.coords, suffix: split.after };
|
|
33
|
+
}
|
|
32
34
|
var COORDS4_RE = /\[\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\]/;
|
|
33
|
-
function
|
|
35
|
+
function splitAtCoords4(line) {
|
|
34
36
|
const m = COORDS4_RE.exec(line);
|
|
35
37
|
if (!m) return null;
|
|
36
38
|
const vals = [m[1], m[2], m[3], m[4]].map(Number);
|
|
37
39
|
if (vals.some(Number.isNaN)) return null;
|
|
38
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
before: line.slice(0, m.index).trim(),
|
|
42
|
+
coords: { a: vals[0], b: vals[1], c: vals[2], d: vals[3] },
|
|
43
|
+
after: line.slice(m.index + m[0].length)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
var TUPLE_LIST_RE = /\[\s{0,8}(\[[^[\]]*\](?:\s{0,8},\s{0,8}\[[^[\]]*\])*)\s{0,8}\]/;
|
|
47
|
+
var LIST_ENTRY_RE = /\[[^[\]]*\]/g;
|
|
48
|
+
var WHOLE_COORDS_RE = /^\[\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\]$/;
|
|
49
|
+
function readTupleList(line, list) {
|
|
50
|
+
const tuples = [];
|
|
51
|
+
const unreadable = [];
|
|
52
|
+
for (const [entry] of list[1].matchAll(LIST_ENTRY_RE)) {
|
|
53
|
+
const coords = WHOLE_COORDS_RE.exec(entry);
|
|
54
|
+
const a = Number(coords?.[1]);
|
|
55
|
+
const b = Number(coords?.[2]);
|
|
56
|
+
if (coords && !Number.isNaN(a) && !Number.isNaN(b)) tuples.push({ a, b });
|
|
57
|
+
else unreadable.push(entry);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
tuples,
|
|
61
|
+
unreadable,
|
|
62
|
+
before: line.slice(0, list.index).trim(),
|
|
63
|
+
after: line.slice(list.index + list[0].length)
|
|
64
|
+
};
|
|
39
65
|
}
|
|
40
|
-
var MULTI_COORDS_RE = /\[\s{0,8}(\[[^[\]]*\](?:\s{0,8},\s{0,8}\[[^[\]]*\])+)\s{0,8}\]/;
|
|
41
66
|
function parseMultiCoords(line) {
|
|
42
|
-
const
|
|
67
|
+
const list = TUPLE_LIST_RE.exec(line);
|
|
68
|
+
return list ? readTupleList(line, list) : null;
|
|
69
|
+
}
|
|
70
|
+
var FIRST_CHILD_TUPLE_RE = /\[\s*([-\d.]+)\s*(?:,\s*([-\d.]+)\s*)?\]/;
|
|
71
|
+
function splitAtFirstChildTuple(line) {
|
|
72
|
+
const m = FIRST_CHILD_TUPLE_RE.exec(line);
|
|
43
73
|
if (!m) return null;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
74
|
+
return {
|
|
75
|
+
before: line.slice(0, m.index).trim(),
|
|
76
|
+
values: m[2] === void 0 ? [m[1]] : [m[1], m[2]],
|
|
77
|
+
after: line.slice(m.index + m[0].length)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function parsePositions(line) {
|
|
81
|
+
const single = COORDS_RE.exec(line);
|
|
82
|
+
const list = TUPLE_LIST_RE.exec(line);
|
|
83
|
+
if (list && (!single || list.index < single.index)) return readTupleList(line, list);
|
|
84
|
+
if (!single) return null;
|
|
85
|
+
const a = Number(single[1]);
|
|
86
|
+
const b = Number(single[2]);
|
|
87
|
+
const readable = !Number.isNaN(a) && !Number.isNaN(b);
|
|
88
|
+
return {
|
|
89
|
+
tuples: readable ? [{ a, b }] : [],
|
|
90
|
+
unreadable: readable ? [] : [single[0]],
|
|
91
|
+
before: line.slice(0, single.index).trim(),
|
|
92
|
+
after: line.slice(single.index + single[0].length)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function flowValueEnd(line, at) {
|
|
96
|
+
return line[at] === "'" && line[at - 1] === "+" ? line.indexOf("'", at + 1) : -1;
|
|
97
|
+
}
|
|
98
|
+
function rewriteOutsideFlowValues(text, rewrite) {
|
|
99
|
+
let rewritten = "";
|
|
100
|
+
let partStart = 0;
|
|
101
|
+
for (let i = 0; i < text.length; i++) {
|
|
102
|
+
const valueEnd = flowValueEnd(text, i);
|
|
103
|
+
if (valueEnd < 0) continue;
|
|
104
|
+
rewritten += rewrite(text.slice(partStart, i)) + text.slice(i, valueEnd + 1);
|
|
105
|
+
partStart = valueEnd + 1;
|
|
106
|
+
i = valueEnd;
|
|
107
|
+
}
|
|
108
|
+
return rewritten + rewrite(text.slice(partStart));
|
|
109
|
+
}
|
|
110
|
+
function indexOfLineComment(line) {
|
|
57
111
|
for (let i = 0; i < line.length - 1; i++) {
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
else if (
|
|
61
|
-
return { code: line.slice(0, i), comment: line.slice(i) };
|
|
62
|
-
}
|
|
112
|
+
const valueEnd = flowValueEnd(line, i);
|
|
113
|
+
if (valueEnd > 0) i = valueEnd;
|
|
114
|
+
else if (line[i] === "/" && line[i + 1] === "/" && line[i - 1] !== ":") return i;
|
|
63
115
|
}
|
|
64
|
-
return
|
|
116
|
+
return -1;
|
|
65
117
|
}
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
118
|
+
function readUrlDefinition(after) {
|
|
119
|
+
const open = after.indexOf("[");
|
|
120
|
+
const close = after.lastIndexOf("]");
|
|
121
|
+
const name = open > 0 ? after.slice(0, open).trim() : "";
|
|
122
|
+
const address = open > 0 && close > open ? after.slice(open + 1, close).trim() : "";
|
|
123
|
+
if (!name || !address || after.slice(close + 1).trim() !== "") return null;
|
|
124
|
+
return { name, address };
|
|
125
|
+
}
|
|
126
|
+
function isUrlDefinition(line) {
|
|
127
|
+
return keywordOf(line) === "url" && readUrlDefinition(line.trim().slice("url".length)) !== null;
|
|
128
|
+
}
|
|
129
|
+
var CommentStripper = class {
|
|
130
|
+
inBlockComment = false;
|
|
131
|
+
get insideBlockComment() {
|
|
132
|
+
return this.inBlockComment;
|
|
133
|
+
}
|
|
134
|
+
strip(raw) {
|
|
135
|
+
const comments = [];
|
|
136
|
+
let working = raw;
|
|
137
|
+
if (this.inBlockComment) {
|
|
138
|
+
const close = working.indexOf("*/");
|
|
139
|
+
if (close < 0) return { code: "", comments: [raw] };
|
|
140
|
+
comments.push(working.slice(0, close + 2));
|
|
141
|
+
working = working.slice(close + 2);
|
|
142
|
+
this.inBlockComment = false;
|
|
143
|
+
}
|
|
144
|
+
const code = isUrlDefinition(working) ? working : this.stripComments(working, comments);
|
|
145
|
+
return { code, comments };
|
|
146
|
+
}
|
|
147
|
+
/** Left to right, so a `/*` inside a `//` comment opens no block comment. */
|
|
148
|
+
stripComments(line, comments) {
|
|
149
|
+
let code = "";
|
|
150
|
+
let rest = line;
|
|
151
|
+
for (; ; ) {
|
|
152
|
+
const lineComment = indexOfLineComment(rest);
|
|
153
|
+
const open = indexOutsideFlowValues(rest, "/*");
|
|
154
|
+
if (lineComment >= 0 && (open < 0 || lineComment < open)) {
|
|
155
|
+
comments.push(rest.slice(lineComment));
|
|
156
|
+
return code + rest.slice(0, lineComment);
|
|
157
|
+
}
|
|
158
|
+
if (open < 0) return code + rest;
|
|
159
|
+
const close = rest.indexOf("*/", open + 2);
|
|
160
|
+
if (close < 0) {
|
|
161
|
+
comments.push(rest.slice(open));
|
|
162
|
+
this.inBlockComment = true;
|
|
163
|
+
return code + rest.slice(0, open);
|
|
164
|
+
}
|
|
165
|
+
comments.push(rest.slice(open, close + 2));
|
|
166
|
+
code += `${rest.slice(0, open)} `;
|
|
167
|
+
rest = rest.slice(close + 2);
|
|
73
168
|
}
|
|
74
|
-
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
function indexOutsideFlowValues(line, needle) {
|
|
172
|
+
for (let i = 0; i + needle.length <= line.length; i++) {
|
|
173
|
+
const valueEnd = flowValueEnd(line, i);
|
|
174
|
+
if (valueEnd > 0) i = valueEnd;
|
|
175
|
+
else if (line.startsWith(needle, i)) return i;
|
|
75
176
|
}
|
|
76
177
|
return -1;
|
|
77
178
|
}
|
|
@@ -96,36 +197,172 @@ function parseDecorators(line) {
|
|
|
96
197
|
});
|
|
97
198
|
return { decorators: dec, rest };
|
|
98
199
|
}
|
|
99
|
-
var
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
200
|
+
var COLOR_SOURCE = String.raw`\(\s*color\s+(#[0-9a-fA-F]{3,8}|[a-zA-Z][\w-]*)\s*\)`;
|
|
201
|
+
var HEIGHT_SOURCE = String.raw`\(\s{0,8}y\s{1,8}([^\s()]+)\s{0,8}\)`;
|
|
202
|
+
var LABEL_OFFSET_SOURCE = String.raw`label\s*\[\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\]`;
|
|
203
|
+
var COLOR_TOKEN_RE = new RegExp(COLOR_SOURCE, "iy");
|
|
204
|
+
var HEIGHT_TOKEN_RE = new RegExp(HEIGHT_SOURCE, "y");
|
|
205
|
+
var LABEL_TOKEN_RE = new RegExp(LABEL_OFFSET_SOURCE, "iy");
|
|
206
|
+
var URL_TOKEN_RE = /url\s{0,8}\(([^()]*)\)/iy;
|
|
207
|
+
var GROUP_TOKEN_RE = /\(([^()]*)\)/y;
|
|
208
|
+
var INERTIA_TOKEN_RE = /inertia\b/iy;
|
|
209
|
+
var WORD_TOKEN_RE = /\(?[^\s(]*/y;
|
|
210
|
+
function matchAt(pattern, text, at) {
|
|
211
|
+
pattern.lastIndex = at;
|
|
212
|
+
return pattern.exec(text);
|
|
213
|
+
}
|
|
214
|
+
function groupItems(content, contentAt) {
|
|
215
|
+
const items = [];
|
|
216
|
+
let itemStart = 0;
|
|
217
|
+
for (const raw of content.split(",")) {
|
|
218
|
+
const leading = raw.length - raw.trimStart().length;
|
|
219
|
+
const text = raw.trim();
|
|
220
|
+
if (text) items.push({ at: contentAt + itemStart + leading, text });
|
|
221
|
+
itemStart += raw.length + 1;
|
|
222
|
+
}
|
|
223
|
+
return items;
|
|
224
|
+
}
|
|
225
|
+
function readSuffixToken(text, at) {
|
|
226
|
+
let m;
|
|
227
|
+
if (m = matchAt(COLOR_TOKEN_RE, text, at)) {
|
|
228
|
+
return { at, text: m[0], kind: "color", color: m[1] };
|
|
229
|
+
}
|
|
230
|
+
if (m = matchAt(HEIGHT_TOKEN_RE, text, at)) {
|
|
231
|
+
return { at, text: m[0], kind: "height", value: m[1] };
|
|
232
|
+
}
|
|
233
|
+
if (m = matchAt(LABEL_TOKEN_RE, text, at)) {
|
|
234
|
+
return { at, text: m[0], kind: "label", dx: m[1], dy: m[2] };
|
|
235
|
+
}
|
|
236
|
+
if (m = matchAt(URL_TOKEN_RE, text, at)) {
|
|
237
|
+
return { at, text: m[0], kind: "url", ref: m[1].trim() };
|
|
238
|
+
}
|
|
239
|
+
if (m = matchAt(GROUP_TOKEN_RE, text, at)) {
|
|
240
|
+
return { at, text: m[0], kind: "group", items: groupItems(m[1], at + 1) };
|
|
241
|
+
}
|
|
242
|
+
if (m = matchAt(INERTIA_TOKEN_RE, text, at)) return { at, text: m[0], kind: "inertia" };
|
|
243
|
+
return { at, text: matchAt(WORD_TOKEN_RE, text, at)[0], kind: "word" };
|
|
244
|
+
}
|
|
245
|
+
function scanSuffix(text) {
|
|
246
|
+
const tokens = [];
|
|
247
|
+
let at = 0;
|
|
248
|
+
while (at < text.length) {
|
|
249
|
+
if (/\s/.test(text[at])) {
|
|
250
|
+
at++;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const token = readSuffixToken(text, at);
|
|
254
|
+
tokens.push(token);
|
|
255
|
+
at += token.text.length;
|
|
256
|
+
}
|
|
257
|
+
return tokens;
|
|
258
|
+
}
|
|
259
|
+
var WHOLE_COLOR_GROUP_RE = new RegExp(`^${COLOR_SOURCE}$`, "i");
|
|
260
|
+
var WHOLE_HEIGHT_GROUP_RE = new RegExp(`^${HEIGHT_SOURCE}$`);
|
|
261
|
+
function splitTrailingGroups(text, kinds) {
|
|
262
|
+
const groups = [];
|
|
263
|
+
let end = text.trimEnd().length;
|
|
264
|
+
while (text[end - 1] === ")") {
|
|
265
|
+
const open = text.lastIndexOf("(", end - 1);
|
|
266
|
+
const group = text.slice(Math.max(open, 0), end);
|
|
267
|
+
const wanted = kinds.color && WHOLE_COLOR_GROUP_RE.test(group) || kinds.height && WHOLE_HEIGHT_GROUP_RE.test(group);
|
|
268
|
+
if (open < 0 || !wanted) break;
|
|
269
|
+
groups.push(group);
|
|
270
|
+
end = open;
|
|
271
|
+
while (end > 0 && /\s/.test(text[end - 1])) end--;
|
|
272
|
+
}
|
|
273
|
+
return { rest: text.slice(0, end), groups: groups.reverse() };
|
|
274
|
+
}
|
|
275
|
+
function splitLeadingColor(text) {
|
|
276
|
+
const start = text.length - text.trimStart().length;
|
|
277
|
+
const m = matchAt(COLOR_TOKEN_RE, text, start);
|
|
278
|
+
if (!m) return { rest: text };
|
|
279
|
+
return { color: m[1], rest: text.slice(start + m[0].length) };
|
|
104
280
|
}
|
|
105
|
-
var
|
|
281
|
+
var MAX_UNCLAMPED_MAGNITUDE = 1e12;
|
|
282
|
+
function isRoundTrippableNumber(value) {
|
|
283
|
+
return Number.isFinite(value) && Math.abs(value) <= MAX_UNCLAMPED_MAGNITUDE;
|
|
284
|
+
}
|
|
285
|
+
function readUnclampedNumber(text) {
|
|
286
|
+
const value = Number(text);
|
|
287
|
+
return isRoundTrippableNumber(value) ? value : null;
|
|
288
|
+
}
|
|
289
|
+
var LABEL_OFFSET_RE = new RegExp(String.raw`\b${LABEL_OFFSET_SOURCE}`, "i");
|
|
106
290
|
function parseLabelOffset(line) {
|
|
107
291
|
const m = LABEL_OFFSET_RE.exec(line);
|
|
108
292
|
if (!m) return { labelOffset: null, rest: line };
|
|
109
|
-
const dx =
|
|
110
|
-
const dy =
|
|
293
|
+
const dx = readUnclampedNumber(m[1]);
|
|
294
|
+
const dy = readUnclampedNumber(m[2]);
|
|
111
295
|
const rest = line.replace(LABEL_OFFSET_RE, " ");
|
|
112
|
-
if (
|
|
296
|
+
if (dx === null || dy === null) return { labelOffset: null, rest, unreadable: m[0] };
|
|
113
297
|
return { labelOffset: { dx, dy }, rest };
|
|
114
298
|
}
|
|
115
|
-
var
|
|
116
|
-
function parseUrlRef(line) {
|
|
117
|
-
const m = URL_REF_RE.exec(line);
|
|
118
|
-
if (!m) return { urlRef: null, rest: line };
|
|
119
|
-
return { urlRef: m[1].trim() || null, rest: line.replace(URL_REF_RE, " ") };
|
|
120
|
-
}
|
|
299
|
+
var KEYWORD_RE = /^\s*([A-Za-z][\w-]*)(?![\w\p{L}\p{M}\p{N}-])/u;
|
|
121
300
|
function keywordOf(line) {
|
|
122
|
-
const m =
|
|
301
|
+
const m = KEYWORD_RE.exec(line);
|
|
123
302
|
return m ? m[1].toLowerCase() : "";
|
|
124
303
|
}
|
|
125
304
|
function slug(label) {
|
|
126
305
|
return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_/, "").replace(/_$/, "") || "x";
|
|
127
306
|
}
|
|
128
307
|
|
|
308
|
+
// src/names.ts
|
|
309
|
+
var NAMED_TYPES = /* @__PURE__ */ new Set([
|
|
310
|
+
"anchor",
|
|
311
|
+
"component",
|
|
312
|
+
"accelerator",
|
|
313
|
+
"submap"
|
|
314
|
+
]);
|
|
315
|
+
function defaultName(type) {
|
|
316
|
+
switch (type) {
|
|
317
|
+
case "anchor":
|
|
318
|
+
return "User";
|
|
319
|
+
case "accelerator":
|
|
320
|
+
return "Accelerator";
|
|
321
|
+
case "submap":
|
|
322
|
+
return "Submap";
|
|
323
|
+
case "pipeline":
|
|
324
|
+
return "Pipeline";
|
|
325
|
+
default:
|
|
326
|
+
return "Component";
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function roundAsWritten(n) {
|
|
330
|
+
return Math.round(n * 1e3) / 1e3;
|
|
331
|
+
}
|
|
332
|
+
var DIVISION_SLASH = "\u2215";
|
|
333
|
+
var FULLWIDTH_PLUS = "\uFF0B";
|
|
334
|
+
var FULLWIDTH_LEFT_BRACE = "\uFF5B";
|
|
335
|
+
function escapeCommentMarkers(text) {
|
|
336
|
+
return rewriteOutsideFlowValues(
|
|
337
|
+
text,
|
|
338
|
+
(part) => part.replace(/\/\*/g, `${DIVISION_SLASH}*`).replace(/(?<!:)\/\//g, DIVISION_SLASH.repeat(2))
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
function writtenText(text) {
|
|
342
|
+
return escapeCommentMarkers(text.replace(/\r?\n/g, " "));
|
|
343
|
+
}
|
|
344
|
+
function writtenNoteText(text) {
|
|
345
|
+
return escapeCommentMarkers(text).replace(/\r?\n/g, "\\n");
|
|
346
|
+
}
|
|
347
|
+
function writtenName(label, elementType) {
|
|
348
|
+
const name = label.replace(/\r?\n/g, " ").replace(/->/g, "\u2192").replace(/\+(?=['<>])/g, FULLWIDTH_PLUS).trim().replace(/^\{/, FULLWIDTH_LEFT_BRACE);
|
|
349
|
+
return escapeCommentMarkers(name) || defaultName(elementType);
|
|
350
|
+
}
|
|
351
|
+
var UniqueNames = class {
|
|
352
|
+
used = /* @__PURE__ */ new Set();
|
|
353
|
+
claim(base) {
|
|
354
|
+
let name = base;
|
|
355
|
+
let suffix = 2;
|
|
356
|
+
while (this.used.has(name)) name = `${base} ${suffix++}`;
|
|
357
|
+
this.used.add(name);
|
|
358
|
+
return name;
|
|
359
|
+
}
|
|
360
|
+
/** Takes a name as it is, even when it is already taken (a name the source text already uses). */
|
|
361
|
+
reserve(name) {
|
|
362
|
+
this.used.add(name);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
129
366
|
// src/parser.ts
|
|
130
367
|
function splitDependency(core) {
|
|
131
368
|
const arrow = core.indexOf("->");
|
|
@@ -153,7 +390,7 @@ function splitFlow(core) {
|
|
|
153
390
|
}
|
|
154
391
|
return null;
|
|
155
392
|
}
|
|
156
|
-
var
|
|
393
|
+
var LINK_LIKE_STATEMENT_KEYWORDS = /* @__PURE__ */ new Set([
|
|
157
394
|
"title",
|
|
158
395
|
"style",
|
|
159
396
|
"size",
|
|
@@ -164,12 +401,111 @@ var NON_LINK_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
164
401
|
"evolve",
|
|
165
402
|
"line"
|
|
166
403
|
]);
|
|
404
|
+
var DECLARATION_KEYWORDS = /* @__PURE__ */ new Set([
|
|
405
|
+
"anchor",
|
|
406
|
+
"component",
|
|
407
|
+
"market",
|
|
408
|
+
"ecosystem",
|
|
409
|
+
"note",
|
|
410
|
+
"pioneers",
|
|
411
|
+
"settlers",
|
|
412
|
+
"townplanners",
|
|
413
|
+
"url",
|
|
414
|
+
"build",
|
|
415
|
+
"buy",
|
|
416
|
+
"outsource",
|
|
417
|
+
"accelerator",
|
|
418
|
+
"deaccelerator",
|
|
419
|
+
"submap"
|
|
420
|
+
]);
|
|
167
421
|
var KNOWN_STYLES = /* @__PURE__ */ new Set(["wardley", "handwritten", "colour", "dark"]);
|
|
168
422
|
function compact(obj) {
|
|
169
423
|
const out = {};
|
|
170
424
|
for (const [k, v] of Object.entries(obj)) if (v !== void 0) out[k] = v;
|
|
171
425
|
return out;
|
|
172
426
|
}
|
|
427
|
+
function linkCore(line) {
|
|
428
|
+
const semi = line.indexOf(";");
|
|
429
|
+
return semi >= 0 ? line.slice(0, semi) : line;
|
|
430
|
+
}
|
|
431
|
+
function readLink(line, lineNo, text) {
|
|
432
|
+
const semi = line.indexOf(";");
|
|
433
|
+
const core = (semi >= 0 ? line.slice(0, semi) : line).trim();
|
|
434
|
+
const label = semi >= 0 ? line.slice(semi + 1).trim() : "";
|
|
435
|
+
const common = { ...label ? { label } : {}, raw: line, lineNo, text };
|
|
436
|
+
const dependency = splitDependency(core);
|
|
437
|
+
if (dependency) return { ...dependency, kind: "dependency", ...common };
|
|
438
|
+
const flow = splitFlow(core);
|
|
439
|
+
if (!flow) return null;
|
|
440
|
+
return {
|
|
441
|
+
left: flow.left,
|
|
442
|
+
right: flow.right,
|
|
443
|
+
kind: "flow",
|
|
444
|
+
bidirectional: flow.op === "<>",
|
|
445
|
+
reverse: flow.op === "<",
|
|
446
|
+
...flow.value ? { flowValue: flow.value } : {},
|
|
447
|
+
...common
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function couldBeStatement(line, keyword, after) {
|
|
451
|
+
return LINK_LIKE_STATEMENT_KEYWORDS.has(keyword) || DECLARATION_KEYWORDS.has(keyword) && parseCoords(line) !== null || keyword === "url" && readUrlDefinition(after) !== null;
|
|
452
|
+
}
|
|
453
|
+
function namesAsWritten(link) {
|
|
454
|
+
const keyword = keywordOf(link.left);
|
|
455
|
+
const nameAfterKeyword = link.left.slice(keyword.length).trim();
|
|
456
|
+
return {
|
|
457
|
+
left: [keyword, nameAfterKeyword].filter(Boolean).join(" "),
|
|
458
|
+
right: link.right.split("->").map((part) => part.trim()).join("->")
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function leftNameIsDeclared(link, names) {
|
|
462
|
+
return names.has(link.left) || names.has(namesAsWritten(link).left);
|
|
463
|
+
}
|
|
464
|
+
function unknownStatementMessage(line) {
|
|
465
|
+
const core = linkCore(line);
|
|
466
|
+
if (/->|\+('[^']*')?(<>|>|<)/.test(core)) return "Link needs a name on both sides";
|
|
467
|
+
const flowValue = /\+'[^']*'/.exec(core);
|
|
468
|
+
if (flowValue) return `Flow ${flowValue[0]} has no direction \u2014 add ">", "<" or "<>"`;
|
|
469
|
+
return `Unknown statement "${line.split(/\s/)[0]}"`;
|
|
470
|
+
}
|
|
471
|
+
function buildEdges(links, idOfName, unresolved, labelOf, built) {
|
|
472
|
+
let dependencies = 0;
|
|
473
|
+
let flows = 0;
|
|
474
|
+
const edges = [];
|
|
475
|
+
for (const link of links) {
|
|
476
|
+
const fromId = idOfName(link.left) ?? idOfName(link.writtenNames?.left ?? link.left);
|
|
477
|
+
const toId = idOfName(link.right) ?? idOfName(link.writtenNames?.right ?? link.right);
|
|
478
|
+
if (!fromId || !toId) {
|
|
479
|
+
unresolved(link, fromId ? link.right : link.left);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (link.kind === "dependency") {
|
|
483
|
+
const edge2 = compact({
|
|
484
|
+
id: `dep_${++dependencies}`,
|
|
485
|
+
edgeType: "dependency",
|
|
486
|
+
from: fromId,
|
|
487
|
+
to: toId,
|
|
488
|
+
label: labelOf(link)
|
|
489
|
+
});
|
|
490
|
+
edges.push(edge2);
|
|
491
|
+
built(edge2, link);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
const [from, to] = link.reverse ? [toId, fromId] : [fromId, toId];
|
|
495
|
+
const edge = compact({
|
|
496
|
+
id: `flow_${++flows}`,
|
|
497
|
+
edgeType: "flow",
|
|
498
|
+
from,
|
|
499
|
+
to,
|
|
500
|
+
bidirectional: link.bidirectional ? true : void 0,
|
|
501
|
+
flowValue: link.flowValue,
|
|
502
|
+
label: labelOf(link)
|
|
503
|
+
});
|
|
504
|
+
edges.push(edge);
|
|
505
|
+
built(edge, link);
|
|
506
|
+
}
|
|
507
|
+
return edges;
|
|
508
|
+
}
|
|
173
509
|
var IdAllocator = class {
|
|
174
510
|
used = /* @__PURE__ */ new Set();
|
|
175
511
|
alloc(prefix, label) {
|
|
@@ -185,612 +521,1122 @@ function parseDSL(text) {
|
|
|
185
521
|
return parseDSLWithDiagnostics(text).map;
|
|
186
522
|
}
|
|
187
523
|
function parseDSLWithDiagnostics(text) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
524
|
+
return readMap(text).result;
|
|
525
|
+
}
|
|
526
|
+
function parseDSLWithSourceMap(text) {
|
|
527
|
+
const reading = readMap(text);
|
|
528
|
+
return { ...reading.result, sourceMap: reading.sourceMap };
|
|
529
|
+
}
|
|
530
|
+
function readMap(text) {
|
|
531
|
+
const reading = new MapReader(/* @__PURE__ */ new Set()).read(text);
|
|
532
|
+
const needsDeclaredNames = reading.keywordLinks.some(
|
|
533
|
+
(link) => leftNameIsDeclared(link, reading.declaredNames)
|
|
534
|
+
);
|
|
535
|
+
return needsDeclaredNames ? new MapReader(reading.declaredNames).read(text) : reading;
|
|
536
|
+
}
|
|
537
|
+
var MapReader = class {
|
|
538
|
+
constructor(linkNames) {
|
|
539
|
+
this.linkNames = linkNames;
|
|
540
|
+
}
|
|
541
|
+
linkNames;
|
|
542
|
+
diagnostics = [];
|
|
543
|
+
ids = new IdAllocator();
|
|
544
|
+
namesInUse = new UniqueNames();
|
|
545
|
+
/** The first element declared under a name wins it. */
|
|
546
|
+
idBySourceName = /* @__PURE__ */ new Map();
|
|
547
|
+
idByElementName = /* @__PURE__ */ new Map();
|
|
548
|
+
elements = [];
|
|
549
|
+
passthrough = [];
|
|
550
|
+
pendingLinks = [];
|
|
551
|
+
keywordLinks = [];
|
|
552
|
+
pendingEvolve = [];
|
|
553
|
+
pendingPipeline = [];
|
|
554
|
+
pendingMethod = [];
|
|
555
|
+
config = { title: wardleySchemaModel.DEFAULT_MAP_TITLE };
|
|
556
|
+
lineThatSetConfig = /* @__PURE__ */ new Map();
|
|
557
|
+
unusedConfigLines = [];
|
|
558
|
+
annotationCounter = 0;
|
|
559
|
+
comments = new CommentStripper();
|
|
560
|
+
urlDefinitions = [];
|
|
561
|
+
pendingUrlRefs = [];
|
|
562
|
+
/** Candidate for a `{` block opening: a block binds only to the directly preceding pipeline line. */
|
|
563
|
+
lastPipeline = null;
|
|
564
|
+
blockPipeline = null;
|
|
565
|
+
blockOpening = { line: 0, text: "" };
|
|
566
|
+
lineNo = 0;
|
|
567
|
+
currentLine = "";
|
|
568
|
+
declarationLineById = /* @__PURE__ */ new Map();
|
|
569
|
+
configLinesByKeyword = /* @__PURE__ */ new Map();
|
|
570
|
+
evolveLinesById = /* @__PURE__ */ new Map();
|
|
571
|
+
methodLinesById = /* @__PURE__ */ new Map();
|
|
572
|
+
blockLines = /* @__PURE__ */ new Map();
|
|
573
|
+
pipelineIds = /* @__PURE__ */ new Map();
|
|
574
|
+
rangeFromChildren = /* @__PURE__ */ new Set();
|
|
575
|
+
addElement(element, line = this.lineNo) {
|
|
576
|
+
this.elements.push(element);
|
|
577
|
+
this.declarationLineById.set(element.id, line);
|
|
578
|
+
}
|
|
579
|
+
recordBlockLine(pipeline, end) {
|
|
580
|
+
const lines = this.blockLines.get(pipeline) ?? {};
|
|
581
|
+
lines[end] = this.lineNo;
|
|
582
|
+
this.blockLines.set(pipeline, lines);
|
|
583
|
+
}
|
|
584
|
+
report = (message) => this.diag(message);
|
|
585
|
+
reportAt = (message, line, text) => this.diag(message, line, text);
|
|
586
|
+
diag(message, atLine = this.lineNo, text = this.currentLine) {
|
|
587
|
+
this.diagnostics.push({ line: atLine, message, text });
|
|
588
|
+
}
|
|
589
|
+
keepLine(statement, lineNo = this.lineNo, source = this.currentLine) {
|
|
590
|
+
this.passthrough.push({ text: statement, isComment: false, lineNo, source });
|
|
591
|
+
}
|
|
592
|
+
keep(statement, message) {
|
|
593
|
+
this.keepLine(statement);
|
|
594
|
+
this.diag(message);
|
|
595
|
+
}
|
|
596
|
+
/** For a line that looks like a known construct but cannot be parsed. */
|
|
597
|
+
failed(line) {
|
|
598
|
+
this.keep(line, "Line could not be interpreted (kept losslessly in rawPassthrough)");
|
|
599
|
+
}
|
|
600
|
+
// A reference binds to the name as written first, so `X 2` stays bound to an explicit "X 2" even
|
|
601
|
+
// when a duplicate "X" was renamed to "X 2"; the serializer then writes that reference by the
|
|
602
|
+
// element's new name, and the next parse binds it the same way.
|
|
603
|
+
idOfName(name) {
|
|
604
|
+
return this.idBySourceName.get(name) ?? this.idByElementName.get(name);
|
|
605
|
+
}
|
|
606
|
+
register(sourceName, elementName, id) {
|
|
607
|
+
if (!this.idBySourceName.has(sourceName)) this.idBySourceName.set(sourceName, id);
|
|
608
|
+
this.idByElementName.set(elementName, id);
|
|
609
|
+
}
|
|
610
|
+
setConfig(keyword, update) {
|
|
611
|
+
const earlier = this.lineThatSetConfig.get(keyword);
|
|
612
|
+
if (earlier !== void 0) {
|
|
613
|
+
this.diag(`Repeated "${keyword}" \u2014 replaces the one on line ${earlier}`);
|
|
614
|
+
}
|
|
615
|
+
this.lineThatSetConfig.set(keyword, this.lineNo);
|
|
616
|
+
const configLines2 = this.configLinesByKeyword.get(keyword) ?? [];
|
|
617
|
+
configLines2.push(this.lineNo);
|
|
618
|
+
this.configLinesByKeyword.set(keyword, configLines2);
|
|
619
|
+
this.config = update;
|
|
620
|
+
}
|
|
621
|
+
keepUnusedConfigLine(keyword, statement, reason) {
|
|
622
|
+
this.unusedConfigLines.push({
|
|
623
|
+
keyword,
|
|
624
|
+
reason,
|
|
625
|
+
index: this.passthrough.length,
|
|
626
|
+
line: this.lineNo,
|
|
627
|
+
text: this.currentLine
|
|
628
|
+
});
|
|
629
|
+
this.keepLine(statement);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Links are read before statements, as names may begin with a keyword (`Component -> X`); a link
|
|
633
|
+
* has no `[a, b]` tuple before its `;`. A line that could also be its keyword's statement is a
|
|
634
|
+
* link only when both names are declared.
|
|
635
|
+
*/
|
|
636
|
+
readsAsLink(line, kw, after) {
|
|
637
|
+
const link = parseCoords(linkCore(line)) ? null : readLink(line, this.lineNo, this.currentLine);
|
|
638
|
+
if (!link) return false;
|
|
639
|
+
if (!couldBeStatement(line, kw, after)) {
|
|
640
|
+
this.pendingLinks.push(link);
|
|
224
641
|
return true;
|
|
225
642
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
bidirectional: flow.op === "<>",
|
|
232
|
-
reverse: flow.op === "<",
|
|
233
|
-
...flow.value ? { flowValue: flow.value } : {},
|
|
234
|
-
...linkLabel ? { label: linkLabel } : {},
|
|
235
|
-
raw: line,
|
|
236
|
-
lineNo
|
|
237
|
-
});
|
|
643
|
+
const declared = this.linkNames;
|
|
644
|
+
const written = namesAsWritten(link);
|
|
645
|
+
const leftDeclared = declared.has(link.left) || declared.has(written.left);
|
|
646
|
+
if (leftDeclared && (declared.has(link.right) || declared.has(written.right))) {
|
|
647
|
+
this.pendingLinks.push({ ...link, writtenNames: written });
|
|
238
648
|
return true;
|
|
239
649
|
}
|
|
650
|
+
this.keywordLinks.push(link);
|
|
651
|
+
if (leftDeclared) this.diag(`Link: "${link.right}" not found \u2014 read as a "${kw}" statement`);
|
|
240
652
|
return false;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
diag("Line could not be interpreted (kept losslessly in rawPassthrough)");
|
|
246
|
-
};
|
|
247
|
-
const clampDiag = (n) => {
|
|
248
|
-
if (n < 0 || n > 1) diag(`Coordinate ${n} is outside [0,1] and was clamped`);
|
|
653
|
+
}
|
|
654
|
+
/** Clamps a normalized value to [0,1] — with a diagnostic instead of a later validation crash. */
|
|
655
|
+
clamp(n, report = this.report) {
|
|
656
|
+
if (n < 0 || n > 1) report(`Coordinate ${n} is outside [0,1] and was clamped`);
|
|
249
657
|
return n < 0 ? 0 : n > 1 ? 1 : n;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
visibility:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const close = working.indexOf("*/", open + 2);
|
|
275
|
-
if (close < 0) {
|
|
276
|
-
rawPassthrough.push(working.slice(open));
|
|
277
|
-
working = working.slice(0, open);
|
|
278
|
-
inBlockComment = true;
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
281
|
-
rawPassthrough.push(working.slice(open, close + 2));
|
|
282
|
-
working = `${working.slice(0, open)} ${working.slice(close + 2)}`;
|
|
283
|
-
open = indexOfOutsideQuotes(working, "/*");
|
|
284
|
-
}
|
|
285
|
-
const { code, comment } = splitLineComment(working);
|
|
286
|
-
if (comment !== null) rawPassthrough.push(comment);
|
|
287
|
-
working = code;
|
|
288
|
-
}
|
|
289
|
-
const line = working.trim();
|
|
290
|
-
if (!line) continue;
|
|
291
|
-
if (blockPipeline) {
|
|
292
|
-
if (line === "}") {
|
|
293
|
-
blockPipeline = null;
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
const childKw = keywordOf(line);
|
|
297
|
-
if (childKw === "component") {
|
|
298
|
-
const child = parseBlockChild(line.slice(childKw.length).trim());
|
|
299
|
-
if (child) {
|
|
300
|
-
blockPipeline.children.push(child);
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
rawPassthrough.push(line);
|
|
305
|
-
continue;
|
|
658
|
+
}
|
|
659
|
+
position(visibility, evolution) {
|
|
660
|
+
return { visibility: this.clamp(visibility), evolution: this.clamp(evolution) };
|
|
661
|
+
}
|
|
662
|
+
reportTextBefore(before) {
|
|
663
|
+
if (before) this.diag(`Ignored text before the coordinates: "${before}"`);
|
|
664
|
+
}
|
|
665
|
+
/** A suffix written in front of the coordinates is part of the name or text (OWM). */
|
|
666
|
+
reportMisplacedSuffix(groups, where, fix) {
|
|
667
|
+
if (groups.length) this.diag(`"${groups.join(" ")}" ${where} \u2014 ${fix}`);
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* A text as the serializer writes it. Its comment markers are protected only by flow values
|
|
671
|
+
* `+'…'` within the text itself; one that other parts of the line protected is escaped now,
|
|
672
|
+
* not on the next parse.
|
|
673
|
+
*/
|
|
674
|
+
readText(text, line = this.lineNo, source = this.currentLine) {
|
|
675
|
+
const written = escapeCommentMarkers(text);
|
|
676
|
+
if (written !== text) {
|
|
677
|
+
this.diag(
|
|
678
|
+
`"${text}" read as "${written}" \u2014 "//" or "/*" there would start a comment once saved`,
|
|
679
|
+
line,
|
|
680
|
+
source
|
|
681
|
+
);
|
|
306
682
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
683
|
+
return written;
|
|
684
|
+
}
|
|
685
|
+
/** Names an element the way the serializer writes it, so the next parse yields the same name. */
|
|
686
|
+
nameElement(sourceName, elementType, atLine = this.lineNo, atText = this.currentLine) {
|
|
687
|
+
const base = writtenName(sourceName, elementType);
|
|
688
|
+
const name = this.namesInUse.claim(base);
|
|
689
|
+
if (name !== base) {
|
|
690
|
+
this.diag(
|
|
691
|
+
`Duplicate name "${sourceName}" \u2014 renamed to "${name}"; links bind to the first "${sourceName}"`,
|
|
692
|
+
atLine,
|
|
693
|
+
atText
|
|
694
|
+
);
|
|
695
|
+
} else if (name !== sourceName) {
|
|
696
|
+
this.diag(renamedMessage(sourceName, name), atLine, atText);
|
|
697
|
+
}
|
|
698
|
+
return name;
|
|
699
|
+
}
|
|
700
|
+
openBlock() {
|
|
701
|
+
if (this.lastPipeline) {
|
|
702
|
+
this.blockPipeline = this.lastPipeline;
|
|
703
|
+
this.recordBlockLine(this.blockPipeline, "open");
|
|
704
|
+
this.blockOpening = { line: this.lineNo, text: this.currentLine };
|
|
705
|
+
} else this.diag('Ignored "{" \u2014 a block must directly follow a pipeline line');
|
|
706
|
+
this.lastPipeline = null;
|
|
707
|
+
}
|
|
708
|
+
closeBlock() {
|
|
709
|
+
if (this.blockPipeline) {
|
|
710
|
+
this.recordBlockLine(this.blockPipeline, "close");
|
|
711
|
+
this.blockPipeline = null;
|
|
712
|
+
} else this.diag('Ignored "}" \u2014 no pipeline block is open');
|
|
713
|
+
this.lastPipeline = null;
|
|
714
|
+
}
|
|
715
|
+
endUnclosedBlock() {
|
|
716
|
+
if (!this.blockPipeline) return;
|
|
717
|
+
this.diag(
|
|
718
|
+
`Pipeline block of "${this.blockPipeline.name}" is never closed \u2014 add "}"`,
|
|
719
|
+
this.blockOpening.line,
|
|
720
|
+
this.blockOpening.text
|
|
721
|
+
);
|
|
722
|
+
this.blockPipeline = null;
|
|
723
|
+
}
|
|
724
|
+
/** A `url(Name)` reference on the element just pushed. */
|
|
725
|
+
addUrlRef(ref) {
|
|
726
|
+
this.pendingUrlRefs.push({
|
|
727
|
+
index: this.elements.length - 1,
|
|
728
|
+
ref,
|
|
729
|
+
line: this.lineNo,
|
|
730
|
+
text: this.currentLine
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
read(text) {
|
|
734
|
+
const sourceLines = text.split(/\r?\n/);
|
|
735
|
+
if (sourceLines.length > 1 && sourceLines[sourceLines.length - 1] === "") sourceLines.pop();
|
|
736
|
+
let blockCommentStart = null;
|
|
737
|
+
for (let i = 0; i < sourceLines.length; i++) {
|
|
738
|
+
const raw = withoutTrailingCarriageReturns(sourceLines[i]);
|
|
739
|
+
this.lineNo = i + 1;
|
|
740
|
+
this.currentLine = raw.trim();
|
|
741
|
+
const stripped = this.comments.strip(raw);
|
|
742
|
+
if (!this.comments.insideBlockComment) blockCommentStart = null;
|
|
743
|
+
else blockCommentStart ??= { line: this.lineNo, text: this.currentLine };
|
|
744
|
+
for (const fragment of stripped.comments) {
|
|
745
|
+
this.passthrough.push({
|
|
746
|
+
text: fragment,
|
|
747
|
+
isComment: true,
|
|
748
|
+
lineNo: this.lineNo,
|
|
749
|
+
source: this.currentLine
|
|
750
|
+
});
|
|
312
751
|
}
|
|
313
|
-
|
|
314
|
-
|
|
752
|
+
this.readCode(stripped.code.trim());
|
|
753
|
+
}
|
|
754
|
+
this.endUnclosedBlock();
|
|
755
|
+
if (blockCommentStart) {
|
|
756
|
+
const { line, text: startText } = blockCommentStart;
|
|
757
|
+
this.diag(
|
|
758
|
+
'Block comment "/*" is never closed \u2014 the rest of the file is a comment',
|
|
759
|
+
line,
|
|
760
|
+
startText
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
return this.finish();
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Braces are structural tokens: `{` at the start of a line, and `}` there while a block is open
|
|
767
|
+
* or on its own; `}` also ends a block line. A name starting with `}` stays a name.
|
|
768
|
+
*/
|
|
769
|
+
readCode(code) {
|
|
770
|
+
let statement = code;
|
|
771
|
+
for (; ; ) {
|
|
772
|
+
if (statement.startsWith("{")) this.openBlock();
|
|
773
|
+
else if (statement.startsWith("}") && (this.blockPipeline || statement === "}")) {
|
|
774
|
+
this.closeBlock();
|
|
775
|
+
} else break;
|
|
776
|
+
statement = statement.slice(1).trim();
|
|
315
777
|
}
|
|
778
|
+
const closesBlock = this.blockPipeline !== null && statement.endsWith("}");
|
|
779
|
+
if (closesBlock) statement = statement.slice(0, -1).trim();
|
|
780
|
+
if (statement) this.readStatement(statement);
|
|
781
|
+
if (closesBlock && this.blockPipeline) {
|
|
782
|
+
this.recordBlockLine(this.blockPipeline, "close");
|
|
783
|
+
this.blockPipeline = null;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
readStatement(line) {
|
|
316
787
|
const kw = keywordOf(line);
|
|
317
788
|
const after = line.slice(kw.length).trim();
|
|
318
|
-
if (kw
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
789
|
+
if (this.blockPipeline && kw === "component" && this.readBlockChild(after)) return;
|
|
790
|
+
this.lastPipeline = null;
|
|
791
|
+
if (this.readsAsLink(line, kw, after)) return;
|
|
322
792
|
switch (kw) {
|
|
323
793
|
case "title":
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
case "anchor":
|
|
327
|
-
|
|
328
|
-
if (!node) {
|
|
329
|
-
failed(line);
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
const id = ids.alloc("anchor", node.name);
|
|
333
|
-
const anchor = compact({
|
|
334
|
-
id,
|
|
335
|
-
elementType: "anchor",
|
|
336
|
-
label: node.name,
|
|
337
|
-
position: pos(node.coords.a, node.coords.b),
|
|
338
|
-
labelOffset: node.labelOffset,
|
|
339
|
-
color: node.color
|
|
340
|
-
});
|
|
341
|
-
elements.push(anchor);
|
|
342
|
-
register(node.name, id);
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
794
|
+
this.setConfig("title", { ...this.config, title: this.readText(after) });
|
|
795
|
+
return;
|
|
796
|
+
case "anchor":
|
|
797
|
+
return this.readAnchor(line, after);
|
|
345
798
|
case "component":
|
|
346
799
|
case "market":
|
|
347
|
-
case "ecosystem":
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const { color, rest } = parseColor(after);
|
|
371
|
-
const coords = parseCoords(rest);
|
|
372
|
-
const textPart = stripCoords(rest).trim().replace(/\\n/g, "\n");
|
|
373
|
-
if (!coords) {
|
|
374
|
-
failed(line);
|
|
375
|
-
break;
|
|
376
|
-
}
|
|
377
|
-
const id = ids.alloc("note", textPart || "note");
|
|
378
|
-
const note = compact({
|
|
379
|
-
id,
|
|
380
|
-
elementType: "note",
|
|
381
|
-
label: textPart,
|
|
382
|
-
position: pos(coords.a, coords.b),
|
|
383
|
-
color
|
|
384
|
-
});
|
|
385
|
-
elements.push(note);
|
|
386
|
-
break;
|
|
387
|
-
}
|
|
388
|
-
case "pipeline": {
|
|
389
|
-
let body = after;
|
|
390
|
-
let opensBlock = false;
|
|
391
|
-
if (body.endsWith("{")) {
|
|
392
|
-
opensBlock = true;
|
|
393
|
-
body = body.slice(0, -1).trim();
|
|
394
|
-
}
|
|
395
|
-
const col = parseColor(body);
|
|
396
|
-
const yMatch = /\(\s{0,8}y\s{1,8}([\d.]+)\s{0,8}\)/.exec(col.rest);
|
|
397
|
-
const bodyRest = yMatch ? col.rest.replace(yMatch[0], " ") : col.rest;
|
|
398
|
-
const coords = parseCoords(bodyRest);
|
|
399
|
-
const name = stripCoords(bodyRest).trim();
|
|
400
|
-
if (!name) {
|
|
401
|
-
failed(line);
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
404
|
-
const pending = {
|
|
405
|
-
name,
|
|
406
|
-
...coords ? { start: coords.a, end: coords.b } : {},
|
|
407
|
-
...yMatch ? { visibility: clamp01(Number(yMatch[1])) } : {},
|
|
408
|
-
...col.color ? { color: col.color } : {},
|
|
409
|
-
raw: line,
|
|
410
|
-
children: []
|
|
411
|
-
};
|
|
412
|
-
pendingPipeline.push(pending);
|
|
413
|
-
if (opensBlock) blockPipeline = pending;
|
|
414
|
-
else lastPipeline = pending;
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
case "evolve": {
|
|
418
|
-
const ev = parseEvolve(after);
|
|
419
|
-
if (!ev) {
|
|
420
|
-
failed(line);
|
|
421
|
-
break;
|
|
422
|
-
}
|
|
423
|
-
pendingEvolve.push({ ...ev, target: clampDiag(ev.target), raw: line, lineNo });
|
|
424
|
-
break;
|
|
425
|
-
}
|
|
426
|
-
case "style": {
|
|
427
|
-
const s = after.toLowerCase();
|
|
428
|
-
if (KNOWN_STYLES.has(s)) config = { ...config, style: s };
|
|
429
|
-
else failed(line);
|
|
430
|
-
break;
|
|
431
|
-
}
|
|
432
|
-
case "size": {
|
|
433
|
-
const coords = parseCoords(after);
|
|
434
|
-
if (coords) config = { ...config, size: { width: coords.a, height: coords.b } };
|
|
435
|
-
else failed(line);
|
|
436
|
-
break;
|
|
437
|
-
}
|
|
438
|
-
case "evolution": {
|
|
439
|
-
const parts = after.split("->").map((s) => s.trim());
|
|
440
|
-
if (parts.length === 4) {
|
|
441
|
-
config = { ...config, evolutionLabels: [parts[0], parts[1], parts[2], parts[3]] };
|
|
442
|
-
} else rawPassthrough.push(raw);
|
|
443
|
-
break;
|
|
444
|
-
}
|
|
445
|
-
case "y-axis": {
|
|
446
|
-
const parts = after.split("->").map((s) => s.trim()).filter(Boolean);
|
|
447
|
-
if (parts.length) {
|
|
448
|
-
config = {
|
|
449
|
-
...config,
|
|
450
|
-
yAxisLabel: parts[0],
|
|
451
|
-
...parts.length >= 3 ? { yAxisEndLabels: [parts[1], parts[2]] } : {}
|
|
452
|
-
};
|
|
453
|
-
} else failed(line);
|
|
454
|
-
break;
|
|
455
|
-
}
|
|
456
|
-
case "annotations": {
|
|
457
|
-
const coords = parseCoords(after);
|
|
458
|
-
if (coords) {
|
|
459
|
-
config = {
|
|
460
|
-
...config,
|
|
461
|
-
annotationsBoxPosition: pos(coords.a, coords.b)
|
|
462
|
-
};
|
|
463
|
-
} else rawPassthrough.push(raw);
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
466
|
-
case "annotation": {
|
|
467
|
-
const numMatch = /^\s*(\d+)/.exec(after);
|
|
468
|
-
const number = numMatch ? Number(numMatch[1]) : ++annoCounter;
|
|
469
|
-
const colA = parseColor(after.replace(/^\s*\d+\s*/, ""));
|
|
470
|
-
const afterNum = colA.rest;
|
|
471
|
-
const multi = parseMultiCoords(afterNum);
|
|
472
|
-
let positions;
|
|
473
|
-
let text2;
|
|
474
|
-
if (multi) {
|
|
475
|
-
positions = multi.tuples.map((t) => pos(t.a, t.b));
|
|
476
|
-
text2 = multi.rest.trim();
|
|
477
|
-
} else {
|
|
478
|
-
const coords = parseCoords(afterNum);
|
|
479
|
-
if (!coords) {
|
|
480
|
-
rawPassthrough.push(line);
|
|
481
|
-
break;
|
|
482
|
-
}
|
|
483
|
-
positions = [pos(coords.a, coords.b)];
|
|
484
|
-
text2 = stripCoords(afterNum).trim();
|
|
485
|
-
}
|
|
486
|
-
const annotation = {
|
|
487
|
-
id: ids.alloc("anno", String(number)),
|
|
488
|
-
elementType: "annotation",
|
|
489
|
-
label: text2,
|
|
490
|
-
position: positions[0],
|
|
491
|
-
number,
|
|
492
|
-
positions,
|
|
493
|
-
text: text2,
|
|
494
|
-
...colA.color ? { color: colA.color } : {}
|
|
495
|
-
};
|
|
496
|
-
elements.push(annotation);
|
|
497
|
-
break;
|
|
498
|
-
}
|
|
499
|
-
case "line": {
|
|
500
|
-
const col = parseColor(after);
|
|
501
|
-
const multi = parseMultiCoords(col.rest);
|
|
502
|
-
if (!multi || multi.tuples.length < 2) {
|
|
503
|
-
failed(line);
|
|
504
|
-
break;
|
|
505
|
-
}
|
|
506
|
-
const flags = multi.rest.toLowerCase();
|
|
507
|
-
const strokeStyle = flags.includes("(dashed)") ? "dashed" : flags.includes("(dotted)") ? "dotted" : void 0;
|
|
508
|
-
const points = multi.tuples.map((t) => pos(t.a, t.b));
|
|
509
|
-
const drawing = {
|
|
510
|
-
id: ids.alloc("draw", "line"),
|
|
511
|
-
elementType: "drawing",
|
|
512
|
-
label: "",
|
|
513
|
-
position: points[0],
|
|
514
|
-
points,
|
|
515
|
-
...flags.includes("(closed)") ? { closed: true } : {},
|
|
516
|
-
...strokeStyle ? { strokeStyle } : {},
|
|
517
|
-
...col.color ? { color: col.color } : {}
|
|
518
|
-
};
|
|
519
|
-
elements.push(drawing);
|
|
520
|
-
break;
|
|
521
|
-
}
|
|
800
|
+
case "ecosystem":
|
|
801
|
+
if (!this.pushComponent(after, kw)) this.failed(line);
|
|
802
|
+
return;
|
|
803
|
+
case "note":
|
|
804
|
+
return this.readNote(line, after);
|
|
805
|
+
case "pipeline":
|
|
806
|
+
return this.readPipeline(line, after);
|
|
807
|
+
case "evolve":
|
|
808
|
+
return this.readEvolve(line, after);
|
|
809
|
+
case "style":
|
|
810
|
+
return this.readStyle(line, after);
|
|
811
|
+
case "size":
|
|
812
|
+
return this.readSize(line, after);
|
|
813
|
+
case "evolution":
|
|
814
|
+
return this.readEvolution(line, after);
|
|
815
|
+
case "y-axis":
|
|
816
|
+
return this.readYAxis(line, after);
|
|
817
|
+
case "annotations":
|
|
818
|
+
return this.readAnnotationsBox(line, after);
|
|
819
|
+
case "annotation":
|
|
820
|
+
return this.readAnnotation(line, after);
|
|
821
|
+
case "line":
|
|
822
|
+
return this.readDrawing(line, after);
|
|
522
823
|
case "pioneers":
|
|
523
824
|
case "settlers":
|
|
524
|
-
case "townplanners":
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
failed(line);
|
|
529
|
-
break;
|
|
530
|
-
}
|
|
531
|
-
elements.push(
|
|
532
|
-
makeAttitude(ids, kw, four.a, four.b, four.c, four.d, col.color)
|
|
533
|
-
);
|
|
534
|
-
break;
|
|
535
|
-
}
|
|
536
|
-
case "url": {
|
|
537
|
-
const open = after.indexOf("[");
|
|
538
|
-
const close = after.lastIndexOf("]");
|
|
539
|
-
const name = open > 0 ? after.slice(0, open).trim() : "";
|
|
540
|
-
const address = open > 0 && close > open ? after.slice(open + 1, close).trim() : "";
|
|
541
|
-
if (!name || !address || after.slice(close + 1).trim() !== "") {
|
|
542
|
-
failed(line);
|
|
543
|
-
break;
|
|
544
|
-
}
|
|
545
|
-
urlDefs.set(name, address);
|
|
546
|
-
break;
|
|
547
|
-
}
|
|
825
|
+
case "townplanners":
|
|
826
|
+
return this.readAttitude(line, after, kw);
|
|
827
|
+
case "url":
|
|
828
|
+
return this.readUrlDefinition(line, after);
|
|
548
829
|
case "build":
|
|
549
830
|
case "buy":
|
|
550
|
-
case "outsource":
|
|
551
|
-
|
|
552
|
-
if (node) {
|
|
553
|
-
const id = ids.alloc("cmp", node.name);
|
|
554
|
-
const component = compact({
|
|
555
|
-
id,
|
|
556
|
-
elementType: "component",
|
|
557
|
-
label: node.name,
|
|
558
|
-
position: pos(node.coords.a, node.coords.b),
|
|
559
|
-
labelOffset: node.labelOffset,
|
|
560
|
-
decorators: { ...node.decorators, method: kw },
|
|
561
|
-
color: node.color
|
|
562
|
-
});
|
|
563
|
-
elements.push(component);
|
|
564
|
-
register(node.name, id);
|
|
565
|
-
} else if (after.trim()) {
|
|
566
|
-
pendingMethod.push({ name: after.trim(), method: kw, raw: line, lineNo });
|
|
567
|
-
} else {
|
|
568
|
-
rawPassthrough.push(line);
|
|
569
|
-
}
|
|
570
|
-
break;
|
|
571
|
-
}
|
|
831
|
+
case "outsource":
|
|
832
|
+
return this.readMethod(line, after, kw);
|
|
572
833
|
case "accelerator":
|
|
573
|
-
case "deaccelerator":
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
break;
|
|
578
|
-
}
|
|
579
|
-
const id = ids.alloc("accel", node.name);
|
|
580
|
-
const accelerator = {
|
|
581
|
-
id,
|
|
582
|
-
elementType: "accelerator",
|
|
583
|
-
direction: kw === "deaccelerator" ? "deaccelerate" : "accelerate",
|
|
584
|
-
label: node.name,
|
|
585
|
-
position: pos(node.coords.a, node.coords.b),
|
|
586
|
-
...node.color ? { color: node.color } : {}
|
|
587
|
-
};
|
|
588
|
-
elements.push(accelerator);
|
|
589
|
-
register(node.name, id);
|
|
590
|
-
break;
|
|
591
|
-
}
|
|
592
|
-
case "submap": {
|
|
593
|
-
const node = parseNode(after);
|
|
594
|
-
if (!node) {
|
|
595
|
-
failed(line);
|
|
596
|
-
break;
|
|
597
|
-
}
|
|
598
|
-
const id = ids.alloc("submap", node.name);
|
|
599
|
-
const submap = {
|
|
600
|
-
id,
|
|
601
|
-
elementType: "submap",
|
|
602
|
-
label: node.name,
|
|
603
|
-
position: pos(node.coords.a, node.coords.b),
|
|
604
|
-
...node.color ? { color: node.color } : {}
|
|
605
|
-
};
|
|
606
|
-
elements.push(submap);
|
|
607
|
-
if (node.urlRef) pendingUrlRefs.push({ index: elements.length - 1, ref: node.urlRef });
|
|
608
|
-
register(node.name, id);
|
|
609
|
-
break;
|
|
610
|
-
}
|
|
834
|
+
case "deaccelerator":
|
|
835
|
+
return this.readAccelerator(line, after, kw);
|
|
836
|
+
case "submap":
|
|
837
|
+
return this.readSubmap(line, after);
|
|
611
838
|
default: {
|
|
612
|
-
|
|
839
|
+
const link = readLink(line, this.lineNo, this.currentLine);
|
|
840
|
+
if (link) this.pendingLinks.push(link);
|
|
841
|
+
else this.keep(line, unknownStatementMessage(line));
|
|
613
842
|
}
|
|
614
843
|
}
|
|
615
844
|
}
|
|
616
|
-
|
|
617
|
-
const
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
compact({
|
|
628
|
-
id: cid,
|
|
629
|
-
elementType: "component",
|
|
630
|
-
label: c.name,
|
|
631
|
-
position: { visibility, evolution: clamp01(c.maturity) },
|
|
632
|
-
labelOffset: c.labelOffset,
|
|
633
|
-
decorators: Object.keys(c.decorators).length ? c.decorators : void 0,
|
|
634
|
-
pipelineId: id,
|
|
635
|
-
color: c.color
|
|
636
|
-
})
|
|
637
|
-
);
|
|
638
|
-
register(c.name, cid);
|
|
639
|
-
childIds.push(cid);
|
|
845
|
+
readBlockChild(after) {
|
|
846
|
+
const blockPipeline = this.blockPipeline;
|
|
847
|
+
const child = parseBlockChild(after, this.report);
|
|
848
|
+
if (child) {
|
|
849
|
+
blockPipeline.children.push({
|
|
850
|
+
...child,
|
|
851
|
+
maturity: this.clamp(child.maturity),
|
|
852
|
+
lineNo: this.lineNo,
|
|
853
|
+
text: this.currentLine
|
|
854
|
+
});
|
|
855
|
+
return true;
|
|
640
856
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
continue;
|
|
857
|
+
if (splitAtCoords(after)?.name) {
|
|
858
|
+
this.diag(
|
|
859
|
+
`Component with [visibility, maturity] inside the block of pipeline "${blockPipeline.name}" is not a child \u2014 use [maturity]`
|
|
860
|
+
);
|
|
646
861
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
readAnchor(line, after) {
|
|
865
|
+
const node = parseNode(after, ANCHOR_SUFFIXES, this.report);
|
|
866
|
+
if (!node) return this.failed(line);
|
|
867
|
+
const label = this.nameElement(node.name, "anchor");
|
|
868
|
+
const id = this.ids.alloc("anchor", label);
|
|
869
|
+
const anchor = compact({
|
|
651
870
|
id,
|
|
652
|
-
elementType: "
|
|
653
|
-
label
|
|
654
|
-
position:
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
childIds,
|
|
658
|
-
...p.color ? { color: p.color } : {}
|
|
659
|
-
};
|
|
660
|
-
elements.push(pipeline, ...childElements);
|
|
661
|
-
}
|
|
662
|
-
for (const ev of pendingEvolve) {
|
|
663
|
-
const id = nameToId.get(ev.name);
|
|
664
|
-
const idx = elements.findIndex((e) => e.id === id);
|
|
665
|
-
if (idx < 0 || elements[idx].elementType !== "component") {
|
|
666
|
-
rawPassthrough.push(ev.raw);
|
|
667
|
-
diag(`evolve: component "${ev.name}" not found`, ev.lineNo, ev.raw);
|
|
668
|
-
continue;
|
|
669
|
-
}
|
|
670
|
-
const movement = compact({
|
|
671
|
-
targetEvolution: ev.target,
|
|
672
|
-
newLabel: ev.newLabel,
|
|
673
|
-
method: ev.method,
|
|
674
|
-
labelOffset: ev.labelOffset
|
|
871
|
+
elementType: "anchor",
|
|
872
|
+
label,
|
|
873
|
+
position: this.position(node.coords.a, node.coords.b),
|
|
874
|
+
labelOffset: node.labelOffset,
|
|
875
|
+
color: node.color
|
|
675
876
|
});
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
for (const pm of pendingMethod) {
|
|
679
|
-
const id = nameToId.get(pm.name);
|
|
680
|
-
const idx = elements.findIndex((e) => e.id === id);
|
|
681
|
-
const el = idx >= 0 ? elements[idx] : void 0;
|
|
682
|
-
if (!el || el.elementType !== "component") {
|
|
683
|
-
rawPassthrough.push(pm.raw);
|
|
684
|
-
diag(`${pm.method}: component "${pm.name}" not found`, pm.lineNo, pm.raw);
|
|
685
|
-
continue;
|
|
686
|
-
}
|
|
687
|
-
elements[idx] = { ...el, decorators: { ...el.decorators, method: pm.method } };
|
|
877
|
+
this.addElement(anchor);
|
|
878
|
+
this.register(node.name, label, id);
|
|
688
879
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
880
|
+
/** `component`/`market`/`ecosystem` and `build`/`buy`/`outsource` lines with coordinates. */
|
|
881
|
+
pushComponent(after, kw) {
|
|
882
|
+
const keywordMethod = METHODS.has(kw) ? kw : void 0;
|
|
883
|
+
const node = parseNode(after, COMPONENT_SUFFIXES, this.report, keywordMethod);
|
|
884
|
+
if (!node) return false;
|
|
885
|
+
const decorators = mergeLegacy(kw, node.decorators);
|
|
886
|
+
const label = this.nameElement(node.name, "component");
|
|
887
|
+
const id = this.ids.alloc("cmp", label);
|
|
888
|
+
const component = compact({
|
|
889
|
+
id,
|
|
890
|
+
elementType: "component",
|
|
891
|
+
label,
|
|
892
|
+
position: this.position(node.coords.a, node.coords.b),
|
|
893
|
+
labelOffset: node.labelOffset,
|
|
894
|
+
decorators: Object.keys(decorators).length ? decorators : void 0,
|
|
895
|
+
color: node.color
|
|
896
|
+
});
|
|
897
|
+
this.addElement(component);
|
|
898
|
+
if (node.urlRef) this.addUrlRef(node.urlRef);
|
|
899
|
+
this.register(node.name, label, id);
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
readNote(line, after) {
|
|
903
|
+
const split = splitAtCoords(after);
|
|
904
|
+
if (!split) return this.failed(line);
|
|
905
|
+
const { color } = readSuffix(split.suffix, NOTE_SUFFIXES, this.report);
|
|
906
|
+
this.reportMisplacedSuffix(
|
|
907
|
+
splitTrailingGroups(split.name, { color: true, height: false }).groups,
|
|
908
|
+
"before the coordinates is read as part of the text",
|
|
909
|
+
"write it after them"
|
|
910
|
+
);
|
|
911
|
+
const textPart = this.readText(split.name.replace(/\\n/g, "\n"));
|
|
912
|
+
const note = compact({
|
|
913
|
+
id: this.ids.alloc("note", textPart || "note"),
|
|
914
|
+
elementType: "note",
|
|
915
|
+
label: textPart,
|
|
916
|
+
position: this.position(split.coords.a, split.coords.b),
|
|
917
|
+
color
|
|
918
|
+
});
|
|
919
|
+
this.addElement(note);
|
|
920
|
+
}
|
|
921
|
+
/** `pipeline X [s, e]`, `pipeline X` (block form, range from children) — optionally with `{` at the end. */
|
|
922
|
+
readPipeline(line, after) {
|
|
923
|
+
this.endUnclosedBlock();
|
|
924
|
+
let body = after;
|
|
925
|
+
let opensBlock = false;
|
|
926
|
+
while (body.endsWith("{")) {
|
|
927
|
+
if (opensBlock) this.diag('Ignored "{" \u2014 a block must directly follow a pipeline line');
|
|
928
|
+
opensBlock = true;
|
|
929
|
+
body = body.slice(0, -1).trim();
|
|
700
930
|
}
|
|
931
|
+
const { name, range, suffixText, misplaced } = splitPipelineLine(body);
|
|
932
|
+
if (!name) return this.failed(line);
|
|
933
|
+
if (range && !range.coords) this.diag(unreadableNumberMessage(range.tuple));
|
|
934
|
+
this.reportMisplacedSuffix(
|
|
935
|
+
misplaced,
|
|
936
|
+
"before the coordinates is read as part of the name",
|
|
937
|
+
"write it after them"
|
|
938
|
+
);
|
|
939
|
+
const notes = [];
|
|
940
|
+
const note = (message) => notes.push(message);
|
|
941
|
+
const suffix = readSuffix(suffixText, PIPELINE_SUFFIXES, note);
|
|
942
|
+
const coords = range?.coords;
|
|
943
|
+
const pending = {
|
|
944
|
+
name,
|
|
945
|
+
lineNo: this.lineNo,
|
|
946
|
+
text: this.currentLine,
|
|
947
|
+
...coords ? { start: this.clamp(coords.a), end: this.clamp(coords.b) } : {},
|
|
948
|
+
...suffix.height !== void 0 ? { visibility: roundAsWritten(this.clamp(suffix.height, note)) } : {},
|
|
949
|
+
...suffix.color ? { color: suffix.color } : {},
|
|
950
|
+
notes,
|
|
951
|
+
raw: line.slice(0, line.length - after.length) + body,
|
|
952
|
+
children: []
|
|
953
|
+
};
|
|
954
|
+
this.pendingPipeline.push(pending);
|
|
955
|
+
if (opensBlock) {
|
|
956
|
+
this.blockPipeline = pending;
|
|
957
|
+
this.recordBlockLine(pending, "open");
|
|
958
|
+
this.blockOpening = { line: this.lineNo, text: this.currentLine };
|
|
959
|
+
} else this.lastPipeline = pending;
|
|
701
960
|
}
|
|
702
|
-
|
|
703
|
-
|
|
961
|
+
readEvolve(line, after) {
|
|
962
|
+
const notes = [];
|
|
963
|
+
const note = (message) => notes.push(message);
|
|
964
|
+
const evolve = parseEvolve(after, note);
|
|
965
|
+
if (!evolve) return this.failed(line);
|
|
966
|
+
this.pendingEvolve.push({
|
|
967
|
+
...evolve,
|
|
968
|
+
target: this.clamp(evolve.target, note),
|
|
969
|
+
notes,
|
|
970
|
+
raw: line,
|
|
971
|
+
lineNo: this.lineNo,
|
|
972
|
+
text: this.currentLine
|
|
973
|
+
});
|
|
704
974
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
link.raw
|
|
717
|
-
);
|
|
718
|
-
continue;
|
|
975
|
+
readStyle(line, after) {
|
|
976
|
+
const style = after.toLowerCase();
|
|
977
|
+
if (KNOWN_STYLES.has(style)) {
|
|
978
|
+
this.setConfig("style", { ...this.config, style });
|
|
979
|
+
} else this.failed(line);
|
|
980
|
+
}
|
|
981
|
+
readSize(line, after) {
|
|
982
|
+
const split = splitAtCoords(after);
|
|
983
|
+
const coords = split?.coords;
|
|
984
|
+
if (!coords || !isRoundTrippableNumber(coords.a) || !isRoundTrippableNumber(coords.b)) {
|
|
985
|
+
return this.failed(line);
|
|
719
986
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
})
|
|
987
|
+
this.reportTextBefore(split.name);
|
|
988
|
+
readSuffix(split.suffix, NO_SUFFIXES, this.report);
|
|
989
|
+
this.setConfig("size", { ...this.config, size: { width: coords.a, height: coords.b } });
|
|
990
|
+
const problem = sizeOutsideLimits(coords.a, coords.b);
|
|
991
|
+
if (problem) this.diag(problem);
|
|
992
|
+
if (!Number.isInteger(coords.a) || !Number.isInteger(coords.b)) {
|
|
993
|
+
this.diag(
|
|
994
|
+
`Map size ${coords.a}\xD7${coords.b} is not in whole pixels \u2014 OWM only reads whole-pixel sizes`
|
|
729
995
|
);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
readEvolution(line, after) {
|
|
999
|
+
const parts = after.split("->").map((s) => s.trim());
|
|
1000
|
+
if (parts.length === 4) {
|
|
1001
|
+
const [first, second, third, fourth] = parts.map((part) => this.readText(part));
|
|
1002
|
+
const evolutionLabels = [first, second, third, fourth];
|
|
1003
|
+
this.setConfig("evolution", { ...this.config, evolutionLabels });
|
|
730
1004
|
} else {
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1005
|
+
const reason = `Evolution needs exactly four stages, found ${parts.length}`;
|
|
1006
|
+
this.keepUnusedConfigLine("evolution", line, reason);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
/** `y-axis Label->BottomLabel->TopLabel` — keeps the end labels losslessly too. */
|
|
1010
|
+
readYAxis(line, after) {
|
|
1011
|
+
const written = after.split("->").map((s) => s.trim());
|
|
1012
|
+
const [label, bottom, top] = written.filter(Boolean).map((part) => this.readText(part));
|
|
1013
|
+
if (!label) return this.keepUnusedConfigLine("y-axis", line, "y-axis has no label");
|
|
1014
|
+
const { yAxisEndLabels: _replaced, ...otherConfig } = this.config;
|
|
1015
|
+
const ends = bottom && top ? [bottom, top] : void 0;
|
|
1016
|
+
this.setConfig("y-axis", {
|
|
1017
|
+
...otherConfig,
|
|
1018
|
+
yAxisLabel: label,
|
|
1019
|
+
...ends ? { yAxisEndLabels: ends } : {}
|
|
1020
|
+
});
|
|
1021
|
+
if (written.length !== 1 && !(written.length === 3 && written.every(Boolean))) {
|
|
1022
|
+
const read = [label, ...ends ?? []].join("->");
|
|
1023
|
+
this.diag(`y-axis takes "Label" or "Label->Bottom->Top" \u2014 read as "${read}"`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
readAnnotationsBox(line, after) {
|
|
1027
|
+
const split = splitAtCoords(after);
|
|
1028
|
+
if (!split) {
|
|
1029
|
+
return this.keep(
|
|
1030
|
+
line,
|
|
1031
|
+
"Annotations box has no position \u2014 not used; add [visibility, maturity]"
|
|
742
1032
|
);
|
|
743
1033
|
}
|
|
1034
|
+
this.reportTextBefore(split.name);
|
|
1035
|
+
readSuffix(split.suffix, NO_SUFFIXES, this.report);
|
|
1036
|
+
const annotationsBoxPosition = this.position(split.coords.a, split.coords.b);
|
|
1037
|
+
this.setConfig("annotations", { ...this.config, annotationsBoxPosition });
|
|
744
1038
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1039
|
+
/** `annotation N <position> [(color …)] <text>`; the position is a tuple or a tuple list. */
|
|
1040
|
+
readAnnotation(line, after) {
|
|
1041
|
+
const numMatch = /^\s*(\d+)/.exec(after);
|
|
1042
|
+
const number = numMatch ? Number(numMatch[1]) : ++this.annotationCounter;
|
|
1043
|
+
if (!Number.isSafeInteger(number)) return this.failed(line);
|
|
1044
|
+
const found = parsePositions(after.replace(/^\s*\d+\s*/, ""));
|
|
1045
|
+
if (!found) {
|
|
1046
|
+
return this.keep(line, "Annotation has no position \u2014 not drawn; add [visibility, maturity]");
|
|
1047
|
+
}
|
|
1048
|
+
if (!found.tuples.length) return this.failed(line);
|
|
1049
|
+
for (const unreadable of found.unreadable) this.diag(unreadableNumberMessage(unreadable));
|
|
1050
|
+
this.reportTextBefore(found.before);
|
|
1051
|
+
const { color, rest } = splitLeadingColor(found.after);
|
|
1052
|
+
this.reportMisplacedSuffix(
|
|
1053
|
+
splitTrailingGroups(rest, { color: true, height: false }).groups,
|
|
1054
|
+
"after the annotation text is read as part of the text",
|
|
1055
|
+
"write it directly after the position"
|
|
1056
|
+
);
|
|
1057
|
+
const text = this.readText(rest.trim());
|
|
1058
|
+
const positions = found.tuples.map((t) => this.position(t.a, t.b));
|
|
1059
|
+
const annotation = {
|
|
1060
|
+
id: this.ids.alloc("anno", String(number)),
|
|
1061
|
+
elementType: "annotation",
|
|
1062
|
+
label: text,
|
|
1063
|
+
position: positions[0],
|
|
1064
|
+
number,
|
|
1065
|
+
positions,
|
|
1066
|
+
text,
|
|
1067
|
+
...color ? { color } : {}
|
|
1068
|
+
};
|
|
1069
|
+
this.addElement(annotation);
|
|
1070
|
+
}
|
|
1071
|
+
/** Project extension (freeform drawing): `line [[v,e], [v,e], …] (closed) (dashed) (color x)`. */
|
|
1072
|
+
readDrawing(line, after) {
|
|
1073
|
+
const list = parseMultiCoords(after);
|
|
1074
|
+
if (!list || list.tuples.length < 2) return this.failed(line);
|
|
1075
|
+
for (const unreadable of list.unreadable) this.diag(unreadableNumberMessage(unreadable));
|
|
1076
|
+
this.reportTextBefore(list.before);
|
|
1077
|
+
const suffix = readSuffix(list.after, DRAWING_SUFFIXES, this.report);
|
|
1078
|
+
const points = list.tuples.map((t) => this.position(t.a, t.b));
|
|
1079
|
+
const drawing = {
|
|
1080
|
+
id: this.ids.alloc("draw", "line"),
|
|
1081
|
+
elementType: "drawing",
|
|
1082
|
+
label: "",
|
|
1083
|
+
position: points[0],
|
|
1084
|
+
points,
|
|
1085
|
+
...suffix.closed ? { closed: true } : {},
|
|
1086
|
+
...suffix.strokeStyle ? { strokeStyle: suffix.strokeStyle } : {},
|
|
1087
|
+
...suffix.color ? { color: suffix.color } : {}
|
|
1088
|
+
};
|
|
1089
|
+
this.addElement(drawing);
|
|
1090
|
+
}
|
|
1091
|
+
/** Canonical OWM form: `<kind> [vis1, mat1, vis2, mat2]` (two corners, normalized). */
|
|
1092
|
+
readAttitude(line, after, kind) {
|
|
1093
|
+
const split = splitAtCoords4(after);
|
|
1094
|
+
if (!split) return this.failed(line);
|
|
1095
|
+
this.reportTextBefore(split.before);
|
|
1096
|
+
const { color } = readSuffix(split.after, ATTITUDE_SUFFIXES, this.report);
|
|
1097
|
+
const { a, b, c, d } = split.coords;
|
|
1098
|
+
const corners = [this.clamp(a), this.clamp(b), this.clamp(c), this.clamp(d)];
|
|
1099
|
+
this.addElement(makeAttitude(this.ids, kind, corners, color));
|
|
1100
|
+
}
|
|
1101
|
+
/** OWM: `url Name [https://…]` — definition, referenced via `url(Name)` on elements. */
|
|
1102
|
+
readUrlDefinition(line, after) {
|
|
1103
|
+
const definition = readUrlDefinition(after);
|
|
1104
|
+
if (!definition) return this.failed(line);
|
|
1105
|
+
this.urlDefinitions.push({ ...definition, lineNo: this.lineNo, text: this.currentLine });
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Legacy OWM: `buy <Name>` (method on an existing component) or `buy <Name> [vis, mat]` (create a
|
|
1109
|
+
* component with a method).
|
|
1110
|
+
*/
|
|
1111
|
+
readMethod(line, after, kw) {
|
|
1112
|
+
if (this.pushComponent(after, kw)) return;
|
|
1113
|
+
if (!after) return this.keep(line, `${kw}: needs a component name \u2014 not used`);
|
|
1114
|
+
this.pendingMethod.push({
|
|
1115
|
+
name: after,
|
|
1116
|
+
method: kw,
|
|
1117
|
+
raw: line,
|
|
1118
|
+
lineNo: this.lineNo,
|
|
1119
|
+
text: this.currentLine
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
readAccelerator(line, after, kw) {
|
|
1123
|
+
const node = parseNode(after, ACCELERATOR_SUFFIXES, this.report);
|
|
1124
|
+
if (!node) return this.failed(line);
|
|
1125
|
+
const label = this.nameElement(node.name, "accelerator");
|
|
1126
|
+
const id = this.ids.alloc("accel", label);
|
|
1127
|
+
const accelerator = {
|
|
1128
|
+
id,
|
|
1129
|
+
elementType: "accelerator",
|
|
1130
|
+
direction: kw === "deaccelerator" ? "deaccelerate" : "accelerate",
|
|
1131
|
+
label,
|
|
1132
|
+
position: this.position(node.coords.a, node.coords.b),
|
|
1133
|
+
...node.color ? { color: node.color } : {}
|
|
1134
|
+
};
|
|
1135
|
+
this.addElement(accelerator);
|
|
1136
|
+
this.register(node.name, label, id);
|
|
1137
|
+
}
|
|
1138
|
+
readSubmap(line, after) {
|
|
1139
|
+
const node = parseNode(after, SUBMAP_SUFFIXES, this.report);
|
|
1140
|
+
if (!node) return this.failed(line);
|
|
1141
|
+
const label = this.nameElement(node.name, "submap");
|
|
1142
|
+
const id = this.ids.alloc("submap", label);
|
|
1143
|
+
const submap = {
|
|
1144
|
+
id,
|
|
1145
|
+
elementType: "submap",
|
|
1146
|
+
label,
|
|
1147
|
+
position: this.position(node.coords.a, node.coords.b),
|
|
1148
|
+
...node.color ? { color: node.color } : {}
|
|
1149
|
+
};
|
|
1150
|
+
this.addElement(submap);
|
|
1151
|
+
if (node.urlRef) this.addUrlRef(node.urlRef);
|
|
1152
|
+
this.register(node.name, label, id);
|
|
1153
|
+
}
|
|
1154
|
+
finish() {
|
|
1155
|
+
const removedFromPassthrough = replacedConfigLines(
|
|
1156
|
+
this.unusedConfigLines,
|
|
1157
|
+
this.config,
|
|
1158
|
+
this.reportAt
|
|
1159
|
+
);
|
|
1160
|
+
this.resolvePipelines();
|
|
1161
|
+
const indexById = new Map(this.elements.map((el, index) => [el.id, index]));
|
|
1162
|
+
this.resolveEvolves(indexById);
|
|
1163
|
+
this.resolveMethods(indexById);
|
|
1164
|
+
const urlReferences = /* @__PURE__ */ new Map();
|
|
1165
|
+
const usedDefinitions = /* @__PURE__ */ new Set();
|
|
1166
|
+
const keptDefinitions = resolveUrlReferences(
|
|
1167
|
+
this.elements,
|
|
1168
|
+
this.pendingUrlRefs,
|
|
1169
|
+
this.urlDefinitions,
|
|
1170
|
+
this.reportAt,
|
|
1171
|
+
(element, definition) => {
|
|
1172
|
+
if (definition) usedDefinitions.add(definition);
|
|
1173
|
+
urlReferences.set(
|
|
1174
|
+
element.id,
|
|
1175
|
+
definition ? { kind: "definition", name: definition.name } : { kind: "embedded" }
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
);
|
|
1179
|
+
for (const definition of keptDefinitions) {
|
|
1180
|
+
this.keepLine(
|
|
1181
|
+
`url ${definition.name} [${definition.address}]`,
|
|
1182
|
+
definition.lineNo,
|
|
1183
|
+
definition.text
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
const declaredNames = /* @__PURE__ */ new Set([...this.idBySourceName.keys(), ...this.idByElementName.keys()]);
|
|
1187
|
+
const edgeLineById = /* @__PURE__ */ new Map();
|
|
1188
|
+
const edges = buildEdges(
|
|
1189
|
+
this.pendingLinks,
|
|
1190
|
+
(name) => this.idOfName(name),
|
|
1191
|
+
(link, missing) => {
|
|
1192
|
+
this.keepLine(link.raw, link.lineNo, link.text);
|
|
1193
|
+
this.diag(`Link: "${missing}" not found`, link.lineNo, link.text);
|
|
1194
|
+
},
|
|
1195
|
+
(link) => link.label && this.readText(link.label, link.lineNo, link.text),
|
|
1196
|
+
(edge, link) => edgeLineById.set(edge.id, link.lineNo)
|
|
1197
|
+
);
|
|
1198
|
+
const kept = this.passthrough.filter((_, index) => !removedFromPassthrough.has(index));
|
|
1199
|
+
const rawPassthrough = withoutClosersOfKeptComments(kept, this.reportAt);
|
|
1200
|
+
const sourceMap = this.sourceMap({
|
|
1201
|
+
removedFromPassthrough,
|
|
1202
|
+
usedDefinitions,
|
|
1203
|
+
urlReferences,
|
|
1204
|
+
edgeLineById,
|
|
1205
|
+
kept
|
|
1206
|
+
});
|
|
1207
|
+
const map = compact({
|
|
1208
|
+
schemaVersion: wardleySchemaModel.CURRENT_SCHEMA_VERSION,
|
|
1209
|
+
config: this.config,
|
|
1210
|
+
elements: this.elements,
|
|
1211
|
+
edges,
|
|
1212
|
+
rawPassthrough: rawPassthrough.length ? rawPassthrough : void 0
|
|
1213
|
+
});
|
|
1214
|
+
return {
|
|
1215
|
+
result: { map: wardleySchemaModel.validateMap(map), diagnostics: this.diagnostics },
|
|
1216
|
+
sourceMap,
|
|
1217
|
+
declaredNames,
|
|
1218
|
+
keywordLinks: this.keywordLinks
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
sourceMap(parts) {
|
|
1222
|
+
const primaryLineOf = /* @__PURE__ */ new Map();
|
|
1223
|
+
const secondaryLinesOf = /* @__PURE__ */ new Map();
|
|
1224
|
+
const addSecondary = (key, line) => {
|
|
1225
|
+
const lines = secondaryLinesOf.get(key) ?? [];
|
|
1226
|
+
lines.push(line);
|
|
1227
|
+
secondaryLinesOf.set(key, lines);
|
|
1228
|
+
};
|
|
1229
|
+
const addRepeated = (key, lines) => {
|
|
1230
|
+
if (!lines?.length) return;
|
|
1231
|
+
primaryLineOf.set(key, lines[lines.length - 1]);
|
|
1232
|
+
for (const line of lines.slice(0, -1)) addSecondary(key, line);
|
|
1233
|
+
};
|
|
1234
|
+
for (const [keyword, lines] of this.configLinesByKeyword) {
|
|
1235
|
+
addRepeated(`config:${keyword}`, lines);
|
|
1236
|
+
}
|
|
1237
|
+
for (const unused of this.unusedConfigLines) {
|
|
1238
|
+
if (parts.removedFromPassthrough.has(unused.index)) {
|
|
1239
|
+
addSecondary(`config:${unused.keyword}`, unused.line);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
for (const definition of parts.usedDefinitions) {
|
|
1243
|
+
primaryLineOf.set(`url:${definition.name}`, definition.lineNo);
|
|
1244
|
+
}
|
|
1245
|
+
for (const el of this.elements) {
|
|
1246
|
+
const isChild = el.elementType === "component" && el.pipelineId !== void 0;
|
|
1247
|
+
const key = `${isChild ? "child" : "element"}:${el.id}`;
|
|
1248
|
+
const line = this.declarationLineById.get(el.id);
|
|
1249
|
+
if (line !== void 0) primaryLineOf.set(key, line);
|
|
1250
|
+
for (const methodLine of this.methodLinesById.get(el.id) ?? []) addSecondary(key, methodLine);
|
|
1251
|
+
addRepeated(`evolve:${el.id}`, this.evolveLinesById.get(el.id));
|
|
1252
|
+
}
|
|
1253
|
+
for (const [id, line] of parts.edgeLineById) primaryLineOf.set(`edge:${id}`, line);
|
|
1254
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
1255
|
+
for (const [pipeline, lines] of this.blockLines) {
|
|
1256
|
+
const id = this.pipelineIds.get(pipeline);
|
|
1257
|
+
if (id !== void 0) blocks.set(id, lines);
|
|
1258
|
+
}
|
|
1259
|
+
return {
|
|
1260
|
+
primaryLineOf,
|
|
1261
|
+
secondaryLinesOf,
|
|
1262
|
+
passthrough: parts.kept.map((entry) => ({ line: entry.lineNo, isComment: entry.isComment })),
|
|
1263
|
+
blocks,
|
|
1264
|
+
rangeFromChildren: this.rangeFromChildren,
|
|
1265
|
+
urlReferences: parts.urlReferences
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* A pipeline pairs with the component written under its name: it takes that component's height,
|
|
1270
|
+
* as the serializer assumes when it leaves out `(y …)`. Block children are not such components.
|
|
1271
|
+
*/
|
|
1272
|
+
resolvePipelines() {
|
|
1273
|
+
const componentByName = /* @__PURE__ */ new Map();
|
|
1274
|
+
for (const el of this.elements) {
|
|
1275
|
+
if (el.elementType === "component") componentByName.set(el.label, el);
|
|
1276
|
+
}
|
|
1277
|
+
for (const p of this.pendingPipeline) {
|
|
1278
|
+
const label = writtenName(p.name, "pipeline");
|
|
1279
|
+
const component = componentByName.get(label);
|
|
1280
|
+
const writtenRange = pipelineRange(p);
|
|
1281
|
+
if (!writtenRange) {
|
|
1282
|
+
this.keepLine(p.raw, p.lineNo, p.text);
|
|
1283
|
+
if (!component) {
|
|
1284
|
+
this.diag(
|
|
1285
|
+
`Pipeline "${p.name}" has no range \u2014 not drawn; add [start, end] or child components`,
|
|
1286
|
+
p.lineNo,
|
|
1287
|
+
p.text
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
for (const message of p.notes) this.diag(message, p.lineNo, p.text);
|
|
1293
|
+
if (label !== p.name) this.diag(renamedMessage(p.name, label), p.lineNo, p.text);
|
|
1294
|
+
const range = widenEmptyRange(writtenRange);
|
|
1295
|
+
const widened = range.start !== writtenRange.start || range.end !== writtenRange.end;
|
|
1296
|
+
if (widened && !writtenRange.fromChildren) {
|
|
1297
|
+
this.diag(
|
|
1298
|
+
`Pipeline "${p.name}" range is empty or reversed \u2014 changed to [${range.start}, ${range.end}]`,
|
|
1299
|
+
p.lineNo,
|
|
1300
|
+
p.text
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
const visibility = p.visibility ?? component?.position.visibility ?? 0.5;
|
|
1304
|
+
const id = this.ids.alloc("pipeline", label);
|
|
1305
|
+
this.pipelineIds.set(p, id);
|
|
1306
|
+
if (writtenRange.fromChildren) this.rangeFromChildren.add(id);
|
|
1307
|
+
if (this.idOfName(p.name) === void 0 && this.idOfName(label) === void 0) {
|
|
1308
|
+
this.register(p.name, label, id);
|
|
1309
|
+
}
|
|
1310
|
+
const childNames = p.children.map(
|
|
1311
|
+
(child) => this.nameElement(child.name, "component", child.lineNo, child.text)
|
|
1312
|
+
);
|
|
1313
|
+
const { pipeline, children } = pipelineElements(
|
|
1314
|
+
{ ...p, name: label },
|
|
1315
|
+
id,
|
|
1316
|
+
visibility,
|
|
1317
|
+
range,
|
|
1318
|
+
this.ids,
|
|
1319
|
+
childNames
|
|
1320
|
+
);
|
|
1321
|
+
this.addElement(pipeline, p.lineNo);
|
|
1322
|
+
children.forEach((child, index) => {
|
|
1323
|
+
this.addElement(child, p.children[index].lineNo);
|
|
1324
|
+
this.register(p.children[index].name, child.label, child.id);
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
componentIndex(name, indexById) {
|
|
1329
|
+
const index = indexById.get(this.idOfName(name) ?? "");
|
|
1330
|
+
return index !== void 0 && this.elements[index].elementType === "component" ? index : -1;
|
|
1331
|
+
}
|
|
1332
|
+
resolveEvolves(indexById) {
|
|
1333
|
+
const evolveLineOf = /* @__PURE__ */ new Map();
|
|
1334
|
+
for (const ev of this.pendingEvolve) {
|
|
1335
|
+
const index = this.componentIndex(ev.name, indexById);
|
|
1336
|
+
if (index < 0) {
|
|
1337
|
+
this.keepLine(ev.raw, ev.lineNo, ev.text);
|
|
1338
|
+
const misplaced = splitMisplacedEvolveSuffix(ev.name);
|
|
1339
|
+
const hint = misplaced && this.componentIndex(misplaced.name, indexById) >= 0 ? misplacedEvolveSuffixHint(misplaced) : "";
|
|
1340
|
+
this.diag(`evolve: component "${ev.name}" not found${hint}`, ev.lineNo, ev.text);
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
for (const message of ev.notes) this.diag(message, ev.lineNo, ev.text);
|
|
1344
|
+
const component = this.elements[index];
|
|
1345
|
+
const earlier = evolveLineOf.get(component.id);
|
|
1346
|
+
if (earlier !== void 0) {
|
|
1347
|
+
const message = `Repeated evolve for "${ev.name}" \u2014 replaces the one on line ${earlier}`;
|
|
1348
|
+
this.diag(message, ev.lineNo, ev.text);
|
|
1349
|
+
}
|
|
1350
|
+
evolveLineOf.set(component.id, ev.lineNo);
|
|
1351
|
+
const evolveLines = this.evolveLinesById.get(component.id) ?? [];
|
|
1352
|
+
evolveLines.push(ev.lineNo);
|
|
1353
|
+
this.evolveLinesById.set(component.id, evolveLines);
|
|
1354
|
+
const movement = compact({
|
|
1355
|
+
targetEvolution: ev.target,
|
|
1356
|
+
newLabel: ev.newLabel && this.readText(ev.newLabel, ev.lineNo, ev.text),
|
|
1357
|
+
method: ev.method,
|
|
1358
|
+
labelOffset: ev.labelOffset
|
|
1359
|
+
});
|
|
1360
|
+
this.elements[index] = { ...component, movement };
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
/** Legacy standalone methods (`buy <name>`). */
|
|
1364
|
+
resolveMethods(indexById) {
|
|
1365
|
+
for (const pm of this.pendingMethod) {
|
|
1366
|
+
const index = this.componentIndex(pm.name, indexById);
|
|
1367
|
+
if (index < 0) {
|
|
1368
|
+
this.keepLine(pm.raw, pm.lineNo, pm.text);
|
|
1369
|
+
this.diag(`${pm.method}: component "${pm.name}" not found`, pm.lineNo, pm.text);
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
const el = this.elements[index];
|
|
1373
|
+
const replaced = el.decorators?.method;
|
|
1374
|
+
if (replaced && replaced !== pm.method) {
|
|
1375
|
+
const message = `${pm.method}: "${pm.name}" already has the method "${replaced}" \u2014 replaced`;
|
|
1376
|
+
this.diag(message, pm.lineNo, pm.text);
|
|
1377
|
+
}
|
|
1378
|
+
this.elements[index] = { ...el, decorators: { ...el.decorators, method: pm.method } };
|
|
1379
|
+
const methodLines = this.methodLinesById.get(el.id) ?? [];
|
|
1380
|
+
methodLines.push(pm.lineNo);
|
|
1381
|
+
this.methodLinesById.set(el.id, methodLines);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
function renamedMessage(sourceName, name) {
|
|
1386
|
+
return `Name "${sourceName}" renamed to "${name}" \u2014 a name cannot hold link operators ("->", "+>", "+<", "+'"), comment markers or a leading "{"`;
|
|
1387
|
+
}
|
|
1388
|
+
function withoutTrailingCarriageReturns(line) {
|
|
1389
|
+
let end = line.length;
|
|
1390
|
+
while (end > 0 && line[end - 1] === "\r") end--;
|
|
1391
|
+
return line.slice(0, end);
|
|
1392
|
+
}
|
|
1393
|
+
function withoutClosersOfKeptComments(entries, report) {
|
|
1394
|
+
const nextParse = new CommentStripper();
|
|
1395
|
+
return entries.map((entry) => {
|
|
1396
|
+
let text = entry.text;
|
|
1397
|
+
if (!entry.isComment && nextParse.insideBlockComment && text.includes("*/")) {
|
|
1398
|
+
text = text.replaceAll("*/", " ");
|
|
1399
|
+
report(
|
|
1400
|
+
'Ignored "*/" \u2014 this line is kept after an unclosed "/*" and would end that comment',
|
|
1401
|
+
entry.lineNo,
|
|
1402
|
+
entry.source
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
nextParse.strip(text);
|
|
1406
|
+
return text;
|
|
768
1407
|
});
|
|
769
1408
|
}
|
|
770
|
-
function
|
|
771
|
-
|
|
1409
|
+
function replacedConfigLines(unusedLines, config, report) {
|
|
1410
|
+
const configIsSet = {
|
|
1411
|
+
evolution: config.evolutionLabels !== void 0,
|
|
1412
|
+
"y-axis": config.yAxisLabel !== void 0
|
|
1413
|
+
};
|
|
1414
|
+
const replacedIndexes = /* @__PURE__ */ new Set();
|
|
1415
|
+
for (const unused of unusedLines) {
|
|
1416
|
+
const replaced = configIsSet[unused.keyword];
|
|
1417
|
+
if (replaced) replacedIndexes.add(unused.index);
|
|
1418
|
+
const outcome = replaced ? `removed; another "${unused.keyword}" line sets it` : "not used";
|
|
1419
|
+
report(`${unused.reason} \u2014 ${outcome}`, unused.line, unused.text);
|
|
1420
|
+
}
|
|
1421
|
+
return replacedIndexes;
|
|
772
1422
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
1423
|
+
function resolveUrlReferences(elements, refs, definitions, report, resolved) {
|
|
1424
|
+
const definitionByName = /* @__PURE__ */ new Map();
|
|
1425
|
+
const replaced = /* @__PURE__ */ new Set();
|
|
1426
|
+
for (const definition of definitions) {
|
|
1427
|
+
const earlier = definitionByName.get(definition.name);
|
|
1428
|
+
if (earlier) {
|
|
1429
|
+
replaced.add(earlier);
|
|
1430
|
+
report(
|
|
1431
|
+
`Repeated url definition "${definition.name}" \u2014 replaces the one on line ${earlier.lineNo}, which is kept unused`,
|
|
1432
|
+
definition.lineNo,
|
|
1433
|
+
definition.text
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
definitionByName.set(definition.name, definition);
|
|
1437
|
+
}
|
|
1438
|
+
const used = /* @__PURE__ */ new Set();
|
|
1439
|
+
for (const { index, ref, line, text } of refs) {
|
|
1440
|
+
const definition = definitionByName.get(ref);
|
|
1441
|
+
if (definition) used.add(definition);
|
|
1442
|
+
const address = definition?.address ?? (/^[a-z][\w+.-]*:\/\//i.test(ref) ? ref : void 0);
|
|
1443
|
+
if (!address) {
|
|
1444
|
+
report(`url(${ref}) has no matching "url ${ref} [address]" line \u2014 ignored`, line, text);
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
const el = elements[index];
|
|
1448
|
+
if (el.elementType === "component") elements[index] = { ...el, url: address };
|
|
1449
|
+
else if (el.elementType === "submap") elements[index] = { ...el, urlRef: address };
|
|
1450
|
+
resolved(el, definition);
|
|
1451
|
+
}
|
|
1452
|
+
return definitions.filter((definition) => replaced.has(definition) || !used.has(definition));
|
|
1453
|
+
}
|
|
1454
|
+
function parseNode(after, suffixes, report, keywordMethod) {
|
|
1455
|
+
const split = splitAtCoords(after);
|
|
1456
|
+
if (!split || !split.name) return null;
|
|
1457
|
+
const suffix = readSuffix(split.suffix, suffixes, report, "after the coordinates", keywordMethod);
|
|
1458
|
+
return compact({
|
|
1459
|
+
name: split.name,
|
|
1460
|
+
coords: split.coords,
|
|
1461
|
+
decorators: suffix.decorators,
|
|
1462
|
+
labelOffset: suffix.labelOffset,
|
|
1463
|
+
urlRef: suffix.urlRef,
|
|
1464
|
+
color: suffix.color
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
var COMPONENT_SUFFIXES = /* @__PURE__ */ new Set(["color", "decorators", "label", "url"]);
|
|
1468
|
+
var PIPELINE_CHILD_SUFFIXES = /* @__PURE__ */ new Set(["color", "decorators", "label"]);
|
|
1469
|
+
var ANCHOR_SUFFIXES = /* @__PURE__ */ new Set(["color", "label"]);
|
|
1470
|
+
var SUBMAP_SUFFIXES = /* @__PURE__ */ new Set(["color", "url"]);
|
|
1471
|
+
var ACCELERATOR_SUFFIXES = /* @__PURE__ */ new Set(["color"]);
|
|
1472
|
+
var NOTE_SUFFIXES = /* @__PURE__ */ new Set(["color"]);
|
|
1473
|
+
var ATTITUDE_SUFFIXES = /* @__PURE__ */ new Set(["color"]);
|
|
1474
|
+
var PIPELINE_SUFFIXES = /* @__PURE__ */ new Set(["color", "height"]);
|
|
1475
|
+
var DRAWING_SUFFIXES = /* @__PURE__ */ new Set(["color", "stroke"]);
|
|
1476
|
+
var EVOLVE_SUFFIXES = /* @__PURE__ */ new Set(["method"]);
|
|
1477
|
+
var NO_SUFFIXES = /* @__PURE__ */ new Set();
|
|
1478
|
+
function readSuffix(text, slots, report, place = "after the coordinates", keywordMethod) {
|
|
1479
|
+
const reading = {};
|
|
1480
|
+
const decorators = keywordMethod ? { method: keywordMethod } : {};
|
|
1481
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1482
|
+
const ignored = [];
|
|
1483
|
+
const readGroupItem = (item) => {
|
|
1484
|
+
if (slots.has("decorators") && (item === "market" || item === "ecosystem" || item === "inertia")) {
|
|
1485
|
+
decorators[item] = true;
|
|
1486
|
+
return true;
|
|
1487
|
+
}
|
|
1488
|
+
if ((slots.has("decorators") || slots.has("method")) && METHODS.has(item)) {
|
|
1489
|
+
decorators.method ??= item;
|
|
1490
|
+
return decorators.method === item;
|
|
1491
|
+
}
|
|
1492
|
+
if (slots.has("stroke") && item === "closed") {
|
|
1493
|
+
reading.closed = true;
|
|
1494
|
+
return true;
|
|
1495
|
+
}
|
|
1496
|
+
if (slots.has("stroke") && (item === "dashed" || item === "dotted")) {
|
|
1497
|
+
reading.strokeStyle ??= item;
|
|
1498
|
+
return reading.strokeStyle === item;
|
|
1499
|
+
}
|
|
1500
|
+
return false;
|
|
1501
|
+
};
|
|
1502
|
+
const firstOf = (token, slot) => {
|
|
1503
|
+
if (!slots.has(slot) || seen.has(token.kind)) return false;
|
|
1504
|
+
seen.add(token.kind);
|
|
1505
|
+
return true;
|
|
1506
|
+
};
|
|
1507
|
+
for (const token of scanSuffix(text)) {
|
|
1508
|
+
switch (token.kind) {
|
|
1509
|
+
case "color":
|
|
1510
|
+
if (firstOf(token, "color")) reading.color = token.color;
|
|
1511
|
+
else ignored.push(token);
|
|
1512
|
+
break;
|
|
1513
|
+
case "height":
|
|
1514
|
+
if (firstOf(token, "height")) {
|
|
1515
|
+
const height = readUnclampedNumber(token.value);
|
|
1516
|
+
if (height === null) report(unreadableNumberMessage(token.text));
|
|
1517
|
+
else reading.height = height;
|
|
1518
|
+
} else ignored.push(token);
|
|
1519
|
+
break;
|
|
1520
|
+
case "label":
|
|
1521
|
+
if (firstOf(token, "label")) {
|
|
1522
|
+
const dx = readUnclampedNumber(token.dx);
|
|
1523
|
+
const dy = readUnclampedNumber(token.dy);
|
|
1524
|
+
if (dx === null || dy === null) report(unreadableNumberMessage(token.text));
|
|
1525
|
+
else reading.labelOffset = { dx, dy };
|
|
1526
|
+
} else ignored.push(token);
|
|
1527
|
+
break;
|
|
1528
|
+
case "url":
|
|
1529
|
+
if (token.ref && firstOf(token, "url")) reading.urlRef = token.ref;
|
|
1530
|
+
else ignored.push(token);
|
|
1531
|
+
break;
|
|
1532
|
+
case "inertia":
|
|
1533
|
+
if (slots.has("decorators")) decorators.inertia = true;
|
|
1534
|
+
else ignored.push(token);
|
|
1535
|
+
break;
|
|
1536
|
+
case "group": {
|
|
1537
|
+
const unfit = token.items.filter((item) => !readGroupItem(item.text.toLowerCase()));
|
|
1538
|
+
if (unfit.length === token.items.length) ignored.push(token);
|
|
1539
|
+
else for (const item of unfit) ignored.push(item);
|
|
1540
|
+
break;
|
|
1541
|
+
}
|
|
1542
|
+
case "word":
|
|
1543
|
+
ignored.push(token);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (ignored.length) report(`Ignored text ${place}: ${quotedSpans(text, ignored)}`);
|
|
1547
|
+
return compact({ ...reading, decorators });
|
|
1548
|
+
}
|
|
1549
|
+
function quotedSpans(text, spans) {
|
|
1550
|
+
const pieces = [];
|
|
1551
|
+
let start = spans[0].at;
|
|
1552
|
+
let end = start + spans[0].text.length;
|
|
1553
|
+
for (let i = 1; i < spans.length; i++) {
|
|
1554
|
+
const span = spans[i];
|
|
1555
|
+
if (text.slice(end, span.at).trim() !== "") {
|
|
1556
|
+
pieces.push(text.slice(start, end));
|
|
1557
|
+
start = span.at;
|
|
1558
|
+
}
|
|
1559
|
+
end = span.at + span.text.length;
|
|
1560
|
+
}
|
|
1561
|
+
pieces.push(text.slice(start, end));
|
|
1562
|
+
return pieces.map((piece) => `"${piece}"`).join(", ");
|
|
1563
|
+
}
|
|
1564
|
+
function splitPipelineLine(body) {
|
|
1565
|
+
const kinds = { color: true, height: true };
|
|
1566
|
+
const split = splitAtTuple(body);
|
|
1567
|
+
if (split) {
|
|
1568
|
+
return {
|
|
1569
|
+
name: split.before,
|
|
1570
|
+
range: { tuple: split.tuple, coords: split.coords },
|
|
1571
|
+
suffixText: split.after,
|
|
1572
|
+
misplaced: splitTrailingGroups(split.before, kinds).groups
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
const { rest, groups } = splitTrailingGroups(body, kinds);
|
|
1576
|
+
return { name: rest, range: null, suffixText: groups.join(" "), misplaced: [] };
|
|
1577
|
+
}
|
|
1578
|
+
function unreadableNumberMessage(text) {
|
|
1579
|
+
return `Unreadable number in "${text}" \u2014 ignored`;
|
|
1580
|
+
}
|
|
1581
|
+
function pipelineRange(p) {
|
|
1582
|
+
if (p.start !== void 0 && p.end !== void 0) {
|
|
1583
|
+
return { start: roundAsWritten(p.start), end: roundAsWritten(p.end) };
|
|
1584
|
+
}
|
|
1585
|
+
const [first, ...others] = p.children;
|
|
1586
|
+
if (!first) return null;
|
|
1587
|
+
let start = first.maturity;
|
|
1588
|
+
let end = first.maturity;
|
|
1589
|
+
for (const child of others) {
|
|
1590
|
+
start = Math.min(start, child.maturity);
|
|
1591
|
+
end = Math.max(end, child.maturity);
|
|
1592
|
+
}
|
|
1593
|
+
return { start: roundAsWritten(start), end: roundAsWritten(end), fromChildren: true };
|
|
1594
|
+
}
|
|
1595
|
+
function widenEmptyRange({ start, end }) {
|
|
1596
|
+
if (end > start) return { start, end };
|
|
1597
|
+
const widenedStart = start >= 1 ? 0.95 : start;
|
|
1598
|
+
return { start: widenedStart, end: roundAsWritten(Math.min(1, widenedStart + 0.05)) };
|
|
1599
|
+
}
|
|
1600
|
+
function pipelineElements(p, id, visibility, range, ids, childNames) {
|
|
1601
|
+
const children = p.children.map(
|
|
1602
|
+
(c, index) => compact({
|
|
1603
|
+
id: ids.alloc("cmp", childNames[index]),
|
|
1604
|
+
elementType: "component",
|
|
1605
|
+
label: childNames[index],
|
|
1606
|
+
position: { visibility, evolution: c.maturity },
|
|
1607
|
+
labelOffset: c.labelOffset,
|
|
1608
|
+
decorators: Object.keys(c.decorators).length ? c.decorators : void 0,
|
|
1609
|
+
pipelineId: id,
|
|
1610
|
+
color: c.color
|
|
1611
|
+
})
|
|
1612
|
+
);
|
|
1613
|
+
const pipeline = {
|
|
1614
|
+
id,
|
|
1615
|
+
elementType: "pipeline",
|
|
1616
|
+
label: p.name,
|
|
1617
|
+
position: { visibility, evolution: (range.start + range.end) / 2 },
|
|
1618
|
+
evolutionStart: range.start,
|
|
1619
|
+
evolutionEnd: range.end,
|
|
1620
|
+
childIds: children.map((child) => child.id),
|
|
1621
|
+
...p.color ? { color: p.color } : {}
|
|
1622
|
+
};
|
|
1623
|
+
return { pipeline, children };
|
|
1624
|
+
}
|
|
1625
|
+
function parseBlockChild(after, report) {
|
|
1626
|
+
const split = splitAtFirstChildTuple(after);
|
|
1627
|
+
if (!split?.before || split.values.length !== 1) return null;
|
|
1628
|
+
const maturity = Number(split.values[0]);
|
|
778
1629
|
if (Number.isNaN(maturity)) return null;
|
|
779
|
-
const
|
|
780
|
-
if (!name) return null;
|
|
781
|
-
const suffix = after.slice(m.index + m[0].length);
|
|
782
|
-
const col = parseColor(suffix);
|
|
783
|
-
const lo = parseLabelOffset(col.rest);
|
|
784
|
-
const dec = parseDecorators(lo.rest);
|
|
1630
|
+
const suffix = readSuffix(split.after, PIPELINE_CHILD_SUFFIXES, report);
|
|
785
1631
|
return compact({
|
|
786
|
-
name,
|
|
1632
|
+
name: split.before,
|
|
787
1633
|
maturity,
|
|
788
|
-
decorators:
|
|
789
|
-
labelOffset:
|
|
790
|
-
color:
|
|
1634
|
+
decorators: suffix.decorators,
|
|
1635
|
+
labelOffset: suffix.labelOffset,
|
|
1636
|
+
color: suffix.color
|
|
791
1637
|
});
|
|
792
1638
|
}
|
|
793
|
-
function makeAttitude(ids, kind, v1, m1, v2, m2, color) {
|
|
1639
|
+
function makeAttitude(ids, kind, [v1, m1, v2, m2], color) {
|
|
794
1640
|
return {
|
|
795
1641
|
id: ids.alloc("attitude", kind),
|
|
796
1642
|
elementType: "attitude",
|
|
@@ -798,12 +1644,12 @@ function makeAttitude(ids, kind, v1, m1, v2, m2, color) {
|
|
|
798
1644
|
label: "",
|
|
799
1645
|
...color ? { color } : {},
|
|
800
1646
|
position: {
|
|
801
|
-
visibility:
|
|
802
|
-
evolution:
|
|
1647
|
+
visibility: Math.max(v1, v2),
|
|
1648
|
+
evolution: Math.min(m1, m2)
|
|
803
1649
|
},
|
|
804
1650
|
corner2: {
|
|
805
|
-
visibility:
|
|
806
|
-
evolution:
|
|
1651
|
+
visibility: Math.min(v1, v2),
|
|
1652
|
+
evolution: Math.max(m1, m2)
|
|
807
1653
|
}
|
|
808
1654
|
};
|
|
809
1655
|
}
|
|
@@ -811,18 +1657,36 @@ function mergeLegacy(kw, dec) {
|
|
|
811
1657
|
const merged = { ...dec };
|
|
812
1658
|
if (kw === "market") merged["market"] = true;
|
|
813
1659
|
if (kw === "ecosystem") merged["ecosystem"] = true;
|
|
1660
|
+
if (METHODS.has(kw)) merged["method"] = kw;
|
|
814
1661
|
return compact(merged);
|
|
815
1662
|
}
|
|
816
|
-
|
|
1663
|
+
var MISPLACED_EVOLVE_SUFFIX_RE = /(?:\((build|buy|outsource)\)|\binertia)$/i;
|
|
1664
|
+
function splitMisplacedEvolveSuffix(name) {
|
|
1665
|
+
const m = MISPLACED_EVOLVE_SUFFIX_RE.exec(name);
|
|
1666
|
+
if (!m) return null;
|
|
1667
|
+
const rest = name.slice(0, m.index).trimEnd();
|
|
1668
|
+
return rest ? { name: rest, suffix: m[0] } : null;
|
|
1669
|
+
}
|
|
1670
|
+
function misplacedEvolveSuffixHint({ name, suffix }) {
|
|
1671
|
+
return suffix.startsWith("(") ? ` \u2014 for component "${name}", write "${suffix}" after the target` : ` \u2014 for component "${name}", write "${suffix}" on its component line`;
|
|
1672
|
+
}
|
|
1673
|
+
var NUMBER_TOKEN_RE = /(^|\s)([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[-+]?\d+)?)(?=\s|\(|$)/gi;
|
|
1674
|
+
function parseEvolve(after, report) {
|
|
817
1675
|
const lo = parseLabelOffset(after);
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
if (
|
|
821
|
-
const
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
if (
|
|
1676
|
+
let targetMatch;
|
|
1677
|
+
for (const match of lo.rest.matchAll(NUMBER_TOKEN_RE)) targetMatch = match;
|
|
1678
|
+
if (!targetMatch) return null;
|
|
1679
|
+
const targetAt = targetMatch.index + targetMatch[1].length;
|
|
1680
|
+
const namePart = lo.rest.slice(0, targetAt).trim();
|
|
1681
|
+
const target = Number(targetMatch[2]);
|
|
1682
|
+
if (!namePart || !Number.isFinite(target)) return null;
|
|
1683
|
+
if (lo.unreadable) report(unreadableNumberMessage(lo.unreadable));
|
|
1684
|
+
const tail = readSuffix(
|
|
1685
|
+
lo.rest.slice(targetAt + targetMatch[2].length),
|
|
1686
|
+
EVOLVE_SUFFIXES,
|
|
1687
|
+
report,
|
|
1688
|
+
"after the evolve target"
|
|
1689
|
+
);
|
|
826
1690
|
const renameIdx = namePart.indexOf("->");
|
|
827
1691
|
const name = renameIdx >= 0 ? namePart.slice(0, renameIdx).trim() : namePart;
|
|
828
1692
|
const newLabel = renameIdx >= 0 ? namePart.slice(renameIdx + 2).trim() : void 0;
|
|
@@ -830,10 +1694,21 @@ function parseEvolve(after) {
|
|
|
830
1694
|
name,
|
|
831
1695
|
newLabel: newLabel || void 0,
|
|
832
1696
|
target,
|
|
833
|
-
method:
|
|
1697
|
+
method: tail.decorators.method,
|
|
834
1698
|
labelOffset: lo.labelOffset ?? void 0
|
|
835
1699
|
});
|
|
836
1700
|
}
|
|
1701
|
+
function sizeOutsideLimits(width, height) {
|
|
1702
|
+
const below = width < wardleySchemaModel.PLOT_MIN.width || height < wardleySchemaModel.PLOT_MIN.height;
|
|
1703
|
+
const above = width > wardleySchemaModel.PLOT_MAX.width || height > wardleySchemaModel.PLOT_MAX.height;
|
|
1704
|
+
if (!below && !above) return null;
|
|
1705
|
+
const drawnWidth = wardleySchemaModel.drawnPlotLength(width, "width");
|
|
1706
|
+
const drawnHeight = wardleySchemaModel.drawnPlotLength(height, "height");
|
|
1707
|
+
const minimum = `${wardleySchemaModel.PLOT_MIN.width}\xD7${wardleySchemaModel.PLOT_MIN.height}`;
|
|
1708
|
+
const maximum = `${wardleySchemaModel.PLOT_MAX.width}\xD7${wardleySchemaModel.PLOT_MAX.height}`;
|
|
1709
|
+
const limit = below && above ? `outside ${minimum} to ${maximum}` : below ? `below the minimum ${minimum}` : `above the maximum ${maximum}`;
|
|
1710
|
+
return `Map size ${width}\xD7${height} is ${limit} \u2014 drawn at ${drawnWidth}\xD7${drawnHeight}`;
|
|
1711
|
+
}
|
|
837
1712
|
|
|
838
1713
|
// src/serializer.ts
|
|
839
1714
|
function urlOf(el) {
|
|
@@ -842,26 +1717,59 @@ function urlOf(el) {
|
|
|
842
1717
|
return void 0;
|
|
843
1718
|
}
|
|
844
1719
|
function r(n) {
|
|
845
|
-
return String(
|
|
846
|
-
}
|
|
847
|
-
var NAMED_TYPES = /* @__PURE__ */ new Set(["anchor", "component", "accelerator", "submap"]);
|
|
848
|
-
function defaultName(type) {
|
|
849
|
-
return type === "anchor" ? "User" : type === "accelerator" ? "Accelerator" : type === "submap" ? "Submap" : "Component";
|
|
1720
|
+
return String(roundAsWritten(n));
|
|
850
1721
|
}
|
|
851
1722
|
function uniqueNames(map) {
|
|
852
|
-
const
|
|
1723
|
+
const namesInUse = new UniqueNames();
|
|
853
1724
|
const byId = /* @__PURE__ */ new Map();
|
|
854
1725
|
for (const el of map.elements) {
|
|
855
|
-
if (
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
used.add(name);
|
|
861
|
-
byId.set(el.id, name);
|
|
1726
|
+
if (NAMED_TYPES.has(el.elementType)) {
|
|
1727
|
+
byId.set(el.id, namesInUse.claim(writtenName(el.label, el.elementType)));
|
|
1728
|
+
} else if (el.elementType === "pipeline") {
|
|
1729
|
+
byId.set(el.id, writtenName(el.label, el.elementType));
|
|
1730
|
+
}
|
|
862
1731
|
}
|
|
863
1732
|
return byId;
|
|
864
1733
|
}
|
|
1734
|
+
var EMBEDDABLE_ADDRESS_RE = /^[a-z][\w+.-]*:\/\/[^\s()]*$/i;
|
|
1735
|
+
function planUrls(map, nameOf, keptDefinitionNames, hints) {
|
|
1736
|
+
const namesInUse = new UniqueNames();
|
|
1737
|
+
for (const name of keptDefinitionNames) namesInUse.claim(name);
|
|
1738
|
+
const addressByHintedName = /* @__PURE__ */ new Map();
|
|
1739
|
+
const definitionNameById = /* @__PURE__ */ new Map();
|
|
1740
|
+
const referenceById = /* @__PURE__ */ new Map();
|
|
1741
|
+
for (const el of map.elements) {
|
|
1742
|
+
const address = urlOf(el);
|
|
1743
|
+
const hint = hints?.get(el.id);
|
|
1744
|
+
if (!address || !hint) continue;
|
|
1745
|
+
if (hint.kind === "embedded") {
|
|
1746
|
+
if (EMBEDDABLE_ADDRESS_RE.test(address)) referenceById.set(el.id, address);
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
const hintedAddress = addressByHintedName.get(hint.name);
|
|
1750
|
+
if (hintedAddress === void 0) {
|
|
1751
|
+
addressByHintedName.set(hint.name, address);
|
|
1752
|
+
namesInUse.reserve(hint.name);
|
|
1753
|
+
} else if (hintedAddress !== address) continue;
|
|
1754
|
+
definitionNameById.set(el.id, hint.name);
|
|
1755
|
+
}
|
|
1756
|
+
const definitions = [];
|
|
1757
|
+
const written = /* @__PURE__ */ new Set();
|
|
1758
|
+
for (const el of map.elements) {
|
|
1759
|
+
const address = urlOf(el);
|
|
1760
|
+
if (!address || referenceById.has(el.id)) continue;
|
|
1761
|
+
let name = definitionNameById.get(el.id);
|
|
1762
|
+
if (name === void 0) {
|
|
1763
|
+
const readable = nameOf(el).replace(/[()[\]<>]/g, " ").replace(/\s+/g, " ").trim();
|
|
1764
|
+
name = namesInUse.claim(`${readable} URL`.trim());
|
|
1765
|
+
}
|
|
1766
|
+
referenceById.set(el.id, name);
|
|
1767
|
+
if (written.has(name)) continue;
|
|
1768
|
+
written.add(name);
|
|
1769
|
+
definitions.push({ name, address });
|
|
1770
|
+
}
|
|
1771
|
+
return { definitions, referenceById };
|
|
1772
|
+
}
|
|
865
1773
|
function offsetSuffix(lo) {
|
|
866
1774
|
return lo ? ` label [${r(lo.dx)}, ${r(lo.dy)}]` : "";
|
|
867
1775
|
}
|
|
@@ -879,34 +1787,83 @@ function decoratorSuffix(dec) {
|
|
|
879
1787
|
if (dec.inertia) out += " inertia";
|
|
880
1788
|
return out;
|
|
881
1789
|
}
|
|
882
|
-
function
|
|
1790
|
+
function configLines(config) {
|
|
1791
|
+
const lines = [{ key: "config:title", text: `title ${writtenText(config.title)}` }];
|
|
1792
|
+
const add = (keyword, text) => lines.push({ key: `config:${keyword}`, text });
|
|
1793
|
+
if (config.style) add("style", `style ${config.style}`);
|
|
1794
|
+
if (config.size) add("size", `size [${r(config.size.width)}, ${r(config.size.height)}]`);
|
|
1795
|
+
if (config.evolutionLabels) {
|
|
1796
|
+
add("evolution", `evolution ${config.evolutionLabels.map(writtenText).join("->")}`);
|
|
1797
|
+
}
|
|
1798
|
+
if (config.yAxisLabel) {
|
|
1799
|
+
const ends = config.yAxisEndLabels;
|
|
1800
|
+
const endLabels = ends ? `->${writtenText(ends[0])}->${writtenText(ends[1])}` : "";
|
|
1801
|
+
add("y-axis", `y-axis ${writtenText(config.yAxisLabel)}${endLabels}`);
|
|
1802
|
+
}
|
|
1803
|
+
if (config.annotationsBoxPosition) {
|
|
1804
|
+
const b = config.annotationsBoxPosition;
|
|
1805
|
+
add("annotations", `annotations [${r(b.visibility)}, ${r(b.evolution)}]`);
|
|
1806
|
+
}
|
|
1807
|
+
return lines;
|
|
1808
|
+
}
|
|
1809
|
+
function passthroughLines(map) {
|
|
1810
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
1811
|
+
if (map.config.evolutionLabels) emitted.add("evolution");
|
|
1812
|
+
if (map.config.yAxisLabel) emitted.add("y-axis");
|
|
1813
|
+
const comments = new CommentStripper();
|
|
883
1814
|
const lines = [];
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
lines.push(`
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
lines.push(`annotations [${r(b.visibility)}, ${r(b.evolution)}]`);
|
|
898
|
-
}
|
|
899
|
-
const urlDefNames = /* @__PURE__ */ new Map();
|
|
1815
|
+
const urlDefinitionNames = [];
|
|
1816
|
+
(map.rawPassthrough ?? []).forEach((raw, index) => {
|
|
1817
|
+
const code = comments.strip(raw).code;
|
|
1818
|
+
const keyword = keywordOf(code);
|
|
1819
|
+
if (emitted.has(keyword)) return;
|
|
1820
|
+
lines.push({ key: `raw:${index}`, text: raw });
|
|
1821
|
+
const definition = keyword === "url" ? readUrlDefinition(code.trim().slice(3)) : null;
|
|
1822
|
+
if (definition) urlDefinitionNames.push(definition.name);
|
|
1823
|
+
});
|
|
1824
|
+
return { lines, urlDefinitionNames };
|
|
1825
|
+
}
|
|
1826
|
+
function pipelineHeights(map, pipelineIds, nameOf) {
|
|
1827
|
+
const componentVisibilityByName = /* @__PURE__ */ new Map();
|
|
900
1828
|
for (const el of map.elements) {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1829
|
+
if (el.elementType !== "component") continue;
|
|
1830
|
+
const isChild = !!el.pipelineId && pipelineIds.has(el.pipelineId);
|
|
1831
|
+
if (!isChild) componentVisibilityByName.set(nameOf(el), el.position.visibility);
|
|
1832
|
+
}
|
|
1833
|
+
return (el) => {
|
|
1834
|
+
if (el.elementType !== "pipeline") return "";
|
|
1835
|
+
const derived = componentVisibilityByName.get(nameOf(el)) ?? 0.5;
|
|
1836
|
+
const written = r(el.position.visibility);
|
|
1837
|
+
return written !== r(derived) ? ` (y ${written})` : "";
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
function edgeLines(map, nameOf) {
|
|
1841
|
+
const namesById = new Map(map.elements.map((el) => [el.id, nameOf(el)]));
|
|
1842
|
+
return map.edges.map((edge) => {
|
|
1843
|
+
const from = namesById.get(edge.from) ?? edge.from;
|
|
1844
|
+
const to = namesById.get(edge.to) ?? edge.to;
|
|
1845
|
+
const annotation = edge.label ? `; ${writtenText(edge.label)}` : "";
|
|
1846
|
+
const key = `edge:${edge.id}`;
|
|
1847
|
+
if (edge.edgeType === "dependency") return { key, text: `${from} -> ${to}${annotation}` };
|
|
1848
|
+
const op = edge.flowValue ? `+'${edge.flowValue}'${edge.bidirectional ? "<>" : ">"}` : edge.bidirectional ? "+<>" : "+>";
|
|
1849
|
+
return { key, text: `${from} ${op} ${to}${annotation}` };
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
function serializeDSL(map) {
|
|
1853
|
+
return [...serializeKeyedLines(map).map((line) => line.text), ""].join("\n");
|
|
1854
|
+
}
|
|
1855
|
+
function serializeKeyedLines(map, hints = {}) {
|
|
1856
|
+
const names = uniqueNames(map);
|
|
1857
|
+
const nameOf = (el) => names.get(el.id) ?? writtenText(el.label);
|
|
1858
|
+
const passthrough = passthroughLines(map);
|
|
1859
|
+
const lines = configLines(map.config);
|
|
1860
|
+
const urls = planUrls(map, nameOf, passthrough.urlDefinitionNames, hints.urlReferences);
|
|
1861
|
+
for (const { name, address } of urls.definitions) {
|
|
1862
|
+
lines.push({ key: `url:${name}`, text: `url ${name} [${address}]` });
|
|
906
1863
|
}
|
|
907
1864
|
const urlSuffix = (el) => {
|
|
908
|
-
const
|
|
909
|
-
return
|
|
1865
|
+
const reference = urls.referenceById.get(el.id);
|
|
1866
|
+
return reference ? ` url(${reference})` : "";
|
|
910
1867
|
};
|
|
911
1868
|
const pipelineIds = new Set(
|
|
912
1869
|
map.elements.filter((e) => e.elementType === "pipeline").map((e) => e.id)
|
|
@@ -920,61 +1877,32 @@ function serializeDSL(map) {
|
|
|
920
1877
|
list.push(el);
|
|
921
1878
|
childrenByPipeline.set(el.pipelineId, list);
|
|
922
1879
|
}
|
|
923
|
-
const
|
|
1880
|
+
const pipelineHeight = pipelineHeights(map, pipelineIds, nameOf);
|
|
924
1881
|
const evolveLines = [];
|
|
925
|
-
const componentVisByLabel = /* @__PURE__ */ new Map();
|
|
926
|
-
for (const el of map.elements) {
|
|
927
|
-
if (el.elementType === "component" && !componentVisByLabel.has(el.label)) {
|
|
928
|
-
componentVisByLabel.set(el.label, el.position.visibility);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
const pipelineYSuffix = (el) => {
|
|
932
|
-
if (el.elementType !== "pipeline") return "";
|
|
933
|
-
const derived = componentVisByLabel.get(el.label) ?? 0.5;
|
|
934
|
-
return Math.abs(el.position.visibility - derived) > 5e-4 ? ` (y ${r(el.position.visibility)})` : "";
|
|
935
|
-
};
|
|
936
1882
|
for (const el of map.elements) {
|
|
937
1883
|
if (el.elementType === "component" && el.movement) {
|
|
938
|
-
evolveLines.push(evolveLine(el, nameOf(el)));
|
|
939
|
-
}
|
|
940
|
-
if (isPipelineChild(el)) continue;
|
|
941
|
-
lines.push(elementLine(el, nameOf(el)) + pipelineYSuffix(el) + urlSuffix(el));
|
|
942
|
-
if (el.elementType === "pipeline") {
|
|
943
|
-
const kids = childrenByPipeline.get(el.id) ?? [];
|
|
944
|
-
if (kids.length) {
|
|
945
|
-
lines.push("{");
|
|
946
|
-
for (const k of kids) {
|
|
947
|
-
lines.push(
|
|
948
|
-
` component ${nameOf(k)} [${r(k.position.evolution)}]${decoratorSuffix(k.decorators)}${colorSuffix(k)}${offsetSuffix(k.labelOffset)}`
|
|
949
|
-
);
|
|
950
|
-
}
|
|
951
|
-
lines.push("}");
|
|
952
|
-
}
|
|
1884
|
+
evolveLines.push({ key: `evolve:${el.id}`, text: evolveLine(el, nameOf(el)) });
|
|
953
1885
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
const labelsById = new Map(map.elements.map((el) => [el.id, el.label]));
|
|
957
|
-
for (const edge of map.edges) {
|
|
958
|
-
const from = names.get(edge.from) ?? labelsById.get(edge.from) ?? edge.from;
|
|
959
|
-
const to = names.get(edge.to) ?? labelsById.get(edge.to) ?? edge.to;
|
|
960
|
-
const annotation = edge.label ? `; ${edge.label}` : "";
|
|
961
|
-
if (edge.edgeType === "dependency") {
|
|
962
|
-
lines.push(`${from} -> ${to}${annotation}`);
|
|
963
|
-
} else {
|
|
964
|
-
const op = edge.flowValue ? `+'${edge.flowValue}'${edge.bidirectional ? "<>" : ">"}` : edge.bidirectional ? "+<>" : "+>";
|
|
965
|
-
lines.push(`${from} ${op} ${to}${annotation}`);
|
|
1886
|
+
if (el.elementType === "component" && el.pipelineId && pipelineIds.has(el.pipelineId)) {
|
|
1887
|
+
continue;
|
|
966
1888
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1889
|
+
lines.push({
|
|
1890
|
+
key: `element:${el.id}`,
|
|
1891
|
+
text: elementLine(el, nameOf(el)) + pipelineHeight(el) + urlSuffix(el)
|
|
1892
|
+
});
|
|
1893
|
+
const kids = childrenByPipeline.get(el.id) ?? [];
|
|
1894
|
+
if (kids.length) {
|
|
1895
|
+
lines.push({ key: `block-open:${el.id}`, text: "{" });
|
|
1896
|
+
for (const k of kids) {
|
|
1897
|
+
lines.push({
|
|
1898
|
+
key: `child:${k.id}`,
|
|
1899
|
+
text: ` component ${nameOf(k)} [${r(k.position.evolution)}]${decoratorSuffix(k.decorators)}${colorSuffix(k)}${offsetSuffix(k.labelOffset)}`
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
lines.push({ key: `block-close:${el.id}`, text: "}" });
|
|
975
1903
|
}
|
|
976
1904
|
}
|
|
977
|
-
return lines
|
|
1905
|
+
return [...lines, ...evolveLines, ...edgeLines(map, nameOf), ...passthrough.lines];
|
|
978
1906
|
}
|
|
979
1907
|
function elementLine(el, name) {
|
|
980
1908
|
const p = el.position;
|
|
@@ -984,14 +1912,14 @@ function elementLine(el, name) {
|
|
|
984
1912
|
case "component":
|
|
985
1913
|
return `component ${name} [${r(p.visibility)}, ${r(p.evolution)}]${decoratorSuffix(el.decorators)}${colorSuffix(el)}${offsetSuffix(el.labelOffset)}`;
|
|
986
1914
|
case "note":
|
|
987
|
-
return `note ${
|
|
1915
|
+
return `note ${writtenNoteText(el.label)} [${r(p.visibility)}, ${r(p.evolution)}]${colorSuffix(el)}`;
|
|
988
1916
|
case "pipeline":
|
|
989
1917
|
return `pipeline ${name} [${r(el.evolutionStart)}, ${r(el.evolutionEnd)}]${colorSuffix(el)}`;
|
|
990
1918
|
case "submap":
|
|
991
1919
|
return `submap ${name} [${r(p.visibility)}, ${r(p.evolution)}]${colorSuffix(el)}`;
|
|
992
1920
|
case "annotation": {
|
|
993
1921
|
const pos = el.positions.length > 1 ? `[${el.positions.map((q) => `[${r(q.visibility)}, ${r(q.evolution)}]`).join(", ")}]` : `[${r(p.visibility)}, ${r(p.evolution)}]`;
|
|
994
|
-
return `annotation ${el.number} ${pos}${colorSuffix(el)} ${el.text}`;
|
|
1922
|
+
return `annotation ${el.number} ${pos}${colorSuffix(el)} ${writtenText(el.text)}`;
|
|
995
1923
|
}
|
|
996
1924
|
case "accelerator":
|
|
997
1925
|
return `${el.direction === "deaccelerate" ? "deaccelerator" : "accelerator"} ${name} [${r(p.visibility)}, ${r(p.evolution)}]${colorSuffix(el)}`;
|
|
@@ -1007,11 +1935,576 @@ function elementLine(el, name) {
|
|
|
1007
1935
|
}
|
|
1008
1936
|
function evolveLine(el, name) {
|
|
1009
1937
|
const mv = el.movement;
|
|
1010
|
-
const label = mv.newLabel ? `${name}->${mv.newLabel}` : name;
|
|
1938
|
+
const label = mv.newLabel ? `${name}->${writtenText(mv.newLabel)}` : name;
|
|
1011
1939
|
const method = mv.method ? ` (${mv.method})` : "";
|
|
1012
1940
|
return `evolve ${label} ${r(mv.targetEvolution)}${method}${offsetSuffix(mv.labelOffset)}`;
|
|
1013
1941
|
}
|
|
1014
1942
|
|
|
1943
|
+
// src/patch.ts
|
|
1944
|
+
function patchDSL(originalText, map, options = {}) {
|
|
1945
|
+
return patchDSLWithOutcome(originalText, map, options).text;
|
|
1946
|
+
}
|
|
1947
|
+
function patchDSLWithOutcome(originalText, map, options = {}) {
|
|
1948
|
+
const original = typeof originalText === "string" ? originalText : "";
|
|
1949
|
+
let canonical;
|
|
1950
|
+
try {
|
|
1951
|
+
canonical = serializeDSL(map);
|
|
1952
|
+
} catch {
|
|
1953
|
+
return { text: original, outcome: "unchanged" };
|
|
1954
|
+
}
|
|
1955
|
+
try {
|
|
1956
|
+
const expected = fingerprint(parseDSL(canonical));
|
|
1957
|
+
const source = readSource(original);
|
|
1958
|
+
const edited = alignToSource(source.map, map, options.previous);
|
|
1959
|
+
for (const mode of ["minimal", "rewrite"]) {
|
|
1960
|
+
const text = patchLines(source, edited, mode);
|
|
1961
|
+
if (text !== null && fingerprint(parseDSL(text)) === expected) {
|
|
1962
|
+
return { text, outcome: mode === "minimal" ? "patched" : "rewritten" };
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
return { text: withLineBreaksOf(source, canonical), outcome: "serialized" };
|
|
1966
|
+
} catch {
|
|
1967
|
+
return { text: canonical, outcome: "serialized" };
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
function readSource(text) {
|
|
1971
|
+
const lines = splitLines(text);
|
|
1972
|
+
const parsed = parseDSLWithSourceMap(text);
|
|
1973
|
+
const crlf = (text.match(/\r\n/g) ?? []).length;
|
|
1974
|
+
const lf = (text.match(/\n/g) ?? []).length - crlf;
|
|
1975
|
+
return {
|
|
1976
|
+
lines,
|
|
1977
|
+
shapes: shapeLines(lines),
|
|
1978
|
+
map: parsed.map,
|
|
1979
|
+
sourceMap: parsed.sourceMap,
|
|
1980
|
+
lineBreak: crlf > lf ? "\r\n" : "\n",
|
|
1981
|
+
endsWithLineBreak: text === "" || text.endsWith("\n")
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
function splitLines(text) {
|
|
1985
|
+
const lines = [];
|
|
1986
|
+
const lineBreaks = /\r?\n/g;
|
|
1987
|
+
let start = 0;
|
|
1988
|
+
for (let match = lineBreaks.exec(text); match; match = lineBreaks.exec(text)) {
|
|
1989
|
+
lines.push({ text: text.slice(start, match.index), lineBreak: match[0] });
|
|
1990
|
+
start = match.index + match[0].length;
|
|
1991
|
+
}
|
|
1992
|
+
if (start < text.length) lines.push({ text: text.slice(start), lineBreak: "" });
|
|
1993
|
+
return lines;
|
|
1994
|
+
}
|
|
1995
|
+
function withoutTrailingCarriageReturns2(text) {
|
|
1996
|
+
let end = text.length;
|
|
1997
|
+
while (end > 0 && text.charCodeAt(end - 1) === 13) end--;
|
|
1998
|
+
return text.slice(0, end);
|
|
1999
|
+
}
|
|
2000
|
+
function shapeLines(lines) {
|
|
2001
|
+
const comments = new CommentStripper();
|
|
2002
|
+
return lines.map(({ text }) => {
|
|
2003
|
+
const raw = withoutTrailingCarriageReturns2(text);
|
|
2004
|
+
const startsInBlockComment = comments.insideBlockComment;
|
|
2005
|
+
let fragments = comments.strip(raw).comments;
|
|
2006
|
+
let rest = raw;
|
|
2007
|
+
let commentHead = "";
|
|
2008
|
+
let commentTail = "";
|
|
2009
|
+
let editable = true;
|
|
2010
|
+
if (startsInBlockComment) {
|
|
2011
|
+
const head = fragments[0];
|
|
2012
|
+
if (head?.endsWith("*/") && raw.startsWith(head)) {
|
|
2013
|
+
commentHead = head;
|
|
2014
|
+
rest = raw.slice(head.length);
|
|
2015
|
+
fragments = fragments.slice(1);
|
|
2016
|
+
} else {
|
|
2017
|
+
editable = false;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
let codePart = rest;
|
|
2021
|
+
if (editable && fragments.length) {
|
|
2022
|
+
const comment = fragments[0];
|
|
2023
|
+
if (fragments.length > 1 || !rest.endsWith(comment)) {
|
|
2024
|
+
editable = false;
|
|
2025
|
+
} else {
|
|
2026
|
+
const beforeComment = rest.slice(0, rest.length - comment.length);
|
|
2027
|
+
codePart = beforeComment.trimEnd();
|
|
2028
|
+
commentTail = beforeComment.slice(codePart.length) + comment;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
return {
|
|
2032
|
+
indent: /^\s*/.exec(codePart)[0],
|
|
2033
|
+
code: codePart.trim(),
|
|
2034
|
+
commentHead,
|
|
2035
|
+
commentTail,
|
|
2036
|
+
editable,
|
|
2037
|
+
endsInBlockComment: comments.insideBlockComment
|
|
2038
|
+
};
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
function withLineBreaksOf(source, text) {
|
|
2042
|
+
if (source.lines.length === 0) return text;
|
|
2043
|
+
const withBreaks = source.lineBreak === "\n" ? text : text.replace(/\n/g, source.lineBreak);
|
|
2044
|
+
return source.endsWithLineBreak ? withBreaks : withBreaks.replace(/\r?\n$/, "");
|
|
2045
|
+
}
|
|
2046
|
+
function identityOf(el) {
|
|
2047
|
+
switch (el.elementType) {
|
|
2048
|
+
case "annotation":
|
|
2049
|
+
return `annotation\0${el.number}`;
|
|
2050
|
+
case "attitude":
|
|
2051
|
+
return `attitude\0${el.kind}`;
|
|
2052
|
+
default:
|
|
2053
|
+
return `${el.elementType}\0${el.label}`;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
function edgeIdentityOf(edge) {
|
|
2057
|
+
return `${edge.edgeType}\0${edge.from}\0${edge.to}`;
|
|
2058
|
+
}
|
|
2059
|
+
function matchElements(source, edited, hint = /* @__PURE__ */ new Map()) {
|
|
2060
|
+
const unmatchedSource = new Map(source.elements.map((el) => [el.id, el]));
|
|
2061
|
+
const idByEditedId = /* @__PURE__ */ new Map();
|
|
2062
|
+
const claim = (editedId, sourceId) => {
|
|
2063
|
+
idByEditedId.set(editedId, sourceId);
|
|
2064
|
+
unmatchedSource.delete(sourceId);
|
|
2065
|
+
};
|
|
2066
|
+
const unclaimed = () => edited.elements.filter((el) => !idByEditedId.has(el.id));
|
|
2067
|
+
for (const el of edited.elements) {
|
|
2068
|
+
const same = unmatchedSource.get(el.id);
|
|
2069
|
+
if (same && identityOf(same) === identityOf(el)) claim(el.id, el.id);
|
|
2070
|
+
}
|
|
2071
|
+
for (const el of unclaimed()) {
|
|
2072
|
+
const hinted = hint.get(el.id);
|
|
2073
|
+
const candidate = hinted === void 0 ? void 0 : unmatchedSource.get(hinted);
|
|
2074
|
+
if (candidate?.elementType === el.elementType) claim(el.id, candidate.id);
|
|
2075
|
+
}
|
|
2076
|
+
const sourceIdsByIdentity = /* @__PURE__ */ new Map();
|
|
2077
|
+
for (const el of unmatchedSource.values()) {
|
|
2078
|
+
const ids = sourceIdsByIdentity.get(identityOf(el)) ?? [];
|
|
2079
|
+
ids.push(el.id);
|
|
2080
|
+
sourceIdsByIdentity.set(identityOf(el), ids);
|
|
2081
|
+
}
|
|
2082
|
+
for (const el of unclaimed()) {
|
|
2083
|
+
const match = sourceIdsByIdentity.get(identityOf(el))?.shift();
|
|
2084
|
+
if (match !== void 0) claim(el.id, match);
|
|
2085
|
+
}
|
|
2086
|
+
for (const el of unclaimed()) {
|
|
2087
|
+
if (unmatchedSource.get(el.id)?.elementType === el.elementType) claim(el.id, el.id);
|
|
2088
|
+
}
|
|
2089
|
+
return idByEditedId;
|
|
2090
|
+
}
|
|
2091
|
+
function alignToSource(source, edited, previous) {
|
|
2092
|
+
const hint = previous ? matchElements(source, previous) : void 0;
|
|
2093
|
+
const idByEditedId = matchElements(source, edited, hint);
|
|
2094
|
+
const usedIds = /* @__PURE__ */ new Set([...source.elements.map((el) => el.id), ...idByEditedId.values()]);
|
|
2095
|
+
const freshId = (id) => {
|
|
2096
|
+
let candidate = id;
|
|
2097
|
+
for (let suffix = 2; usedIds.has(candidate); suffix++) candidate = `${id}~${suffix}`;
|
|
2098
|
+
usedIds.add(candidate);
|
|
2099
|
+
return candidate;
|
|
2100
|
+
};
|
|
2101
|
+
for (const el of edited.elements) {
|
|
2102
|
+
if (!idByEditedId.has(el.id)) idByEditedId.set(el.id, freshId(el.id));
|
|
2103
|
+
}
|
|
2104
|
+
const rename = (id) => idByEditedId.get(id) ?? id;
|
|
2105
|
+
const elements = edited.elements.map((el) => {
|
|
2106
|
+
const renamed = { ...el, id: rename(el.id) };
|
|
2107
|
+
if (renamed.elementType === "component" && renamed.pipelineId !== void 0) {
|
|
2108
|
+
return { ...renamed, pipelineId: rename(renamed.pipelineId) };
|
|
2109
|
+
}
|
|
2110
|
+
if (renamed.elementType === "pipeline") {
|
|
2111
|
+
return { ...renamed, childIds: renamed.childIds.map(rename) };
|
|
2112
|
+
}
|
|
2113
|
+
return renamed;
|
|
2114
|
+
});
|
|
2115
|
+
const edges = alignEdges(
|
|
2116
|
+
source.edges,
|
|
2117
|
+
edited.edges.map((edge) => ({ ...edge, from: rename(edge.from), to: rename(edge.to) }))
|
|
2118
|
+
);
|
|
2119
|
+
return {
|
|
2120
|
+
...edited,
|
|
2121
|
+
elements: inSourceOrder(elements, source.elements),
|
|
2122
|
+
edges: inSourceOrder(edges, source.edges)
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
function alignEdges(sourceEdges, editedEdges) {
|
|
2126
|
+
const unmatchedSource = new Map(sourceEdges.map((edge) => [edge.id, edge]));
|
|
2127
|
+
const idByIndex = /* @__PURE__ */ new Map();
|
|
2128
|
+
const claim = (index, id) => {
|
|
2129
|
+
idByIndex.set(index, id);
|
|
2130
|
+
unmatchedSource.delete(id);
|
|
2131
|
+
};
|
|
2132
|
+
editedEdges.forEach((edge, index) => {
|
|
2133
|
+
const same = unmatchedSource.get(edge.id);
|
|
2134
|
+
if (same && edgeIdentityOf(same) === edgeIdentityOf(edge)) claim(index, edge.id);
|
|
2135
|
+
});
|
|
2136
|
+
editedEdges.forEach((edge, index) => {
|
|
2137
|
+
if (idByIndex.has(index)) return;
|
|
2138
|
+
const match = [...unmatchedSource.values()].find(
|
|
2139
|
+
(candidate) => edgeIdentityOf(candidate) === edgeIdentityOf(edge)
|
|
2140
|
+
);
|
|
2141
|
+
if (match) claim(index, match.id);
|
|
2142
|
+
});
|
|
2143
|
+
editedEdges.forEach((edge, index) => {
|
|
2144
|
+
if (idByIndex.has(index)) return;
|
|
2145
|
+
if (unmatchedSource.get(edge.id)?.edgeType === edge.edgeType) claim(index, edge.id);
|
|
2146
|
+
});
|
|
2147
|
+
const usedIds = /* @__PURE__ */ new Set([...sourceEdges.map((edge) => edge.id), ...idByIndex.values()]);
|
|
2148
|
+
return editedEdges.map((edge, index) => {
|
|
2149
|
+
let id = idByIndex.get(index);
|
|
2150
|
+
if (id === void 0) {
|
|
2151
|
+
id = edge.id;
|
|
2152
|
+
for (let suffix = 2; usedIds.has(id); suffix++) id = `${edge.id}~${suffix}`;
|
|
2153
|
+
usedIds.add(id);
|
|
2154
|
+
}
|
|
2155
|
+
return { ...edge, id };
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
function inSourceOrder(items, sourceItems) {
|
|
2159
|
+
const sourceIndex = new Map(sourceItems.map((item, index) => [item.id, index]));
|
|
2160
|
+
const position = (item) => sourceIndex.get(item.id) ?? Number.POSITIVE_INFINITY;
|
|
2161
|
+
return items.map((item, index) => ({ item, index })).sort((a, b) => position(a.item) - position(b.item) || a.index - b.index).map(({ item }) => item);
|
|
2162
|
+
}
|
|
2163
|
+
var isRawKey = (key) => key.startsWith("raw:");
|
|
2164
|
+
function keyedTexts(lines) {
|
|
2165
|
+
return new Map(lines.filter((line) => !isRawKey(line.key)).map((line) => [line.key, line.text]));
|
|
2166
|
+
}
|
|
2167
|
+
function listsByLine(lineCount) {
|
|
2168
|
+
return Array.from({ length: lineCount }, () => []);
|
|
2169
|
+
}
|
|
2170
|
+
function diffPassthrough(before, after) {
|
|
2171
|
+
const remainingByText = /* @__PURE__ */ new Map();
|
|
2172
|
+
for (const line of before.filter((entry) => isRawKey(entry.key))) {
|
|
2173
|
+
const indexes = remainingByText.get(line.text) ?? [];
|
|
2174
|
+
indexes.push(Number(line.key.slice("raw:".length)));
|
|
2175
|
+
remainingByText.set(line.text, indexes);
|
|
2176
|
+
}
|
|
2177
|
+
const added = [];
|
|
2178
|
+
for (const line of after.filter((entry) => isRawKey(entry.key))) {
|
|
2179
|
+
if (remainingByText.get(line.text)?.shift() === void 0) added.push(line);
|
|
2180
|
+
}
|
|
2181
|
+
return { removed: new Set([...remainingByText.values()].flat()), added };
|
|
2182
|
+
}
|
|
2183
|
+
function pipelineOfEachChild(map) {
|
|
2184
|
+
const pipelineById = /* @__PURE__ */ new Map();
|
|
2185
|
+
for (const el of map.elements) {
|
|
2186
|
+
if (el.elementType === "component" && el.pipelineId !== void 0) {
|
|
2187
|
+
pipelineById.set(el.id, el.pipelineId);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
return pipelineById;
|
|
2191
|
+
}
|
|
2192
|
+
function reparentedChildKeys(source, edited) {
|
|
2193
|
+
const pipelineBefore = pipelineOfEachChild(source);
|
|
2194
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2195
|
+
for (const [id, pipelineId] of pipelineOfEachChild(edited)) {
|
|
2196
|
+
const previous = pipelineBefore.get(id);
|
|
2197
|
+
if (previous !== void 0 && previous !== pipelineId) keys.add(`child:${id}`);
|
|
2198
|
+
}
|
|
2199
|
+
return keys;
|
|
2200
|
+
}
|
|
2201
|
+
function pipelinesNoLongerSpannedByChildren(edited, rangeFromChildren) {
|
|
2202
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2203
|
+
if (rangeFromChildren.size === 0) return keys;
|
|
2204
|
+
const evolutionsByPipeline = /* @__PURE__ */ new Map();
|
|
2205
|
+
for (const el of edited.elements) {
|
|
2206
|
+
if (el.elementType !== "component" || el.pipelineId === void 0) continue;
|
|
2207
|
+
const evolutions = evolutionsByPipeline.get(el.pipelineId) ?? [];
|
|
2208
|
+
evolutions.push(roundAsWritten(el.position.evolution));
|
|
2209
|
+
evolutionsByPipeline.set(el.pipelineId, evolutions);
|
|
2210
|
+
}
|
|
2211
|
+
for (const el of edited.elements) {
|
|
2212
|
+
if (el.elementType !== "pipeline" || !rangeFromChildren.has(el.id)) continue;
|
|
2213
|
+
const evolutions = evolutionsByPipeline.get(el.id) ?? [];
|
|
2214
|
+
const read = evolutions.length > 0 ? widenEmptyRange({ start: Math.min(...evolutions), end: Math.max(...evolutions) }) : void 0;
|
|
2215
|
+
const spanned = read !== void 0 && read.start === roundAsWritten(el.evolutionStart) && read.end === roundAsWritten(el.evolutionEnd);
|
|
2216
|
+
if (!spanned) keys.add(`element:${el.id}`);
|
|
2217
|
+
}
|
|
2218
|
+
return keys;
|
|
2219
|
+
}
|
|
2220
|
+
function patchLines(source, edited, mode) {
|
|
2221
|
+
const { lines, shapes, sourceMap } = source;
|
|
2222
|
+
const hints = { urlReferences: sourceMap.urlReferences };
|
|
2223
|
+
const before = serializeKeyedLines(source.map, hints);
|
|
2224
|
+
const after = serializeKeyedLines(edited, hints);
|
|
2225
|
+
const textBefore = keyedTexts(before);
|
|
2226
|
+
const textAfter = keyedTexts(after);
|
|
2227
|
+
const passthrough = diffPassthrough(before, after);
|
|
2228
|
+
const reparented = reparentedChildKeys(source.map, edited);
|
|
2229
|
+
const unspanned = pipelinesNoLongerSpannedByChildren(edited, sourceMap.rangeFromChildren);
|
|
2230
|
+
const statusOf = (key) => {
|
|
2231
|
+
const written = textAfter.get(key);
|
|
2232
|
+
if (written === void 0 || reparented.has(key)) return "deleted";
|
|
2233
|
+
if (mode === "rewrite" || written !== textBefore.get(key) || unspanned.has(key)) {
|
|
2234
|
+
return "changed";
|
|
2235
|
+
}
|
|
2236
|
+
return "same";
|
|
2237
|
+
};
|
|
2238
|
+
const primaryKeysByLine = listsByLine(lines.length);
|
|
2239
|
+
for (const [key, line] of sourceMap.primaryLineOf) {
|
|
2240
|
+
if (textBefore.has(key)) primaryKeysByLine[line - 1]?.push(key);
|
|
2241
|
+
}
|
|
2242
|
+
const secondaryKeysByLine = listsByLine(lines.length);
|
|
2243
|
+
for (const [key, secondaryLines] of sourceMap.secondaryLinesOf) {
|
|
2244
|
+
if (!textBefore.has(key)) continue;
|
|
2245
|
+
for (const line of secondaryLines) secondaryKeysByLine[line - 1]?.push(key);
|
|
2246
|
+
}
|
|
2247
|
+
const passthroughByLine = listsByLine(lines.length);
|
|
2248
|
+
sourceMap.passthrough.forEach((entry, index) => passthroughByLine[entry.line - 1]?.push(index));
|
|
2249
|
+
const bracesByLine = listsByLine(lines.length);
|
|
2250
|
+
for (const [pipelineId, block] of sourceMap.blocks) {
|
|
2251
|
+
if (block.open !== void 0) {
|
|
2252
|
+
bracesByLine[block.open - 1]?.push({ pipelineId, end: "open" });
|
|
2253
|
+
}
|
|
2254
|
+
if (block.close !== void 0) {
|
|
2255
|
+
bracesByLine[block.close - 1]?.push({ pipelineId, end: "close" });
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
const braceRemoved = (pipelineId) => !textAfter.has(`block-open:${pipelineId}`) && (textBefore.has(`block-open:${pipelineId}`) || !textAfter.has(`element:${pipelineId}`));
|
|
2259
|
+
const result = [];
|
|
2260
|
+
for (let index = 0; index < lines.length; index++) {
|
|
2261
|
+
const patched = patchLine(index);
|
|
2262
|
+
if (patched === void 0) return null;
|
|
2263
|
+
result.push(patched);
|
|
2264
|
+
}
|
|
2265
|
+
function patchLine(index) {
|
|
2266
|
+
const line = lines[index];
|
|
2267
|
+
const shape = shapes[index];
|
|
2268
|
+
const primaries = primaryKeysByLine[index];
|
|
2269
|
+
const removedPassthrough = passthroughByLine[index].filter(
|
|
2270
|
+
(entry) => passthrough.removed.has(entry)
|
|
2271
|
+
);
|
|
2272
|
+
const braces = bracesByLine[index];
|
|
2273
|
+
const changed = primaries.some((key) => statusOf(key) !== "same") || secondaryKeysByLine[index].some((key) => statusOf(key) !== "same") || removedPassthrough.length > 0 || braces.some((brace) => braceRemoved(brace.pipelineId));
|
|
2274
|
+
if (!changed) return line.text;
|
|
2275
|
+
if (!shape.editable || primaries.length > 1) return void 0;
|
|
2276
|
+
if (removedPassthrough.some((entry) => sourceMap.passthrough[entry].isComment)) {
|
|
2277
|
+
return void 0;
|
|
2278
|
+
}
|
|
2279
|
+
let statement = null;
|
|
2280
|
+
const primary = primaries[0];
|
|
2281
|
+
if (primary !== void 0) {
|
|
2282
|
+
const status = statusOf(primary);
|
|
2283
|
+
if (status === "changed") statement = textAfter.get(primary).trim();
|
|
2284
|
+
else if (status === "deleted") statement = "";
|
|
2285
|
+
} else if (secondaryKeysByLine[index].length || removedPassthrough.length) {
|
|
2286
|
+
statement = "";
|
|
2287
|
+
}
|
|
2288
|
+
const open = braces.find((brace) => brace.end === "open");
|
|
2289
|
+
const close = braces.find((brace) => brace.end === "close");
|
|
2290
|
+
let rest = shape.code;
|
|
2291
|
+
let prefix = "";
|
|
2292
|
+
let suffix = "";
|
|
2293
|
+
if (open && primary !== `element:${open.pipelineId}` && rest.startsWith("{")) {
|
|
2294
|
+
prefix = braceRemoved(open.pipelineId) ? "" : "{";
|
|
2295
|
+
rest = rest.slice(1).trimStart();
|
|
2296
|
+
} else if (open && rest.endsWith("{")) {
|
|
2297
|
+
suffix = braceRemoved(open.pipelineId) ? "" : "{";
|
|
2298
|
+
rest = rest.slice(0, -1).trimEnd();
|
|
2299
|
+
} else if (open) return void 0;
|
|
2300
|
+
if (close && rest.endsWith("}") && !(open && suffix)) {
|
|
2301
|
+
suffix = braceRemoved(close.pipelineId) ? "" : "}";
|
|
2302
|
+
rest = rest.slice(0, -1).trimEnd();
|
|
2303
|
+
} else if (close) return void 0;
|
|
2304
|
+
const assembled = [prefix, statement ?? rest, suffix].filter(Boolean).join(" ");
|
|
2305
|
+
if (assembled) return shape.commentHead + shape.indent + assembled + shape.commentTail;
|
|
2306
|
+
if (shape.commentHead) return shape.commentHead + shape.commentTail;
|
|
2307
|
+
return shape.commentTail ? shape.indent + shape.commentTail.trimStart() : null;
|
|
2308
|
+
}
|
|
2309
|
+
const insertions = planInsertions(source, edited, after, textBefore, textAfter, reparented);
|
|
2310
|
+
if (!insertions) return null;
|
|
2311
|
+
passthrough.added.forEach((line, index) => {
|
|
2312
|
+
addInsertion(insertions, lines.length - 1, line.text, after.length + index);
|
|
2313
|
+
});
|
|
2314
|
+
return assemble(source, result, insertions);
|
|
2315
|
+
}
|
|
2316
|
+
function addInsertion(insertions, afterLine, text, order) {
|
|
2317
|
+
const list = insertions.get(afterLine) ?? [];
|
|
2318
|
+
list.push({ text, order });
|
|
2319
|
+
insertions.set(afterLine, list);
|
|
2320
|
+
}
|
|
2321
|
+
function planInsertions(source, edited, after, textBefore, textAfter, reparented) {
|
|
2322
|
+
const { lines, shapes, sourceMap } = source;
|
|
2323
|
+
const lastLine = lines.length - 1;
|
|
2324
|
+
const lineOf = (key) => {
|
|
2325
|
+
const line = sourceMap.primaryLineOf.get(key);
|
|
2326
|
+
return line === void 0 ? void 0 : line - 1;
|
|
2327
|
+
};
|
|
2328
|
+
const pipelineLineOf = (pipelineId) => lineOf(`element:${pipelineId}`);
|
|
2329
|
+
const childLinesOf = (pipelineId) => source.map.elements.flatMap((el) => {
|
|
2330
|
+
if (el.elementType !== "component" || el.pipelineId !== pipelineId) return [];
|
|
2331
|
+
const line = lineOf(`child:${el.id}`);
|
|
2332
|
+
return line === void 0 ? [] : [line];
|
|
2333
|
+
});
|
|
2334
|
+
const blockEndOf = (pipelineId) => {
|
|
2335
|
+
const block = sourceMap.blocks.get(pipelineId);
|
|
2336
|
+
if (block?.open === void 0 || pipelineLineOf(pipelineId) === void 0) return void 0;
|
|
2337
|
+
if (block.close !== void 0) return block.close - 1;
|
|
2338
|
+
return Math.max(block.open - 1, ...childLinesOf(pipelineId));
|
|
2339
|
+
};
|
|
2340
|
+
const anchorInBlock = (pipelineId) => {
|
|
2341
|
+
const block = sourceMap.blocks.get(pipelineId);
|
|
2342
|
+
let anchor;
|
|
2343
|
+
if (block.close === void 0) anchor = blockEndOf(pipelineId);
|
|
2344
|
+
else if (block.close !== block.open && shapes[block.close - 1].code === "}") {
|
|
2345
|
+
anchor = block.close - 2;
|
|
2346
|
+
} else return null;
|
|
2347
|
+
return shapes[anchor]?.endsInBlockComment ? null : anchor;
|
|
2348
|
+
};
|
|
2349
|
+
const hasSourceBlock = (pipelineId) => blockEndOf(pipelineId) !== void 0;
|
|
2350
|
+
const endOf = (key) => {
|
|
2351
|
+
const line = lineOf(key);
|
|
2352
|
+
if (line === void 0 || !key.startsWith("element:")) return line;
|
|
2353
|
+
return Math.max(line, blockEndOf(key.slice("element:".length)) ?? line);
|
|
2354
|
+
};
|
|
2355
|
+
const sourceElementTypes = new Map(source.map.elements.map((el) => [el.id, el.elementType]));
|
|
2356
|
+
const editedElementById = new Map(edited.elements.map((el) => [el.id, el]));
|
|
2357
|
+
const lastLineOf = (accepts) => {
|
|
2358
|
+
let last;
|
|
2359
|
+
for (const key of sourceMap.primaryLineOf.keys()) {
|
|
2360
|
+
if (!accepts(key) || !textBefore.has(key)) continue;
|
|
2361
|
+
const end = endOf(key);
|
|
2362
|
+
if (end !== void 0 && (last === void 0 || end > last)) last = end;
|
|
2363
|
+
}
|
|
2364
|
+
return last;
|
|
2365
|
+
};
|
|
2366
|
+
const isElementOfType = (type) => (key) => key.startsWith("element:") && sourceElementTypes.get(key.slice("element:".length)) === type;
|
|
2367
|
+
const startsWith = (prefix) => (key) => key.startsWith(prefix);
|
|
2368
|
+
const settle = (line) => {
|
|
2369
|
+
if (line < 0) return -1;
|
|
2370
|
+
let settled = line;
|
|
2371
|
+
for (const pipelineId of sourceMap.blocks.keys()) {
|
|
2372
|
+
const start = pipelineLineOf(pipelineId);
|
|
2373
|
+
const end = blockEndOf(pipelineId);
|
|
2374
|
+
if (start === void 0 || end === void 0) continue;
|
|
2375
|
+
if (settled >= start && settled < end) settled = end;
|
|
2376
|
+
}
|
|
2377
|
+
while (settled < lastLine && shapes[settled].endsInBlockComment) settled++;
|
|
2378
|
+
return settled;
|
|
2379
|
+
};
|
|
2380
|
+
const firstOf = (...candidates) => {
|
|
2381
|
+
for (const candidate of candidates) if (candidate !== void 0) return candidate;
|
|
2382
|
+
return lastLine;
|
|
2383
|
+
};
|
|
2384
|
+
const needsInsertion = (key) => reparented.has(key) || !sourceMap.primaryLineOf.has(key) && textBefore.get(key) !== textAfter.get(key);
|
|
2385
|
+
const anchorOfElement = (id) => {
|
|
2386
|
+
const type = editedElementById.get(id)?.elementType ?? "";
|
|
2387
|
+
return settle(
|
|
2388
|
+
firstOf(
|
|
2389
|
+
lastLineOf(isElementOfType(type)),
|
|
2390
|
+
lastLineOf(startsWith("element:")),
|
|
2391
|
+
lastLineOf(startsWith("url:")),
|
|
2392
|
+
lastLineOf(startsWith("config:"))
|
|
2393
|
+
)
|
|
2394
|
+
);
|
|
2395
|
+
};
|
|
2396
|
+
const anchorOfNewBlock = (pipelineId) => pipelineLineOf(pipelineId) ?? anchorOfElement(pipelineId);
|
|
2397
|
+
const insertions = /* @__PURE__ */ new Map();
|
|
2398
|
+
for (let order = 0; order < after.length; order++) {
|
|
2399
|
+
const { key, text } = after[order];
|
|
2400
|
+
if (isRawKey(key)) continue;
|
|
2401
|
+
const separator = key.indexOf(":");
|
|
2402
|
+
const kind = key.slice(0, separator);
|
|
2403
|
+
const id = key.slice(separator + 1);
|
|
2404
|
+
let anchor;
|
|
2405
|
+
if (kind === "block-open" || kind === "block-close") {
|
|
2406
|
+
if (textBefore.has(`block-open:${id}`)) continue;
|
|
2407
|
+
if (hasSourceBlock(id)) {
|
|
2408
|
+
if (anchorInBlock(id) === null) return null;
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
anchor = anchorOfNewBlock(id);
|
|
2412
|
+
} else if (!needsInsertion(key)) {
|
|
2413
|
+
continue;
|
|
2414
|
+
} else if (kind === "config") {
|
|
2415
|
+
anchor = settle(firstOf(lastLineOf(startsWith("config:")), -1));
|
|
2416
|
+
} else if (kind === "url") {
|
|
2417
|
+
const lastUrl = lastLineOf(startsWith("url:"));
|
|
2418
|
+
anchor = settle(firstOf(lastUrl, lastLineOf(startsWith("config:")), -1));
|
|
2419
|
+
} else if (kind === "element") {
|
|
2420
|
+
anchor = anchorOfElement(id);
|
|
2421
|
+
} else if (kind === "child") {
|
|
2422
|
+
const child = editedElementById.get(id);
|
|
2423
|
+
const pipelineId = child?.elementType === "component" ? child.pipelineId : void 0;
|
|
2424
|
+
if (pipelineId === void 0) return null;
|
|
2425
|
+
anchor = hasSourceBlock(pipelineId) ? anchorInBlock(pipelineId) : anchorOfNewBlock(pipelineId);
|
|
2426
|
+
} else if (kind === "evolve") {
|
|
2427
|
+
anchor = settle(firstOf(lastLineOf(startsWith("evolve:")), endOf(`element:${id}`)));
|
|
2428
|
+
} else if (kind === "edge") {
|
|
2429
|
+
anchor = settle(firstOf(lastLineOf(startsWith("edge:"))));
|
|
2430
|
+
} else return null;
|
|
2431
|
+
if (anchor === null) return null;
|
|
2432
|
+
addInsertion(insertions, anchor, text, order);
|
|
2433
|
+
}
|
|
2434
|
+
return insertions;
|
|
2435
|
+
}
|
|
2436
|
+
function assemble(source, result, insertions) {
|
|
2437
|
+
const output = [];
|
|
2438
|
+
const insertAfter = (line) => {
|
|
2439
|
+
const list = [...insertions.get(line) ?? []].sort((a, b) => a.order - b.order);
|
|
2440
|
+
for (const { text } of list) output.push({ text, lineBreak: "" });
|
|
2441
|
+
};
|
|
2442
|
+
insertAfter(-1);
|
|
2443
|
+
result.forEach((text, index) => {
|
|
2444
|
+
if (text !== null) output.push({ text, lineBreak: source.lines[index].lineBreak });
|
|
2445
|
+
insertAfter(index);
|
|
2446
|
+
});
|
|
2447
|
+
return output.map(({ text, lineBreak }, index) => {
|
|
2448
|
+
const isLast = index === output.length - 1;
|
|
2449
|
+
if (isLast && !source.endsWithLineBreak) return text;
|
|
2450
|
+
return text + (lineBreak || source.lineBreak);
|
|
2451
|
+
}).join("");
|
|
2452
|
+
}
|
|
2453
|
+
function normalized(value) {
|
|
2454
|
+
if (typeof value === "number") return roundAsWritten(value);
|
|
2455
|
+
if (Array.isArray(value)) return value.map(normalized);
|
|
2456
|
+
if (value !== null && typeof value === "object") {
|
|
2457
|
+
return Object.fromEntries(
|
|
2458
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, entry]) => [key, normalized(entry)])
|
|
2459
|
+
);
|
|
2460
|
+
}
|
|
2461
|
+
return value;
|
|
2462
|
+
}
|
|
2463
|
+
function fingerprint(map) {
|
|
2464
|
+
const elementById = new Map(map.elements.map((el) => [el.id, el]));
|
|
2465
|
+
const reference = (id) => {
|
|
2466
|
+
const el = elementById.get(id);
|
|
2467
|
+
return el ? `${el.elementType}:${el.label}` : `?${id}`;
|
|
2468
|
+
};
|
|
2469
|
+
const entries = [`config ${JSON.stringify(normalized(map.config))}`];
|
|
2470
|
+
for (const el of map.elements) {
|
|
2471
|
+
const { id: _id, ...rest } = el;
|
|
2472
|
+
const described = { ...rest };
|
|
2473
|
+
if (el.elementType === "component" && el.pipelineId !== void 0) {
|
|
2474
|
+
described["pipelineId"] = reference(el.pipelineId);
|
|
2475
|
+
}
|
|
2476
|
+
if (el.elementType === "pipeline") described["childIds"] = el.childIds.map(reference).sort();
|
|
2477
|
+
entries.push(`element ${JSON.stringify(normalized(described))}`);
|
|
2478
|
+
}
|
|
2479
|
+
for (const edge of map.edges) {
|
|
2480
|
+
const { id: _id, ...rest } = edge;
|
|
2481
|
+
const described = { ...rest, from: reference(edge.from), to: reference(edge.to) };
|
|
2482
|
+
entries.push(`edge ${JSON.stringify(normalized(described))}`);
|
|
2483
|
+
}
|
|
2484
|
+
for (const raw of map.rawPassthrough ?? []) entries.push(`raw ${raw}`);
|
|
2485
|
+
return entries.sort().join("\n");
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
// src/raw-passthrough.ts
|
|
2489
|
+
function withoutRawStatements(rawPassthrough, keyword) {
|
|
2490
|
+
if (!rawPassthrough) return rawPassthrough;
|
|
2491
|
+
const wanted = keyword.toLowerCase();
|
|
2492
|
+
const comments = new CommentStripper();
|
|
2493
|
+
let changed = false;
|
|
2494
|
+
const kept = [];
|
|
2495
|
+
for (const raw of rawPassthrough) {
|
|
2496
|
+
const stripped = comments.strip(raw);
|
|
2497
|
+
if (keywordOf(stripped.code) !== wanted) {
|
|
2498
|
+
kept.push(raw);
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
changed = true;
|
|
2502
|
+
if (stripped.comments.length) kept.push(stripped.comments.join(" "));
|
|
2503
|
+
}
|
|
2504
|
+
if (!changed) return rawPassthrough;
|
|
2505
|
+
return kept.length ? kept : void 0;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
1015
2508
|
Object.defineProperty(exports, "loadMap", {
|
|
1016
2509
|
enumerable: true,
|
|
1017
2510
|
get: function () { return wardleySchemaModel.loadMap; }
|
|
@@ -1030,7 +2523,9 @@ exports.parseDSL = parseDSL;
|
|
|
1030
2523
|
exports.parseDSLWithDiagnostics = parseDSLWithDiagnostics;
|
|
1031
2524
|
exports.parseDecorators = parseDecorators;
|
|
1032
2525
|
exports.parseLabelOffset = parseLabelOffset;
|
|
2526
|
+
exports.patchDSL = patchDSL;
|
|
1033
2527
|
exports.serializeDSL = serializeDSL;
|
|
1034
2528
|
exports.slug = slug;
|
|
2529
|
+
exports.withoutRawStatements = withoutRawStatements;
|
|
1035
2530
|
//# sourceMappingURL=index.cjs.map
|
|
1036
2531
|
//# sourceMappingURL=index.cjs.map
|