@formicoidea/labre-framework-uml 0.50.0 → 0.52.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/dist/consts.d.ts +4 -4
- package/dist/consts.js +6 -6
- package/dist/drawio-decode.d.ts +38 -4
- package/dist/drawio-decode.js +118 -13
- package/dist/drawio-import.d.ts +1 -1
- package/dist/drawio-import.js +156 -21
- package/dist/edge-styles.d.ts +1 -1
- package/dist/edge-styles.js +1 -1
- package/dist/filename.js +1 -1
- package/dist/import.js +36 -12
- package/dist/interchange-headless.d.ts +13 -0
- package/dist/interchange-headless.js +13 -0
- package/dist/interchange.js +1 -1
- package/dist/keywords.js +3 -1
- package/dist/model.d.ts +8 -1
- package/dist/model.js +10 -1
- package/dist/plantuml-import.d.ts +10 -0
- package/dist/plantuml-import.js +39 -11
- package/dist/presets.d.ts +2 -1
- package/dist/presets.js +1 -1
- package/dist/translations.js +6 -2
- package/dist/xmi-import.d.ts +12 -0
- package/dist/xmi-import.js +61 -22
- package/dist/xml-reader.d.ts +1 -157
- package/dist/xml-reader.js +3 -430
- package/package.json +7 -3
package/dist/import.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { connectorEndLabelBox, ConnectorMode, TextAlign, UML_FRAME_BAND_HEIGHT, UML_LIFELINE_HEAD, } from '@formicoidea/labre-core/model';
|
|
1
|
+
import { connectorEndLabelBox, ConnectorMode, TextAlign, UML_FRAME_BAND_HEIGHT, UML_LIFELINE_HEAD, } from '@formicoidea/labre-core/model/headless';
|
|
2
2
|
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
3
3
|
import { umlCompartmentBoxes, umlLabelRoleOf, } from './component.js';
|
|
4
4
|
import { UML_BODY_FONT_SIZE, UML_DIAGRAM_BOX, UML_EDGE_WIDTH, UML_FRAGMENT_BOX, UML_INK, UML_NAME_FONT_SIZE, UML_NODE_BOX, UML_PARTITION_BOX, UML_REGION_BOX, UML_SUBJECT_BOX, } from './consts.js';
|
|
@@ -184,8 +184,10 @@ export function umlSequenceFragment(columns, y0, y1) {
|
|
|
184
184
|
return { x: 0, y: y0, w: UML_FRAGMENT_BOX.w, h: Math.max(1, y1 - y0) };
|
|
185
185
|
}
|
|
186
186
|
const centres = columns.map(column => column.x + column.w / 2);
|
|
187
|
-
const left = Math.min(
|
|
188
|
-
|
|
187
|
+
const left = centres.reduce((a, b) => Math.min(a, b), Infinity) -
|
|
188
|
+
UML_SD_FRAGMENT_PADDING;
|
|
189
|
+
const right = centres.reduce((a, b) => Math.max(a, b), -Infinity) +
|
|
190
|
+
UML_SD_FRAGMENT_PADDING;
|
|
189
191
|
return { x: left, y: y0, w: right - left, h: Math.max(1, y1 - y0) };
|
|
190
192
|
}
|
|
191
193
|
/**
|
|
@@ -237,8 +239,12 @@ export function umlInventLayout(nodes, relations = []) {
|
|
|
237
239
|
const inner = children.map(measure);
|
|
238
240
|
const columns = Math.ceil(Math.sqrt(children.length));
|
|
239
241
|
const rows = Math.ceil(children.length / columns);
|
|
240
|
-
const cellW =
|
|
241
|
-
|
|
242
|
+
const cellW = inner
|
|
243
|
+
.map(box => box.w)
|
|
244
|
+
.reduce((a, b) => Math.max(a, b), -Infinity);
|
|
245
|
+
const cellH = inner
|
|
246
|
+
.map(box => box.h)
|
|
247
|
+
.reduce((a, b) => Math.max(a, b), -Infinity);
|
|
242
248
|
const size = {
|
|
243
249
|
w: Math.max(node.size.w, UML_IMPORT_CONTAINER_INSET * 2 +
|
|
244
250
|
columns * cellW +
|
|
@@ -283,8 +289,12 @@ export function umlInventLayout(nodes, relations = []) {
|
|
|
283
289
|
if (children.length === 0)
|
|
284
290
|
return;
|
|
285
291
|
const columns = Math.ceil(Math.sqrt(children.length));
|
|
286
|
-
const cellW =
|
|
287
|
-
|
|
292
|
+
const cellW = children
|
|
293
|
+
.map(child => (measured.get(child.id) ?? child.size).w)
|
|
294
|
+
.reduce((a, b) => Math.max(a, b), -Infinity);
|
|
295
|
+
const cellH = children
|
|
296
|
+
.map(child => (measured.get(child.id) ?? child.size).h)
|
|
297
|
+
.reduce((a, b) => Math.max(a, b), -Infinity);
|
|
288
298
|
children.forEach((child, index) => {
|
|
289
299
|
const column = index % columns;
|
|
290
300
|
const row = Math.floor(index / columns);
|
|
@@ -899,10 +909,22 @@ export function umlElementsFromModel(models, options) {
|
|
|
899
909
|
const drawn = drafts
|
|
900
910
|
.map(draft => draft.bounds)
|
|
901
911
|
.filter((box) => box !== undefined);
|
|
902
|
-
const left = drawn.length > 0
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
const
|
|
912
|
+
const left = drawn.length > 0
|
|
913
|
+
? drawn.map(box => box.x).reduce((a, b) => Math.min(a, b), Infinity)
|
|
914
|
+
: 0;
|
|
915
|
+
const top = drawn.length > 0
|
|
916
|
+
? drawn.map(box => box.y).reduce((a, b) => Math.min(a, b), Infinity)
|
|
917
|
+
: 0;
|
|
918
|
+
const right = drawn.length > 0
|
|
919
|
+
? drawn
|
|
920
|
+
.map(box => box.x + box.w)
|
|
921
|
+
.reduce((a, b) => Math.max(a, b), -Infinity)
|
|
922
|
+
: 0;
|
|
923
|
+
const bottom = drawn.length > 0
|
|
924
|
+
? drawn
|
|
925
|
+
.map(box => box.y + box.h)
|
|
926
|
+
.reduce((a, b) => Math.max(a, b), -Infinity)
|
|
927
|
+
: 0;
|
|
906
928
|
const frame = {
|
|
907
929
|
x: origin.x,
|
|
908
930
|
y: frameTop,
|
|
@@ -1182,7 +1204,9 @@ function placeDrafts(drafts, model, layout) {
|
|
|
1182
1204
|
.map(draft => draft.bounds)
|
|
1183
1205
|
.filter((box) => box !== undefined);
|
|
1184
1206
|
const below = placed.length > 0
|
|
1185
|
-
?
|
|
1207
|
+
? placed
|
|
1208
|
+
.map(box => box.y + box.h)
|
|
1209
|
+
.reduce((a, b) => Math.max(a, b), -Infinity) + UML_IMPORT_GUTTER
|
|
1186
1210
|
: 0;
|
|
1187
1211
|
// Containment, so the invented layout draws the nesting the file stated:
|
|
1188
1212
|
// a class in its package, a case in its subject, an action in its lane, a
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@labre/affine-gfx-uml/interchange` — UML's whole interchange, with no editor
|
|
3
|
+
* behind it (ADR 0012, P3 as amended 2026-10-14).
|
|
4
|
+
*
|
|
5
|
+
* Every UML capability is pure on plain data — the exporters read structural
|
|
6
|
+
* records (`umlModelsFrom` picks by `type`, never by `instanceof`) — so this
|
|
7
|
+
* entry is the registry list itself, plus `decodeDrawio`, the container step a
|
|
8
|
+
* server runs before `uml:drawio:import` (capped, `DRAWIO_MAX_INFLATED_BYTES`).
|
|
9
|
+
*
|
|
10
|
+
* Held by `interchange-headless.unit.spec.ts` in `packages/affine/all`.
|
|
11
|
+
*/
|
|
12
|
+
export * from './drawio-decode.js';
|
|
13
|
+
export * from './interchange.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@labre/affine-gfx-uml/interchange` — UML's whole interchange, with no editor
|
|
3
|
+
* behind it (ADR 0012, P3 as amended 2026-10-14).
|
|
4
|
+
*
|
|
5
|
+
* Every UML capability is pure on plain data — the exporters read structural
|
|
6
|
+
* records (`umlModelsFrom` picks by `type`, never by `instanceof`) — so this
|
|
7
|
+
* entry is the registry list itself, plus `decodeDrawio`, the container step a
|
|
8
|
+
* server runs before `uml:drawio:import` (capped, `DRAWIO_MAX_INFLATED_BYTES`).
|
|
9
|
+
*
|
|
10
|
+
* Held by `interchange-headless.unit.spec.ts` in `packages/affine/all`.
|
|
11
|
+
*/
|
|
12
|
+
export * from './drawio-decode.js';
|
|
13
|
+
export * from './interchange.js';
|
package/dist/interchange.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { interchangeCapabilityId } from '@formicoidea/labre-core/
|
|
1
|
+
import { interchangeCapabilityId } from '@formicoidea/labre-core/interchange';
|
|
2
2
|
import { importDrawio, UML_DRAWIO_FORMAT_ID } from './drawio-import.js';
|
|
3
3
|
import { exportUmlPlantuml, exportUmlXmi } from './export.js';
|
|
4
4
|
import { umlSafeFilename } from './filename.js';
|
package/dist/keywords.js
CHANGED
|
@@ -297,7 +297,9 @@ export const UML_SLOTS_SEED_KEY = 'com.labre.uml.seed.slots';
|
|
|
297
297
|
* tool that predates a unicode-safe pipeline writes, and a class pasted in from
|
|
298
298
|
* one would otherwise export as a class named `<<interface>> Foo`.
|
|
299
299
|
*/
|
|
300
|
-
|
|
300
|
+
// Bounded, as `STEREOTYPE` in `plantuml-import.ts` is and for its reason:
|
|
301
|
+
// `[^>]*` re-scanned the line from every `<<` that never closed.
|
|
302
|
+
const GUILLEMET_GROUP = /«([^»]{0,256})»|<<([^>]{0,256})>>/g;
|
|
301
303
|
/** `{abstract}`, with whatever spacing and case the author used (§9.2.4). */
|
|
302
304
|
const ABSTRACT_MARKER = /\{\s*abstract\s*\}/i;
|
|
303
305
|
/**
|
package/dist/model.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { UmlDiagramKind, UmlFragmentOperand, UmlFragmentOperator, UmlNodeKind } from '@formicoidea/labre-core/model';
|
|
2
2
|
import { type UmlBox, type UmlComponent, type UmlComponentElement, type UmlComponentGroup } from './component.js';
|
|
3
3
|
import { type UmlAssociationEnd, type UmlOperation, type UmlProperty } from './grammar.js';
|
|
4
4
|
/**
|
|
@@ -595,6 +595,13 @@ export interface UmlModel {
|
|
|
595
595
|
*/
|
|
596
596
|
warnings: string[];
|
|
597
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* The relationship a connector ROLE states — `uml:generalization` → `'generalization'`
|
|
600
|
+
* — or `undefined` for a role that states none. The one reading of
|
|
601
|
+
* {@link RELATION_ROLE} backwards, exported for the draw.io reader, which meets
|
|
602
|
+
* the same roles as `labre-role` attributes on a drawing the canvas wrote.
|
|
603
|
+
*/
|
|
604
|
+
export declare function umlRelationKindOfRole(role: string | undefined): UmlRelationKind | undefined;
|
|
598
605
|
/**
|
|
599
606
|
* An element's box, from whichever of the three spellings it carries.
|
|
600
607
|
*
|
package/dist/model.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UML_DIAGRAM_KIND_TAG, UML_FRAGMENT_BAND, UML_FRAME_BAND_HEIGHT, UML_LIFELINE_HEAD, } from '@formicoidea/labre-core/model';
|
|
1
|
+
import { UML_DIAGRAM_KIND_TAG, UML_FRAGMENT_BAND, UML_FRAME_BAND_HEIGHT, UML_LIFELINE_HEAD, } from '@formicoidea/labre-core/model/headless';
|
|
2
2
|
import { umlComponentSiblings, umlGroupOf, umlTierText, } from './component.js';
|
|
3
3
|
import { parseActivityEdge, parseCompartment, parseEndLabel, parseLifelineIdent, parseOperation, parseProperty, parseStateBehavior, parseTransition, } from './grammar.js';
|
|
4
4
|
import { stereotypesOf } from './keywords.js';
|
|
@@ -60,6 +60,15 @@ const RELATION_OF_ROLE = new Map(Object.entries(RELATION_ROLE).map(([kind, role]
|
|
|
60
60
|
role,
|
|
61
61
|
kind,
|
|
62
62
|
]));
|
|
63
|
+
/**
|
|
64
|
+
* The relationship a connector ROLE states — `uml:generalization` → `'generalization'`
|
|
65
|
+
* — or `undefined` for a role that states none. The one reading of
|
|
66
|
+
* {@link RELATION_ROLE} backwards, exported for the draw.io reader, which meets
|
|
67
|
+
* the same roles as `labre-role` attributes on a drawing the canvas wrote.
|
|
68
|
+
*/
|
|
69
|
+
export function umlRelationKindOfRole(role) {
|
|
70
|
+
return role === undefined ? undefined : RELATION_OF_ROLE.get(role);
|
|
71
|
+
}
|
|
63
72
|
/* ── Geometry ─────────────────────────────────────────────────────────── */
|
|
64
73
|
/**
|
|
65
74
|
* An element's box, from whichever of the three spellings it carries.
|
|
@@ -94,6 +94,16 @@ export interface UmlPlantumlImport {
|
|
|
94
94
|
* They are contributed to the manifest by `./translations.ts`, with the
|
|
95
95
|
* framework, because they ship in the UML bundle rather than in core.
|
|
96
96
|
*/
|
|
97
|
+
/**
|
|
98
|
+
* The most PlantUML source the reader takes: 1 MiB, the size of a diagram
|
|
99
|
+
* nobody types, and the bound on what the line regexes below can be handed
|
|
100
|
+
* (the server's budget: 256 MiB, two seconds).
|
|
101
|
+
*/
|
|
102
|
+
export declare const PLANTUML_MAX_CHARACTERS: number;
|
|
103
|
+
/** The reader's refusal, static so a server can hand the key on (ADR 0023). */
|
|
104
|
+
export declare const UML_PLANTUML_ERRORS: {
|
|
105
|
+
readonly tooLarge: readonly ["com.labre.uml.import.plantuml.too-large", "This PlantUML file is longer than an import accepts, so it was not read."];
|
|
106
|
+
};
|
|
97
107
|
export declare const UML_PLANTUML_REMARKS: {
|
|
98
108
|
readonly relationEnd: readonly ["com.labre.uml.import.plantuml.relation-end", "The {{side}} end of the {{kind}} between \"{{left}}\" and \"{{right}}\" is written \"{{raw}}\". It is drawn on the board, but UML gives that relationship's ends no multiplicity, so no export writes it."];
|
|
99
109
|
readonly duplicateDeclaration: readonly ["com.labre.uml.import.plantuml.duplicate-declaration", "This document declares \"{{name}}\" twice. The second declaration was read as a separate artefact under an id of Labre's own."];
|
package/dist/plantuml-import.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { InterchangeImportError } from '@formicoidea/labre-core/interchange';
|
|
2
|
+
import { UML_DIAGRAM_KIND_TAG } from '@formicoidea/labre-core/model/headless';
|
|
2
3
|
import { UML_FRAGMENT_BOX, UML_NODE_BOX, UML_REGION_BOX, UML_SUBJECT_BOX, } from './consts.js';
|
|
3
4
|
import { parseEndLabel, parseLifelineIdent, parseOperation, parseProperty, parseTransition, } from './grammar.js';
|
|
4
5
|
import { UML_SD_COLUMN_GAP, UML_SD_EVENT_STEP, UML_SD_FRAGMENT_PADDING, umlInventLayout, umlSequenceColumn, umlSequenceDestruction, umlSequenceExecution, umlSequenceFragment, umlSequenceSlot, } from './import.js';
|
|
@@ -33,6 +34,19 @@ import { ADORNED_RELATION_KINDS } from './model.js';
|
|
|
33
34
|
* They are contributed to the manifest by `./translations.ts`, with the
|
|
34
35
|
* framework, because they ship in the UML bundle rather than in core.
|
|
35
36
|
*/
|
|
37
|
+
/**
|
|
38
|
+
* The most PlantUML source the reader takes: 1 MiB, the size of a diagram
|
|
39
|
+
* nobody types, and the bound on what the line regexes below can be handed
|
|
40
|
+
* (the server's budget: 256 MiB, two seconds).
|
|
41
|
+
*/
|
|
42
|
+
export const PLANTUML_MAX_CHARACTERS = 1024 * 1024;
|
|
43
|
+
/** The reader's refusal, static so a server can hand the key on (ADR 0023). */
|
|
44
|
+
export const UML_PLANTUML_ERRORS = {
|
|
45
|
+
tooLarge: [
|
|
46
|
+
'com.labre.uml.import.plantuml.too-large',
|
|
47
|
+
'This PlantUML file is longer than an import accepts, so it was not read.',
|
|
48
|
+
],
|
|
49
|
+
};
|
|
36
50
|
export const UML_PLANTUML_REMARKS = {
|
|
37
51
|
relationEnd: [
|
|
38
52
|
'com.labre.uml.import.plantuml.relation-end',
|
|
@@ -82,7 +96,7 @@ const KIND_OF_TAG = new Map(Object.entries(UML_DIAGRAM_KIND_TAG).map(([kind, tag
|
|
|
82
96
|
* boundary box — because that is the only thing this pack draws round a group of
|
|
83
97
|
* use cases, and it is what our own writer emits for one.
|
|
84
98
|
*/
|
|
85
|
-
const DECLARATION_KIND = {
|
|
99
|
+
const DECLARATION_KIND = Object.assign(Object.create(null), {
|
|
86
100
|
class: 'class',
|
|
87
101
|
entity: 'class',
|
|
88
102
|
interface: 'interface',
|
|
@@ -100,7 +114,7 @@ const DECLARATION_KIND = {
|
|
|
100
114
|
artifact: 'artifact',
|
|
101
115
|
node: 'node',
|
|
102
116
|
state: 'state',
|
|
103
|
-
};
|
|
117
|
+
});
|
|
104
118
|
/** The declaration words, as the alternation the line matcher is built on. */
|
|
105
119
|
const DECLARATION_WORDS = [
|
|
106
120
|
'abstract class',
|
|
@@ -117,7 +131,9 @@ const DECLARATION_WORDS = [
|
|
|
117
131
|
* (a terminate was borrowing the stop). Reading them the other way would turn a
|
|
118
132
|
* diagram somebody drew into a different one on the way home.
|
|
119
133
|
*/
|
|
120
|
-
|
|
134
|
+
// No prototype, like every table here read by a word the FILE wrote:
|
|
135
|
+
// `<<constructor>>` must read as a stereotype, not as `Object`.
|
|
136
|
+
const STATE_KIND_OF_STEREOTYPE = Object.assign(Object.create(null), {
|
|
121
137
|
start: 'initial',
|
|
122
138
|
end: 'final-state',
|
|
123
139
|
choice: 'choice',
|
|
@@ -128,7 +144,7 @@ const STATE_KIND_OF_STEREOTYPE = {
|
|
|
128
144
|
entrypoint: 'entry-point',
|
|
129
145
|
exitpoint: 'exit-point',
|
|
130
146
|
terminate: 'terminate',
|
|
131
|
-
};
|
|
147
|
+
});
|
|
132
148
|
/** The vertices §14.2.4.6 files under `Pseudostate`, as a set. */
|
|
133
149
|
const PSEUDOSTATE_KINDS = new Set([
|
|
134
150
|
'initial',
|
|
@@ -184,18 +200,25 @@ const OPERATORS = [
|
|
|
184
200
|
* role, and reading it as prose would import an include as an untyped dependency
|
|
185
201
|
* carrying a stereotype.
|
|
186
202
|
*/
|
|
187
|
-
const KEYWORD_ROLE = {
|
|
203
|
+
const KEYWORD_ROLE = Object.assign(Object.create(null), {
|
|
188
204
|
include: 'include',
|
|
189
205
|
extend: 'extend',
|
|
190
206
|
deploy: 'deploy',
|
|
191
207
|
manifest: 'manifest',
|
|
192
208
|
use: 'dependency',
|
|
193
|
-
};
|
|
209
|
+
});
|
|
194
210
|
/** PlantUML's contextual beginning-and-end marker (`plantuml.ts`). */
|
|
195
211
|
const TERMINAL = '[*]';
|
|
196
212
|
/* ── Reading a line ───────────────────────────────────────────────────── */
|
|
197
|
-
/**
|
|
198
|
-
|
|
213
|
+
/**
|
|
214
|
+
* A `<<stereo>>` / `«stereo»` group, wherever it was written.
|
|
215
|
+
*
|
|
216
|
+
* Bounded like draw.io's `ASCII_GUILLEMETS`: an unbounded `[^>]*` re-scanned
|
|
217
|
+
* the rest of the line from every `<<` that never closed, 1.8 s for a 40 KiB
|
|
218
|
+
* `class A <<<<…` and four times that per doubling (counter-review of
|
|
219
|
+
* 49aa247ce). No stereotype is 256 characters long.
|
|
220
|
+
*/
|
|
221
|
+
const STEREOTYPE = /<<([^>]{0,256})>>|«([^»]{0,256})»/g;
|
|
199
222
|
/** Direction hints inside an arrow — `-down->`, `-up->`: layout, not model. */
|
|
200
223
|
const DIRECTION_HINT = /-(?:left|right|up|down|le|ri|do)-/gi;
|
|
201
224
|
/** A colour or a style tail — `#red`, `#line:blue;line.dashed`. */
|
|
@@ -1020,6 +1043,11 @@ function layOut(model, contains) {
|
|
|
1020
1043
|
* the same source always produces the same models, down to the coordinates.
|
|
1021
1044
|
*/
|
|
1022
1045
|
export function importPlantuml(source) {
|
|
1046
|
+
if (source.length > PLANTUML_MAX_CHARACTERS) {
|
|
1047
|
+
throw new InterchangeImportError(UML_PLANTUML_ERRORS.tooLarge[1], {
|
|
1048
|
+
messageKey: UML_PLANTUML_ERRORS.tooLarge[0],
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1023
1051
|
const notes = [];
|
|
1024
1052
|
const note = (entry) => notes.push(entry);
|
|
1025
1053
|
const lines = source.replaceAll('\r\n', '\n').split('\n');
|
|
@@ -1125,7 +1153,7 @@ function sequenceArrow(token) {
|
|
|
1125
1153
|
};
|
|
1126
1154
|
}
|
|
1127
1155
|
/** §17.6.4's operators, read back off the block keyword that opened them. */
|
|
1128
|
-
const FRAGMENT_OPERATOR_OF_WORD = {
|
|
1156
|
+
const FRAGMENT_OPERATOR_OF_WORD = Object.assign(Object.create(null), {
|
|
1129
1157
|
alt: 'alt',
|
|
1130
1158
|
opt: 'opt',
|
|
1131
1159
|
loop: 'loop',
|
|
@@ -1138,7 +1166,7 @@ const FRAGMENT_OPERATOR_OF_WORD = {
|
|
|
1138
1166
|
assert: 'assert',
|
|
1139
1167
|
ignore: 'ignore',
|
|
1140
1168
|
consider: 'consider',
|
|
1141
|
-
};
|
|
1169
|
+
});
|
|
1142
1170
|
/**
|
|
1143
1171
|
* The declarations that can only be a CLASS diagram — §9's classifiers and
|
|
1144
1172
|
* §12's package.
|
package/dist/presets.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { TextAlign, TextDecoration
|
|
1
|
+
import { TextAlign, TextDecoration } from '@formicoidea/labre-core/model/headless';
|
|
2
|
+
import type { UmlNodeKind } from '@formicoidea/labre-core/model';
|
|
2
3
|
import type { UmlBox } from './component.js';
|
|
3
4
|
/**
|
|
4
5
|
* What a UML shape IS, as props — the ONE description the creation site, the
|
package/dist/presets.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FontFamily, FontStyle, FontWeight, ShapeStyle, StrokeStyle, TextAlign, TextDecoration, } from '@formicoidea/labre-core/model';
|
|
1
|
+
import { FontFamily, FontStyle, FontWeight, ShapeStyle, StrokeStyle, TextAlign, TextDecoration, } from '@formicoidea/labre-core/model/headless';
|
|
2
2
|
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
3
3
|
import { UML_CARD, UML_INK, UML_NAME_FONT_SIZE, UML_NODE_BOX, UML_NODE_RADIUS, UML_NODE_STROKE_WIDTH, } from './consts.js';
|
|
4
4
|
import { UML_ROLE_OF_KIND } from './roles.js';
|
package/dist/translations.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { collectTranslationKeys, commandCategoryTranslationEntries, commandTranslationEntries, mergeTranslationEntries, } from '@formicoidea/labre-core/std';
|
|
2
2
|
import { UML_DIAGRAM_FRAME, UML_SUBJECT_FRAME } from './background.js';
|
|
3
3
|
import { umlCommands } from './commands.js';
|
|
4
|
+
import { UML_DRAWIO_DECODE_ERRORS } from './drawio-decode.js';
|
|
4
5
|
import { UML_DRAWIO_REMARKS } from './drawio-import.js';
|
|
5
6
|
import { UML_LAYOUT_REMARKS } from './import.js';
|
|
6
7
|
import { UML_ATTRIBUTES_SEED, UML_ATTRIBUTES_SEED_KEY, UML_NAME_SEED, UML_OPERATIONS_SEED, UML_OPERATIONS_SEED_KEY, UML_SLOTS_SEED, UML_SLOTS_SEED_KEY, umlSeedKey, } from './keywords.js';
|
|
7
8
|
import { UML_DIAGRAM_KIND_MENU } from './kinds.js';
|
|
8
9
|
import { UML_LEGEND_SECTION_WORDINGS } from './legend.js';
|
|
9
10
|
import { UML_FRAGMENT_OPERATOR_MENU } from './operators.js';
|
|
10
|
-
import { UML_PLANTUML_REMARKS } from './plantuml-import.js';
|
|
11
|
+
import { UML_PLANTUML_ERRORS, UML_PLANTUML_REMARKS, } from './plantuml-import.js';
|
|
11
12
|
import { UML_PROFILES } from './profiles.js';
|
|
12
13
|
import { UML_READINGS } from './reading.js';
|
|
13
14
|
import { UML_ROLES } from './roles.js';
|
|
14
15
|
import { UML_RULES } from './rules.js';
|
|
15
|
-
import { UML_XMI_REMARKS } from './xmi-import.js';
|
|
16
|
+
import { UML_XMI_ERRORS, UML_XMI_REMARKS } from './xmi-import.js';
|
|
16
17
|
/**
|
|
17
18
|
* The words a placed artefact is SEEDED with — its name compartment, and the
|
|
18
19
|
* three compartment lines a fresh classifier carries.
|
|
@@ -57,6 +58,9 @@ const chromeEntries = () => [
|
|
|
57
58
|
...Object.values(UML_PLANTUML_REMARKS),
|
|
58
59
|
...Object.values(UML_XMI_REMARKS),
|
|
59
60
|
...Object.values(UML_DRAWIO_REMARKS),
|
|
61
|
+
...Object.values(UML_DRAWIO_DECODE_ERRORS),
|
|
62
|
+
...Object.values(UML_PLANTUML_ERRORS),
|
|
63
|
+
...Object.values(UML_XMI_ERRORS),
|
|
60
64
|
...Object.values(UML_LAYOUT_REMARKS),
|
|
61
65
|
].map(([key, fallback]) => ({ key, fallback, source: 'chrome' }));
|
|
62
66
|
/**
|
package/dist/xmi-import.d.ts
CHANGED
|
@@ -131,6 +131,18 @@ export declare const UML_XMI_QUARANTINE_REASON: {
|
|
|
131
131
|
* cannot become a `[key, english]` pair without breaking that contract. BPMN
|
|
132
132
|
* carries the identical constraint on its own quarantine reasons.
|
|
133
133
|
*/
|
|
134
|
+
/**
|
|
135
|
+
* The deepest an XMI file may nest: 1 000 levels. The reader walks packages,
|
|
136
|
+
* consumed subtrees and the quarantine sweep by recursion, and 5 000 nested
|
|
137
|
+
* packages overflowed the stack with a bare `RangeError` (counter-review of
|
|
138
|
+
* 49aa247ce). A real model nests a few dozen levels; one cap at the door bounds
|
|
139
|
+
* every recursion below it.
|
|
140
|
+
*/
|
|
141
|
+
export declare const XMI_MAX_DEPTH = 1000;
|
|
142
|
+
/** The reader's refusal, static so a server can hand the key on (ADR 0023). */
|
|
143
|
+
export declare const UML_XMI_ERRORS: {
|
|
144
|
+
readonly tooDeep: readonly ["com.labre.uml.import.xmi.too-deep", "This XMI file nests its elements deeper than an import accepts, so it was not read."];
|
|
145
|
+
};
|
|
134
146
|
export declare const UML_XMI_REMARKS: {
|
|
135
147
|
readonly mintedId: readonly ["com.labre.uml.import.xmi.minted-id", "<{{tag}}> carries no xmi:id, so one was minted for it. Nothing in the file can refer to it, and nothing does."];
|
|
136
148
|
readonly unlistedOperator: readonly ["com.labre.uml.import.xmi.unlisted-operator", "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."];
|
package/dist/xmi-import.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { InterchangeImportError } from '@formicoidea/labre-core/interchange';
|
|
1
2
|
import { UML_SD_EVENT_STEP, umlSequenceColumn, umlSequenceDestruction, umlSequenceExecution, umlSequenceFragment, umlSequenceSlot, } from './import.js';
|
|
2
3
|
import { formatEndLabel, } from './grammar.js';
|
|
3
4
|
import { readXml, xmlAttr, xmlChild, xmlChildren, xmlDescendants, xmlFragmentOf, } from './xml-reader.js';
|
|
@@ -14,7 +15,8 @@ import { readXml, xmlAttr, xmlChild, xmlChildren, xmlDescendants, xmlFragmentOf,
|
|
|
14
15
|
*/
|
|
15
16
|
const META_PREFIXES = new Set(['uml', 'UML', 'Uml']);
|
|
16
17
|
/** The classifier metaclasses, and the {@link UmlClassifier.kind} each is. */
|
|
17
|
-
|
|
18
|
+
// Tables read by a name the FILE wrote have no prototype (`<constructor>`).
|
|
19
|
+
const CLASSIFIER_KIND = Object.assign(Object.create(null), {
|
|
18
20
|
Class: 'class',
|
|
19
21
|
Interface: 'interface',
|
|
20
22
|
Enumeration: 'enumeration',
|
|
@@ -25,20 +27,20 @@ const CLASSIFIER_KIND = {
|
|
|
25
27
|
// file said.
|
|
26
28
|
DataType: 'class',
|
|
27
29
|
PrimitiveType: 'class',
|
|
28
|
-
};
|
|
30
|
+
});
|
|
29
31
|
/** The metaclasses {@link CLASSIFIER_KIND} maps that need their keyword said. */
|
|
30
|
-
const CLASSIFIER_KEYWORD = {
|
|
32
|
+
const CLASSIFIER_KEYWORD = Object.assign(Object.create(null), {
|
|
31
33
|
DataType: 'datatype',
|
|
32
34
|
PrimitiveType: 'primitive',
|
|
33
|
-
};
|
|
35
|
+
});
|
|
34
36
|
/** The three cubes of §19.4.2, and the one `kind` they differ by. */
|
|
35
|
-
const DEPLOYMENT_KIND = {
|
|
37
|
+
const DEPLOYMENT_KIND = Object.assign(Object.create(null), {
|
|
36
38
|
Node: 'node',
|
|
37
39
|
Device: 'device',
|
|
38
40
|
ExecutionEnvironment: 'execution-environment',
|
|
39
|
-
};
|
|
41
|
+
});
|
|
40
42
|
/** The inverse of `xmi.ts`'s `ACTIVITY_NODE_TYPE`. */
|
|
41
|
-
const ACTIVITY_NODE_KIND = {
|
|
43
|
+
const ACTIVITY_NODE_KIND = Object.assign(Object.create(null), {
|
|
42
44
|
OpaqueAction: 'action',
|
|
43
45
|
Action: 'action',
|
|
44
46
|
CallBehaviorAction: 'action',
|
|
@@ -58,9 +60,9 @@ const ACTIVITY_NODE_KIND = {
|
|
|
58
60
|
// whose trigger names a TimeEvent, so the two are told apart by the EVENT and
|
|
59
61
|
// not by the metaclass. See {@link readActivity}.
|
|
60
62
|
AcceptEventAction: 'accept-event',
|
|
61
|
-
};
|
|
63
|
+
});
|
|
62
64
|
/** The inverse of `xmi.ts`'s `PSEUDOSTATE_KIND` (§14.5.7's enumeration). */
|
|
63
|
-
const PSEUDOSTATE_OF_KIND = {
|
|
65
|
+
const PSEUDOSTATE_OF_KIND = Object.assign(Object.create(null), {
|
|
64
66
|
initial: 'initial',
|
|
65
67
|
choice: 'choice',
|
|
66
68
|
junction: 'junction',
|
|
@@ -71,16 +73,16 @@ const PSEUDOSTATE_OF_KIND = {
|
|
|
71
73
|
terminate: 'terminate',
|
|
72
74
|
fork: 'fork',
|
|
73
75
|
join: 'fork',
|
|
74
|
-
};
|
|
76
|
+
});
|
|
75
77
|
/** The dependency metaclasses, and the relation each states. */
|
|
76
|
-
const DEPENDENCY_KIND = {
|
|
78
|
+
const DEPENDENCY_KIND = Object.assign(Object.create(null), {
|
|
77
79
|
Dependency: 'dependency',
|
|
78
80
|
Usage: 'dependency',
|
|
79
81
|
Abstraction: 'dependency',
|
|
80
82
|
Realization: 'realization',
|
|
81
83
|
Deployment: 'deploy',
|
|
82
84
|
Manifestation: 'manifest',
|
|
83
|
-
};
|
|
85
|
+
});
|
|
84
86
|
/** What Labre stamps on the XMI extensions it writes — `xmi.ts`'s `EXTENDER`. */
|
|
85
87
|
const LABRE_EXTENDER = 'labre';
|
|
86
88
|
/** The four VisibilityKinds, as §7.4 spells them in a file. */
|
|
@@ -109,6 +111,33 @@ export const UML_XMI_QUARANTINE_REASON = {
|
|
|
109
111
|
* cannot become a `[key, english]` pair without breaking that contract. BPMN
|
|
110
112
|
* carries the identical constraint on its own quarantine reasons.
|
|
111
113
|
*/
|
|
114
|
+
/**
|
|
115
|
+
* The deepest an XMI file may nest: 1 000 levels. The reader walks packages,
|
|
116
|
+
* consumed subtrees and the quarantine sweep by recursion, and 5 000 nested
|
|
117
|
+
* packages overflowed the stack with a bare `RangeError` (counter-review of
|
|
118
|
+
* 49aa247ce). A real model nests a few dozen levels; one cap at the door bounds
|
|
119
|
+
* every recursion below it.
|
|
120
|
+
*/
|
|
121
|
+
export const XMI_MAX_DEPTH = 1_000;
|
|
122
|
+
/** The reader's refusal, static so a server can hand the key on (ADR 0023). */
|
|
123
|
+
export const UML_XMI_ERRORS = {
|
|
124
|
+
tooDeep: [
|
|
125
|
+
'com.labre.uml.import.xmi.too-deep',
|
|
126
|
+
'This XMI file nests its elements deeper than an import accepts, so it was not read.',
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
/** The depth of the deepest element, walked with a stack. */
|
|
130
|
+
function depthOf(roots) {
|
|
131
|
+
let deepest = 0;
|
|
132
|
+
const stack = roots.map(node => ({ node, depth: 1 }));
|
|
133
|
+
while (stack.length > 0) {
|
|
134
|
+
const { node, depth } = stack.pop();
|
|
135
|
+
deepest = Math.max(deepest, depth);
|
|
136
|
+
for (const child of node.children)
|
|
137
|
+
stack.push({ node: child, depth: depth + 1 });
|
|
138
|
+
}
|
|
139
|
+
return deepest;
|
|
140
|
+
}
|
|
112
141
|
export const UML_XMI_REMARKS = {
|
|
113
142
|
mintedId: [
|
|
114
143
|
'com.labre.uml.import.xmi.minted-id',
|
|
@@ -233,16 +262,17 @@ function foreignOf(ctx, scope) {
|
|
|
233
262
|
/** One attribute, kept under the scope of the element that carried it. */
|
|
234
263
|
function carryAttr(ctx, owner, scope, name, value) {
|
|
235
264
|
const payload = foreignOf(ctx, owner);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
265
|
+
// In place, in bags with no prototype: a copy per attribute was quadratic,
|
|
266
|
+
// and the keys are the file's (BPMN's keep-helpers, same review).
|
|
267
|
+
const attrs = (payload.attrs ??= Object.create(null));
|
|
268
|
+
(attrs[scope] ??= Object.create(null))[name] =
|
|
269
|
+
value;
|
|
240
270
|
ctx.counts.carried += 1;
|
|
241
271
|
}
|
|
242
272
|
/** One fragment, kept and declared un-re-emittable (D5). */
|
|
243
273
|
function quarantine(ctx, owner, fragment, reason, entry) {
|
|
244
274
|
const payload = foreignOf(ctx, owner);
|
|
245
|
-
|
|
275
|
+
(payload.quarantined ??= []).push({ fragment, reason });
|
|
246
276
|
ctx.counts.quarantined += 1;
|
|
247
277
|
note(ctx, { kind: 'quarantined', ...entry, message: reason });
|
|
248
278
|
}
|
|
@@ -293,7 +323,9 @@ function map(ctx, node) {
|
|
|
293
323
|
* model everywhere in this format, and anything else on an element Labre draws
|
|
294
324
|
* is something Labre does not model and must not lose.
|
|
295
325
|
*/
|
|
296
|
-
|
|
326
|
+
// No prototype: read by a local name the file chose (`<constructor>`), the
|
|
327
|
+
// BPMN reader's TypeError (security review, 2026-10-14).
|
|
328
|
+
const READ_ATTRS = Object.assign(Object.create(null), {
|
|
297
329
|
packagedElement: [
|
|
298
330
|
'xmi:type',
|
|
299
331
|
'type',
|
|
@@ -339,7 +371,7 @@ const READ_ATTRS = {
|
|
|
339
371
|
'isQuery',
|
|
340
372
|
],
|
|
341
373
|
'': ['xmi:type', 'type', 'xmi:id', 'id', 'name'],
|
|
342
|
-
};
|
|
374
|
+
});
|
|
343
375
|
/** Everything on this element the reader did not read, kept verbatim. */
|
|
344
376
|
function carryUnknownAttrs(ctx, node, owner, scope = owner, understood = READ_ATTRS[node.local] ?? READ_ATTRS['']) {
|
|
345
377
|
for (const [name, value] of Object.entries(node.attrs)) {
|
|
@@ -1219,14 +1251,14 @@ function readStateMachine(ctx, draft, node, id) {
|
|
|
1219
1251
|
}
|
|
1220
1252
|
/* ── Interactions (§17) ───────────────────────────────────────────────── */
|
|
1221
1253
|
/** §17.4.3's `messageSort`, read back to the arrow §17.4.4 draws for it. */
|
|
1222
|
-
const MESSAGE_KIND_OF_SORT = {
|
|
1254
|
+
const MESSAGE_KIND_OF_SORT = Object.assign(Object.create(null), {
|
|
1223
1255
|
synchCall: 'message-sync',
|
|
1224
1256
|
asynchCall: 'message-async',
|
|
1225
1257
|
asynchSignal: 'message-async',
|
|
1226
1258
|
reply: 'message-reply',
|
|
1227
1259
|
createMessage: 'message-create',
|
|
1228
1260
|
deleteMessage: 'message-delete',
|
|
1229
|
-
};
|
|
1261
|
+
});
|
|
1230
1262
|
/** The metaclasses §17.2.4 draws as the bar on a spine, whatever the tool. */
|
|
1231
1263
|
const EXECUTION_TYPES = new Set([
|
|
1232
1264
|
'BehaviorExecutionSpecification',
|
|
@@ -1999,6 +2031,11 @@ function rootOf(document) {
|
|
|
1999
2031
|
*/
|
|
2000
2032
|
export function importXmi(source, context = {}) {
|
|
2001
2033
|
const document = readXml(source);
|
|
2034
|
+
if (depthOf(document.roots) > XMI_MAX_DEPTH) {
|
|
2035
|
+
throw new InterchangeImportError(UML_XMI_ERRORS.tooDeep[1], {
|
|
2036
|
+
messageKey: UML_XMI_ERRORS.tooDeep[0],
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2002
2039
|
// The three bags keyed by an id the FILE chose have no prototype: `__proto__`
|
|
2003
2040
|
// and `constructor` are ids a document is free to use, and on a plain object
|
|
2004
2041
|
// the first writes through to `Object.prototype` and the second reads a
|
|
@@ -2246,7 +2283,9 @@ export function umlElementsWithForeign(elements, foreign, formatId) {
|
|
|
2246
2283
|
// Flattened onto the frame's own payload rather than nested under a second
|
|
2247
2284
|
// key, because `ForeignInterchange` is one record per format and the SCOPES
|
|
2248
2285
|
// inside it are what keep two source elements apart.
|
|
2249
|
-
const attrs = residue.reduce((all, [, payload]) => (
|
|
2286
|
+
const attrs = residue.reduce((all, [, payload]) => Object.assign(all, payload.attrs),
|
|
2287
|
+
// No prototype: a scope is an id the file chose, `__proto__` included.
|
|
2288
|
+
Object.assign(Object.create(null), own.attrs));
|
|
2250
2289
|
const quarantined = [
|
|
2251
2290
|
...(own.quarantined ?? []),
|
|
2252
2291
|
...residue.flatMap(([, payload]) => payload.quarantined ?? []),
|