@formicoidea/labre-framework-uml 0.49.0 → 0.51.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/xml-reader.d.ts
CHANGED
|
@@ -1,157 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* A tolerant XML READER — the inverse of `xml.ts`, and the one thing an
|
|
3
|
-
* importer cannot do without.
|
|
4
|
-
*
|
|
5
|
-
* ## Why not `DOMParser`
|
|
6
|
-
*
|
|
7
|
-
* BPMN's reader calls it (`gfx/bpmn/src/import.ts`), and it is the right call
|
|
8
|
-
* there: that parser has always run in a browser. This one must not. An
|
|
9
|
-
* interchange parser is a PURE function of a string (`docs/adr/0012` P3) — the
|
|
10
|
-
* same function an editor command, a unit test and labre-mcp all call — and
|
|
11
|
-
* `DOMParser` is a DOM global that exists in exactly one of those three. A
|
|
12
|
-
* reader that needs `happy-dom` to be loaded is a reader with an environment in
|
|
13
|
-
* its signature.
|
|
14
|
-
*
|
|
15
|
-
* So: two hundred lines of scanner, and no global in sight.
|
|
16
|
-
*
|
|
17
|
-
* ## Tolerant, and what that costs
|
|
18
|
-
*
|
|
19
|
-
* **It never throws.** A file that stops mid-element, a close tag that names
|
|
20
|
-
* the wrong element, an attribute with no quotes, an entity nobody declared —
|
|
21
|
-
* every one of them produces a tree with a NOTE beside it rather than an
|
|
22
|
-
* exception. That is not sloppiness, it is the job: the files this reader is
|
|
23
|
-
* handed were written by Papyrus, StarUML, Enterprise Architect and a dozen
|
|
24
|
-
* scripts, and a user who exported a model from one of them is entitled to get
|
|
25
|
-
* back everything it said even when part of it is malformed. The alternative —
|
|
26
|
-
* one exception for one stray `&` — imports nothing at all.
|
|
27
|
-
*
|
|
28
|
-
* What it does NOT do is pretend to be a conformant parser. There is no DTD, no
|
|
29
|
-
* entity declaration, no namespace RESOLUTION (prefixes are kept exactly as the
|
|
30
|
-
* file spelled them — see {@link XmlNode.prefix}), no attribute-value
|
|
31
|
-
* normalization beyond the character references XML 1.0 §3.3.3 makes
|
|
32
|
-
* mandatory. Every one of those is a feature an XMI file in the wild does not
|
|
33
|
-
* use, and each would be a piece of surface with nothing behind it.
|
|
34
|
-
*
|
|
35
|
-
* ## Verbatim means the source's own bytes
|
|
36
|
-
*
|
|
37
|
-
* Every node records the byte RANGE it was read from ({@link XmlNode.range}),
|
|
38
|
-
* and {@link xmlFragmentOf} hands back that slice of the original string. So a
|
|
39
|
-
* quarantined fragment (ADR 0012 D5) is the file's own text — its comments, its
|
|
40
|
-
* whitespace, its attribute order, its prefixes — and not this reader's opinion
|
|
41
|
-
* of it. {@link serializeXmlNode} re-serializes from the tree instead, and is
|
|
42
|
-
* the fallback for a node assembled rather than read.
|
|
43
|
-
*
|
|
44
|
-
* Pure: a string in, a tree out. No `std`, no DOM, no clock.
|
|
45
|
-
*/
|
|
46
|
-
/**
|
|
47
|
-
* One element.
|
|
48
|
-
*
|
|
49
|
-
* `name` is the qualified name AS WRITTEN (`uml:Model`), with {@link prefix}
|
|
50
|
-
* and {@link local} split out beside it, because an importer asks both
|
|
51
|
-
* questions: the prefix is how a file says which vocabulary it means, and the
|
|
52
|
-
* local name is the only half two tools reliably agree on — Papyrus writes
|
|
53
|
-
* `uml:Class`, an older Enterprise Architect writes `UML:Class`, and a
|
|
54
|
-
* namespace-defaulted file writes `Class`.
|
|
55
|
-
*/
|
|
56
|
-
export interface XmlNode {
|
|
57
|
-
/** The qualified name, verbatim: `uml:Model`, `packagedElement`. */
|
|
58
|
-
name: string;
|
|
59
|
-
/** The prefix, `''` when the name carries none. */
|
|
60
|
-
prefix: string;
|
|
61
|
-
/** The local name — `Model`, `packagedElement`. */
|
|
62
|
-
local: string;
|
|
63
|
-
/** Attributes by qualified name, values entity-decoded, in document order. */
|
|
64
|
-
attrs: Record<string, string>;
|
|
65
|
-
children: XmlNode[];
|
|
66
|
-
/**
|
|
67
|
-
* The element's character data — text and CDATA, concatenated and decoded.
|
|
68
|
-
*
|
|
69
|
-
* Absent rather than `''` when the element holds nothing but whitespace and
|
|
70
|
-
* children, which is what every element of an indented XMI document holds.
|
|
71
|
-
* A `<body>` with prose in it has this; a `<packagedElement>` does not.
|
|
72
|
-
*/
|
|
73
|
-
text?: string;
|
|
74
|
-
/**
|
|
75
|
-
* `[start, end)` in the SOURCE string — what {@link xmlFragmentOf} slices.
|
|
76
|
-
*
|
|
77
|
-
* `end` is the offset just past this element's close tag (or past `/>`). For
|
|
78
|
-
* an element the file never closed it is the end of what was read, which is
|
|
79
|
-
* the honest answer and keeps the slice well-formed enough to re-parse.
|
|
80
|
-
*/
|
|
81
|
-
range: [number, number];
|
|
82
|
-
}
|
|
83
|
-
/** A document, and everything the reader had to complain about to read it. */
|
|
84
|
-
export interface XmlDocument {
|
|
85
|
-
/** The first top-level element, or `undefined` for a document with none. */
|
|
86
|
-
root?: XmlNode;
|
|
87
|
-
/**
|
|
88
|
-
* Every top-level element, in order.
|
|
89
|
-
*
|
|
90
|
-
* A well-formed document has exactly one. A file that has been concatenated,
|
|
91
|
-
* truncated or hand-edited has none or several, and the reader says what it
|
|
92
|
-
* found rather than deciding which one was meant.
|
|
93
|
-
*/
|
|
94
|
-
roots: XmlNode[];
|
|
95
|
-
/** One line each, in the user's words — see the header on tolerance. */
|
|
96
|
-
notes: string[];
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Character data as its characters — the five named entities and any numeric
|
|
100
|
-
* reference.
|
|
101
|
-
*
|
|
102
|
-
* An entity this reader does not know (` `, a DTD-declared one) is left
|
|
103
|
-
* EXACTLY as it was written, unresolved and unannounced. Two reasons, and the
|
|
104
|
-
* second is the one that matters: resolving it would need the DTD the file
|
|
105
|
-
* does not ship, and dropping it would lose a character; leaving the reference
|
|
106
|
-
* standing means a later re-serialization writes back what the file said, which
|
|
107
|
-
* is the promise `xmlFragmentOf` makes and this keeps true for text as well.
|
|
108
|
-
*/
|
|
109
|
-
export declare function decodeXmlText(value: string): string;
|
|
110
|
-
/**
|
|
111
|
-
* Read a document. Never throws; see the header.
|
|
112
|
-
*
|
|
113
|
-
* The scan is a single left-to-right pass with an explicit stack, which is what
|
|
114
|
-
* makes the tolerance cheap: every recovery is "say what is wrong, and carry on
|
|
115
|
-
* from the next `<`", and there is no recursion to unwind.
|
|
116
|
-
*/
|
|
117
|
-
export declare function readXml(source: string): XmlDocument;
|
|
118
|
-
/**
|
|
119
|
-
* The element's own bytes, from the string it was read from.
|
|
120
|
-
*
|
|
121
|
-
* This is what a quarantined fragment is (ADR 0012 D5): the file's text, not a
|
|
122
|
-
* re-rendering of this reader's tree. Comments, attribute order, prefixes and
|
|
123
|
-
* whitespace all survive, because none of them was ever taken apart.
|
|
124
|
-
*/
|
|
125
|
-
export declare function xmlFragmentOf(source: string, node: XmlNode): string;
|
|
126
|
-
/**
|
|
127
|
-
* A node back as XML, from the TREE rather than from the source.
|
|
128
|
-
*
|
|
129
|
-
* The fallback for a fragment that has no range to slice — one assembled by a
|
|
130
|
-
* caller, or read out of a document this function's caller no longer holds. It
|
|
131
|
-
* is faithful to what the tree records and therefore drops what the tree does
|
|
132
|
-
* not: comments, processing instructions, and the file's own indentation.
|
|
133
|
-
* Prefer {@link xmlFragmentOf} whenever the source string is still in hand.
|
|
134
|
-
*/
|
|
135
|
-
export declare function serializeXmlNode(node: XmlNode): string;
|
|
136
|
-
/** Children with this local name, whatever prefix the file gave them. */
|
|
137
|
-
export declare function xmlChildren(node: XmlNode, local: string): XmlNode[];
|
|
138
|
-
/** The first child with this local name, if any. */
|
|
139
|
-
export declare function xmlChild(node: XmlNode, local: string): XmlNode | undefined;
|
|
140
|
-
/**
|
|
141
|
-
* An attribute by LOCAL name — `xmi:id`, `xmi:ID` and a bare `id` all answer.
|
|
142
|
-
*
|
|
143
|
-
* Prefix-insensitive on purpose, and it is the difference between reading three
|
|
144
|
-
* tools and reading one: the XMI prefix is `xmi` by convention and by nothing
|
|
145
|
-
* else, Enterprise Architect has shipped files under `XMI`, and an
|
|
146
|
-
* attribute-defaulted file drops it altogether. The QUALIFIED name is still in
|
|
147
|
-
* `attrs` for anything that needs it, and {@link xmlFragmentOf} still writes
|
|
148
|
-
* back what the file said.
|
|
149
|
-
*
|
|
150
|
-
* The first match in document order wins, which only matters for a file that
|
|
151
|
-
* spells the same attribute twice under two prefixes.
|
|
152
|
-
*/
|
|
153
|
-
export declare function xmlAttr(node: XmlNode, local: string): string | undefined;
|
|
154
|
-
/** Every `xmlns` / `xmlns:*` declaration on this element, by prefix (`''`). */
|
|
155
|
-
export declare function xmlNamespaces(node: XmlNode): Record<string, string>;
|
|
156
|
-
/** Every element under `node`, at any depth, in document order. */
|
|
157
|
-
export declare function xmlDescendants(node: XmlNode): XmlNode[];
|
|
1
|
+
export { decodeXmlText, readXml, serializeXmlNode, type XmlDocument, xmlAttr, xmlChild, xmlChildren, type XmlContent, xmlDescendants, xmlFragmentOf, xmlNamespaces, type XmlNode, type XmlReadOptions, } from '@formicoidea/labre-core/interchange';
|
package/dist/xml-reader.js
CHANGED
|
@@ -1,430 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* ## Why not `DOMParser`
|
|
6
|
-
*
|
|
7
|
-
* BPMN's reader calls it (`gfx/bpmn/src/import.ts`), and it is the right call
|
|
8
|
-
* there: that parser has always run in a browser. This one must not. An
|
|
9
|
-
* interchange parser is a PURE function of a string (`docs/adr/0012` P3) — the
|
|
10
|
-
* same function an editor command, a unit test and labre-mcp all call — and
|
|
11
|
-
* `DOMParser` is a DOM global that exists in exactly one of those three. A
|
|
12
|
-
* reader that needs `happy-dom` to be loaded is a reader with an environment in
|
|
13
|
-
* its signature.
|
|
14
|
-
*
|
|
15
|
-
* So: two hundred lines of scanner, and no global in sight.
|
|
16
|
-
*
|
|
17
|
-
* ## Tolerant, and what that costs
|
|
18
|
-
*
|
|
19
|
-
* **It never throws.** A file that stops mid-element, a close tag that names
|
|
20
|
-
* the wrong element, an attribute with no quotes, an entity nobody declared —
|
|
21
|
-
* every one of them produces a tree with a NOTE beside it rather than an
|
|
22
|
-
* exception. That is not sloppiness, it is the job: the files this reader is
|
|
23
|
-
* handed were written by Papyrus, StarUML, Enterprise Architect and a dozen
|
|
24
|
-
* scripts, and a user who exported a model from one of them is entitled to get
|
|
25
|
-
* back everything it said even when part of it is malformed. The alternative —
|
|
26
|
-
* one exception for one stray `&` — imports nothing at all.
|
|
27
|
-
*
|
|
28
|
-
* What it does NOT do is pretend to be a conformant parser. There is no DTD, no
|
|
29
|
-
* entity declaration, no namespace RESOLUTION (prefixes are kept exactly as the
|
|
30
|
-
* file spelled them — see {@link XmlNode.prefix}), no attribute-value
|
|
31
|
-
* normalization beyond the character references XML 1.0 §3.3.3 makes
|
|
32
|
-
* mandatory. Every one of those is a feature an XMI file in the wild does not
|
|
33
|
-
* use, and each would be a piece of surface with nothing behind it.
|
|
34
|
-
*
|
|
35
|
-
* ## Verbatim means the source's own bytes
|
|
36
|
-
*
|
|
37
|
-
* Every node records the byte RANGE it was read from ({@link XmlNode.range}),
|
|
38
|
-
* and {@link xmlFragmentOf} hands back that slice of the original string. So a
|
|
39
|
-
* quarantined fragment (ADR 0012 D5) is the file's own text — its comments, its
|
|
40
|
-
* whitespace, its attribute order, its prefixes — and not this reader's opinion
|
|
41
|
-
* of it. {@link serializeXmlNode} re-serializes from the tree instead, and is
|
|
42
|
-
* the fallback for a node assembled rather than read.
|
|
43
|
-
*
|
|
44
|
-
* Pure: a string in, a tree out. No `std`, no DOM, no clock.
|
|
45
|
-
*/
|
|
46
|
-
/* ── Entities ─────────────────────────────────────────────────────────── */
|
|
47
|
-
/** The five XML 1.0 §4.6 predefined entities, and nothing else. */
|
|
48
|
-
const NAMED_ENTITIES = {
|
|
49
|
-
amp: '&',
|
|
50
|
-
lt: '<',
|
|
51
|
-
gt: '>',
|
|
52
|
-
quot: '"',
|
|
53
|
-
apos: "'",
|
|
54
|
-
};
|
|
55
|
-
/**
|
|
56
|
-
* Character data as its characters — the five named entities and any numeric
|
|
57
|
-
* reference.
|
|
58
|
-
*
|
|
59
|
-
* An entity this reader does not know (` `, a DTD-declared one) is left
|
|
60
|
-
* EXACTLY as it was written, unresolved and unannounced. Two reasons, and the
|
|
61
|
-
* second is the one that matters: resolving it would need the DTD the file
|
|
62
|
-
* does not ship, and dropping it would lose a character; leaving the reference
|
|
63
|
-
* standing means a later re-serialization writes back what the file said, which
|
|
64
|
-
* is the promise `xmlFragmentOf` makes and this keeps true for text as well.
|
|
65
|
-
*/
|
|
66
|
-
export function decodeXmlText(value) {
|
|
67
|
-
if (!value.includes('&'))
|
|
68
|
-
return value;
|
|
69
|
-
return value.replaceAll(/&(#x[0-9a-fA-F]+|#\d+|[A-Za-z][\w.-]*);/g, (match, reference) => {
|
|
70
|
-
if (reference.startsWith('#')) {
|
|
71
|
-
const hex = reference[1] === 'x' || reference[1] === 'X';
|
|
72
|
-
const code = Number.parseInt(hex ? reference.slice(2) : reference.slice(1), hex ? 16 : 10);
|
|
73
|
-
// Out of Unicode's range is a reference no character answers, and
|
|
74
|
-
// `String.fromCodePoint` THROWS on one — which is the single place this
|
|
75
|
-
// reader could have raised. It hands back the reference instead.
|
|
76
|
-
return Number.isInteger(code) && code >= 0 && code <= 0x10ffff
|
|
77
|
-
? String.fromCodePoint(code)
|
|
78
|
-
: match;
|
|
79
|
-
}
|
|
80
|
-
return NAMED_ENTITIES[reference] ?? match;
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
/* ── The scanner ──────────────────────────────────────────────────────── */
|
|
84
|
-
/** A name character, generously: XML's NameChar minus the exotic ranges. */
|
|
85
|
-
const NAME_CHAR = /[\w.:_\-À-]/;
|
|
86
|
-
/** The prefix and the local half of a qualified name. */
|
|
87
|
-
function splitName(name) {
|
|
88
|
-
const colon = name.indexOf(':');
|
|
89
|
-
return colon < 0
|
|
90
|
-
? { prefix: '', local: name }
|
|
91
|
-
: { prefix: name.slice(0, colon), local: name.slice(colon + 1) };
|
|
92
|
-
}
|
|
93
|
-
/** A node with no children and no text, ready to be filled. */
|
|
94
|
-
function nodeOf(name, start) {
|
|
95
|
-
const { prefix, local } = splitName(name);
|
|
96
|
-
return {
|
|
97
|
-
name,
|
|
98
|
-
prefix,
|
|
99
|
-
local,
|
|
100
|
-
attrs: {},
|
|
101
|
-
children: [],
|
|
102
|
-
range: [start, start],
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Read a document. Never throws; see the header.
|
|
107
|
-
*
|
|
108
|
-
* The scan is a single left-to-right pass with an explicit stack, which is what
|
|
109
|
-
* makes the tolerance cheap: every recovery is "say what is wrong, and carry on
|
|
110
|
-
* from the next `<`", and there is no recursion to unwind.
|
|
111
|
-
*/
|
|
112
|
-
export function readXml(source) {
|
|
113
|
-
const notes = [];
|
|
114
|
-
const roots = [];
|
|
115
|
-
const stack = [];
|
|
116
|
-
/** Character data accumulated for the element currently open. */
|
|
117
|
-
const pending = [];
|
|
118
|
-
let cursor = 0;
|
|
119
|
-
/** Attach the text read since the last tag to whatever is open. */
|
|
120
|
-
const flushText = () => {
|
|
121
|
-
if (pending.length === 0)
|
|
122
|
-
return;
|
|
123
|
-
const text = pending.join('');
|
|
124
|
-
pending.length = 0;
|
|
125
|
-
const open = stack[stack.length - 1];
|
|
126
|
-
if (!open)
|
|
127
|
-
return;
|
|
128
|
-
// Whitespace between two indented children is layout, not content, and an
|
|
129
|
-
// element that held only that keeps no `text` at all — which is what lets a
|
|
130
|
-
// caller ask `if (node.text)` and mean "the file wrote something here".
|
|
131
|
-
if (!/\S/.test(text) && open.children.length > 0)
|
|
132
|
-
return;
|
|
133
|
-
if (!/\S/.test(text) && open.text === undefined)
|
|
134
|
-
return;
|
|
135
|
-
open.text = (open.text ?? '') + text;
|
|
136
|
-
};
|
|
137
|
-
const close = (node, end) => {
|
|
138
|
-
node.range[1] = end;
|
|
139
|
-
const parent = stack[stack.length - 1];
|
|
140
|
-
if (parent)
|
|
141
|
-
parent.children.push(node);
|
|
142
|
-
else
|
|
143
|
-
roots.push(node);
|
|
144
|
-
};
|
|
145
|
-
while (cursor < source.length) {
|
|
146
|
-
const open = source.indexOf('<', cursor);
|
|
147
|
-
if (open < 0) {
|
|
148
|
-
pending.push(decodeXmlText(source.slice(cursor)));
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
if (open > cursor)
|
|
152
|
-
pending.push(decodeXmlText(source.slice(cursor, open)));
|
|
153
|
-
/* ── The four things that are not an element ─────────────────────── */
|
|
154
|
-
if (source.startsWith('<!--', open)) {
|
|
155
|
-
const end = source.indexOf('-->', open + 4);
|
|
156
|
-
if (end < 0) {
|
|
157
|
-
notes.push('A comment in this file is never closed; the rest was read.');
|
|
158
|
-
break;
|
|
159
|
-
}
|
|
160
|
-
cursor = end + 3;
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
if (source.startsWith('<![CDATA[', open)) {
|
|
164
|
-
const end = source.indexOf(']]>', open + 9);
|
|
165
|
-
if (end < 0) {
|
|
166
|
-
notes.push('A CDATA section in this file is never closed.');
|
|
167
|
-
pending.push(source.slice(open + 9));
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
// CDATA is character data with the escaping turned OFF, so it is pushed
|
|
171
|
-
// raw: decoding it would resolve a `&` the author wrote literally.
|
|
172
|
-
pending.push(source.slice(open + 9, end));
|
|
173
|
-
cursor = end + 3;
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
if (source.startsWith('<?', open)) {
|
|
177
|
-
const end = source.indexOf('?>', open + 2);
|
|
178
|
-
if (end < 0) {
|
|
179
|
-
notes.push('A processing instruction in this file is never closed.');
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
cursor = end + 2;
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
if (source.startsWith('<!', open)) {
|
|
186
|
-
// A DOCTYPE, with or without an internal subset. The subset is skipped
|
|
187
|
-
// whole rather than parsed: this reader declares no entities, so what is
|
|
188
|
-
// in there could only change how a `&name;` it cannot resolve is read.
|
|
189
|
-
let scan = open + 2;
|
|
190
|
-
let depth = 0;
|
|
191
|
-
while (scan < source.length) {
|
|
192
|
-
const char = source[scan];
|
|
193
|
-
if (char === '[')
|
|
194
|
-
depth += 1;
|
|
195
|
-
else if (char === ']')
|
|
196
|
-
depth -= 1;
|
|
197
|
-
else if (char === '>' && depth <= 0)
|
|
198
|
-
break;
|
|
199
|
-
scan += 1;
|
|
200
|
-
}
|
|
201
|
-
cursor = Math.min(scan + 1, source.length);
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
/* ── A close tag ─────────────────────────────────────────────────── */
|
|
205
|
-
if (source.startsWith('</', open)) {
|
|
206
|
-
const end = source.indexOf('>', open);
|
|
207
|
-
if (end < 0) {
|
|
208
|
-
notes.push('A close tag in this file is never finished.');
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
flushText();
|
|
212
|
-
const name = source.slice(open + 2, end).trim();
|
|
213
|
-
const depth = stack.findLastIndex(node => node.name === name);
|
|
214
|
-
if (depth < 0) {
|
|
215
|
-
notes.push(`</${name}> closes an element this file never opened; it was ignored.`);
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
// Everything still open INSIDE the element being closed is closed with
|
|
219
|
-
// it, in order. `<a><b></a>` is a file that lost a tag, not a file with
|
|
220
|
-
// no `<b>` in it, and the tree keeps the `<b>`.
|
|
221
|
-
for (let index = stack.length - 1; index > depth; index -= 1) {
|
|
222
|
-
const orphan = stack.pop();
|
|
223
|
-
notes.push(`<${orphan.name}> is closed by </${name}> rather than by a tag of ` +
|
|
224
|
-
`its own; it was read as far as it goes.`);
|
|
225
|
-
close(orphan, open);
|
|
226
|
-
}
|
|
227
|
-
const node = stack.pop();
|
|
228
|
-
close(node, end + 1);
|
|
229
|
-
}
|
|
230
|
-
cursor = end + 1;
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
/* ── An open tag ─────────────────────────────────────────────────── */
|
|
234
|
-
let scan = open + 1;
|
|
235
|
-
while (scan < source.length && NAME_CHAR.test(source[scan]))
|
|
236
|
-
scan += 1;
|
|
237
|
-
const name = source.slice(open + 1, scan);
|
|
238
|
-
if (name.length === 0) {
|
|
239
|
-
// A bare `<` in character data — illegal, and every second hand-written
|
|
240
|
-
// file has one. It is kept as the character it is.
|
|
241
|
-
notes.push('A "<" in this file starts no tag; it was read as a plain character.');
|
|
242
|
-
pending.push('<');
|
|
243
|
-
cursor = open + 1;
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
flushText();
|
|
247
|
-
const node = nodeOf(name, open);
|
|
248
|
-
/* ── Its attributes ──────────────────────────────────────────────── */
|
|
249
|
-
let selfClosing = false;
|
|
250
|
-
let tagEnd = source.length;
|
|
251
|
-
while (scan < source.length) {
|
|
252
|
-
while (scan < source.length && /\s/.test(source[scan]))
|
|
253
|
-
scan += 1;
|
|
254
|
-
if (scan >= source.length)
|
|
255
|
-
break;
|
|
256
|
-
if (source.startsWith('/>', scan)) {
|
|
257
|
-
selfClosing = true;
|
|
258
|
-
tagEnd = scan + 2;
|
|
259
|
-
break;
|
|
260
|
-
}
|
|
261
|
-
if (source[scan] === '>') {
|
|
262
|
-
tagEnd = scan + 1;
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
const attrStart = scan;
|
|
266
|
-
while (scan < source.length && NAME_CHAR.test(source[scan]))
|
|
267
|
-
scan += 1;
|
|
268
|
-
const attrName = source.slice(attrStart, scan);
|
|
269
|
-
if (attrName.length === 0) {
|
|
270
|
-
// Something that is neither a name, a `>` nor whitespace. Skipped one
|
|
271
|
-
// character at a time so the scan cannot stall on it.
|
|
272
|
-
scan += 1;
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
while (scan < source.length && /\s/.test(source[scan]))
|
|
276
|
-
scan += 1;
|
|
277
|
-
if (source[scan] !== '=') {
|
|
278
|
-
// A valueless attribute — HTML's habit, not XML's. Recorded as the
|
|
279
|
-
// empty string, which is what every tolerant reader does with one.
|
|
280
|
-
notes.push(`The attribute "${attrName}" on <${name}> has no value; it was read ` +
|
|
281
|
-
`as empty.`);
|
|
282
|
-
node.attrs[attrName] = '';
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
scan += 1;
|
|
286
|
-
while (scan < source.length && /\s/.test(source[scan]))
|
|
287
|
-
scan += 1;
|
|
288
|
-
const quote = source[scan];
|
|
289
|
-
let value = '';
|
|
290
|
-
if (quote === '"' || quote === "'") {
|
|
291
|
-
const end = source.indexOf(quote, scan + 1);
|
|
292
|
-
if (end < 0) {
|
|
293
|
-
notes.push(`The value of "${attrName}" on <${name}> is never closed; the ` +
|
|
294
|
-
`rest of the file was read as its value.`);
|
|
295
|
-
value = source.slice(scan + 1);
|
|
296
|
-
scan = source.length;
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
value = source.slice(scan + 1, end);
|
|
300
|
-
scan = end + 1;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
// Unquoted. Read to the next whitespace or tag end, which is the only
|
|
305
|
-
// reading available and the one every tolerant parser agrees on.
|
|
306
|
-
const start = scan;
|
|
307
|
-
while (scan < source.length && !/[\s>/]/.test(source[scan]))
|
|
308
|
-
scan += 1;
|
|
309
|
-
value = source.slice(start, scan);
|
|
310
|
-
notes.push(`The value of "${attrName}" on <${name}> is not quoted; it was read ` +
|
|
311
|
-
`up to the next space.`);
|
|
312
|
-
}
|
|
313
|
-
node.attrs[attrName] = decodeXmlText(value);
|
|
314
|
-
}
|
|
315
|
-
if (tagEnd === source.length && !selfClosing && source[scan] !== '>') {
|
|
316
|
-
notes.push(`<${name}> is never finished; the file stops inside its tag.`);
|
|
317
|
-
}
|
|
318
|
-
if (selfClosing) {
|
|
319
|
-
close(node, tagEnd);
|
|
320
|
-
}
|
|
321
|
-
else {
|
|
322
|
-
stack.push(node);
|
|
323
|
-
}
|
|
324
|
-
cursor = tagEnd;
|
|
325
|
-
}
|
|
326
|
-
flushText();
|
|
327
|
-
// Whatever the file left open. Closed at the end of what there was, so the
|
|
328
|
-
// tree holds every element the file named even when it stops mid-document.
|
|
329
|
-
while (stack.length > 0) {
|
|
330
|
-
const orphan = stack.pop();
|
|
331
|
-
notes.push(`<${orphan.name}> is never closed; it was read to the end.`);
|
|
332
|
-
close(orphan, source.length);
|
|
333
|
-
}
|
|
334
|
-
return { root: roots[0], roots, notes };
|
|
335
|
-
}
|
|
336
|
-
/* ── Getting back out ─────────────────────────────────────────────────── */
|
|
337
|
-
/**
|
|
338
|
-
* The element's own bytes, from the string it was read from.
|
|
339
|
-
*
|
|
340
|
-
* This is what a quarantined fragment is (ADR 0012 D5): the file's text, not a
|
|
341
|
-
* re-rendering of this reader's tree. Comments, attribute order, prefixes and
|
|
342
|
-
* whitespace all survive, because none of them was ever taken apart.
|
|
343
|
-
*/
|
|
344
|
-
export function xmlFragmentOf(source, node) {
|
|
345
|
-
return source.slice(node.range[0], node.range[1]);
|
|
346
|
-
}
|
|
347
|
-
/** The three characters that would otherwise start markup. */
|
|
348
|
-
function escapeText(value) {
|
|
349
|
-
return value
|
|
350
|
-
.replaceAll('&', '&')
|
|
351
|
-
.replaceAll('<', '<')
|
|
352
|
-
.replaceAll('>', '>');
|
|
353
|
-
}
|
|
354
|
-
/** An attribute value — see `xml.ts`, whose escaping this mirrors exactly. */
|
|
355
|
-
function escapeAttr(value) {
|
|
356
|
-
return escapeText(value)
|
|
357
|
-
.replaceAll('"', '"')
|
|
358
|
-
.replaceAll("'", ''')
|
|
359
|
-
.replaceAll('\n', ' ')
|
|
360
|
-
.replaceAll('\r', ' ')
|
|
361
|
-
.replaceAll('\t', '	');
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* A node back as XML, from the TREE rather than from the source.
|
|
365
|
-
*
|
|
366
|
-
* The fallback for a fragment that has no range to slice — one assembled by a
|
|
367
|
-
* caller, or read out of a document this function's caller no longer holds. It
|
|
368
|
-
* is faithful to what the tree records and therefore drops what the tree does
|
|
369
|
-
* not: comments, processing instructions, and the file's own indentation.
|
|
370
|
-
* Prefer {@link xmlFragmentOf} whenever the source string is still in hand.
|
|
371
|
-
*/
|
|
372
|
-
export function serializeXmlNode(node) {
|
|
373
|
-
const attrs = Object.entries(node.attrs)
|
|
374
|
-
.map(([key, value]) => ` ${key}="${escapeAttr(value)}"`)
|
|
375
|
-
.join('');
|
|
376
|
-
const inner = [
|
|
377
|
-
node.text === undefined ? '' : escapeText(node.text),
|
|
378
|
-
...node.children.map(child => serializeXmlNode(child)),
|
|
379
|
-
].join('');
|
|
380
|
-
if (inner.length === 0)
|
|
381
|
-
return `<${node.name}${attrs}/>`;
|
|
382
|
-
return `<${node.name}${attrs}>${inner}</${node.name}>`;
|
|
383
|
-
}
|
|
384
|
-
/* ── Reading a tree ───────────────────────────────────────────────────── */
|
|
385
|
-
/** Children with this local name, whatever prefix the file gave them. */
|
|
386
|
-
export function xmlChildren(node, local) {
|
|
387
|
-
return node.children.filter(child => child.local === local);
|
|
388
|
-
}
|
|
389
|
-
/** The first child with this local name, if any. */
|
|
390
|
-
export function xmlChild(node, local) {
|
|
391
|
-
return node.children.find(child => child.local === local);
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* An attribute by LOCAL name — `xmi:id`, `xmi:ID` and a bare `id` all answer.
|
|
395
|
-
*
|
|
396
|
-
* Prefix-insensitive on purpose, and it is the difference between reading three
|
|
397
|
-
* tools and reading one: the XMI prefix is `xmi` by convention and by nothing
|
|
398
|
-
* else, Enterprise Architect has shipped files under `XMI`, and an
|
|
399
|
-
* attribute-defaulted file drops it altogether. The QUALIFIED name is still in
|
|
400
|
-
* `attrs` for anything that needs it, and {@link xmlFragmentOf} still writes
|
|
401
|
-
* back what the file said.
|
|
402
|
-
*
|
|
403
|
-
* The first match in document order wins, which only matters for a file that
|
|
404
|
-
* spells the same attribute twice under two prefixes.
|
|
405
|
-
*/
|
|
406
|
-
export function xmlAttr(node, local) {
|
|
407
|
-
const direct = node.attrs[local];
|
|
408
|
-
if (direct !== undefined)
|
|
409
|
-
return direct;
|
|
410
|
-
for (const [key, value] of Object.entries(node.attrs)) {
|
|
411
|
-
if (splitName(key).local === local)
|
|
412
|
-
return value;
|
|
413
|
-
}
|
|
414
|
-
return undefined;
|
|
415
|
-
}
|
|
416
|
-
/** Every `xmlns` / `xmlns:*` declaration on this element, by prefix (`''`). */
|
|
417
|
-
export function xmlNamespaces(node) {
|
|
418
|
-
const declarations = {};
|
|
419
|
-
for (const [key, value] of Object.entries(node.attrs)) {
|
|
420
|
-
if (key === 'xmlns')
|
|
421
|
-
declarations[''] = value;
|
|
422
|
-
else if (key.startsWith('xmlns:'))
|
|
423
|
-
declarations[key.slice(6)] = value;
|
|
424
|
-
}
|
|
425
|
-
return declarations;
|
|
426
|
-
}
|
|
427
|
-
/** Every element under `node`, at any depth, in document order. */
|
|
428
|
-
export function xmlDescendants(node) {
|
|
429
|
-
return node.children.flatMap(child => [child, ...xmlDescendants(child)]);
|
|
430
|
-
}
|
|
1
|
+
// The reader moved to the surface package so BPMN and the SVG sketch read with
|
|
2
|
+
// it too (ADR 0012, P3 as amended 2026-10-14); this keeps UML's public names.
|
|
3
|
+
export { decodeXmlText, readXml, serializeXmlNode, xmlAttr, xmlChild, xmlChildren, xmlDescendants, xmlFragmentOf, xmlNamespaces, } from '@formicoidea/labre-core/interchange';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formicoidea/labre-framework-uml",
|
|
3
3
|
"description": "Labre uml framework for @formicoidea/labre-core.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.51.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"author": "lajola",
|
|
@@ -25,14 +25,18 @@
|
|
|
25
25
|
"./commands-manifest": {
|
|
26
26
|
"types": "./dist/commands-manifest.d.ts",
|
|
27
27
|
"import": "./dist/commands-manifest.js"
|
|
28
|
+
},
|
|
29
|
+
"./interchange": {
|
|
30
|
+
"types": "./dist/interchange-headless.d.ts",
|
|
31
|
+
"import": "./dist/interchange-headless.js"
|
|
28
32
|
}
|
|
29
33
|
},
|
|
30
34
|
"files": [
|
|
31
35
|
"dist"
|
|
32
36
|
],
|
|
33
37
|
"dependencies": {
|
|
34
|
-
"@formicoidea/labre-core": "0.
|
|
35
|
-
"@formicoidea/labre-ddd-shared": "0.
|
|
38
|
+
"@formicoidea/labre-core": "0.51.0",
|
|
39
|
+
"@formicoidea/labre-ddd-shared": "0.51.0",
|
|
36
40
|
"lit": "^3.2.0"
|
|
37
41
|
}
|
|
38
42
|
}
|