@istar-ts/core 0.2.0 → 0.2.1
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 +1486 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +578 -0
- package/package.json +12 -5
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1486 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/metamodel.ts
|
|
3
|
+
/**
|
|
4
|
+
* The iStar 2.0 metamodel, ported from piStar's `tool/language/metamodel.js`.
|
|
5
|
+
*
|
|
6
|
+
* Kind names carry the `istar.` prefix exactly as they appear in piStar save files.
|
|
7
|
+
*/
|
|
8
|
+
const ISTAR_PREFIX = "istar";
|
|
9
|
+
const METAMODEL_VERSION = "0.2";
|
|
10
|
+
/** Containers: elements that hold inner elements (piStar `metamodel.containers`). */
|
|
11
|
+
const ACTOR_KINDS = [
|
|
12
|
+
"istar.Actor",
|
|
13
|
+
"istar.Agent",
|
|
14
|
+
"istar.Role"
|
|
15
|
+
];
|
|
16
|
+
/** Intentional elements (piStar `metamodel.nodes`). */
|
|
17
|
+
const NODE_KINDS = [
|
|
18
|
+
"istar.Goal",
|
|
19
|
+
"istar.Quality",
|
|
20
|
+
"istar.Resource",
|
|
21
|
+
"istar.Task"
|
|
22
|
+
];
|
|
23
|
+
const ELEMENT_KINDS = [...ACTOR_KINDS, ...NODE_KINDS];
|
|
24
|
+
/** Links between two containers (piStar `metamodel.containerLinks`). */
|
|
25
|
+
const ACTOR_LINK_KINDS = ["istar.IsALink", "istar.ParticipatesInLink"];
|
|
26
|
+
/** Links that connect two containers through a dependum (piStar `metamodel.dependencyLinks`). */
|
|
27
|
+
const DEPENDENCY_LINK_KINDS = ["istar.DependencyLink"];
|
|
28
|
+
/** Links between two intentional elements (piStar `metamodel.nodeLinks`). */
|
|
29
|
+
const NODE_LINK_KINDS = [
|
|
30
|
+
"istar.AndRefinementLink",
|
|
31
|
+
"istar.OrRefinementLink",
|
|
32
|
+
"istar.NeededByLink",
|
|
33
|
+
"istar.QualificationLink",
|
|
34
|
+
"istar.ContributionLink"
|
|
35
|
+
];
|
|
36
|
+
const LINK_KINDS = [
|
|
37
|
+
...ACTOR_LINK_KINDS,
|
|
38
|
+
...DEPENDENCY_LINK_KINDS,
|
|
39
|
+
...NODE_LINK_KINDS
|
|
40
|
+
];
|
|
41
|
+
/** Possible values of a Contribution link's `label`. */
|
|
42
|
+
const CONTRIBUTION_LABELS = [
|
|
43
|
+
"make",
|
|
44
|
+
"help",
|
|
45
|
+
"hurt",
|
|
46
|
+
"break"
|
|
47
|
+
];
|
|
48
|
+
const innerNode = {
|
|
49
|
+
canBeInnerElement: true,
|
|
50
|
+
canBeDependum: true,
|
|
51
|
+
canBeOnPaper: false
|
|
52
|
+
};
|
|
53
|
+
const NODE_KIND_INFO = {
|
|
54
|
+
"istar.Goal": innerNode,
|
|
55
|
+
"istar.Quality": innerNode,
|
|
56
|
+
"istar.Resource": innerNode,
|
|
57
|
+
"istar.Task": innerNode
|
|
58
|
+
};
|
|
59
|
+
const LINK_KIND_INFO = {
|
|
60
|
+
"istar.IsALink": { label: "is-a" },
|
|
61
|
+
"istar.ParticipatesInLink": { label: "participates-in" },
|
|
62
|
+
"istar.DependencyLink": {},
|
|
63
|
+
"istar.AndRefinementLink": {},
|
|
64
|
+
"istar.OrRefinementLink": {},
|
|
65
|
+
"istar.NeededByLink": { tryReversedWhenAdding: true },
|
|
66
|
+
"istar.QualificationLink": { tryReversedWhenAdding: true },
|
|
67
|
+
"istar.ContributionLink": {
|
|
68
|
+
changeableLabel: true,
|
|
69
|
+
possibleLabels: CONTRIBUTION_LABELS
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Default sizes of each element kind, from piStar's `shapes.js`. For actors this is the
|
|
74
|
+
* initial boundary size; the collapsed actor symbol is a circle of radius 40.
|
|
75
|
+
*/
|
|
76
|
+
const DEFAULT_ELEMENT_SIZE = {
|
|
77
|
+
"istar.Actor": {
|
|
78
|
+
width: 200,
|
|
79
|
+
height: 120
|
|
80
|
+
},
|
|
81
|
+
"istar.Agent": {
|
|
82
|
+
width: 200,
|
|
83
|
+
height: 120
|
|
84
|
+
},
|
|
85
|
+
"istar.Role": {
|
|
86
|
+
width: 200,
|
|
87
|
+
height: 120
|
|
88
|
+
},
|
|
89
|
+
"istar.Goal": {
|
|
90
|
+
width: 90,
|
|
91
|
+
height: 35
|
|
92
|
+
},
|
|
93
|
+
"istar.Quality": {
|
|
94
|
+
width: 90,
|
|
95
|
+
height: 55
|
|
96
|
+
},
|
|
97
|
+
"istar.Resource": {
|
|
98
|
+
width: 90,
|
|
99
|
+
height: 35
|
|
100
|
+
},
|
|
101
|
+
"istar.Task": {
|
|
102
|
+
width: 95,
|
|
103
|
+
height: 36
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const actorKinds = new Set(ACTOR_KINDS);
|
|
107
|
+
const nodeKinds = new Set(NODE_KINDS);
|
|
108
|
+
const elementKinds = new Set(ELEMENT_KINDS);
|
|
109
|
+
const linkKinds = new Set(LINK_KINDS);
|
|
110
|
+
const actorLinkKinds = new Set(ACTOR_LINK_KINDS);
|
|
111
|
+
const nodeLinkKinds = new Set(NODE_LINK_KINDS);
|
|
112
|
+
function isActorKind(kind) {
|
|
113
|
+
return actorKinds.has(kind);
|
|
114
|
+
}
|
|
115
|
+
function isNodeKind(kind) {
|
|
116
|
+
return nodeKinds.has(kind);
|
|
117
|
+
}
|
|
118
|
+
function isElementKind(kind) {
|
|
119
|
+
return elementKinds.has(kind);
|
|
120
|
+
}
|
|
121
|
+
function isLinkKind(kind) {
|
|
122
|
+
return linkKinds.has(kind);
|
|
123
|
+
}
|
|
124
|
+
function isActorLinkKind(kind) {
|
|
125
|
+
return actorLinkKinds.has(kind);
|
|
126
|
+
}
|
|
127
|
+
function isNodeLinkKind(kind) {
|
|
128
|
+
return nodeLinkKinds.has(kind);
|
|
129
|
+
}
|
|
130
|
+
function isDependencyLinkKind(kind) {
|
|
131
|
+
return kind === "istar.DependencyLink";
|
|
132
|
+
}
|
|
133
|
+
function isContributionLabel(value) {
|
|
134
|
+
return CONTRIBUTION_LABELS.includes(value);
|
|
135
|
+
}
|
|
136
|
+
/** `'istar.Goal'` → `'Goal'` */
|
|
137
|
+
function shortKindName(kind) {
|
|
138
|
+
return kind.slice(6);
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/model.ts
|
|
142
|
+
/** Tool identifier written by piStar 2.1.0, whose file format this library emulates. */
|
|
143
|
+
const PISTAR_TOOL = "pistar.2.1.0";
|
|
144
|
+
const ISTAR_VERSION = "2.0";
|
|
145
|
+
const DEFAULT_DIAGRAM = {
|
|
146
|
+
width: 2e3,
|
|
147
|
+
height: 1300
|
|
148
|
+
};
|
|
149
|
+
function createEmptyModel(diagram = DEFAULT_DIAGRAM) {
|
|
150
|
+
return {
|
|
151
|
+
elements: /* @__PURE__ */ new Map(),
|
|
152
|
+
links: /* @__PURE__ */ new Map(),
|
|
153
|
+
diagram,
|
|
154
|
+
tool: PISTAR_TOOL,
|
|
155
|
+
istar: "2.0"
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function isActor(element) {
|
|
159
|
+
return element !== void 0 && isActorKind(element.kind);
|
|
160
|
+
}
|
|
161
|
+
function isNode(element) {
|
|
162
|
+
return element !== void 0 && !isActorKind(element.kind);
|
|
163
|
+
}
|
|
164
|
+
/** Inner elements of an actor, in model order. */
|
|
165
|
+
function childrenOf(model, actorId) {
|
|
166
|
+
const result = [];
|
|
167
|
+
for (const element of model.elements.values()) if (element.parent === actorId) result.push(element);
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
/** Links that have the element as source or target. */
|
|
171
|
+
function linksOf(model, elementId) {
|
|
172
|
+
const result = [];
|
|
173
|
+
for (const link of model.links.values()) if (link.source === elementId || link.target === elementId) result.push(link);
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
/** For a dependum, the two halves of its dependency: depender → dependum → dependee. */
|
|
177
|
+
function dependencyLinksOf(model, dependumId) {
|
|
178
|
+
let inbound;
|
|
179
|
+
let outbound;
|
|
180
|
+
for (const link of model.links.values()) {
|
|
181
|
+
if (link.kind !== "istar.DependencyLink") continue;
|
|
182
|
+
if (link.target === dependumId && !inbound) inbound = link;
|
|
183
|
+
if (link.source === dependumId && !outbound) outbound = link;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
inbound,
|
|
187
|
+
outbound
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/constraints.ts
|
|
192
|
+
const OK = { ok: true };
|
|
193
|
+
function fail(code, reason) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
code,
|
|
197
|
+
reason
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Checks whether a link of `kind` may be added from `source` to `target` in `model`.
|
|
202
|
+
*
|
|
203
|
+
* For `istar.DependencyLink`, `source` is the depender and `target` the dependee (an actor or
|
|
204
|
+
* one of its inner elements); the dependum sits between them and is not part of the check.
|
|
205
|
+
*/
|
|
206
|
+
function canLink(model, source, target, kind) {
|
|
207
|
+
const s = resolve(model, source);
|
|
208
|
+
const t = resolve(model, target);
|
|
209
|
+
if (!s) return fail("unknown-element", `unknown source element "${refId(source)}"`);
|
|
210
|
+
if (!t) return fail("unknown-element", `unknown target element "${refId(target)}"`);
|
|
211
|
+
const check = CHECKS[kind];
|
|
212
|
+
if (!check) return fail("unknown-link-kind", `unknown link kind "${String(kind)}"`);
|
|
213
|
+
return check(new Graph(model), s, t);
|
|
214
|
+
}
|
|
215
|
+
function refId(ref) {
|
|
216
|
+
return typeof ref === "string" ? ref : ref.id;
|
|
217
|
+
}
|
|
218
|
+
function resolve(model, ref) {
|
|
219
|
+
return model.elements.get(refId(ref));
|
|
220
|
+
}
|
|
221
|
+
/** The graph queries used by upstream constraints (`istar.isThereLinkBetween`, etc.). */
|
|
222
|
+
var Graph = class {
|
|
223
|
+
model;
|
|
224
|
+
constructor(model) {
|
|
225
|
+
this.model = model;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* True when a link (optionally of `kind`) already connects the two elements, in either
|
|
229
|
+
* direction. Mirrors `istar.isThereLinkBetween`.
|
|
230
|
+
*/
|
|
231
|
+
isThereLinkBetween(a, b, kind) {
|
|
232
|
+
for (const link of this.model.links.values()) {
|
|
233
|
+
if (kind && link.kind !== kind) continue;
|
|
234
|
+
if (link.source === a.id && link.target === b.id || link.source === b.id && link.target === a.id) return true;
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
isSourceOfType(element, kind) {
|
|
239
|
+
for (const link of this.model.links.values()) if (link.kind === kind && link.source === element.id) return true;
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
isTargetOfType(element, kind) {
|
|
243
|
+
for (const link of this.model.links.values()) if (link.kind === kind && link.target === element.id) return true;
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
const is = (element, ...shortNames) => shortNames.some((name) => element.kind === `istar.${name}`);
|
|
248
|
+
const isKindOfActor = (element) => isActorKind(element.kind);
|
|
249
|
+
const isDependum = (element) => element.isDependum === true;
|
|
250
|
+
const isALink = (graph, source, target) => {
|
|
251
|
+
if (!is(source, "Actor", "Role")) return fail("invalid-source", "the source of Is-A links must be a Role or a general Actor (iStar 2.0 Guide, Page 6).");
|
|
252
|
+
if (source.kind !== target.kind) return fail("actor-types-differ", "the source and target of Is-A links must be of the same type - Actor and Actor, or Role and Role (iStar 2.0 Guide, Page 6).");
|
|
253
|
+
if (!is(target, "Actor", "Role")) return fail("invalid-target", "the target of Is-A links must be a Role or a general Actor (iStar 2.0 Guide, Page 6).");
|
|
254
|
+
if (source.id === target.id) return fail("self-link", "you cannot make Is-A links from an actor onto itself.");
|
|
255
|
+
if (graph.isThereLinkBetween(source, target)) return fail("duplicate-link", "there can only be one Actor link between the same two actors (iStar 2.0 Guide, Page 14).");
|
|
256
|
+
return OK;
|
|
257
|
+
};
|
|
258
|
+
const participatesInLink = (graph, source, target) => {
|
|
259
|
+
if (!isKindOfActor(source)) return fail("invalid-source", "the source of a Participates-In link must be some kind of actor (iStar 2.0 Guide, Page 6)");
|
|
260
|
+
if (!isKindOfActor(target)) return fail("invalid-target", "the target of a Participates-In link must be some kind of actor (iStar 2.0 Guide, Page 6)");
|
|
261
|
+
if (source.id === target.id) return fail("self-link", "you cannot make a Participates-In link from an actor onto itself.");
|
|
262
|
+
if (graph.isThereLinkBetween(source, target)) return fail("duplicate-link", "there can only be one Actor link between the same two actors (iStar 2.0 Guide, Page 14).");
|
|
263
|
+
return OK;
|
|
264
|
+
};
|
|
265
|
+
/** The actor an element belongs to: itself for actors, its parent for inner elements. */
|
|
266
|
+
function owningActorId(element) {
|
|
267
|
+
return isKindOfActor(element) ? element.id : element.parent;
|
|
268
|
+
}
|
|
269
|
+
const dependencyLink = (graph, source, target) => {
|
|
270
|
+
if (source.id === target.id) return fail("self-link", "a Dependency link cannot link an element onto itself");
|
|
271
|
+
if (isDependum(source)) return fail("dependum-endpoint", "a Dependency link cannot start from a dependum");
|
|
272
|
+
if (isDependum(target)) return fail("dependum-endpoint", "a Dependency link cannot end in a dependum");
|
|
273
|
+
if (owningActorId(source) === owningActorId(target)) return fail("same-actor", "a Dependency link must involve two different actors (iStar 2.0 Guide, Page 14)");
|
|
274
|
+
if (graph.isTargetOfType(source, "istar.OrRefinementLink") || graph.isTargetOfType(source, "istar.AndRefinementLink")) return fail("refined-depender", "a refined element cannot be the Depender Element in a Dependency link (iStar 2.0 Guide, Page 14). Instead, you can try to add the Dependency originating from a child.");
|
|
275
|
+
if (graph.isTargetOfType(source, "istar.ContributionLink")) return fail("contributed-depender", "a contributed element cannot be the Depender Element in a Dependency link (iStar 2.0 Guide, Page 14). Instead, you can try to add the Dependency originating from a child.");
|
|
276
|
+
return OK;
|
|
277
|
+
};
|
|
278
|
+
function refinementLink(label) {
|
|
279
|
+
const other = label === "AND" ? "istar.OrRefinementLink" : "istar.AndRefinementLink";
|
|
280
|
+
return (graph, source, target) => {
|
|
281
|
+
if (!is(source, "Task", "Goal")) return fail("invalid-source", `the source of an ${label}-refinement link must be a Goal or a Task (iStar 2.0 Guide, Table 1)`);
|
|
282
|
+
if (!is(target, "Task", "Goal")) return fail("invalid-target", `the target of an ${label}-refinement link must be a Goal or a Task (iStar 2.0 Guide, Table 1)`);
|
|
283
|
+
if (source.id === target.id) return fail("self-link", `you cannot make an ${label}-refinement link from an element onto itself`);
|
|
284
|
+
if (isDependum(source) || isDependum(target)) return fail("dependum-endpoint", `you cannot make an ${label}-refinement link with a dependum (iStar 2.0 Guide, Page 14)`);
|
|
285
|
+
if (source.parent !== target.parent) return fail("different-actors", `the source and target of an ${label}-refinement link must pertain to the same actor (iStar 2.0 Guide, Page 14)`);
|
|
286
|
+
if (graph.isThereLinkBetween(source, target)) return fail("duplicate-link", "there can only be one refinement link between the same two elements");
|
|
287
|
+
if (graph.isSourceOfType(target, "istar.DependencyLink")) return fail("depender-target", "you cannot refine a Depender Element; that is, an element that is the source of a Dependency (iStar 2.0 Guide, Page 14). Instead, you can try to move the dependency to the sub-element.");
|
|
288
|
+
if (graph.isTargetOfType(target, other)) return fail("mixed-refinement", label === "AND" ? "you cannot mix AND-refinements with OR-refinements targeting the same element (iStar 2.0 Guide, Page 10)." : "you cannot mix OR-refinements with AND-refinements targeting the same element (iStar 2.0 Guide, Page 10).");
|
|
289
|
+
return OK;
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const neededByLink = (graph, source, target) => {
|
|
293
|
+
if (!is(source, "Resource")) return fail("invalid-source", "the source of a Needed-By link must be a Resource (iStar 2.0 Guide, Table 1)");
|
|
294
|
+
if (!is(target, "Task")) return fail("invalid-target", "the target of a Needed-By link must be a Task (iStar 2.0 Guide, Table 1)");
|
|
295
|
+
if (source.id === target.id) return fail("self-link", "you cannot make a Needed-By link from an element onto itself");
|
|
296
|
+
if (isDependum(source) || isDependum(target)) return fail("dependum-endpoint", "you cannot make a Needed-By link with a dependum (iStar 2.0 Guide, Page 14)");
|
|
297
|
+
if (source.parent !== target.parent) return fail("different-actors", "the source and target of a Needed-By link must pertain to the same actor (iStar 2.0 Guide, Page 14)");
|
|
298
|
+
if (graph.isThereLinkBetween(source, target)) return fail("duplicate-link", "there can only be one Needed-By link between the same two elements");
|
|
299
|
+
return OK;
|
|
300
|
+
};
|
|
301
|
+
const contributionLink = (graph, source, target) => {
|
|
302
|
+
if (!is(source, "Goal", "Quality", "Task", "Resource")) return fail("invalid-source", "the source of a Contribution link must be a Goal, a Quality, a Task or a Resource (iStar 2.0 Guide, Table 1)");
|
|
303
|
+
if (!is(target, "Quality")) return fail("invalid-target", "the target of a Contribution link must be a Quality (iStar 2.0 Guide, Table 1)");
|
|
304
|
+
if (source.id === target.id) return fail("self-link", "you cannot make a Contribution link from an element onto itself (iStar 2.0 Guide, Page 15)");
|
|
305
|
+
if (isDependum(source) || isDependum(target)) return fail("dependum-endpoint", "you cannot make a Contribution link with a dependum (iStar 2.0 Guide, Page 14)");
|
|
306
|
+
if (source.parent !== target.parent) return fail("different-actors", "the source and target of a Contribution link must pertain to the same actor (iStar 2.0 Guide, Page 14)");
|
|
307
|
+
if (graph.isThereLinkBetween(source, target, "istar.ContributionLink")) return fail("duplicate-link", "there can only be one Contribution link between the same two elements");
|
|
308
|
+
if (graph.isThereLinkBetween(source, target, "istar.QualificationLink")) return fail("contribution-and-qualification", "you cannot have Contribution and Qualification links between the same two elements (iStar 2.0 Guide, Page 15)");
|
|
309
|
+
if (graph.isSourceOfType(target, "istar.DependencyLink")) return fail("depender-target", "you cannot contribute to a Depender Element; that is, an element that is the source of a Dependency (iStar 2.0 Guide, Page 14). Instead, you can try to move the dependency to the sub-quality.");
|
|
310
|
+
return OK;
|
|
311
|
+
};
|
|
312
|
+
const qualificationLink = (graph, source, target) => {
|
|
313
|
+
if (!is(source, "Quality")) return fail("invalid-source", "the source of a Qualification link must be a Quality (iStar 2.0 Guide, Table 1)");
|
|
314
|
+
if (!is(target, "Goal", "Task", "Resource")) return fail("invalid-target", "the target of a Qualification link must be a Goal, a Task or a Resource (iStar 2.0 Guide, Table 1)");
|
|
315
|
+
if (source.id === target.id) return fail("self-link", "you cannot make a Qualification link from an element onto itself");
|
|
316
|
+
if (isDependum(source) || isDependum(target)) return fail("dependum-endpoint", "you cannot make a Qualification link with a dependum (iStar 2.0 Guide, Page 14)");
|
|
317
|
+
if (source.parent !== target.parent) return fail("different-actors", "the source and target of a Qualification link must pertain to the same actor (iStar 2.0 Guide, Page 14)");
|
|
318
|
+
if (graph.isThereLinkBetween(source, target, "istar.QualificationLink")) return fail("duplicate-link", "there can only be one Qualification link between the same two elements");
|
|
319
|
+
if (graph.isThereLinkBetween(source, target, "istar.ContributionLink")) return fail("contribution-and-qualification", "you cannot have Qualification and Contribution links between the same two elements (iStar 2.0 Guide, Page 15)");
|
|
320
|
+
return OK;
|
|
321
|
+
};
|
|
322
|
+
const CHECKS = {
|
|
323
|
+
"istar.IsALink": isALink,
|
|
324
|
+
"istar.ParticipatesInLink": participatesInLink,
|
|
325
|
+
"istar.DependencyLink": dependencyLink,
|
|
326
|
+
"istar.AndRefinementLink": refinementLink("AND"),
|
|
327
|
+
"istar.OrRefinementLink": refinementLink("OR"),
|
|
328
|
+
"istar.NeededByLink": neededByLink,
|
|
329
|
+
"istar.ContributionLink": contributionLink,
|
|
330
|
+
"istar.QualificationLink": qualificationLink
|
|
331
|
+
};
|
|
332
|
+
/**
|
|
333
|
+
* Re-validates every link of a model, the way piStar does when loading a file: dependencies
|
|
334
|
+
* first (each checked before its own links are added), then the remaining links in file
|
|
335
|
+
* order, each against the graph built so far. Invalid links are reported, not removed.
|
|
336
|
+
*/
|
|
337
|
+
function validateModel(model) {
|
|
338
|
+
const issues = [];
|
|
339
|
+
const accepted = /* @__PURE__ */ new Map();
|
|
340
|
+
const view = () => ({
|
|
341
|
+
...model,
|
|
342
|
+
links: accepted
|
|
343
|
+
});
|
|
344
|
+
const report = (linkId, kind, source, target, check) => {
|
|
345
|
+
if (!check.ok) issues.push({
|
|
346
|
+
linkId,
|
|
347
|
+
kind,
|
|
348
|
+
source,
|
|
349
|
+
target,
|
|
350
|
+
code: check.code,
|
|
351
|
+
message: check.reason
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
const dependencyLinks = [...model.links.values()].filter((l) => l.kind === "istar.DependencyLink");
|
|
355
|
+
for (const dependum of model.elements.values()) {
|
|
356
|
+
if (!dependum.isDependum) continue;
|
|
357
|
+
const inbound = dependencyLinks.find((l) => l.target === dependum.id);
|
|
358
|
+
const outbound = dependencyLinks.find((l) => l.source === dependum.id);
|
|
359
|
+
const depender = inbound?.source ?? dependum.dependency?.source;
|
|
360
|
+
const dependee = outbound?.target ?? dependum.dependency?.target;
|
|
361
|
+
if (depender !== void 0 && dependee !== void 0) report(inbound?.id ?? dependum.id, "istar.DependencyLink", depender, dependee, canLink(view(), depender, dependee, "istar.DependencyLink"));
|
|
362
|
+
if (inbound) accepted.set(inbound.id, inbound);
|
|
363
|
+
if (outbound) accepted.set(outbound.id, outbound);
|
|
364
|
+
}
|
|
365
|
+
for (const link of model.links.values()) {
|
|
366
|
+
if (link.kind === "istar.DependencyLink") continue;
|
|
367
|
+
if (model.elements.has(link.source) && model.elements.has(link.target)) report(link.id, link.kind, link.source, link.target, canLink(view(), link.source, link.target, link.kind));
|
|
368
|
+
else report(link.id, link.kind, link.source, link.target, {
|
|
369
|
+
ok: false,
|
|
370
|
+
code: "unknown-element",
|
|
371
|
+
reason: "the link refers to an element that does not exist"
|
|
372
|
+
});
|
|
373
|
+
accepted.set(link.id, link);
|
|
374
|
+
}
|
|
375
|
+
return issues;
|
|
376
|
+
}
|
|
377
|
+
//#endregion
|
|
378
|
+
//#region src/serialization.ts
|
|
379
|
+
var PistarParseError = class extends Error {
|
|
380
|
+
name = "PistarParseError";
|
|
381
|
+
};
|
|
382
|
+
const layouts = /* @__PURE__ */ new WeakMap();
|
|
383
|
+
/** The key order recorded when `model` was parsed, if it came from `parsePistar`. */
|
|
384
|
+
function getSourceLayout(model) {
|
|
385
|
+
return layouts.get(model);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Carries the recorded key order over to a model derived from `from` (the store does this for
|
|
389
|
+
* every edit, so saving an edited file keeps a stable diff).
|
|
390
|
+
*/
|
|
391
|
+
function inheritSourceLayout(from, to) {
|
|
392
|
+
const layout = layouts.get(from);
|
|
393
|
+
if (layout && from !== to) layouts.set(to, layout);
|
|
394
|
+
return to;
|
|
395
|
+
}
|
|
396
|
+
const ELEMENT_KEYS = /* @__PURE__ */ new Set([
|
|
397
|
+
"id",
|
|
398
|
+
"text",
|
|
399
|
+
"type",
|
|
400
|
+
"x",
|
|
401
|
+
"y",
|
|
402
|
+
"customProperties"
|
|
403
|
+
]);
|
|
404
|
+
const ACTOR_KEYS = /* @__PURE__ */ new Set([...ELEMENT_KEYS, "nodes"]);
|
|
405
|
+
const DEPENDENCY_KEYS = /* @__PURE__ */ new Set([
|
|
406
|
+
...ELEMENT_KEYS,
|
|
407
|
+
"source",
|
|
408
|
+
"target"
|
|
409
|
+
]);
|
|
410
|
+
const LINK_KEYS = /* @__PURE__ */ new Set([
|
|
411
|
+
"id",
|
|
412
|
+
"type",
|
|
413
|
+
"source",
|
|
414
|
+
"target",
|
|
415
|
+
"name",
|
|
416
|
+
"customProperties",
|
|
417
|
+
"label"
|
|
418
|
+
]);
|
|
419
|
+
const DIAGRAM_KEYS = /* @__PURE__ */ new Set([
|
|
420
|
+
"width",
|
|
421
|
+
"height",
|
|
422
|
+
"name",
|
|
423
|
+
"customProperties"
|
|
424
|
+
]);
|
|
425
|
+
const TOP_LEVEL_KEYS = [
|
|
426
|
+
"actors",
|
|
427
|
+
"orphans",
|
|
428
|
+
"dependencies",
|
|
429
|
+
"links",
|
|
430
|
+
"display",
|
|
431
|
+
"tool",
|
|
432
|
+
"istar",
|
|
433
|
+
"saveDate",
|
|
434
|
+
"diagram"
|
|
435
|
+
];
|
|
436
|
+
const TOP_LEVEL_KEY_SET = new Set(TOP_LEVEL_KEYS);
|
|
437
|
+
function isRecord(value) {
|
|
438
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
439
|
+
}
|
|
440
|
+
function extraOf(json, known) {
|
|
441
|
+
let extra;
|
|
442
|
+
for (const key of Object.keys(json)) if (!known.has(key)) (extra ??= {})[key] = json[key];
|
|
443
|
+
return extra;
|
|
444
|
+
}
|
|
445
|
+
function asArray(value, where) {
|
|
446
|
+
if (value === void 0) return [];
|
|
447
|
+
if (!Array.isArray(value)) throw new PistarParseError(`"${where}" must be an array`);
|
|
448
|
+
return value;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Parses a piStar save file (JSON text or an already-parsed object). The input is not mutated.
|
|
452
|
+
*
|
|
453
|
+
* Unlike the piStar tool, elements with `x` or `y` equal to 0 are kept: upstream's
|
|
454
|
+
* `if (element.x && element.y)` silently drops them, which looks unintended.
|
|
455
|
+
*/
|
|
456
|
+
function parsePistar(input) {
|
|
457
|
+
let json;
|
|
458
|
+
if (typeof input === "string") try {
|
|
459
|
+
json = JSON.parse(input);
|
|
460
|
+
} catch (error) {
|
|
461
|
+
throw new PistarParseError(`invalid JSON: ${error.message}`, { cause: error });
|
|
462
|
+
}
|
|
463
|
+
else json = structuredClone(input);
|
|
464
|
+
if (!isRecord(json)) throw new PistarParseError("a piStar model must be a JSON object");
|
|
465
|
+
const display = isRecord(json.display) ? json.display : {};
|
|
466
|
+
const usedDisplay = /* @__PURE__ */ new Set();
|
|
467
|
+
const elements = /* @__PURE__ */ new Map();
|
|
468
|
+
const links = /* @__PURE__ */ new Map();
|
|
469
|
+
const displayFor = (id) => {
|
|
470
|
+
const entry = display[id];
|
|
471
|
+
if (!isRecord(entry)) return void 0;
|
|
472
|
+
usedDisplay.add(id);
|
|
473
|
+
return entry;
|
|
474
|
+
};
|
|
475
|
+
const readElement = (raw, where, known, extras) => {
|
|
476
|
+
if (!isRecord(raw)) throw new PistarParseError(`${where}: expected an object`);
|
|
477
|
+
const { id, type } = raw;
|
|
478
|
+
if (typeof id !== "string" || id === "") throw new PistarParseError(`${where}: missing "id"`);
|
|
479
|
+
if (typeof type !== "string" || !isElementKind(type)) throw new PistarParseError(`${where} (${id}): unknown element type ${JSON.stringify(type)}`);
|
|
480
|
+
if (elements.has(id) || links.has(id)) throw new PistarParseError(`${where}: duplicated id "${id}"`);
|
|
481
|
+
const element = {
|
|
482
|
+
id,
|
|
483
|
+
kind: type,
|
|
484
|
+
name: typeof raw.text === "string" ? raw.text : "",
|
|
485
|
+
x: typeof raw.x === "number" ? raw.x : 0,
|
|
486
|
+
y: typeof raw.y === "number" ? raw.y : 0,
|
|
487
|
+
...extras,
|
|
488
|
+
...isRecord(raw.customProperties) ? { customProperties: raw.customProperties } : {},
|
|
489
|
+
...optional("display", displayFor(id)),
|
|
490
|
+
...optional("extra", extraOf(raw, known))
|
|
491
|
+
};
|
|
492
|
+
elements.set(id, element);
|
|
493
|
+
return element;
|
|
494
|
+
};
|
|
495
|
+
asArray(json.actors, "actors").forEach((raw, i) => {
|
|
496
|
+
const actor = readElement(raw, `actors[${i}]`, ACTOR_KEYS, {});
|
|
497
|
+
if (!isActorKind(actor.kind)) throw new PistarParseError(`actors[${i}] (${actor.id}): ${actor.kind} is not an actor kind`);
|
|
498
|
+
asArray(raw.nodes, `actors[${i}].nodes`).forEach((node, j) => {
|
|
499
|
+
if (isActorKind(readElement(node, `actors[${i}].nodes[${j}]`, ELEMENT_KEYS, { parent: actor.id }).kind)) throw new PistarParseError(`actors[${i}].nodes[${j}]: actors cannot be nested`);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
asArray(json.orphans, "orphans").forEach((raw, i) => {
|
|
503
|
+
readElement(raw, `orphans[${i}]`, ELEMENT_KEYS, {});
|
|
504
|
+
});
|
|
505
|
+
asArray(json.dependencies, "dependencies").forEach((raw, i) => {
|
|
506
|
+
const r = raw;
|
|
507
|
+
const dependency = typeof r.source === "string" && typeof r.target === "string" ? { dependency: {
|
|
508
|
+
source: r.source,
|
|
509
|
+
target: r.target
|
|
510
|
+
} } : {};
|
|
511
|
+
readElement(raw, `dependencies[${i}]`, DEPENDENCY_KEYS, {
|
|
512
|
+
isDependum: true,
|
|
513
|
+
...dependency
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
asArray(json.links, "links").forEach((raw, i) => {
|
|
517
|
+
if (!isRecord(raw)) throw new PistarParseError(`links[${i}]: expected an object`);
|
|
518
|
+
const { id, type, source, target } = raw;
|
|
519
|
+
if (typeof id !== "string" || id === "") throw new PistarParseError(`links[${i}]: missing "id"`);
|
|
520
|
+
if (typeof type !== "string" || !isLinkKind(type)) throw new PistarParseError(`links[${i}] (${id}): unknown link type ${JSON.stringify(type)}`);
|
|
521
|
+
if (typeof source !== "string" || typeof target !== "string") throw new PistarParseError(`links[${i}] (${id}): missing "source" or "target"`);
|
|
522
|
+
if (elements.has(id) || links.has(id)) throw new PistarParseError(`links[${i}]: duplicated id "${id}"`);
|
|
523
|
+
links.set(id, {
|
|
524
|
+
id,
|
|
525
|
+
kind: type,
|
|
526
|
+
source,
|
|
527
|
+
target,
|
|
528
|
+
...optional("name", typeof raw.name === "string" ? raw.name : void 0),
|
|
529
|
+
...isRecord(raw.customProperties) ? { customProperties: raw.customProperties } : {},
|
|
530
|
+
...optional("label", typeof raw.label === "string" ? raw.label : void 0),
|
|
531
|
+
...optional("display", displayFor(id)),
|
|
532
|
+
...optional("extra", extraOf(raw, LINK_KEYS))
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
let extraDisplay;
|
|
536
|
+
for (const [id, value] of Object.entries(display)) if (!usedDisplay.has(id)) (extraDisplay ??= {})[id] = value;
|
|
537
|
+
let diagram;
|
|
538
|
+
if (isRecord(json.diagram)) {
|
|
539
|
+
const d = json.diagram;
|
|
540
|
+
diagram = {
|
|
541
|
+
...optional("width", typeof d.width === "number" ? d.width : void 0),
|
|
542
|
+
...optional("height", typeof d.height === "number" ? d.height : void 0),
|
|
543
|
+
...optional("name", typeof d.name === "string" ? d.name : void 0),
|
|
544
|
+
...isRecord(d.customProperties) ? { customProperties: d.customProperties } : {},
|
|
545
|
+
...optional("extra", extraOf(d, DIAGRAM_KEYS))
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
const model = {
|
|
549
|
+
elements,
|
|
550
|
+
links,
|
|
551
|
+
...optional("diagram", diagram),
|
|
552
|
+
...optional("tool", typeof json.tool === "string" ? json.tool : void 0),
|
|
553
|
+
...optional("istar", typeof json.istar === "string" ? json.istar : void 0),
|
|
554
|
+
...optional("saveDate", typeof json.saveDate === "string" ? json.saveDate : void 0),
|
|
555
|
+
...optional("extraDisplay", extraDisplay),
|
|
556
|
+
...optional("extra", extraOf(json, TOP_LEVEL_KEY_SET))
|
|
557
|
+
};
|
|
558
|
+
layouts.set(model, {
|
|
559
|
+
keys: Object.keys(json),
|
|
560
|
+
display: Object.keys(display)
|
|
561
|
+
});
|
|
562
|
+
return model;
|
|
563
|
+
}
|
|
564
|
+
function optional(key, value) {
|
|
565
|
+
return value === void 0 ? {} : { [key]: value };
|
|
566
|
+
}
|
|
567
|
+
function formatDate(date) {
|
|
568
|
+
return date.toUTCString();
|
|
569
|
+
}
|
|
570
|
+
function elementJson(element) {
|
|
571
|
+
return {
|
|
572
|
+
id: element.id,
|
|
573
|
+
text: element.name,
|
|
574
|
+
type: element.kind,
|
|
575
|
+
x: element.x,
|
|
576
|
+
y: element.y,
|
|
577
|
+
...element.customProperties ? { customProperties: { ...element.customProperties } } : {}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
/** Builds the save-file object for a model. `toPistar` is this plus `JSON.stringify`. */
|
|
581
|
+
function toPistarObject(model, options = {}) {
|
|
582
|
+
const actors = [];
|
|
583
|
+
const orphans = [];
|
|
584
|
+
const dependencies = [];
|
|
585
|
+
const children = /* @__PURE__ */ new Map();
|
|
586
|
+
for (const element of model.elements.values()) if (element.parent !== void 0 && !isActorKind(element.kind)) {
|
|
587
|
+
let list = children.get(element.parent);
|
|
588
|
+
if (!list) children.set(element.parent, list = []);
|
|
589
|
+
list.push({
|
|
590
|
+
...elementJson(element),
|
|
591
|
+
...element.extra
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
for (const element of model.elements.values()) if (isActorKind(element.kind)) actors.push({
|
|
595
|
+
...elementJson(element),
|
|
596
|
+
nodes: children.get(element.id) ?? [],
|
|
597
|
+
...element.extra
|
|
598
|
+
});
|
|
599
|
+
else if (element.isDependum) {
|
|
600
|
+
const { inbound, outbound } = dependencyLinksOf(model, element.id);
|
|
601
|
+
dependencies.push({
|
|
602
|
+
...elementJson(element),
|
|
603
|
+
source: inbound?.source ?? element.dependency?.source ?? "",
|
|
604
|
+
target: outbound?.target ?? element.dependency?.target ?? "",
|
|
605
|
+
...element.extra
|
|
606
|
+
});
|
|
607
|
+
} else if (element.parent === void 0 || !model.elements.has(element.parent)) orphans.push({
|
|
608
|
+
...elementJson(element),
|
|
609
|
+
...element.extra
|
|
610
|
+
});
|
|
611
|
+
const links = [];
|
|
612
|
+
for (const link of model.links.values()) links.push({
|
|
613
|
+
id: link.id,
|
|
614
|
+
type: link.kind,
|
|
615
|
+
source: link.source,
|
|
616
|
+
target: link.target,
|
|
617
|
+
...link.name !== void 0 ? { name: link.name } : {},
|
|
618
|
+
...link.customProperties ? { customProperties: { ...link.customProperties } } : {},
|
|
619
|
+
...link.label !== void 0 ? { label: link.label } : {},
|
|
620
|
+
...link.extra
|
|
621
|
+
});
|
|
622
|
+
const layout = layouts.get(model);
|
|
623
|
+
const display = {};
|
|
624
|
+
const displayOf = (id) => {
|
|
625
|
+
const owner = model.elements.get(id) ?? model.links.get(id);
|
|
626
|
+
if (owner) return owner.display;
|
|
627
|
+
return model.extraDisplay?.[id];
|
|
628
|
+
};
|
|
629
|
+
for (const id of layout?.display ?? []) {
|
|
630
|
+
const entry = displayOf(id);
|
|
631
|
+
if (entry !== void 0) display[id] = { ...entry };
|
|
632
|
+
}
|
|
633
|
+
for (const owner of [...model.elements.values(), ...model.links.values()]) if (owner.display && !(owner.id in display)) display[owner.id] = { ...owner.display };
|
|
634
|
+
for (const [id, entry] of Object.entries(model.extraDisplay ?? {})) if (!(id in display)) display[id] = entry;
|
|
635
|
+
let saveDate;
|
|
636
|
+
if (options.saveDate instanceof Date) saveDate = formatDate(options.saveDate);
|
|
637
|
+
else saveDate = options.saveDate ?? model.saveDate ?? formatDate(/* @__PURE__ */ new Date());
|
|
638
|
+
const d = model.diagram;
|
|
639
|
+
const diagram = d && {
|
|
640
|
+
...d.width !== void 0 ? { width: d.width } : {},
|
|
641
|
+
...d.height !== void 0 ? { height: d.height } : {},
|
|
642
|
+
...d.name !== void 0 ? { name: d.name } : {},
|
|
643
|
+
...d.customProperties ? { customProperties: { ...d.customProperties } } : {},
|
|
644
|
+
...d.extra
|
|
645
|
+
};
|
|
646
|
+
const values = {
|
|
647
|
+
...model.extra,
|
|
648
|
+
actors,
|
|
649
|
+
orphans,
|
|
650
|
+
dependencies,
|
|
651
|
+
links,
|
|
652
|
+
display,
|
|
653
|
+
tool: model.tool,
|
|
654
|
+
istar: model.istar,
|
|
655
|
+
saveDate,
|
|
656
|
+
diagram
|
|
657
|
+
};
|
|
658
|
+
if (layout) for (const key of [
|
|
659
|
+
"actors",
|
|
660
|
+
"orphans",
|
|
661
|
+
"dependencies",
|
|
662
|
+
"links",
|
|
663
|
+
"display"
|
|
664
|
+
]) {
|
|
665
|
+
const value = values[key];
|
|
666
|
+
if ((Array.isArray(value) ? value.length === 0 : Object.keys(value).length === 0) && !layout.keys.includes(key)) values[key] = void 0;
|
|
667
|
+
}
|
|
668
|
+
const order = [
|
|
669
|
+
...layout?.keys ?? [],
|
|
670
|
+
...TOP_LEVEL_KEYS,
|
|
671
|
+
...Object.keys(model.extra ?? {})
|
|
672
|
+
];
|
|
673
|
+
const file = {};
|
|
674
|
+
for (const key of order) if (!(key in file) && values[key] !== void 0) file[key] = values[key];
|
|
675
|
+
return file;
|
|
676
|
+
}
|
|
677
|
+
/** Serializes a model to piStar's JSON text (2-space indentation, no trailing newline). */
|
|
678
|
+
function toPistar(model, options) {
|
|
679
|
+
return JSON.stringify(toPistarObject(model, options), null, 2);
|
|
680
|
+
}
|
|
681
|
+
//#endregion
|
|
682
|
+
//#region src/operations.ts
|
|
683
|
+
var ModelOperationError = class extends Error {
|
|
684
|
+
name = "ModelOperationError";
|
|
685
|
+
};
|
|
686
|
+
const defaultIdGenerator = () => {
|
|
687
|
+
const c = globalThis.crypto;
|
|
688
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
689
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
|
|
690
|
+
const r = Math.random() * 16 | 0;
|
|
691
|
+
return (ch === "x" ? r : r & 3 | 8).toString(16);
|
|
692
|
+
});
|
|
693
|
+
};
|
|
694
|
+
function derive(from, changes) {
|
|
695
|
+
return inheritSourceLayout(from, {
|
|
696
|
+
...from,
|
|
697
|
+
...changes
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
function requireElement(model, id) {
|
|
701
|
+
const element = model.elements.get(id);
|
|
702
|
+
if (!element) throw new ModelOperationError(`unknown element "${id}"`);
|
|
703
|
+
return element;
|
|
704
|
+
}
|
|
705
|
+
function requireLink(model, id) {
|
|
706
|
+
const link = model.links.get(id);
|
|
707
|
+
if (!link) throw new ModelOperationError(`unknown link "${id}"`);
|
|
708
|
+
return link;
|
|
709
|
+
}
|
|
710
|
+
function withoutUndefined(value) {
|
|
711
|
+
const out = {};
|
|
712
|
+
for (const [k, v] of Object.entries(value)) if (v !== void 0) out[k] = v;
|
|
713
|
+
return out;
|
|
714
|
+
}
|
|
715
|
+
function addElement(model, input, ctx = {}) {
|
|
716
|
+
const id = input.id ?? (ctx.createId ?? defaultIdGenerator)();
|
|
717
|
+
if (model.elements.has(id) || model.links.has(id)) throw new ModelOperationError(`id "${id}" is already in use`);
|
|
718
|
+
if (input.parent !== void 0) {
|
|
719
|
+
if (isActorKind(input.kind)) throw new ModelOperationError("actors cannot be nested");
|
|
720
|
+
if (!isActorKind(requireElement(model, input.parent).kind)) throw new ModelOperationError(`parent "${input.parent}" is not an actor`);
|
|
721
|
+
}
|
|
722
|
+
const element = withoutUndefined({
|
|
723
|
+
id,
|
|
724
|
+
kind: input.kind,
|
|
725
|
+
name: input.name ?? shortKindName(input.kind),
|
|
726
|
+
x: input.x,
|
|
727
|
+
y: input.y,
|
|
728
|
+
parent: input.parent,
|
|
729
|
+
customProperties: input.customProperties,
|
|
730
|
+
display: input.display
|
|
731
|
+
});
|
|
732
|
+
return {
|
|
733
|
+
model: derive(model, { elements: new Map(model.elements).set(id, element) }),
|
|
734
|
+
element
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function mergeDisplay(current, patch) {
|
|
738
|
+
if (patch === void 0) return current;
|
|
739
|
+
if (patch === null) return void 0;
|
|
740
|
+
const merged = { ...current };
|
|
741
|
+
for (const [key, value] of Object.entries(patch)) if (value === void 0) delete merged[key];
|
|
742
|
+
else merged[key] = value;
|
|
743
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
744
|
+
}
|
|
745
|
+
function updateElement(model, id, patch) {
|
|
746
|
+
const current = requireElement(model, id);
|
|
747
|
+
const next = withoutUndefined({
|
|
748
|
+
...current,
|
|
749
|
+
name: patch.name ?? current.name,
|
|
750
|
+
customProperties: patch.customProperties === void 0 ? current.customProperties : patch.customProperties ?? void 0,
|
|
751
|
+
display: mergeDisplay(current.display, patch.display)
|
|
752
|
+
});
|
|
753
|
+
return derive(model, { elements: new Map(model.elements).set(id, next) });
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Moves an element to an absolute position. Moving an actor shifts its inner elements by the
|
|
757
|
+
* same offset.
|
|
758
|
+
*/
|
|
759
|
+
function moveElement(model, id, x, y) {
|
|
760
|
+
const current = requireElement(model, id);
|
|
761
|
+
const dx = x - current.x;
|
|
762
|
+
const dy = y - current.y;
|
|
763
|
+
if (dx === 0 && dy === 0) return model;
|
|
764
|
+
const elements = new Map(model.elements);
|
|
765
|
+
elements.set(id, {
|
|
766
|
+
...current,
|
|
767
|
+
x,
|
|
768
|
+
y
|
|
769
|
+
});
|
|
770
|
+
if (isActorKind(current.kind)) {
|
|
771
|
+
for (const child of model.elements.values()) if (child.parent === id) elements.set(child.id, {
|
|
772
|
+
...child,
|
|
773
|
+
x: child.x + dx,
|
|
774
|
+
y: child.y + dy
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
return derive(model, { elements });
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Moves an inner element into another actor (or out of any actor with `null`). The element
|
|
781
|
+
* becomes the actor's last child. Links that become invalid are kept; `validateModel` reports
|
|
782
|
+
* them.
|
|
783
|
+
*/
|
|
784
|
+
function nestElement(model, id, parent) {
|
|
785
|
+
const current = requireElement(model, id);
|
|
786
|
+
if (isActorKind(current.kind)) throw new ModelOperationError("actors cannot be nested");
|
|
787
|
+
if (current.isDependum) throw new ModelOperationError("a dependum cannot be nested");
|
|
788
|
+
if ((current.parent ?? null) === parent) return model;
|
|
789
|
+
if (parent !== null && !isActorKind(requireElement(model, parent).kind)) throw new ModelOperationError(`parent "${parent}" is not an actor`);
|
|
790
|
+
const elements = new Map(model.elements);
|
|
791
|
+
elements.delete(id);
|
|
792
|
+
const { parent: _old, ...rest } = current;
|
|
793
|
+
elements.set(id, parent === null ? rest : {
|
|
794
|
+
...rest,
|
|
795
|
+
parent
|
|
796
|
+
});
|
|
797
|
+
return derive(model, { elements });
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Removes elements and everything that depends on them: inner elements of removed actors,
|
|
801
|
+
* links touching removed elements, and the rest of any dependency that loses a part.
|
|
802
|
+
*/
|
|
803
|
+
function removeElements(model, ids) {
|
|
804
|
+
return removeCascade(model, new Set(ids), /* @__PURE__ */ new Set());
|
|
805
|
+
}
|
|
806
|
+
function removeElement(model, id) {
|
|
807
|
+
requireElement(model, id);
|
|
808
|
+
return removeElements(model, [id]);
|
|
809
|
+
}
|
|
810
|
+
function removeCascade(model, elementIds, linkIds) {
|
|
811
|
+
let changed = true;
|
|
812
|
+
while (changed) {
|
|
813
|
+
changed = false;
|
|
814
|
+
for (const element of model.elements.values()) {
|
|
815
|
+
if (elementIds.has(element.id)) continue;
|
|
816
|
+
if (element.parent !== void 0 && elementIds.has(element.parent)) {
|
|
817
|
+
elementIds.add(element.id);
|
|
818
|
+
changed = true;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
for (const link of model.links.values()) {
|
|
822
|
+
if (linkIds.has(link.id)) continue;
|
|
823
|
+
if (elementIds.has(link.source) || elementIds.has(link.target)) {
|
|
824
|
+
linkIds.add(link.id);
|
|
825
|
+
changed = true;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
for (const link of model.links.values()) {
|
|
829
|
+
if (!linkIds.has(link.id) || link.kind !== "istar.DependencyLink") continue;
|
|
830
|
+
for (const end of [link.source, link.target]) if (model.elements.get(end)?.isDependum && !elementIds.has(end)) {
|
|
831
|
+
elementIds.add(end);
|
|
832
|
+
changed = true;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (elementIds.size === 0 && linkIds.size === 0) return model;
|
|
837
|
+
const elements = new Map(model.elements);
|
|
838
|
+
for (const id of elementIds) elements.delete(id);
|
|
839
|
+
const links = new Map(model.links);
|
|
840
|
+
for (const id of linkIds) links.delete(id);
|
|
841
|
+
return derive(model, {
|
|
842
|
+
elements,
|
|
843
|
+
links
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
/** Adds a link between two elements if the iStar constraints allow it. */
|
|
847
|
+
function connect(model, input, options = {}) {
|
|
848
|
+
if (input.kind === "istar.DependencyLink") throw new ModelOperationError("use addDependency to create dependencies");
|
|
849
|
+
let { source, target } = input;
|
|
850
|
+
let reversed = false;
|
|
851
|
+
if (!options.force) {
|
|
852
|
+
let check = canLink(model, source, target, input.kind);
|
|
853
|
+
const tryReversed = options.tryReversed ?? LINK_KIND_INFO[input.kind].tryReversedWhenAdding;
|
|
854
|
+
if (!check.ok && tryReversed) {
|
|
855
|
+
const swapped = canLink(model, target, source, input.kind);
|
|
856
|
+
if (swapped.ok) {
|
|
857
|
+
[source, target] = [target, source];
|
|
858
|
+
reversed = true;
|
|
859
|
+
check = swapped;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (!check.ok) return check;
|
|
863
|
+
} else {
|
|
864
|
+
requireElement(model, source);
|
|
865
|
+
requireElement(model, target);
|
|
866
|
+
}
|
|
867
|
+
const id = input.id ?? (options.createId ?? defaultIdGenerator)();
|
|
868
|
+
if (model.elements.has(id) || model.links.has(id)) throw new ModelOperationError(`id "${id}" is already in use`);
|
|
869
|
+
const link = withoutUndefined({
|
|
870
|
+
id,
|
|
871
|
+
kind: input.kind,
|
|
872
|
+
source,
|
|
873
|
+
target,
|
|
874
|
+
label: input.label,
|
|
875
|
+
name: input.name,
|
|
876
|
+
customProperties: input.customProperties
|
|
877
|
+
});
|
|
878
|
+
return {
|
|
879
|
+
ok: true,
|
|
880
|
+
model: derive(model, { links: new Map(model.links).set(id, link) }),
|
|
881
|
+
link,
|
|
882
|
+
reversed
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
/** Adds depender → dependum → dependee. */
|
|
886
|
+
function addDependency(model, input, options = {}) {
|
|
887
|
+
if (!isNodeKind(input.dependum.kind)) throw new ModelOperationError(`${input.dependum.kind} cannot be a dependum`);
|
|
888
|
+
if (!options.force) {
|
|
889
|
+
const check = canLink(model, input.depender, input.dependee, "istar.DependencyLink");
|
|
890
|
+
if (!check.ok) return check;
|
|
891
|
+
}
|
|
892
|
+
const depender = requireElement(model, input.depender);
|
|
893
|
+
const dependee = requireElement(model, input.dependee);
|
|
894
|
+
const createId = options.createId ?? defaultIdGenerator;
|
|
895
|
+
const added = addElement(model, {
|
|
896
|
+
kind: input.dependum.kind,
|
|
897
|
+
id: input.dependum.id,
|
|
898
|
+
name: input.dependum.name,
|
|
899
|
+
x: input.dependum.x ?? (depender.x + dependee.x) / 2,
|
|
900
|
+
y: input.dependum.y ?? (depender.y + dependee.y) / 2,
|
|
901
|
+
customProperties: input.dependum.customProperties
|
|
902
|
+
}, { createId });
|
|
903
|
+
const dependum = {
|
|
904
|
+
...added.element,
|
|
905
|
+
isDependum: true
|
|
906
|
+
};
|
|
907
|
+
const [id1, id2] = input.linkIds ?? [createId(), createId()];
|
|
908
|
+
const first = {
|
|
909
|
+
id: id1,
|
|
910
|
+
kind: "istar.DependencyLink",
|
|
911
|
+
source: depender.id,
|
|
912
|
+
target: dependum.id
|
|
913
|
+
};
|
|
914
|
+
const second = {
|
|
915
|
+
id: id2,
|
|
916
|
+
kind: "istar.DependencyLink",
|
|
917
|
+
source: dependum.id,
|
|
918
|
+
target: dependee.id
|
|
919
|
+
};
|
|
920
|
+
for (const id of [id1, id2]) if (added.model.elements.has(id) || added.model.links.has(id) || id1 === id2) throw new ModelOperationError(`id "${id}" is already in use`);
|
|
921
|
+
return {
|
|
922
|
+
ok: true,
|
|
923
|
+
model: derive(model, {
|
|
924
|
+
elements: new Map(added.model.elements).set(dependum.id, dependum),
|
|
925
|
+
links: new Map(model.links).set(id1, first).set(id2, second)
|
|
926
|
+
}),
|
|
927
|
+
dependum,
|
|
928
|
+
links: [first, second]
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
/** Removes a link. Removing either half of a dependency removes the whole dependency. */
|
|
932
|
+
function disconnect(model, id) {
|
|
933
|
+
requireLink(model, id);
|
|
934
|
+
return removeCascade(model, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set([id]));
|
|
935
|
+
}
|
|
936
|
+
function pick(value, fallback) {
|
|
937
|
+
return value === void 0 ? fallback : value ?? void 0;
|
|
938
|
+
}
|
|
939
|
+
function updateLink(model, id, patch) {
|
|
940
|
+
const current = requireLink(model, id);
|
|
941
|
+
const next = withoutUndefined({
|
|
942
|
+
...current,
|
|
943
|
+
label: pick(patch.label, current.label),
|
|
944
|
+
name: pick(patch.name, current.name),
|
|
945
|
+
customProperties: pick(patch.customProperties, current.customProperties),
|
|
946
|
+
display: mergeDisplay(current.display, patch.display)
|
|
947
|
+
});
|
|
948
|
+
return derive(model, { links: new Map(model.links).set(id, next) });
|
|
949
|
+
}
|
|
950
|
+
function updateDiagram(model, patch) {
|
|
951
|
+
return derive(model, { diagram: withoutUndefined({
|
|
952
|
+
...model.diagram,
|
|
953
|
+
...patch
|
|
954
|
+
}) });
|
|
955
|
+
}
|
|
956
|
+
/** Where dependency depender/dependee currently point (see `dependencyLinksOf`). */
|
|
957
|
+
function dependencyEnds(model, dependumId) {
|
|
958
|
+
const { inbound, outbound } = dependencyLinksOf(model, dependumId);
|
|
959
|
+
const fallback = model.elements.get(dependumId)?.dependency;
|
|
960
|
+
return {
|
|
961
|
+
depender: inbound?.source ?? fallback?.source,
|
|
962
|
+
dependee: outbound?.target ?? fallback?.target
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
//#endregion
|
|
966
|
+
//#region src/store.ts
|
|
967
|
+
function createModelStore(initial = createEmptyModel(), options = {}) {
|
|
968
|
+
const limit = options.historyLimit ?? 100;
|
|
969
|
+
const ctx = { createId: options.createId ?? defaultIdGenerator };
|
|
970
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
971
|
+
let model = initial;
|
|
972
|
+
let past = [];
|
|
973
|
+
let future = [];
|
|
974
|
+
let tx;
|
|
975
|
+
const emit = (event) => {
|
|
976
|
+
for (const listener of listeners) listener(event);
|
|
977
|
+
};
|
|
978
|
+
const commit = (next, change) => {
|
|
979
|
+
if (next === model) return;
|
|
980
|
+
if (tx) {
|
|
981
|
+
model = next;
|
|
982
|
+
tx.changes.push(change);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const previous = model;
|
|
986
|
+
past.push(previous);
|
|
987
|
+
if (past.length > limit) past = past.slice(past.length - limit);
|
|
988
|
+
future = [];
|
|
989
|
+
model = next;
|
|
990
|
+
emit({
|
|
991
|
+
model,
|
|
992
|
+
previous,
|
|
993
|
+
changes: [change],
|
|
994
|
+
source: "edit"
|
|
995
|
+
});
|
|
996
|
+
};
|
|
997
|
+
return {
|
|
998
|
+
getModel: () => model,
|
|
999
|
+
subscribe(listener) {
|
|
1000
|
+
listeners.add(listener);
|
|
1001
|
+
return () => listeners.delete(listener);
|
|
1002
|
+
},
|
|
1003
|
+
canLink: (source, target, kind) => canLink(model, source, target, kind),
|
|
1004
|
+
addElement(input) {
|
|
1005
|
+
const result = addElement(model, input, ctx);
|
|
1006
|
+
commit(result.model, {
|
|
1007
|
+
type: "addElement",
|
|
1008
|
+
id: result.element.id
|
|
1009
|
+
});
|
|
1010
|
+
return result.element;
|
|
1011
|
+
},
|
|
1012
|
+
updateElement(id, patch) {
|
|
1013
|
+
commit(updateElement(model, id, patch), {
|
|
1014
|
+
type: "updateElement",
|
|
1015
|
+
id,
|
|
1016
|
+
patch
|
|
1017
|
+
});
|
|
1018
|
+
},
|
|
1019
|
+
moveElement(id, x, y) {
|
|
1020
|
+
commit(moveElement(model, id, x, y), {
|
|
1021
|
+
type: "moveElement",
|
|
1022
|
+
id,
|
|
1023
|
+
x,
|
|
1024
|
+
y
|
|
1025
|
+
});
|
|
1026
|
+
},
|
|
1027
|
+
nestElement(id, parent) {
|
|
1028
|
+
commit(nestElement(model, id, parent), {
|
|
1029
|
+
type: "nestElement",
|
|
1030
|
+
id,
|
|
1031
|
+
parent
|
|
1032
|
+
});
|
|
1033
|
+
},
|
|
1034
|
+
removeElement(id) {
|
|
1035
|
+
commit(removeElement(model, id), {
|
|
1036
|
+
type: "removeElements",
|
|
1037
|
+
ids: [id]
|
|
1038
|
+
});
|
|
1039
|
+
},
|
|
1040
|
+
removeElements(ids) {
|
|
1041
|
+
const list = [...ids];
|
|
1042
|
+
commit(removeElements(model, list), {
|
|
1043
|
+
type: "removeElements",
|
|
1044
|
+
ids: list
|
|
1045
|
+
});
|
|
1046
|
+
},
|
|
1047
|
+
setCollapsed(actorId, collapsed) {
|
|
1048
|
+
const patch = { display: { collapsed: collapsed ? true : void 0 } };
|
|
1049
|
+
commit(updateElement(model, actorId, patch), {
|
|
1050
|
+
type: "updateElement",
|
|
1051
|
+
id: actorId,
|
|
1052
|
+
patch
|
|
1053
|
+
});
|
|
1054
|
+
},
|
|
1055
|
+
connect(input, connectOptions) {
|
|
1056
|
+
const result = connect(model, input, {
|
|
1057
|
+
...connectOptions,
|
|
1058
|
+
...ctx
|
|
1059
|
+
});
|
|
1060
|
+
if (result.ok) commit(result.model, {
|
|
1061
|
+
type: "connect",
|
|
1062
|
+
id: result.link.id
|
|
1063
|
+
});
|
|
1064
|
+
return result;
|
|
1065
|
+
},
|
|
1066
|
+
addDependency(input, dependencyOptions) {
|
|
1067
|
+
const result = addDependency(model, input, {
|
|
1068
|
+
...dependencyOptions,
|
|
1069
|
+
...ctx
|
|
1070
|
+
});
|
|
1071
|
+
if (result.ok) commit(result.model, {
|
|
1072
|
+
type: "addDependency",
|
|
1073
|
+
dependumId: result.dependum.id,
|
|
1074
|
+
linkIds: [result.links[0].id, result.links[1].id]
|
|
1075
|
+
});
|
|
1076
|
+
return result;
|
|
1077
|
+
},
|
|
1078
|
+
updateLink(id, patch) {
|
|
1079
|
+
commit(updateLink(model, id, patch), {
|
|
1080
|
+
type: "updateLink",
|
|
1081
|
+
id,
|
|
1082
|
+
patch
|
|
1083
|
+
});
|
|
1084
|
+
},
|
|
1085
|
+
disconnect(id) {
|
|
1086
|
+
commit(disconnect(model, id), {
|
|
1087
|
+
type: "disconnect",
|
|
1088
|
+
id
|
|
1089
|
+
});
|
|
1090
|
+
},
|
|
1091
|
+
updateDiagram(patch) {
|
|
1092
|
+
commit(updateDiagram(model, patch), {
|
|
1093
|
+
type: "updateDiagram",
|
|
1094
|
+
patch
|
|
1095
|
+
});
|
|
1096
|
+
},
|
|
1097
|
+
replace(next) {
|
|
1098
|
+
commit(next, { type: "replace" });
|
|
1099
|
+
},
|
|
1100
|
+
load(next) {
|
|
1101
|
+
if (tx) throw new ModelOperationError("cannot load inside a transaction");
|
|
1102
|
+
const previous = model;
|
|
1103
|
+
model = next;
|
|
1104
|
+
past = [];
|
|
1105
|
+
future = [];
|
|
1106
|
+
emit({
|
|
1107
|
+
model,
|
|
1108
|
+
previous,
|
|
1109
|
+
changes: [],
|
|
1110
|
+
source: "load"
|
|
1111
|
+
});
|
|
1112
|
+
},
|
|
1113
|
+
transaction(fn) {
|
|
1114
|
+
if (tx) {
|
|
1115
|
+
tx.depth++;
|
|
1116
|
+
try {
|
|
1117
|
+
return fn();
|
|
1118
|
+
} finally {
|
|
1119
|
+
tx.depth--;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
tx = {
|
|
1123
|
+
start: model,
|
|
1124
|
+
changes: [],
|
|
1125
|
+
depth: 1
|
|
1126
|
+
};
|
|
1127
|
+
let result;
|
|
1128
|
+
try {
|
|
1129
|
+
result = fn();
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
model = tx.start;
|
|
1132
|
+
tx = void 0;
|
|
1133
|
+
throw error;
|
|
1134
|
+
}
|
|
1135
|
+
const { start, changes } = tx;
|
|
1136
|
+
tx = void 0;
|
|
1137
|
+
if (model !== start) {
|
|
1138
|
+
past.push(start);
|
|
1139
|
+
if (past.length > limit) past = past.slice(past.length - limit);
|
|
1140
|
+
future = [];
|
|
1141
|
+
emit({
|
|
1142
|
+
model,
|
|
1143
|
+
previous: start,
|
|
1144
|
+
changes,
|
|
1145
|
+
source: "edit"
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
return result;
|
|
1149
|
+
},
|
|
1150
|
+
undo() {
|
|
1151
|
+
const previous = past.pop();
|
|
1152
|
+
if (!previous || tx) return false;
|
|
1153
|
+
future.push(model);
|
|
1154
|
+
const current = model;
|
|
1155
|
+
model = previous;
|
|
1156
|
+
emit({
|
|
1157
|
+
model,
|
|
1158
|
+
previous: current,
|
|
1159
|
+
changes: [],
|
|
1160
|
+
source: "undo"
|
|
1161
|
+
});
|
|
1162
|
+
return true;
|
|
1163
|
+
},
|
|
1164
|
+
redo() {
|
|
1165
|
+
const next = future.pop();
|
|
1166
|
+
if (!next || tx) return false;
|
|
1167
|
+
past.push(model);
|
|
1168
|
+
const current = model;
|
|
1169
|
+
model = next;
|
|
1170
|
+
emit({
|
|
1171
|
+
model,
|
|
1172
|
+
previous: current,
|
|
1173
|
+
changes: [],
|
|
1174
|
+
source: "redo"
|
|
1175
|
+
});
|
|
1176
|
+
return true;
|
|
1177
|
+
},
|
|
1178
|
+
canUndo: () => past.length > 0,
|
|
1179
|
+
canRedo: () => future.length > 0,
|
|
1180
|
+
clearHistory() {
|
|
1181
|
+
past = [];
|
|
1182
|
+
future = [];
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
//#endregion
|
|
1187
|
+
//#region src/properties.ts
|
|
1188
|
+
function string(options) {
|
|
1189
|
+
return {
|
|
1190
|
+
type: "string",
|
|
1191
|
+
options: options ?? {},
|
|
1192
|
+
parse(raw) {
|
|
1193
|
+
return {
|
|
1194
|
+
ok: true,
|
|
1195
|
+
value: raw
|
|
1196
|
+
};
|
|
1197
|
+
},
|
|
1198
|
+
serialize(value) {
|
|
1199
|
+
return value;
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
function number(options) {
|
|
1204
|
+
const opts = options ?? {};
|
|
1205
|
+
const integer = options?.integer;
|
|
1206
|
+
const min = options?.min;
|
|
1207
|
+
const max = options?.max;
|
|
1208
|
+
return {
|
|
1209
|
+
type: "number",
|
|
1210
|
+
options: opts,
|
|
1211
|
+
integer,
|
|
1212
|
+
min,
|
|
1213
|
+
max,
|
|
1214
|
+
parse(raw) {
|
|
1215
|
+
const value = Number(raw);
|
|
1216
|
+
if (!Number.isFinite(value)) return {
|
|
1217
|
+
ok: false,
|
|
1218
|
+
error: "must be a finite number"
|
|
1219
|
+
};
|
|
1220
|
+
if (integer && !Number.isInteger(value)) return {
|
|
1221
|
+
ok: false,
|
|
1222
|
+
error: "must be an integer"
|
|
1223
|
+
};
|
|
1224
|
+
if (min !== void 0 && value < min) return {
|
|
1225
|
+
ok: false,
|
|
1226
|
+
error: `must be at least ${min}`
|
|
1227
|
+
};
|
|
1228
|
+
if (max !== void 0 && value > max) return {
|
|
1229
|
+
ok: false,
|
|
1230
|
+
error: `must be at most ${max}`
|
|
1231
|
+
};
|
|
1232
|
+
return {
|
|
1233
|
+
ok: true,
|
|
1234
|
+
value
|
|
1235
|
+
};
|
|
1236
|
+
},
|
|
1237
|
+
serialize(value) {
|
|
1238
|
+
return String(value);
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
function boolean(options) {
|
|
1243
|
+
return {
|
|
1244
|
+
type: "boolean",
|
|
1245
|
+
options: options ?? {},
|
|
1246
|
+
parse(raw) {
|
|
1247
|
+
if (raw === "true") return {
|
|
1248
|
+
ok: true,
|
|
1249
|
+
value: true
|
|
1250
|
+
};
|
|
1251
|
+
if (raw === "false") return {
|
|
1252
|
+
ok: true,
|
|
1253
|
+
value: false
|
|
1254
|
+
};
|
|
1255
|
+
return {
|
|
1256
|
+
ok: false,
|
|
1257
|
+
error: "must be true or false"
|
|
1258
|
+
};
|
|
1259
|
+
},
|
|
1260
|
+
serialize(value) {
|
|
1261
|
+
return value ? "true" : "false";
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
function enumOf(values, options) {
|
|
1266
|
+
return {
|
|
1267
|
+
type: "enum",
|
|
1268
|
+
options: options ?? {},
|
|
1269
|
+
values,
|
|
1270
|
+
parse(raw) {
|
|
1271
|
+
if (values.includes(raw)) return {
|
|
1272
|
+
ok: true,
|
|
1273
|
+
value: raw
|
|
1274
|
+
};
|
|
1275
|
+
return {
|
|
1276
|
+
ok: false,
|
|
1277
|
+
error: `must be one of: ${values.join(", ")}`
|
|
1278
|
+
};
|
|
1279
|
+
},
|
|
1280
|
+
serialize(value) {
|
|
1281
|
+
return value;
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
const prop = {
|
|
1286
|
+
string,
|
|
1287
|
+
number,
|
|
1288
|
+
boolean,
|
|
1289
|
+
enum: enumOf
|
|
1290
|
+
};
|
|
1291
|
+
function isCustomPropertiesRecord(source) {
|
|
1292
|
+
if ("customProperties" in source) return false;
|
|
1293
|
+
const owner = source;
|
|
1294
|
+
return !(typeof owner.kind === "string" && owner.kind.startsWith("istar.") && (typeof owner.x === "number" || owner.source !== void 0 && owner.target !== void 0));
|
|
1295
|
+
}
|
|
1296
|
+
function resolveCustomProperties(source) {
|
|
1297
|
+
if (source === void 0) return void 0;
|
|
1298
|
+
if (isCustomPropertiesRecord(source)) return source;
|
|
1299
|
+
return source.customProperties;
|
|
1300
|
+
}
|
|
1301
|
+
function isRequired(options) {
|
|
1302
|
+
return options.optional !== true && options.default === void 0;
|
|
1303
|
+
}
|
|
1304
|
+
function emptyMeansAbsent(type, optional) {
|
|
1305
|
+
return type !== "string" && optional === true;
|
|
1306
|
+
}
|
|
1307
|
+
function readShape(shape, source) {
|
|
1308
|
+
const custom = resolveCustomProperties(source);
|
|
1309
|
+
const values = {};
|
|
1310
|
+
const issues = [];
|
|
1311
|
+
for (const key of Object.keys(shape)) {
|
|
1312
|
+
const field = shape[key];
|
|
1313
|
+
if (!field) continue;
|
|
1314
|
+
const opts = field.options;
|
|
1315
|
+
const raw = custom !== void 0 && Object.prototype.hasOwnProperty.call(custom, key) ? custom[key] : void 0;
|
|
1316
|
+
if (raw === void 0) {
|
|
1317
|
+
if (opts.default !== void 0) values[key] = opts.default;
|
|
1318
|
+
else if (isRequired(opts)) issues.push({
|
|
1319
|
+
key,
|
|
1320
|
+
message: "is required"
|
|
1321
|
+
});
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
if (raw === "" && field.type !== "string") {
|
|
1325
|
+
if (emptyMeansAbsent(field.type, opts.optional)) {
|
|
1326
|
+
if (opts.default !== void 0) values[key] = opts.default;
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
issues.push({
|
|
1330
|
+
key,
|
|
1331
|
+
message: field.type === "boolean" ? "must be true or false" : field.type === "enum" ? `must be one of: ${field.values?.join(", ") ?? ""}` : "must be a finite number"
|
|
1332
|
+
});
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
const parsed = field.parse(raw);
|
|
1336
|
+
if (!parsed.ok) {
|
|
1337
|
+
issues.push({
|
|
1338
|
+
key,
|
|
1339
|
+
message: parsed.error
|
|
1340
|
+
});
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
values[key] = parsed.value;
|
|
1344
|
+
}
|
|
1345
|
+
return {
|
|
1346
|
+
values,
|
|
1347
|
+
issues
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
function defineProperties(kind, shape, options) {
|
|
1351
|
+
const crossValidate = options?.validate;
|
|
1352
|
+
return {
|
|
1353
|
+
kind,
|
|
1354
|
+
shape,
|
|
1355
|
+
read(source) {
|
|
1356
|
+
return readShape(shape, source);
|
|
1357
|
+
},
|
|
1358
|
+
validate(source) {
|
|
1359
|
+
const { values, issues } = readShape(shape, source);
|
|
1360
|
+
const cross = crossValidate ? crossValidate(values) : [];
|
|
1361
|
+
return [...issues, ...cross];
|
|
1362
|
+
},
|
|
1363
|
+
write(current, patch) {
|
|
1364
|
+
const result = {};
|
|
1365
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1366
|
+
if (current) for (const [key, value] of Object.entries(current)) {
|
|
1367
|
+
if (Object.prototype.hasOwnProperty.call(patch, key)) {
|
|
1368
|
+
const patched = patch[key];
|
|
1369
|
+
if (patched === void 0) continue;
|
|
1370
|
+
const field = shape[key];
|
|
1371
|
+
if (field) result[key] = field.serialize(patched);
|
|
1372
|
+
} else result[key] = value;
|
|
1373
|
+
seen.add(key);
|
|
1374
|
+
}
|
|
1375
|
+
for (const key of Object.keys(shape)) {
|
|
1376
|
+
if (seen.has(key)) continue;
|
|
1377
|
+
if (!Object.prototype.hasOwnProperty.call(patch, key)) continue;
|
|
1378
|
+
const patched = patch[key];
|
|
1379
|
+
if (patched === void 0) continue;
|
|
1380
|
+
const field = shape[key];
|
|
1381
|
+
if (field) result[key] = field.serialize(patched);
|
|
1382
|
+
}
|
|
1383
|
+
return result;
|
|
1384
|
+
},
|
|
1385
|
+
defaults() {
|
|
1386
|
+
const result = {};
|
|
1387
|
+
for (const key of Object.keys(shape)) {
|
|
1388
|
+
const field = shape[key];
|
|
1389
|
+
if (!field) continue;
|
|
1390
|
+
const def = field.options.default;
|
|
1391
|
+
if (def !== void 0) result[key] = field.serialize(def);
|
|
1392
|
+
}
|
|
1393
|
+
return result;
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
function validateModelProperties(model, schemas) {
|
|
1398
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
1399
|
+
for (const schema of schemas) {
|
|
1400
|
+
const list = byKind.get(schema.kind) ?? [];
|
|
1401
|
+
list.push(schema);
|
|
1402
|
+
byKind.set(schema.kind, list);
|
|
1403
|
+
}
|
|
1404
|
+
const issues = [];
|
|
1405
|
+
for (const element of model.elements.values()) {
|
|
1406
|
+
const kindSchemas = byKind.get(element.kind);
|
|
1407
|
+
if (!kindSchemas) continue;
|
|
1408
|
+
for (const schema of kindSchemas) for (const issue of schema.validate(element)) issues.push({
|
|
1409
|
+
...issue,
|
|
1410
|
+
id: element.id
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
for (const link of model.links.values()) {
|
|
1414
|
+
const kindSchemas = byKind.get(link.kind);
|
|
1415
|
+
if (!kindSchemas) continue;
|
|
1416
|
+
for (const schema of kindSchemas) for (const issue of schema.validate(link)) issues.push({
|
|
1417
|
+
...issue,
|
|
1418
|
+
id: link.id
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
return issues;
|
|
1422
|
+
}
|
|
1423
|
+
//#endregion
|
|
1424
|
+
exports.ACTOR_KINDS = ACTOR_KINDS;
|
|
1425
|
+
exports.ACTOR_LINK_KINDS = ACTOR_LINK_KINDS;
|
|
1426
|
+
exports.CONTRIBUTION_LABELS = CONTRIBUTION_LABELS;
|
|
1427
|
+
exports.DEFAULT_DIAGRAM = DEFAULT_DIAGRAM;
|
|
1428
|
+
exports.DEFAULT_ELEMENT_SIZE = DEFAULT_ELEMENT_SIZE;
|
|
1429
|
+
exports.DEPENDENCY_LINK_KINDS = DEPENDENCY_LINK_KINDS;
|
|
1430
|
+
exports.ELEMENT_KINDS = ELEMENT_KINDS;
|
|
1431
|
+
exports.ISTAR_PREFIX = ISTAR_PREFIX;
|
|
1432
|
+
exports.ISTAR_VERSION = ISTAR_VERSION;
|
|
1433
|
+
exports.LINK_KINDS = LINK_KINDS;
|
|
1434
|
+
exports.LINK_KIND_INFO = LINK_KIND_INFO;
|
|
1435
|
+
exports.METAMODEL_VERSION = METAMODEL_VERSION;
|
|
1436
|
+
exports.ModelOperationError = ModelOperationError;
|
|
1437
|
+
exports.NODE_KINDS = NODE_KINDS;
|
|
1438
|
+
exports.NODE_KIND_INFO = NODE_KIND_INFO;
|
|
1439
|
+
exports.NODE_LINK_KINDS = NODE_LINK_KINDS;
|
|
1440
|
+
exports.PISTAR_TOOL = PISTAR_TOOL;
|
|
1441
|
+
exports.PistarParseError = PistarParseError;
|
|
1442
|
+
exports.addDependency = addDependency;
|
|
1443
|
+
exports.addElement = addElement;
|
|
1444
|
+
exports.boolean = boolean;
|
|
1445
|
+
exports.canLink = canLink;
|
|
1446
|
+
exports.childrenOf = childrenOf;
|
|
1447
|
+
exports.connect = connect;
|
|
1448
|
+
exports.createEmptyModel = createEmptyModel;
|
|
1449
|
+
exports.createModelStore = createModelStore;
|
|
1450
|
+
exports.defaultIdGenerator = defaultIdGenerator;
|
|
1451
|
+
exports.defineProperties = defineProperties;
|
|
1452
|
+
exports.dependencyEnds = dependencyEnds;
|
|
1453
|
+
exports.dependencyLinksOf = dependencyLinksOf;
|
|
1454
|
+
exports.disconnect = disconnect;
|
|
1455
|
+
exports.enumOf = enumOf;
|
|
1456
|
+
exports.getSourceLayout = getSourceLayout;
|
|
1457
|
+
exports.inheritSourceLayout = inheritSourceLayout;
|
|
1458
|
+
exports.isActor = isActor;
|
|
1459
|
+
exports.isActorKind = isActorKind;
|
|
1460
|
+
exports.isActorLinkKind = isActorLinkKind;
|
|
1461
|
+
exports.isContributionLabel = isContributionLabel;
|
|
1462
|
+
exports.isDependencyLinkKind = isDependencyLinkKind;
|
|
1463
|
+
exports.isElementKind = isElementKind;
|
|
1464
|
+
exports.isLinkKind = isLinkKind;
|
|
1465
|
+
exports.isNode = isNode;
|
|
1466
|
+
exports.isNodeKind = isNodeKind;
|
|
1467
|
+
exports.isNodeLinkKind = isNodeLinkKind;
|
|
1468
|
+
exports.linksOf = linksOf;
|
|
1469
|
+
exports.moveElement = moveElement;
|
|
1470
|
+
exports.nestElement = nestElement;
|
|
1471
|
+
exports.number = number;
|
|
1472
|
+
exports.parsePistar = parsePistar;
|
|
1473
|
+
exports.prop = prop;
|
|
1474
|
+
exports.removeElement = removeElement;
|
|
1475
|
+
exports.removeElements = removeElements;
|
|
1476
|
+
exports.shortKindName = shortKindName;
|
|
1477
|
+
exports.string = string;
|
|
1478
|
+
exports.toPistar = toPistar;
|
|
1479
|
+
exports.toPistarObject = toPistarObject;
|
|
1480
|
+
exports.updateDiagram = updateDiagram;
|
|
1481
|
+
exports.updateElement = updateElement;
|
|
1482
|
+
exports.updateLink = updateLink;
|
|
1483
|
+
exports.validateModel = validateModel;
|
|
1484
|
+
exports.validateModelProperties = validateModelProperties;
|
|
1485
|
+
|
|
1486
|
+
//# sourceMappingURL=index.cjs.map
|