@miragon/wardley-dsl 0.5.2 → 0.5.3
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/dist/index.cjs +1465 -703
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +1466 -704
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -13,64 +13,165 @@ function parseCoords(line) {
|
|
|
13
13
|
if (Number.isNaN(a) || Number.isNaN(b)) return null;
|
|
14
14
|
return { a, b };
|
|
15
15
|
}
|
|
16
|
-
function
|
|
17
|
-
return line.replace(COORDS_RE, " ");
|
|
18
|
-
}
|
|
19
|
-
function splitAtCoords(line) {
|
|
16
|
+
function splitAtTuple(line) {
|
|
20
17
|
const m = COORDS_RE.exec(line);
|
|
21
18
|
if (!m) return null;
|
|
22
19
|
const a = Number(m[1]);
|
|
23
20
|
const b = Number(m[2]);
|
|
24
|
-
if (Number.isNaN(a) || Number.isNaN(b)) return null;
|
|
25
21
|
return {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
before: line.slice(0, m.index).trim(),
|
|
23
|
+
tuple: m[0],
|
|
24
|
+
coords: Number.isNaN(a) || Number.isNaN(b) ? null : { a, b },
|
|
25
|
+
after: line.slice(m.index + m[0].length)
|
|
29
26
|
};
|
|
30
27
|
}
|
|
28
|
+
function splitAtCoords(line) {
|
|
29
|
+
const split = splitAtTuple(line);
|
|
30
|
+
if (!split?.coords) return null;
|
|
31
|
+
return { name: split.before, coords: split.coords, suffix: split.after };
|
|
32
|
+
}
|
|
31
33
|
var COORDS4_RE = /\[\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\]/;
|
|
32
|
-
function
|
|
34
|
+
function splitAtCoords4(line) {
|
|
33
35
|
const m = COORDS4_RE.exec(line);
|
|
34
36
|
if (!m) return null;
|
|
35
37
|
const vals = [m[1], m[2], m[3], m[4]].map(Number);
|
|
36
38
|
if (vals.some(Number.isNaN)) return null;
|
|
37
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
before: line.slice(0, m.index).trim(),
|
|
41
|
+
coords: { a: vals[0], b: vals[1], c: vals[2], d: vals[3] },
|
|
42
|
+
after: line.slice(m.index + m[0].length)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
var TUPLE_LIST_RE = /\[\s{0,8}(\[[^[\]]*\](?:\s{0,8},\s{0,8}\[[^[\]]*\])*)\s{0,8}\]/;
|
|
46
|
+
var LIST_ENTRY_RE = /\[[^[\]]*\]/g;
|
|
47
|
+
var WHOLE_COORDS_RE = /^\[\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\]$/;
|
|
48
|
+
function readTupleList(line, list) {
|
|
49
|
+
const tuples = [];
|
|
50
|
+
const unreadable = [];
|
|
51
|
+
for (const [entry] of list[1].matchAll(LIST_ENTRY_RE)) {
|
|
52
|
+
const coords = WHOLE_COORDS_RE.exec(entry);
|
|
53
|
+
const a = Number(coords?.[1]);
|
|
54
|
+
const b = Number(coords?.[2]);
|
|
55
|
+
if (coords && !Number.isNaN(a) && !Number.isNaN(b)) tuples.push({ a, b });
|
|
56
|
+
else unreadable.push(entry);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
tuples,
|
|
60
|
+
unreadable,
|
|
61
|
+
before: line.slice(0, list.index).trim(),
|
|
62
|
+
after: line.slice(list.index + list[0].length)
|
|
63
|
+
};
|
|
38
64
|
}
|
|
39
|
-
var MULTI_COORDS_RE = /\[\s{0,8}(\[[^[\]]*\](?:\s{0,8},\s{0,8}\[[^[\]]*\])+)\s{0,8}\]/;
|
|
40
65
|
function parseMultiCoords(line) {
|
|
41
|
-
const
|
|
66
|
+
const list = TUPLE_LIST_RE.exec(line);
|
|
67
|
+
return list ? readTupleList(line, list) : null;
|
|
68
|
+
}
|
|
69
|
+
var FIRST_CHILD_TUPLE_RE = /\[\s*([-\d.]+)\s*(?:,\s*([-\d.]+)\s*)?\]/;
|
|
70
|
+
function splitAtFirstChildTuple(line) {
|
|
71
|
+
const m = FIRST_CHILD_TUPLE_RE.exec(line);
|
|
42
72
|
if (!m) return null;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
return {
|
|
74
|
+
before: line.slice(0, m.index).trim(),
|
|
75
|
+
values: m[2] === void 0 ? [m[1]] : [m[1], m[2]],
|
|
76
|
+
after: line.slice(m.index + m[0].length)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function parsePositions(line) {
|
|
80
|
+
const single = COORDS_RE.exec(line);
|
|
81
|
+
const list = TUPLE_LIST_RE.exec(line);
|
|
82
|
+
if (list && (!single || list.index < single.index)) return readTupleList(line, list);
|
|
83
|
+
if (!single) return null;
|
|
84
|
+
const a = Number(single[1]);
|
|
85
|
+
const b = Number(single[2]);
|
|
86
|
+
const readable = !Number.isNaN(a) && !Number.isNaN(b);
|
|
87
|
+
return {
|
|
88
|
+
tuples: readable ? [{ a, b }] : [],
|
|
89
|
+
unreadable: readable ? [] : [single[0]],
|
|
90
|
+
before: line.slice(0, single.index).trim(),
|
|
91
|
+
after: line.slice(single.index + single[0].length)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function flowValueEnd(line, at) {
|
|
95
|
+
return line[at] === "'" && line[at - 1] === "+" ? line.indexOf("'", at + 1) : -1;
|
|
96
|
+
}
|
|
97
|
+
function rewriteOutsideFlowValues(text, rewrite) {
|
|
98
|
+
let rewritten = "";
|
|
99
|
+
let partStart = 0;
|
|
100
|
+
for (let i = 0; i < text.length; i++) {
|
|
101
|
+
const valueEnd = flowValueEnd(text, i);
|
|
102
|
+
if (valueEnd < 0) continue;
|
|
103
|
+
rewritten += rewrite(text.slice(partStart, i)) + text.slice(i, valueEnd + 1);
|
|
104
|
+
partStart = valueEnd + 1;
|
|
105
|
+
i = valueEnd;
|
|
106
|
+
}
|
|
107
|
+
return rewritten + rewrite(text.slice(partStart));
|
|
108
|
+
}
|
|
109
|
+
function indexOfLineComment(line) {
|
|
56
110
|
for (let i = 0; i < line.length - 1; i++) {
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
else if (
|
|
60
|
-
return { code: line.slice(0, i), comment: line.slice(i) };
|
|
61
|
-
}
|
|
111
|
+
const valueEnd = flowValueEnd(line, i);
|
|
112
|
+
if (valueEnd > 0) i = valueEnd;
|
|
113
|
+
else if (line[i] === "/" && line[i + 1] === "/" && line[i - 1] !== ":") return i;
|
|
62
114
|
}
|
|
63
|
-
return
|
|
115
|
+
return -1;
|
|
64
116
|
}
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
117
|
+
function readUrlDefinition(after) {
|
|
118
|
+
const open = after.indexOf("[");
|
|
119
|
+
const close = after.lastIndexOf("]");
|
|
120
|
+
const name = open > 0 ? after.slice(0, open).trim() : "";
|
|
121
|
+
const address = open > 0 && close > open ? after.slice(open + 1, close).trim() : "";
|
|
122
|
+
if (!name || !address || after.slice(close + 1).trim() !== "") return null;
|
|
123
|
+
return { name, address };
|
|
124
|
+
}
|
|
125
|
+
function isUrlDefinition(line) {
|
|
126
|
+
return keywordOf(line) === "url" && readUrlDefinition(line.trim().slice("url".length)) !== null;
|
|
127
|
+
}
|
|
128
|
+
var CommentStripper = class {
|
|
129
|
+
inBlockComment = false;
|
|
130
|
+
get insideBlockComment() {
|
|
131
|
+
return this.inBlockComment;
|
|
132
|
+
}
|
|
133
|
+
strip(raw) {
|
|
134
|
+
const comments = [];
|
|
135
|
+
let working = raw;
|
|
136
|
+
if (this.inBlockComment) {
|
|
137
|
+
const close = working.indexOf("*/");
|
|
138
|
+
if (close < 0) return { code: "", comments: [raw] };
|
|
139
|
+
comments.push(working.slice(0, close + 2));
|
|
140
|
+
working = working.slice(close + 2);
|
|
141
|
+
this.inBlockComment = false;
|
|
142
|
+
}
|
|
143
|
+
const code = isUrlDefinition(working) ? working : this.stripComments(working, comments);
|
|
144
|
+
return { code, comments };
|
|
145
|
+
}
|
|
146
|
+
/** Left to right, so a `/*` inside a `//` comment opens no block comment. */
|
|
147
|
+
stripComments(line, comments) {
|
|
148
|
+
let code = "";
|
|
149
|
+
let rest = line;
|
|
150
|
+
for (; ; ) {
|
|
151
|
+
const lineComment = indexOfLineComment(rest);
|
|
152
|
+
const open = indexOutsideFlowValues(rest, "/*");
|
|
153
|
+
if (lineComment >= 0 && (open < 0 || lineComment < open)) {
|
|
154
|
+
comments.push(rest.slice(lineComment));
|
|
155
|
+
return code + rest.slice(0, lineComment);
|
|
156
|
+
}
|
|
157
|
+
if (open < 0) return code + rest;
|
|
158
|
+
const close = rest.indexOf("*/", open + 2);
|
|
159
|
+
if (close < 0) {
|
|
160
|
+
comments.push(rest.slice(open));
|
|
161
|
+
this.inBlockComment = true;
|
|
162
|
+
return code + rest.slice(0, open);
|
|
163
|
+
}
|
|
164
|
+
comments.push(rest.slice(open, close + 2));
|
|
165
|
+
code += `${rest.slice(0, open)} `;
|
|
166
|
+
rest = rest.slice(close + 2);
|
|
72
167
|
}
|
|
73
|
-
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
function indexOutsideFlowValues(line, needle) {
|
|
171
|
+
for (let i = 0; i + needle.length <= line.length; i++) {
|
|
172
|
+
const valueEnd = flowValueEnd(line, i);
|
|
173
|
+
if (valueEnd > 0) i = valueEnd;
|
|
174
|
+
else if (line.startsWith(needle, i)) return i;
|
|
74
175
|
}
|
|
75
176
|
return -1;
|
|
76
177
|
}
|
|
@@ -95,36 +196,168 @@ function parseDecorators(line) {
|
|
|
95
196
|
});
|
|
96
197
|
return { decorators: dec, rest };
|
|
97
198
|
}
|
|
98
|
-
var
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
199
|
+
var COLOR_SOURCE = String.raw`\(\s*color\s+(#[0-9a-fA-F]{3,8}|[a-zA-Z][\w-]*)\s*\)`;
|
|
200
|
+
var HEIGHT_SOURCE = String.raw`\(\s{0,8}y\s{1,8}([^\s()]+)\s{0,8}\)`;
|
|
201
|
+
var LABEL_OFFSET_SOURCE = String.raw`label\s*\[\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\]`;
|
|
202
|
+
var COLOR_TOKEN_RE = new RegExp(COLOR_SOURCE, "iy");
|
|
203
|
+
var HEIGHT_TOKEN_RE = new RegExp(HEIGHT_SOURCE, "y");
|
|
204
|
+
var LABEL_TOKEN_RE = new RegExp(LABEL_OFFSET_SOURCE, "iy");
|
|
205
|
+
var URL_TOKEN_RE = /url\s{0,8}\(([^()]*)\)/iy;
|
|
206
|
+
var GROUP_TOKEN_RE = /\(([^()]*)\)/y;
|
|
207
|
+
var INERTIA_TOKEN_RE = /inertia\b/iy;
|
|
208
|
+
var WORD_TOKEN_RE = /\(?[^\s(]*/y;
|
|
209
|
+
function matchAt(pattern, text, at) {
|
|
210
|
+
pattern.lastIndex = at;
|
|
211
|
+
return pattern.exec(text);
|
|
212
|
+
}
|
|
213
|
+
function groupItems(content, contentAt) {
|
|
214
|
+
const items = [];
|
|
215
|
+
let itemStart = 0;
|
|
216
|
+
for (const raw of content.split(",")) {
|
|
217
|
+
const leading = raw.length - raw.trimStart().length;
|
|
218
|
+
const text = raw.trim();
|
|
219
|
+
if (text) items.push({ at: contentAt + itemStart + leading, text });
|
|
220
|
+
itemStart += raw.length + 1;
|
|
221
|
+
}
|
|
222
|
+
return items;
|
|
223
|
+
}
|
|
224
|
+
function readSuffixToken(text, at) {
|
|
225
|
+
let m;
|
|
226
|
+
if (m = matchAt(COLOR_TOKEN_RE, text, at)) {
|
|
227
|
+
return { at, text: m[0], kind: "color", color: m[1] };
|
|
228
|
+
}
|
|
229
|
+
if (m = matchAt(HEIGHT_TOKEN_RE, text, at)) {
|
|
230
|
+
return { at, text: m[0], kind: "height", value: m[1] };
|
|
231
|
+
}
|
|
232
|
+
if (m = matchAt(LABEL_TOKEN_RE, text, at)) {
|
|
233
|
+
return { at, text: m[0], kind: "label", dx: m[1], dy: m[2] };
|
|
234
|
+
}
|
|
235
|
+
if (m = matchAt(URL_TOKEN_RE, text, at)) {
|
|
236
|
+
return { at, text: m[0], kind: "url", ref: m[1].trim() };
|
|
237
|
+
}
|
|
238
|
+
if (m = matchAt(GROUP_TOKEN_RE, text, at)) {
|
|
239
|
+
return { at, text: m[0], kind: "group", items: groupItems(m[1], at + 1) };
|
|
240
|
+
}
|
|
241
|
+
if (m = matchAt(INERTIA_TOKEN_RE, text, at)) return { at, text: m[0], kind: "inertia" };
|
|
242
|
+
return { at, text: matchAt(WORD_TOKEN_RE, text, at)[0], kind: "word" };
|
|
103
243
|
}
|
|
104
|
-
|
|
244
|
+
function scanSuffix(text) {
|
|
245
|
+
const tokens = [];
|
|
246
|
+
let at = 0;
|
|
247
|
+
while (at < text.length) {
|
|
248
|
+
if (/\s/.test(text[at])) {
|
|
249
|
+
at++;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const token = readSuffixToken(text, at);
|
|
253
|
+
tokens.push(token);
|
|
254
|
+
at += token.text.length;
|
|
255
|
+
}
|
|
256
|
+
return tokens;
|
|
257
|
+
}
|
|
258
|
+
var WHOLE_COLOR_GROUP_RE = new RegExp(`^${COLOR_SOURCE}$`, "i");
|
|
259
|
+
var WHOLE_HEIGHT_GROUP_RE = new RegExp(`^${HEIGHT_SOURCE}$`);
|
|
260
|
+
function splitTrailingGroups(text, kinds) {
|
|
261
|
+
const groups = [];
|
|
262
|
+
let end = text.trimEnd().length;
|
|
263
|
+
while (text[end - 1] === ")") {
|
|
264
|
+
const open = text.lastIndexOf("(", end - 1);
|
|
265
|
+
const group = text.slice(Math.max(open, 0), end);
|
|
266
|
+
const wanted = kinds.color && WHOLE_COLOR_GROUP_RE.test(group) || kinds.height && WHOLE_HEIGHT_GROUP_RE.test(group);
|
|
267
|
+
if (open < 0 || !wanted) break;
|
|
268
|
+
groups.push(group);
|
|
269
|
+
end = open;
|
|
270
|
+
while (end > 0 && /\s/.test(text[end - 1])) end--;
|
|
271
|
+
}
|
|
272
|
+
return { rest: text.slice(0, end), groups: groups.reverse() };
|
|
273
|
+
}
|
|
274
|
+
function splitLeadingColor(text) {
|
|
275
|
+
const start = text.length - text.trimStart().length;
|
|
276
|
+
const m = matchAt(COLOR_TOKEN_RE, text, start);
|
|
277
|
+
if (!m) return { rest: text };
|
|
278
|
+
return { color: m[1], rest: text.slice(start + m[0].length) };
|
|
279
|
+
}
|
|
280
|
+
var MAX_UNCLAMPED_MAGNITUDE = 1e12;
|
|
281
|
+
function isRoundTrippableNumber(value) {
|
|
282
|
+
return Number.isFinite(value) && Math.abs(value) <= MAX_UNCLAMPED_MAGNITUDE;
|
|
283
|
+
}
|
|
284
|
+
function readUnclampedNumber(text) {
|
|
285
|
+
const value = Number(text);
|
|
286
|
+
return isRoundTrippableNumber(value) ? value : null;
|
|
287
|
+
}
|
|
288
|
+
var LABEL_OFFSET_RE = new RegExp(String.raw`\b${LABEL_OFFSET_SOURCE}`, "i");
|
|
105
289
|
function parseLabelOffset(line) {
|
|
106
290
|
const m = LABEL_OFFSET_RE.exec(line);
|
|
107
291
|
if (!m) return { labelOffset: null, rest: line };
|
|
108
|
-
const dx =
|
|
109
|
-
const dy =
|
|
292
|
+
const dx = readUnclampedNumber(m[1]);
|
|
293
|
+
const dy = readUnclampedNumber(m[2]);
|
|
110
294
|
const rest = line.replace(LABEL_OFFSET_RE, " ");
|
|
111
|
-
if (
|
|
295
|
+
if (dx === null || dy === null) return { labelOffset: null, rest, unreadable: m[0] };
|
|
112
296
|
return { labelOffset: { dx, dy }, rest };
|
|
113
297
|
}
|
|
114
|
-
var
|
|
115
|
-
function parseUrlRef(line) {
|
|
116
|
-
const m = URL_REF_RE.exec(line);
|
|
117
|
-
if (!m) return { urlRef: null, rest: line };
|
|
118
|
-
return { urlRef: m[1].trim() || null, rest: line.replace(URL_REF_RE, " ") };
|
|
119
|
-
}
|
|
298
|
+
var KEYWORD_RE = /^\s*([A-Za-z][\w-]*)(?![\w\p{L}\p{M}\p{N}-])/u;
|
|
120
299
|
function keywordOf(line) {
|
|
121
|
-
const m =
|
|
300
|
+
const m = KEYWORD_RE.exec(line);
|
|
122
301
|
return m ? m[1].toLowerCase() : "";
|
|
123
302
|
}
|
|
124
303
|
function slug(label) {
|
|
125
304
|
return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_/, "").replace(/_$/, "") || "x";
|
|
126
305
|
}
|
|
127
306
|
|
|
307
|
+
// src/names.ts
|
|
308
|
+
var NAMED_TYPES = /* @__PURE__ */ new Set([
|
|
309
|
+
"anchor",
|
|
310
|
+
"component",
|
|
311
|
+
"accelerator",
|
|
312
|
+
"submap"
|
|
313
|
+
]);
|
|
314
|
+
function defaultName(type) {
|
|
315
|
+
switch (type) {
|
|
316
|
+
case "anchor":
|
|
317
|
+
return "User";
|
|
318
|
+
case "accelerator":
|
|
319
|
+
return "Accelerator";
|
|
320
|
+
case "submap":
|
|
321
|
+
return "Submap";
|
|
322
|
+
case "pipeline":
|
|
323
|
+
return "Pipeline";
|
|
324
|
+
default:
|
|
325
|
+
return "Component";
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function roundAsWritten(n) {
|
|
329
|
+
return Math.round(n * 1e3) / 1e3;
|
|
330
|
+
}
|
|
331
|
+
var DIVISION_SLASH = "\u2215";
|
|
332
|
+
var FULLWIDTH_PLUS = "\uFF0B";
|
|
333
|
+
var FULLWIDTH_LEFT_BRACE = "\uFF5B";
|
|
334
|
+
function escapeCommentMarkers(text) {
|
|
335
|
+
return rewriteOutsideFlowValues(
|
|
336
|
+
text,
|
|
337
|
+
(part) => part.replace(/\/\*/g, `${DIVISION_SLASH}*`).replace(/(?<!:)\/\//g, DIVISION_SLASH.repeat(2))
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
function writtenText(text) {
|
|
341
|
+
return escapeCommentMarkers(text.replace(/\r?\n/g, " "));
|
|
342
|
+
}
|
|
343
|
+
function writtenNoteText(text) {
|
|
344
|
+
return escapeCommentMarkers(text).replace(/\r?\n/g, "\\n");
|
|
345
|
+
}
|
|
346
|
+
function writtenName(label, elementType) {
|
|
347
|
+
const name = label.replace(/\r?\n/g, " ").replace(/->/g, "\u2192").replace(/\+(?=['<>])/g, FULLWIDTH_PLUS).trim().replace(/^\{/, FULLWIDTH_LEFT_BRACE);
|
|
348
|
+
return escapeCommentMarkers(name) || defaultName(elementType);
|
|
349
|
+
}
|
|
350
|
+
var UniqueNames = class {
|
|
351
|
+
used = /* @__PURE__ */ new Set();
|
|
352
|
+
claim(base) {
|
|
353
|
+
let name = base;
|
|
354
|
+
let suffix = 2;
|
|
355
|
+
while (this.used.has(name)) name = `${base} ${suffix++}`;
|
|
356
|
+
this.used.add(name);
|
|
357
|
+
return name;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
128
361
|
// src/parser.ts
|
|
129
362
|
function splitDependency(core) {
|
|
130
363
|
const arrow = core.indexOf("->");
|
|
@@ -152,7 +385,7 @@ function splitFlow(core) {
|
|
|
152
385
|
}
|
|
153
386
|
return null;
|
|
154
387
|
}
|
|
155
|
-
var
|
|
388
|
+
var LINK_LIKE_STATEMENT_KEYWORDS = /* @__PURE__ */ new Set([
|
|
156
389
|
"title",
|
|
157
390
|
"style",
|
|
158
391
|
"size",
|
|
@@ -163,12 +396,111 @@ var NON_LINK_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
163
396
|
"evolve",
|
|
164
397
|
"line"
|
|
165
398
|
]);
|
|
399
|
+
var DECLARATION_KEYWORDS = /* @__PURE__ */ new Set([
|
|
400
|
+
"anchor",
|
|
401
|
+
"component",
|
|
402
|
+
"market",
|
|
403
|
+
"ecosystem",
|
|
404
|
+
"note",
|
|
405
|
+
"pioneers",
|
|
406
|
+
"settlers",
|
|
407
|
+
"townplanners",
|
|
408
|
+
"url",
|
|
409
|
+
"build",
|
|
410
|
+
"buy",
|
|
411
|
+
"outsource",
|
|
412
|
+
"accelerator",
|
|
413
|
+
"deaccelerator",
|
|
414
|
+
"submap"
|
|
415
|
+
]);
|
|
166
416
|
var KNOWN_STYLES = /* @__PURE__ */ new Set(["wardley", "handwritten", "colour", "dark"]);
|
|
167
417
|
function compact(obj) {
|
|
168
418
|
const out = {};
|
|
169
419
|
for (const [k, v] of Object.entries(obj)) if (v !== void 0) out[k] = v;
|
|
170
420
|
return out;
|
|
171
421
|
}
|
|
422
|
+
function linkCore(line) {
|
|
423
|
+
const semi = line.indexOf(";");
|
|
424
|
+
return semi >= 0 ? line.slice(0, semi) : line;
|
|
425
|
+
}
|
|
426
|
+
function readLink(line, lineNo, text) {
|
|
427
|
+
const semi = line.indexOf(";");
|
|
428
|
+
const core = (semi >= 0 ? line.slice(0, semi) : line).trim();
|
|
429
|
+
const label = semi >= 0 ? line.slice(semi + 1).trim() : "";
|
|
430
|
+
const common = { ...label ? { label } : {}, raw: line, lineNo, text };
|
|
431
|
+
const dependency = splitDependency(core);
|
|
432
|
+
if (dependency) return { ...dependency, kind: "dependency", ...common };
|
|
433
|
+
const flow = splitFlow(core);
|
|
434
|
+
if (!flow) return null;
|
|
435
|
+
return {
|
|
436
|
+
left: flow.left,
|
|
437
|
+
right: flow.right,
|
|
438
|
+
kind: "flow",
|
|
439
|
+
bidirectional: flow.op === "<>",
|
|
440
|
+
reverse: flow.op === "<",
|
|
441
|
+
...flow.value ? { flowValue: flow.value } : {},
|
|
442
|
+
...common
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function couldBeStatement(line, keyword, after) {
|
|
446
|
+
return LINK_LIKE_STATEMENT_KEYWORDS.has(keyword) || DECLARATION_KEYWORDS.has(keyword) && parseCoords(line) !== null || keyword === "url" && readUrlDefinition(after) !== null;
|
|
447
|
+
}
|
|
448
|
+
function namesAsWritten(link) {
|
|
449
|
+
const keyword = keywordOf(link.left);
|
|
450
|
+
const nameAfterKeyword = link.left.slice(keyword.length).trim();
|
|
451
|
+
return {
|
|
452
|
+
left: [keyword, nameAfterKeyword].filter(Boolean).join(" "),
|
|
453
|
+
right: link.right.split("->").map((part) => part.trim()).join("->")
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function leftNameIsDeclared(link, names) {
|
|
457
|
+
return names.has(link.left) || names.has(namesAsWritten(link).left);
|
|
458
|
+
}
|
|
459
|
+
function unknownStatementMessage(line) {
|
|
460
|
+
const core = linkCore(line);
|
|
461
|
+
if (/->|\+('[^']*')?(<>|>|<)/.test(core)) return "Link needs a name on both sides";
|
|
462
|
+
const flowValue = /\+'[^']*'/.exec(core);
|
|
463
|
+
if (flowValue) return `Flow ${flowValue[0]} has no direction \u2014 add ">", "<" or "<>"`;
|
|
464
|
+
return `Unknown statement "${line.split(/\s/)[0]}"`;
|
|
465
|
+
}
|
|
466
|
+
function buildEdges(links, idOfName, unresolved, labelOf) {
|
|
467
|
+
let dependencies = 0;
|
|
468
|
+
let flows = 0;
|
|
469
|
+
const edges = [];
|
|
470
|
+
for (const link of links) {
|
|
471
|
+
const fromId = idOfName(link.left) ?? idOfName(link.writtenNames?.left ?? link.left);
|
|
472
|
+
const toId = idOfName(link.right) ?? idOfName(link.writtenNames?.right ?? link.right);
|
|
473
|
+
if (!fromId || !toId) {
|
|
474
|
+
unresolved(link, fromId ? link.right : link.left);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (link.kind === "dependency") {
|
|
478
|
+
edges.push(
|
|
479
|
+
compact({
|
|
480
|
+
id: `dep_${++dependencies}`,
|
|
481
|
+
edgeType: "dependency",
|
|
482
|
+
from: fromId,
|
|
483
|
+
to: toId,
|
|
484
|
+
label: labelOf(link)
|
|
485
|
+
})
|
|
486
|
+
);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const [from, to] = link.reverse ? [toId, fromId] : [fromId, toId];
|
|
490
|
+
edges.push(
|
|
491
|
+
compact({
|
|
492
|
+
id: `flow_${++flows}`,
|
|
493
|
+
edgeType: "flow",
|
|
494
|
+
from,
|
|
495
|
+
to,
|
|
496
|
+
bidirectional: link.bidirectional ? true : void 0,
|
|
497
|
+
flowValue: link.flowValue,
|
|
498
|
+
label: labelOf(link)
|
|
499
|
+
})
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
return edges;
|
|
503
|
+
}
|
|
172
504
|
var IdAllocator = class {
|
|
173
505
|
used = /* @__PURE__ */ new Set();
|
|
174
506
|
alloc(prefix, label) {
|
|
@@ -184,612 +516,1007 @@ function parseDSL(text) {
|
|
|
184
516
|
return parseDSLWithDiagnostics(text).map;
|
|
185
517
|
}
|
|
186
518
|
function parseDSLWithDiagnostics(text) {
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
519
|
+
const reading = new MapReader(/* @__PURE__ */ new Set()).read(text);
|
|
520
|
+
const needsDeclaredNames = reading.keywordLinks.some(
|
|
521
|
+
(link) => leftNameIsDeclared(link, reading.declaredNames)
|
|
522
|
+
);
|
|
523
|
+
return needsDeclaredNames ? new MapReader(reading.declaredNames).read(text).result : reading.result;
|
|
524
|
+
}
|
|
525
|
+
var MapReader = class {
|
|
526
|
+
constructor(linkNames) {
|
|
527
|
+
this.linkNames = linkNames;
|
|
528
|
+
}
|
|
529
|
+
linkNames;
|
|
530
|
+
diagnostics = [];
|
|
531
|
+
ids = new IdAllocator();
|
|
532
|
+
namesInUse = new UniqueNames();
|
|
533
|
+
/** The first element declared under a name wins it. */
|
|
534
|
+
idBySourceName = /* @__PURE__ */ new Map();
|
|
535
|
+
idByElementName = /* @__PURE__ */ new Map();
|
|
536
|
+
elements = [];
|
|
537
|
+
passthrough = [];
|
|
538
|
+
pendingLinks = [];
|
|
539
|
+
keywordLinks = [];
|
|
540
|
+
pendingEvolve = [];
|
|
541
|
+
pendingPipeline = [];
|
|
542
|
+
pendingMethod = [];
|
|
543
|
+
config = { title: "Untitled Map" };
|
|
544
|
+
lineThatSetConfig = /* @__PURE__ */ new Map();
|
|
545
|
+
unusedConfigLines = [];
|
|
546
|
+
annotationCounter = 0;
|
|
547
|
+
comments = new CommentStripper();
|
|
548
|
+
urlDefinitions = [];
|
|
549
|
+
pendingUrlRefs = [];
|
|
550
|
+
/** Candidate for a `{` block opening: a block binds only to the directly preceding pipeline line. */
|
|
551
|
+
lastPipeline = null;
|
|
552
|
+
blockPipeline = null;
|
|
553
|
+
blockOpening = { line: 0, text: "" };
|
|
554
|
+
lineNo = 0;
|
|
555
|
+
currentLine = "";
|
|
556
|
+
report = (message) => this.diag(message);
|
|
557
|
+
reportAt = (message, line, text) => this.diag(message, line, text);
|
|
558
|
+
diag(message, atLine = this.lineNo, text = this.currentLine) {
|
|
559
|
+
this.diagnostics.push({ line: atLine, message, text });
|
|
560
|
+
}
|
|
561
|
+
keepLine(statement, lineNo = this.lineNo, source = this.currentLine) {
|
|
562
|
+
this.passthrough.push({ text: statement, isComment: false, lineNo, source });
|
|
563
|
+
}
|
|
564
|
+
keep(statement, message) {
|
|
565
|
+
this.keepLine(statement);
|
|
566
|
+
this.diag(message);
|
|
567
|
+
}
|
|
568
|
+
/** For a line that looks like a known construct but cannot be parsed. */
|
|
569
|
+
failed(line) {
|
|
570
|
+
this.keep(line, "Line could not be interpreted (kept losslessly in rawPassthrough)");
|
|
571
|
+
}
|
|
572
|
+
// A reference binds to the name as written first, so `X 2` stays bound to an explicit "X 2" even
|
|
573
|
+
// when a duplicate "X" was renamed to "X 2"; the serializer then writes that reference by the
|
|
574
|
+
// element's new name, and the next parse binds it the same way.
|
|
575
|
+
idOfName(name) {
|
|
576
|
+
return this.idBySourceName.get(name) ?? this.idByElementName.get(name);
|
|
577
|
+
}
|
|
578
|
+
register(sourceName, elementName, id) {
|
|
579
|
+
if (!this.idBySourceName.has(sourceName)) this.idBySourceName.set(sourceName, id);
|
|
580
|
+
this.idByElementName.set(elementName, id);
|
|
581
|
+
}
|
|
582
|
+
setConfig(keyword, update) {
|
|
583
|
+
const earlier = this.lineThatSetConfig.get(keyword);
|
|
584
|
+
if (earlier !== void 0) {
|
|
585
|
+
this.diag(`Repeated "${keyword}" \u2014 replaces the one on line ${earlier}`);
|
|
586
|
+
}
|
|
587
|
+
this.lineThatSetConfig.set(keyword, this.lineNo);
|
|
588
|
+
this.config = update;
|
|
589
|
+
}
|
|
590
|
+
keepUnusedConfigLine(keyword, statement, reason) {
|
|
591
|
+
this.unusedConfigLines.push({
|
|
592
|
+
keyword,
|
|
593
|
+
reason,
|
|
594
|
+
index: this.passthrough.length,
|
|
595
|
+
line: this.lineNo,
|
|
596
|
+
text: this.currentLine
|
|
597
|
+
});
|
|
598
|
+
this.keepLine(statement);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Links are read before statements, as names may begin with a keyword (`Component -> X`); a link
|
|
602
|
+
* has no `[a, b]` tuple before its `;`. A line that could also be its keyword's statement is a
|
|
603
|
+
* link only when both names are declared.
|
|
604
|
+
*/
|
|
605
|
+
readsAsLink(line, kw, after) {
|
|
606
|
+
const link = parseCoords(linkCore(line)) ? null : readLink(line, this.lineNo, this.currentLine);
|
|
607
|
+
if (!link) return false;
|
|
608
|
+
if (!couldBeStatement(line, kw, after)) {
|
|
609
|
+
this.pendingLinks.push(link);
|
|
223
610
|
return true;
|
|
224
611
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
bidirectional: flow.op === "<>",
|
|
231
|
-
reverse: flow.op === "<",
|
|
232
|
-
...flow.value ? { flowValue: flow.value } : {},
|
|
233
|
-
...linkLabel ? { label: linkLabel } : {},
|
|
234
|
-
raw: line,
|
|
235
|
-
lineNo
|
|
236
|
-
});
|
|
612
|
+
const declared = this.linkNames;
|
|
613
|
+
const written = namesAsWritten(link);
|
|
614
|
+
const leftDeclared = declared.has(link.left) || declared.has(written.left);
|
|
615
|
+
if (leftDeclared && (declared.has(link.right) || declared.has(written.right))) {
|
|
616
|
+
this.pendingLinks.push({ ...link, writtenNames: written });
|
|
237
617
|
return true;
|
|
238
618
|
}
|
|
619
|
+
this.keywordLinks.push(link);
|
|
620
|
+
if (leftDeclared) this.diag(`Link: "${link.right}" not found \u2014 read as a "${kw}" statement`);
|
|
239
621
|
return false;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
diag("Line could not be interpreted (kept losslessly in rawPassthrough)");
|
|
245
|
-
};
|
|
246
|
-
const clampDiag = (n) => {
|
|
247
|
-
if (n < 0 || n > 1) diag(`Coordinate ${n} is outside [0,1] and was clamped`);
|
|
622
|
+
}
|
|
623
|
+
/** Clamps a normalized value to [0,1] — with a diagnostic instead of a later validation crash. */
|
|
624
|
+
clamp(n, report = this.report) {
|
|
625
|
+
if (n < 0 || n > 1) report(`Coordinate ${n} is outside [0,1] and was clamped`);
|
|
248
626
|
return n < 0 ? 0 : n > 1 ? 1 : n;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
visibility:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const close = working.indexOf("*/", open + 2);
|
|
274
|
-
if (close < 0) {
|
|
275
|
-
rawPassthrough.push(working.slice(open));
|
|
276
|
-
working = working.slice(0, open);
|
|
277
|
-
inBlockComment = true;
|
|
278
|
-
break;
|
|
279
|
-
}
|
|
280
|
-
rawPassthrough.push(working.slice(open, close + 2));
|
|
281
|
-
working = `${working.slice(0, open)} ${working.slice(close + 2)}`;
|
|
282
|
-
open = indexOfOutsideQuotes(working, "/*");
|
|
283
|
-
}
|
|
284
|
-
const { code, comment } = splitLineComment(working);
|
|
285
|
-
if (comment !== null) rawPassthrough.push(comment);
|
|
286
|
-
working = code;
|
|
287
|
-
}
|
|
288
|
-
const line = working.trim();
|
|
289
|
-
if (!line) continue;
|
|
290
|
-
if (blockPipeline) {
|
|
291
|
-
if (line === "}") {
|
|
292
|
-
blockPipeline = null;
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
const childKw = keywordOf(line);
|
|
296
|
-
if (childKw === "component") {
|
|
297
|
-
const child = parseBlockChild(line.slice(childKw.length).trim());
|
|
298
|
-
if (child) {
|
|
299
|
-
blockPipeline.children.push(child);
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
rawPassthrough.push(line);
|
|
304
|
-
continue;
|
|
627
|
+
}
|
|
628
|
+
position(visibility, evolution) {
|
|
629
|
+
return { visibility: this.clamp(visibility), evolution: this.clamp(evolution) };
|
|
630
|
+
}
|
|
631
|
+
reportTextBefore(before) {
|
|
632
|
+
if (before) this.diag(`Ignored text before the coordinates: "${before}"`);
|
|
633
|
+
}
|
|
634
|
+
/** A suffix written in front of the coordinates is part of the name or text (OWM). */
|
|
635
|
+
reportMisplacedSuffix(groups, where, fix) {
|
|
636
|
+
if (groups.length) this.diag(`"${groups.join(" ")}" ${where} \u2014 ${fix}`);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* A text as the serializer writes it. Its comment markers are protected only by flow values
|
|
640
|
+
* `+'…'` within the text itself; one that other parts of the line protected is escaped now,
|
|
641
|
+
* not on the next parse.
|
|
642
|
+
*/
|
|
643
|
+
readText(text, line = this.lineNo, source = this.currentLine) {
|
|
644
|
+
const written = escapeCommentMarkers(text);
|
|
645
|
+
if (written !== text) {
|
|
646
|
+
this.diag(
|
|
647
|
+
`"${text}" read as "${written}" \u2014 "//" or "/*" there would start a comment once saved`,
|
|
648
|
+
line,
|
|
649
|
+
source
|
|
650
|
+
);
|
|
305
651
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
652
|
+
return written;
|
|
653
|
+
}
|
|
654
|
+
/** Names an element the way the serializer writes it, so the next parse yields the same name. */
|
|
655
|
+
nameElement(sourceName, elementType, atLine = this.lineNo, atText = this.currentLine) {
|
|
656
|
+
const base = writtenName(sourceName, elementType);
|
|
657
|
+
const name = this.namesInUse.claim(base);
|
|
658
|
+
if (name !== base) {
|
|
659
|
+
this.diag(
|
|
660
|
+
`Duplicate name "${sourceName}" \u2014 renamed to "${name}"; links bind to the first "${sourceName}"`,
|
|
661
|
+
atLine,
|
|
662
|
+
atText
|
|
663
|
+
);
|
|
664
|
+
} else if (name !== sourceName) {
|
|
665
|
+
this.diag(renamedMessage(sourceName, name), atLine, atText);
|
|
666
|
+
}
|
|
667
|
+
return name;
|
|
668
|
+
}
|
|
669
|
+
openBlock() {
|
|
670
|
+
if (this.lastPipeline) {
|
|
671
|
+
this.blockPipeline = this.lastPipeline;
|
|
672
|
+
this.blockOpening = { line: this.lineNo, text: this.currentLine };
|
|
673
|
+
} else this.diag('Ignored "{" \u2014 a block must directly follow a pipeline line');
|
|
674
|
+
this.lastPipeline = null;
|
|
675
|
+
}
|
|
676
|
+
closeBlock() {
|
|
677
|
+
if (this.blockPipeline) this.blockPipeline = null;
|
|
678
|
+
else this.diag('Ignored "}" \u2014 no pipeline block is open');
|
|
679
|
+
this.lastPipeline = null;
|
|
680
|
+
}
|
|
681
|
+
endUnclosedBlock() {
|
|
682
|
+
if (!this.blockPipeline) return;
|
|
683
|
+
this.diag(
|
|
684
|
+
`Pipeline block of "${this.blockPipeline.name}" is never closed \u2014 add "}"`,
|
|
685
|
+
this.blockOpening.line,
|
|
686
|
+
this.blockOpening.text
|
|
687
|
+
);
|
|
688
|
+
this.blockPipeline = null;
|
|
689
|
+
}
|
|
690
|
+
/** A `url(Name)` reference on the element just pushed. */
|
|
691
|
+
addUrlRef(ref) {
|
|
692
|
+
this.pendingUrlRefs.push({
|
|
693
|
+
index: this.elements.length - 1,
|
|
694
|
+
ref,
|
|
695
|
+
line: this.lineNo,
|
|
696
|
+
text: this.currentLine
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
read(text) {
|
|
700
|
+
const sourceLines = text.split(/\r?\n/);
|
|
701
|
+
if (sourceLines.length > 1 && sourceLines[sourceLines.length - 1] === "") sourceLines.pop();
|
|
702
|
+
let blockCommentStart = null;
|
|
703
|
+
for (let i = 0; i < sourceLines.length; i++) {
|
|
704
|
+
const raw = withoutTrailingCarriageReturns(sourceLines[i]);
|
|
705
|
+
this.lineNo = i + 1;
|
|
706
|
+
this.currentLine = raw.trim();
|
|
707
|
+
const stripped = this.comments.strip(raw);
|
|
708
|
+
if (!this.comments.insideBlockComment) blockCommentStart = null;
|
|
709
|
+
else blockCommentStart ??= { line: this.lineNo, text: this.currentLine };
|
|
710
|
+
for (const fragment of stripped.comments) {
|
|
711
|
+
this.passthrough.push({
|
|
712
|
+
text: fragment,
|
|
713
|
+
isComment: true,
|
|
714
|
+
lineNo: this.lineNo,
|
|
715
|
+
source: this.currentLine
|
|
716
|
+
});
|
|
311
717
|
}
|
|
312
|
-
|
|
313
|
-
continue;
|
|
718
|
+
this.readCode(stripped.code.trim());
|
|
314
719
|
}
|
|
720
|
+
this.endUnclosedBlock();
|
|
721
|
+
if (blockCommentStart) {
|
|
722
|
+
const { line, text: startText } = blockCommentStart;
|
|
723
|
+
this.diag(
|
|
724
|
+
'Block comment "/*" is never closed \u2014 the rest of the file is a comment',
|
|
725
|
+
line,
|
|
726
|
+
startText
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
return this.finish();
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Braces are structural tokens: `{` at the start of a line, and `}` there while a block is open
|
|
733
|
+
* or on its own; `}` also ends a block line. A name starting with `}` stays a name.
|
|
734
|
+
*/
|
|
735
|
+
readCode(code) {
|
|
736
|
+
let statement = code;
|
|
737
|
+
for (; ; ) {
|
|
738
|
+
if (statement.startsWith("{")) this.openBlock();
|
|
739
|
+
else if (statement.startsWith("}") && (this.blockPipeline || statement === "}")) {
|
|
740
|
+
this.closeBlock();
|
|
741
|
+
} else break;
|
|
742
|
+
statement = statement.slice(1).trim();
|
|
743
|
+
}
|
|
744
|
+
const closesBlock = this.blockPipeline !== null && statement.endsWith("}");
|
|
745
|
+
if (closesBlock) statement = statement.slice(0, -1).trim();
|
|
746
|
+
if (statement) this.readStatement(statement);
|
|
747
|
+
if (closesBlock) this.blockPipeline = null;
|
|
748
|
+
}
|
|
749
|
+
readStatement(line) {
|
|
315
750
|
const kw = keywordOf(line);
|
|
316
751
|
const after = line.slice(kw.length).trim();
|
|
317
|
-
if (kw
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
752
|
+
if (this.blockPipeline && kw === "component" && this.readBlockChild(after)) return;
|
|
753
|
+
this.lastPipeline = null;
|
|
754
|
+
if (this.readsAsLink(line, kw, after)) return;
|
|
321
755
|
switch (kw) {
|
|
322
756
|
case "title":
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
case "anchor":
|
|
326
|
-
|
|
327
|
-
if (!node) {
|
|
328
|
-
failed(line);
|
|
329
|
-
break;
|
|
330
|
-
}
|
|
331
|
-
const id = ids.alloc("anchor", node.name);
|
|
332
|
-
const anchor = compact({
|
|
333
|
-
id,
|
|
334
|
-
elementType: "anchor",
|
|
335
|
-
label: node.name,
|
|
336
|
-
position: pos(node.coords.a, node.coords.b),
|
|
337
|
-
labelOffset: node.labelOffset,
|
|
338
|
-
color: node.color
|
|
339
|
-
});
|
|
340
|
-
elements.push(anchor);
|
|
341
|
-
register(node.name, id);
|
|
342
|
-
break;
|
|
343
|
-
}
|
|
757
|
+
this.setConfig("title", { ...this.config, title: this.readText(after) });
|
|
758
|
+
return;
|
|
759
|
+
case "anchor":
|
|
760
|
+
return this.readAnchor(line, after);
|
|
344
761
|
case "component":
|
|
345
762
|
case "market":
|
|
346
|
-
case "ecosystem":
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const { color, rest } = parseColor(after);
|
|
370
|
-
const coords = parseCoords(rest);
|
|
371
|
-
const textPart = stripCoords(rest).trim().replace(/\\n/g, "\n");
|
|
372
|
-
if (!coords) {
|
|
373
|
-
failed(line);
|
|
374
|
-
break;
|
|
375
|
-
}
|
|
376
|
-
const id = ids.alloc("note", textPart || "note");
|
|
377
|
-
const note = compact({
|
|
378
|
-
id,
|
|
379
|
-
elementType: "note",
|
|
380
|
-
label: textPart,
|
|
381
|
-
position: pos(coords.a, coords.b),
|
|
382
|
-
color
|
|
383
|
-
});
|
|
384
|
-
elements.push(note);
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
|
-
case "pipeline": {
|
|
388
|
-
let body = after;
|
|
389
|
-
let opensBlock = false;
|
|
390
|
-
if (body.endsWith("{")) {
|
|
391
|
-
opensBlock = true;
|
|
392
|
-
body = body.slice(0, -1).trim();
|
|
393
|
-
}
|
|
394
|
-
const col = parseColor(body);
|
|
395
|
-
const yMatch = /\(\s{0,8}y\s{1,8}([\d.]+)\s{0,8}\)/.exec(col.rest);
|
|
396
|
-
const bodyRest = yMatch ? col.rest.replace(yMatch[0], " ") : col.rest;
|
|
397
|
-
const coords = parseCoords(bodyRest);
|
|
398
|
-
const name = stripCoords(bodyRest).trim();
|
|
399
|
-
if (!name) {
|
|
400
|
-
failed(line);
|
|
401
|
-
break;
|
|
402
|
-
}
|
|
403
|
-
const pending = {
|
|
404
|
-
name,
|
|
405
|
-
...coords ? { start: coords.a, end: coords.b } : {},
|
|
406
|
-
...yMatch ? { visibility: clamp01(Number(yMatch[1])) } : {},
|
|
407
|
-
...col.color ? { color: col.color } : {},
|
|
408
|
-
raw: line,
|
|
409
|
-
children: []
|
|
410
|
-
};
|
|
411
|
-
pendingPipeline.push(pending);
|
|
412
|
-
if (opensBlock) blockPipeline = pending;
|
|
413
|
-
else lastPipeline = pending;
|
|
414
|
-
break;
|
|
415
|
-
}
|
|
416
|
-
case "evolve": {
|
|
417
|
-
const ev = parseEvolve(after);
|
|
418
|
-
if (!ev) {
|
|
419
|
-
failed(line);
|
|
420
|
-
break;
|
|
421
|
-
}
|
|
422
|
-
pendingEvolve.push({ ...ev, target: clampDiag(ev.target), raw: line, lineNo });
|
|
423
|
-
break;
|
|
424
|
-
}
|
|
425
|
-
case "style": {
|
|
426
|
-
const s = after.toLowerCase();
|
|
427
|
-
if (KNOWN_STYLES.has(s)) config = { ...config, style: s };
|
|
428
|
-
else failed(line);
|
|
429
|
-
break;
|
|
430
|
-
}
|
|
431
|
-
case "size": {
|
|
432
|
-
const coords = parseCoords(after);
|
|
433
|
-
if (coords) config = { ...config, size: { width: coords.a, height: coords.b } };
|
|
434
|
-
else failed(line);
|
|
435
|
-
break;
|
|
436
|
-
}
|
|
437
|
-
case "evolution": {
|
|
438
|
-
const parts = after.split("->").map((s) => s.trim());
|
|
439
|
-
if (parts.length === 4) {
|
|
440
|
-
config = { ...config, evolutionLabels: [parts[0], parts[1], parts[2], parts[3]] };
|
|
441
|
-
} else rawPassthrough.push(raw);
|
|
442
|
-
break;
|
|
443
|
-
}
|
|
444
|
-
case "y-axis": {
|
|
445
|
-
const parts = after.split("->").map((s) => s.trim()).filter(Boolean);
|
|
446
|
-
if (parts.length) {
|
|
447
|
-
config = {
|
|
448
|
-
...config,
|
|
449
|
-
yAxisLabel: parts[0],
|
|
450
|
-
...parts.length >= 3 ? { yAxisEndLabels: [parts[1], parts[2]] } : {}
|
|
451
|
-
};
|
|
452
|
-
} else failed(line);
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
|
-
case "annotations": {
|
|
456
|
-
const coords = parseCoords(after);
|
|
457
|
-
if (coords) {
|
|
458
|
-
config = {
|
|
459
|
-
...config,
|
|
460
|
-
annotationsBoxPosition: pos(coords.a, coords.b)
|
|
461
|
-
};
|
|
462
|
-
} else rawPassthrough.push(raw);
|
|
463
|
-
break;
|
|
464
|
-
}
|
|
465
|
-
case "annotation": {
|
|
466
|
-
const numMatch = /^\s*(\d+)/.exec(after);
|
|
467
|
-
const number = numMatch ? Number(numMatch[1]) : ++annoCounter;
|
|
468
|
-
const colA = parseColor(after.replace(/^\s*\d+\s*/, ""));
|
|
469
|
-
const afterNum = colA.rest;
|
|
470
|
-
const multi = parseMultiCoords(afterNum);
|
|
471
|
-
let positions;
|
|
472
|
-
let text2;
|
|
473
|
-
if (multi) {
|
|
474
|
-
positions = multi.tuples.map((t) => pos(t.a, t.b));
|
|
475
|
-
text2 = multi.rest.trim();
|
|
476
|
-
} else {
|
|
477
|
-
const coords = parseCoords(afterNum);
|
|
478
|
-
if (!coords) {
|
|
479
|
-
rawPassthrough.push(line);
|
|
480
|
-
break;
|
|
481
|
-
}
|
|
482
|
-
positions = [pos(coords.a, coords.b)];
|
|
483
|
-
text2 = stripCoords(afterNum).trim();
|
|
484
|
-
}
|
|
485
|
-
const annotation = {
|
|
486
|
-
id: ids.alloc("anno", String(number)),
|
|
487
|
-
elementType: "annotation",
|
|
488
|
-
label: text2,
|
|
489
|
-
position: positions[0],
|
|
490
|
-
number,
|
|
491
|
-
positions,
|
|
492
|
-
text: text2,
|
|
493
|
-
...colA.color ? { color: colA.color } : {}
|
|
494
|
-
};
|
|
495
|
-
elements.push(annotation);
|
|
496
|
-
break;
|
|
497
|
-
}
|
|
498
|
-
case "line": {
|
|
499
|
-
const col = parseColor(after);
|
|
500
|
-
const multi = parseMultiCoords(col.rest);
|
|
501
|
-
if (!multi || multi.tuples.length < 2) {
|
|
502
|
-
failed(line);
|
|
503
|
-
break;
|
|
504
|
-
}
|
|
505
|
-
const flags = multi.rest.toLowerCase();
|
|
506
|
-
const strokeStyle = flags.includes("(dashed)") ? "dashed" : flags.includes("(dotted)") ? "dotted" : void 0;
|
|
507
|
-
const points = multi.tuples.map((t) => pos(t.a, t.b));
|
|
508
|
-
const drawing = {
|
|
509
|
-
id: ids.alloc("draw", "line"),
|
|
510
|
-
elementType: "drawing",
|
|
511
|
-
label: "",
|
|
512
|
-
position: points[0],
|
|
513
|
-
points,
|
|
514
|
-
...flags.includes("(closed)") ? { closed: true } : {},
|
|
515
|
-
...strokeStyle ? { strokeStyle } : {},
|
|
516
|
-
...col.color ? { color: col.color } : {}
|
|
517
|
-
};
|
|
518
|
-
elements.push(drawing);
|
|
519
|
-
break;
|
|
520
|
-
}
|
|
763
|
+
case "ecosystem":
|
|
764
|
+
if (!this.pushComponent(after, kw)) this.failed(line);
|
|
765
|
+
return;
|
|
766
|
+
case "note":
|
|
767
|
+
return this.readNote(line, after);
|
|
768
|
+
case "pipeline":
|
|
769
|
+
return this.readPipeline(line, after);
|
|
770
|
+
case "evolve":
|
|
771
|
+
return this.readEvolve(line, after);
|
|
772
|
+
case "style":
|
|
773
|
+
return this.readStyle(line, after);
|
|
774
|
+
case "size":
|
|
775
|
+
return this.readSize(line, after);
|
|
776
|
+
case "evolution":
|
|
777
|
+
return this.readEvolution(line, after);
|
|
778
|
+
case "y-axis":
|
|
779
|
+
return this.readYAxis(line, after);
|
|
780
|
+
case "annotations":
|
|
781
|
+
return this.readAnnotationsBox(line, after);
|
|
782
|
+
case "annotation":
|
|
783
|
+
return this.readAnnotation(line, after);
|
|
784
|
+
case "line":
|
|
785
|
+
return this.readDrawing(line, after);
|
|
521
786
|
case "pioneers":
|
|
522
787
|
case "settlers":
|
|
523
|
-
case "townplanners":
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
failed(line);
|
|
528
|
-
break;
|
|
529
|
-
}
|
|
530
|
-
elements.push(
|
|
531
|
-
makeAttitude(ids, kw, four.a, four.b, four.c, four.d, col.color)
|
|
532
|
-
);
|
|
533
|
-
break;
|
|
534
|
-
}
|
|
535
|
-
case "url": {
|
|
536
|
-
const open = after.indexOf("[");
|
|
537
|
-
const close = after.lastIndexOf("]");
|
|
538
|
-
const name = open > 0 ? after.slice(0, open).trim() : "";
|
|
539
|
-
const address = open > 0 && close > open ? after.slice(open + 1, close).trim() : "";
|
|
540
|
-
if (!name || !address || after.slice(close + 1).trim() !== "") {
|
|
541
|
-
failed(line);
|
|
542
|
-
break;
|
|
543
|
-
}
|
|
544
|
-
urlDefs.set(name, address);
|
|
545
|
-
break;
|
|
546
|
-
}
|
|
788
|
+
case "townplanners":
|
|
789
|
+
return this.readAttitude(line, after, kw);
|
|
790
|
+
case "url":
|
|
791
|
+
return this.readUrlDefinition(line, after);
|
|
547
792
|
case "build":
|
|
548
793
|
case "buy":
|
|
549
|
-
case "outsource":
|
|
550
|
-
|
|
551
|
-
if (node) {
|
|
552
|
-
const id = ids.alloc("cmp", node.name);
|
|
553
|
-
const component = compact({
|
|
554
|
-
id,
|
|
555
|
-
elementType: "component",
|
|
556
|
-
label: node.name,
|
|
557
|
-
position: pos(node.coords.a, node.coords.b),
|
|
558
|
-
labelOffset: node.labelOffset,
|
|
559
|
-
decorators: { ...node.decorators, method: kw },
|
|
560
|
-
color: node.color
|
|
561
|
-
});
|
|
562
|
-
elements.push(component);
|
|
563
|
-
register(node.name, id);
|
|
564
|
-
} else if (after.trim()) {
|
|
565
|
-
pendingMethod.push({ name: after.trim(), method: kw, raw: line, lineNo });
|
|
566
|
-
} else {
|
|
567
|
-
rawPassthrough.push(line);
|
|
568
|
-
}
|
|
569
|
-
break;
|
|
570
|
-
}
|
|
794
|
+
case "outsource":
|
|
795
|
+
return this.readMethod(line, after, kw);
|
|
571
796
|
case "accelerator":
|
|
572
|
-
case "deaccelerator":
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
break;
|
|
577
|
-
}
|
|
578
|
-
const id = ids.alloc("accel", node.name);
|
|
579
|
-
const accelerator = {
|
|
580
|
-
id,
|
|
581
|
-
elementType: "accelerator",
|
|
582
|
-
direction: kw === "deaccelerator" ? "deaccelerate" : "accelerate",
|
|
583
|
-
label: node.name,
|
|
584
|
-
position: pos(node.coords.a, node.coords.b),
|
|
585
|
-
...node.color ? { color: node.color } : {}
|
|
586
|
-
};
|
|
587
|
-
elements.push(accelerator);
|
|
588
|
-
register(node.name, id);
|
|
589
|
-
break;
|
|
590
|
-
}
|
|
591
|
-
case "submap": {
|
|
592
|
-
const node = parseNode(after);
|
|
593
|
-
if (!node) {
|
|
594
|
-
failed(line);
|
|
595
|
-
break;
|
|
596
|
-
}
|
|
597
|
-
const id = ids.alloc("submap", node.name);
|
|
598
|
-
const submap = {
|
|
599
|
-
id,
|
|
600
|
-
elementType: "submap",
|
|
601
|
-
label: node.name,
|
|
602
|
-
position: pos(node.coords.a, node.coords.b),
|
|
603
|
-
...node.color ? { color: node.color } : {}
|
|
604
|
-
};
|
|
605
|
-
elements.push(submap);
|
|
606
|
-
if (node.urlRef) pendingUrlRefs.push({ index: elements.length - 1, ref: node.urlRef });
|
|
607
|
-
register(node.name, id);
|
|
608
|
-
break;
|
|
609
|
-
}
|
|
797
|
+
case "deaccelerator":
|
|
798
|
+
return this.readAccelerator(line, after, kw);
|
|
799
|
+
case "submap":
|
|
800
|
+
return this.readSubmap(line, after);
|
|
610
801
|
default: {
|
|
611
|
-
|
|
802
|
+
const link = readLink(line, this.lineNo, this.currentLine);
|
|
803
|
+
if (link) this.pendingLinks.push(link);
|
|
804
|
+
else this.keep(line, unknownStatementMessage(line));
|
|
612
805
|
}
|
|
613
806
|
}
|
|
614
807
|
}
|
|
615
|
-
|
|
616
|
-
const
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
compact({
|
|
627
|
-
id: cid,
|
|
628
|
-
elementType: "component",
|
|
629
|
-
label: c.name,
|
|
630
|
-
position: { visibility, evolution: clamp01(c.maturity) },
|
|
631
|
-
labelOffset: c.labelOffset,
|
|
632
|
-
decorators: Object.keys(c.decorators).length ? c.decorators : void 0,
|
|
633
|
-
pipelineId: id,
|
|
634
|
-
color: c.color
|
|
635
|
-
})
|
|
636
|
-
);
|
|
637
|
-
register(c.name, cid);
|
|
638
|
-
childIds.push(cid);
|
|
808
|
+
readBlockChild(after) {
|
|
809
|
+
const blockPipeline = this.blockPipeline;
|
|
810
|
+
const child = parseBlockChild(after, this.report);
|
|
811
|
+
if (child) {
|
|
812
|
+
blockPipeline.children.push({
|
|
813
|
+
...child,
|
|
814
|
+
maturity: this.clamp(child.maturity),
|
|
815
|
+
lineNo: this.lineNo,
|
|
816
|
+
text: this.currentLine
|
|
817
|
+
});
|
|
818
|
+
return true;
|
|
639
819
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
continue;
|
|
820
|
+
if (splitAtCoords(after)?.name) {
|
|
821
|
+
this.diag(
|
|
822
|
+
`Component with [visibility, maturity] inside the block of pipeline "${blockPipeline.name}" is not a child \u2014 use [maturity]`
|
|
823
|
+
);
|
|
645
824
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
const
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
readAnchor(line, after) {
|
|
828
|
+
const node = parseNode(after, ANCHOR_SUFFIXES, this.report);
|
|
829
|
+
if (!node) return this.failed(line);
|
|
830
|
+
const label = this.nameElement(node.name, "anchor");
|
|
831
|
+
const id = this.ids.alloc("anchor", label);
|
|
832
|
+
const anchor = compact({
|
|
833
|
+
id,
|
|
834
|
+
elementType: "anchor",
|
|
835
|
+
label,
|
|
836
|
+
position: this.position(node.coords.a, node.coords.b),
|
|
837
|
+
labelOffset: node.labelOffset,
|
|
838
|
+
color: node.color
|
|
839
|
+
});
|
|
840
|
+
this.elements.push(anchor);
|
|
841
|
+
this.register(node.name, label, id);
|
|
842
|
+
}
|
|
843
|
+
/** `component`/`market`/`ecosystem` and `build`/`buy`/`outsource` lines with coordinates. */
|
|
844
|
+
pushComponent(after, kw) {
|
|
845
|
+
const keywordMethod = METHODS.has(kw) ? kw : void 0;
|
|
846
|
+
const node = parseNode(after, COMPONENT_SUFFIXES, this.report, keywordMethod);
|
|
847
|
+
if (!node) return false;
|
|
848
|
+
const decorators = mergeLegacy(kw, node.decorators);
|
|
849
|
+
const label = this.nameElement(node.name, "component");
|
|
850
|
+
const id = this.ids.alloc("cmp", label);
|
|
851
|
+
const component = compact({
|
|
650
852
|
id,
|
|
651
|
-
elementType: "
|
|
652
|
-
label
|
|
653
|
-
position:
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
853
|
+
elementType: "component",
|
|
854
|
+
label,
|
|
855
|
+
position: this.position(node.coords.a, node.coords.b),
|
|
856
|
+
labelOffset: node.labelOffset,
|
|
857
|
+
decorators: Object.keys(decorators).length ? decorators : void 0,
|
|
858
|
+
color: node.color
|
|
859
|
+
});
|
|
860
|
+
this.elements.push(component);
|
|
861
|
+
if (node.urlRef) this.addUrlRef(node.urlRef);
|
|
862
|
+
this.register(node.name, label, id);
|
|
863
|
+
return true;
|
|
864
|
+
}
|
|
865
|
+
readNote(line, after) {
|
|
866
|
+
const split = splitAtCoords(after);
|
|
867
|
+
if (!split) return this.failed(line);
|
|
868
|
+
const { color } = readSuffix(split.suffix, NOTE_SUFFIXES, this.report);
|
|
869
|
+
this.reportMisplacedSuffix(
|
|
870
|
+
splitTrailingGroups(split.name, { color: true, height: false }).groups,
|
|
871
|
+
"before the coordinates is read as part of the text",
|
|
872
|
+
"write it after them"
|
|
873
|
+
);
|
|
874
|
+
const textPart = this.readText(split.name.replace(/\\n/g, "\n"));
|
|
875
|
+
const note = compact({
|
|
876
|
+
id: this.ids.alloc("note", textPart || "note"),
|
|
877
|
+
elementType: "note",
|
|
878
|
+
label: textPart,
|
|
879
|
+
position: this.position(split.coords.a, split.coords.b),
|
|
880
|
+
color
|
|
881
|
+
});
|
|
882
|
+
this.elements.push(note);
|
|
883
|
+
}
|
|
884
|
+
/** `pipeline X [s, e]`, `pipeline X` (block form, range from children) — optionally with `{` at the end. */
|
|
885
|
+
readPipeline(line, after) {
|
|
886
|
+
this.endUnclosedBlock();
|
|
887
|
+
let body = after;
|
|
888
|
+
let opensBlock = false;
|
|
889
|
+
while (body.endsWith("{")) {
|
|
890
|
+
if (opensBlock) this.diag('Ignored "{" \u2014 a block must directly follow a pipeline line');
|
|
891
|
+
opensBlock = true;
|
|
892
|
+
body = body.slice(0, -1).trim();
|
|
893
|
+
}
|
|
894
|
+
const { name, range, suffixText, misplaced } = splitPipelineLine(body);
|
|
895
|
+
if (!name) return this.failed(line);
|
|
896
|
+
if (range && !range.coords) this.diag(unreadableNumberMessage(range.tuple));
|
|
897
|
+
this.reportMisplacedSuffix(
|
|
898
|
+
misplaced,
|
|
899
|
+
"before the coordinates is read as part of the name",
|
|
900
|
+
"write it after them"
|
|
901
|
+
);
|
|
902
|
+
const notes = [];
|
|
903
|
+
const note = (message) => notes.push(message);
|
|
904
|
+
const suffix = readSuffix(suffixText, PIPELINE_SUFFIXES, note);
|
|
905
|
+
const coords = range?.coords;
|
|
906
|
+
const pending = {
|
|
907
|
+
name,
|
|
908
|
+
lineNo: this.lineNo,
|
|
909
|
+
text: this.currentLine,
|
|
910
|
+
...coords ? { start: this.clamp(coords.a), end: this.clamp(coords.b) } : {},
|
|
911
|
+
...suffix.height !== void 0 ? { visibility: roundAsWritten(this.clamp(suffix.height, note)) } : {},
|
|
912
|
+
...suffix.color ? { color: suffix.color } : {},
|
|
913
|
+
notes,
|
|
914
|
+
raw: line.slice(0, line.length - after.length) + body,
|
|
915
|
+
children: []
|
|
658
916
|
};
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
917
|
+
this.pendingPipeline.push(pending);
|
|
918
|
+
if (opensBlock) {
|
|
919
|
+
this.blockPipeline = pending;
|
|
920
|
+
this.blockOpening = { line: this.lineNo, text: this.currentLine };
|
|
921
|
+
} else this.lastPipeline = pending;
|
|
922
|
+
}
|
|
923
|
+
readEvolve(line, after) {
|
|
924
|
+
const notes = [];
|
|
925
|
+
const note = (message) => notes.push(message);
|
|
926
|
+
const evolve = parseEvolve(after, note);
|
|
927
|
+
if (!evolve) return this.failed(line);
|
|
928
|
+
this.pendingEvolve.push({
|
|
929
|
+
...evolve,
|
|
930
|
+
target: this.clamp(evolve.target, note),
|
|
931
|
+
notes,
|
|
932
|
+
raw: line,
|
|
933
|
+
lineNo: this.lineNo,
|
|
934
|
+
text: this.currentLine
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
readStyle(line, after) {
|
|
938
|
+
const style = after.toLowerCase();
|
|
939
|
+
if (KNOWN_STYLES.has(style)) {
|
|
940
|
+
this.setConfig("style", { ...this.config, style });
|
|
941
|
+
} else this.failed(line);
|
|
942
|
+
}
|
|
943
|
+
readSize(line, after) {
|
|
944
|
+
const split = splitAtCoords(after);
|
|
945
|
+
const coords = split?.coords;
|
|
946
|
+
if (!coords || !isRoundTrippableNumber(coords.a) || !isRoundTrippableNumber(coords.b)) {
|
|
947
|
+
return this.failed(line);
|
|
948
|
+
}
|
|
949
|
+
this.reportTextBefore(split.name);
|
|
950
|
+
readSuffix(split.suffix, NO_SUFFIXES, this.report);
|
|
951
|
+
this.setConfig("size", { ...this.config, size: { width: coords.a, height: coords.b } });
|
|
952
|
+
}
|
|
953
|
+
readEvolution(line, after) {
|
|
954
|
+
const parts = after.split("->").map((s) => s.trim());
|
|
955
|
+
if (parts.length === 4) {
|
|
956
|
+
const [first, second, third, fourth] = parts.map((part) => this.readText(part));
|
|
957
|
+
const evolutionLabels = [first, second, third, fourth];
|
|
958
|
+
this.setConfig("evolution", { ...this.config, evolutionLabels });
|
|
959
|
+
} else {
|
|
960
|
+
const reason = `Evolution needs exactly four stages, found ${parts.length}`;
|
|
961
|
+
this.keepUnusedConfigLine("evolution", line, reason);
|
|
668
962
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
963
|
+
}
|
|
964
|
+
/** `y-axis Label->BottomLabel->TopLabel` — keeps the end labels losslessly too. */
|
|
965
|
+
readYAxis(line, after) {
|
|
966
|
+
const written = after.split("->").map((s) => s.trim());
|
|
967
|
+
const [label, bottom, top] = written.filter(Boolean).map((part) => this.readText(part));
|
|
968
|
+
if (!label) return this.keepUnusedConfigLine("y-axis", line, "y-axis has no label");
|
|
969
|
+
const { yAxisEndLabels: _replaced, ...otherConfig } = this.config;
|
|
970
|
+
const ends = bottom && top ? [bottom, top] : void 0;
|
|
971
|
+
this.setConfig("y-axis", {
|
|
972
|
+
...otherConfig,
|
|
973
|
+
yAxisLabel: label,
|
|
974
|
+
...ends ? { yAxisEndLabels: ends } : {}
|
|
674
975
|
});
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const id = nameToId.get(pm.name);
|
|
679
|
-
const idx = elements.findIndex((e) => e.id === id);
|
|
680
|
-
const el = idx >= 0 ? elements[idx] : void 0;
|
|
681
|
-
if (!el || el.elementType !== "component") {
|
|
682
|
-
rawPassthrough.push(pm.raw);
|
|
683
|
-
diag(`${pm.method}: component "${pm.name}" not found`, pm.lineNo, pm.raw);
|
|
684
|
-
continue;
|
|
976
|
+
if (written.length !== 1 && !(written.length === 3 && written.every(Boolean))) {
|
|
977
|
+
const read = [label, ...ends ?? []].join("->");
|
|
978
|
+
this.diag(`y-axis takes "Label" or "Label->Bottom->Top" \u2014 read as "${read}"`);
|
|
685
979
|
}
|
|
686
|
-
elements[idx] = { ...el, decorators: { ...el.decorators, method: pm.method } };
|
|
687
980
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
if (el.elementType === "component") {
|
|
696
|
-
elements[index] = { ...el, url: address };
|
|
697
|
-
} else if (el.elementType === "submap") {
|
|
698
|
-
elements[index] = { ...el, urlRef: address };
|
|
981
|
+
readAnnotationsBox(line, after) {
|
|
982
|
+
const split = splitAtCoords(after);
|
|
983
|
+
if (!split) {
|
|
984
|
+
return this.keep(
|
|
985
|
+
line,
|
|
986
|
+
"Annotations box has no position \u2014 not used; add [visibility, maturity]"
|
|
987
|
+
);
|
|
699
988
|
}
|
|
989
|
+
this.reportTextBefore(split.name);
|
|
990
|
+
readSuffix(split.suffix, NO_SUFFIXES, this.report);
|
|
991
|
+
const annotationsBoxPosition = this.position(split.coords.a, split.coords.b);
|
|
992
|
+
this.setConfig("annotations", { ...this.config, annotationsBoxPosition });
|
|
700
993
|
}
|
|
701
|
-
|
|
702
|
-
|
|
994
|
+
/** `annotation N <position> [(color …)] <text>`; the position is a tuple or a tuple list. */
|
|
995
|
+
readAnnotation(line, after) {
|
|
996
|
+
const numMatch = /^\s*(\d+)/.exec(after);
|
|
997
|
+
const number = numMatch ? Number(numMatch[1]) : ++this.annotationCounter;
|
|
998
|
+
if (!Number.isSafeInteger(number)) return this.failed(line);
|
|
999
|
+
const found = parsePositions(after.replace(/^\s*\d+\s*/, ""));
|
|
1000
|
+
if (!found) {
|
|
1001
|
+
return this.keep(line, "Annotation has no position \u2014 not drawn; add [visibility, maturity]");
|
|
1002
|
+
}
|
|
1003
|
+
if (!found.tuples.length) return this.failed(line);
|
|
1004
|
+
for (const unreadable of found.unreadable) this.diag(unreadableNumberMessage(unreadable));
|
|
1005
|
+
this.reportTextBefore(found.before);
|
|
1006
|
+
const { color, rest } = splitLeadingColor(found.after);
|
|
1007
|
+
this.reportMisplacedSuffix(
|
|
1008
|
+
splitTrailingGroups(rest, { color: true, height: false }).groups,
|
|
1009
|
+
"after the annotation text is read as part of the text",
|
|
1010
|
+
"write it directly after the position"
|
|
1011
|
+
);
|
|
1012
|
+
const text = this.readText(rest.trim());
|
|
1013
|
+
const positions = found.tuples.map((t) => this.position(t.a, t.b));
|
|
1014
|
+
const annotation = {
|
|
1015
|
+
id: this.ids.alloc("anno", String(number)),
|
|
1016
|
+
elementType: "annotation",
|
|
1017
|
+
label: text,
|
|
1018
|
+
position: positions[0],
|
|
1019
|
+
number,
|
|
1020
|
+
positions,
|
|
1021
|
+
text,
|
|
1022
|
+
...color ? { color } : {}
|
|
1023
|
+
};
|
|
1024
|
+
this.elements.push(annotation);
|
|
703
1025
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
1026
|
+
/** Project extension (freeform drawing): `line [[v,e], [v,e], …] (closed) (dashed) (color x)`. */
|
|
1027
|
+
readDrawing(line, after) {
|
|
1028
|
+
const list = parseMultiCoords(after);
|
|
1029
|
+
if (!list || list.tuples.length < 2) return this.failed(line);
|
|
1030
|
+
for (const unreadable of list.unreadable) this.diag(unreadableNumberMessage(unreadable));
|
|
1031
|
+
this.reportTextBefore(list.before);
|
|
1032
|
+
const suffix = readSuffix(list.after, DRAWING_SUFFIXES, this.report);
|
|
1033
|
+
const points = list.tuples.map((t) => this.position(t.a, t.b));
|
|
1034
|
+
const drawing = {
|
|
1035
|
+
id: this.ids.alloc("draw", "line"),
|
|
1036
|
+
elementType: "drawing",
|
|
1037
|
+
label: "",
|
|
1038
|
+
position: points[0],
|
|
1039
|
+
points,
|
|
1040
|
+
...suffix.closed ? { closed: true } : {},
|
|
1041
|
+
...suffix.strokeStyle ? { strokeStyle: suffix.strokeStyle } : {},
|
|
1042
|
+
...suffix.color ? { color: suffix.color } : {}
|
|
1043
|
+
};
|
|
1044
|
+
this.elements.push(drawing);
|
|
1045
|
+
}
|
|
1046
|
+
/** Canonical OWM form: `<kind> [vis1, mat1, vis2, mat2]` (two corners, normalized). */
|
|
1047
|
+
readAttitude(line, after, kind) {
|
|
1048
|
+
const split = splitAtCoords4(after);
|
|
1049
|
+
if (!split) return this.failed(line);
|
|
1050
|
+
this.reportTextBefore(split.before);
|
|
1051
|
+
const { color } = readSuffix(split.after, ATTITUDE_SUFFIXES, this.report);
|
|
1052
|
+
const { a, b, c, d } = split.coords;
|
|
1053
|
+
const corners = [this.clamp(a), this.clamp(b), this.clamp(c), this.clamp(d)];
|
|
1054
|
+
this.elements.push(makeAttitude(this.ids, kind, corners, color));
|
|
1055
|
+
}
|
|
1056
|
+
/** OWM: `url Name [https://…]` — definition, referenced via `url(Name)` on elements. */
|
|
1057
|
+
readUrlDefinition(line, after) {
|
|
1058
|
+
const definition = readUrlDefinition(after);
|
|
1059
|
+
if (!definition) return this.failed(line);
|
|
1060
|
+
this.urlDefinitions.push({ ...definition, lineNo: this.lineNo, text: this.currentLine });
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Legacy OWM: `buy <Name>` (method on an existing component) or `buy <Name> [vis, mat]` (create a
|
|
1064
|
+
* component with a method).
|
|
1065
|
+
*/
|
|
1066
|
+
readMethod(line, after, kw) {
|
|
1067
|
+
if (this.pushComponent(after, kw)) return;
|
|
1068
|
+
if (!after) return this.keep(line, `${kw}: needs a component name \u2014 not used`);
|
|
1069
|
+
this.pendingMethod.push({
|
|
1070
|
+
name: after,
|
|
1071
|
+
method: kw,
|
|
1072
|
+
raw: line,
|
|
1073
|
+
lineNo: this.lineNo,
|
|
1074
|
+
text: this.currentLine
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
readAccelerator(line, after, kw) {
|
|
1078
|
+
const node = parseNode(after, ACCELERATOR_SUFFIXES, this.report);
|
|
1079
|
+
if (!node) return this.failed(line);
|
|
1080
|
+
const label = this.nameElement(node.name, "accelerator");
|
|
1081
|
+
const id = this.ids.alloc("accel", label);
|
|
1082
|
+
const accelerator = {
|
|
1083
|
+
id,
|
|
1084
|
+
elementType: "accelerator",
|
|
1085
|
+
direction: kw === "deaccelerator" ? "deaccelerate" : "accelerate",
|
|
1086
|
+
label,
|
|
1087
|
+
position: this.position(node.coords.a, node.coords.b),
|
|
1088
|
+
...node.color ? { color: node.color } : {}
|
|
1089
|
+
};
|
|
1090
|
+
this.elements.push(accelerator);
|
|
1091
|
+
this.register(node.name, label, id);
|
|
1092
|
+
}
|
|
1093
|
+
readSubmap(line, after) {
|
|
1094
|
+
const node = parseNode(after, SUBMAP_SUFFIXES, this.report);
|
|
1095
|
+
if (!node) return this.failed(line);
|
|
1096
|
+
const label = this.nameElement(node.name, "submap");
|
|
1097
|
+
const id = this.ids.alloc("submap", label);
|
|
1098
|
+
const submap = {
|
|
1099
|
+
id,
|
|
1100
|
+
elementType: "submap",
|
|
1101
|
+
label,
|
|
1102
|
+
position: this.position(node.coords.a, node.coords.b),
|
|
1103
|
+
...node.color ? { color: node.color } : {}
|
|
1104
|
+
};
|
|
1105
|
+
this.elements.push(submap);
|
|
1106
|
+
if (node.urlRef) this.addUrlRef(node.urlRef);
|
|
1107
|
+
this.register(node.name, label, id);
|
|
1108
|
+
}
|
|
1109
|
+
finish() {
|
|
1110
|
+
const removedFromPassthrough = replacedConfigLines(
|
|
1111
|
+
this.unusedConfigLines,
|
|
1112
|
+
this.config,
|
|
1113
|
+
this.reportAt
|
|
1114
|
+
);
|
|
1115
|
+
this.resolvePipelines();
|
|
1116
|
+
const indexById = new Map(this.elements.map((el, index) => [el.id, index]));
|
|
1117
|
+
this.resolveEvolves(indexById);
|
|
1118
|
+
this.resolveMethods(indexById);
|
|
1119
|
+
const keptDefinitions = resolveUrlReferences(
|
|
1120
|
+
this.elements,
|
|
1121
|
+
this.pendingUrlRefs,
|
|
1122
|
+
this.urlDefinitions,
|
|
1123
|
+
this.reportAt
|
|
1124
|
+
);
|
|
1125
|
+
for (const definition of keptDefinitions) {
|
|
1126
|
+
this.keepLine(
|
|
1127
|
+
`url ${definition.name} [${definition.address}]`,
|
|
1128
|
+
definition.lineNo,
|
|
1129
|
+
definition.text
|
|
716
1130
|
);
|
|
717
|
-
continue;
|
|
718
1131
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1132
|
+
const declaredNames = /* @__PURE__ */ new Set([...this.idBySourceName.keys(), ...this.idByElementName.keys()]);
|
|
1133
|
+
const edges = buildEdges(
|
|
1134
|
+
this.pendingLinks,
|
|
1135
|
+
(name) => this.idOfName(name),
|
|
1136
|
+
(link, missing) => {
|
|
1137
|
+
this.keepLine(link.raw, link.lineNo, link.text);
|
|
1138
|
+
this.diag(`Link: "${missing}" not found`, link.lineNo, link.text);
|
|
1139
|
+
},
|
|
1140
|
+
(link) => link.label && this.readText(link.label, link.lineNo, link.text)
|
|
1141
|
+
);
|
|
1142
|
+
const kept = this.passthrough.filter((_, index) => !removedFromPassthrough.has(index));
|
|
1143
|
+
const rawPassthrough = withoutClosersOfKeptComments(kept, this.reportAt);
|
|
1144
|
+
const map = compact({
|
|
1145
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
1146
|
+
config: this.config,
|
|
1147
|
+
elements: this.elements,
|
|
1148
|
+
edges,
|
|
1149
|
+
rawPassthrough: rawPassthrough.length ? rawPassthrough : void 0
|
|
1150
|
+
});
|
|
1151
|
+
return {
|
|
1152
|
+
result: { map: validateMap(map), diagnostics: this.diagnostics },
|
|
1153
|
+
declaredNames,
|
|
1154
|
+
keywordLinks: this.keywordLinks
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* A pipeline pairs with the component written under its name: it takes that component's height,
|
|
1159
|
+
* as the serializer assumes when it leaves out `(y …)`. Block children are not such components.
|
|
1160
|
+
*/
|
|
1161
|
+
resolvePipelines() {
|
|
1162
|
+
const componentByName = /* @__PURE__ */ new Map();
|
|
1163
|
+
for (const el of this.elements) {
|
|
1164
|
+
if (el.elementType === "component") componentByName.set(el.label, el);
|
|
1165
|
+
}
|
|
1166
|
+
for (const p of this.pendingPipeline) {
|
|
1167
|
+
const label = writtenName(p.name, "pipeline");
|
|
1168
|
+
const component = componentByName.get(label);
|
|
1169
|
+
const writtenRange = pipelineRange(p);
|
|
1170
|
+
if (!writtenRange) {
|
|
1171
|
+
this.keepLine(p.raw, p.lineNo, p.text);
|
|
1172
|
+
if (!component) {
|
|
1173
|
+
this.diag(
|
|
1174
|
+
`Pipeline "${p.name}" has no range \u2014 not drawn; add [start, end] or child components`,
|
|
1175
|
+
p.lineNo,
|
|
1176
|
+
p.text
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
for (const message of p.notes) this.diag(message, p.lineNo, p.text);
|
|
1182
|
+
if (label !== p.name) this.diag(renamedMessage(p.name, label), p.lineNo, p.text);
|
|
1183
|
+
const range = widenEmptyRange(writtenRange);
|
|
1184
|
+
const widened = range.start !== writtenRange.start || range.end !== writtenRange.end;
|
|
1185
|
+
if (widened && !writtenRange.fromChildren) {
|
|
1186
|
+
this.diag(
|
|
1187
|
+
`Pipeline "${p.name}" range is empty or reversed \u2014 changed to [${range.start}, ${range.end}]`,
|
|
1188
|
+
p.lineNo,
|
|
1189
|
+
p.text
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
const visibility = p.visibility ?? component?.position.visibility ?? 0.5;
|
|
1193
|
+
const id = this.ids.alloc("pipeline", label);
|
|
1194
|
+
if (this.idOfName(p.name) === void 0 && this.idOfName(label) === void 0) {
|
|
1195
|
+
this.register(p.name, label, id);
|
|
1196
|
+
}
|
|
1197
|
+
const childNames = p.children.map(
|
|
1198
|
+
(child) => this.nameElement(child.name, "component", child.lineNo, child.text)
|
|
728
1199
|
);
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
to: t,
|
|
737
|
-
bidirectional: link.bidirectional ? true : void 0,
|
|
738
|
-
flowValue: link.flowValue,
|
|
739
|
-
label: link.label
|
|
740
|
-
})
|
|
1200
|
+
const { pipeline, children } = pipelineElements(
|
|
1201
|
+
{ ...p, name: label },
|
|
1202
|
+
id,
|
|
1203
|
+
visibility,
|
|
1204
|
+
range,
|
|
1205
|
+
this.ids,
|
|
1206
|
+
childNames
|
|
741
1207
|
);
|
|
1208
|
+
this.elements.push(pipeline);
|
|
1209
|
+
children.forEach((child, index) => {
|
|
1210
|
+
this.elements.push(child);
|
|
1211
|
+
this.register(p.children[index].name, child.label, child.id);
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
componentIndex(name, indexById) {
|
|
1216
|
+
const index = indexById.get(this.idOfName(name) ?? "");
|
|
1217
|
+
return index !== void 0 && this.elements[index].elementType === "component" ? index : -1;
|
|
1218
|
+
}
|
|
1219
|
+
resolveEvolves(indexById) {
|
|
1220
|
+
const evolveLineOf = /* @__PURE__ */ new Map();
|
|
1221
|
+
for (const ev of this.pendingEvolve) {
|
|
1222
|
+
const index = this.componentIndex(ev.name, indexById);
|
|
1223
|
+
if (index < 0) {
|
|
1224
|
+
this.keepLine(ev.raw, ev.lineNo, ev.text);
|
|
1225
|
+
const misplaced = splitMisplacedEvolveSuffix(ev.name);
|
|
1226
|
+
const hint = misplaced && this.componentIndex(misplaced.name, indexById) >= 0 ? misplacedEvolveSuffixHint(misplaced) : "";
|
|
1227
|
+
this.diag(`evolve: component "${ev.name}" not found${hint}`, ev.lineNo, ev.text);
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
for (const message of ev.notes) this.diag(message, ev.lineNo, ev.text);
|
|
1231
|
+
const component = this.elements[index];
|
|
1232
|
+
const earlier = evolveLineOf.get(component.id);
|
|
1233
|
+
if (earlier !== void 0) {
|
|
1234
|
+
const message = `Repeated evolve for "${ev.name}" \u2014 replaces the one on line ${earlier}`;
|
|
1235
|
+
this.diag(message, ev.lineNo, ev.text);
|
|
1236
|
+
}
|
|
1237
|
+
evolveLineOf.set(component.id, ev.lineNo);
|
|
1238
|
+
const movement = compact({
|
|
1239
|
+
targetEvolution: ev.target,
|
|
1240
|
+
newLabel: ev.newLabel && this.readText(ev.newLabel, ev.lineNo, ev.text),
|
|
1241
|
+
method: ev.method,
|
|
1242
|
+
labelOffset: ev.labelOffset
|
|
1243
|
+
});
|
|
1244
|
+
this.elements[index] = { ...component, movement };
|
|
742
1245
|
}
|
|
743
1246
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1247
|
+
/** Legacy standalone methods (`buy <name>`). */
|
|
1248
|
+
resolveMethods(indexById) {
|
|
1249
|
+
for (const pm of this.pendingMethod) {
|
|
1250
|
+
const index = this.componentIndex(pm.name, indexById);
|
|
1251
|
+
if (index < 0) {
|
|
1252
|
+
this.keepLine(pm.raw, pm.lineNo, pm.text);
|
|
1253
|
+
this.diag(`${pm.method}: component "${pm.name}" not found`, pm.lineNo, pm.text);
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
const el = this.elements[index];
|
|
1257
|
+
const replaced = el.decorators?.method;
|
|
1258
|
+
if (replaced && replaced !== pm.method) {
|
|
1259
|
+
const message = `${pm.method}: "${pm.name}" already has the method "${replaced}" \u2014 replaced`;
|
|
1260
|
+
this.diag(message, pm.lineNo, pm.text);
|
|
1261
|
+
}
|
|
1262
|
+
this.elements[index] = { ...el, decorators: { ...el.decorators, method: pm.method } };
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
function renamedMessage(sourceName, name) {
|
|
1267
|
+
return `Name "${sourceName}" renamed to "${name}" \u2014 a name cannot hold link operators ("->", "+>", "+<", "+'"), comment markers or a leading "{"`;
|
|
1268
|
+
}
|
|
1269
|
+
function withoutTrailingCarriageReturns(line) {
|
|
1270
|
+
let end = line.length;
|
|
1271
|
+
while (end > 0 && line[end - 1] === "\r") end--;
|
|
1272
|
+
return line.slice(0, end);
|
|
1273
|
+
}
|
|
1274
|
+
function withoutClosersOfKeptComments(entries, report) {
|
|
1275
|
+
const nextParse = new CommentStripper();
|
|
1276
|
+
return entries.map((entry) => {
|
|
1277
|
+
let text = entry.text;
|
|
1278
|
+
if (!entry.isComment && nextParse.insideBlockComment && text.includes("*/")) {
|
|
1279
|
+
text = text.replaceAll("*/", " ");
|
|
1280
|
+
report(
|
|
1281
|
+
'Ignored "*/" \u2014 this line is kept after an unclosed "/*" and would end that comment',
|
|
1282
|
+
entry.lineNo,
|
|
1283
|
+
entry.source
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
nextParse.strip(text);
|
|
1287
|
+
return text;
|
|
750
1288
|
});
|
|
751
|
-
return { map: validateMap(map), diagnostics };
|
|
752
1289
|
}
|
|
753
|
-
function
|
|
1290
|
+
function replacedConfigLines(unusedLines, config, report) {
|
|
1291
|
+
const configIsSet = {
|
|
1292
|
+
evolution: config.evolutionLabels !== void 0,
|
|
1293
|
+
"y-axis": config.yAxisLabel !== void 0
|
|
1294
|
+
};
|
|
1295
|
+
const replacedIndexes = /* @__PURE__ */ new Set();
|
|
1296
|
+
for (const unused of unusedLines) {
|
|
1297
|
+
const replaced = configIsSet[unused.keyword];
|
|
1298
|
+
if (replaced) replacedIndexes.add(unused.index);
|
|
1299
|
+
const outcome = replaced ? `removed; another "${unused.keyword}" line sets it` : "not used";
|
|
1300
|
+
report(`${unused.reason} \u2014 ${outcome}`, unused.line, unused.text);
|
|
1301
|
+
}
|
|
1302
|
+
return replacedIndexes;
|
|
1303
|
+
}
|
|
1304
|
+
function resolveUrlReferences(elements, refs, definitions, report) {
|
|
1305
|
+
const definitionByName = /* @__PURE__ */ new Map();
|
|
1306
|
+
const replaced = /* @__PURE__ */ new Set();
|
|
1307
|
+
for (const definition of definitions) {
|
|
1308
|
+
const earlier = definitionByName.get(definition.name);
|
|
1309
|
+
if (earlier) {
|
|
1310
|
+
replaced.add(earlier);
|
|
1311
|
+
report(
|
|
1312
|
+
`Repeated url definition "${definition.name}" \u2014 replaces the one on line ${earlier.lineNo}, which is kept unused`,
|
|
1313
|
+
definition.lineNo,
|
|
1314
|
+
definition.text
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
definitionByName.set(definition.name, definition);
|
|
1318
|
+
}
|
|
1319
|
+
const used = /* @__PURE__ */ new Set();
|
|
1320
|
+
for (const { index, ref, line, text } of refs) {
|
|
1321
|
+
const definition = definitionByName.get(ref);
|
|
1322
|
+
if (definition) used.add(definition);
|
|
1323
|
+
const address = definition?.address ?? (/^[a-z][\w+.-]*:\/\//i.test(ref) ? ref : void 0);
|
|
1324
|
+
if (!address) {
|
|
1325
|
+
report(`url(${ref}) has no matching "url ${ref} [address]" line \u2014 ignored`, line, text);
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
const el = elements[index];
|
|
1329
|
+
if (el.elementType === "component") elements[index] = { ...el, url: address };
|
|
1330
|
+
else if (el.elementType === "submap") elements[index] = { ...el, urlRef: address };
|
|
1331
|
+
}
|
|
1332
|
+
return definitions.filter((definition) => replaced.has(definition) || !used.has(definition));
|
|
1333
|
+
}
|
|
1334
|
+
function parseNode(after, suffixes, report, keywordMethod) {
|
|
754
1335
|
const split = splitAtCoords(after);
|
|
755
1336
|
if (!split || !split.name) return null;
|
|
756
|
-
const
|
|
757
|
-
const url = parseUrlRef(col.rest);
|
|
758
|
-
const lo = parseLabelOffset(url.rest);
|
|
759
|
-
const dec = parseDecorators(lo.rest);
|
|
1337
|
+
const suffix = readSuffix(split.suffix, suffixes, report, "after the coordinates", keywordMethod);
|
|
760
1338
|
return compact({
|
|
761
1339
|
name: split.name,
|
|
762
1340
|
coords: split.coords,
|
|
763
|
-
decorators:
|
|
764
|
-
labelOffset:
|
|
765
|
-
urlRef:
|
|
766
|
-
color:
|
|
1341
|
+
decorators: suffix.decorators,
|
|
1342
|
+
labelOffset: suffix.labelOffset,
|
|
1343
|
+
urlRef: suffix.urlRef,
|
|
1344
|
+
color: suffix.color
|
|
767
1345
|
});
|
|
768
1346
|
}
|
|
769
|
-
|
|
770
|
-
|
|
1347
|
+
var COMPONENT_SUFFIXES = /* @__PURE__ */ new Set(["color", "decorators", "label", "url"]);
|
|
1348
|
+
var PIPELINE_CHILD_SUFFIXES = /* @__PURE__ */ new Set(["color", "decorators", "label"]);
|
|
1349
|
+
var ANCHOR_SUFFIXES = /* @__PURE__ */ new Set(["color", "label"]);
|
|
1350
|
+
var SUBMAP_SUFFIXES = /* @__PURE__ */ new Set(["color", "url"]);
|
|
1351
|
+
var ACCELERATOR_SUFFIXES = /* @__PURE__ */ new Set(["color"]);
|
|
1352
|
+
var NOTE_SUFFIXES = /* @__PURE__ */ new Set(["color"]);
|
|
1353
|
+
var ATTITUDE_SUFFIXES = /* @__PURE__ */ new Set(["color"]);
|
|
1354
|
+
var PIPELINE_SUFFIXES = /* @__PURE__ */ new Set(["color", "height"]);
|
|
1355
|
+
var DRAWING_SUFFIXES = /* @__PURE__ */ new Set(["color", "stroke"]);
|
|
1356
|
+
var EVOLVE_SUFFIXES = /* @__PURE__ */ new Set(["method"]);
|
|
1357
|
+
var NO_SUFFIXES = /* @__PURE__ */ new Set();
|
|
1358
|
+
function readSuffix(text, slots, report, place = "after the coordinates", keywordMethod) {
|
|
1359
|
+
const reading = {};
|
|
1360
|
+
const decorators = keywordMethod ? { method: keywordMethod } : {};
|
|
1361
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1362
|
+
const ignored = [];
|
|
1363
|
+
const readGroupItem = (item) => {
|
|
1364
|
+
if (slots.has("decorators") && (item === "market" || item === "ecosystem" || item === "inertia")) {
|
|
1365
|
+
decorators[item] = true;
|
|
1366
|
+
return true;
|
|
1367
|
+
}
|
|
1368
|
+
if ((slots.has("decorators") || slots.has("method")) && METHODS.has(item)) {
|
|
1369
|
+
decorators.method ??= item;
|
|
1370
|
+
return decorators.method === item;
|
|
1371
|
+
}
|
|
1372
|
+
if (slots.has("stroke") && item === "closed") {
|
|
1373
|
+
reading.closed = true;
|
|
1374
|
+
return true;
|
|
1375
|
+
}
|
|
1376
|
+
if (slots.has("stroke") && (item === "dashed" || item === "dotted")) {
|
|
1377
|
+
reading.strokeStyle ??= item;
|
|
1378
|
+
return reading.strokeStyle === item;
|
|
1379
|
+
}
|
|
1380
|
+
return false;
|
|
1381
|
+
};
|
|
1382
|
+
const firstOf = (token, slot) => {
|
|
1383
|
+
if (!slots.has(slot) || seen.has(token.kind)) return false;
|
|
1384
|
+
seen.add(token.kind);
|
|
1385
|
+
return true;
|
|
1386
|
+
};
|
|
1387
|
+
for (const token of scanSuffix(text)) {
|
|
1388
|
+
switch (token.kind) {
|
|
1389
|
+
case "color":
|
|
1390
|
+
if (firstOf(token, "color")) reading.color = token.color;
|
|
1391
|
+
else ignored.push(token);
|
|
1392
|
+
break;
|
|
1393
|
+
case "height":
|
|
1394
|
+
if (firstOf(token, "height")) {
|
|
1395
|
+
const height = readUnclampedNumber(token.value);
|
|
1396
|
+
if (height === null) report(unreadableNumberMessage(token.text));
|
|
1397
|
+
else reading.height = height;
|
|
1398
|
+
} else ignored.push(token);
|
|
1399
|
+
break;
|
|
1400
|
+
case "label":
|
|
1401
|
+
if (firstOf(token, "label")) {
|
|
1402
|
+
const dx = readUnclampedNumber(token.dx);
|
|
1403
|
+
const dy = readUnclampedNumber(token.dy);
|
|
1404
|
+
if (dx === null || dy === null) report(unreadableNumberMessage(token.text));
|
|
1405
|
+
else reading.labelOffset = { dx, dy };
|
|
1406
|
+
} else ignored.push(token);
|
|
1407
|
+
break;
|
|
1408
|
+
case "url":
|
|
1409
|
+
if (token.ref && firstOf(token, "url")) reading.urlRef = token.ref;
|
|
1410
|
+
else ignored.push(token);
|
|
1411
|
+
break;
|
|
1412
|
+
case "inertia":
|
|
1413
|
+
if (slots.has("decorators")) decorators.inertia = true;
|
|
1414
|
+
else ignored.push(token);
|
|
1415
|
+
break;
|
|
1416
|
+
case "group": {
|
|
1417
|
+
const unfit = token.items.filter((item) => !readGroupItem(item.text.toLowerCase()));
|
|
1418
|
+
if (unfit.length === token.items.length) ignored.push(token);
|
|
1419
|
+
else for (const item of unfit) ignored.push(item);
|
|
1420
|
+
break;
|
|
1421
|
+
}
|
|
1422
|
+
case "word":
|
|
1423
|
+
ignored.push(token);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (ignored.length) report(`Ignored text ${place}: ${quotedSpans(text, ignored)}`);
|
|
1427
|
+
return compact({ ...reading, decorators });
|
|
771
1428
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1429
|
+
function quotedSpans(text, spans) {
|
|
1430
|
+
const pieces = [];
|
|
1431
|
+
let start = spans[0].at;
|
|
1432
|
+
let end = start + spans[0].text.length;
|
|
1433
|
+
for (let i = 1; i < spans.length; i++) {
|
|
1434
|
+
const span = spans[i];
|
|
1435
|
+
if (text.slice(end, span.at).trim() !== "") {
|
|
1436
|
+
pieces.push(text.slice(start, end));
|
|
1437
|
+
start = span.at;
|
|
1438
|
+
}
|
|
1439
|
+
end = span.at + span.text.length;
|
|
1440
|
+
}
|
|
1441
|
+
pieces.push(text.slice(start, end));
|
|
1442
|
+
return pieces.map((piece) => `"${piece}"`).join(", ");
|
|
1443
|
+
}
|
|
1444
|
+
function splitPipelineLine(body) {
|
|
1445
|
+
const kinds = { color: true, height: true };
|
|
1446
|
+
const split = splitAtTuple(body);
|
|
1447
|
+
if (split) {
|
|
1448
|
+
return {
|
|
1449
|
+
name: split.before,
|
|
1450
|
+
range: { tuple: split.tuple, coords: split.coords },
|
|
1451
|
+
suffixText: split.after,
|
|
1452
|
+
misplaced: splitTrailingGroups(split.before, kinds).groups
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
const { rest, groups } = splitTrailingGroups(body, kinds);
|
|
1456
|
+
return { name: rest, range: null, suffixText: groups.join(" "), misplaced: [] };
|
|
1457
|
+
}
|
|
1458
|
+
function unreadableNumberMessage(text) {
|
|
1459
|
+
return `Unreadable number in "${text}" \u2014 ignored`;
|
|
1460
|
+
}
|
|
1461
|
+
function pipelineRange(p) {
|
|
1462
|
+
if (p.start !== void 0 && p.end !== void 0) {
|
|
1463
|
+
return { start: roundAsWritten(p.start), end: roundAsWritten(p.end) };
|
|
1464
|
+
}
|
|
1465
|
+
const [first, ...others] = p.children;
|
|
1466
|
+
if (!first) return null;
|
|
1467
|
+
let start = first.maturity;
|
|
1468
|
+
let end = first.maturity;
|
|
1469
|
+
for (const child of others) {
|
|
1470
|
+
start = Math.min(start, child.maturity);
|
|
1471
|
+
end = Math.max(end, child.maturity);
|
|
1472
|
+
}
|
|
1473
|
+
return { start: roundAsWritten(start), end: roundAsWritten(end), fromChildren: true };
|
|
1474
|
+
}
|
|
1475
|
+
function widenEmptyRange({ start, end }) {
|
|
1476
|
+
if (end > start) return { start, end };
|
|
1477
|
+
const widenedStart = start >= 1 ? 0.95 : start;
|
|
1478
|
+
return { start: widenedStart, end: roundAsWritten(Math.min(1, widenedStart + 0.05)) };
|
|
1479
|
+
}
|
|
1480
|
+
function pipelineElements(p, id, visibility, range, ids, childNames) {
|
|
1481
|
+
const children = p.children.map(
|
|
1482
|
+
(c, index) => compact({
|
|
1483
|
+
id: ids.alloc("cmp", childNames[index]),
|
|
1484
|
+
elementType: "component",
|
|
1485
|
+
label: childNames[index],
|
|
1486
|
+
position: { visibility, evolution: c.maturity },
|
|
1487
|
+
labelOffset: c.labelOffset,
|
|
1488
|
+
decorators: Object.keys(c.decorators).length ? c.decorators : void 0,
|
|
1489
|
+
pipelineId: id,
|
|
1490
|
+
color: c.color
|
|
1491
|
+
})
|
|
1492
|
+
);
|
|
1493
|
+
const pipeline = {
|
|
1494
|
+
id,
|
|
1495
|
+
elementType: "pipeline",
|
|
1496
|
+
label: p.name,
|
|
1497
|
+
position: { visibility, evolution: (range.start + range.end) / 2 },
|
|
1498
|
+
evolutionStart: range.start,
|
|
1499
|
+
evolutionEnd: range.end,
|
|
1500
|
+
childIds: children.map((child) => child.id),
|
|
1501
|
+
...p.color ? { color: p.color } : {}
|
|
1502
|
+
};
|
|
1503
|
+
return { pipeline, children };
|
|
1504
|
+
}
|
|
1505
|
+
function parseBlockChild(after, report) {
|
|
1506
|
+
const split = splitAtFirstChildTuple(after);
|
|
1507
|
+
if (!split?.before || split.values.length !== 1) return null;
|
|
1508
|
+
const maturity = Number(split.values[0]);
|
|
777
1509
|
if (Number.isNaN(maturity)) return null;
|
|
778
|
-
const
|
|
779
|
-
if (!name) return null;
|
|
780
|
-
const suffix = after.slice(m.index + m[0].length);
|
|
781
|
-
const col = parseColor(suffix);
|
|
782
|
-
const lo = parseLabelOffset(col.rest);
|
|
783
|
-
const dec = parseDecorators(lo.rest);
|
|
1510
|
+
const suffix = readSuffix(split.after, PIPELINE_CHILD_SUFFIXES, report);
|
|
784
1511
|
return compact({
|
|
785
|
-
name,
|
|
1512
|
+
name: split.before,
|
|
786
1513
|
maturity,
|
|
787
|
-
decorators:
|
|
788
|
-
labelOffset:
|
|
789
|
-
color:
|
|
1514
|
+
decorators: suffix.decorators,
|
|
1515
|
+
labelOffset: suffix.labelOffset,
|
|
1516
|
+
color: suffix.color
|
|
790
1517
|
});
|
|
791
1518
|
}
|
|
792
|
-
function makeAttitude(ids, kind, v1, m1, v2, m2, color) {
|
|
1519
|
+
function makeAttitude(ids, kind, [v1, m1, v2, m2], color) {
|
|
793
1520
|
return {
|
|
794
1521
|
id: ids.alloc("attitude", kind),
|
|
795
1522
|
elementType: "attitude",
|
|
@@ -797,12 +1524,12 @@ function makeAttitude(ids, kind, v1, m1, v2, m2, color) {
|
|
|
797
1524
|
label: "",
|
|
798
1525
|
...color ? { color } : {},
|
|
799
1526
|
position: {
|
|
800
|
-
visibility:
|
|
801
|
-
evolution:
|
|
1527
|
+
visibility: Math.max(v1, v2),
|
|
1528
|
+
evolution: Math.min(m1, m2)
|
|
802
1529
|
},
|
|
803
1530
|
corner2: {
|
|
804
|
-
visibility:
|
|
805
|
-
evolution:
|
|
1531
|
+
visibility: Math.min(v1, v2),
|
|
1532
|
+
evolution: Math.max(m1, m2)
|
|
806
1533
|
}
|
|
807
1534
|
};
|
|
808
1535
|
}
|
|
@@ -810,18 +1537,36 @@ function mergeLegacy(kw, dec) {
|
|
|
810
1537
|
const merged = { ...dec };
|
|
811
1538
|
if (kw === "market") merged["market"] = true;
|
|
812
1539
|
if (kw === "ecosystem") merged["ecosystem"] = true;
|
|
1540
|
+
if (METHODS.has(kw)) merged["method"] = kw;
|
|
813
1541
|
return compact(merged);
|
|
814
1542
|
}
|
|
815
|
-
|
|
1543
|
+
var MISPLACED_EVOLVE_SUFFIX_RE = /(?:\((build|buy|outsource)\)|\binertia)$/i;
|
|
1544
|
+
function splitMisplacedEvolveSuffix(name) {
|
|
1545
|
+
const m = MISPLACED_EVOLVE_SUFFIX_RE.exec(name);
|
|
1546
|
+
if (!m) return null;
|
|
1547
|
+
const rest = name.slice(0, m.index).trimEnd();
|
|
1548
|
+
return rest ? { name: rest, suffix: m[0] } : null;
|
|
1549
|
+
}
|
|
1550
|
+
function misplacedEvolveSuffixHint({ name, suffix }) {
|
|
1551
|
+
return suffix.startsWith("(") ? ` \u2014 for component "${name}", write "${suffix}" after the target` : ` \u2014 for component "${name}", write "${suffix}" on its component line`;
|
|
1552
|
+
}
|
|
1553
|
+
var NUMBER_TOKEN_RE = /(^|\s)([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[-+]?\d+)?)(?=\s|\(|$)/gi;
|
|
1554
|
+
function parseEvolve(after, report) {
|
|
816
1555
|
const lo = parseLabelOffset(after);
|
|
817
|
-
|
|
818
|
-
const
|
|
819
|
-
if (
|
|
820
|
-
const
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
if (
|
|
1556
|
+
let targetMatch;
|
|
1557
|
+
for (const match of lo.rest.matchAll(NUMBER_TOKEN_RE)) targetMatch = match;
|
|
1558
|
+
if (!targetMatch) return null;
|
|
1559
|
+
const targetAt = targetMatch.index + targetMatch[1].length;
|
|
1560
|
+
const namePart = lo.rest.slice(0, targetAt).trim();
|
|
1561
|
+
const target = Number(targetMatch[2]);
|
|
1562
|
+
if (!namePart || !Number.isFinite(target)) return null;
|
|
1563
|
+
if (lo.unreadable) report(unreadableNumberMessage(lo.unreadable));
|
|
1564
|
+
const tail = readSuffix(
|
|
1565
|
+
lo.rest.slice(targetAt + targetMatch[2].length),
|
|
1566
|
+
EVOLVE_SUFFIXES,
|
|
1567
|
+
report,
|
|
1568
|
+
"after the evolve target"
|
|
1569
|
+
);
|
|
825
1570
|
const renameIdx = namePart.indexOf("->");
|
|
826
1571
|
const name = renameIdx >= 0 ? namePart.slice(0, renameIdx).trim() : namePart;
|
|
827
1572
|
const newLabel = renameIdx >= 0 ? namePart.slice(renameIdx + 2).trim() : void 0;
|
|
@@ -829,7 +1574,7 @@ function parseEvolve(after) {
|
|
|
829
1574
|
name,
|
|
830
1575
|
newLabel: newLabel || void 0,
|
|
831
1576
|
target,
|
|
832
|
-
method:
|
|
1577
|
+
method: tail.decorators.method,
|
|
833
1578
|
labelOffset: lo.labelOffset ?? void 0
|
|
834
1579
|
});
|
|
835
1580
|
}
|
|
@@ -841,23 +1586,28 @@ function urlOf(el) {
|
|
|
841
1586
|
return void 0;
|
|
842
1587
|
}
|
|
843
1588
|
function r(n) {
|
|
844
|
-
return String(
|
|
845
|
-
}
|
|
846
|
-
var NAMED_TYPES = /* @__PURE__ */ new Set(["anchor", "component", "accelerator", "submap"]);
|
|
847
|
-
function defaultName(type) {
|
|
848
|
-
return type === "anchor" ? "User" : type === "accelerator" ? "Accelerator" : type === "submap" ? "Submap" : "Component";
|
|
1589
|
+
return String(roundAsWritten(n));
|
|
849
1590
|
}
|
|
850
1591
|
function uniqueNames(map) {
|
|
851
|
-
const
|
|
1592
|
+
const namesInUse = new UniqueNames();
|
|
852
1593
|
const byId = /* @__PURE__ */ new Map();
|
|
853
1594
|
for (const el of map.elements) {
|
|
854
|
-
if (
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1595
|
+
if (NAMED_TYPES.has(el.elementType)) {
|
|
1596
|
+
byId.set(el.id, namesInUse.claim(writtenName(el.label, el.elementType)));
|
|
1597
|
+
} else if (el.elementType === "pipeline") {
|
|
1598
|
+
byId.set(el.id, writtenName(el.label, el.elementType));
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return byId;
|
|
1602
|
+
}
|
|
1603
|
+
function urlDefinitionNames(map, nameOf, keptDefinitionNames) {
|
|
1604
|
+
const namesInUse = new UniqueNames();
|
|
1605
|
+
for (const name of keptDefinitionNames) namesInUse.claim(name);
|
|
1606
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1607
|
+
for (const el of map.elements) {
|
|
1608
|
+
if (!urlOf(el)) continue;
|
|
1609
|
+
const readable = nameOf(el).replace(/[()[\]<>]/g, " ").replace(/\s+/g, " ").trim();
|
|
1610
|
+
byId.set(el.id, namesInUse.claim(`${readable} URL`.trim()));
|
|
861
1611
|
}
|
|
862
1612
|
return byId;
|
|
863
1613
|
}
|
|
@@ -878,30 +1628,75 @@ function decoratorSuffix(dec) {
|
|
|
878
1628
|
if (dec.inertia) out += " inertia";
|
|
879
1629
|
return out;
|
|
880
1630
|
}
|
|
881
|
-
function
|
|
882
|
-
const lines = [];
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
const b = map.config.annotationsBoxPosition;
|
|
1631
|
+
function configLines(config) {
|
|
1632
|
+
const lines = [`title ${writtenText(config.title)}`];
|
|
1633
|
+
if (config.style) lines.push(`style ${config.style}`);
|
|
1634
|
+
if (config.size) lines.push(`size [${r(config.size.width)}, ${r(config.size.height)}]`);
|
|
1635
|
+
if (config.evolutionLabels) {
|
|
1636
|
+
lines.push(`evolution ${config.evolutionLabels.map(writtenText).join("->")}`);
|
|
1637
|
+
}
|
|
1638
|
+
if (config.yAxisLabel) {
|
|
1639
|
+
const ends = config.yAxisEndLabels;
|
|
1640
|
+
const endLabels = ends ? `->${writtenText(ends[0])}->${writtenText(ends[1])}` : "";
|
|
1641
|
+
lines.push(`y-axis ${writtenText(config.yAxisLabel)}${endLabels}`);
|
|
1642
|
+
}
|
|
1643
|
+
if (config.annotationsBoxPosition) {
|
|
1644
|
+
const b = config.annotationsBoxPosition;
|
|
896
1645
|
lines.push(`annotations [${r(b.visibility)}, ${r(b.evolution)}]`);
|
|
897
1646
|
}
|
|
898
|
-
|
|
1647
|
+
return lines;
|
|
1648
|
+
}
|
|
1649
|
+
function passthroughLines(map) {
|
|
1650
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
1651
|
+
if (map.config.evolutionLabels) emitted.add("evolution");
|
|
1652
|
+
if (map.config.yAxisLabel) emitted.add("y-axis");
|
|
1653
|
+
const comments = new CommentStripper();
|
|
1654
|
+
const lines = [];
|
|
1655
|
+
const urlDefinitionNames2 = [];
|
|
1656
|
+
for (const raw of map.rawPassthrough ?? []) {
|
|
1657
|
+
const code = comments.strip(raw).code;
|
|
1658
|
+
const keyword = keywordOf(code);
|
|
1659
|
+
if (emitted.has(keyword)) continue;
|
|
1660
|
+
lines.push(raw);
|
|
1661
|
+
const definition = keyword === "url" ? readUrlDefinition(code.trim().slice(3)) : null;
|
|
1662
|
+
if (definition) urlDefinitionNames2.push(definition.name);
|
|
1663
|
+
}
|
|
1664
|
+
return { lines, urlDefinitionNames: urlDefinitionNames2 };
|
|
1665
|
+
}
|
|
1666
|
+
function pipelineHeights(map, pipelineIds, nameOf) {
|
|
1667
|
+
const componentVisibilityByName = /* @__PURE__ */ new Map();
|
|
899
1668
|
for (const el of map.elements) {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1669
|
+
if (el.elementType !== "component") continue;
|
|
1670
|
+
const isChild = !!el.pipelineId && pipelineIds.has(el.pipelineId);
|
|
1671
|
+
if (!isChild) componentVisibilityByName.set(nameOf(el), el.position.visibility);
|
|
1672
|
+
}
|
|
1673
|
+
return (el) => {
|
|
1674
|
+
if (el.elementType !== "pipeline") return "";
|
|
1675
|
+
const derived = componentVisibilityByName.get(nameOf(el)) ?? 0.5;
|
|
1676
|
+
const written = r(el.position.visibility);
|
|
1677
|
+
return written !== r(derived) ? ` (y ${written})` : "";
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
function edgeLines(map, nameOf) {
|
|
1681
|
+
const namesById = new Map(map.elements.map((el) => [el.id, nameOf(el)]));
|
|
1682
|
+
return map.edges.map((edge) => {
|
|
1683
|
+
const from = namesById.get(edge.from) ?? edge.from;
|
|
1684
|
+
const to = namesById.get(edge.to) ?? edge.to;
|
|
1685
|
+
const annotation = edge.label ? `; ${writtenText(edge.label)}` : "";
|
|
1686
|
+
if (edge.edgeType === "dependency") return `${from} -> ${to}${annotation}`;
|
|
1687
|
+
const op = edge.flowValue ? `+'${edge.flowValue}'${edge.bidirectional ? "<>" : ">"}` : edge.bidirectional ? "+<>" : "+>";
|
|
1688
|
+
return `${from} ${op} ${to}${annotation}`;
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
function serializeDSL(map) {
|
|
1692
|
+
const names = uniqueNames(map);
|
|
1693
|
+
const nameOf = (el) => names.get(el.id) ?? writtenText(el.label);
|
|
1694
|
+
const passthrough = passthroughLines(map);
|
|
1695
|
+
const lines = configLines(map.config);
|
|
1696
|
+
const urlDefNames = urlDefinitionNames(map, nameOf, passthrough.urlDefinitionNames);
|
|
1697
|
+
for (const el of map.elements) {
|
|
1698
|
+
const defName = urlDefNames.get(el.id);
|
|
1699
|
+
if (defName) lines.push(`url ${defName} [${urlOf(el)}]`);
|
|
905
1700
|
}
|
|
906
1701
|
const urlSuffix = (el) => {
|
|
907
1702
|
const def = urlDefNames.get(el.id);
|
|
@@ -919,61 +1714,28 @@ function serializeDSL(map) {
|
|
|
919
1714
|
list.push(el);
|
|
920
1715
|
childrenByPipeline.set(el.pipelineId, list);
|
|
921
1716
|
}
|
|
922
|
-
const
|
|
1717
|
+
const pipelineHeight = pipelineHeights(map, pipelineIds, nameOf);
|
|
923
1718
|
const evolveLines = [];
|
|
924
|
-
const componentVisByLabel = /* @__PURE__ */ new Map();
|
|
925
|
-
for (const el of map.elements) {
|
|
926
|
-
if (el.elementType === "component" && !componentVisByLabel.has(el.label)) {
|
|
927
|
-
componentVisByLabel.set(el.label, el.position.visibility);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
const pipelineYSuffix = (el) => {
|
|
931
|
-
if (el.elementType !== "pipeline") return "";
|
|
932
|
-
const derived = componentVisByLabel.get(el.label) ?? 0.5;
|
|
933
|
-
return Math.abs(el.position.visibility - derived) > 5e-4 ? ` (y ${r(el.position.visibility)})` : "";
|
|
934
|
-
};
|
|
935
1719
|
for (const el of map.elements) {
|
|
936
1720
|
if (el.elementType === "component" && el.movement) {
|
|
937
1721
|
evolveLines.push(evolveLine(el, nameOf(el)));
|
|
938
1722
|
}
|
|
939
|
-
if (
|
|
940
|
-
|
|
941
|
-
if (el.elementType === "pipeline") {
|
|
942
|
-
const kids = childrenByPipeline.get(el.id) ?? [];
|
|
943
|
-
if (kids.length) {
|
|
944
|
-
lines.push("{");
|
|
945
|
-
for (const k of kids) {
|
|
946
|
-
lines.push(
|
|
947
|
-
` component ${nameOf(k)} [${r(k.position.evolution)}]${decoratorSuffix(k.decorators)}${colorSuffix(k)}${offsetSuffix(k.labelOffset)}`
|
|
948
|
-
);
|
|
949
|
-
}
|
|
950
|
-
lines.push("}");
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
for (const line of evolveLines) lines.push(line);
|
|
955
|
-
const labelsById = new Map(map.elements.map((el) => [el.id, el.label]));
|
|
956
|
-
for (const edge of map.edges) {
|
|
957
|
-
const from = names.get(edge.from) ?? labelsById.get(edge.from) ?? edge.from;
|
|
958
|
-
const to = names.get(edge.to) ?? labelsById.get(edge.to) ?? edge.to;
|
|
959
|
-
const annotation = edge.label ? `; ${edge.label}` : "";
|
|
960
|
-
if (edge.edgeType === "dependency") {
|
|
961
|
-
lines.push(`${from} -> ${to}${annotation}`);
|
|
962
|
-
} else {
|
|
963
|
-
const op = edge.flowValue ? `+'${edge.flowValue}'${edge.bidirectional ? "<>" : ">"}` : edge.bidirectional ? "+<>" : "+>";
|
|
964
|
-
lines.push(`${from} ${op} ${to}${annotation}`);
|
|
1723
|
+
if (el.elementType === "component" && el.pipelineId && pipelineIds.has(el.pipelineId)) {
|
|
1724
|
+
continue;
|
|
965
1725
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1726
|
+
lines.push(elementLine(el, nameOf(el)) + pipelineHeight(el) + urlSuffix(el));
|
|
1727
|
+
const kids = childrenByPipeline.get(el.id) ?? [];
|
|
1728
|
+
if (kids.length) {
|
|
1729
|
+
lines.push("{");
|
|
1730
|
+
for (const k of kids) {
|
|
1731
|
+
lines.push(
|
|
1732
|
+
` component ${nameOf(k)} [${r(k.position.evolution)}]${decoratorSuffix(k.decorators)}${colorSuffix(k)}${offsetSuffix(k.labelOffset)}`
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
lines.push("}");
|
|
974
1736
|
}
|
|
975
1737
|
}
|
|
976
|
-
return lines.join("\n")
|
|
1738
|
+
return [...lines, ...evolveLines, ...edgeLines(map, nameOf), ...passthrough.lines, ""].join("\n");
|
|
977
1739
|
}
|
|
978
1740
|
function elementLine(el, name) {
|
|
979
1741
|
const p = el.position;
|
|
@@ -983,14 +1745,14 @@ function elementLine(el, name) {
|
|
|
983
1745
|
case "component":
|
|
984
1746
|
return `component ${name} [${r(p.visibility)}, ${r(p.evolution)}]${decoratorSuffix(el.decorators)}${colorSuffix(el)}${offsetSuffix(el.labelOffset)}`;
|
|
985
1747
|
case "note":
|
|
986
|
-
return `note ${
|
|
1748
|
+
return `note ${writtenNoteText(el.label)} [${r(p.visibility)}, ${r(p.evolution)}]${colorSuffix(el)}`;
|
|
987
1749
|
case "pipeline":
|
|
988
1750
|
return `pipeline ${name} [${r(el.evolutionStart)}, ${r(el.evolutionEnd)}]${colorSuffix(el)}`;
|
|
989
1751
|
case "submap":
|
|
990
1752
|
return `submap ${name} [${r(p.visibility)}, ${r(p.evolution)}]${colorSuffix(el)}`;
|
|
991
1753
|
case "annotation": {
|
|
992
1754
|
const pos = el.positions.length > 1 ? `[${el.positions.map((q) => `[${r(q.visibility)}, ${r(q.evolution)}]`).join(", ")}]` : `[${r(p.visibility)}, ${r(p.evolution)}]`;
|
|
993
|
-
return `annotation ${el.number} ${pos}${colorSuffix(el)} ${el.text}`;
|
|
1755
|
+
return `annotation ${el.number} ${pos}${colorSuffix(el)} ${writtenText(el.text)}`;
|
|
994
1756
|
}
|
|
995
1757
|
case "accelerator":
|
|
996
1758
|
return `${el.direction === "deaccelerate" ? "deaccelerator" : "accelerator"} ${name} [${r(p.visibility)}, ${r(p.evolution)}]${colorSuffix(el)}`;
|
|
@@ -1006,7 +1768,7 @@ function elementLine(el, name) {
|
|
|
1006
1768
|
}
|
|
1007
1769
|
function evolveLine(el, name) {
|
|
1008
1770
|
const mv = el.movement;
|
|
1009
|
-
const label = mv.newLabel ? `${name}->${mv.newLabel}` : name;
|
|
1771
|
+
const label = mv.newLabel ? `${name}->${writtenText(mv.newLabel)}` : name;
|
|
1010
1772
|
const method = mv.method ? ` (${mv.method})` : "";
|
|
1011
1773
|
return `evolve ${label} ${r(mv.targetEvolution)}${method}${offsetSuffix(mv.labelOffset)}`;
|
|
1012
1774
|
}
|