@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.
@@ -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 (`&nbsp;`, 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';
@@ -1,430 +1,3 @@
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
- /* ── 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 (`&nbsp;`, 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 `&amp;` 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('&', '&amp;')
351
- .replaceAll('<', '&lt;')
352
- .replaceAll('>', '&gt;');
353
- }
354
- /** An attribute value — see `xml.ts`, whose escaping this mirrors exactly. */
355
- function escapeAttr(value) {
356
- return escapeText(value)
357
- .replaceAll('"', '&quot;')
358
- .replaceAll("'", '&apos;')
359
- .replaceAll('\n', '&#10;')
360
- .replaceAll('\r', '&#13;')
361
- .replaceAll('\t', '&#9;');
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.50.0",
4
+ "version": "0.52.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.50.0",
35
- "@formicoidea/labre-ddd-shared": "0.50.0",
38
+ "@formicoidea/labre-core": "0.52.0",
39
+ "@formicoidea/labre-ddd-shared": "0.52.0",
36
40
  "lit": "^3.2.0"
37
41
  }
38
42
  }