@formicoidea/labre-framework-uml 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +384 -0
- package/dist/actions.d.ts +430 -0
- package/dist/actions.js +724 -0
- package/dist/background.d.ts +153 -0
- package/dist/background.js +557 -0
- package/dist/board-hit.d.ts +137 -0
- package/dist/board-hit.js +191 -0
- package/dist/commands-manifest.d.ts +19 -0
- package/dist/commands-manifest.js +571 -0
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +983 -0
- package/dist/component.d.ts +521 -0
- package/dist/component.js +708 -0
- package/dist/consts.d.ts +365 -0
- package/dist/consts.js +463 -0
- package/dist/descriptor.d.ts +12 -0
- package/dist/descriptor.js +10 -0
- package/dist/drawio-decode.d.ts +32 -0
- package/dist/drawio-decode.js +96 -0
- package/dist/drawio-import.d.ts +133 -0
- package/dist/drawio-import.js +885 -0
- package/dist/edge-morph.d.ts +154 -0
- package/dist/edge-morph.js +153 -0
- package/dist/edge-styles.d.ts +78 -0
- package/dist/edge-styles.js +210 -0
- package/dist/effects.d.ts +9 -0
- package/dist/effects.js +6 -0
- package/dist/element-renderer.d.ts +64 -0
- package/dist/element-renderer.js +114 -0
- package/dist/element-view.d.ts +234 -0
- package/dist/element-view.js +430 -0
- package/dist/export.d.ts +61 -0
- package/dist/export.js +41 -0
- package/dist/filename.d.ts +17 -0
- package/dist/filename.js +26 -0
- package/dist/frame-tag.d.ts +70 -0
- package/dist/frame-tag.js +78 -0
- package/dist/grammar.d.ts +695 -0
- package/dist/grammar.js +1436 -0
- package/dist/import.d.ts +306 -0
- package/dist/import.js +1270 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +67 -0
- package/dist/interchange.d.ts +125 -0
- package/dist/interchange.js +372 -0
- package/dist/keywords.d.ts +204 -0
- package/dist/keywords.js +361 -0
- package/dist/kinds.d.ts +73 -0
- package/dist/kinds.js +91 -0
- package/dist/legend.d.ts +30 -0
- package/dist/legend.js +255 -0
- package/dist/model.d.ts +862 -0
- package/dist/model.js +1316 -0
- package/dist/morph.d.ts +178 -0
- package/dist/morph.js +496 -0
- package/dist/node/act-glyphs.d.ts +30 -0
- package/dist/node/act-glyphs.js +142 -0
- package/dist/node/compartment-watcher.d.ts +107 -0
- package/dist/node/compartment-watcher.js +163 -0
- package/dist/node/fit.d.ts +85 -0
- package/dist/node/fit.js +129 -0
- package/dist/node/node-renderer.d.ts +49 -0
- package/dist/node/node-renderer.js +611 -0
- package/dist/node/node-view.d.ts +51 -0
- package/dist/node/node-view.js +89 -0
- package/dist/node/paint.d.ts +84 -0
- package/dist/node/paint.js +143 -0
- package/dist/node/sd-glyphs.d.ts +36 -0
- package/dist/node/sd-glyphs.js +79 -0
- package/dist/node/stm-glyphs.d.ts +22 -0
- package/dist/node/stm-glyphs.js +111 -0
- package/dist/node/tier-metrics.d.ts +38 -0
- package/dist/node/tier-metrics.js +45 -0
- package/dist/operators.d.ts +45 -0
- package/dist/operators.js +75 -0
- package/dist/plantuml-import.d.ts +113 -0
- package/dist/plantuml-import.js +1723 -0
- package/dist/plantuml.d.ts +94 -0
- package/dist/plantuml.js +985 -0
- package/dist/presets.d.ts +149 -0
- package/dist/presets.js +302 -0
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +358 -0
- package/dist/reading.d.ts +160 -0
- package/dist/reading.js +648 -0
- package/dist/roles.d.ts +257 -0
- package/dist/roles.js +997 -0
- package/dist/rules.d.ts +267 -0
- package/dist/rules.js +3090 -0
- package/dist/templates.d.ts +2 -0
- package/dist/templates.js +197 -0
- package/dist/toolbar/config.d.ts +228 -0
- package/dist/toolbar/config.js +833 -0
- package/dist/toolbar/icons.d.ts +440 -0
- package/dist/toolbar/icons.js +711 -0
- package/dist/toolbar/senior-tool.d.ts +11 -0
- package/dist/toolbar/senior-tool.js +21 -0
- package/dist/toolbar/uml-menu.d.ts +17 -0
- package/dist/toolbar/uml-menu.js +20 -0
- package/dist/toolbar/uml-senior-button.d.ts +18 -0
- package/dist/toolbar/uml-senior-button.js +22 -0
- package/dist/translations.d.ts +27 -0
- package/dist/translations.js +117 -0
- package/dist/view.d.ts +33 -0
- package/dist/view.js +236 -0
- package/dist/xmi-import.d.ts +174 -0
- package/dist/xmi-import.js +2268 -0
- package/dist/xmi.d.ts +61 -0
- package/dist/xmi.js +1860 -0
- package/dist/xml-reader.d.ts +157 -0
- package/dist/xml-reader.js +430 -0
- package/dist/xml.d.ts +72 -0
- package/dist/xml.js +96 -0
- package/package.json +38 -0
package/dist/xmi.js
ADDED
|
@@ -0,0 +1,1860 @@
|
|
|
1
|
+
import { parseTrigger, } from './grammar.js';
|
|
2
|
+
import { umlCentreInside, umlInteractionTimeline, } from './model.js';
|
|
3
|
+
import { el, serializeDocument, } from './xml.js';
|
|
4
|
+
/**
|
|
5
|
+
* The diagrams as an **XMI 2.5.1 document** — the interchange file every UML
|
|
6
|
+
* tool reads.
|
|
7
|
+
*
|
|
8
|
+
* ## Which XMI, exactly
|
|
9
|
+
*
|
|
10
|
+
* The one a 2016 UML tool writes and reads, and the three strings that say so
|
|
11
|
+
* (Annex E of UML 2.5.1 gives the namespaces; the XMI version is XMI 2.5.1's
|
|
12
|
+
* own):
|
|
13
|
+
*
|
|
14
|
+
* - `xmi:version="20131001"` with `xmlns:xmi="http://www.omg.org/spec/XMI/20131001"`;
|
|
15
|
+
* - `xmlns:uml="http://www.omg.org/spec/UML/20161101"` — the UML 2.5.1
|
|
16
|
+
* metamodel namespace;
|
|
17
|
+
* - `PrimitiveTypes.xmi` under the same base, which is where `String`,
|
|
18
|
+
* `Integer`, `Boolean`, `Real` and `UnlimitedNatural` live.
|
|
19
|
+
*
|
|
20
|
+
* Getting these wrong is the difference between a file Papyrus opens and a file
|
|
21
|
+
* it refuses, which is why they are constants with a citation rather than
|
|
22
|
+
* literals in a template.
|
|
23
|
+
*
|
|
24
|
+
* ## No diagram interchange
|
|
25
|
+
*
|
|
26
|
+
* Not one coordinate is written. UML's DI is a second metamodel with its own
|
|
27
|
+
* namespace, and every tool lays a model out its own way on import anyway — so a
|
|
28
|
+
* half-DI would be a promise the file cannot keep, and a full one is a chantier
|
|
29
|
+
* of its own. What the file carries is the MODEL: the classifiers, their
|
|
30
|
+
* features, and the relationships between them. The picture stays in Labre.
|
|
31
|
+
*
|
|
32
|
+
* The one thing geometry still decides is CONTAINMENT — which package a class is
|
|
33
|
+
* drawn inside, which subject a use case sits in — because on a UML diagram that
|
|
34
|
+
* is not layout, it is the statement (§12.2.4, §18.1.4).
|
|
35
|
+
*
|
|
36
|
+
* ## Deterministic ids
|
|
37
|
+
*
|
|
38
|
+
* `_1`, `_2`, … minted in document order, never a hash and never a random id:
|
|
39
|
+
* the same model exports to the same bytes, so a golden test is a diff and a
|
|
40
|
+
* re-export is not a spurious change in someone's version control.
|
|
41
|
+
*
|
|
42
|
+
* ## Pure
|
|
43
|
+
*
|
|
44
|
+
* A model in, a string out. No `std`, no DOM, no clock — `docs/adr/0012` P3.
|
|
45
|
+
*/
|
|
46
|
+
/* ── The namespaces (UML 2.5.1 Annex E) ───────────────────────────────── */
|
|
47
|
+
export const XMI_VERSION = '20131001';
|
|
48
|
+
export const XMI_NAMESPACE = 'http://www.omg.org/spec/XMI/20131001';
|
|
49
|
+
export const UML_NAMESPACE = 'http://www.omg.org/spec/UML/20161101';
|
|
50
|
+
export const UML_PRIMITIVE_TYPES = `${UML_NAMESPACE}/PrimitiveTypes.xmi`;
|
|
51
|
+
/** What Labre stamps on the XMI extensions it writes. */
|
|
52
|
+
const EXTENDER = 'labre';
|
|
53
|
+
/**
|
|
54
|
+
* The five PrimitiveTypes UML itself ships, keyed by the spellings an author
|
|
55
|
+
* types.
|
|
56
|
+
*
|
|
57
|
+
* Referenced by `href` into `PrimitiveTypes.xmi` rather than declared in the
|
|
58
|
+
* file, because they are not ours to declare: every tool resolves that URI to
|
|
59
|
+
* the same five DataTypes, and a locally minted `String` would be a sixth type
|
|
60
|
+
* that merely looks like it.
|
|
61
|
+
*
|
|
62
|
+
* The lowercase spellings are here because they are what people write —
|
|
63
|
+
* `boolean`, `int`, `string` — and folding them costs nothing while refusing
|
|
64
|
+
* them would leave a perfectly ordinary attribute untyped. `int` and `float` are
|
|
65
|
+
* the two aliases that are NOT mere case: they are named in the map rather than
|
|
66
|
+
* inferred, so the reading is visible.
|
|
67
|
+
*/
|
|
68
|
+
const PRIMITIVE_TYPES = {
|
|
69
|
+
string: 'String',
|
|
70
|
+
integer: 'Integer',
|
|
71
|
+
int: 'Integer',
|
|
72
|
+
boolean: 'Boolean',
|
|
73
|
+
bool: 'Boolean',
|
|
74
|
+
real: 'Real',
|
|
75
|
+
float: 'Real',
|
|
76
|
+
double: 'Real',
|
|
77
|
+
unlimitednatural: 'UnlimitedNatural',
|
|
78
|
+
};
|
|
79
|
+
/** The `xmi:type` each classifier kind is serialized as. */
|
|
80
|
+
const CLASSIFIER_TYPE = {
|
|
81
|
+
class: 'uml:Class',
|
|
82
|
+
interface: 'uml:Interface',
|
|
83
|
+
enumeration: 'uml:Enumeration',
|
|
84
|
+
object: 'uml:InstanceSpecification',
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* The keywords a classifier's `xmi:type` already states.
|
|
88
|
+
*
|
|
89
|
+
* `«interface»` in a box header is not extra information once the element is
|
|
90
|
+
* serialized as a `uml:Interface` — it IS that fact, written in notation. Only
|
|
91
|
+
* the keywords that say something the metamodel has no slot for survive into the
|
|
92
|
+
* extension below.
|
|
93
|
+
*/
|
|
94
|
+
const METACLASS_KEYWORDS = new Set([
|
|
95
|
+
'interface',
|
|
96
|
+
'enumeration',
|
|
97
|
+
'datatype',
|
|
98
|
+
'class',
|
|
99
|
+
'object',
|
|
100
|
+
'instance',
|
|
101
|
+
]);
|
|
102
|
+
/** Annex C keywords that make a Dependency a Usage (§7.8.4, Table C.1). */
|
|
103
|
+
const USAGE_KEYWORDS = new Set(['use', 'call', 'create', 'instantiate']);
|
|
104
|
+
/**
|
|
105
|
+
* The `xmi:type` each deployment cube is serialized as (§19.4.2).
|
|
106
|
+
*
|
|
107
|
+
* Three metaclasses for one glyph: §19.4.4 draws a Device as "a Node graphic
|
|
108
|
+
* with the keyword «device»" and an ExecutionEnvironment as a Node annotated
|
|
109
|
+
* with its own, so the cube is the same drawing and the TYPE is the whole of the
|
|
110
|
+
* difference. Total over the discriminant, so a fourth cube cannot land without
|
|
111
|
+
* being given a metaclass.
|
|
112
|
+
*/
|
|
113
|
+
const DEPLOYMENT_TYPE = {
|
|
114
|
+
node: 'uml:Node',
|
|
115
|
+
device: 'uml:Device',
|
|
116
|
+
'execution-environment': 'uml:ExecutionEnvironment',
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* The keywords the structural metaclasses already state.
|
|
120
|
+
*
|
|
121
|
+
* `«component»` on a component box, `«artifact»` on an artefact, `«device»` on a
|
|
122
|
+
* device: each is the stencil's own seed (`keywords.ts`), each is what the
|
|
123
|
+
* `xmi:type` beside it says, and carrying it into the extension as well would
|
|
124
|
+
* file a tool-specific note whose only content is the element's own metaclass.
|
|
125
|
+
* The same reading {@link METACLASS_KEYWORDS} makes for the classifiers.
|
|
126
|
+
*/
|
|
127
|
+
const STRUCTURAL_KEYWORDS = new Set([
|
|
128
|
+
'component',
|
|
129
|
+
'artifact',
|
|
130
|
+
'node',
|
|
131
|
+
'device',
|
|
132
|
+
'executionenvironment',
|
|
133
|
+
'execution environment',
|
|
134
|
+
]);
|
|
135
|
+
/* ── Id minting ───────────────────────────────────────────────────────── */
|
|
136
|
+
/** `_1`, `_2`, … in the order they are asked for. */
|
|
137
|
+
class Ids {
|
|
138
|
+
#next = 0;
|
|
139
|
+
mint() {
|
|
140
|
+
this.#next += 1;
|
|
141
|
+
return `_${this.#next}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/* ── Geometry helpers ─────────────────────────────────────────────────── */
|
|
145
|
+
/** The area of a box — what "the SMALLEST containing box" is measured on. */
|
|
146
|
+
function areaOf(box) {
|
|
147
|
+
return Math.max(0, box.w) * Math.max(0, box.h);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The smallest container whose box holds this element's centre — `undefined`
|
|
151
|
+
* for one drawn outside them all.
|
|
152
|
+
*
|
|
153
|
+
* The most-nested container wins, which is the only reading that lets a package
|
|
154
|
+
* drawn inside a package mean what it draws.
|
|
155
|
+
*
|
|
156
|
+
* `strictlyLarger` is asked for when the element is ITSELF a container, and it
|
|
157
|
+
* is what makes nesting a strict order: a candidate must be strictly bigger than
|
|
158
|
+
* the thing it contains, so two packages drawn exactly on top of each other
|
|
159
|
+
* cannot each claim the other and the recursive walk below cannot loop.
|
|
160
|
+
*/
|
|
161
|
+
function containerOf(element, containers, strictlyLarger = false) {
|
|
162
|
+
const bounds = element.bounds;
|
|
163
|
+
if (!bounds)
|
|
164
|
+
return undefined;
|
|
165
|
+
const own = areaOf(bounds);
|
|
166
|
+
let best;
|
|
167
|
+
let bestArea = Number.POSITIVE_INFINITY;
|
|
168
|
+
for (const candidate of containers) {
|
|
169
|
+
if (candidate.id === element.id || !candidate.bounds)
|
|
170
|
+
continue;
|
|
171
|
+
if (!umlCentreInside(bounds, candidate.bounds))
|
|
172
|
+
continue;
|
|
173
|
+
const area = areaOf(candidate.bounds);
|
|
174
|
+
if (strictlyLarger && area <= own)
|
|
175
|
+
continue;
|
|
176
|
+
if (area < bestArea) {
|
|
177
|
+
best = candidate;
|
|
178
|
+
bestArea = area;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return best;
|
|
182
|
+
}
|
|
183
|
+
/** Every type name a model mentions, in the order the file will mention it. */
|
|
184
|
+
function typeNamesOf(model) {
|
|
185
|
+
const names = [];
|
|
186
|
+
const add = (value) => {
|
|
187
|
+
if (value)
|
|
188
|
+
names.push(value);
|
|
189
|
+
};
|
|
190
|
+
for (const classifier of model.classifiers) {
|
|
191
|
+
for (const attribute of classifier.attributes)
|
|
192
|
+
add(attribute.type);
|
|
193
|
+
for (const operation of classifier.operations) {
|
|
194
|
+
for (const parameter of operation.parameters)
|
|
195
|
+
add(parameter.type);
|
|
196
|
+
add(operation.returnType);
|
|
197
|
+
}
|
|
198
|
+
add(classifier.instanceOf);
|
|
199
|
+
}
|
|
200
|
+
return names;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Ids for everything a reference can point at, minted before anything is
|
|
204
|
+
* written.
|
|
205
|
+
*
|
|
206
|
+
* Two passes rather than one because XMI is full of FORWARD references: an
|
|
207
|
+
* attribute is typed by a class declared further down, an association names both
|
|
208
|
+
* of its ends, a generalization points at a superclass in another package. A
|
|
209
|
+
* single pass would have to patch strings after the fact, which is how an id
|
|
210
|
+
* that does not resolve gets into a file.
|
|
211
|
+
*/
|
|
212
|
+
function planOf(models, ids) {
|
|
213
|
+
const plan = {
|
|
214
|
+
ids,
|
|
215
|
+
idOf: new Map(),
|
|
216
|
+
classifierByName: new Map(),
|
|
217
|
+
kindOf: new Map(),
|
|
218
|
+
interactionByName: new Map(),
|
|
219
|
+
synthesized: new Map(),
|
|
220
|
+
};
|
|
221
|
+
const claim = (element) => {
|
|
222
|
+
plan.idOf.set(element.id, ids.mint());
|
|
223
|
+
};
|
|
224
|
+
for (const model of models) {
|
|
225
|
+
plan.idOf.set(model.diagram.id, ids.mint());
|
|
226
|
+
for (const node of model.packages)
|
|
227
|
+
claim(node);
|
|
228
|
+
for (const classifier of model.classifiers) {
|
|
229
|
+
claim(classifier);
|
|
230
|
+
plan.kindOf.set(classifier.id, classifier.kind);
|
|
231
|
+
// A named classifier is what a type name resolves to. The FIRST wins:
|
|
232
|
+
// two classes called `Order` on two sheets of one export are one name in
|
|
233
|
+
// one `uml:Model`, and document order is the tie-break everything else in
|
|
234
|
+
// this framework breaks on.
|
|
235
|
+
if (classifier.name && !plan.classifierByName.has(classifier.name)) {
|
|
236
|
+
plan.classifierByName.set(classifier.name, plan.idOf.get(classifier.id));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const node of model.actors)
|
|
240
|
+
claim(node);
|
|
241
|
+
for (const node of model.useCases)
|
|
242
|
+
claim(node);
|
|
243
|
+
for (const node of model.subjects)
|
|
244
|
+
claim(node);
|
|
245
|
+
for (const note of model.notes)
|
|
246
|
+
claim(note);
|
|
247
|
+
// Phase 2, appended AFTER the phase-1 claims and never interleaved with
|
|
248
|
+
// them: the ids are minted in the order they are asked for, so a model with
|
|
249
|
+
// no structural artefacts mints none here and exports to exactly the bytes
|
|
250
|
+
// it exported to before these four lines existed. That is what makes a
|
|
251
|
+
// golden document a regression test rather than a chore.
|
|
252
|
+
for (const component of model.components) {
|
|
253
|
+
claim(component);
|
|
254
|
+
// A port is an `ownedAttribute` and therefore referenceable: §11.6.4's own
|
|
255
|
+
// figure wires a Dependency between two of them.
|
|
256
|
+
for (const port of component.ports)
|
|
257
|
+
claim(port);
|
|
258
|
+
}
|
|
259
|
+
for (const artifact of model.artifacts)
|
|
260
|
+
claim(artifact);
|
|
261
|
+
for (const node of model.nodes)
|
|
262
|
+
claim(node);
|
|
263
|
+
// The behaviour sheets, appended after the structural claims and never
|
|
264
|
+
// interleaved with them, for the reason the four lines above give: a model
|
|
265
|
+
// with no flow on it mints nothing here and exports to exactly the bytes it
|
|
266
|
+
// exported to before these lines existed.
|
|
267
|
+
//
|
|
268
|
+
// Everything a REFERENCE can reach is claimed. An activity edge names its
|
|
269
|
+
// two ends, a partition lists its nodes, a transition names its source and
|
|
270
|
+
// its target, and a composite state is pointed at by nothing but is a
|
|
271
|
+
// `uml:State` all the same — so every drawn glyph gets an id and the
|
|
272
|
+
// minted-on-the-spot ones (the edges, the triggers, the regions) are exactly
|
|
273
|
+
// the elements nothing refers to.
|
|
274
|
+
for (const activity of model.activities) {
|
|
275
|
+
for (const node of activity.nodes)
|
|
276
|
+
claim(node);
|
|
277
|
+
for (const partition of activity.partitions)
|
|
278
|
+
claim(partition);
|
|
279
|
+
}
|
|
280
|
+
for (const machine of model.stateMachines) {
|
|
281
|
+
for (const region of machine.regions)
|
|
282
|
+
claim(region);
|
|
283
|
+
for (const state of machine.states)
|
|
284
|
+
claim(state);
|
|
285
|
+
for (const final of machine.finalStates)
|
|
286
|
+
claim(final);
|
|
287
|
+
for (const pseudo of machine.pseudostates)
|
|
288
|
+
claim(pseudo);
|
|
289
|
+
}
|
|
290
|
+
// Phase 3, appended after the phase-2 claims for the reason those were
|
|
291
|
+
// appended after phase 1's: a model with no conversation on it mints
|
|
292
|
+
// nothing here and exports to exactly the bytes it exported to before these
|
|
293
|
+
// lines existed.
|
|
294
|
+
//
|
|
295
|
+
// The Interaction takes the SHEET's own id, not one of its own: §17.2.4
|
|
296
|
+
// draws the interaction as the frame, so the package and the Interaction are
|
|
297
|
+
// two elements of one sheet and each needs an id — hence the mint here
|
|
298
|
+
// beside, rather than reusing the package's.
|
|
299
|
+
for (const interaction of model.interactions) {
|
|
300
|
+
plan.idOf.set(`${interaction.id}#interaction`, ids.mint());
|
|
301
|
+
if (interaction.name &&
|
|
302
|
+
!plan.interactionByName.has(interaction.name.trim())) {
|
|
303
|
+
plan.interactionByName.set(interaction.name.trim(), plan.idOf.get(`${interaction.id}#interaction`));
|
|
304
|
+
}
|
|
305
|
+
for (const lifeline of interaction.lifelines)
|
|
306
|
+
claim(lifeline);
|
|
307
|
+
for (const execution of interaction.executions)
|
|
308
|
+
claim(execution);
|
|
309
|
+
for (const destruction of interaction.destructions)
|
|
310
|
+
claim(destruction);
|
|
311
|
+
for (const fragment of interaction.fragments)
|
|
312
|
+
claim(fragment);
|
|
313
|
+
// A message is claimed like an artefact: its two occurrences point back
|
|
314
|
+
// at it, and an occurrence written before the message it names is the
|
|
315
|
+
// ordinary case (§17.4 owns the Messages on the Interaction, the
|
|
316
|
+
// occurrences in the ordered fragment list).
|
|
317
|
+
for (const message of interaction.messages) {
|
|
318
|
+
plan.idOf.set(message.id, ids.mint());
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// A type the author named that is neither a UML PrimitiveType nor a box on any
|
|
323
|
+
// sheet — `Money`, `OrderId`. It gets a DataType of its own at the root rather
|
|
324
|
+
// than being dropped: the author typed it, so the file says it, and an
|
|
325
|
+
// importer shows `total : Money` instead of an untyped attribute.
|
|
326
|
+
for (const model of models) {
|
|
327
|
+
for (const name of typeNamesOf(model)) {
|
|
328
|
+
if (PRIMITIVE_TYPES[name.toLowerCase()])
|
|
329
|
+
continue;
|
|
330
|
+
if (plan.classifierByName.has(name))
|
|
331
|
+
continue;
|
|
332
|
+
if (plan.synthesized.has(name))
|
|
333
|
+
continue;
|
|
334
|
+
plan.synthesized.set(name, ids.mint());
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return plan;
|
|
338
|
+
}
|
|
339
|
+
/* ── Values ───────────────────────────────────────────────────────────── */
|
|
340
|
+
/**
|
|
341
|
+
* A default value as the LiteralSpecification it looks like.
|
|
342
|
+
*
|
|
343
|
+
* `3` is a `LiteralInteger` and `true` a `LiteralBoolean` because that is what
|
|
344
|
+
* every tool writes and what an importer can compute with; everything else is a
|
|
345
|
+
* `LiteralString`, verbatim. Nothing is evaluated — §9.5.4's `<default>` is an
|
|
346
|
+
* expression, and this writer is not an interpreter.
|
|
347
|
+
*/
|
|
348
|
+
function literalOf(value, ids) {
|
|
349
|
+
const attrs = (type) => ({
|
|
350
|
+
'xmi:type': type,
|
|
351
|
+
'xmi:id': ids.mint(),
|
|
352
|
+
value,
|
|
353
|
+
});
|
|
354
|
+
if (/^[+-]?\d+$/.test(value)) {
|
|
355
|
+
return el('defaultValue', attrs('uml:LiteralInteger'));
|
|
356
|
+
}
|
|
357
|
+
if (value === 'true' || value === 'false') {
|
|
358
|
+
return el('defaultValue', attrs('uml:LiteralBoolean'));
|
|
359
|
+
}
|
|
360
|
+
return el('defaultValue', attrs('uml:LiteralString'));
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* `lowerValue` and `upperValue` — written only when the author STATED a
|
|
364
|
+
* multiplicity.
|
|
365
|
+
*
|
|
366
|
+
* An omitted range means `1` (§9.5.4 says so in as many words), and that is also
|
|
367
|
+
* the metamodel's default, so writing it out would add two elements that say
|
|
368
|
+
* what the absence already says. The upper bound is an `UnlimitedNatural`, which
|
|
369
|
+
* is what lets it be `*`.
|
|
370
|
+
*/
|
|
371
|
+
function multiplicityElements(multiplicity, ids) {
|
|
372
|
+
if (!multiplicity)
|
|
373
|
+
return [];
|
|
374
|
+
return [
|
|
375
|
+
el('lowerValue', {
|
|
376
|
+
'xmi:type': 'uml:LiteralInteger',
|
|
377
|
+
'xmi:id': ids.mint(),
|
|
378
|
+
value: multiplicity.lower,
|
|
379
|
+
}),
|
|
380
|
+
el('upperValue', {
|
|
381
|
+
'xmi:type': 'uml:LiteralUnlimitedNatural',
|
|
382
|
+
'xmi:id': ids.mint(),
|
|
383
|
+
value: String(multiplicity.upper),
|
|
384
|
+
}),
|
|
385
|
+
];
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* The `<type>` of a typed element — an in-file reference, a UML PrimitiveType,
|
|
389
|
+
* or nothing.
|
|
390
|
+
*
|
|
391
|
+
* The nested `<type xmi:idref="…"/>` form rather than a `type="…"` attribute:
|
|
392
|
+
* both are legal XMI, and the element form is the one that can carry an `href`
|
|
393
|
+
* for a type that lives in ANOTHER document, which is exactly what a
|
|
394
|
+
* PrimitiveType is. One shape for both cases means one thing for a reader to
|
|
395
|
+
* understand and one thing for a spec to check.
|
|
396
|
+
*/
|
|
397
|
+
function typeElement(name, plan) {
|
|
398
|
+
if (!name)
|
|
399
|
+
return undefined;
|
|
400
|
+
const primitive = PRIMITIVE_TYPES[name.toLowerCase()];
|
|
401
|
+
if (primitive) {
|
|
402
|
+
return el('type', {
|
|
403
|
+
'xmi:type': 'uml:PrimitiveType',
|
|
404
|
+
href: `${UML_PRIMITIVE_TYPES}#${primitive}`,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
const local = plan.classifierByName.get(name) ?? plan.synthesized.get(name);
|
|
408
|
+
return local ? el('type', { 'xmi:idref': local }) : undefined;
|
|
409
|
+
}
|
|
410
|
+
/* ── Features ─────────────────────────────────────────────────────────── */
|
|
411
|
+
/** `visibility="public"`, or nothing when the author wrote no marker. */
|
|
412
|
+
function visibilityAttrs(visibility) {
|
|
413
|
+
return visibility ? { visibility } : {};
|
|
414
|
+
}
|
|
415
|
+
/** One attribute compartment line as an `ownedAttribute` (§9.5.4). */
|
|
416
|
+
function attributeElement(property, plan) {
|
|
417
|
+
const children = [];
|
|
418
|
+
const type = typeElement(property.type, plan);
|
|
419
|
+
if (type)
|
|
420
|
+
children.push(type);
|
|
421
|
+
children.push(...multiplicityElements(property.multiplicity, plan.ids));
|
|
422
|
+
if (property.defaultValue) {
|
|
423
|
+
children.push(literalOf(property.defaultValue, plan.ids));
|
|
424
|
+
}
|
|
425
|
+
return el('ownedAttribute', {
|
|
426
|
+
'xmi:type': 'uml:Property',
|
|
427
|
+
'xmi:id': plan.ids.mint(),
|
|
428
|
+
name: property.name,
|
|
429
|
+
...visibilityAttrs(property.visibility),
|
|
430
|
+
// Written only when TRUE: both default to false in the metamodel, and an
|
|
431
|
+
// explicit `false` everywhere is noise in a file a human reads.
|
|
432
|
+
...(property.isDerived ? { isDerived: 'true' } : {}),
|
|
433
|
+
...(property.isStatic ? { isStatic: 'true' } : {}),
|
|
434
|
+
}, children);
|
|
435
|
+
}
|
|
436
|
+
/** One operation compartment line as an `ownedOperation` (§9.6.4). */
|
|
437
|
+
function operationElement(operation, plan) {
|
|
438
|
+
const children = operation.parameters.map(parameter => {
|
|
439
|
+
const parts = [];
|
|
440
|
+
const type = typeElement(parameter.type, plan);
|
|
441
|
+
if (type)
|
|
442
|
+
parts.push(type);
|
|
443
|
+
parts.push(...multiplicityElements(parameter.multiplicity, plan.ids));
|
|
444
|
+
if (parameter.defaultValue) {
|
|
445
|
+
parts.push(literalOf(parameter.defaultValue, plan.ids));
|
|
446
|
+
}
|
|
447
|
+
return el('ownedParameter', {
|
|
448
|
+
'xmi:type': 'uml:Parameter',
|
|
449
|
+
'xmi:id': plan.ids.mint(),
|
|
450
|
+
name: parameter.name,
|
|
451
|
+
// §9.4.4: an omitted direction defaults to `in`. The DEFAULT is applied
|
|
452
|
+
// here, where a file needs a value, rather than in the parser, which
|
|
453
|
+
// reports what the line said.
|
|
454
|
+
direction: parameter.direction ?? 'in',
|
|
455
|
+
}, parts);
|
|
456
|
+
});
|
|
457
|
+
// The return type is a Parameter with `direction="return"` — UML has no
|
|
458
|
+
// separate slot for it, and a writer that invented one would produce an
|
|
459
|
+
// operation whose result no importer can find.
|
|
460
|
+
if (operation.returnType || operation.returnMultiplicity) {
|
|
461
|
+
const parts = [];
|
|
462
|
+
const type = typeElement(operation.returnType, plan);
|
|
463
|
+
if (type)
|
|
464
|
+
parts.push(type);
|
|
465
|
+
parts.push(...multiplicityElements(operation.returnMultiplicity, plan.ids));
|
|
466
|
+
children.push(el('ownedParameter', {
|
|
467
|
+
'xmi:type': 'uml:Parameter',
|
|
468
|
+
'xmi:id': plan.ids.mint(),
|
|
469
|
+
direction: 'return',
|
|
470
|
+
}, parts));
|
|
471
|
+
}
|
|
472
|
+
return el('ownedOperation', {
|
|
473
|
+
'xmi:type': 'uml:Operation',
|
|
474
|
+
'xmi:id': plan.ids.mint(),
|
|
475
|
+
name: operation.name,
|
|
476
|
+
...visibilityAttrs(operation.visibility),
|
|
477
|
+
...(operation.isAbstract ? { isAbstract: 'true' } : {}),
|
|
478
|
+
...(operation.isStatic ? { isStatic: 'true' } : {}),
|
|
479
|
+
...(operation.modifiers.some(m => m.toLowerCase() === 'query')
|
|
480
|
+
? { isQuery: 'true' }
|
|
481
|
+
: {}),
|
|
482
|
+
}, children);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* The keywords the metamodel has nowhere to put, as an XMI extension.
|
|
486
|
+
*
|
|
487
|
+
* `«entity»` on a class is a STEREOTYPE application, and applying a stereotype
|
|
488
|
+
* in UML requires a Profile the author never wrote — so there is no valid place
|
|
489
|
+
* in the model for it. `xmi:Extension` is XMI's own answer to exactly this
|
|
490
|
+
* question (XMI 2.5.1 §7.9): tool-specific content, in a container every reader
|
|
491
|
+
* is required to skip if it does not recognise the extender. So the word travels
|
|
492
|
+
* with the file and no importer trips over it.
|
|
493
|
+
*
|
|
494
|
+
* The keywords that merely restate the metaclass are not extended — see
|
|
495
|
+
* {@link METACLASS_KEYWORDS}.
|
|
496
|
+
*/
|
|
497
|
+
function keywordExtension(keywords, stated = METACLASS_KEYWORDS) {
|
|
498
|
+
const extra = keywords.filter(keyword => !stated.has(keyword.toLowerCase()));
|
|
499
|
+
if (extra.length === 0)
|
|
500
|
+
return undefined;
|
|
501
|
+
return el('xmi:Extension', { extender: EXTENDER }, extra.map(keyword => el('keyword', { name: keyword })));
|
|
502
|
+
}
|
|
503
|
+
/* ── Classifiers ──────────────────────────────────────────────────────── */
|
|
504
|
+
/**
|
|
505
|
+
* One classifier as a `packagedElement`, with every relationship it OWNS.
|
|
506
|
+
*
|
|
507
|
+
* Three relationships are children of their source rather than siblings of it,
|
|
508
|
+
* because that is where the metamodel puts them: a `Generalization` is owned by
|
|
509
|
+
* the specific Classifier, an `InterfaceRealization` by the implementing one,
|
|
510
|
+
* and both are meaningless detached from it.
|
|
511
|
+
*/
|
|
512
|
+
function classifierElement(classifier, model, plan) {
|
|
513
|
+
const id = plan.idOf.get(classifier.id);
|
|
514
|
+
const children = [];
|
|
515
|
+
if (classifier.kind === 'enumeration') {
|
|
516
|
+
// An enumeration's compartment holds LITERALS, not properties: the line is
|
|
517
|
+
// parsed for its name and nothing else, so `NEW` and `+ NEW` both name the
|
|
518
|
+
// same literal.
|
|
519
|
+
for (const literal of classifier.attributes) {
|
|
520
|
+
children.push(el('ownedLiteral', {
|
|
521
|
+
'xmi:type': 'uml:EnumerationLiteral',
|
|
522
|
+
'xmi:id': plan.ids.mint(),
|
|
523
|
+
name: literal.name,
|
|
524
|
+
}));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
else if (classifier.kind === 'object') {
|
|
528
|
+
// A `slot` with a `value` and NO `definingFeature`.
|
|
529
|
+
//
|
|
530
|
+
// The metamodel requires the feature, and it is the one reference this
|
|
531
|
+
// writer cannot honestly mint: `Slot::definingFeature` points at a Property
|
|
532
|
+
// of the instance's Classifier, and an object diagram almost never has that
|
|
533
|
+
// class on it — §11.6.4's compartment names the feature in TEXT, which is
|
|
534
|
+
// all the author gave us. The choice is between a slot a strict validator
|
|
535
|
+
// flags and dropping what somebody typed into their diagram; the slot stays.
|
|
536
|
+
//
|
|
537
|
+
// A `Slot` is not a NamedElement, so the feature's NAME has nowhere to go in
|
|
538
|
+
// the model either, and it rides in the extension XMI provides for exactly
|
|
539
|
+
// this — where a reader that wants it can find it and a reader that does not
|
|
540
|
+
// is required to skip it.
|
|
541
|
+
for (const slot of classifier.slots) {
|
|
542
|
+
children.push(el('slot', { 'xmi:type': 'uml:Slot', 'xmi:id': plan.ids.mint() }, [
|
|
543
|
+
el('value', {
|
|
544
|
+
'xmi:type': 'uml:LiteralString',
|
|
545
|
+
'xmi:id': plan.ids.mint(),
|
|
546
|
+
value: slot.value ?? '',
|
|
547
|
+
}),
|
|
548
|
+
el('xmi:Extension', { extender: EXTENDER }, [
|
|
549
|
+
el('feature', { name: slot.name }),
|
|
550
|
+
]),
|
|
551
|
+
]));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
for (const attribute of classifier.attributes) {
|
|
556
|
+
children.push(attributeElement(attribute, plan));
|
|
557
|
+
}
|
|
558
|
+
for (const operation of classifier.operations) {
|
|
559
|
+
children.push(operationElement(operation, plan));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
for (const relation of model.relations) {
|
|
563
|
+
if (relation.sourceId !== classifier.id)
|
|
564
|
+
continue;
|
|
565
|
+
const target = plan.idOf.get(relation.targetId);
|
|
566
|
+
if (!target)
|
|
567
|
+
continue;
|
|
568
|
+
if (relation.kind === 'generalization') {
|
|
569
|
+
children.push(el('generalization', {
|
|
570
|
+
'xmi:type': 'uml:Generalization',
|
|
571
|
+
'xmi:id': plan.ids.mint(),
|
|
572
|
+
general: target,
|
|
573
|
+
}));
|
|
574
|
+
}
|
|
575
|
+
else if (relation.kind === 'realization' &&
|
|
576
|
+
plan.kindOf.get(relation.targetId) === 'interface') {
|
|
577
|
+
// An InterfaceRealization only when the far end IS an Interface — its
|
|
578
|
+
// `contract` is typed `Interface` in the metamodel, so pointing it at a
|
|
579
|
+
// class would be a reference no conformant reader can resolve. A
|
|
580
|
+
// realization of anything else is written as a `uml:Realization` beside
|
|
581
|
+
// the classifiers instead.
|
|
582
|
+
children.push(el('interfaceRealization', {
|
|
583
|
+
'xmi:type': 'uml:InterfaceRealization',
|
|
584
|
+
'xmi:id': plan.ids.mint(),
|
|
585
|
+
client: id,
|
|
586
|
+
supplier: target,
|
|
587
|
+
contract: target,
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const extension = keywordExtension(classifier.keywords);
|
|
592
|
+
if (extension)
|
|
593
|
+
children.push(extension);
|
|
594
|
+
// `object : Class` — the Classifier the instance is of. Resolved against the
|
|
595
|
+
// boxes on the export first and against the DataTypes minted for the names
|
|
596
|
+
// nothing answered for second, so `order1 : Order` is typed even on an object
|
|
597
|
+
// diagram that does not draw `Order` (which is most of them).
|
|
598
|
+
const instanceOf = classifier.kind === 'object' && classifier.instanceOf
|
|
599
|
+
? (plan.classifierByName.get(classifier.instanceOf) ??
|
|
600
|
+
plan.synthesized.get(classifier.instanceOf))
|
|
601
|
+
: undefined;
|
|
602
|
+
return el('packagedElement', {
|
|
603
|
+
'xmi:type': CLASSIFIER_TYPE[classifier.kind],
|
|
604
|
+
'xmi:id': id,
|
|
605
|
+
name: classifier.name,
|
|
606
|
+
...(classifier.isAbstract ? { isAbstract: 'true' } : {}),
|
|
607
|
+
...(instanceOf ? { classifier: instanceOf } : {}),
|
|
608
|
+
}, children);
|
|
609
|
+
}
|
|
610
|
+
/* ── Use case artefacts ───────────────────────────────────────────────── */
|
|
611
|
+
/** One use case, with the `Include`s and `Extend`s it owns (§18.1.4). */
|
|
612
|
+
function useCaseElement(useCase, model, plan) {
|
|
613
|
+
const children = [];
|
|
614
|
+
for (const relation of model.relations) {
|
|
615
|
+
if (relation.sourceId !== useCase.id)
|
|
616
|
+
continue;
|
|
617
|
+
const target = plan.idOf.get(relation.targetId);
|
|
618
|
+
if (!target)
|
|
619
|
+
continue;
|
|
620
|
+
if (relation.kind === 'include') {
|
|
621
|
+
// Owned by the INCLUDING use case, pointing at the addition — the
|
|
622
|
+
// direction the role table states (source = the base case).
|
|
623
|
+
children.push(el('include', {
|
|
624
|
+
'xmi:type': 'uml:Include',
|
|
625
|
+
'xmi:id': plan.ids.mint(),
|
|
626
|
+
addition: target,
|
|
627
|
+
}));
|
|
628
|
+
}
|
|
629
|
+
else if (relation.kind === 'extend') {
|
|
630
|
+
// Owned by the EXTENDING use case, pointing at the case it extends —
|
|
631
|
+
// `Extend::extension` is the owner in the metamodel, which is why the
|
|
632
|
+
// role's source is the extending case and not the base one.
|
|
633
|
+
children.push(el('extend', {
|
|
634
|
+
'xmi:type': 'uml:Extend',
|
|
635
|
+
'xmi:id': plan.ids.mint(),
|
|
636
|
+
extendedCase: target,
|
|
637
|
+
}));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const extension = keywordExtension(useCase.keywords);
|
|
641
|
+
if (extension)
|
|
642
|
+
children.push(extension);
|
|
643
|
+
return el('packagedElement', {
|
|
644
|
+
'xmi:type': 'uml:UseCase',
|
|
645
|
+
'xmi:id': plan.idOf.get(useCase.id),
|
|
646
|
+
name: useCase.name,
|
|
647
|
+
}, children);
|
|
648
|
+
}
|
|
649
|
+
/* ── Structural artefacts (§11.6, §19.3, §19.4) ───────────────────────── */
|
|
650
|
+
/**
|
|
651
|
+
* One component as a `packagedElement`, with its ports and its two kinds of
|
|
652
|
+
* interface.
|
|
653
|
+
*
|
|
654
|
+
* ## The interfaces are MINTED, and that is the whole difficulty
|
|
655
|
+
*
|
|
656
|
+
* A lollipop on this canvas is a circle with a name written beside it — §10.4.4
|
|
657
|
+
* draws exactly that, and nothing else — so the model holds a STRING where XMI
|
|
658
|
+
* needs an element. There is no honest alternative to minting one: an
|
|
659
|
+
* `InterfaceRealization`'s `contract` is typed `Interface` in the metamodel, so
|
|
660
|
+
* a realization pointing at nothing is a reference no importer can resolve, and
|
|
661
|
+
* dropping the lollipop would throw away the only statement the author made
|
|
662
|
+
* about what the component offers.
|
|
663
|
+
*
|
|
664
|
+
* The minted `uml:Interface` is a `packagedElement` of the COMPONENT rather than
|
|
665
|
+
* of the diagram's package, which §11.6.4's "packaged elements" compartment is
|
|
666
|
+
* the notation for: it is the component's own declaration of the contract it
|
|
667
|
+
* realizes, and putting it at the sheet level would make two components that
|
|
668
|
+
* both provide `IOrder` look like one shared interface the author never drew.
|
|
669
|
+
* Two components providing the same name therefore produce two Interfaces, and
|
|
670
|
+
* that is the honest reading of two lollipops — a drawing that means one
|
|
671
|
+
* interface draws it once and wires both components to it, which is the
|
|
672
|
+
* rectangle notation §11.6.4 offers for exactly this case.
|
|
673
|
+
*
|
|
674
|
+
* ## Provided is an `interfaceRealization`, required is a `uml:Usage`
|
|
675
|
+
*
|
|
676
|
+
* §10.4.4 in as many words: the ball notation IS an InterfaceRealization
|
|
677
|
+
* dependency from the classifier to the interface, and the socket IS a Usage
|
|
678
|
+
* dependency. So the two glyphs are not two decorations on one relationship,
|
|
679
|
+
* they are two different metaclasses, and a writer that collapsed them would
|
|
680
|
+
* lose the direction of every dependency on the sheet.
|
|
681
|
+
*/
|
|
682
|
+
function componentElement(component, plan) {
|
|
683
|
+
const id = plan.idOf.get(component.id);
|
|
684
|
+
const children = [];
|
|
685
|
+
for (const port of component.ports) {
|
|
686
|
+
const portId = plan.idOf.get(port.id);
|
|
687
|
+
if (!portId)
|
|
688
|
+
continue;
|
|
689
|
+
children.push(el('ownedAttribute', {
|
|
690
|
+
'xmi:type': 'uml:Port',
|
|
691
|
+
'xmi:id': portId,
|
|
692
|
+
// §11.3.4: "The name of a Port may be suppressed. Every depiction of an
|
|
693
|
+
// unnamed Port denotes a different Port from any other Port." So an
|
|
694
|
+
// unnamed square is a port with no `name` attribute, never a port called
|
|
695
|
+
// the empty string.
|
|
696
|
+
...(port.name ? { name: port.name } : {}),
|
|
697
|
+
}));
|
|
698
|
+
}
|
|
699
|
+
for (const name of component.provided) {
|
|
700
|
+
const interfaceId = plan.ids.mint();
|
|
701
|
+
children.push(el('packagedElement', {
|
|
702
|
+
'xmi:type': 'uml:Interface',
|
|
703
|
+
'xmi:id': interfaceId,
|
|
704
|
+
name,
|
|
705
|
+
}), el('interfaceRealization', {
|
|
706
|
+
'xmi:type': 'uml:InterfaceRealization',
|
|
707
|
+
'xmi:id': plan.ids.mint(),
|
|
708
|
+
client: id,
|
|
709
|
+
supplier: interfaceId,
|
|
710
|
+
contract: interfaceId,
|
|
711
|
+
}));
|
|
712
|
+
}
|
|
713
|
+
for (const name of component.required) {
|
|
714
|
+
const interfaceId = plan.ids.mint();
|
|
715
|
+
children.push(el('packagedElement', {
|
|
716
|
+
'xmi:type': 'uml:Interface',
|
|
717
|
+
'xmi:id': interfaceId,
|
|
718
|
+
name,
|
|
719
|
+
}), el('packagedElement', {
|
|
720
|
+
'xmi:type': 'uml:Usage',
|
|
721
|
+
'xmi:id': plan.ids.mint(),
|
|
722
|
+
client: id,
|
|
723
|
+
supplier: interfaceId,
|
|
724
|
+
}));
|
|
725
|
+
}
|
|
726
|
+
const extension = keywordExtension(component.keywords, STRUCTURAL_KEYWORDS);
|
|
727
|
+
if (extension)
|
|
728
|
+
children.push(extension);
|
|
729
|
+
return el('packagedElement', {
|
|
730
|
+
'xmi:type': 'uml:Component',
|
|
731
|
+
'xmi:id': id,
|
|
732
|
+
name: component.name,
|
|
733
|
+
...(component.isAbstract ? { isAbstract: 'true' } : {}),
|
|
734
|
+
}, children);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* One artefact, with the Manifestations it OWNS (§19.3.2).
|
|
738
|
+
*
|
|
739
|
+
* The manifestation is a child rather than a sibling because the metamodel says
|
|
740
|
+
* so — `Artifact::manifestation` subsets `ownedElement` — and because it is
|
|
741
|
+
* meaningless detached from the file that does the manifesting, exactly as a
|
|
742
|
+
* Generalization is meaningless detached from the specific classifier.
|
|
743
|
+
*
|
|
744
|
+
* Three attributes for two facts: `utilizedElement` subsets `supplier` and both
|
|
745
|
+
* have to be written, because `utilizedElement` is the name the Manifestation
|
|
746
|
+
* metaclass gives the end and `supplier` is the one `Dependency` gives it, and
|
|
747
|
+
* an importer reading only the general form would otherwise find the dependency
|
|
748
|
+
* empty.
|
|
749
|
+
*/
|
|
750
|
+
function artifactElement(artifact, model, plan) {
|
|
751
|
+
const id = plan.idOf.get(artifact.id);
|
|
752
|
+
const children = [];
|
|
753
|
+
for (const relation of model.relations) {
|
|
754
|
+
if (relation.kind !== 'manifest' || relation.sourceId !== artifact.id) {
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
const utilized = plan.idOf.get(relation.targetId);
|
|
758
|
+
if (!utilized)
|
|
759
|
+
continue;
|
|
760
|
+
children.push(el('manifestation', {
|
|
761
|
+
'xmi:type': 'uml:Manifestation',
|
|
762
|
+
'xmi:id': plan.ids.mint(),
|
|
763
|
+
...(relation.label ? { name: relation.label } : {}),
|
|
764
|
+
client: id,
|
|
765
|
+
supplier: utilized,
|
|
766
|
+
utilizedElement: utilized,
|
|
767
|
+
}));
|
|
768
|
+
}
|
|
769
|
+
const extension = keywordExtension(artifact.keywords, STRUCTURAL_KEYWORDS);
|
|
770
|
+
if (extension)
|
|
771
|
+
children.push(extension);
|
|
772
|
+
return el('packagedElement', {
|
|
773
|
+
'xmi:type': 'uml:Artifact',
|
|
774
|
+
'xmi:id': id,
|
|
775
|
+
name: artifact.name,
|
|
776
|
+
}, children);
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* One node, device or execution environment, with the Deployments it OWNS.
|
|
780
|
+
*
|
|
781
|
+
* `Deployment::location` subsets both `client` and `owner` — the target is where
|
|
782
|
+
* the deployment LIVES in the metamodel — so the element is a child of the cube
|
|
783
|
+
* and names it back, and `deployedArtifact` subsets `supplier` the way
|
|
784
|
+
* `utilizedElement` does one metaclass over. Writing all four is the same
|
|
785
|
+
* belt-and-braces {@link artifactElement} explains: the specific names are what
|
|
786
|
+
* a UML importer reads, the general ones are what a generic XMI reader reads,
|
|
787
|
+
* and they cannot disagree because they are minted from one id.
|
|
788
|
+
*
|
|
789
|
+
* §19.2.4 also allows the artefacts to be drawn INSIDE the cube instead of being
|
|
790
|
+
* joined to it by an arrow. This writer reads the arrow alone: nesting is
|
|
791
|
+
* geometry, and the two ways of saying it produce the same `deployment` element,
|
|
792
|
+
* so the day the canvas learns to read a nested artefact nothing in this
|
|
793
|
+
* function changes.
|
|
794
|
+
*/
|
|
795
|
+
function deploymentNodeElement(node, model, plan) {
|
|
796
|
+
const id = plan.idOf.get(node.id);
|
|
797
|
+
const children = [];
|
|
798
|
+
for (const relation of model.relations) {
|
|
799
|
+
if (relation.kind !== 'deploy' || relation.targetId !== node.id)
|
|
800
|
+
continue;
|
|
801
|
+
const artifact = plan.idOf.get(relation.sourceId);
|
|
802
|
+
if (!artifact)
|
|
803
|
+
continue;
|
|
804
|
+
children.push(el('deployment', {
|
|
805
|
+
'xmi:type': 'uml:Deployment',
|
|
806
|
+
'xmi:id': plan.ids.mint(),
|
|
807
|
+
...(relation.label ? { name: relation.label } : {}),
|
|
808
|
+
client: id,
|
|
809
|
+
supplier: artifact,
|
|
810
|
+
location: id,
|
|
811
|
+
deployedArtifact: artifact,
|
|
812
|
+
}));
|
|
813
|
+
}
|
|
814
|
+
const extension = keywordExtension(node.keywords, STRUCTURAL_KEYWORDS);
|
|
815
|
+
if (extension)
|
|
816
|
+
children.push(extension);
|
|
817
|
+
return el('packagedElement', {
|
|
818
|
+
'xmi:type': DEPLOYMENT_TYPE[node.kind],
|
|
819
|
+
'xmi:id': id,
|
|
820
|
+
name: node.name,
|
|
821
|
+
}, children);
|
|
822
|
+
}
|
|
823
|
+
/* ── Behaviour artefacts (§15.2, §14.2) ───────────────────────────────── */
|
|
824
|
+
/**
|
|
825
|
+
* The metaclass each activity glyph is written as — §15.7's own names.
|
|
826
|
+
*
|
|
827
|
+
* `uml:OpaqueAction` for the rounded rectangle, because that is the Action a
|
|
828
|
+
* drawing states: §16.2 makes OpaqueAction the one whose behaviour is given as
|
|
829
|
+
* a text nobody has to interpret, which is exactly what a box with a sentence in
|
|
830
|
+
* it says. Every stronger reading — a CallBehaviorAction, a CallOperationAction
|
|
831
|
+
* — would need a Behavior or an Operation to point at, and the drawing names
|
|
832
|
+
* none.
|
|
833
|
+
*
|
|
834
|
+
* `uml:ForkNode` for the bar, and the join is NOT told apart here: §15.3.4
|
|
835
|
+
* draws one bar for both and `roles.ts` gives it one role, so the file says what
|
|
836
|
+
* the picture says. An importer counting the edges gets the same answer a reader
|
|
837
|
+
* does.
|
|
838
|
+
*
|
|
839
|
+
* `Record<UmlActivityNodeKind, string>` and therefore compile-total: a glyph
|
|
840
|
+
* appended to the union with no metaclass to write it as fails the build here
|
|
841
|
+
* rather than vanishing from a file.
|
|
842
|
+
*/
|
|
843
|
+
const ACTIVITY_NODE_TYPE = {
|
|
844
|
+
action: 'uml:OpaqueAction',
|
|
845
|
+
initial: 'uml:InitialNode',
|
|
846
|
+
'activity-final': 'uml:ActivityFinalNode',
|
|
847
|
+
'flow-final': 'uml:FlowFinalNode',
|
|
848
|
+
decision: 'uml:DecisionNode',
|
|
849
|
+
fork: 'uml:ForkNode',
|
|
850
|
+
'object-node': 'uml:ObjectNode',
|
|
851
|
+
'send-signal': 'uml:SendSignalAction',
|
|
852
|
+
'accept-event': 'uml:AcceptEventAction',
|
|
853
|
+
// §16.10.4's hourglass IS an AcceptEventAction — the one whose trigger names a
|
|
854
|
+
// TimeEvent. The metamodel has no `AcceptTimeEventAction`, and inventing one
|
|
855
|
+
// would produce a file no importer can read.
|
|
856
|
+
'time-event': 'uml:AcceptEventAction',
|
|
857
|
+
};
|
|
858
|
+
/** The `kind` attribute §14.5.7's PseudostateKind enumeration spells. */
|
|
859
|
+
const PSEUDOSTATE_KIND = {
|
|
860
|
+
initial: 'initial',
|
|
861
|
+
choice: 'choice',
|
|
862
|
+
junction: 'junction',
|
|
863
|
+
'shallow-history': 'shallowHistory',
|
|
864
|
+
'deep-history': 'deepHistory',
|
|
865
|
+
'entry-point': 'entryPoint',
|
|
866
|
+
'exit-point': 'exitPoint',
|
|
867
|
+
terminate: 'terminate',
|
|
868
|
+
// §14.2.4 draws the fork and the join as one bar, like §15.3.4 does. One role,
|
|
869
|
+
// one PseudostateKind written, and the edges say which it is.
|
|
870
|
+
fork: 'fork',
|
|
871
|
+
};
|
|
872
|
+
/** An `OpaqueExpression` carrying a body — §8.3's text-valued specification. */
|
|
873
|
+
function opaqueExpression(tag, body, plan) {
|
|
874
|
+
return el(tag, {
|
|
875
|
+
'xmi:type': 'uml:OpaqueExpression',
|
|
876
|
+
'xmi:id': plan.ids.mint(),
|
|
877
|
+
body,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
/** An `OpaqueBehavior` carrying a body — a state's entry, do or exit. */
|
|
881
|
+
function opaqueBehavior(tag, body, plan) {
|
|
882
|
+
return el(tag, {
|
|
883
|
+
'xmi:type': 'uml:OpaqueBehavior',
|
|
884
|
+
'xmi:id': plan.ids.mint(),
|
|
885
|
+
name: body,
|
|
886
|
+
body,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
function signalIdFor(name, side, plan) {
|
|
890
|
+
const known = side.signals.get(name);
|
|
891
|
+
if (known)
|
|
892
|
+
return known;
|
|
893
|
+
const id = plan.ids.mint();
|
|
894
|
+
side.signals.set(name, id);
|
|
895
|
+
side.extras.push(el('packagedElement', {
|
|
896
|
+
'xmi:type': 'uml:Signal',
|
|
897
|
+
'xmi:id': id,
|
|
898
|
+
name,
|
|
899
|
+
}));
|
|
900
|
+
return id;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* One activity node as an `<node>` of its Activity.
|
|
904
|
+
*
|
|
905
|
+
* The three event-shaped glyphs are the only ones that cost more than a line,
|
|
906
|
+
* and each costs what the metamodel charges:
|
|
907
|
+
*
|
|
908
|
+
* - a **send signal** names the Signal it sends (§16.3.3), which is a
|
|
909
|
+
* reference, so the Signal is minted beside the Activity;
|
|
910
|
+
* - an **accept event** carries a Trigger whose Event is a SignalEvent
|
|
911
|
+
* (§16.10.3) — a reference again, so both the event and its signal are
|
|
912
|
+
* minted beside;
|
|
913
|
+
* - a **time event** carries a Trigger whose Event is a TimeEvent, and its
|
|
914
|
+
* `when` IS a containment (`{subsets ownedElement}`), so the TimeExpression
|
|
915
|
+
* is written inside it. `isRelative` comes from the glyph's own words through
|
|
916
|
+
* {@link parseTrigger}: `after` is relative, `at` is absolute, and §13.3.4
|
|
917
|
+
* says so in exactly those terms.
|
|
918
|
+
*
|
|
919
|
+
* A control node writes NO `name` attribute. §15.3.4 gives the disc, the
|
|
920
|
+
* bullseye, the crossed circle, the diamond and the bar no label at all, and the
|
|
921
|
+
* creation site draws none, so an empty `name=""` would be this writer inventing
|
|
922
|
+
* a nameless element where the notation has an unnamed one.
|
|
923
|
+
*/
|
|
924
|
+
function activityNodeElement(node, side, plan) {
|
|
925
|
+
const id = plan.idOf.get(node.id);
|
|
926
|
+
const name = node.name.trim();
|
|
927
|
+
const children = [];
|
|
928
|
+
const attrs = {
|
|
929
|
+
'xmi:type': ACTIVITY_NODE_TYPE[node.kind],
|
|
930
|
+
'xmi:id': id,
|
|
931
|
+
};
|
|
932
|
+
if (node.kind === 'send-signal') {
|
|
933
|
+
attrs.signal = signalIdFor(name || 'Signal', side, plan);
|
|
934
|
+
}
|
|
935
|
+
else if (node.kind === 'accept-event') {
|
|
936
|
+
const eventId = plan.ids.mint();
|
|
937
|
+
side.extras.push(el('packagedElement', {
|
|
938
|
+
'xmi:type': 'uml:SignalEvent',
|
|
939
|
+
'xmi:id': eventId,
|
|
940
|
+
...(name ? { name } : {}),
|
|
941
|
+
signal: signalIdFor(name || 'Signal', side, plan),
|
|
942
|
+
}));
|
|
943
|
+
children.push(el('trigger', {
|
|
944
|
+
'xmi:type': 'uml:Trigger',
|
|
945
|
+
'xmi:id': plan.ids.mint(),
|
|
946
|
+
...(name ? { name } : {}),
|
|
947
|
+
event: eventId,
|
|
948
|
+
}));
|
|
949
|
+
}
|
|
950
|
+
else if (node.kind === 'time-event') {
|
|
951
|
+
const trigger = parseTrigger(name);
|
|
952
|
+
const eventId = plan.ids.mint();
|
|
953
|
+
side.extras.push(el('packagedElement', {
|
|
954
|
+
'xmi:type': 'uml:TimeEvent',
|
|
955
|
+
'xmi:id': eventId,
|
|
956
|
+
...(name ? { name } : {}),
|
|
957
|
+
// §13.3.3.4: `after` is a relative TimeEvent, `at` an absolute one.
|
|
958
|
+
// Written only when TRUE, like every other boolean in this file.
|
|
959
|
+
...(trigger.kind === 'relative-time' ? { isRelative: 'true' } : {}),
|
|
960
|
+
}, [
|
|
961
|
+
el('when', { 'xmi:type': 'uml:TimeExpression', 'xmi:id': plan.ids.mint() }, [
|
|
962
|
+
el('expr', {
|
|
963
|
+
'xmi:type': 'uml:LiteralString',
|
|
964
|
+
'xmi:id': plan.ids.mint(),
|
|
965
|
+
// The TimeExpression, without the keyword that classified it —
|
|
966
|
+
// `after 5 s` is a relative event whose expression is `5 s`, and
|
|
967
|
+
// writing the keyword into the value would make an importer
|
|
968
|
+
// wait "after after 5 s".
|
|
969
|
+
value: trigger.expression ?? name,
|
|
970
|
+
}),
|
|
971
|
+
]),
|
|
972
|
+
]));
|
|
973
|
+
children.push(el('trigger', {
|
|
974
|
+
'xmi:type': 'uml:Trigger',
|
|
975
|
+
'xmi:id': plan.ids.mint(),
|
|
976
|
+
...(name ? { name } : {}),
|
|
977
|
+
event: eventId,
|
|
978
|
+
}));
|
|
979
|
+
}
|
|
980
|
+
// The control nodes stay unnamed — see the header.
|
|
981
|
+
const named = node.kind === 'initial' ||
|
|
982
|
+
node.kind === 'activity-final' ||
|
|
983
|
+
node.kind === 'flow-final' ||
|
|
984
|
+
node.kind === 'decision' ||
|
|
985
|
+
node.kind === 'fork'
|
|
986
|
+
? {}
|
|
987
|
+
: name
|
|
988
|
+
? { name }
|
|
989
|
+
: {};
|
|
990
|
+
return el('node', { ...attrs, ...named }, children);
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* One activity edge as an `<edge>` of its Activity.
|
|
994
|
+
*
|
|
995
|
+
* `guard` and `weight` are both `{subsets ownedElement}` on ActivityEdge, so
|
|
996
|
+
* both are written as CHILDREN — a guard as the OpaqueExpression §15.2.4's
|
|
997
|
+
* bracketed text is, a weight as a LiteralInteger when it is a plain number and
|
|
998
|
+
* a LiteralUnlimitedNatural when the author wrote `*`. §15.2.4 admits any
|
|
999
|
+
* ValueSpecification there; anything this writer cannot read as a number goes
|
|
1000
|
+
* through as an OpaqueExpression, which keeps the author's words and asks an
|
|
1001
|
+
* importer to interpret them.
|
|
1002
|
+
*
|
|
1003
|
+
* An end this sheet has no id for is an edge the file cannot carry, and it is
|
|
1004
|
+
* skipped rather than written with a dangling reference — the same call
|
|
1005
|
+
* {@link associationElement} makes. `umlModelFrom` has already warned the
|
|
1006
|
+
* author about it.
|
|
1007
|
+
*/
|
|
1008
|
+
function activityEdgeElement(edge, plan) {
|
|
1009
|
+
const source = plan.idOf.get(edge.sourceId);
|
|
1010
|
+
const target = plan.idOf.get(edge.targetId);
|
|
1011
|
+
if (!source || !target)
|
|
1012
|
+
return undefined;
|
|
1013
|
+
const children = [];
|
|
1014
|
+
if (edge.guard)
|
|
1015
|
+
children.push(opaqueExpression('guard', edge.guard, plan));
|
|
1016
|
+
if (edge.weight) {
|
|
1017
|
+
const weight = edge.weight.trim();
|
|
1018
|
+
children.push(weight === '*'
|
|
1019
|
+
? el('weight', {
|
|
1020
|
+
'xmi:type': 'uml:LiteralUnlimitedNatural',
|
|
1021
|
+
'xmi:id': plan.ids.mint(),
|
|
1022
|
+
value: '*',
|
|
1023
|
+
})
|
|
1024
|
+
: /^\d+$/.test(weight)
|
|
1025
|
+
? el('weight', {
|
|
1026
|
+
'xmi:type': 'uml:LiteralInteger',
|
|
1027
|
+
'xmi:id': plan.ids.mint(),
|
|
1028
|
+
value: weight,
|
|
1029
|
+
})
|
|
1030
|
+
: opaqueExpression('weight', weight, plan));
|
|
1031
|
+
}
|
|
1032
|
+
return el('edge', {
|
|
1033
|
+
'xmi:type': edge.kind === 'object-flow' ? 'uml:ObjectFlow' : 'uml:ControlFlow',
|
|
1034
|
+
'xmi:id': plan.ids.mint(),
|
|
1035
|
+
...(edge.name ? { name: edge.name } : {}),
|
|
1036
|
+
source,
|
|
1037
|
+
target,
|
|
1038
|
+
}, children);
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* One Activity as a `packagedElement`, and whatever it had to mint beside
|
|
1042
|
+
* itself.
|
|
1043
|
+
*
|
|
1044
|
+
* The partitions are `group`s — §15.6.2 makes ActivityPartition an
|
|
1045
|
+
* ActivityGroup, and `Activity::group` is where a group lives — each listing the
|
|
1046
|
+
* nodes it holds as a space-separated `node` idref list, which is how XMI writes
|
|
1047
|
+
* a multi-valued reference. The membership itself came from geometry
|
|
1048
|
+
* (`model.ts`), so a lane the author widened takes the actions it now covers and
|
|
1049
|
+
* the file follows the drawing.
|
|
1050
|
+
*
|
|
1051
|
+
* `isReadOnly`, `isSingleExecution` and the pre/post-conditions of §15.2.4 are
|
|
1052
|
+
* not written: the notation states none of them on this canvas, and a default
|
|
1053
|
+
* spelled out is a fact nobody stated.
|
|
1054
|
+
*/
|
|
1055
|
+
function activityElements(activity, plan) {
|
|
1056
|
+
const side = { extras: [], signals: new Map() };
|
|
1057
|
+
const children = [];
|
|
1058
|
+
for (const node of activity.nodes) {
|
|
1059
|
+
children.push(activityNodeElement(node, side, plan));
|
|
1060
|
+
}
|
|
1061
|
+
for (const edge of activity.edges) {
|
|
1062
|
+
const element = activityEdgeElement(edge, plan);
|
|
1063
|
+
if (element)
|
|
1064
|
+
children.push(element);
|
|
1065
|
+
}
|
|
1066
|
+
for (const partition of activity.partitions) {
|
|
1067
|
+
const nodes = partition.nodeIds
|
|
1068
|
+
.map(nodeId => plan.idOf.get(nodeId))
|
|
1069
|
+
.filter((nodeId) => Boolean(nodeId));
|
|
1070
|
+
children.push(el('group', {
|
|
1071
|
+
'xmi:type': 'uml:ActivityPartition',
|
|
1072
|
+
'xmi:id': plan.idOf.get(partition.id),
|
|
1073
|
+
name: partition.name,
|
|
1074
|
+
...(nodes.length > 0 ? { node: nodes.join(' ') } : {}),
|
|
1075
|
+
}));
|
|
1076
|
+
}
|
|
1077
|
+
return [
|
|
1078
|
+
...side.extras,
|
|
1079
|
+
el('packagedElement', {
|
|
1080
|
+
'xmi:type': 'uml:Activity',
|
|
1081
|
+
'xmi:id': plan.ids.mint(),
|
|
1082
|
+
name: activity.name,
|
|
1083
|
+
}, children),
|
|
1084
|
+
];
|
|
1085
|
+
}
|
|
1086
|
+
/** One state's vertices, as the `<subvertex>`es of the region that holds them. */
|
|
1087
|
+
function subvertexElements(machine, regionId, plan) {
|
|
1088
|
+
const children = [];
|
|
1089
|
+
for (const state of machine.states) {
|
|
1090
|
+
if (state.regionId !== regionId)
|
|
1091
|
+
continue;
|
|
1092
|
+
const inner = [];
|
|
1093
|
+
for (const body of state.entry) {
|
|
1094
|
+
inner.push(opaqueBehavior('entry', body, plan));
|
|
1095
|
+
}
|
|
1096
|
+
for (const body of state.doActivity) {
|
|
1097
|
+
inner.push(opaqueBehavior('doActivity', body, plan));
|
|
1098
|
+
}
|
|
1099
|
+
for (const body of state.exit) {
|
|
1100
|
+
inner.push(opaqueBehavior('exit', body, plan));
|
|
1101
|
+
}
|
|
1102
|
+
children.push(el('subvertex', {
|
|
1103
|
+
'xmi:type': 'uml:State',
|
|
1104
|
+
'xmi:id': plan.idOf.get(state.id),
|
|
1105
|
+
name: state.name,
|
|
1106
|
+
}, inner));
|
|
1107
|
+
}
|
|
1108
|
+
for (const final of machine.finalStates) {
|
|
1109
|
+
if (final.regionId !== regionId)
|
|
1110
|
+
continue;
|
|
1111
|
+
children.push(el('subvertex', {
|
|
1112
|
+
'xmi:type': 'uml:FinalState',
|
|
1113
|
+
'xmi:id': plan.idOf.get(final.id),
|
|
1114
|
+
// §14.2.4.5 draws the bullseye with no name, and the creation site
|
|
1115
|
+
// writes none. A named one is an author who named it.
|
|
1116
|
+
...(final.name.trim() ? { name: final.name } : {}),
|
|
1117
|
+
}));
|
|
1118
|
+
}
|
|
1119
|
+
for (const pseudo of machine.pseudostates) {
|
|
1120
|
+
if (pseudo.regionId !== regionId)
|
|
1121
|
+
continue;
|
|
1122
|
+
children.push(el('subvertex', {
|
|
1123
|
+
'xmi:type': 'uml:Pseudostate',
|
|
1124
|
+
'xmi:id': plan.idOf.get(pseudo.id),
|
|
1125
|
+
...(pseudo.name.trim() ? { name: pseudo.name } : {}),
|
|
1126
|
+
kind: PSEUDOSTATE_KIND[pseudo.kind],
|
|
1127
|
+
}));
|
|
1128
|
+
}
|
|
1129
|
+
// The COMPOSITE states drawn inside this region. Each is a `uml:State` with a
|
|
1130
|
+
// Region of its own — §14.2.4's decomposition compartment, and the shape
|
|
1131
|
+
// `roles.ts` records the canvas draws as a frame.
|
|
1132
|
+
for (const region of machine.regions) {
|
|
1133
|
+
if (region.parentId !== regionId)
|
|
1134
|
+
continue;
|
|
1135
|
+
children.push(el('subvertex', {
|
|
1136
|
+
'xmi:type': 'uml:State',
|
|
1137
|
+
'xmi:id': plan.idOf.get(region.id),
|
|
1138
|
+
name: region.name,
|
|
1139
|
+
}, [regionElement(machine, region, plan)]));
|
|
1140
|
+
}
|
|
1141
|
+
return children;
|
|
1142
|
+
}
|
|
1143
|
+
/** One `<region>` — its vertices, and the transitions that run between them. */
|
|
1144
|
+
function regionElement(machine, region, plan) {
|
|
1145
|
+
const regionId = region?.id;
|
|
1146
|
+
const children = subvertexElements(machine, regionId, plan);
|
|
1147
|
+
// A transition belongs to the region its SOURCE is in — §14.5.12 owns a
|
|
1148
|
+
// Transition on a Region, and the source is the vertex the arc leaves. A
|
|
1149
|
+
// transition crossing out of a composite state is therefore written inside it,
|
|
1150
|
+
// which is where §14.2.4 draws it from.
|
|
1151
|
+
const regionOfVertex = new Map();
|
|
1152
|
+
for (const state of machine.states) {
|
|
1153
|
+
regionOfVertex.set(state.id, state.regionId);
|
|
1154
|
+
}
|
|
1155
|
+
for (const final of machine.finalStates) {
|
|
1156
|
+
regionOfVertex.set(final.id, final.regionId);
|
|
1157
|
+
}
|
|
1158
|
+
for (const pseudo of machine.pseudostates) {
|
|
1159
|
+
regionOfVertex.set(pseudo.id, pseudo.regionId);
|
|
1160
|
+
}
|
|
1161
|
+
for (const nested of machine.regions) {
|
|
1162
|
+
regionOfVertex.set(nested.id, nested.parentId);
|
|
1163
|
+
}
|
|
1164
|
+
for (const transition of machine.transitions) {
|
|
1165
|
+
if (regionOfVertex.get(transition.sourceId) !== regionId)
|
|
1166
|
+
continue;
|
|
1167
|
+
const source = plan.idOf.get(transition.sourceId);
|
|
1168
|
+
const target = plan.idOf.get(transition.targetId);
|
|
1169
|
+
if (!source || !target)
|
|
1170
|
+
continue;
|
|
1171
|
+
const inner = [];
|
|
1172
|
+
for (const trigger of transition.triggers) {
|
|
1173
|
+
// A Trigger's `event` is a reference and this drawing names no Event
|
|
1174
|
+
// declaration — §14.2.4.8 says as much, "SignalEvent triggers and
|
|
1175
|
+
// CallEvent triggers are not distinguishable by syntax and must be
|
|
1176
|
+
// discriminated by their declaration elsewhere". So the Trigger carries
|
|
1177
|
+
// the author's word as its NAME and points at nothing, which an importer
|
|
1178
|
+
// can bind and this writer cannot honestly guess.
|
|
1179
|
+
inner.push(el('trigger', {
|
|
1180
|
+
'xmi:type': 'uml:Trigger',
|
|
1181
|
+
'xmi:id': plan.ids.mint(),
|
|
1182
|
+
name: trigger,
|
|
1183
|
+
}));
|
|
1184
|
+
}
|
|
1185
|
+
if (transition.guard) {
|
|
1186
|
+
// `Transition::guard` is a CONSTRAINT (`{subsets ownedElement}`), not a
|
|
1187
|
+
// ValueSpecification — which is where it differs from an ActivityEdge's,
|
|
1188
|
+
// one clause over. The expression goes in its `specification`.
|
|
1189
|
+
inner.push(el('guard', { 'xmi:type': 'uml:Constraint', 'xmi:id': plan.ids.mint() }, [opaqueExpression('specification', transition.guard, plan)]));
|
|
1190
|
+
}
|
|
1191
|
+
if (transition.effect) {
|
|
1192
|
+
inner.push(opaqueBehavior('effect', transition.effect, plan));
|
|
1193
|
+
}
|
|
1194
|
+
children.push(el('transition', {
|
|
1195
|
+
'xmi:type': 'uml:Transition',
|
|
1196
|
+
'xmi:id': plan.ids.mint(),
|
|
1197
|
+
source,
|
|
1198
|
+
target,
|
|
1199
|
+
}, inner));
|
|
1200
|
+
}
|
|
1201
|
+
return el('region', {
|
|
1202
|
+
'xmi:type': 'uml:Region',
|
|
1203
|
+
// A Region is minted rather than claimed: the canvas draws the COMPOSITE
|
|
1204
|
+
// STATE (which is claimed, above) and never the region inside it, and the
|
|
1205
|
+
// top region of a machine is one §14.2.4 makes implicit — the frame IS it.
|
|
1206
|
+
// Nothing refers to a Region by id, so nothing needs it planned.
|
|
1207
|
+
'xmi:id': plan.ids.mint(),
|
|
1208
|
+
name: region ? region.name : machine.name,
|
|
1209
|
+
}, children);
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* One StateMachine as a `packagedElement`.
|
|
1213
|
+
*
|
|
1214
|
+
* ONE top-level Region, always, and the sheet's vertices that sit inside no
|
|
1215
|
+
* drawn composite state are its subvertices. §14.2.4 makes the top region
|
|
1216
|
+
* implicit — the frame IS it — so there is nothing on the canvas to read it off,
|
|
1217
|
+
* and a machine with no region drawn would otherwise have nowhere to put a
|
|
1218
|
+
* single state.
|
|
1219
|
+
*
|
|
1220
|
+
* Orthogonal regions — two or more side by side inside one composite state,
|
|
1221
|
+
* separated by a dashed line — are a PHASE 3 refinement and this writer cannot
|
|
1222
|
+
* produce one: the canvas draws a composite state as a single `umlRegion` box
|
|
1223
|
+
* (`roles.ts` records the arbitration), so every composite state here has
|
|
1224
|
+
* exactly one region. What is written is always a legal StateMachine; it is
|
|
1225
|
+
* simply never an orthogonal one.
|
|
1226
|
+
*/
|
|
1227
|
+
function stateMachineElement(machine, plan) {
|
|
1228
|
+
return el('packagedElement', {
|
|
1229
|
+
'xmi:type': 'uml:StateMachine',
|
|
1230
|
+
'xmi:id': plan.ids.mint(),
|
|
1231
|
+
name: machine.name,
|
|
1232
|
+
}, [regionElement(machine, undefined, plan)]);
|
|
1233
|
+
}
|
|
1234
|
+
/* ── Interactions (§17) ───────────────────────────────────────────────── */
|
|
1235
|
+
/**
|
|
1236
|
+
* §17.4.3's `messageSort`, which is the one attribute that says WHAT KIND of
|
|
1237
|
+
* message an arrow is.
|
|
1238
|
+
*
|
|
1239
|
+
* A total map over {@link UmlMessageKind}, so a sixth arrow added to the union
|
|
1240
|
+
* fails the build here rather than exporting as a synchronous call.
|
|
1241
|
+
*/
|
|
1242
|
+
const MESSAGE_SORT = {
|
|
1243
|
+
'message-sync': 'synchCall',
|
|
1244
|
+
'message-async': 'asynchCall',
|
|
1245
|
+
'message-reply': 'reply',
|
|
1246
|
+
'message-create': 'createMessage',
|
|
1247
|
+
'message-delete': 'deleteMessage',
|
|
1248
|
+
};
|
|
1249
|
+
/**
|
|
1250
|
+
* One Interaction as a `packagedElement` — its lifelines, the ordered account of
|
|
1251
|
+
* what happens on it, and its messages.
|
|
1252
|
+
*
|
|
1253
|
+
* ## The shape, and why the fragments are a tree
|
|
1254
|
+
*
|
|
1255
|
+
* §17.4.4's arrow is TWO things in the metamodel: a `Message`, owned by the
|
|
1256
|
+
* Interaction, and a pair of `MessageOccurrenceSpecification`s, which are
|
|
1257
|
+
* InteractionFragments and therefore live in the ORDERED `fragment` list that IS
|
|
1258
|
+
* the sequence. So the `<fragment>`s are written in time order, each pointing
|
|
1259
|
+
* back at its message, and the `<message>`s follow — which is where §17.4 puts
|
|
1260
|
+
* them (`Interaction::message` is a containment of its own) and what lets an
|
|
1261
|
+
* occurrence refer to one that has not been written yet.
|
|
1262
|
+
*
|
|
1263
|
+
* A CombinedFragment is a fragment like any other, and its operands OWN the
|
|
1264
|
+
* fragments drawn in their bands (§17.6.4). That nesting is
|
|
1265
|
+
* {@link umlInteractionTimeline}'s, computed once for both writers, which is why
|
|
1266
|
+
* an `alt`'s two branches hold the same messages here as they do in the `.puml`.
|
|
1267
|
+
*
|
|
1268
|
+
* ## What is written for an ExecutionSpecification
|
|
1269
|
+
*
|
|
1270
|
+
* Three elements, because the metamodel needs three: an
|
|
1271
|
+
* `ExecutionOccurrenceSpecification` where the bar starts, a
|
|
1272
|
+
* `BehaviorExecutionSpecification` pointing at both ends, and a second
|
|
1273
|
+
* occurrence where it finishes. §17.2.4's `start` and `finish` are
|
|
1274
|
+
* OccurrenceSpecifications, not heights, and a bar written with neither would be
|
|
1275
|
+
* an activation attached to no moment in the conversation.
|
|
1276
|
+
*/
|
|
1277
|
+
function interactionElement(interaction, plan) {
|
|
1278
|
+
const children = [];
|
|
1279
|
+
for (const lifeline of interaction.lifelines) {
|
|
1280
|
+
// §17.3.4's `: <Type>` half, and the one fact about a lifeline the
|
|
1281
|
+
// metamodel has nowhere to put: `Lifeline::represents` wants a
|
|
1282
|
+
// ConnectableElement — a Property of the enclosing Classifier — and a
|
|
1283
|
+
// whiteboard has drawn no such Property. The author wrote a type NAME in a
|
|
1284
|
+
// head. So it travels where XMI 2.5.1 §7.9 puts what a tool knows and the
|
|
1285
|
+
// metamodel does not model, exactly as an Annex C keyword does, and comes
|
|
1286
|
+
// back as the same half of the same compartment.
|
|
1287
|
+
const extension = keywordExtension(lifeline.keywords) ??
|
|
1288
|
+
(lifeline.type ? el('xmi:Extension', { extender: EXTENDER }) : undefined);
|
|
1289
|
+
if (extension && lifeline.type) {
|
|
1290
|
+
extension.children.push(el('represents', { name: lifeline.type }));
|
|
1291
|
+
}
|
|
1292
|
+
children.push(el('lifeline', {
|
|
1293
|
+
'xmi:type': 'uml:Lifeline',
|
|
1294
|
+
'xmi:id': plan.idOf.get(lifeline.id),
|
|
1295
|
+
name: lifeline.name,
|
|
1296
|
+
}, extension ? [extension] : []));
|
|
1297
|
+
}
|
|
1298
|
+
/** The `xmi:id` of the LIFELINE an end of a message is drawn on. */
|
|
1299
|
+
const spineOf = new Map();
|
|
1300
|
+
for (const lifeline of interaction.lifelines) {
|
|
1301
|
+
const id = plan.idOf.get(lifeline.id);
|
|
1302
|
+
if (id)
|
|
1303
|
+
spineOf.set(lifeline.id, id);
|
|
1304
|
+
}
|
|
1305
|
+
for (const execution of interaction.executions) {
|
|
1306
|
+
const id = execution.lifelineId
|
|
1307
|
+
? plan.idOf.get(execution.lifelineId)
|
|
1308
|
+
: undefined;
|
|
1309
|
+
if (id)
|
|
1310
|
+
spineOf.set(execution.id, id);
|
|
1311
|
+
}
|
|
1312
|
+
for (const destruction of interaction.destructions) {
|
|
1313
|
+
const id = destruction.lifelineId
|
|
1314
|
+
? plan.idOf.get(destruction.lifelineId)
|
|
1315
|
+
: undefined;
|
|
1316
|
+
if (id)
|
|
1317
|
+
spineOf.set(destruction.id, id);
|
|
1318
|
+
}
|
|
1319
|
+
// Minted before the walk, because a BehaviorExecutionSpecification names the
|
|
1320
|
+
// occurrence it FINISHES at and that occurrence is written further down the
|
|
1321
|
+
// list. Two passes rather than a patched string — the reason `planOf` exists.
|
|
1322
|
+
const startOf = new Map();
|
|
1323
|
+
const finishOf = new Map();
|
|
1324
|
+
for (const execution of interaction.executions) {
|
|
1325
|
+
startOf.set(execution.id, plan.ids.mint());
|
|
1326
|
+
finishOf.set(execution.id, plan.ids.mint());
|
|
1327
|
+
}
|
|
1328
|
+
const sendOf = new Map();
|
|
1329
|
+
const receiveOf = new Map();
|
|
1330
|
+
for (const message of interaction.messages) {
|
|
1331
|
+
sendOf.set(message.id, plan.ids.mint());
|
|
1332
|
+
receiveOf.set(message.id, plan.ids.mint());
|
|
1333
|
+
}
|
|
1334
|
+
const fragmentsOf = (entries) => {
|
|
1335
|
+
const out = [];
|
|
1336
|
+
for (const entry of entries) {
|
|
1337
|
+
switch (entry.at) {
|
|
1338
|
+
case 'message': {
|
|
1339
|
+
const { message } = entry;
|
|
1340
|
+
const from = spineOf.get(message.sourceId);
|
|
1341
|
+
const to = spineOf.get(message.targetId);
|
|
1342
|
+
const owned = plan.idOf.get(message.id);
|
|
1343
|
+
// An occurrence covers exactly one lifeline (§17.2.4), so an end this
|
|
1344
|
+
// sheet has no lifeline for writes no occurrence — and the Message
|
|
1345
|
+
// below writes no `sendEvent` for it either.
|
|
1346
|
+
if (from) {
|
|
1347
|
+
out.push(el('fragment', {
|
|
1348
|
+
'xmi:type': 'uml:MessageOccurrenceSpecification',
|
|
1349
|
+
'xmi:id': sendOf.get(message.id),
|
|
1350
|
+
covered: from,
|
|
1351
|
+
message: owned,
|
|
1352
|
+
}));
|
|
1353
|
+
}
|
|
1354
|
+
if (to) {
|
|
1355
|
+
out.push(el('fragment', {
|
|
1356
|
+
'xmi:type': 'uml:MessageOccurrenceSpecification',
|
|
1357
|
+
'xmi:id': receiveOf.get(message.id),
|
|
1358
|
+
covered: to,
|
|
1359
|
+
message: owned,
|
|
1360
|
+
}));
|
|
1361
|
+
}
|
|
1362
|
+
break;
|
|
1363
|
+
}
|
|
1364
|
+
case 'execution-start': {
|
|
1365
|
+
const { execution } = entry;
|
|
1366
|
+
const covered = spineOf.get(execution.id);
|
|
1367
|
+
out.push(el('fragment', {
|
|
1368
|
+
'xmi:type': 'uml:ExecutionOccurrenceSpecification',
|
|
1369
|
+
'xmi:id': startOf.get(execution.id),
|
|
1370
|
+
...(covered ? { covered } : {}),
|
|
1371
|
+
}), el('fragment', {
|
|
1372
|
+
'xmi:type': 'uml:BehaviorExecutionSpecification',
|
|
1373
|
+
'xmi:id': plan.idOf.get(execution.id),
|
|
1374
|
+
...(covered ? { covered } : {}),
|
|
1375
|
+
start: startOf.get(execution.id),
|
|
1376
|
+
finish: finishOf.get(execution.id),
|
|
1377
|
+
}));
|
|
1378
|
+
break;
|
|
1379
|
+
}
|
|
1380
|
+
case 'execution-finish': {
|
|
1381
|
+
const covered = spineOf.get(entry.execution.id);
|
|
1382
|
+
out.push(el('fragment', {
|
|
1383
|
+
'xmi:type': 'uml:ExecutionOccurrenceSpecification',
|
|
1384
|
+
'xmi:id': finishOf.get(entry.execution.id),
|
|
1385
|
+
...(covered ? { covered } : {}),
|
|
1386
|
+
}));
|
|
1387
|
+
break;
|
|
1388
|
+
}
|
|
1389
|
+
case 'destruction': {
|
|
1390
|
+
const covered = spineOf.get(entry.destruction.id);
|
|
1391
|
+
out.push(el('fragment', {
|
|
1392
|
+
'xmi:type': 'uml:DestructionOccurrenceSpecification',
|
|
1393
|
+
'xmi:id': plan.idOf.get(entry.destruction.id),
|
|
1394
|
+
...(covered ? { covered } : {}),
|
|
1395
|
+
}));
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
case 'fragment': {
|
|
1399
|
+
const { fragment, operands } = entry;
|
|
1400
|
+
const covered = fragment.coveredLifelineIds
|
|
1401
|
+
.map(id => plan.idOf.get(id))
|
|
1402
|
+
.filter((id) => Boolean(id));
|
|
1403
|
+
if (fragment.operator === 'ref') {
|
|
1404
|
+
// §17.7.4's InteractionUse. `refersTo` when another sheet of this
|
|
1405
|
+
// document declares an Interaction of that name — which is the
|
|
1406
|
+
// whole point of a `ref` — and the NAME alone when nothing here
|
|
1407
|
+
// answers for it, because the author still said which interaction
|
|
1408
|
+
// they meant.
|
|
1409
|
+
const refersTo = plan.interactionByName.get(fragment.name.trim());
|
|
1410
|
+
out.push(el('fragment', {
|
|
1411
|
+
'xmi:type': 'uml:InteractionUse',
|
|
1412
|
+
'xmi:id': plan.idOf.get(fragment.id),
|
|
1413
|
+
...(fragment.name ? { name: fragment.name } : {}),
|
|
1414
|
+
...(covered.length > 0 ? { covered: covered.join(' ') } : {}),
|
|
1415
|
+
...(refersTo ? { refersTo } : {}),
|
|
1416
|
+
}));
|
|
1417
|
+
// An InteractionUse owns no fragments, so anything drawn over it is
|
|
1418
|
+
// written beside it rather than lost.
|
|
1419
|
+
for (const band of operands)
|
|
1420
|
+
out.push(...fragmentsOf(band.entries));
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
out.push(el('fragment', {
|
|
1424
|
+
'xmi:type': 'uml:CombinedFragment',
|
|
1425
|
+
'xmi:id': plan.idOf.get(fragment.id),
|
|
1426
|
+
...(covered.length > 0 ? { covered: covered.join(' ') } : {}),
|
|
1427
|
+
interactionOperator: fragment.operator,
|
|
1428
|
+
}, operands.map(band => {
|
|
1429
|
+
const inner = [];
|
|
1430
|
+
const guard = (band.operand.guard ?? '').trim();
|
|
1431
|
+
if (guard) {
|
|
1432
|
+
inner.push(el('guard', {
|
|
1433
|
+
'xmi:type': 'uml:InteractionConstraint',
|
|
1434
|
+
'xmi:id': plan.ids.mint(),
|
|
1435
|
+
}, [
|
|
1436
|
+
el('specification', {
|
|
1437
|
+
'xmi:type': 'uml:LiteralString',
|
|
1438
|
+
'xmi:id': plan.ids.mint(),
|
|
1439
|
+
value: guard,
|
|
1440
|
+
}),
|
|
1441
|
+
]));
|
|
1442
|
+
}
|
|
1443
|
+
inner.push(...fragmentsOf(band.entries));
|
|
1444
|
+
return el('operand', {
|
|
1445
|
+
'xmi:type': 'uml:InteractionOperand',
|
|
1446
|
+
'xmi:id': plan.ids.mint(),
|
|
1447
|
+
}, inner);
|
|
1448
|
+
})));
|
|
1449
|
+
break;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return out;
|
|
1454
|
+
};
|
|
1455
|
+
children.push(...fragmentsOf(umlInteractionTimeline(interaction)));
|
|
1456
|
+
for (const message of interaction.messages) {
|
|
1457
|
+
const from = spineOf.get(message.sourceId);
|
|
1458
|
+
const to = spineOf.get(message.targetId);
|
|
1459
|
+
children.push(el('message', {
|
|
1460
|
+
'xmi:type': 'uml:Message',
|
|
1461
|
+
'xmi:id': plan.idOf.get(message.id),
|
|
1462
|
+
...(message.label ? { name: message.label } : {}),
|
|
1463
|
+
messageSort: MESSAGE_SORT[message.kind],
|
|
1464
|
+
...(from ? { sendEvent: sendOf.get(message.id) } : {}),
|
|
1465
|
+
...(to ? { receiveEvent: receiveOf.get(message.id) } : {}),
|
|
1466
|
+
}));
|
|
1467
|
+
}
|
|
1468
|
+
return el('packagedElement', {
|
|
1469
|
+
'xmi:type': 'uml:Interaction',
|
|
1470
|
+
'xmi:id': plan.idOf.get(`${interaction.id}#interaction`),
|
|
1471
|
+
name: interaction.name,
|
|
1472
|
+
}, children);
|
|
1473
|
+
}
|
|
1474
|
+
/* ── Relationships that are packaged elements ─────────────────────────── */
|
|
1475
|
+
/**
|
|
1476
|
+
* An association, with both ends owned by the association itself.
|
|
1477
|
+
*
|
|
1478
|
+
* ## Why both ends are `ownedEnd`s
|
|
1479
|
+
*
|
|
1480
|
+
* UML lets an end be owned either by the Association or by the Classifier at the
|
|
1481
|
+
* other end (where it shows up as an attribute). A canvas connector states
|
|
1482
|
+
* neither: the author drew a line, not a navigable attribute. Owning both ends
|
|
1483
|
+
* on the association is the reading that adds nothing the author did not say,
|
|
1484
|
+
* and it is what every tool produces for a line drawn with no role names.
|
|
1485
|
+
*
|
|
1486
|
+
* ## Which end carries the diamond
|
|
1487
|
+
*
|
|
1488
|
+
* The aggregation flag goes on the end whose TYPE is the PART — the property the
|
|
1489
|
+
* whole conceptually owns — because that is what `Property::isComposite` means
|
|
1490
|
+
* in the metamodel ("the object containing the attribute is a container for the
|
|
1491
|
+
* object contained in the attribute"). The diamond is then drawn at the whole's
|
|
1492
|
+
* end of the line, which is the source: the role table makes the source the
|
|
1493
|
+
* whole, so the flag lands on the end typed by the target. Putting it on the
|
|
1494
|
+
* other end would draw the diamond on the wrong classifier in every tool that
|
|
1495
|
+
* opens the file.
|
|
1496
|
+
*
|
|
1497
|
+
* ## Which `ownedEnd` an end LABEL lands on
|
|
1498
|
+
*
|
|
1499
|
+
* The one drawn at the same end. §11.5.4 defines an Association end as "the
|
|
1500
|
+
* connection between the line depicting an Association and the icon … depicting
|
|
1501
|
+
* the connected Classifier", and everything written near it — the name string,
|
|
1502
|
+
* the multiplicity, the visibility glyph — adorns the memberEnd whose TYPE is
|
|
1503
|
+
* that classifier. So `relation.sourceEnd` becomes the `ownedEnd` carrying
|
|
1504
|
+
* `<type idref="{source}">`, and `targetEnd` the one typed by the target.
|
|
1505
|
+
*
|
|
1506
|
+
* The question is worth stating because the OTHER reading is the familiar one:
|
|
1507
|
+
* where a Classifier owns the end (§11.5.4's dot notation — "the dot shows that
|
|
1508
|
+
* the model includes a Property of the type represented by the Classifier
|
|
1509
|
+
* touched by the dot. This Property is owned by the Classifier at the other
|
|
1510
|
+
* end"), the end drawn at B is an attribute OF A, and a writer that put the
|
|
1511
|
+
* adornments on the owning classifier's side would swap every multiplicity on
|
|
1512
|
+
* the diagram. It does not arise here: this writer owns BOTH ends on the
|
|
1513
|
+
* association and types each one explicitly, so "the end at X" and "the end
|
|
1514
|
+
* typed by X" are the same end, and the mapping is direct. `xmi-import.ts` reads
|
|
1515
|
+
* it back through the same identity.
|
|
1516
|
+
*
|
|
1517
|
+
* The aggregation flag is not a counter-example: §11.5.4 has the diamond drawn
|
|
1518
|
+
* at the end OPPOSITE the flagged one ("a hollow diamond is added as a terminal
|
|
1519
|
+
* adornment at the end of the Association line opposite the end marked with
|
|
1520
|
+
* aggregation = AggregationKind::shared"), which is precisely why it is the one
|
|
1521
|
+
* adornment written across the line rather than beside its own end.
|
|
1522
|
+
*/
|
|
1523
|
+
function associationElement(relation, plan,
|
|
1524
|
+
// §19.4.3: "A CommunicationPath is an Association between two
|
|
1525
|
+
// DeploymentTargets". A specialization of Association, drawn as one and
|
|
1526
|
+
// structured as one, so it takes this writer whole and differs by its
|
|
1527
|
+
// metaclass alone — which is the entire content of this parameter.
|
|
1528
|
+
type = 'uml:Association') {
|
|
1529
|
+
const source = plan.idOf.get(relation.sourceId);
|
|
1530
|
+
const target = plan.idOf.get(relation.targetId);
|
|
1531
|
+
if (!source || !target)
|
|
1532
|
+
return undefined;
|
|
1533
|
+
const id = plan.ids.mint();
|
|
1534
|
+
const sourceEnd = plan.ids.mint();
|
|
1535
|
+
const targetEnd = plan.ids.mint();
|
|
1536
|
+
const aggregation = relation.kind === 'composition'
|
|
1537
|
+
? 'composite'
|
|
1538
|
+
: relation.kind === 'aggregation'
|
|
1539
|
+
? 'shared'
|
|
1540
|
+
: undefined;
|
|
1541
|
+
return el('packagedElement', {
|
|
1542
|
+
'xmi:type': type,
|
|
1543
|
+
'xmi:id': id,
|
|
1544
|
+
...(relation.label ? { name: relation.label } : {}),
|
|
1545
|
+
memberEnd: `${sourceEnd} ${targetEnd}`,
|
|
1546
|
+
}, [
|
|
1547
|
+
el('ownedEnd', {
|
|
1548
|
+
'xmi:type': 'uml:Property',
|
|
1549
|
+
'xmi:id': sourceEnd,
|
|
1550
|
+
association: id,
|
|
1551
|
+
...endAttrs(relation.sourceEnd),
|
|
1552
|
+
}, [
|
|
1553
|
+
el('type', { 'xmi:idref': source }),
|
|
1554
|
+
...multiplicityElements(relation.sourceEnd?.multiplicity, plan.ids),
|
|
1555
|
+
]),
|
|
1556
|
+
el('ownedEnd', {
|
|
1557
|
+
'xmi:type': 'uml:Property',
|
|
1558
|
+
'xmi:id': targetEnd,
|
|
1559
|
+
association: id,
|
|
1560
|
+
...endAttrs(relation.targetEnd),
|
|
1561
|
+
...(aggregation ? { aggregation } : {}),
|
|
1562
|
+
}, [
|
|
1563
|
+
el('type', { 'xmi:idref': target }),
|
|
1564
|
+
...multiplicityElements(relation.targetEnd?.multiplicity, plan.ids),
|
|
1565
|
+
]),
|
|
1566
|
+
]);
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* The `name` and `visibility` of one association end, when the label said them.
|
|
1570
|
+
*
|
|
1571
|
+
* Nothing when it did not, which is what keeps a connector with no end labels
|
|
1572
|
+
* byte-identical to what phase 1 wrote: the fields are absent from the IR, so
|
|
1573
|
+
* the attributes are absent from the file, so every golden of a plain
|
|
1574
|
+
* association still matches character for character.
|
|
1575
|
+
*/
|
|
1576
|
+
function endAttrs(end) {
|
|
1577
|
+
if (!end)
|
|
1578
|
+
return {};
|
|
1579
|
+
return {
|
|
1580
|
+
...(end.role ? { name: end.role } : {}),
|
|
1581
|
+
...visibilityAttrs(end.visibility),
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* A dependency — a `uml:Usage` when its label carries one of Annex C's usage
|
|
1586
|
+
* keywords, a plain `uml:Dependency` otherwise.
|
|
1587
|
+
*
|
|
1588
|
+
* `«use»`, `«call»` and `«create»` are not decoration on a dashed arrow: Table
|
|
1589
|
+
* C.1 gives all three the metamodel element `Usage`, so an arrow labelled `«use»`
|
|
1590
|
+
* IS a Usage and writing it as a bare Dependency would throw away the one thing
|
|
1591
|
+
* the author said about it.
|
|
1592
|
+
*/
|
|
1593
|
+
function dependencyElement(relation, plan) {
|
|
1594
|
+
const client = plan.idOf.get(relation.sourceId);
|
|
1595
|
+
const supplier = plan.idOf.get(relation.targetId);
|
|
1596
|
+
if (!client || !supplier)
|
|
1597
|
+
return undefined;
|
|
1598
|
+
const label = relation.label ?? '';
|
|
1599
|
+
const isUsage = [...USAGE_KEYWORDS].some(keyword => label.toLowerCase().includes(keyword));
|
|
1600
|
+
return el('packagedElement', {
|
|
1601
|
+
'xmi:type': isUsage ? 'uml:Usage' : 'uml:Dependency',
|
|
1602
|
+
'xmi:id': plan.ids.mint(),
|
|
1603
|
+
...(relation.label ? { name: relation.label } : {}),
|
|
1604
|
+
client,
|
|
1605
|
+
supplier,
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
/** A realization whose far end is not an Interface — see {@link classifierElement}. */
|
|
1609
|
+
function realizationElement(relation, plan) {
|
|
1610
|
+
const client = plan.idOf.get(relation.sourceId);
|
|
1611
|
+
const supplier = plan.idOf.get(relation.targetId);
|
|
1612
|
+
if (!client || !supplier)
|
|
1613
|
+
return undefined;
|
|
1614
|
+
return el('packagedElement', {
|
|
1615
|
+
'xmi:type': 'uml:Realization',
|
|
1616
|
+
'xmi:id': plan.ids.mint(),
|
|
1617
|
+
...(relation.label ? { name: relation.label } : {}),
|
|
1618
|
+
client,
|
|
1619
|
+
supplier,
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
/* ── One diagram ──────────────────────────────────────────────────────── */
|
|
1623
|
+
/**
|
|
1624
|
+
* One diagram as a `uml:Package`.
|
|
1625
|
+
*
|
|
1626
|
+
* A package per diagram even when there is only one, because a `uml:Model` with
|
|
1627
|
+
* loose classifiers in it says the four sheets an architect drew are one
|
|
1628
|
+
* namespace, which is exactly the merge ADR 0017 refuses on the canvas.
|
|
1629
|
+
*/
|
|
1630
|
+
function diagramPackage(model, plan) {
|
|
1631
|
+
const children = [];
|
|
1632
|
+
/** Where each classifier and sub-package is drawn — §12.2.4's containment. */
|
|
1633
|
+
const packageOf = new Map();
|
|
1634
|
+
for (const pkg of model.packages) {
|
|
1635
|
+
packageOf.set(pkg.id, containerOf(pkg, model.packages, true)?.id);
|
|
1636
|
+
}
|
|
1637
|
+
for (const classifier of model.classifiers) {
|
|
1638
|
+
packageOf.set(classifier.id, containerOf(classifier, model.packages)?.id);
|
|
1639
|
+
}
|
|
1640
|
+
/** Which subject a use case sits in — §18.1.4's. */
|
|
1641
|
+
const subjectOf = new Map();
|
|
1642
|
+
for (const useCase of model.useCases) {
|
|
1643
|
+
subjectOf.set(useCase.id, containerOf(useCase, model.subjects)?.id);
|
|
1644
|
+
}
|
|
1645
|
+
const packageElement = (pkg) => {
|
|
1646
|
+
const inner = [];
|
|
1647
|
+
for (const nested of model.packages) {
|
|
1648
|
+
if (packageOf.get(nested.id) === pkg.id)
|
|
1649
|
+
inner.push(packageElement(nested));
|
|
1650
|
+
}
|
|
1651
|
+
for (const classifier of model.classifiers) {
|
|
1652
|
+
if (packageOf.get(classifier.id) === pkg.id) {
|
|
1653
|
+
inner.push(classifierElement(classifier, model, plan));
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
const extension = keywordExtension(pkg.keywords);
|
|
1657
|
+
if (extension)
|
|
1658
|
+
inner.push(extension);
|
|
1659
|
+
return el('packagedElement', {
|
|
1660
|
+
'xmi:type': 'uml:Package',
|
|
1661
|
+
'xmi:id': plan.idOf.get(pkg.id),
|
|
1662
|
+
name: pkg.name,
|
|
1663
|
+
}, inner);
|
|
1664
|
+
};
|
|
1665
|
+
for (const pkg of model.packages) {
|
|
1666
|
+
if (!packageOf.get(pkg.id))
|
|
1667
|
+
children.push(packageElement(pkg));
|
|
1668
|
+
}
|
|
1669
|
+
for (const classifier of model.classifiers) {
|
|
1670
|
+
if (!packageOf.get(classifier.id)) {
|
|
1671
|
+
children.push(classifierElement(classifier, model, plan));
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
for (const actor of model.actors) {
|
|
1675
|
+
const extension = keywordExtension(actor.keywords);
|
|
1676
|
+
children.push(el('packagedElement', {
|
|
1677
|
+
'xmi:type': 'uml:Actor',
|
|
1678
|
+
'xmi:id': plan.idOf.get(actor.id),
|
|
1679
|
+
name: actor.name,
|
|
1680
|
+
}, extension ? [extension] : []));
|
|
1681
|
+
}
|
|
1682
|
+
for (const subject of model.subjects) {
|
|
1683
|
+
// The subject of a use case diagram is a Classifier, and the cases drawn
|
|
1684
|
+
// inside it are its `ownedUseCase`s (§18.1.4) — which is containment, not a
|
|
1685
|
+
// reference, so they are serialized here rather than beside it.
|
|
1686
|
+
const owned = model.useCases
|
|
1687
|
+
.filter(useCase => subjectOf.get(useCase.id) === subject.id)
|
|
1688
|
+
.map(useCase => {
|
|
1689
|
+
const element = useCaseElement(useCase, model, plan);
|
|
1690
|
+
return el('ownedUseCase', element.attrs, element.children);
|
|
1691
|
+
});
|
|
1692
|
+
children.push(el('packagedElement', {
|
|
1693
|
+
'xmi:type': 'uml:Component',
|
|
1694
|
+
'xmi:id': plan.idOf.get(subject.id),
|
|
1695
|
+
name: subject.name,
|
|
1696
|
+
}, owned));
|
|
1697
|
+
}
|
|
1698
|
+
for (const useCase of model.useCases) {
|
|
1699
|
+
if (!subjectOf.get(useCase.id)) {
|
|
1700
|
+
children.push(useCaseElement(useCase, model, plan));
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
// The structural artefacts, flat at the sheet's own package. Unlike a class,
|
|
1704
|
+
// none of them is attributed to a drawn PACKAGE: §12.2.4's containment is a
|
|
1705
|
+
// statement about namespaces made by drawing a box round a classifier, and a
|
|
1706
|
+
// component diagram drawn inside a package frame is a drawing this pack does
|
|
1707
|
+
// not read that way yet. What the cubes DO contain is written as deployments,
|
|
1708
|
+
// which is a reference rather than a nesting.
|
|
1709
|
+
for (const component of model.components) {
|
|
1710
|
+
children.push(componentElement(component, plan));
|
|
1711
|
+
}
|
|
1712
|
+
for (const artifact of model.artifacts) {
|
|
1713
|
+
children.push(artifactElement(artifact, model, plan));
|
|
1714
|
+
}
|
|
1715
|
+
for (const node of model.nodes) {
|
|
1716
|
+
children.push(deploymentNodeElement(node, model, plan));
|
|
1717
|
+
}
|
|
1718
|
+
// The two behaviours. Each is a `packagedElement` of the sheet's own package —
|
|
1719
|
+
// an Activity and a StateMachine are both Behaviors and both PackageableElements
|
|
1720
|
+
// — and the Activity brings with it the Signals and Events its action glyphs
|
|
1721
|
+
// had to reference (see {@link BehaviourSideElements}).
|
|
1722
|
+
for (const activity of model.activities) {
|
|
1723
|
+
children.push(...activityElements(activity, plan));
|
|
1724
|
+
}
|
|
1725
|
+
for (const machine of model.stateMachines) {
|
|
1726
|
+
children.push(stateMachineElement(machine, plan));
|
|
1727
|
+
}
|
|
1728
|
+
// §17.2 — the Interaction, a Behavior and a PackageableElement like the other
|
|
1729
|
+
// two, and written after them for the reason they were written after the
|
|
1730
|
+
// structural artefacts: appended, never interleaved.
|
|
1731
|
+
for (const interaction of model.interactions) {
|
|
1732
|
+
children.push(interactionElement(interaction, plan));
|
|
1733
|
+
}
|
|
1734
|
+
// Relationships that are packaged elements in their own right, owned by the
|
|
1735
|
+
// diagram's package: the nearest common namespace of their two ends, which
|
|
1736
|
+
// for one sheet is the sheet.
|
|
1737
|
+
for (const relation of model.relations) {
|
|
1738
|
+
let element;
|
|
1739
|
+
switch (relation.kind) {
|
|
1740
|
+
case 'association':
|
|
1741
|
+
case 'aggregation':
|
|
1742
|
+
case 'composition':
|
|
1743
|
+
element = associationElement(relation, plan);
|
|
1744
|
+
break;
|
|
1745
|
+
case 'dependency':
|
|
1746
|
+
element = dependencyElement(relation, plan);
|
|
1747
|
+
break;
|
|
1748
|
+
case 'realization':
|
|
1749
|
+
element =
|
|
1750
|
+
plan.kindOf.get(relation.targetId) === 'interface'
|
|
1751
|
+
? undefined
|
|
1752
|
+
: realizationElement(relation, plan);
|
|
1753
|
+
break;
|
|
1754
|
+
case 'communication-path':
|
|
1755
|
+
// §19.4.3 — an Association between two DeploymentTargets, and an
|
|
1756
|
+
// Association is a packaged element in its own right.
|
|
1757
|
+
element = associationElement(relation, plan, 'uml:CommunicationPath');
|
|
1758
|
+
break;
|
|
1759
|
+
// Written by the element that OWNS them, above.
|
|
1760
|
+
case 'generalization':
|
|
1761
|
+
case 'include':
|
|
1762
|
+
case 'extend':
|
|
1763
|
+
// …and the two structural dependencies, owned by the artefact that
|
|
1764
|
+
// manifests and by the cube that hosts (§19.3.2, §19.2.2).
|
|
1765
|
+
case 'manifest':
|
|
1766
|
+
case 'deploy':
|
|
1767
|
+
// …and the three behaviour edges, owned by the Activity and the
|
|
1768
|
+
// StateMachine that hold them: `Activity::edge` and `Region::transition`
|
|
1769
|
+
// are both containments, so a control flow written here as well would be
|
|
1770
|
+
// the same arrow in the file twice.
|
|
1771
|
+
case 'control-flow':
|
|
1772
|
+
case 'object-flow':
|
|
1773
|
+
case 'transition':
|
|
1774
|
+
// …and the five MESSAGES, owned by the Interaction that holds them
|
|
1775
|
+
// (`Interaction::message`) and written there in the ORDER §17.4.4 draws
|
|
1776
|
+
// them in, which is a fact a list of relations does not carry.
|
|
1777
|
+
case 'message-sync':
|
|
1778
|
+
case 'message-async':
|
|
1779
|
+
case 'message-reply':
|
|
1780
|
+
case 'message-create':
|
|
1781
|
+
case 'message-delete':
|
|
1782
|
+
// Not a model relationship at all: an anchor attaches a Comment, and it
|
|
1783
|
+
// is written as that Comment's `annotatedElement`.
|
|
1784
|
+
case 'anchor':
|
|
1785
|
+
element = undefined;
|
|
1786
|
+
break;
|
|
1787
|
+
default: {
|
|
1788
|
+
// Exhaustive: a relationship added to the union with nothing to write
|
|
1789
|
+
// it as fails the build here rather than vanishing from a file.
|
|
1790
|
+
const never = relation.kind;
|
|
1791
|
+
throw new Error(`unhandled UML relation: ${String(never)}`);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
if (element)
|
|
1795
|
+
children.push(element);
|
|
1796
|
+
}
|
|
1797
|
+
for (const note of model.notes) {
|
|
1798
|
+
const anchored = model.relations
|
|
1799
|
+
.filter(relation => relation.kind === 'anchor')
|
|
1800
|
+
.map(relation => relation.sourceId === note.id
|
|
1801
|
+
? relation.targetId
|
|
1802
|
+
: relation.targetId === note.id
|
|
1803
|
+
? relation.sourceId
|
|
1804
|
+
: undefined)
|
|
1805
|
+
.map(id => (id ? plan.idOf.get(id) : undefined))
|
|
1806
|
+
.filter((id) => Boolean(id));
|
|
1807
|
+
children.push(el('ownedComment', {
|
|
1808
|
+
'xmi:type': 'uml:Comment',
|
|
1809
|
+
'xmi:id': plan.idOf.get(note.id),
|
|
1810
|
+
...(anchored.length > 0
|
|
1811
|
+
? { annotatedElement: anchored.join(' ') }
|
|
1812
|
+
: {}),
|
|
1813
|
+
body: note.body,
|
|
1814
|
+
}));
|
|
1815
|
+
}
|
|
1816
|
+
// Which of Annex A's frame kinds this package was drawn as — the one fact the
|
|
1817
|
+
// UML metamodel has no slot for, kept where XMI puts tool data.
|
|
1818
|
+
children.push(el('xmi:Extension', { extender: EXTENDER }, [
|
|
1819
|
+
el('diagram', { kind: model.diagram.kind }),
|
|
1820
|
+
]));
|
|
1821
|
+
return el('packagedElement', {
|
|
1822
|
+
'xmi:type': 'uml:Package',
|
|
1823
|
+
'xmi:id': plan.idOf.get(model.diagram.id),
|
|
1824
|
+
name: model.diagram.name || model.diagram.heading,
|
|
1825
|
+
}, children);
|
|
1826
|
+
}
|
|
1827
|
+
/* ── The document ─────────────────────────────────────────────────────── */
|
|
1828
|
+
/**
|
|
1829
|
+
* Every diagram as one XMI document — one `uml:Model`, one package per diagram.
|
|
1830
|
+
*
|
|
1831
|
+
* A LIST rather than a single model, because that is what the format wants: XMI
|
|
1832
|
+
* carries a whole model, and four sheets of one architecture are four packages
|
|
1833
|
+
* of it, not four files. Exporting one diagram is `exportXmi([model])` and
|
|
1834
|
+
* produces the same shape, which is what keeps a one-diagram file and a
|
|
1835
|
+
* four-diagram file readable by the same importer.
|
|
1836
|
+
*/
|
|
1837
|
+
export function exportXmi(models, options = {}) {
|
|
1838
|
+
const ids = new Ids();
|
|
1839
|
+
// The root takes `_1` before anything else, so the model element of a file is
|
|
1840
|
+
// always the first id in it.
|
|
1841
|
+
const rootId = ids.mint();
|
|
1842
|
+
const plan = planOf(models, ids);
|
|
1843
|
+
const children = models.map(model => diagramPackage(model, plan));
|
|
1844
|
+
// The DataTypes nothing on the canvas answered for, declared once at the root
|
|
1845
|
+
// so every package that mentions one refers to the same element.
|
|
1846
|
+
for (const [name, id] of plan.synthesized) {
|
|
1847
|
+
children.push(el('packagedElement', {
|
|
1848
|
+
'xmi:type': 'uml:DataType',
|
|
1849
|
+
'xmi:id': id,
|
|
1850
|
+
name,
|
|
1851
|
+
}));
|
|
1852
|
+
}
|
|
1853
|
+
return serializeDocument(el('uml:Model', {
|
|
1854
|
+
'xmi:version': XMI_VERSION,
|
|
1855
|
+
'xmlns:xmi': XMI_NAMESPACE,
|
|
1856
|
+
'xmlns:uml': UML_NAMESPACE,
|
|
1857
|
+
'xmi:id': rootId,
|
|
1858
|
+
name: options.name || 'Model',
|
|
1859
|
+
}, children));
|
|
1860
|
+
}
|