@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
|
@@ -0,0 +1,2268 @@
|
|
|
1
|
+
import { UML_SD_EVENT_STEP, umlSequenceColumn, umlSequenceDestruction, umlSequenceExecution, umlSequenceFragment, umlSequenceSlot, } from './import.js';
|
|
2
|
+
import { formatEndLabel, } from './grammar.js';
|
|
3
|
+
import { readXml, xmlAttr, xmlChild, xmlChildren, xmlDescendants, xmlFragmentOf, } from './xml-reader.js';
|
|
4
|
+
/* ── The vocabulary ───────────────────────────────────────────────────── */
|
|
5
|
+
/**
|
|
6
|
+
* The prefixes a metaclass may be written under.
|
|
7
|
+
*
|
|
8
|
+
* `uml` is Annex E's own (`tag "org.omg.xmi.nsPrefix" set to "uml"`), and it is
|
|
9
|
+
* what Papyrus, StarUML, MagicDraw and our own writer all use. The other two
|
|
10
|
+
* are casings Enterprise Architect has shipped. A bare `type="Class"` with no
|
|
11
|
+
* prefix at all is read too, further down, but only when the value NAMES a
|
|
12
|
+
* metaclass this reader knows — because `type="_7"` on an `ownedAttribute` is
|
|
13
|
+
* an id reference and means something else entirely.
|
|
14
|
+
*/
|
|
15
|
+
const META_PREFIXES = new Set(['uml', 'UML', 'Uml']);
|
|
16
|
+
/** The classifier metaclasses, and the {@link UmlClassifier.kind} each is. */
|
|
17
|
+
const CLASSIFIER_KIND = {
|
|
18
|
+
Class: 'class',
|
|
19
|
+
Interface: 'interface',
|
|
20
|
+
Enumeration: 'enumeration',
|
|
21
|
+
InstanceSpecification: 'object',
|
|
22
|
+
// §10.2 makes a DataType a Classifier with attributes and operations, and the
|
|
23
|
+
// canvas draws it as the box §9.2.4 draws — which is a class with a keyword
|
|
24
|
+
// on it. The keyword is added rather than assumed, so the box says what the
|
|
25
|
+
// file said.
|
|
26
|
+
DataType: 'class',
|
|
27
|
+
PrimitiveType: 'class',
|
|
28
|
+
};
|
|
29
|
+
/** The metaclasses {@link CLASSIFIER_KIND} maps that need their keyword said. */
|
|
30
|
+
const CLASSIFIER_KEYWORD = {
|
|
31
|
+
DataType: 'datatype',
|
|
32
|
+
PrimitiveType: 'primitive',
|
|
33
|
+
};
|
|
34
|
+
/** The three cubes of §19.4.2, and the one `kind` they differ by. */
|
|
35
|
+
const DEPLOYMENT_KIND = {
|
|
36
|
+
Node: 'node',
|
|
37
|
+
Device: 'device',
|
|
38
|
+
ExecutionEnvironment: 'execution-environment',
|
|
39
|
+
};
|
|
40
|
+
/** The inverse of `xmi.ts`'s `ACTIVITY_NODE_TYPE`. */
|
|
41
|
+
const ACTIVITY_NODE_KIND = {
|
|
42
|
+
OpaqueAction: 'action',
|
|
43
|
+
Action: 'action',
|
|
44
|
+
CallBehaviorAction: 'action',
|
|
45
|
+
CallOperationAction: 'action',
|
|
46
|
+
InitialNode: 'initial',
|
|
47
|
+
ActivityFinalNode: 'activity-final',
|
|
48
|
+
FlowFinalNode: 'flow-final',
|
|
49
|
+
DecisionNode: 'decision',
|
|
50
|
+
MergeNode: 'decision',
|
|
51
|
+
ForkNode: 'fork',
|
|
52
|
+
JoinNode: 'fork',
|
|
53
|
+
ObjectNode: 'object-node',
|
|
54
|
+
CentralBufferNode: 'object-node',
|
|
55
|
+
DataStoreNode: 'object-node',
|
|
56
|
+
SendSignalAction: 'send-signal',
|
|
57
|
+
// Ambiguous by design — §16.10.4 draws the hourglass as an AcceptEventAction
|
|
58
|
+
// whose trigger names a TimeEvent, so the two are told apart by the EVENT and
|
|
59
|
+
// not by the metaclass. See {@link readActivity}.
|
|
60
|
+
AcceptEventAction: 'accept-event',
|
|
61
|
+
};
|
|
62
|
+
/** The inverse of `xmi.ts`'s `PSEUDOSTATE_KIND` (§14.5.7's enumeration). */
|
|
63
|
+
const PSEUDOSTATE_OF_KIND = {
|
|
64
|
+
initial: 'initial',
|
|
65
|
+
choice: 'choice',
|
|
66
|
+
junction: 'junction',
|
|
67
|
+
shallowHistory: 'shallow-history',
|
|
68
|
+
deepHistory: 'deep-history',
|
|
69
|
+
entryPoint: 'entry-point',
|
|
70
|
+
exitPoint: 'exit-point',
|
|
71
|
+
terminate: 'terminate',
|
|
72
|
+
fork: 'fork',
|
|
73
|
+
join: 'fork',
|
|
74
|
+
};
|
|
75
|
+
/** The dependency metaclasses, and the relation each states. */
|
|
76
|
+
const DEPENDENCY_KIND = {
|
|
77
|
+
Dependency: 'dependency',
|
|
78
|
+
Usage: 'dependency',
|
|
79
|
+
Abstraction: 'dependency',
|
|
80
|
+
Realization: 'realization',
|
|
81
|
+
Deployment: 'deploy',
|
|
82
|
+
Manifestation: 'manifest',
|
|
83
|
+
};
|
|
84
|
+
/** What Labre stamps on the XMI extensions it writes — `xmi.ts`'s `EXTENDER`. */
|
|
85
|
+
const LABRE_EXTENDER = 'labre';
|
|
86
|
+
/** The four VisibilityKinds, as §7.4 spells them in a file. */
|
|
87
|
+
const VISIBILITIES = new Set(['public', 'private', 'protected', 'package']);
|
|
88
|
+
/** The reasons a fragment is kept and not written back (ADR 0012 D5). */
|
|
89
|
+
export const UML_XMI_QUARANTINE_REASON = {
|
|
90
|
+
unmapped: 'This element has no Labre artefact, and the XMI writer builds its ' +
|
|
91
|
+
'document out of the diagram alone — it has no slot to splice a fragment ' +
|
|
92
|
+
'back into. It is kept in the document, verbatim, and not written back.',
|
|
93
|
+
danglingEnd: 'One of this relationship’s ends names an element the file does not ' +
|
|
94
|
+
'declare, so there is nothing on the canvas to draw it between. It is kept ' +
|
|
95
|
+
'in the document, verbatim, and not written back.',
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* The remarks this reader raises whose wording is FIXED, as `[key, English]`
|
|
99
|
+
* pairs — the same table, the same shape and the same reasons as
|
|
100
|
+
* `UML_PLANTUML_REMARKS` (`plantuml-import.ts`, where the contract is written
|
|
101
|
+
* out) and BPMN's own `BPMN_IMPORT_REMARKS`: one entry per SHAPE of sentence,
|
|
102
|
+
* `{{name}}` holes filled at the call site by `InterchangeNote.messageParams`,
|
|
103
|
+
* the English kept here as the fallback so a playground with no catalogue
|
|
104
|
+
* reads exactly what it read before.
|
|
105
|
+
*
|
|
106
|
+
* {@link UML_XMI_QUARANTINE_REASON} above is NOT part of it, and cannot be:
|
|
107
|
+
* its plain string is also written verbatim into
|
|
108
|
+
* `ForeignInterchange.quarantined[].reason` — data, never translated — so it
|
|
109
|
+
* cannot become a `[key, english]` pair without breaking that contract. BPMN
|
|
110
|
+
* carries the identical constraint on its own quarantine reasons.
|
|
111
|
+
*/
|
|
112
|
+
export const UML_XMI_REMARKS = {
|
|
113
|
+
mintedId: [
|
|
114
|
+
'com.labre.uml.import.xmi.minted-id',
|
|
115
|
+
'<{{tag}}> carries no xmi:id, so one was minted for it. Nothing in the file can refer to it, and nothing does.',
|
|
116
|
+
],
|
|
117
|
+
unlistedOperator: [
|
|
118
|
+
'com.labre.uml.import.xmi.unlisted-operator',
|
|
119
|
+
'This combined fragment\'s operator is "{{operator}}", which UML 2.5.1 §17.6.4 does not list. It is drawn as an "alt" and the file\'s own word is kept beside it.',
|
|
120
|
+
],
|
|
121
|
+
undrawnOccurrence: [
|
|
122
|
+
'com.labre.uml.import.xmi.undrawn-occurrence',
|
|
123
|
+
'A message in "{{name}}" names an occurrence this sheet has no lifeline for, so it is not drawn. The file still says it, and nothing was removed from the document.',
|
|
124
|
+
],
|
|
125
|
+
wrongRoot: [
|
|
126
|
+
'com.labre.uml.import.xmi.wrong-root',
|
|
127
|
+
'A UML interchange file opens on <uml:Model> or <uml:Package>; this one opens on <{{tag}}>. Nothing was imported.',
|
|
128
|
+
],
|
|
129
|
+
noXml: [
|
|
130
|
+
'com.labre.uml.import.xmi.no-xml',
|
|
131
|
+
'There is no XML element in this file, so there is no model in it.',
|
|
132
|
+
],
|
|
133
|
+
offSheetRelation: [
|
|
134
|
+
'com.labre.uml.import.xmi.off-sheet-relation',
|
|
135
|
+
'A {{kind}} in "{{name}}" runs to an element that is not on this sheet, so it is not drawn. The file still says it, and nothing was removed from the document.',
|
|
136
|
+
],
|
|
137
|
+
modelWithoutDiagram: [
|
|
138
|
+
'com.labre.uml.import.xmi.model-without-diagram',
|
|
139
|
+
'This file carries a model and no diagram, which is what the UML interchange format is for. Every position on the canvas was invented; none of them came from the file.',
|
|
140
|
+
],
|
|
141
|
+
nestedWithoutDrawing: [
|
|
142
|
+
'com.labre.uml.import.xmi.nested-without-drawing',
|
|
143
|
+
'{{count}} element(s) are declared inside another in this file — a class in a package, a use case in its subject. Labre states that by DRAWING one box inside the other, and this file carries no drawing, so they were laid out side by side. Move them into the box to say it again.',
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
/* ── Reading one node ─────────────────────────────────────────────────── */
|
|
147
|
+
/** The local half of a qualified name — `uml:Class` is `Class`. */
|
|
148
|
+
function localOf(qualified) {
|
|
149
|
+
const colon = qualified.indexOf(':');
|
|
150
|
+
return colon < 0 ? qualified : qualified.slice(colon + 1);
|
|
151
|
+
}
|
|
152
|
+
/** The prefix half — `''` when there is none. */
|
|
153
|
+
function prefixOf(qualified) {
|
|
154
|
+
const colon = qualified.indexOf(':');
|
|
155
|
+
return colon < 0 ? '' : qualified.slice(0, colon);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The METACLASS an element states, in three spellings and a fallback.
|
|
159
|
+
*
|
|
160
|
+
* 1. `xmi:type="uml:Class"` — what our writer, Papyrus and MagicDraw write.
|
|
161
|
+
* 2. `type="uml:Class"` — StarUML's, and the reason this function exists at
|
|
162
|
+
* all: a bare `type` is an id reference on half the elements in a file, so
|
|
163
|
+
* it counts as a metaclass ONLY when its value is prefixed with one of
|
|
164
|
+
* {@link META_PREFIXES}.
|
|
165
|
+
* 3. `<uml:Class …/>` — the element itself named for its metaclass, which is
|
|
166
|
+
* what a UML2/Ecore file does at the root and inside a `xmi:XMI` wrapper.
|
|
167
|
+
*/
|
|
168
|
+
function metaOf(node) {
|
|
169
|
+
for (const [key, value] of Object.entries(node.attrs)) {
|
|
170
|
+
if (localOf(key) !== 'type')
|
|
171
|
+
continue;
|
|
172
|
+
const prefix = prefixOf(key);
|
|
173
|
+
if (prefix === '') {
|
|
174
|
+
if (META_PREFIXES.has(prefixOf(value)))
|
|
175
|
+
return localOf(value);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
// `xmi:type`, `XMI:type`, `xsi:type` — a prefixed `type` is always the
|
|
179
|
+
// metaclass, whatever the file calls the XMI namespace.
|
|
180
|
+
if (value)
|
|
181
|
+
return localOf(value);
|
|
182
|
+
}
|
|
183
|
+
return META_PREFIXES.has(node.prefix) ? node.local : undefined;
|
|
184
|
+
}
|
|
185
|
+
/** `true` only for the string `true` — everything else is "not stated". */
|
|
186
|
+
function isTrue(value) {
|
|
187
|
+
return value === 'true';
|
|
188
|
+
}
|
|
189
|
+
/** A `visibility="…"` that names one of §7.4's four, or nothing. */
|
|
190
|
+
function visibilityAttr(node) {
|
|
191
|
+
const raw = xmlAttr(node, 'visibility');
|
|
192
|
+
return raw && VISIBILITIES.has(raw) ? raw : undefined;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The single id an element points at, whichever of the four spellings it uses.
|
|
196
|
+
*
|
|
197
|
+
* `xmi:idref` is XMI's own, a bare attribute value is what every reference our
|
|
198
|
+
* writer emits looks like (`general="_4"`), a nested `<type xmi:idref="…"/>` is
|
|
199
|
+
* the element form, and `href="…#Name"` points into another document — the last
|
|
200
|
+
* resolves to the FRAGMENT, which for `PrimitiveTypes.xmi#String` is the name of
|
|
201
|
+
* the type and is exactly what a type name is.
|
|
202
|
+
*/
|
|
203
|
+
function refOf(node) {
|
|
204
|
+
const idref = xmlAttr(node, 'idref');
|
|
205
|
+
if (idref)
|
|
206
|
+
return idref;
|
|
207
|
+
const href = xmlAttr(node, 'href');
|
|
208
|
+
if (href) {
|
|
209
|
+
const hash = href.lastIndexOf('#');
|
|
210
|
+
return hash < 0 ? href : href.slice(hash + 1);
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
function note(ctx, entry) {
|
|
215
|
+
ctx.notes.push(entry);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* The payload riding on one source id, created on first use.
|
|
219
|
+
*
|
|
220
|
+
* `ctx.foreign` has NO prototype (see {@link importXmi}), which is what makes
|
|
221
|
+
* this lookup safe: on a plain object, a file whose `xmi:id` is `__proto__`
|
|
222
|
+
* would have this return `Object.prototype` and the caller write its payload
|
|
223
|
+
* onto it — every object in the process, poisoned by opening a document.
|
|
224
|
+
*/
|
|
225
|
+
function foreignOf(ctx, scope) {
|
|
226
|
+
const known = ctx.foreign[scope];
|
|
227
|
+
if (known)
|
|
228
|
+
return known;
|
|
229
|
+
const payload = { id: scope };
|
|
230
|
+
ctx.foreign[scope] = payload;
|
|
231
|
+
return payload;
|
|
232
|
+
}
|
|
233
|
+
/** One attribute, kept under the scope of the element that carried it. */
|
|
234
|
+
function carryAttr(ctx, owner, scope, name, value) {
|
|
235
|
+
const payload = foreignOf(ctx, owner);
|
|
236
|
+
payload.attrs = {
|
|
237
|
+
...payload.attrs,
|
|
238
|
+
[scope]: { ...payload.attrs?.[scope], [name]: value },
|
|
239
|
+
};
|
|
240
|
+
ctx.counts.carried += 1;
|
|
241
|
+
}
|
|
242
|
+
/** One fragment, kept and declared un-re-emittable (D5). */
|
|
243
|
+
function quarantine(ctx, owner, fragment, reason, entry) {
|
|
244
|
+
const payload = foreignOf(ctx, owner);
|
|
245
|
+
payload.quarantined = [...(payload.quarantined ?? []), { fragment, reason }];
|
|
246
|
+
ctx.counts.quarantined += 1;
|
|
247
|
+
note(ctx, { kind: 'quarantined', ...entry, message: reason });
|
|
248
|
+
}
|
|
249
|
+
/** Read, and ride under the enclosing artefact's scope. */
|
|
250
|
+
function consume(ctx, ...nodes) {
|
|
251
|
+
for (const node of nodes)
|
|
252
|
+
if (node)
|
|
253
|
+
ctx.consumed.add(node);
|
|
254
|
+
}
|
|
255
|
+
/** Read whole — the element and everything under it. */
|
|
256
|
+
function consumeSubtree(ctx, node) {
|
|
257
|
+
ctx.consumed.add(node);
|
|
258
|
+
for (const child of node.children)
|
|
259
|
+
consumeSubtree(ctx, child);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Read, and become an artefact of its own — the scope everything under it is
|
|
263
|
+
* filed against.
|
|
264
|
+
*
|
|
265
|
+
* The id is the file's, verbatim (D3). An element the file left unidentified
|
|
266
|
+
* gets a minted one and a note: the surface element's identity is Labre's
|
|
267
|
+
* either way, but a relationship pointing at an id nobody declared is a
|
|
268
|
+
* dangling reference, and this is where that becomes visible.
|
|
269
|
+
*/
|
|
270
|
+
function map(ctx, node) {
|
|
271
|
+
ctx.consumed.add(node);
|
|
272
|
+
let id = xmlAttr(node, 'id');
|
|
273
|
+
if (!id) {
|
|
274
|
+
ctx.minted += 1;
|
|
275
|
+
id = `_g${ctx.minted}`;
|
|
276
|
+
note(ctx, {
|
|
277
|
+
kind: 'substituted-id',
|
|
278
|
+
element: node.name,
|
|
279
|
+
messageKey: UML_XMI_REMARKS.mintedId[0],
|
|
280
|
+
message: `<${node.name}> carries no xmi:id, so one was minted for it. Nothing in the file can refer to it, and nothing does.`,
|
|
281
|
+
messageParams: { tag: node.name },
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
ctx.scopeOf.set(node, id);
|
|
285
|
+
ctx.counts.mapped += 1;
|
|
286
|
+
return id;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* The attributes each element states in the DRAWING, and therefore the ones
|
|
290
|
+
* that are not carried.
|
|
291
|
+
*
|
|
292
|
+
* The default row is the honest one, and it is BPMN's: an id and a name are the
|
|
293
|
+
* model everywhere in this format, and anything else on an element Labre draws
|
|
294
|
+
* is something Labre does not model and must not lose.
|
|
295
|
+
*/
|
|
296
|
+
const READ_ATTRS = {
|
|
297
|
+
packagedElement: [
|
|
298
|
+
'xmi:type',
|
|
299
|
+
'type',
|
|
300
|
+
'xmi:id',
|
|
301
|
+
'id',
|
|
302
|
+
'name',
|
|
303
|
+
'isAbstract',
|
|
304
|
+
'classifier',
|
|
305
|
+
'client',
|
|
306
|
+
'supplier',
|
|
307
|
+
'memberEnd',
|
|
308
|
+
'general',
|
|
309
|
+
'contract',
|
|
310
|
+
'source',
|
|
311
|
+
'target',
|
|
312
|
+
'kind',
|
|
313
|
+
],
|
|
314
|
+
ownedAttribute: [
|
|
315
|
+
'xmi:type',
|
|
316
|
+
'type',
|
|
317
|
+
'xmi:id',
|
|
318
|
+
'id',
|
|
319
|
+
'name',
|
|
320
|
+
'visibility',
|
|
321
|
+
'isDerived',
|
|
322
|
+
'isStatic',
|
|
323
|
+
// `aggregation` is deliberately NOT here: a Property's aggregation is a
|
|
324
|
+
// fact this reader does not model on an attribute (it models it on an
|
|
325
|
+
// association END, where the diamond is drawn), so a file that states one —
|
|
326
|
+
// `aggregation="none"` included, which is a default spelled out — has it
|
|
327
|
+
// carried rather than swallowed.
|
|
328
|
+
'association',
|
|
329
|
+
],
|
|
330
|
+
ownedOperation: [
|
|
331
|
+
'xmi:type',
|
|
332
|
+
'type',
|
|
333
|
+
'xmi:id',
|
|
334
|
+
'id',
|
|
335
|
+
'name',
|
|
336
|
+
'visibility',
|
|
337
|
+
'isAbstract',
|
|
338
|
+
'isStatic',
|
|
339
|
+
'isQuery',
|
|
340
|
+
],
|
|
341
|
+
'': ['xmi:type', 'type', 'xmi:id', 'id', 'name'],
|
|
342
|
+
};
|
|
343
|
+
/** Everything on this element the reader did not read, kept verbatim. */
|
|
344
|
+
function carryUnknownAttrs(ctx, node, owner, scope = owner, understood = READ_ATTRS[node.local] ?? READ_ATTRS['']) {
|
|
345
|
+
for (const [name, value] of Object.entries(node.attrs)) {
|
|
346
|
+
if (name === 'xmlns' || name.startsWith('xmlns:'))
|
|
347
|
+
continue;
|
|
348
|
+
if (understood.includes(name))
|
|
349
|
+
continue;
|
|
350
|
+
// A prefixed spelling of an attribute the row names unprefixed, and the
|
|
351
|
+
// other way round: `xmi:id` and `id` are one attribute in three dialects.
|
|
352
|
+
if (understood.some(known => localOf(known) === localOf(name)))
|
|
353
|
+
continue;
|
|
354
|
+
carryAttr(ctx, owner, scope, name, value);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/* ── Labre's own extension ────────────────────────────────────────────── */
|
|
358
|
+
/** Is this the `xmi:Extension` our writer stamps? */
|
|
359
|
+
function isLabreExtension(node) {
|
|
360
|
+
return (node.local === 'Extension' && xmlAttr(node, 'extender') === LABRE_EXTENDER);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* The keywords an element states, out of the extension `xmi.ts` files them in.
|
|
364
|
+
*
|
|
365
|
+
* The metaclass keywords are NOT put back — `«interface»` on a `uml:Interface`
|
|
366
|
+
* is the fact the type already states, and the writer drops it for that reason
|
|
367
|
+
* (`METACLASS_KEYWORDS`). Only what the file actually wrote comes back, which
|
|
368
|
+
* is what makes the round-trip an equality rather than an approximation.
|
|
369
|
+
*/
|
|
370
|
+
function readKeywords(ctx, node) {
|
|
371
|
+
const keywords = [];
|
|
372
|
+
for (const child of node.children) {
|
|
373
|
+
if (!isLabreExtension(child))
|
|
374
|
+
continue;
|
|
375
|
+
let read = false;
|
|
376
|
+
for (const entry of xmlChildren(child, 'keyword')) {
|
|
377
|
+
const name = xmlAttr(entry, 'name');
|
|
378
|
+
if (name === undefined)
|
|
379
|
+
continue;
|
|
380
|
+
keywords.push(name);
|
|
381
|
+
consume(ctx, entry);
|
|
382
|
+
read = true;
|
|
383
|
+
}
|
|
384
|
+
// Consumed only when something in it was ours: a `xmi:Extension` stamped
|
|
385
|
+
// `labre` that holds something this build has never heard of is a fragment
|
|
386
|
+
// to keep, not one to swallow.
|
|
387
|
+
if (read && child.children.every(entry => ctx.consumed.has(entry))) {
|
|
388
|
+
consume(ctx, child);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return keywords;
|
|
392
|
+
}
|
|
393
|
+
/** Everything `UmlNodeBase` says, read off one element. */
|
|
394
|
+
function readBase(ctx, node, id) {
|
|
395
|
+
const bounds = ctx.layout[id];
|
|
396
|
+
return {
|
|
397
|
+
id,
|
|
398
|
+
name: xmlAttr(node, 'name') ?? '',
|
|
399
|
+
keywords: readKeywords(ctx, node),
|
|
400
|
+
isAbstract: isTrue(xmlAttr(node, 'isAbstract')),
|
|
401
|
+
...(bounds ? { bounds } : {}),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
/* ── Types, features, compartments ────────────────────────────────────── */
|
|
405
|
+
/** The NAME of the type a typed element points at, however it points. */
|
|
406
|
+
function typeNameOf(ctx, node) {
|
|
407
|
+
const child = xmlChild(node, 'type');
|
|
408
|
+
if (child) {
|
|
409
|
+
consume(ctx, child);
|
|
410
|
+
const ref = refOf(child);
|
|
411
|
+
if (ref)
|
|
412
|
+
return nameOfId(ctx, ref) ?? ref;
|
|
413
|
+
}
|
|
414
|
+
// `type="_7"` — an idref, which is the reading exactly when the value is NOT
|
|
415
|
+
// a metaclass name (see {@link metaOf}).
|
|
416
|
+
const attr = node.attrs['type'];
|
|
417
|
+
if (attr && !META_PREFIXES.has(prefixOf(attr))) {
|
|
418
|
+
return nameOfId(ctx, attr) ?? attr;
|
|
419
|
+
}
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
/** What the file calls the element with this id. */
|
|
423
|
+
function nameOfId(ctx, id) {
|
|
424
|
+
const node = ctx.byId.get(id);
|
|
425
|
+
if (!node)
|
|
426
|
+
return undefined;
|
|
427
|
+
const name = xmlAttr(node, 'name');
|
|
428
|
+
if (name)
|
|
429
|
+
return name;
|
|
430
|
+
const href = xmlAttr(node, 'href');
|
|
431
|
+
return href ? localOf(href.slice(href.lastIndexOf('#') + 1)) : undefined;
|
|
432
|
+
}
|
|
433
|
+
/** `lowerValue` / `upperValue`, when the author stated a range (§9.5.4). */
|
|
434
|
+
function readMultiplicity(ctx, node) {
|
|
435
|
+
const lower = xmlChild(node, 'lowerValue');
|
|
436
|
+
const upper = xmlChild(node, 'upperValue');
|
|
437
|
+
consume(ctx, lower, upper);
|
|
438
|
+
if (!lower && !upper)
|
|
439
|
+
return undefined;
|
|
440
|
+
const lowerRaw = lower ? (xmlAttr(lower, 'value') ?? '0') : '1';
|
|
441
|
+
const upperRaw = upper ? (xmlAttr(upper, 'value') ?? '1') : '1';
|
|
442
|
+
const lowerNumber = Number.parseInt(lowerRaw, 10);
|
|
443
|
+
const upperNumber = Number.parseInt(upperRaw, 10);
|
|
444
|
+
return {
|
|
445
|
+
lower: Number.isFinite(lowerNumber) ? lowerNumber : 0,
|
|
446
|
+
// `*` is the unlimited value of UnlimitedNatural (Annex E.3 requires it to
|
|
447
|
+
// be serialized as exactly that); `-1` is the sentinel Eclipse UML2 writes
|
|
448
|
+
// for it, and reading it as the number would be an upper bound below the
|
|
449
|
+
// lower one.
|
|
450
|
+
upper: upperRaw === '*' || upperRaw === '-1'
|
|
451
|
+
? '*'
|
|
452
|
+
: Number.isFinite(upperNumber)
|
|
453
|
+
? upperNumber
|
|
454
|
+
: 1,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/** A `<defaultValue value="…"/>`, whichever LiteralSpecification it is. */
|
|
458
|
+
function readDefault(ctx, node) {
|
|
459
|
+
const value = xmlChild(node, 'defaultValue');
|
|
460
|
+
if (!value)
|
|
461
|
+
return undefined;
|
|
462
|
+
consume(ctx, value);
|
|
463
|
+
return xmlAttr(value, 'value') ?? value.text?.trim();
|
|
464
|
+
}
|
|
465
|
+
/** One `ownedAttribute` as the compartment line it was read off. */
|
|
466
|
+
function readProperty(ctx, node) {
|
|
467
|
+
const multiplicity = readMultiplicity(ctx, node);
|
|
468
|
+
const defaultValue = readDefault(ctx, node);
|
|
469
|
+
const type = typeNameOf(ctx, node);
|
|
470
|
+
const visibility = visibilityAttr(node);
|
|
471
|
+
return {
|
|
472
|
+
...(visibility ? { visibility } : {}),
|
|
473
|
+
isDerived: isTrue(xmlAttr(node, 'isDerived')),
|
|
474
|
+
name: xmlAttr(node, 'name') ?? '',
|
|
475
|
+
...(type ? { type } : {}),
|
|
476
|
+
...(multiplicity ? { multiplicity } : {}),
|
|
477
|
+
...(defaultValue ? { defaultValue } : {}),
|
|
478
|
+
modifiers: [],
|
|
479
|
+
...(isTrue(xmlAttr(node, 'isStatic')) ? { isStatic: true } : {}),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* One `ownedParameter`, minus the one that carries the return type.
|
|
484
|
+
*
|
|
485
|
+
* The direction is kept whenever the file STATES one, `in` included — and the
|
|
486
|
+
* file states one on every parameter, because §9.4.4's default is applied by
|
|
487
|
+
* the writer that needs a value (`xmi.ts`, `direction: parameter.direction ??
|
|
488
|
+
* 'in'`). So "the author wrote no direction" is a fact the format does not
|
|
489
|
+
* carry, and this reader reports what the file says rather than guessing which
|
|
490
|
+
* `in` was typed and which was supplied.
|
|
491
|
+
*/
|
|
492
|
+
function readParameter(ctx, node) {
|
|
493
|
+
const multiplicity = readMultiplicity(ctx, node);
|
|
494
|
+
const defaultValue = readDefault(ctx, node);
|
|
495
|
+
const type = typeNameOf(ctx, node);
|
|
496
|
+
const direction = xmlAttr(node, 'direction');
|
|
497
|
+
return {
|
|
498
|
+
...(direction ? { direction: direction } : {}),
|
|
499
|
+
name: xmlAttr(node, 'name') ?? '',
|
|
500
|
+
...(type ? { type } : {}),
|
|
501
|
+
...(multiplicity ? { multiplicity } : {}),
|
|
502
|
+
...(defaultValue ? { defaultValue } : {}),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* One `ownedOperation`, with §9.4.4's return parameter folded back into it.
|
|
507
|
+
*
|
|
508
|
+
* `direction="return"` is where UML puts a result — there is no separate slot —
|
|
509
|
+
* so reading it back into {@link UmlOperation.returnType} is not a convenience,
|
|
510
|
+
* it is the inverse of what the writer had to do to say it at all.
|
|
511
|
+
*/
|
|
512
|
+
function readOperation(ctx, node) {
|
|
513
|
+
const parameters = [];
|
|
514
|
+
let returnType;
|
|
515
|
+
let returnMultiplicity;
|
|
516
|
+
for (const child of xmlChildren(node, 'ownedParameter')) {
|
|
517
|
+
consume(ctx, child);
|
|
518
|
+
if (xmlAttr(child, 'direction') === 'return') {
|
|
519
|
+
returnMultiplicity = readMultiplicity(ctx, child);
|
|
520
|
+
returnType = typeNameOf(ctx, child);
|
|
521
|
+
readDefault(ctx, child);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
parameters.push(readParameter(ctx, child));
|
|
525
|
+
}
|
|
526
|
+
const visibility = visibilityAttr(node);
|
|
527
|
+
return {
|
|
528
|
+
...(visibility ? { visibility } : {}),
|
|
529
|
+
name: xmlAttr(node, 'name') ?? '',
|
|
530
|
+
parameters,
|
|
531
|
+
...(returnType ? { returnType } : {}),
|
|
532
|
+
...(returnMultiplicity ? { returnMultiplicity } : {}),
|
|
533
|
+
modifiers: isTrue(xmlAttr(node, 'isQuery')) ? ['query'] : [],
|
|
534
|
+
...(isTrue(xmlAttr(node, 'isStatic')) ? { isStatic: true } : {}),
|
|
535
|
+
...(isTrue(xmlAttr(node, 'isAbstract')) ? { isAbstract: true } : {}),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/* ── Compartments, re-spelled ─────────────────────────────────────────── */
|
|
539
|
+
/** The four visibility markers of §7.4, by the VisibilityKind each is. */
|
|
540
|
+
const VISIBILITY_MARKER = {
|
|
541
|
+
public: '+',
|
|
542
|
+
private: '-',
|
|
543
|
+
protected: '#',
|
|
544
|
+
package: '~',
|
|
545
|
+
};
|
|
546
|
+
/** `[1..*]`, `[0..1]`, `[3]` — §7.5.4's own notation. */
|
|
547
|
+
function multiplicityText(multiplicity) {
|
|
548
|
+
const { lower, upper } = multiplicity;
|
|
549
|
+
return lower === upper ? `[${lower}]` : `[${lower}..${upper}]`;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* A parsed property back as the compartment line it would have been typed as.
|
|
553
|
+
*
|
|
554
|
+
* `UmlClassifier.lines` is the compartment AS THE AUTHOR TYPED IT, and an
|
|
555
|
+
* import has no author to ask: the file carries the structure, not the text.
|
|
556
|
+
* So the line is re-spelled in §9.5.4's own notation, which is the one form
|
|
557
|
+
* both writers already agree on — PlantUML prints these lines verbatim, and
|
|
558
|
+
* feeding it a line it could not have produced is the one way this reader could
|
|
559
|
+
* poison the other export.
|
|
560
|
+
*
|
|
561
|
+
* It is re-spelled and not invented: every part of it came out of the file.
|
|
562
|
+
*/
|
|
563
|
+
function propertyLine(property) {
|
|
564
|
+
const parts = [];
|
|
565
|
+
if (property.visibility)
|
|
566
|
+
parts.push(VISIBILITY_MARKER[property.visibility]);
|
|
567
|
+
if (property.isDerived)
|
|
568
|
+
parts.push('/');
|
|
569
|
+
parts.push(property.name);
|
|
570
|
+
if (property.type)
|
|
571
|
+
parts.push(`: ${property.type}`);
|
|
572
|
+
if (property.multiplicity)
|
|
573
|
+
parts.push(multiplicityText(property.multiplicity));
|
|
574
|
+
if (property.defaultValue)
|
|
575
|
+
parts.push(`= ${property.defaultValue}`);
|
|
576
|
+
if (property.isStatic)
|
|
577
|
+
parts.push('{static}');
|
|
578
|
+
return parts.join(' ').trim();
|
|
579
|
+
}
|
|
580
|
+
/** A parsed operation back as its compartment line — §9.6.4's notation. */
|
|
581
|
+
function operationLine(operation) {
|
|
582
|
+
const parameters = operation.parameters
|
|
583
|
+
.map(parameter => {
|
|
584
|
+
const head = parameter.direction
|
|
585
|
+
? `${parameter.direction} ${parameter.name}`
|
|
586
|
+
: parameter.name;
|
|
587
|
+
const typed = parameter.type ? `${head} : ${parameter.type}` : head;
|
|
588
|
+
const bounded = parameter.multiplicity
|
|
589
|
+
? `${typed} ${multiplicityText(parameter.multiplicity)}`
|
|
590
|
+
: typed;
|
|
591
|
+
return parameter.defaultValue
|
|
592
|
+
? `${bounded} = ${parameter.defaultValue}`
|
|
593
|
+
: bounded;
|
|
594
|
+
})
|
|
595
|
+
.join(', ');
|
|
596
|
+
const parts = [];
|
|
597
|
+
if (operation.visibility)
|
|
598
|
+
parts.push(VISIBILITY_MARKER[operation.visibility]);
|
|
599
|
+
parts.push(`${operation.name}(${parameters})`);
|
|
600
|
+
if (operation.returnType)
|
|
601
|
+
parts.push(`: ${operation.returnType}`);
|
|
602
|
+
if (operation.returnMultiplicity) {
|
|
603
|
+
parts.push(multiplicityText(operation.returnMultiplicity));
|
|
604
|
+
}
|
|
605
|
+
if (operation.isAbstract)
|
|
606
|
+
parts.push('{abstract}');
|
|
607
|
+
if (operation.isStatic)
|
|
608
|
+
parts.push('{static}');
|
|
609
|
+
for (const modifier of operation.modifiers)
|
|
610
|
+
parts.push(`{${modifier}}`);
|
|
611
|
+
return parts.join(' ').trim();
|
|
612
|
+
}
|
|
613
|
+
/* ── Diagram interchange (Annex E.5's `umldi`, and Papyrus's `notation`) ─ */
|
|
614
|
+
/** A number an attribute states, or nothing. */
|
|
615
|
+
function numberAttr(node, name) {
|
|
616
|
+
const raw = xmlAttr(node, name);
|
|
617
|
+
if (raw === undefined || raw.trim() === '')
|
|
618
|
+
return undefined;
|
|
619
|
+
const value = Number(raw);
|
|
620
|
+
return Number.isFinite(value) ? value : undefined;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Every shape the file draws, by the id of the element it draws.
|
|
624
|
+
*
|
|
625
|
+
* Two dialects, and the same four numbers in both:
|
|
626
|
+
*
|
|
627
|
+
* - **UMLDI** (Annex E.5, `umldi` prefix): a `UMLShape` with a `modelElement`
|
|
628
|
+
* and a `<bounds x= y= width= height=/>`;
|
|
629
|
+
* - **Papyrus `.notation`** (GMF): a `<children xmi:type="notation:Shape"
|
|
630
|
+
* element="…">` with a `<layoutConstraint xmi:type="notation:Bounds">`.
|
|
631
|
+
*
|
|
632
|
+
* Read by SHAPE rather than by walking the diagram tree, because the diagram
|
|
633
|
+
* tree is where the two dialects differ and the shape is where they agree.
|
|
634
|
+
* Edges are not read at all: a connector on this canvas routes itself between
|
|
635
|
+
* its two ends and re-routes whenever either moves, so a frozen waypoint list
|
|
636
|
+
* would be a path that stops following the drawing (D4's own reasoning about
|
|
637
|
+
* two-point edges, applied to all of them until phase 3 says otherwise).
|
|
638
|
+
*/
|
|
639
|
+
function readLayout(root) {
|
|
640
|
+
// No prototype, for the reason `importXmi` gives: the key is the file's.
|
|
641
|
+
const layout = Object.create(null);
|
|
642
|
+
for (const node of [root, ...xmlDescendants(root)]) {
|
|
643
|
+
const meta = xmlAttr(node, 'type') ?? '';
|
|
644
|
+
const isShape = node.local === 'UMLShape' ||
|
|
645
|
+
localOf(meta) === 'UMLShape' ||
|
|
646
|
+
localOf(meta) === 'Shape';
|
|
647
|
+
if (!isShape)
|
|
648
|
+
continue;
|
|
649
|
+
const target = xmlAttr(node, 'modelElement') ??
|
|
650
|
+
xmlAttr(node, 'element') ??
|
|
651
|
+
refOf(xmlChild(node, 'modelElement') ?? node);
|
|
652
|
+
if (!target)
|
|
653
|
+
continue;
|
|
654
|
+
const box = xmlChild(node, 'bounds') ?? xmlChild(node, 'layoutConstraint');
|
|
655
|
+
const holder = box ?? node;
|
|
656
|
+
const x = numberAttr(holder, 'x');
|
|
657
|
+
const y = numberAttr(holder, 'y');
|
|
658
|
+
const w = numberAttr(holder, 'width');
|
|
659
|
+
const h = numberAttr(holder, 'height');
|
|
660
|
+
if (x === undefined || y === undefined)
|
|
661
|
+
continue;
|
|
662
|
+
layout[target] = { x, y, w: w ?? 0, h: h ?? 0 };
|
|
663
|
+
}
|
|
664
|
+
return layout;
|
|
665
|
+
}
|
|
666
|
+
function emptyModel(id, name) {
|
|
667
|
+
return {
|
|
668
|
+
diagram: { id, kind: 'class', name, heading: '' },
|
|
669
|
+
classifiers: [],
|
|
670
|
+
packages: [],
|
|
671
|
+
actors: [],
|
|
672
|
+
useCases: [],
|
|
673
|
+
subjects: [],
|
|
674
|
+
notes: [],
|
|
675
|
+
components: [],
|
|
676
|
+
ports: [],
|
|
677
|
+
artifacts: [],
|
|
678
|
+
nodes: [],
|
|
679
|
+
activities: [],
|
|
680
|
+
stateMachines: [],
|
|
681
|
+
interactions: [],
|
|
682
|
+
relations: [],
|
|
683
|
+
warnings: [],
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
/** Record an artefact of this sheet, so a relation can resolve against it. */
|
|
687
|
+
function own(draft, id) {
|
|
688
|
+
draft.ids.add(id);
|
|
689
|
+
return id;
|
|
690
|
+
}
|
|
691
|
+
/** The label a relationship wears — its `name`, when the file wrote one. */
|
|
692
|
+
function labelOf(node) {
|
|
693
|
+
const name = xmlAttr(node, 'name');
|
|
694
|
+
return name ? { label: name } : {};
|
|
695
|
+
}
|
|
696
|
+
/** One relation, once both of its ends are ids this sheet owns. */
|
|
697
|
+
function relate(draft, kind, sourceId, targetId,
|
|
698
|
+
// The centre label AND, for an association, §11.5.4's two end labels — every
|
|
699
|
+
// part of a relation that is not its kind or its two ends.
|
|
700
|
+
label) {
|
|
701
|
+
if (!sourceId || !targetId)
|
|
702
|
+
return false;
|
|
703
|
+
const relation = { kind, sourceId, targetId, ...label };
|
|
704
|
+
draft.model.relations.push(relation);
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
/* ── The element readers ──────────────────────────────────────────────── */
|
|
708
|
+
/**
|
|
709
|
+
* One classifier, with the features and the two relationships it owns.
|
|
710
|
+
*
|
|
711
|
+
* An `ownedAttribute` that names an `association` is NOT a feature: it is an
|
|
712
|
+
* association END the classifier happens to own (§11.5.4 lets either side own
|
|
713
|
+
* one), and reading it as an attribute would put `order : Customer` in the
|
|
714
|
+
* compartment of a class whose author drew a line.
|
|
715
|
+
*/
|
|
716
|
+
function readClassifier(ctx, draft, node, meta, id) {
|
|
717
|
+
const kind = CLASSIFIER_KIND[meta];
|
|
718
|
+
const base = readBase(ctx, node, id);
|
|
719
|
+
const keyword = CLASSIFIER_KEYWORD[meta];
|
|
720
|
+
const attributes = [];
|
|
721
|
+
const operations = [];
|
|
722
|
+
const slots = [];
|
|
723
|
+
for (const child of node.children) {
|
|
724
|
+
if (child.local === 'ownedAttribute') {
|
|
725
|
+
const association = xmlAttr(child, 'association');
|
|
726
|
+
const endId = xmlAttr(child, 'id');
|
|
727
|
+
if (association && endId) {
|
|
728
|
+
consume(ctx, child);
|
|
729
|
+
draft.ends.set(endId, {
|
|
730
|
+
typeId: endTypeId(ctx, child),
|
|
731
|
+
...(xmlAttr(child, 'aggregation') &&
|
|
732
|
+
xmlAttr(child, 'aggregation') !== 'none'
|
|
733
|
+
? { aggregation: xmlAttr(child, 'aggregation') }
|
|
734
|
+
: {}),
|
|
735
|
+
});
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
consume(ctx, child);
|
|
739
|
+
carryUnknownAttrs(ctx, child, id, endId ?? id);
|
|
740
|
+
attributes.push(readProperty(ctx, child));
|
|
741
|
+
}
|
|
742
|
+
else if (child.local === 'ownedLiteral') {
|
|
743
|
+
// An enumeration's compartment holds LITERALS: a name and nothing else,
|
|
744
|
+
// which is what the writer reads out of the line and all it writes back.
|
|
745
|
+
consume(ctx, child);
|
|
746
|
+
attributes.push({
|
|
747
|
+
isDerived: false,
|
|
748
|
+
name: xmlAttr(child, 'name') ?? '',
|
|
749
|
+
modifiers: [],
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
else if (child.local === 'ownedOperation') {
|
|
753
|
+
consume(ctx, child);
|
|
754
|
+
carryUnknownAttrs(ctx, child, id, xmlAttr(child, 'id') ?? id);
|
|
755
|
+
operations.push(readOperation(ctx, child));
|
|
756
|
+
}
|
|
757
|
+
else if (child.local === 'slot') {
|
|
758
|
+
consume(ctx, child);
|
|
759
|
+
const value = xmlChild(child, 'value');
|
|
760
|
+
const extension = child.children.find(isLabreExtension);
|
|
761
|
+
const feature = extension ? xmlChild(extension, 'feature') : undefined;
|
|
762
|
+
consume(ctx, value, extension, feature);
|
|
763
|
+
const slotValue = value ? xmlAttr(value, 'value') : undefined;
|
|
764
|
+
slots.push({
|
|
765
|
+
name: feature ? (xmlAttr(feature, 'name') ?? '') : '',
|
|
766
|
+
...(slotValue ? { value: slotValue } : {}),
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
else if (child.local === 'generalization') {
|
|
770
|
+
consume(ctx, child);
|
|
771
|
+
relate(draft, 'generalization', id, xmlAttr(child, 'general'));
|
|
772
|
+
}
|
|
773
|
+
else if (child.local === 'interfaceRealization') {
|
|
774
|
+
consume(ctx, child);
|
|
775
|
+
relate(draft, 'realization', id, xmlAttr(child, 'contract') ?? xmlAttr(child, 'supplier'), labelOf(child));
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const classifier = {
|
|
779
|
+
...base,
|
|
780
|
+
keywords: keyword ? [keyword, ...base.keywords] : base.keywords,
|
|
781
|
+
kind,
|
|
782
|
+
attributes: kind === 'object' ? [] : attributes,
|
|
783
|
+
operations: kind === 'object' ? [] : operations,
|
|
784
|
+
slots: kind === 'object' ? slots : [],
|
|
785
|
+
lines: {
|
|
786
|
+
attributes: kind === 'object' ? [] : attributes.map(propertyLine),
|
|
787
|
+
operations: kind === 'object' ? [] : operations.map(operationLine),
|
|
788
|
+
},
|
|
789
|
+
};
|
|
790
|
+
const instanceOf = xmlAttr(node, 'classifier');
|
|
791
|
+
if (kind === 'object' && instanceOf) {
|
|
792
|
+
const named = nameOfId(ctx, instanceOf);
|
|
793
|
+
if (named)
|
|
794
|
+
classifier.instanceOf = named;
|
|
795
|
+
}
|
|
796
|
+
draft.model.classifiers.push(classifier);
|
|
797
|
+
own(draft, id);
|
|
798
|
+
}
|
|
799
|
+
/** What an association end is typed by — an id, not a name. */
|
|
800
|
+
function endTypeId(ctx, node) {
|
|
801
|
+
const child = xmlChild(node, 'type');
|
|
802
|
+
if (child) {
|
|
803
|
+
consume(ctx, child);
|
|
804
|
+
const ref = refOf(child);
|
|
805
|
+
if (ref)
|
|
806
|
+
return ref;
|
|
807
|
+
}
|
|
808
|
+
const attr = node.attrs['type'];
|
|
809
|
+
return attr && !META_PREFIXES.has(prefixOf(attr)) ? attr : undefined;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* One association, with the diamond on the end the metamodel puts it on.
|
|
813
|
+
*
|
|
814
|
+
* `Property::isComposite` is a statement about the end whose TYPE is the part —
|
|
815
|
+
* "the object containing the attribute is a container for the object contained
|
|
816
|
+
* in the attribute" — so the flagged end is the PART and the whole is the other
|
|
817
|
+
* one. That is the reading `xmi.ts` writes with, and reading it back the same
|
|
818
|
+
* way is what keeps a diamond on the same classifier after a round trip.
|
|
819
|
+
*
|
|
820
|
+
* ## Where an end's adornments land
|
|
821
|
+
*
|
|
822
|
+
* On the end DRAWN at the classifier that types it — the identity §11.5.4 states
|
|
823
|
+
* and `xmi.ts` writes with, read back the same way round. So the `lowerValue` /
|
|
824
|
+
* `upperValue`, the `name` and the `visibility` of the end typed by the WHOLE
|
|
825
|
+
* become {@link UmlRelation.sourceEnd}, and the part's end becomes `targetEnd`.
|
|
826
|
+
* They used to ride in `interchange.xmi.attrs` because the canvas had nowhere to
|
|
827
|
+
* draw them (ADR 0018); ADR 0020 gave the connector two end labels, so they are
|
|
828
|
+
* now on the board and are no longer carried — carrying a fact the drawing shows
|
|
829
|
+
* would put it in the document twice and re-emit it beside the one the writer
|
|
830
|
+
* produces. Everything else an end says is still carried verbatim.
|
|
831
|
+
*/
|
|
832
|
+
function readAssociation(ctx, draft, node, id, kind) {
|
|
833
|
+
// Both spellings of `memberEnd`: an idref list, and one element per end.
|
|
834
|
+
const listed = (xmlAttr(node, 'memberEnd') ?? '')
|
|
835
|
+
.split(/\s+/)
|
|
836
|
+
.filter(Boolean);
|
|
837
|
+
for (const child of xmlChildren(node, 'memberEnd')) {
|
|
838
|
+
consume(ctx, child);
|
|
839
|
+
const ref = refOf(child);
|
|
840
|
+
if (ref)
|
|
841
|
+
listed.push(ref);
|
|
842
|
+
}
|
|
843
|
+
for (const child of xmlChildren(node, 'ownedEnd')) {
|
|
844
|
+
consume(ctx, child);
|
|
845
|
+
const endId = xmlAttr(child, 'id');
|
|
846
|
+
if (!endId)
|
|
847
|
+
continue;
|
|
848
|
+
const aggregation = xmlAttr(child, 'aggregation');
|
|
849
|
+
draft.ends.set(endId, {
|
|
850
|
+
typeId: endTypeId(ctx, child),
|
|
851
|
+
...(aggregation && aggregation !== 'none' ? { aggregation } : {}),
|
|
852
|
+
...adornmentsOf(ctx, child),
|
|
853
|
+
});
|
|
854
|
+
if (!listed.includes(endId))
|
|
855
|
+
listed.push(endId);
|
|
856
|
+
if (aggregation && aggregation !== 'none') {
|
|
857
|
+
carryAttr(ctx, id, endId, 'aggregation', aggregation);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
// Ends the association only NAMES — owned by a classifier, which is where
|
|
861
|
+
// Papyrus and StarUML put a navigable one.
|
|
862
|
+
for (const endId of listed) {
|
|
863
|
+
if (draft.ends.has(endId))
|
|
864
|
+
continue;
|
|
865
|
+
const owned = ctx.byId.get(endId);
|
|
866
|
+
if (!owned)
|
|
867
|
+
continue;
|
|
868
|
+
const aggregation = xmlAttr(owned, 'aggregation');
|
|
869
|
+
draft.ends.set(endId, {
|
|
870
|
+
typeId: endTypeId(ctx, owned),
|
|
871
|
+
...(aggregation && aggregation !== 'none' ? { aggregation } : {}),
|
|
872
|
+
...adornmentsOf(ctx, owned),
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
const [first, second] = listed.map(endId => ({
|
|
876
|
+
id: endId,
|
|
877
|
+
...(draft.ends.get(endId) ?? {}),
|
|
878
|
+
}));
|
|
879
|
+
if (!first || !second || !first.typeId || !second.typeId) {
|
|
880
|
+
quarantine(ctx, draft.model.diagram.id, xmlFragmentOf(ctx.source, node), UML_XMI_QUARANTINE_REASON.danglingEnd, { sourceId: id, element: node.name });
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
// The flagged end is the PART; the whole is the other one. With no flag the
|
|
884
|
+
// file's own order is the direction, which is what the writer wrote.
|
|
885
|
+
const flagged = second.aggregation
|
|
886
|
+
? second
|
|
887
|
+
: first.aggregation
|
|
888
|
+
? first
|
|
889
|
+
: undefined;
|
|
890
|
+
const part = flagged ?? second;
|
|
891
|
+
const whole = flagged === first ? second : first;
|
|
892
|
+
const aggregation = flagged?.aggregation;
|
|
893
|
+
const relationKind = kind === 'communication-path'
|
|
894
|
+
? 'communication-path'
|
|
895
|
+
: aggregation === 'composite'
|
|
896
|
+
? 'composition'
|
|
897
|
+
: aggregation === 'shared'
|
|
898
|
+
? 'aggregation'
|
|
899
|
+
: 'association';
|
|
900
|
+
carryAttr(ctx, id, '@ends', 'source', whole.id);
|
|
901
|
+
carryAttr(ctx, id, '@ends', 'target', part.id);
|
|
902
|
+
relate(draft, relationKind, whole.typeId, part.typeId, {
|
|
903
|
+
...labelOf(node),
|
|
904
|
+
...(whole.label ? { sourceEnd: whole.label } : {}),
|
|
905
|
+
...(part.label ? { targetEnd: part.label } : {}),
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* The §11.5.4 adornments one association end states — or nothing.
|
|
910
|
+
*
|
|
911
|
+
* `raw` is composed rather than read off the file, because the file has no such
|
|
912
|
+
* string: XMI states the range, the name and the visibility as three separate
|
|
913
|
+
* pieces of markup, and what goes BESIDE the end on the board is their one-line
|
|
914
|
+
* spelling. {@link formatEndLabel} is the same printer the PlantUML writer uses,
|
|
915
|
+
* so an end imported from XMI and one exported to a `.puml` read identically.
|
|
916
|
+
*/
|
|
917
|
+
function adornmentsOf(ctx, node) {
|
|
918
|
+
const multiplicity = readMultiplicity(ctx, node);
|
|
919
|
+
const role = xmlAttr(node, 'name');
|
|
920
|
+
const visibility = visibilityAttr(node);
|
|
921
|
+
if (!multiplicity && !role && !visibility)
|
|
922
|
+
return {};
|
|
923
|
+
const parts = {
|
|
924
|
+
...(multiplicity ? { multiplicity } : {}),
|
|
925
|
+
...(role ? { role } : {}),
|
|
926
|
+
...(visibility ? { visibility } : {}),
|
|
927
|
+
};
|
|
928
|
+
return { label: { ...parts, raw: formatEndLabel({ ...parts, raw: '' }) } };
|
|
929
|
+
}
|
|
930
|
+
/** One component, with its ports and the two kinds of interface it wires. */
|
|
931
|
+
function readComponent(ctx, draft, node, id) {
|
|
932
|
+
const base = readBase(ctx, node, id);
|
|
933
|
+
const ports = [];
|
|
934
|
+
const provided = [];
|
|
935
|
+
const required = [];
|
|
936
|
+
/** The interfaces this component declares, so a wire can name one. */
|
|
937
|
+
const declared = new Map();
|
|
938
|
+
for (const child of node.children) {
|
|
939
|
+
const meta = metaOf(child) ?? '';
|
|
940
|
+
if (child.local === 'ownedAttribute' && meta === 'Port') {
|
|
941
|
+
consume(ctx, child);
|
|
942
|
+
const portId = map(ctx, child);
|
|
943
|
+
ports.push({ ...readBase(ctx, child, portId), ownerId: id });
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (child.local === 'packagedElement' && meta === 'Interface') {
|
|
947
|
+
consume(ctx, child);
|
|
948
|
+
const interfaceId = xmlAttr(child, 'id');
|
|
949
|
+
if (interfaceId)
|
|
950
|
+
declared.set(interfaceId, xmlAttr(child, 'name') ?? '');
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (child.local === 'interfaceRealization') {
|
|
954
|
+
consume(ctx, child);
|
|
955
|
+
const contract = xmlAttr(child, 'contract') ?? xmlAttr(child, 'supplier');
|
|
956
|
+
const name = contract
|
|
957
|
+
? (declared.get(contract) ?? nameOfId(ctx, contract))
|
|
958
|
+
: undefined;
|
|
959
|
+
if (name !== undefined)
|
|
960
|
+
provided.push(name);
|
|
961
|
+
else if (contract)
|
|
962
|
+
relate(draft, 'realization', id, contract);
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
if (child.local === 'packagedElement' && meta === 'Usage') {
|
|
966
|
+
consume(ctx, child);
|
|
967
|
+
const supplier = xmlAttr(child, 'supplier');
|
|
968
|
+
const name = supplier
|
|
969
|
+
? (declared.get(supplier) ?? nameOfId(ctx, supplier))
|
|
970
|
+
: undefined;
|
|
971
|
+
if (name !== undefined && supplier && declared.has(supplier)) {
|
|
972
|
+
required.push(name);
|
|
973
|
+
}
|
|
974
|
+
else if (supplier) {
|
|
975
|
+
relate(draft, 'dependency', id, supplier, labelOf(child));
|
|
976
|
+
}
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
// An interface the component declared and nothing wired to is still one the
|
|
981
|
+
// file said it provides — the lollipop with no line is §10.4.4's own drawing.
|
|
982
|
+
const wired = new Set([...provided, ...required]);
|
|
983
|
+
for (const name of declared.values()) {
|
|
984
|
+
if (!wired.has(name))
|
|
985
|
+
provided.push(name);
|
|
986
|
+
}
|
|
987
|
+
for (const port of ports)
|
|
988
|
+
own(draft, port.id);
|
|
989
|
+
draft.model.ports.push(...ports);
|
|
990
|
+
return { ...base, ports, provided, required };
|
|
991
|
+
}
|
|
992
|
+
/** One use case, with the two relationships §18.1.4 makes it own. */
|
|
993
|
+
function readUseCase(ctx, draft, node, id) {
|
|
994
|
+
for (const child of xmlChildren(node, 'include')) {
|
|
995
|
+
consume(ctx, child);
|
|
996
|
+
relate(draft, 'include', id, xmlAttr(child, 'addition'));
|
|
997
|
+
}
|
|
998
|
+
for (const child of xmlChildren(node, 'extend')) {
|
|
999
|
+
consume(ctx, child);
|
|
1000
|
+
relate(draft, 'extend', id, xmlAttr(child, 'extendedCase'));
|
|
1001
|
+
}
|
|
1002
|
+
return readBase(ctx, node, id);
|
|
1003
|
+
}
|
|
1004
|
+
/** One activity — its glyphs, its arrows and its lanes (§15.2.4, §15.6.4). */
|
|
1005
|
+
function readActivity(ctx, draft, node, id) {
|
|
1006
|
+
const nodes = [];
|
|
1007
|
+
const edges = [];
|
|
1008
|
+
const partitions = [];
|
|
1009
|
+
const byId = new Map();
|
|
1010
|
+
for (const child of xmlChildren(node, 'node')) {
|
|
1011
|
+
const meta = metaOf(child) ?? '';
|
|
1012
|
+
const kind = ACTIVITY_NODE_KIND[meta];
|
|
1013
|
+
if (!kind)
|
|
1014
|
+
continue;
|
|
1015
|
+
consume(ctx, child);
|
|
1016
|
+
const nodeId = map(ctx, child);
|
|
1017
|
+
carryUnknownAttrs(ctx, child, nodeId, nodeId, [
|
|
1018
|
+
'xmi:type',
|
|
1019
|
+
'type',
|
|
1020
|
+
'xmi:id',
|
|
1021
|
+
'id',
|
|
1022
|
+
'name',
|
|
1023
|
+
'signal',
|
|
1024
|
+
]);
|
|
1025
|
+
// §16.10.4's hourglass IS an AcceptEventAction — the one whose trigger
|
|
1026
|
+
// names a TimeEvent. The metaclass cannot tell the two apart, so the EVENT
|
|
1027
|
+
// is asked, exactly as the writer had to say it through one.
|
|
1028
|
+
let resolved = kind;
|
|
1029
|
+
for (const trigger of xmlChildren(child, 'trigger')) {
|
|
1030
|
+
consume(ctx, trigger);
|
|
1031
|
+
const event = xmlAttr(trigger, 'event');
|
|
1032
|
+
const declaration = event ? ctx.byId.get(event) : undefined;
|
|
1033
|
+
if (declaration && (metaOf(declaration) ?? '') === 'TimeEvent') {
|
|
1034
|
+
resolved = 'time-event';
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const activityNode = {
|
|
1038
|
+
...readBase(ctx, child, nodeId),
|
|
1039
|
+
kind: resolved,
|
|
1040
|
+
};
|
|
1041
|
+
nodes.push(activityNode);
|
|
1042
|
+
byId.set(nodeId, activityNode);
|
|
1043
|
+
own(draft, nodeId);
|
|
1044
|
+
const signal = xmlAttr(child, 'signal');
|
|
1045
|
+
if (signal)
|
|
1046
|
+
draft.usedSignals.add(signal);
|
|
1047
|
+
}
|
|
1048
|
+
for (const child of xmlChildren(node, 'edge')) {
|
|
1049
|
+
consume(ctx, child);
|
|
1050
|
+
const source = xmlAttr(child, 'source');
|
|
1051
|
+
const target = xmlAttr(child, 'target');
|
|
1052
|
+
if (!source || !target)
|
|
1053
|
+
continue;
|
|
1054
|
+
const guard = xmlChild(child, 'guard');
|
|
1055
|
+
const weight = xmlChild(child, 'weight');
|
|
1056
|
+
consume(ctx, guard, weight);
|
|
1057
|
+
const name = xmlAttr(child, 'name');
|
|
1058
|
+
const guardText = guard
|
|
1059
|
+
? (xmlAttr(guard, 'body') ?? guard.text?.trim())
|
|
1060
|
+
: undefined;
|
|
1061
|
+
const weightText = weight
|
|
1062
|
+
? (xmlAttr(weight, 'value') ?? xmlAttr(weight, 'body'))
|
|
1063
|
+
: undefined;
|
|
1064
|
+
edges.push({
|
|
1065
|
+
kind: (metaOf(child) ?? '') === 'ObjectFlow' ? 'object-flow' : 'control-flow',
|
|
1066
|
+
sourceId: source,
|
|
1067
|
+
targetId: target,
|
|
1068
|
+
...(name ? { name } : {}),
|
|
1069
|
+
...(guardText ? { guard: guardText } : {}),
|
|
1070
|
+
...(weightText ? { weight: weightText } : {}),
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
for (const child of xmlChildren(node, 'group')) {
|
|
1074
|
+
if ((metaOf(child) ?? '') !== 'ActivityPartition')
|
|
1075
|
+
continue;
|
|
1076
|
+
consume(ctx, child);
|
|
1077
|
+
const partitionId = map(ctx, child);
|
|
1078
|
+
const nodeIds = (xmlAttr(child, 'node') ?? '')
|
|
1079
|
+
.split(/\s+/)
|
|
1080
|
+
.filter(nodeId => byId.has(nodeId));
|
|
1081
|
+
for (const nodeId of nodeIds)
|
|
1082
|
+
byId.get(nodeId).partitionId = partitionId;
|
|
1083
|
+
partitions.push({
|
|
1084
|
+
...readBase(ctx, child, partitionId),
|
|
1085
|
+
// §15.6.4's band runs down the sheet unless the author turned it, and the
|
|
1086
|
+
// metamodel has no slot for which way it was drawn: `ActivityPartition`
|
|
1087
|
+
// says what it holds, never how it is oriented. A column is what the
|
|
1088
|
+
// creation site draws and what an invented layout can honour.
|
|
1089
|
+
orientation: 'vertical',
|
|
1090
|
+
nodeIds,
|
|
1091
|
+
});
|
|
1092
|
+
own(draft, partitionId);
|
|
1093
|
+
}
|
|
1094
|
+
return { id, name: xmlAttr(node, 'name') ?? '', nodes, edges, partitions };
|
|
1095
|
+
}
|
|
1096
|
+
/** One state machine — its regions, its vertices and its transitions. */
|
|
1097
|
+
function readStateMachine(ctx, draft, node, id) {
|
|
1098
|
+
const machine = {
|
|
1099
|
+
id,
|
|
1100
|
+
name: xmlAttr(node, 'name') ?? '',
|
|
1101
|
+
regions: [],
|
|
1102
|
+
states: [],
|
|
1103
|
+
finalStates: [],
|
|
1104
|
+
pseudostates: [],
|
|
1105
|
+
transitions: [],
|
|
1106
|
+
};
|
|
1107
|
+
/**
|
|
1108
|
+
* One `<region>`, and everything drawn in it.
|
|
1109
|
+
*
|
|
1110
|
+
* `regionId` is the COMPOSITE STATE the region belongs to, not the region
|
|
1111
|
+
* element: the canvas draws the composite state as a box and never draws the
|
|
1112
|
+
* region inside it, so that box's id is the one every vertex is filed under
|
|
1113
|
+
* — which is exactly what `xmi.ts`'s `subvertexElements` selects on.
|
|
1114
|
+
*/
|
|
1115
|
+
const readRegion = (region, regionId) => {
|
|
1116
|
+
consume(ctx, region);
|
|
1117
|
+
for (const child of xmlChildren(region, 'subvertex')) {
|
|
1118
|
+
const meta = metaOf(child) ?? '';
|
|
1119
|
+
consume(ctx, child);
|
|
1120
|
+
const vertexId = map(ctx, child);
|
|
1121
|
+
const base = readBase(ctx, child, vertexId);
|
|
1122
|
+
own(draft, vertexId);
|
|
1123
|
+
if (meta === 'FinalState') {
|
|
1124
|
+
machine.finalStates.push({
|
|
1125
|
+
...base,
|
|
1126
|
+
...(regionId ? { regionId } : {}),
|
|
1127
|
+
});
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
if (meta === 'Pseudostate') {
|
|
1131
|
+
const kind = PSEUDOSTATE_OF_KIND[xmlAttr(child, 'kind') ?? 'initial'];
|
|
1132
|
+
const pseudostate = {
|
|
1133
|
+
...base,
|
|
1134
|
+
kind: kind ?? 'junction',
|
|
1135
|
+
...(regionId ? { regionId } : {}),
|
|
1136
|
+
};
|
|
1137
|
+
machine.pseudostates.push(pseudostate);
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
const nested = xmlChild(child, 'region');
|
|
1141
|
+
if (nested) {
|
|
1142
|
+
// A `uml:State` with a Region of its own is §14.2.4's COMPOSITE state,
|
|
1143
|
+
// and on this canvas that is a `umlRegion` box rather than a state.
|
|
1144
|
+
machine.regions.push({
|
|
1145
|
+
...base,
|
|
1146
|
+
...(regionId ? { parentId: regionId } : {}),
|
|
1147
|
+
});
|
|
1148
|
+
readRegion(nested, vertexId);
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
const entry = [];
|
|
1152
|
+
const doActivity = [];
|
|
1153
|
+
const exit = [];
|
|
1154
|
+
for (const behaviour of child.children) {
|
|
1155
|
+
const body = xmlAttr(behaviour, 'body') ??
|
|
1156
|
+
xmlAttr(behaviour, 'name') ??
|
|
1157
|
+
behaviour.text?.trim();
|
|
1158
|
+
if (body === undefined)
|
|
1159
|
+
continue;
|
|
1160
|
+
if (behaviour.local === 'entry')
|
|
1161
|
+
entry.push(body);
|
|
1162
|
+
else if (behaviour.local === 'doActivity')
|
|
1163
|
+
doActivity.push(body);
|
|
1164
|
+
else if (behaviour.local === 'exit')
|
|
1165
|
+
exit.push(body);
|
|
1166
|
+
else
|
|
1167
|
+
continue;
|
|
1168
|
+
consume(ctx, behaviour);
|
|
1169
|
+
}
|
|
1170
|
+
machine.states.push({
|
|
1171
|
+
...base,
|
|
1172
|
+
entry,
|
|
1173
|
+
doActivity,
|
|
1174
|
+
exit,
|
|
1175
|
+
lines: [],
|
|
1176
|
+
...(regionId ? { regionId } : {}),
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
for (const child of xmlChildren(region, 'transition')) {
|
|
1180
|
+
consume(ctx, child);
|
|
1181
|
+
const source = xmlAttr(child, 'source');
|
|
1182
|
+
const target = xmlAttr(child, 'target');
|
|
1183
|
+
if (!source || !target)
|
|
1184
|
+
continue;
|
|
1185
|
+
const triggers = [];
|
|
1186
|
+
for (const trigger of xmlChildren(child, 'trigger')) {
|
|
1187
|
+
consume(ctx, trigger);
|
|
1188
|
+
const name = xmlAttr(trigger, 'name');
|
|
1189
|
+
if (name)
|
|
1190
|
+
triggers.push(name);
|
|
1191
|
+
}
|
|
1192
|
+
const guard = xmlChild(child, 'guard');
|
|
1193
|
+
const effect = xmlChild(child, 'effect');
|
|
1194
|
+
consume(ctx, guard, effect);
|
|
1195
|
+
const specification = guard
|
|
1196
|
+
? xmlChild(guard, 'specification')
|
|
1197
|
+
: undefined;
|
|
1198
|
+
consume(ctx, specification);
|
|
1199
|
+
const guardText = specification
|
|
1200
|
+
? (xmlAttr(specification, 'body') ?? specification.text?.trim())
|
|
1201
|
+
: guard
|
|
1202
|
+
? xmlAttr(guard, 'body')
|
|
1203
|
+
: undefined;
|
|
1204
|
+
const effectText = effect
|
|
1205
|
+
? (xmlAttr(effect, 'body') ?? xmlAttr(effect, 'name'))
|
|
1206
|
+
: undefined;
|
|
1207
|
+
machine.transitions.push({
|
|
1208
|
+
sourceId: source,
|
|
1209
|
+
targetId: target,
|
|
1210
|
+
triggers,
|
|
1211
|
+
...(guardText ? { guard: guardText } : {}),
|
|
1212
|
+
...(effectText ? { effect: effectText } : {}),
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
for (const region of xmlChildren(node, 'region'))
|
|
1217
|
+
readRegion(region, undefined);
|
|
1218
|
+
return machine;
|
|
1219
|
+
}
|
|
1220
|
+
/* ── Interactions (§17) ───────────────────────────────────────────────── */
|
|
1221
|
+
/** §17.4.3's `messageSort`, read back to the arrow §17.4.4 draws for it. */
|
|
1222
|
+
const MESSAGE_KIND_OF_SORT = {
|
|
1223
|
+
synchCall: 'message-sync',
|
|
1224
|
+
asynchCall: 'message-async',
|
|
1225
|
+
asynchSignal: 'message-async',
|
|
1226
|
+
reply: 'message-reply',
|
|
1227
|
+
createMessage: 'message-create',
|
|
1228
|
+
deleteMessage: 'message-delete',
|
|
1229
|
+
};
|
|
1230
|
+
/** The metaclasses §17.2.4 draws as the bar on a spine, whatever the tool. */
|
|
1231
|
+
const EXECUTION_TYPES = new Set([
|
|
1232
|
+
'BehaviorExecutionSpecification',
|
|
1233
|
+
'ActionExecutionSpecification',
|
|
1234
|
+
'ExecutionSpecification',
|
|
1235
|
+
]);
|
|
1236
|
+
/** §17.6.4's operators, as a set — what an `interactionOperator` is checked against. */
|
|
1237
|
+
const FRAGMENT_OPERATORS = new Set([
|
|
1238
|
+
'alt',
|
|
1239
|
+
'opt',
|
|
1240
|
+
'loop',
|
|
1241
|
+
'par',
|
|
1242
|
+
'break',
|
|
1243
|
+
'critical',
|
|
1244
|
+
'seq',
|
|
1245
|
+
'strict',
|
|
1246
|
+
'neg',
|
|
1247
|
+
'assert',
|
|
1248
|
+
'ignore',
|
|
1249
|
+
'consider',
|
|
1250
|
+
]);
|
|
1251
|
+
/**
|
|
1252
|
+
* One `uml:Interaction`, read — §17's whole vocabulary in one pass.
|
|
1253
|
+
*
|
|
1254
|
+
* ## Why the ORDER of the fragment list is the whole of it
|
|
1255
|
+
*
|
|
1256
|
+
* §17.2.4 makes an Interaction's `fragment` an ORDERED collection and says the
|
|
1257
|
+
* order is time. A file therefore states the sequence and no heights, and a
|
|
1258
|
+
* canvas states heights and no sequence, so this reader turns one into the
|
|
1259
|
+
* other: every event in the list takes the next slot,
|
|
1260
|
+
* {@link UML_SD_EVENT_STEP} below the last, and `model.ts` reads the order back
|
|
1261
|
+
* off those heights. A slot per EVENT — not per message — is what keeps an
|
|
1262
|
+
* `activate` written between two arrows between them on the way back.
|
|
1263
|
+
*
|
|
1264
|
+
* ## What each metaclass becomes
|
|
1265
|
+
*
|
|
1266
|
+
* A pair of `MessageOccurrenceSpecification`s is ONE arrow and takes ONE slot:
|
|
1267
|
+
* they are the two ends of the same horizontal line (§17.4.4). An
|
|
1268
|
+
* `ExecutionOccurrenceSpecification` is the top or the bottom of a bar, and the
|
|
1269
|
+
* `BehaviorExecutionSpecification` between them carries no slot of its own — it
|
|
1270
|
+
* is the bar, and the bar is the space between its two occurrences. A
|
|
1271
|
+
* `DestructionOccurrenceSpecification` is the cross. A `CombinedFragment` is the
|
|
1272
|
+
* rectangle: it takes a slot for its top, one for each `else` after the first
|
|
1273
|
+
* operand, and one for its bottom.
|
|
1274
|
+
*/
|
|
1275
|
+
function readInteraction(ctx, draft, node, id) {
|
|
1276
|
+
const interaction = {
|
|
1277
|
+
id,
|
|
1278
|
+
name: xmlAttr(node, 'name') ?? '',
|
|
1279
|
+
lifelines: [],
|
|
1280
|
+
messages: [],
|
|
1281
|
+
fragments: [],
|
|
1282
|
+
executions: [],
|
|
1283
|
+
destructions: [],
|
|
1284
|
+
};
|
|
1285
|
+
const pass = { occurrence: new Map(), slot: 0 };
|
|
1286
|
+
const take = () => umlSequenceSlot(pass.slot++);
|
|
1287
|
+
const columnOf = new Map();
|
|
1288
|
+
for (const child of xmlChildren(node, 'lifeline')) {
|
|
1289
|
+
consume(ctx, child);
|
|
1290
|
+
const lifelineId = map(ctx, child);
|
|
1291
|
+
carryUnknownAttrs(ctx, child, lifelineId, lifelineId, [
|
|
1292
|
+
'xmi:type',
|
|
1293
|
+
'type',
|
|
1294
|
+
'xmi:id',
|
|
1295
|
+
'id',
|
|
1296
|
+
'name',
|
|
1297
|
+
'represents',
|
|
1298
|
+
'coveredBy',
|
|
1299
|
+
]);
|
|
1300
|
+
const record = {
|
|
1301
|
+
...readBase(ctx, child, lifelineId),
|
|
1302
|
+
...lifelineTypeOf(ctx, child),
|
|
1303
|
+
};
|
|
1304
|
+
columnOf.set(lifelineId, interaction.lifelines.length);
|
|
1305
|
+
interaction.lifelines.push(record);
|
|
1306
|
+
own(draft, lifelineId);
|
|
1307
|
+
}
|
|
1308
|
+
/** The bars, with their two occurrences still unresolved. */
|
|
1309
|
+
const bars = [];
|
|
1310
|
+
/** Message id → the slot its FIRST occurrence took — see the docblock. */
|
|
1311
|
+
const seen = new Map();
|
|
1312
|
+
const readFragments = (parent) => {
|
|
1313
|
+
const touched = new Set();
|
|
1314
|
+
for (const child of xmlChildren(parent, 'fragment')) {
|
|
1315
|
+
const meta = metaOf(child) ?? '';
|
|
1316
|
+
consume(ctx, child);
|
|
1317
|
+
if (meta === 'MessageOccurrenceSpecification') {
|
|
1318
|
+
const fragmentId = map(ctx, child);
|
|
1319
|
+
carryUnknownAttrs(ctx, child, fragmentId, fragmentId, [
|
|
1320
|
+
'xmi:type',
|
|
1321
|
+
'type',
|
|
1322
|
+
'xmi:id',
|
|
1323
|
+
'id',
|
|
1324
|
+
'name',
|
|
1325
|
+
'covered',
|
|
1326
|
+
'message',
|
|
1327
|
+
]);
|
|
1328
|
+
const covered = xmlAttr(child, 'covered')?.split(/\s+/)[0];
|
|
1329
|
+
const message = xmlAttr(child, 'message');
|
|
1330
|
+
// The SECOND occurrence of a message is the other end of the same
|
|
1331
|
+
// horizontal line (§17.4.4), so it shares the slot rather than taking
|
|
1332
|
+
// one — otherwise every arrow would come back twice as far apart as the
|
|
1333
|
+
// file drew it.
|
|
1334
|
+
const y = message && seen.has(message) ? seen.get(message) : take();
|
|
1335
|
+
if (message)
|
|
1336
|
+
seen.set(message, y);
|
|
1337
|
+
pass.occurrence.set(fragmentId, { ...(covered ? { covered } : {}), y });
|
|
1338
|
+
if (covered)
|
|
1339
|
+
touched.add(covered);
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
if (meta === 'ExecutionOccurrenceSpecification') {
|
|
1343
|
+
const fragmentId = map(ctx, child);
|
|
1344
|
+
carryUnknownAttrs(ctx, child, fragmentId, fragmentId, [
|
|
1345
|
+
'xmi:type',
|
|
1346
|
+
'type',
|
|
1347
|
+
'xmi:id',
|
|
1348
|
+
'id',
|
|
1349
|
+
'name',
|
|
1350
|
+
'covered',
|
|
1351
|
+
'execution',
|
|
1352
|
+
]);
|
|
1353
|
+
const covered = xmlAttr(child, 'covered')?.split(/\s+/)[0];
|
|
1354
|
+
pass.occurrence.set(fragmentId, {
|
|
1355
|
+
...(covered ? { covered } : {}),
|
|
1356
|
+
y: take(),
|
|
1357
|
+
});
|
|
1358
|
+
if (covered)
|
|
1359
|
+
touched.add(covered);
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
if (EXECUTION_TYPES.has(meta)) {
|
|
1363
|
+
const fragmentId = map(ctx, child);
|
|
1364
|
+
carryUnknownAttrs(ctx, child, fragmentId, fragmentId, [
|
|
1365
|
+
'xmi:type',
|
|
1366
|
+
'type',
|
|
1367
|
+
'xmi:id',
|
|
1368
|
+
'id',
|
|
1369
|
+
'name',
|
|
1370
|
+
'covered',
|
|
1371
|
+
'start',
|
|
1372
|
+
'finish',
|
|
1373
|
+
]);
|
|
1374
|
+
const covered = xmlAttr(child, 'covered')?.split(/\s+/)[0];
|
|
1375
|
+
const record = {
|
|
1376
|
+
...readBase(ctx, child, fragmentId),
|
|
1377
|
+
...(covered ? { lifelineId: covered } : {}),
|
|
1378
|
+
y0: 0,
|
|
1379
|
+
y1: 0,
|
|
1380
|
+
};
|
|
1381
|
+
interaction.executions.push(record);
|
|
1382
|
+
own(draft, fragmentId);
|
|
1383
|
+
bars.push({
|
|
1384
|
+
record,
|
|
1385
|
+
...(xmlAttr(child, 'start')
|
|
1386
|
+
? { start: xmlAttr(child, 'start') }
|
|
1387
|
+
: {}),
|
|
1388
|
+
...(xmlAttr(child, 'finish')
|
|
1389
|
+
? { finish: xmlAttr(child, 'finish') }
|
|
1390
|
+
: {}),
|
|
1391
|
+
});
|
|
1392
|
+
if (covered)
|
|
1393
|
+
touched.add(covered);
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
if (meta === 'DestructionOccurrenceSpecification') {
|
|
1397
|
+
const fragmentId = map(ctx, child);
|
|
1398
|
+
carryUnknownAttrs(ctx, child, fragmentId, fragmentId, [
|
|
1399
|
+
'xmi:type',
|
|
1400
|
+
'type',
|
|
1401
|
+
'xmi:id',
|
|
1402
|
+
'id',
|
|
1403
|
+
'name',
|
|
1404
|
+
'covered',
|
|
1405
|
+
]);
|
|
1406
|
+
const covered = xmlAttr(child, 'covered')?.split(/\s+/)[0];
|
|
1407
|
+
const record = {
|
|
1408
|
+
...readBase(ctx, child, fragmentId),
|
|
1409
|
+
...(covered ? { lifelineId: covered } : {}),
|
|
1410
|
+
y: take(),
|
|
1411
|
+
};
|
|
1412
|
+
interaction.destructions.push(record);
|
|
1413
|
+
pass.occurrence.set(fragmentId, {
|
|
1414
|
+
...(covered ? { covered } : {}),
|
|
1415
|
+
y: record.y,
|
|
1416
|
+
});
|
|
1417
|
+
own(draft, fragmentId);
|
|
1418
|
+
if (covered)
|
|
1419
|
+
touched.add(covered);
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
if (meta === 'InteractionUse') {
|
|
1423
|
+
const fragmentId = map(ctx, child);
|
|
1424
|
+
carryUnknownAttrs(ctx, child, fragmentId, fragmentId, [
|
|
1425
|
+
'xmi:type',
|
|
1426
|
+
'type',
|
|
1427
|
+
'xmi:id',
|
|
1428
|
+
'id',
|
|
1429
|
+
'name',
|
|
1430
|
+
'covered',
|
|
1431
|
+
'refersTo',
|
|
1432
|
+
]);
|
|
1433
|
+
const covered = (xmlAttr(child, 'covered') ?? '')
|
|
1434
|
+
.split(/\s+/)
|
|
1435
|
+
.filter(Boolean);
|
|
1436
|
+
const y0 = take();
|
|
1437
|
+
const y1 = take();
|
|
1438
|
+
// §17.7.4's `refersTo` points at an Interaction that may be on another
|
|
1439
|
+
// sheet of this document; the NAME is what a board can draw, so the
|
|
1440
|
+
// reference is resolved to one when the file declares it and the
|
|
1441
|
+
// element's own name is kept otherwise.
|
|
1442
|
+
const refersTo = xmlAttr(child, 'refersTo');
|
|
1443
|
+
const named = xmlAttr(child, 'name') ??
|
|
1444
|
+
(refersTo ? nameOfId(ctx, refersTo) : undefined) ??
|
|
1445
|
+
'';
|
|
1446
|
+
interaction.fragments.push({
|
|
1447
|
+
...readBase(ctx, child, fragmentId),
|
|
1448
|
+
name: named,
|
|
1449
|
+
operator: 'ref',
|
|
1450
|
+
operands: [{ y0, y1 }],
|
|
1451
|
+
coveredLifelineIds: covered,
|
|
1452
|
+
bounds: { x: 0, y: y0, w: 0, h: Math.max(1, y1 - y0) },
|
|
1453
|
+
});
|
|
1454
|
+
own(draft, fragmentId);
|
|
1455
|
+
for (const each of covered)
|
|
1456
|
+
touched.add(each);
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
if (meta === 'CombinedFragment') {
|
|
1460
|
+
const fragmentId = map(ctx, child);
|
|
1461
|
+
carryUnknownAttrs(ctx, child, fragmentId, fragmentId, [
|
|
1462
|
+
'xmi:type',
|
|
1463
|
+
'type',
|
|
1464
|
+
'xmi:id',
|
|
1465
|
+
'id',
|
|
1466
|
+
'name',
|
|
1467
|
+
'covered',
|
|
1468
|
+
'interactionOperator',
|
|
1469
|
+
]);
|
|
1470
|
+
const stated = xmlAttr(child, 'interactionOperator') ?? 'seq';
|
|
1471
|
+
const operator = (FRAGMENT_OPERATORS.has(stated) ? stated : 'alt');
|
|
1472
|
+
if (!FRAGMENT_OPERATORS.has(stated)) {
|
|
1473
|
+
// CARRIED, and not merely mentioned (ADR 0012 D1): the word is the
|
|
1474
|
+
// one thing this fragment said that the drawing cannot, so it rides
|
|
1475
|
+
// along on the element's foreign payload. `interactionOperator` is in
|
|
1476
|
+
// the understood list above — a word the pentagon DOES draw is not
|
|
1477
|
+
// foreign matter — so the carry is written here, where it is.
|
|
1478
|
+
carryAttr(ctx, fragmentId, fragmentId, 'interactionOperator', stated);
|
|
1479
|
+
note(ctx, {
|
|
1480
|
+
kind: 'warning',
|
|
1481
|
+
element: child.name,
|
|
1482
|
+
sourceId: fragmentId,
|
|
1483
|
+
messageKey: UML_XMI_REMARKS.unlistedOperator[0],
|
|
1484
|
+
message: `This combined fragment's operator is "${stated}", which UML 2.5.1 §17.6.4 does not list. It is drawn as an "alt" and the file's own word is kept beside it.`,
|
|
1485
|
+
messageParams: { operator: stated },
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
const declared = (xmlAttr(child, 'covered') ?? '')
|
|
1489
|
+
.split(/\s+/)
|
|
1490
|
+
.filter(Boolean);
|
|
1491
|
+
const y0 = take();
|
|
1492
|
+
const bands = [];
|
|
1493
|
+
const inside = new Set(declared);
|
|
1494
|
+
const operands = xmlChildren(child, 'operand');
|
|
1495
|
+
for (const [index, operand] of operands.entries()) {
|
|
1496
|
+
consume(ctx, operand);
|
|
1497
|
+
map(ctx, operand);
|
|
1498
|
+
const opened = index === 0 ? y0 : take();
|
|
1499
|
+
const guardNode = xmlChild(operand, 'guard');
|
|
1500
|
+
consume(ctx, guardNode);
|
|
1501
|
+
const specification = guardNode
|
|
1502
|
+
? xmlChild(guardNode, 'specification')
|
|
1503
|
+
: undefined;
|
|
1504
|
+
consume(ctx, specification);
|
|
1505
|
+
const guard = ((specification
|
|
1506
|
+
? (xmlAttr(specification, 'value') ??
|
|
1507
|
+
xmlAttr(specification, 'body') ??
|
|
1508
|
+
specification.text)
|
|
1509
|
+
: undefined) ??
|
|
1510
|
+
(guardNode
|
|
1511
|
+
? (xmlAttr(guardNode, 'name') ?? guardNode.text)
|
|
1512
|
+
: undefined) ??
|
|
1513
|
+
'').trim();
|
|
1514
|
+
const held = readFragments(operand);
|
|
1515
|
+
for (const each of held.covered)
|
|
1516
|
+
inside.add(each);
|
|
1517
|
+
bands.push({ ...(guard ? { guard } : {}), y0: opened, y1: 0 });
|
|
1518
|
+
}
|
|
1519
|
+
if (bands.length === 0)
|
|
1520
|
+
bands.push({ y0, y1: 0 });
|
|
1521
|
+
const y1 = take();
|
|
1522
|
+
for (const [index, band] of bands.entries()) {
|
|
1523
|
+
band.y1 = index + 1 < bands.length ? bands[index + 1].y0 : y1;
|
|
1524
|
+
}
|
|
1525
|
+
interaction.fragments.push({
|
|
1526
|
+
...readBase(ctx, child, fragmentId),
|
|
1527
|
+
name: bands[0]?.guard ?? '',
|
|
1528
|
+
operator,
|
|
1529
|
+
operands: bands,
|
|
1530
|
+
coveredLifelineIds: [...inside],
|
|
1531
|
+
bounds: { x: 0, y: y0, w: 0, h: Math.max(1, y1 - y0) },
|
|
1532
|
+
});
|
|
1533
|
+
own(draft, fragmentId);
|
|
1534
|
+
for (const each of inside)
|
|
1535
|
+
touched.add(each);
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
// Anything else in the ordered list — a StateInvariant, a
|
|
1539
|
+
// ContinuationSpecification, a coregion — is out of scope (ADR 0022) and
|
|
1540
|
+
// is left for the sweep to quarantine verbatim.
|
|
1541
|
+
ctx.consumed.delete(child);
|
|
1542
|
+
}
|
|
1543
|
+
return { covered: touched };
|
|
1544
|
+
};
|
|
1545
|
+
readFragments(node);
|
|
1546
|
+
for (const bar of bars) {
|
|
1547
|
+
const start = bar.start ? pass.occurrence.get(bar.start) : undefined;
|
|
1548
|
+
const finish = bar.finish ? pass.occurrence.get(bar.finish) : undefined;
|
|
1549
|
+
bar.record.y0 = start?.y ?? 0;
|
|
1550
|
+
bar.record.y1 = finish?.y ?? bar.record.y0 + UML_SD_EVENT_STEP;
|
|
1551
|
+
if (!bar.record.lifelineId) {
|
|
1552
|
+
const covered = start?.covered ?? finish?.covered;
|
|
1553
|
+
if (covered)
|
|
1554
|
+
bar.record.lifelineId = covered;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
for (const child of xmlChildren(node, 'message')) {
|
|
1558
|
+
consume(ctx, child);
|
|
1559
|
+
const messageId = map(ctx, child);
|
|
1560
|
+
carryUnknownAttrs(ctx, child, messageId, messageId, [
|
|
1561
|
+
'xmi:type',
|
|
1562
|
+
'type',
|
|
1563
|
+
'xmi:id',
|
|
1564
|
+
'id',
|
|
1565
|
+
'name',
|
|
1566
|
+
'messageSort',
|
|
1567
|
+
'sendEvent',
|
|
1568
|
+
'receiveEvent',
|
|
1569
|
+
'connector',
|
|
1570
|
+
]);
|
|
1571
|
+
const send = xmlAttr(child, 'sendEvent');
|
|
1572
|
+
const receive = xmlAttr(child, 'receiveEvent');
|
|
1573
|
+
const from = send ? pass.occurrence.get(send) : undefined;
|
|
1574
|
+
const to = receive ? pass.occurrence.get(receive) : undefined;
|
|
1575
|
+
if (!from?.covered || !to?.covered) {
|
|
1576
|
+
note(ctx, {
|
|
1577
|
+
kind: 'warning',
|
|
1578
|
+
element: child.name,
|
|
1579
|
+
sourceId: messageId,
|
|
1580
|
+
messageKey: UML_XMI_REMARKS.undrawnOccurrence[0],
|
|
1581
|
+
message: `A message in "${interaction.name}" names an occurrence this sheet has no lifeline for, so it is not drawn. The file still says it, and nothing was removed from the document.`,
|
|
1582
|
+
messageParams: { name: interaction.name },
|
|
1583
|
+
});
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
const sort = xmlAttr(child, 'messageSort') ?? 'synchCall';
|
|
1587
|
+
const label = xmlAttr(child, 'name');
|
|
1588
|
+
// §17.4.4 draws a deleteMessage ENDING on the cross, and that is what the
|
|
1589
|
+
// board draws too: the arrow's target is the destruction, not the spine.
|
|
1590
|
+
const cross = interaction.destructions.find(record => record.lifelineId === to.covered && record.y >= from.y);
|
|
1591
|
+
const kind = MESSAGE_KIND_OF_SORT[sort] ?? 'message-sync';
|
|
1592
|
+
interaction.messages.push({
|
|
1593
|
+
id: messageId,
|
|
1594
|
+
kind,
|
|
1595
|
+
sourceId: from.covered,
|
|
1596
|
+
targetId: kind === 'message-delete' && cross ? cross.id : to.covered,
|
|
1597
|
+
...(label ? { label } : {}),
|
|
1598
|
+
y: from.y,
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
// §17.4.4: top to bottom is time. The `<message>` list is not ordered by the
|
|
1602
|
+
// metamodel — the `fragment` list is — so the order comes off the occurrences.
|
|
1603
|
+
interaction.messages.sort((a, b) => a.y - b.y);
|
|
1604
|
+
/* ── The geometry the file does not carry ──────────────────────────── */
|
|
1605
|
+
const height = umlSequenceSlot(pass.slot) + UML_SD_EVENT_STEP;
|
|
1606
|
+
const columns = new Map();
|
|
1607
|
+
for (const lifeline of interaction.lifelines) {
|
|
1608
|
+
const box = umlSequenceColumn(columnOf.get(lifeline.id) ?? 0, height);
|
|
1609
|
+
lifeline.bounds = box;
|
|
1610
|
+
columns.set(lifeline.id, box);
|
|
1611
|
+
}
|
|
1612
|
+
for (const record of interaction.executions) {
|
|
1613
|
+
const column = record.lifelineId
|
|
1614
|
+
? columns.get(record.lifelineId)
|
|
1615
|
+
: undefined;
|
|
1616
|
+
if (column) {
|
|
1617
|
+
record.bounds = umlSequenceExecution(column, record.y0, record.y1);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
for (const record of interaction.destructions) {
|
|
1621
|
+
const column = record.lifelineId
|
|
1622
|
+
? columns.get(record.lifelineId)
|
|
1623
|
+
: undefined;
|
|
1624
|
+
if (column)
|
|
1625
|
+
record.bounds = umlSequenceDestruction(column, record.y);
|
|
1626
|
+
}
|
|
1627
|
+
for (const record of interaction.fragments) {
|
|
1628
|
+
const covered = record.coveredLifelineIds
|
|
1629
|
+
.map(each => columns.get(each))
|
|
1630
|
+
.filter((box) => box !== undefined);
|
|
1631
|
+
const y0 = record.bounds?.y ?? 0;
|
|
1632
|
+
const y1 = y0 + (record.bounds?.h ?? UML_SD_EVENT_STEP);
|
|
1633
|
+
record.bounds = umlSequenceFragment(covered.length > 0 ? covered : [...columns.values()], y0, y1);
|
|
1634
|
+
if (record.coveredLifelineIds.length === 0) {
|
|
1635
|
+
record.coveredLifelineIds = interaction.lifelines.map(each => each.id);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
return interaction;
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* §17.3.4's `: <Type>` half of a lifeline head, wherever the file put it.
|
|
1642
|
+
*
|
|
1643
|
+
* `Lifeline::represents` is a ConnectableElement, so a tool that has one writes
|
|
1644
|
+
* an idref and this reader resolves it to a NAME — which is what a head can
|
|
1645
|
+
* draw. Our own writer has no Property to point at and files the type under
|
|
1646
|
+
* `xmi:Extension` instead (XMI 2.5.1 §7.9), so that is read too, and the head
|
|
1647
|
+
* comes back exactly as it went out.
|
|
1648
|
+
*/
|
|
1649
|
+
function lifelineTypeOf(ctx, node) {
|
|
1650
|
+
for (const child of node.children) {
|
|
1651
|
+
if (!isLabreExtension(child))
|
|
1652
|
+
continue;
|
|
1653
|
+
const represents = xmlChild(child, 'represents');
|
|
1654
|
+
const named = represents ? xmlAttr(represents, 'name') : undefined;
|
|
1655
|
+
if (named) {
|
|
1656
|
+
consume(ctx, represents);
|
|
1657
|
+
if (child.children.every(entry => ctx.consumed.has(entry))) {
|
|
1658
|
+
consume(ctx, child);
|
|
1659
|
+
}
|
|
1660
|
+
return { type: named };
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
// A tool that HAS a ConnectableElement to point at: §17.3.4's `: <Type>` is
|
|
1664
|
+
// then the type of that Property, not its name — `anOrder : Order` is a
|
|
1665
|
+
// property called `anOrder` typed by `Order`, and the head writes the second.
|
|
1666
|
+
// A Property with no type says nothing a head can draw, so nothing is
|
|
1667
|
+
// invented.
|
|
1668
|
+
const represents = xmlAttr(node, 'represents');
|
|
1669
|
+
const property = represents ? ctx.byId.get(represents) : undefined;
|
|
1670
|
+
const typed = property &&
|
|
1671
|
+
(xmlAttr(property, 'type') ??
|
|
1672
|
+
(xmlChild(property, 'type')
|
|
1673
|
+
? refOf(xmlChild(property, 'type'))
|
|
1674
|
+
: undefined));
|
|
1675
|
+
const named = typed ? nameOfId(ctx, typed) : undefined;
|
|
1676
|
+
return named ? { type: named } : {};
|
|
1677
|
+
}
|
|
1678
|
+
/* ── One package ──────────────────────────────────────────────────────── */
|
|
1679
|
+
/**
|
|
1680
|
+
* Everything one package declares, read into one model.
|
|
1681
|
+
*
|
|
1682
|
+
* Recursive over nested packages, and FLAT in the model: `UmlModel.packages` is
|
|
1683
|
+
* a list of boxes whose nesting the drawing states (`xmi.ts`'s `containerOf`
|
|
1684
|
+
* reads it off geometry), and a file that carries no geometry has not said
|
|
1685
|
+
* where anything is drawn. The nesting is therefore a fact this import cannot
|
|
1686
|
+
* place on the canvas, and it says so once rather than silently drawing a flat
|
|
1687
|
+
* sheet that looks like the author's.
|
|
1688
|
+
*/
|
|
1689
|
+
function readPackage(ctx, draft, node, containerId) {
|
|
1690
|
+
const contained = (childId) => {
|
|
1691
|
+
if (!containerId)
|
|
1692
|
+
return;
|
|
1693
|
+
draft.nested += 1;
|
|
1694
|
+
// Recorded as well as counted: the count is what the report says, and the
|
|
1695
|
+
// pair is what the invented layout needs to DRAW the nesting rather than
|
|
1696
|
+
// apologise for it (`import.ts`, `UmlLayoutHints.containment`).
|
|
1697
|
+
ctx.containment[childId] = containerId;
|
|
1698
|
+
};
|
|
1699
|
+
for (const child of node.children) {
|
|
1700
|
+
if (isLabreExtension(child))
|
|
1701
|
+
continue;
|
|
1702
|
+
const meta = metaOf(child) ?? '';
|
|
1703
|
+
if (child.local === 'ownedComment' || meta === 'Comment') {
|
|
1704
|
+
consume(ctx, child);
|
|
1705
|
+
const commentId = map(ctx, child);
|
|
1706
|
+
const bodyChild = xmlChild(child, 'body');
|
|
1707
|
+
consume(ctx, bodyChild);
|
|
1708
|
+
const body = xmlAttr(child, 'body') ?? bodyChild?.text ?? child.text ?? '';
|
|
1709
|
+
draft.model.notes.push({
|
|
1710
|
+
id: commentId,
|
|
1711
|
+
name: '',
|
|
1712
|
+
keywords: [],
|
|
1713
|
+
isAbstract: false,
|
|
1714
|
+
...(ctx.layout[commentId] ? { bounds: ctx.layout[commentId] } : {}),
|
|
1715
|
+
body,
|
|
1716
|
+
});
|
|
1717
|
+
own(draft, commentId);
|
|
1718
|
+
for (const annotated of (xmlAttr(child, 'annotatedElement') ?? '')
|
|
1719
|
+
.split(/\s+/)
|
|
1720
|
+
.filter(Boolean)) {
|
|
1721
|
+
relate(draft, 'anchor', commentId, annotated);
|
|
1722
|
+
}
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
if (child.local !== 'packagedElement' && child.local !== 'ownedUseCase') {
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
// A use case owned by its subject (§18.1.4) — containment, not a reference,
|
|
1729
|
+
// so the case is read here and the subject is what it is drawn inside.
|
|
1730
|
+
if (child.local === 'ownedUseCase' || meta === 'UseCase') {
|
|
1731
|
+
consume(ctx, child);
|
|
1732
|
+
const useCaseId = map(ctx, child);
|
|
1733
|
+
carryUnknownAttrs(ctx, child, useCaseId);
|
|
1734
|
+
draft.model.useCases.push(readUseCase(ctx, draft, child, useCaseId));
|
|
1735
|
+
own(draft, useCaseId);
|
|
1736
|
+
contained(useCaseId);
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
if (meta === 'Package') {
|
|
1740
|
+
consume(ctx, child);
|
|
1741
|
+
const packageId = map(ctx, child);
|
|
1742
|
+
carryUnknownAttrs(ctx, child, packageId);
|
|
1743
|
+
draft.model.packages.push(readBase(ctx, child, packageId));
|
|
1744
|
+
own(draft, packageId);
|
|
1745
|
+
contained(packageId);
|
|
1746
|
+
readPackage(ctx, draft, child, packageId);
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
if (CLASSIFIER_KIND[meta]) {
|
|
1750
|
+
consume(ctx, child);
|
|
1751
|
+
const classifierId = map(ctx, child);
|
|
1752
|
+
carryUnknownAttrs(ctx, child, classifierId);
|
|
1753
|
+
readClassifier(ctx, draft, child, meta, classifierId);
|
|
1754
|
+
contained(classifierId);
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
if (meta === 'Actor') {
|
|
1758
|
+
consume(ctx, child);
|
|
1759
|
+
const actorId = map(ctx, child);
|
|
1760
|
+
carryUnknownAttrs(ctx, child, actorId);
|
|
1761
|
+
draft.model.actors.push(readBase(ctx, child, actorId));
|
|
1762
|
+
own(draft, actorId);
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
if (meta === 'Component') {
|
|
1766
|
+
consume(ctx, child);
|
|
1767
|
+
const componentId = map(ctx, child);
|
|
1768
|
+
carryUnknownAttrs(ctx, child, componentId);
|
|
1769
|
+
// The SUBJECT of a use case diagram is a Component with cases in it —
|
|
1770
|
+
// which is what §18.1.4 makes it and what `xmi.ts` writes. A component
|
|
1771
|
+
// with no `ownedUseCase` is a component (§11.6.4), and the drawing is the
|
|
1772
|
+
// only thing that differs.
|
|
1773
|
+
if (xmlChild(child, 'ownedUseCase') || draft.subjectLikely) {
|
|
1774
|
+
draft.model.subjects.push(readBase(ctx, child, componentId));
|
|
1775
|
+
own(draft, componentId);
|
|
1776
|
+
readPackage(ctx, draft, child, componentId);
|
|
1777
|
+
}
|
|
1778
|
+
else {
|
|
1779
|
+
draft.model.components.push(readComponent(ctx, draft, child, componentId));
|
|
1780
|
+
own(draft, componentId);
|
|
1781
|
+
}
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
if (meta === 'Artifact') {
|
|
1785
|
+
consume(ctx, child);
|
|
1786
|
+
const artifactId = map(ctx, child);
|
|
1787
|
+
carryUnknownAttrs(ctx, child, artifactId);
|
|
1788
|
+
draft.model.artifacts.push(readBase(ctx, child, artifactId));
|
|
1789
|
+
own(draft, artifactId);
|
|
1790
|
+
for (const manifestation of xmlChildren(child, 'manifestation')) {
|
|
1791
|
+
consume(ctx, manifestation);
|
|
1792
|
+
relate(draft, 'manifest', artifactId, xmlAttr(manifestation, 'utilizedElement') ??
|
|
1793
|
+
xmlAttr(manifestation, 'supplier'), labelOf(manifestation));
|
|
1794
|
+
}
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
if (DEPLOYMENT_KIND[meta]) {
|
|
1798
|
+
consume(ctx, child);
|
|
1799
|
+
const cubeId = map(ctx, child);
|
|
1800
|
+
carryUnknownAttrs(ctx, child, cubeId);
|
|
1801
|
+
draft.model.nodes.push({
|
|
1802
|
+
...readBase(ctx, child, cubeId),
|
|
1803
|
+
kind: DEPLOYMENT_KIND[meta],
|
|
1804
|
+
});
|
|
1805
|
+
own(draft, cubeId);
|
|
1806
|
+
for (const deployment of xmlChildren(child, 'deployment')) {
|
|
1807
|
+
consume(ctx, deployment);
|
|
1808
|
+
relate(draft, 'deploy', xmlAttr(deployment, 'deployedArtifact') ??
|
|
1809
|
+
xmlAttr(deployment, 'supplier'), cubeId, labelOf(deployment));
|
|
1810
|
+
}
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1813
|
+
if (meta === 'Association' || meta === 'CommunicationPath') {
|
|
1814
|
+
consume(ctx, child);
|
|
1815
|
+
const associationId = map(ctx, child);
|
|
1816
|
+
carryUnknownAttrs(ctx, child, associationId);
|
|
1817
|
+
readAssociation(ctx, draft, child, associationId, meta === 'CommunicationPath' ? 'communication-path' : 'association');
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
if (DEPENDENCY_KIND[meta]) {
|
|
1821
|
+
consume(ctx, child);
|
|
1822
|
+
const dependencyId = map(ctx, child);
|
|
1823
|
+
carryUnknownAttrs(ctx, child, dependencyId);
|
|
1824
|
+
const clientChild = xmlChild(child, 'client');
|
|
1825
|
+
const supplierChild = xmlChild(child, 'supplier');
|
|
1826
|
+
consume(ctx, clientChild, supplierChild);
|
|
1827
|
+
const client = xmlAttr(child, 'client') ??
|
|
1828
|
+
(clientChild ? refOf(clientChild) : undefined);
|
|
1829
|
+
const supplier = xmlAttr(child, 'supplier') ??
|
|
1830
|
+
(supplierChild ? refOf(supplierChild) : undefined);
|
|
1831
|
+
relate(draft, DEPENDENCY_KIND[meta], client, supplier, labelOf(child));
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
if (meta === 'Activity') {
|
|
1835
|
+
consume(ctx, child);
|
|
1836
|
+
const activityId = map(ctx, child);
|
|
1837
|
+
carryUnknownAttrs(ctx, child, activityId);
|
|
1838
|
+
draft.model.activities.push(readActivity(ctx, draft, child, activityId));
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
if (meta === 'StateMachine') {
|
|
1842
|
+
consume(ctx, child);
|
|
1843
|
+
const machineId = map(ctx, child);
|
|
1844
|
+
carryUnknownAttrs(ctx, child, machineId);
|
|
1845
|
+
draft.model.stateMachines.push(readStateMachine(ctx, draft, child, machineId));
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1848
|
+
if (meta === 'Interaction') {
|
|
1849
|
+
consume(ctx, child);
|
|
1850
|
+
const interactionId = map(ctx, child);
|
|
1851
|
+
carryUnknownAttrs(ctx, child, interactionId);
|
|
1852
|
+
draft.model.interactions.push(readInteraction(ctx, draft, child, interactionId));
|
|
1853
|
+
continue;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* The Signals and Events an activity glyph REFERENCED, folded into the sheet.
|
|
1859
|
+
*
|
|
1860
|
+
* `xmi.ts` mints a `uml:Signal` beside an Activity because a SendSignalAction's
|
|
1861
|
+
* `signal` is a reference and a reference needs something to point at. Reading
|
|
1862
|
+
* it back means recognising exactly those: a Signal a glyph names is the glyph's
|
|
1863
|
+
* own name written twice, and re-drawing it would put a box on the canvas the
|
|
1864
|
+
* author never drew. A Signal NOTHING names is a declaration the file makes on
|
|
1865
|
+
* its own account, and it is quarantined like anything else this reader cannot
|
|
1866
|
+
* draw.
|
|
1867
|
+
*/
|
|
1868
|
+
function consumeReferencedEvents(ctx, draft, node) {
|
|
1869
|
+
for (const child of xmlChildren(node, 'packagedElement')) {
|
|
1870
|
+
const meta = metaOf(child) ?? '';
|
|
1871
|
+
if (meta !== 'Signal' && meta !== 'SignalEvent' && meta !== 'TimeEvent') {
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
const id = xmlAttr(child, 'id');
|
|
1875
|
+
if (!id)
|
|
1876
|
+
continue;
|
|
1877
|
+
if (meta === 'Signal' && !draft.usedSignals.has(id))
|
|
1878
|
+
continue;
|
|
1879
|
+
if (meta !== 'Signal' && !referencedByATrigger(node, id))
|
|
1880
|
+
continue;
|
|
1881
|
+
// Whole: a TimeEvent owns its `<when>` TimeExpression, and the expression
|
|
1882
|
+
// is the glyph's own words read back through `parseTrigger` at export.
|
|
1883
|
+
consumeSubtree(ctx, child);
|
|
1884
|
+
// A signal referenced by an event that is itself referenced.
|
|
1885
|
+
const signal = xmlAttr(child, 'signal');
|
|
1886
|
+
if (signal)
|
|
1887
|
+
draft.usedSignals.add(signal);
|
|
1888
|
+
}
|
|
1889
|
+
// A second pass, because a SignalEvent names its Signal and the two are
|
|
1890
|
+
// written in either order.
|
|
1891
|
+
for (const child of xmlChildren(node, 'packagedElement')) {
|
|
1892
|
+
const id = xmlAttr(child, 'id');
|
|
1893
|
+
if ((metaOf(child) ?? '') !== 'Signal' || !id)
|
|
1894
|
+
continue;
|
|
1895
|
+
if (draft.usedSignals.has(id))
|
|
1896
|
+
consume(ctx, child);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
/** Does any `<trigger event="…">` under this package name that id? */
|
|
1900
|
+
function referencedByATrigger(node, id) {
|
|
1901
|
+
return xmlDescendants(node).some(descendant => descendant.local === 'trigger' && xmlAttr(descendant, 'event') === id);
|
|
1902
|
+
}
|
|
1903
|
+
/* ── The diagram kind ─────────────────────────────────────────────────── */
|
|
1904
|
+
/**
|
|
1905
|
+
* Which Annex A frame this package was drawn as.
|
|
1906
|
+
*
|
|
1907
|
+
* Our own extension when the file carries one — it is the one fact the UML
|
|
1908
|
+
* metamodel has no slot for, which is why `xmi.ts` files it under
|
|
1909
|
+
* `xmi:Extension` in the first place. Otherwise INFERRED from what the package
|
|
1910
|
+
* declares, in the order a diagram is recognised by eye: actors and use cases
|
|
1911
|
+
* make a use case sheet whatever else is on it, a flow makes an activity, a
|
|
1912
|
+
* machine makes a state machine, cubes and artefacts make a deployment,
|
|
1913
|
+
* components make a component diagram, and everything else is a class diagram
|
|
1914
|
+
* — which is also what `UmlDiagramElementModel` defaults to.
|
|
1915
|
+
*/
|
|
1916
|
+
function declaredDiagramKind(ctx, node) {
|
|
1917
|
+
for (const child of node.children) {
|
|
1918
|
+
if (!isLabreExtension(child))
|
|
1919
|
+
continue;
|
|
1920
|
+
const diagram = xmlChild(child, 'diagram');
|
|
1921
|
+
const kind = diagram ? xmlAttr(diagram, 'kind') : undefined;
|
|
1922
|
+
if (!kind)
|
|
1923
|
+
continue;
|
|
1924
|
+
consume(ctx, diagram);
|
|
1925
|
+
if (child.children.every(entry => ctx.consumed.has(entry))) {
|
|
1926
|
+
consume(ctx, child);
|
|
1927
|
+
}
|
|
1928
|
+
return kind;
|
|
1929
|
+
}
|
|
1930
|
+
return undefined;
|
|
1931
|
+
}
|
|
1932
|
+
/** What the package DECLARES, when nothing said which frame it was drawn as. */
|
|
1933
|
+
function inferDiagramKind(model) {
|
|
1934
|
+
// An Interaction first: §17.2.4's sheet is the one that draws its
|
|
1935
|
+
// participants as lifelines, and a `uml:Actor` referenced by one would
|
|
1936
|
+
// otherwise make it look like a use case diagram.
|
|
1937
|
+
if (model.interactions.length > 0)
|
|
1938
|
+
return 'sd';
|
|
1939
|
+
if (model.useCases.length > 0 || model.actors.length > 0)
|
|
1940
|
+
return 'uc';
|
|
1941
|
+
if (model.activities.length > 0)
|
|
1942
|
+
return 'act';
|
|
1943
|
+
if (model.stateMachines.length > 0)
|
|
1944
|
+
return 'stm';
|
|
1945
|
+
if (model.nodes.length > 0 || model.artifacts.length > 0)
|
|
1946
|
+
return 'dep';
|
|
1947
|
+
if (model.components.length > 0)
|
|
1948
|
+
return 'cmp';
|
|
1949
|
+
return 'class';
|
|
1950
|
+
}
|
|
1951
|
+
/* ── The sweep ────────────────────────────────────────────────────────── */
|
|
1952
|
+
/**
|
|
1953
|
+
* Everything the reader did not read, kept verbatim under the nearest artefact.
|
|
1954
|
+
*
|
|
1955
|
+
* One pass, after the whole document has been read, because "nearest mapped
|
|
1956
|
+
* ancestor" is a question only the finished tree can answer. It descends into
|
|
1957
|
+
* what was consumed and stops at what was not: an `<eAnnotations>` is
|
|
1958
|
+
* quarantined whole, with its children inside it, rather than once per node —
|
|
1959
|
+
* the fragment is the file's own bytes and the file nested them.
|
|
1960
|
+
*/
|
|
1961
|
+
function sweep(ctx, node, scope) {
|
|
1962
|
+
for (const child of node.children) {
|
|
1963
|
+
const nearer = ctx.scopeOf.get(child);
|
|
1964
|
+
if (nearer !== undefined) {
|
|
1965
|
+
sweep(ctx, child, nearer);
|
|
1966
|
+
continue;
|
|
1967
|
+
}
|
|
1968
|
+
if (ctx.consumed.has(child)) {
|
|
1969
|
+
sweep(ctx, child, scope);
|
|
1970
|
+
continue;
|
|
1971
|
+
}
|
|
1972
|
+
quarantine(ctx, scope, xmlFragmentOf(ctx.source, child), UML_XMI_QUARANTINE_REASON.unmapped, {
|
|
1973
|
+
element: child.name,
|
|
1974
|
+
...(xmlAttr(child, 'id') ? { sourceId: xmlAttr(child, 'id') } : {}),
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
/* ── The document ─────────────────────────────────────────────────────── */
|
|
1979
|
+
/** The `uml:Model` / `uml:Package` a file opens on, through any wrapper. */
|
|
1980
|
+
function rootOf(document) {
|
|
1981
|
+
for (const root of document.roots) {
|
|
1982
|
+
if (root.local === 'Model' || root.local === 'Package')
|
|
1983
|
+
return root;
|
|
1984
|
+
// `<xmi:XMI>` is XMI's own multi-root wrapper, and Enterprise Architect
|
|
1985
|
+
// writes one round everything.
|
|
1986
|
+
if (root.local === 'XMI') {
|
|
1987
|
+
const inner = root.children.find(child => child.local === 'Model' || child.local === 'Package');
|
|
1988
|
+
if (inner)
|
|
1989
|
+
return inner;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
return undefined;
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Read an XMI 2.5.1 document as models plus a report.
|
|
1996
|
+
*
|
|
1997
|
+
* See the module comment for the contract, and `docs/adr/0012` D1–D6 for why it
|
|
1998
|
+
* is this contract and not a shorter one.
|
|
1999
|
+
*/
|
|
2000
|
+
export function importXmi(source, context = {}) {
|
|
2001
|
+
const document = readXml(source);
|
|
2002
|
+
// The three bags keyed by an id the FILE chose have no prototype: `__proto__`
|
|
2003
|
+
// and `constructor` are ids a document is free to use, and on a plain object
|
|
2004
|
+
// the first writes through to `Object.prototype` and the second reads a
|
|
2005
|
+
// function back as a box. Nothing downstream asks these bags for an inherited
|
|
2006
|
+
// method — spread, `Object.keys` and `Object.entries` all still work.
|
|
2007
|
+
const ctx = {
|
|
2008
|
+
source,
|
|
2009
|
+
notes: [],
|
|
2010
|
+
foreign: Object.create(null),
|
|
2011
|
+
layout: Object.create(null),
|
|
2012
|
+
byId: new Map(),
|
|
2013
|
+
consumed: new Set(),
|
|
2014
|
+
scopeOf: new Map(),
|
|
2015
|
+
counts: { mapped: 0, carried: 0, quarantined: 0 },
|
|
2016
|
+
containment: Object.create(null),
|
|
2017
|
+
minted: 0,
|
|
2018
|
+
};
|
|
2019
|
+
for (const complaint of document.notes) {
|
|
2020
|
+
note(ctx, { kind: 'warning', message: complaint });
|
|
2021
|
+
}
|
|
2022
|
+
const root = rootOf(document);
|
|
2023
|
+
if (!root) {
|
|
2024
|
+
const opened = document.roots[0]?.name;
|
|
2025
|
+
note(ctx, {
|
|
2026
|
+
kind: 'warning',
|
|
2027
|
+
...(opened ? { element: opened } : {}),
|
|
2028
|
+
// Two SHAPES of sentence, so two keys — never one key whose wording
|
|
2029
|
+
// depends on a branch the host cannot see.
|
|
2030
|
+
...(opened
|
|
2031
|
+
? {
|
|
2032
|
+
messageKey: UML_XMI_REMARKS.wrongRoot[0],
|
|
2033
|
+
message: `A UML interchange file opens on <uml:Model> or <uml:Package>; this one opens on <${opened}>. Nothing was imported.`,
|
|
2034
|
+
messageParams: { tag: opened },
|
|
2035
|
+
}
|
|
2036
|
+
: {
|
|
2037
|
+
messageKey: UML_XMI_REMARKS.noXml[0],
|
|
2038
|
+
message: UML_XMI_REMARKS.noXml[1],
|
|
2039
|
+
}),
|
|
2040
|
+
});
|
|
2041
|
+
return {
|
|
2042
|
+
models: [],
|
|
2043
|
+
layout: {},
|
|
2044
|
+
containment: {},
|
|
2045
|
+
foreign: {},
|
|
2046
|
+
report: { mapped: 0, carried: 0, quarantined: 0, notes: ctx.notes },
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
for (const node of [root, ...xmlDescendants(root)]) {
|
|
2050
|
+
const id = xmlAttr(node, 'id');
|
|
2051
|
+
if (id && !ctx.byId.has(id))
|
|
2052
|
+
ctx.byId.set(id, node);
|
|
2053
|
+
}
|
|
2054
|
+
// The DI lives beside the model, not under it — a `.notation` island, a
|
|
2055
|
+
// `umldi` root — so it is read off the whole document rather than off `root`.
|
|
2056
|
+
for (const island of document.roots) {
|
|
2057
|
+
Object.assign(ctx.layout, readLayout(island));
|
|
2058
|
+
}
|
|
2059
|
+
/* ── The packages that are the sheets ──────────────────────────────── */
|
|
2060
|
+
const packages = root.local === 'Package'
|
|
2061
|
+
? [root]
|
|
2062
|
+
: root.children.filter(child => child.local === 'packagedElement' &&
|
|
2063
|
+
(metaOf(child) ?? '') === 'Package');
|
|
2064
|
+
const models = [];
|
|
2065
|
+
const drafts = [];
|
|
2066
|
+
/** A `uml:Model` whose classifiers hang loose — Papyrus's own shape. */
|
|
2067
|
+
const sheets = packages.length > 0 ? packages : [root];
|
|
2068
|
+
for (const sheet of sheets) {
|
|
2069
|
+
const sheetId = map(ctx, sheet);
|
|
2070
|
+
carryUnknownAttrs(ctx, sheet, sheetId);
|
|
2071
|
+
const name = xmlAttr(sheet, 'name') ?? context.name ?? '';
|
|
2072
|
+
const declaredKind = declaredDiagramKind(ctx, sheet);
|
|
2073
|
+
const draft = {
|
|
2074
|
+
model: emptyModel(sheetId, name),
|
|
2075
|
+
ids: new Set(),
|
|
2076
|
+
ends: new Map(),
|
|
2077
|
+
usedSignals: new Set(),
|
|
2078
|
+
nested: 0,
|
|
2079
|
+
...(declaredKind ? { declaredKind } : {}),
|
|
2080
|
+
subjectLikely: declaredKind === 'uc' ||
|
|
2081
|
+
xmlDescendants(sheet).some(each => each.local === 'ownedUseCase' || (metaOf(each) ?? '') === 'UseCase'),
|
|
2082
|
+
};
|
|
2083
|
+
readPackage(ctx, draft, sheet);
|
|
2084
|
+
consumeReferencedEvents(ctx, draft, sheet);
|
|
2085
|
+
drafts.push(draft);
|
|
2086
|
+
models.push(draft.model);
|
|
2087
|
+
}
|
|
2088
|
+
/* ── The root's own declarations ───────────────────────────────────── */
|
|
2089
|
+
// A `uml:DataType` at the root is what `xmi.ts` mints for a type name nothing
|
|
2090
|
+
// on the canvas answered for. It is READ — every `total : Money` in the file
|
|
2091
|
+
// resolves through it — and it is not drawn, because the author never drew
|
|
2092
|
+
// it. Consumed rather than quarantined for exactly that reason.
|
|
2093
|
+
if (root.local !== 'Package') {
|
|
2094
|
+
for (const child of root.children) {
|
|
2095
|
+
const meta = metaOf(child) ?? '';
|
|
2096
|
+
if (child.local !== 'packagedElement')
|
|
2097
|
+
continue;
|
|
2098
|
+
if (meta === 'DataType' || meta === 'PrimitiveType')
|
|
2099
|
+
consume(ctx, child);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
/* ── What each sheet can actually draw ─────────────────────────────── */
|
|
2103
|
+
for (const draft of drafts) {
|
|
2104
|
+
const { model } = draft;
|
|
2105
|
+
const kept = [];
|
|
2106
|
+
for (const relation of model.relations) {
|
|
2107
|
+
if (draft.ids.has(relation.sourceId) &&
|
|
2108
|
+
draft.ids.has(relation.targetId)) {
|
|
2109
|
+
kept.push(relation);
|
|
2110
|
+
continue;
|
|
2111
|
+
}
|
|
2112
|
+
note(ctx, {
|
|
2113
|
+
kind: 'warning',
|
|
2114
|
+
messageKey: UML_XMI_REMARKS.offSheetRelation[0],
|
|
2115
|
+
message: `A ${relation.kind} in "${model.diagram.name}" runs to an element that is not on this sheet, so it is not drawn. The file still says it, and nothing was removed from the document.`,
|
|
2116
|
+
messageParams: { kind: relation.kind, name: model.diagram.name },
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
model.relations = kept;
|
|
2120
|
+
// A partition or a lane whose nodes are elsewhere is still a lane.
|
|
2121
|
+
for (const activity of model.activities) {
|
|
2122
|
+
activity.edges = activity.edges.filter(edge => draft.ids.has(edge.sourceId) && draft.ids.has(edge.targetId));
|
|
2123
|
+
}
|
|
2124
|
+
for (const machine of model.stateMachines) {
|
|
2125
|
+
machine.transitions = machine.transitions.filter(transition => draft.ids.has(transition.sourceId) &&
|
|
2126
|
+
draft.ids.has(transition.targetId));
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
/* ── The frame each sheet is ───────────────────────────────────────── */
|
|
2130
|
+
for (const draft of drafts) {
|
|
2131
|
+
const kind = draft.declaredKind ?? inferDiagramKind(draft.model);
|
|
2132
|
+
draft.model.diagram.kind = kind;
|
|
2133
|
+
draft.model.diagram.heading = `${kind} ${draft.model.diagram.name}`.trim();
|
|
2134
|
+
const bounds = ctx.layout[draft.model.diagram.id];
|
|
2135
|
+
if (bounds)
|
|
2136
|
+
draft.model.diagram.bounds = bounds;
|
|
2137
|
+
}
|
|
2138
|
+
/* ── Nothing is lost (D1, D5) ──────────────────────────────────────── */
|
|
2139
|
+
// D6's residue, and its one accepted asymmetry: what has no artefact of its
|
|
2140
|
+
// own rides on the FIRST sheet's frame. Delete that frame and what it carried
|
|
2141
|
+
// goes with it — which is the whole argument of D2, made once more.
|
|
2142
|
+
const residue = drafts[0]?.model.diagram.id;
|
|
2143
|
+
if (residue) {
|
|
2144
|
+
for (const island of document.roots)
|
|
2145
|
+
sweep(ctx, island, residue);
|
|
2146
|
+
}
|
|
2147
|
+
if (Object.keys(ctx.layout).length === 0 && ctx.counts.mapped > 0) {
|
|
2148
|
+
note(ctx, {
|
|
2149
|
+
kind: 'invented-layout',
|
|
2150
|
+
messageKey: UML_XMI_REMARKS.modelWithoutDiagram[0],
|
|
2151
|
+
message: UML_XMI_REMARKS.modelWithoutDiagram[1],
|
|
2152
|
+
});
|
|
2153
|
+
// The one statement a file can make that this reader can read and cannot
|
|
2154
|
+
// draw. Said once per document, in the user's words, and said because a
|
|
2155
|
+
// silent flattening is the kind of loss they would only find months later:
|
|
2156
|
+
// on this canvas a package holds what its box holds (§12.2.4 is drawn, not
|
|
2157
|
+
// stored), so containment needs geometry, and a model file carries none.
|
|
2158
|
+
const nested = drafts.reduce((total, draft) => total + draft.nested, 0);
|
|
2159
|
+
if (nested > 0) {
|
|
2160
|
+
note(ctx, {
|
|
2161
|
+
kind: 'warning',
|
|
2162
|
+
// The one wording this lot changed: the count's own singular/plural
|
|
2163
|
+
// is the HOST's (ADR 0023), so the English fallback stays neutral —
|
|
2164
|
+
// `{{count}} element(s) are` — exactly as BPMN's `{{count}} lane(s)`
|
|
2165
|
+
// does, rather than picking English's agreement in the library.
|
|
2166
|
+
messageKey: UML_XMI_REMARKS.nestedWithoutDrawing[0],
|
|
2167
|
+
message: `${nested} element(s) are declared inside another in this file — a class in a package, a use case in its subject. Labre states that by DRAWING one box inside the other, and this file carries no drawing, so they were laid out side by side. Move them into the box to say it again.`,
|
|
2168
|
+
messageParams: { count: nested },
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
// The behaviour edges, counted once they are known to be DRAWN.
|
|
2173
|
+
//
|
|
2174
|
+
// An `<edge>` and a `<transition>` are children of the Activity and the
|
|
2175
|
+
// StateMachine rather than elements with an id of their own, so `map()` — the
|
|
2176
|
+
// one place `mapped` is incremented — never sees them. Counted here, after the
|
|
2177
|
+
// pass that drops the ones running off the sheet, so the report says how many
|
|
2178
|
+
// arrows the board has rather than how many the file wrote.
|
|
2179
|
+
for (const model of models) {
|
|
2180
|
+
for (const activity of model.activities) {
|
|
2181
|
+
ctx.counts.mapped += activity.edges.length;
|
|
2182
|
+
}
|
|
2183
|
+
for (const machine of model.stateMachines) {
|
|
2184
|
+
ctx.counts.mapped += machine.transitions.length;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
const version = xmlAttr(root, 'version');
|
|
2188
|
+
return {
|
|
2189
|
+
models,
|
|
2190
|
+
layout: ctx.layout,
|
|
2191
|
+
containment: ctx.containment,
|
|
2192
|
+
foreign: ctx.foreign,
|
|
2193
|
+
report: {
|
|
2194
|
+
mapped: ctx.counts.mapped,
|
|
2195
|
+
carried: ctx.counts.carried,
|
|
2196
|
+
quarantined: ctx.counts.quarantined,
|
|
2197
|
+
notes: ctx.notes,
|
|
2198
|
+
...(version ? { sourceVersion: version } : {}),
|
|
2199
|
+
},
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
/* ── Putting the foreign matter back on the elements ──────────────────── */
|
|
2203
|
+
/**
|
|
2204
|
+
* The materializer's elements, each carrying what the file said about it.
|
|
2205
|
+
*
|
|
2206
|
+
* ## Why this is a second pass and not an argument
|
|
2207
|
+
*
|
|
2208
|
+
* `umlElementsFromModel` already writes `interchange[formatId] = { id }` on
|
|
2209
|
+
* every element it mints — the source id, verbatim, which is D3's fixed point —
|
|
2210
|
+
* so the id map this needs is not a thing the parser has to be told: it is
|
|
2211
|
+
* written on the elements. Merging here keeps the materializer one function of
|
|
2212
|
+
* one IR, shared by three importers, none of which has to know what the others
|
|
2213
|
+
* carry.
|
|
2214
|
+
*
|
|
2215
|
+
* The payload is written as ONE whole blob per format, never patched field by
|
|
2216
|
+
* field: the `Y.Map` entry is the entire record, so a partial update is a
|
|
2217
|
+
* last-write-wins overwrite of everything (D2).
|
|
2218
|
+
*
|
|
2219
|
+
* ## The residue (D6)
|
|
2220
|
+
*
|
|
2221
|
+
* A payload whose source id matched no element — the document-scope matter, a
|
|
2222
|
+
* quarantined fragment off an element that was never drawn — rides on the FIRST
|
|
2223
|
+
* element, which for this materializer is the first sheet's frame. Its accepted
|
|
2224
|
+
* asymmetry is D6's own: delete that frame and what it carried goes with it.
|
|
2225
|
+
*/
|
|
2226
|
+
export function umlElementsWithForeign(elements, foreign, formatId) {
|
|
2227
|
+
const placed = new Set();
|
|
2228
|
+
const merged = elements.map(props => {
|
|
2229
|
+
const carried = props.interchange;
|
|
2230
|
+
const sourceId = carried?.[formatId]?.id;
|
|
2231
|
+
const payload = sourceId ? foreign[sourceId] : undefined;
|
|
2232
|
+
if (!sourceId || !payload)
|
|
2233
|
+
return props;
|
|
2234
|
+
placed.add(sourceId);
|
|
2235
|
+
return {
|
|
2236
|
+
...props,
|
|
2237
|
+
interchange: { ...carried, [formatId]: payload },
|
|
2238
|
+
};
|
|
2239
|
+
});
|
|
2240
|
+
const residue = Object.entries(foreign).filter(([sourceId]) => !placed.has(sourceId));
|
|
2241
|
+
const first = merged[0];
|
|
2242
|
+
if (residue.length === 0 || !first)
|
|
2243
|
+
return merged;
|
|
2244
|
+
const carried = first.interchange;
|
|
2245
|
+
const own = carried?.[formatId] ?? {};
|
|
2246
|
+
// Flattened onto the frame's own payload rather than nested under a second
|
|
2247
|
+
// key, because `ForeignInterchange` is one record per format and the SCOPES
|
|
2248
|
+
// inside it are what keep two source elements apart.
|
|
2249
|
+
const attrs = residue.reduce((all, [, payload]) => ({ ...all, ...payload.attrs }), { ...own.attrs });
|
|
2250
|
+
const quarantined = [
|
|
2251
|
+
...(own.quarantined ?? []),
|
|
2252
|
+
...residue.flatMap(([, payload]) => payload.quarantined ?? []),
|
|
2253
|
+
];
|
|
2254
|
+
merged[0] = {
|
|
2255
|
+
...first,
|
|
2256
|
+
interchange: {
|
|
2257
|
+
...carried,
|
|
2258
|
+
[formatId]: {
|
|
2259
|
+
...own,
|
|
2260
|
+
// Absent rather than empty: `{}` costs a `Y.Map` entry and makes "this
|
|
2261
|
+
// element carried nothing" untestable (D2).
|
|
2262
|
+
...(Object.keys(attrs).length > 0 ? { attrs } : {}),
|
|
2263
|
+
...(quarantined.length > 0 ? { quarantined } : {}),
|
|
2264
|
+
},
|
|
2265
|
+
},
|
|
2266
|
+
};
|
|
2267
|
+
return merged;
|
|
2268
|
+
}
|