@bpmnkit/core 0.1.1 → 0.2.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/README.md +32 -1
- package/dist/bpmn/agentic.d.ts +121 -0
- package/dist/bpmn/agentic.js +97 -0
- package/dist/bpmn/auto-layout.d.ts +5 -5
- package/dist/bpmn/auto-layout.js +592 -36
- package/dist/bpmn/bpmn-builder.d.ts +265 -3
- package/dist/bpmn/bpmn-builder.js +603 -197
- package/dist/bpmn/bpmn-model.d.ts +114 -0
- package/dist/bpmn/bpmn-parser.js +1414 -521
- package/dist/bpmn/bpmn-serializer.js +107 -19
- package/dist/bpmn/compact.d.ts +17 -2
- package/dist/bpmn/compact.js +3 -3
- package/dist/bpmn/full-operations.d.ts +89 -0
- package/dist/bpmn/full-operations.js +478 -0
- package/dist/bpmn/index.d.ts +19 -0
- package/dist/bpmn/index.js +21 -0
- package/dist/bpmn/optimize/agentic.d.ts +10 -0
- package/dist/bpmn/optimize/agentic.js +88 -0
- package/dist/bpmn/optimize/deploy.d.ts +16 -0
- package/dist/bpmn/optimize/deploy.js +143 -0
- package/dist/bpmn/optimize/feel-syntax.d.ts +12 -0
- package/dist/bpmn/optimize/feel-syntax.js +87 -0
- package/dist/bpmn/optimize/feel.js +7 -4
- package/dist/bpmn/optimize/flow.js +22 -2
- package/dist/bpmn/optimize/index.js +20 -9
- package/dist/bpmn/optimize/patterns.js +23 -16
- package/dist/bpmn/optimize/tasks.js +30 -7
- package/dist/bpmn/optimize/types.d.ts +10 -1
- package/dist/bpmn/optimize/utils.js +2 -4
- package/dist/bpmn/optimize/variable-flow.js +58 -67
- package/dist/bpmn/semantic-hash.d.ts +93 -0
- package/dist/bpmn/semantic-hash.js +155 -0
- package/dist/bpmn/sha256.d.ts +17 -0
- package/dist/bpmn/sha256.js +95 -0
- package/dist/bpmn/zeebe-extensions.d.ts +83 -0
- package/dist/bpmn/zeebe-extensions.js +117 -0
- package/dist/bpmn/zeebe-placement.d.ts +12 -0
- package/dist/bpmn/zeebe-placement.js +140 -0
- package/dist/errors.d.ts +40 -1
- package/dist/errors.js +41 -0
- package/dist/index.d.ts +16 -5
- package/dist/index.js +9 -3
- package/dist/layout/annotations.js +36 -1
- package/dist/layout/collaboration/alignment.d.ts +26 -0
- package/dist/layout/collaboration/alignment.js +66 -0
- package/dist/layout/collaboration/ordering.d.ts +21 -0
- package/dist/layout/collaboration/ordering.js +102 -0
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/layout-engine.d.ts +13 -3
- package/dist/layout/layout-engine.js +9 -4
- package/dist/layout/semantic/bands.d.ts +19 -0
- package/dist/layout/semantic/bands.js +324 -0
- package/dist/layout/semantic/graph.d.ts +37 -0
- package/dist/layout/semantic/graph.js +242 -0
- package/dist/layout/semantic/index.d.ts +13 -0
- package/dist/layout/semantic/index.js +181 -0
- package/dist/layout/semantic/place.d.ts +40 -0
- package/dist/layout/semantic/place.js +271 -0
- package/dist/layout/semantic/route.d.ts +14 -0
- package/dist/layout/semantic/route.js +514 -0
- package/dist/layout/types.d.ts +17 -0
- package/dist/node/index.d.ts +10 -0
- package/dist/node/index.js +9 -0
- package/dist/node/write.d.ts +81 -0
- package/dist/node/write.js +167 -0
- package/dist/plan/compile.d.ts +39 -0
- package/dist/plan/compile.js +380 -0
- package/dist/plan/extract.d.ts +31 -0
- package/dist/plan/extract.js +248 -0
- package/dist/plan/index.d.ts +6 -0
- package/dist/plan/index.js +5 -0
- package/dist/plan/merge.d.ts +13 -0
- package/dist/plan/merge.js +80 -0
- package/dist/plan/slug.d.ts +5 -0
- package/dist/plan/slug.js +22 -0
- package/dist/plan/types.d.ts +225 -0
- package/dist/plan/types.js +13 -0
- package/dist/types/id-generator.js +11 -3
- package/dist/xml/index.d.ts +3 -1
- package/dist/xml/index.js +2 -1
- package/dist/xml/xml-parser.d.ts +32 -0
- package/dist/xml/xml-parser.js +394 -143
- package/package.json +9 -2
package/dist/xml/xml-parser.js
CHANGED
|
@@ -1,153 +1,375 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
//
|
|
2
|
+
// Event scanner
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
+
/** What a sink wants the scanner to do with an element after its start tag. */
|
|
5
|
+
export const Visit = {
|
|
6
|
+
/** Report child elements and character data. */
|
|
7
|
+
All: 0,
|
|
8
|
+
/**
|
|
9
|
+
* Skip the element's content: the scanner fast-forwards past the matching
|
|
10
|
+
* end tag without building attributes or decoding text for anything inside,
|
|
11
|
+
* and `end` is not called for it.
|
|
12
|
+
*/
|
|
13
|
+
Skip: 1,
|
|
14
|
+
/** Report child elements but drop character data without decoding it. */
|
|
15
|
+
ElementsOnly: 2,
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Scan an XML document, reporting the root element and everything inside it to
|
|
19
|
+
* `sink`. Returns false when the document has no root element. Content after
|
|
20
|
+
* the root element is ignored.
|
|
21
|
+
*/
|
|
22
|
+
export function scanXml(xml, sink) {
|
|
23
|
+
return new XmlScanner(xml).scan(sink);
|
|
24
|
+
}
|
|
4
25
|
/**
|
|
5
26
|
* Parse an XML string into an XmlElement tree.
|
|
6
27
|
* Returns the root element with all namespace prefixes preserved.
|
|
7
28
|
* @throws Error if the XML has no root element.
|
|
8
29
|
*/
|
|
9
30
|
export function parseXml(xml) {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
if (!root)
|
|
31
|
+
const builder = new TreeBuilder();
|
|
32
|
+
if (!scanXml(xml, builder) || builder.root === undefined) {
|
|
13
33
|
throw new Error("Failed to parse XML: no root element found");
|
|
14
|
-
|
|
34
|
+
}
|
|
35
|
+
return builder.root;
|
|
36
|
+
}
|
|
37
|
+
/** Sink that materialises the document as an XmlElement tree. */
|
|
38
|
+
class TreeBuilder {
|
|
39
|
+
root;
|
|
40
|
+
stack = [];
|
|
41
|
+
start(name, _local, attributes) {
|
|
42
|
+
const el = { name, attributes, children: [] };
|
|
43
|
+
const parent = this.stack[this.stack.length - 1];
|
|
44
|
+
if (parent)
|
|
45
|
+
parent.children.push(el);
|
|
46
|
+
else
|
|
47
|
+
this.root = el;
|
|
48
|
+
this.stack.push(el);
|
|
49
|
+
return Visit.All;
|
|
50
|
+
}
|
|
51
|
+
text(text) {
|
|
52
|
+
const el = this.stack[this.stack.length - 1];
|
|
53
|
+
el.text = el.text === undefined ? text : el.text + text;
|
|
54
|
+
}
|
|
55
|
+
end() {
|
|
56
|
+
this.stack.pop();
|
|
57
|
+
}
|
|
15
58
|
}
|
|
16
|
-
|
|
59
|
+
// Character codes the scanner switches on. Comparing codes avoids allocating a
|
|
60
|
+
// one-character string for every position visited.
|
|
61
|
+
const LT = 0x3c; // <
|
|
62
|
+
const GT = 0x3e; // >
|
|
63
|
+
const SLASH = 0x2f; // /
|
|
64
|
+
const EQ = 0x3d; // =
|
|
65
|
+
const QUESTION = 0x3f; // ?
|
|
66
|
+
const BANG = 0x21; // !
|
|
67
|
+
const DQUOTE = 0x22; // "
|
|
68
|
+
const SQUOTE = 0x27; // '
|
|
69
|
+
const COLON = 0x3a; // :
|
|
70
|
+
const SPACE = 0x20;
|
|
71
|
+
const TAB = 0x09;
|
|
72
|
+
const LF = 0x0a;
|
|
73
|
+
const CR = 0x0d;
|
|
74
|
+
class XmlScanner {
|
|
17
75
|
s;
|
|
76
|
+
n;
|
|
18
77
|
i = 0;
|
|
78
|
+
/** Index of the first ":" in the name most recently read, or -1. */
|
|
79
|
+
colon = -1;
|
|
80
|
+
/** Position of the next "&" at or after the last place we looked, or -1 for none. */
|
|
81
|
+
nextAmp = 0;
|
|
19
82
|
constructor(source) {
|
|
20
83
|
this.s = source;
|
|
84
|
+
this.n = source.length;
|
|
21
85
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
while (this.i < this.s.length) {
|
|
86
|
+
scan(sink) {
|
|
87
|
+
while (this.i < this.n) {
|
|
25
88
|
this.skipWhitespace();
|
|
26
|
-
if (this.i >= this.
|
|
89
|
+
if (this.i >= this.n)
|
|
27
90
|
break;
|
|
28
|
-
if (this.s
|
|
91
|
+
if (this.s.charCodeAt(this.i) !== LT) {
|
|
29
92
|
// text outside root — skip
|
|
30
93
|
this.i++;
|
|
31
94
|
continue;
|
|
32
95
|
}
|
|
33
|
-
|
|
96
|
+
const next = this.s.charCodeAt(this.i + 1);
|
|
97
|
+
if (next === QUESTION) {
|
|
34
98
|
this.skipPi();
|
|
35
99
|
}
|
|
36
|
-
else if (
|
|
37
|
-
this.
|
|
100
|
+
else if (next === BANG) {
|
|
101
|
+
if (this.s.startsWith("<!--", this.i))
|
|
102
|
+
this.skipComment();
|
|
103
|
+
else
|
|
104
|
+
this.skipBang();
|
|
38
105
|
}
|
|
39
|
-
else
|
|
40
|
-
this.
|
|
106
|
+
else {
|
|
107
|
+
this.element(sink);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
/** Scan the element starting at `i` (which is "<") and everything inside it. */
|
|
114
|
+
element(sink) {
|
|
115
|
+
const s = this.s;
|
|
116
|
+
// Open elements, innermost last, with whether each one wants character data.
|
|
117
|
+
const open = [];
|
|
118
|
+
const wantText = [];
|
|
119
|
+
if (this.openTag(sink, open, wantText) === 0)
|
|
120
|
+
return;
|
|
121
|
+
while (open.length > 0) {
|
|
122
|
+
if (this.i >= this.n)
|
|
123
|
+
throw new Error(`Expected "</" at position ${this.i}`);
|
|
124
|
+
const c = s.charCodeAt(this.i);
|
|
125
|
+
if (c !== LT) {
|
|
126
|
+
if (wantText[wantText.length - 1])
|
|
127
|
+
sink.text(this.readText());
|
|
128
|
+
else
|
|
129
|
+
this.skipText();
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const next = s.charCodeAt(this.i + 1);
|
|
133
|
+
if (next === SLASH) {
|
|
134
|
+
this.i += 2;
|
|
135
|
+
const closing = this.readName();
|
|
136
|
+
const name = open[open.length - 1];
|
|
137
|
+
if (closing !== name) {
|
|
138
|
+
throw new Error(`Mismatched closing tag: expected </${name}>, got </${closing}>`);
|
|
139
|
+
}
|
|
140
|
+
this.skipWhitespace();
|
|
141
|
+
this.expectChar(GT, ">");
|
|
142
|
+
open.pop();
|
|
143
|
+
wantText.pop();
|
|
144
|
+
sink.end(name);
|
|
145
|
+
}
|
|
146
|
+
else if (next === BANG) {
|
|
147
|
+
if (s.startsWith("<!--", this.i)) {
|
|
148
|
+
this.skipComment();
|
|
149
|
+
}
|
|
150
|
+
else if (s.startsWith("<![CDATA[", this.i)) {
|
|
151
|
+
if (wantText[wantText.length - 1])
|
|
152
|
+
sink.text(this.readCData());
|
|
153
|
+
else
|
|
154
|
+
this.skipCData();
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// Unknown declaration inside content — treat it as an element and
|
|
158
|
+
// let the name reader fail on it, as before.
|
|
159
|
+
this.openTag(sink, open, wantText);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (next === QUESTION) {
|
|
163
|
+
this.skipPi();
|
|
41
164
|
}
|
|
42
165
|
else {
|
|
43
|
-
|
|
44
|
-
break;
|
|
166
|
+
this.openTag(sink, open, wantText);
|
|
45
167
|
}
|
|
46
168
|
}
|
|
47
|
-
return root;
|
|
48
169
|
}
|
|
49
|
-
|
|
50
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Read a start tag at `i`, report it, and push it onto `open` unless it was
|
|
172
|
+
* self-closing or the sink skipped it. Returns the new depth of `open`.
|
|
173
|
+
*/
|
|
174
|
+
openTag(sink, open, wantText) {
|
|
175
|
+
const s = this.s;
|
|
176
|
+
this.i++; // <
|
|
51
177
|
const name = this.readName();
|
|
178
|
+
const local = this.colon >= 0 ? s.substring(this.colon + 1, this.i) : name;
|
|
52
179
|
const attributes = {};
|
|
53
180
|
this.readAttributes(attributes);
|
|
54
181
|
this.skipWhitespace();
|
|
55
|
-
if (
|
|
56
|
-
// self-closing
|
|
182
|
+
if (s.charCodeAt(this.i) === SLASH && s.charCodeAt(this.i + 1) === GT) {
|
|
57
183
|
this.i += 2;
|
|
58
|
-
|
|
184
|
+
if (sink.start(name, local, attributes, true) !== Visit.Skip)
|
|
185
|
+
sink.end(name);
|
|
186
|
+
return open.length;
|
|
59
187
|
}
|
|
60
|
-
this.
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
188
|
+
this.expectChar(GT, ">");
|
|
189
|
+
const visit = sink.start(name, local, attributes, false);
|
|
190
|
+
if (visit === Visit.Skip) {
|
|
191
|
+
this.skipContent(name);
|
|
192
|
+
return open.length;
|
|
193
|
+
}
|
|
194
|
+
open.push(name);
|
|
195
|
+
wantText.push(visit === Visit.All);
|
|
196
|
+
return open.length;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Fast-forward past the content and end tag of the element `name` whose
|
|
200
|
+
* start tag was just consumed, validating nesting but allocating nothing.
|
|
201
|
+
*/
|
|
202
|
+
skipContent(name) {
|
|
203
|
+
const s = this.s;
|
|
204
|
+
// Open elements below `name`, as (start, length) spans into the source.
|
|
205
|
+
const spans = [];
|
|
206
|
+
let depth = 1;
|
|
207
|
+
while (depth > 0) {
|
|
208
|
+
const lt = s.indexOf("<", this.i);
|
|
209
|
+
if (lt === -1)
|
|
210
|
+
throw new Error(`Expected "</" at position ${this.n}`);
|
|
211
|
+
this.i = lt;
|
|
212
|
+
const next = s.charCodeAt(lt + 1);
|
|
213
|
+
if (next === SLASH) {
|
|
214
|
+
this.i += 2;
|
|
215
|
+
const start = this.i;
|
|
216
|
+
this.skipName();
|
|
217
|
+
const length = this.i - start;
|
|
218
|
+
if (spans.length === 0) {
|
|
219
|
+
if (length !== name.length || !s.startsWith(name, start)) {
|
|
220
|
+
throw new Error(`Mismatched closing tag: expected </${name}>, got </${s.substring(start, this.i)}>`);
|
|
221
|
+
}
|
|
76
222
|
}
|
|
77
223
|
else {
|
|
78
|
-
|
|
224
|
+
const openLength = spans.pop();
|
|
225
|
+
const openStart = spans.pop();
|
|
226
|
+
if (!this.sameSpan(openStart, openLength, start, length)) {
|
|
227
|
+
throw new Error(`Mismatched closing tag: expected </${s.substring(openStart, openStart + openLength)}>, got </${s.substring(start, this.i)}>`);
|
|
228
|
+
}
|
|
79
229
|
}
|
|
230
|
+
this.skipWhitespace();
|
|
231
|
+
this.expectChar(GT, ">");
|
|
232
|
+
depth--;
|
|
233
|
+
}
|
|
234
|
+
else if (next === BANG && s.startsWith("<!--", lt)) {
|
|
235
|
+
this.skipComment();
|
|
236
|
+
}
|
|
237
|
+
else if (next === BANG && s.startsWith("<![CDATA[", lt)) {
|
|
238
|
+
const end = s.indexOf("]]>", lt + 9);
|
|
239
|
+
if (end === -1)
|
|
240
|
+
throw new Error("Unterminated CDATA section");
|
|
241
|
+
this.i = end + 3;
|
|
242
|
+
}
|
|
243
|
+
else if (next === QUESTION) {
|
|
244
|
+
this.skipPi();
|
|
80
245
|
}
|
|
81
246
|
else {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
247
|
+
this.i++;
|
|
248
|
+
const start = this.i;
|
|
249
|
+
this.skipName();
|
|
250
|
+
const length = this.i - start;
|
|
251
|
+
this.skipAttributes();
|
|
252
|
+
this.skipWhitespace();
|
|
253
|
+
if (s.charCodeAt(this.i) === SLASH && s.charCodeAt(this.i + 1) === GT) {
|
|
254
|
+
this.i += 2;
|
|
255
|
+
continue;
|
|
85
256
|
}
|
|
257
|
+
this.expectChar(GT, ">");
|
|
258
|
+
spans.push(start, length);
|
|
259
|
+
depth++;
|
|
86
260
|
}
|
|
87
261
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
262
|
+
}
|
|
263
|
+
sameSpan(aStart, aLength, bStart, bLength) {
|
|
264
|
+
if (aLength !== bLength)
|
|
265
|
+
return false;
|
|
266
|
+
const s = this.s;
|
|
267
|
+
for (let k = 0; k < aLength; k++) {
|
|
268
|
+
if (s.charCodeAt(aStart + k) !== s.charCodeAt(bStart + k))
|
|
269
|
+
return false;
|
|
93
270
|
}
|
|
94
|
-
|
|
95
|
-
this.expect(">");
|
|
96
|
-
const el = { name, attributes, children };
|
|
97
|
-
if (text !== undefined)
|
|
98
|
-
el.text = text;
|
|
99
|
-
return el;
|
|
271
|
+
return true;
|
|
100
272
|
}
|
|
101
273
|
readAttributes(attrs) {
|
|
102
|
-
|
|
274
|
+
const s = this.s;
|
|
275
|
+
while (this.i < this.n) {
|
|
103
276
|
this.skipWhitespace();
|
|
104
|
-
const ch =
|
|
105
|
-
if (ch ===
|
|
277
|
+
const ch = s.charCodeAt(this.i);
|
|
278
|
+
if (ch === GT || ch === SLASH)
|
|
106
279
|
return;
|
|
107
280
|
const key = this.readName();
|
|
108
281
|
this.skipWhitespace();
|
|
109
|
-
this.
|
|
282
|
+
this.expectChar(EQ, "=");
|
|
283
|
+
this.skipWhitespace();
|
|
284
|
+
attrs[key] = this.readAttrValue();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** Like readAttributes, but validates only; nothing is built. */
|
|
288
|
+
skipAttributes() {
|
|
289
|
+
const s = this.s;
|
|
290
|
+
while (this.i < this.n) {
|
|
291
|
+
this.skipWhitespace();
|
|
292
|
+
const ch = s.charCodeAt(this.i);
|
|
293
|
+
if (ch === GT || ch === SLASH)
|
|
294
|
+
return;
|
|
295
|
+
this.skipName();
|
|
296
|
+
this.skipWhitespace();
|
|
297
|
+
this.expectChar(EQ, "=");
|
|
110
298
|
this.skipWhitespace();
|
|
111
|
-
const
|
|
112
|
-
|
|
299
|
+
const quote = s.charCodeAt(this.i);
|
|
300
|
+
if (quote !== DQUOTE && quote !== SQUOTE) {
|
|
301
|
+
throw new Error(`Expected quote at position ${this.i}`);
|
|
302
|
+
}
|
|
303
|
+
this.i++;
|
|
304
|
+
const end = s.indexOf(quote === DQUOTE ? '"' : "'", this.i);
|
|
305
|
+
this.i = end === -1 ? this.n + 1 : end + 1;
|
|
113
306
|
}
|
|
114
307
|
}
|
|
115
308
|
readAttrValue() {
|
|
116
|
-
const quote = this.s
|
|
117
|
-
if (quote !==
|
|
309
|
+
const quote = this.s.charCodeAt(this.i);
|
|
310
|
+
if (quote !== DQUOTE && quote !== SQUOTE) {
|
|
118
311
|
throw new Error(`Expected quote at position ${this.i}`);
|
|
119
312
|
}
|
|
120
313
|
this.i++;
|
|
121
314
|
const start = this.i;
|
|
122
|
-
|
|
123
|
-
|
|
315
|
+
let end = this.s.indexOf(quote === DQUOTE ? '"' : "'", start);
|
|
316
|
+
if (end === -1)
|
|
317
|
+
end = this.n;
|
|
318
|
+
this.i = end + 1; // skip closing quote
|
|
319
|
+
return this.slice(start, end);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Substring with entities decoded. Values rarely contain "&", so instead of
|
|
323
|
+
* scanning each one, the position of the next "&" is remembered and only
|
|
324
|
+
* refreshed once the scan has moved past it.
|
|
325
|
+
*/
|
|
326
|
+
slice(start, end) {
|
|
327
|
+
let amp = this.nextAmp;
|
|
328
|
+
if (amp !== -1 && amp < start) {
|
|
329
|
+
amp = this.s.indexOf("&", start);
|
|
330
|
+
this.nextAmp = amp;
|
|
124
331
|
}
|
|
125
|
-
const value = this.s.substring(start,
|
|
126
|
-
|
|
127
|
-
return decodeXmlEntities(value);
|
|
332
|
+
const value = this.s.substring(start, end);
|
|
333
|
+
return amp !== -1 && amp < end ? decodeXmlEntities(value) : value;
|
|
128
334
|
}
|
|
129
335
|
readText() {
|
|
130
336
|
const start = this.i;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
337
|
+
let end = this.s.indexOf("<", start);
|
|
338
|
+
if (end === -1)
|
|
339
|
+
end = this.n;
|
|
340
|
+
this.i = end;
|
|
341
|
+
return this.slice(start, end);
|
|
342
|
+
}
|
|
343
|
+
skipText() {
|
|
344
|
+
const end = this.s.indexOf("<", this.i);
|
|
345
|
+
this.i = end === -1 ? this.n : end;
|
|
346
|
+
}
|
|
347
|
+
skipCData() {
|
|
348
|
+
const end = this.s.indexOf("]]>", this.i + 9);
|
|
349
|
+
if (end === -1)
|
|
350
|
+
throw new Error("Unterminated CDATA section");
|
|
351
|
+
this.i = end + 3;
|
|
135
352
|
}
|
|
136
353
|
readName() {
|
|
137
354
|
const start = this.i;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
355
|
+
this.skipName();
|
|
356
|
+
return this.s.substring(start, this.i);
|
|
357
|
+
}
|
|
358
|
+
/** Advance past a name, recording the position of its first ":" in `colon`. */
|
|
359
|
+
skipName() {
|
|
360
|
+
const s = this.s;
|
|
361
|
+
let i = this.i;
|
|
362
|
+
let colon = -1;
|
|
363
|
+
while (i < this.n) {
|
|
364
|
+
const c = s.charCodeAt(i);
|
|
365
|
+
if (c === SPACE || c === TAB || c === LF || c === CR || c === GT || c === SLASH || c === EQ)
|
|
147
366
|
break;
|
|
148
|
-
|
|
367
|
+
if (c === COLON && colon === -1)
|
|
368
|
+
colon = i;
|
|
369
|
+
i++;
|
|
149
370
|
}
|
|
150
|
-
|
|
371
|
+
this.colon = colon;
|
|
372
|
+
this.i = i;
|
|
151
373
|
}
|
|
152
374
|
readCData() {
|
|
153
375
|
this.i += 9; // skip <![CDATA[
|
|
@@ -161,65 +383,68 @@ class XmlReader {
|
|
|
161
383
|
skipPi() {
|
|
162
384
|
this.i += 2; // skip <?
|
|
163
385
|
const end = this.s.indexOf("?>", this.i);
|
|
164
|
-
this.i = end === -1 ? this.
|
|
386
|
+
this.i = end === -1 ? this.n : end + 2;
|
|
165
387
|
}
|
|
166
388
|
skipComment() {
|
|
167
389
|
this.i += 4; // skip <!--
|
|
168
390
|
const end = this.s.indexOf("-->", this.i);
|
|
169
|
-
this.i = end === -1 ? this.
|
|
391
|
+
this.i = end === -1 ? this.n : end + 3;
|
|
170
392
|
}
|
|
171
393
|
skipBang() {
|
|
172
394
|
// Skip <!DOCTYPE ...> and similar
|
|
173
395
|
this.i += 2;
|
|
174
396
|
let depth = 1;
|
|
175
|
-
while (this.i < this.
|
|
176
|
-
|
|
397
|
+
while (this.i < this.n && depth > 0) {
|
|
398
|
+
const c = this.s.charCodeAt(this.i);
|
|
399
|
+
if (c === LT)
|
|
177
400
|
depth++;
|
|
178
|
-
else if (
|
|
401
|
+
else if (c === GT)
|
|
179
402
|
depth--;
|
|
180
403
|
this.i++;
|
|
181
404
|
}
|
|
182
405
|
}
|
|
183
406
|
skipWhitespace() {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
407
|
+
const s = this.s;
|
|
408
|
+
let i = this.i;
|
|
409
|
+
while (i < this.n) {
|
|
410
|
+
const c = s.charCodeAt(i);
|
|
411
|
+
if (c !== SPACE && c !== TAB && c !== LF && c !== CR)
|
|
187
412
|
break;
|
|
188
|
-
|
|
413
|
+
i++;
|
|
189
414
|
}
|
|
415
|
+
this.i = i;
|
|
190
416
|
}
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
throw new Error(`Expected "${
|
|
417
|
+
expectChar(code, shown) {
|
|
418
|
+
if (this.s.charCodeAt(this.i) !== code) {
|
|
419
|
+
throw new Error(`Expected "${shown}" at position ${this.i}`);
|
|
194
420
|
}
|
|
195
|
-
this.i
|
|
196
|
-
}
|
|
197
|
-
startsWith(prefix) {
|
|
198
|
-
return this.s.startsWith(prefix, this.i);
|
|
421
|
+
this.i++;
|
|
199
422
|
}
|
|
200
423
|
}
|
|
201
424
|
// ---------------------------------------------------------------------------
|
|
202
425
|
// Entity helpers
|
|
203
426
|
// ---------------------------------------------------------------------------
|
|
427
|
+
const ENTITY_RE = /&(?:amp|lt|gt|quot|apos|#x[0-9a-fA-F]+|#[0-9]+);/g;
|
|
428
|
+
function decodeEntity(m) {
|
|
429
|
+
if (m === "&")
|
|
430
|
+
return "&";
|
|
431
|
+
if (m === "<")
|
|
432
|
+
return "<";
|
|
433
|
+
if (m === ">")
|
|
434
|
+
return ">";
|
|
435
|
+
if (m === """)
|
|
436
|
+
return '"';
|
|
437
|
+
if (m === "'")
|
|
438
|
+
return "'";
|
|
439
|
+
if (m.startsWith("&#x"))
|
|
440
|
+
return String.fromCodePoint(Number.parseInt(m.slice(3, -1), 16));
|
|
441
|
+
return String.fromCodePoint(Number.parseInt(m.slice(2, -1), 10));
|
|
442
|
+
}
|
|
204
443
|
/** Decode XML predefined and numeric character entities in a string. */
|
|
205
444
|
function decodeXmlEntities(s) {
|
|
206
445
|
if (!s.includes("&"))
|
|
207
446
|
return s;
|
|
208
|
-
return s.replace(
|
|
209
|
-
if (m === "&")
|
|
210
|
-
return "&";
|
|
211
|
-
if (m === "<")
|
|
212
|
-
return "<";
|
|
213
|
-
if (m === ">")
|
|
214
|
-
return ">";
|
|
215
|
-
if (m === """)
|
|
216
|
-
return '"';
|
|
217
|
-
if (m === "'")
|
|
218
|
-
return "'";
|
|
219
|
-
if (m.startsWith("&#x"))
|
|
220
|
-
return String.fromCodePoint(Number.parseInt(m.slice(3, -1), 16));
|
|
221
|
-
return String.fromCodePoint(Number.parseInt(m.slice(2, -1), 10));
|
|
222
|
-
});
|
|
447
|
+
return s.replace(ENTITY_RE, decodeEntity);
|
|
223
448
|
}
|
|
224
449
|
// ---------------------------------------------------------------------------
|
|
225
450
|
// Serializer
|
|
@@ -229,48 +454,74 @@ function decodeXmlEntities(s) {
|
|
|
229
454
|
* Produces a well-formed XML document with declaration.
|
|
230
455
|
*/
|
|
231
456
|
export function serializeXml(element) {
|
|
232
|
-
|
|
233
|
-
writeElement(parts, element, 0);
|
|
234
|
-
parts.push("\n");
|
|
235
|
-
return parts.join("");
|
|
457
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n${writeElement(element, 0)}\n`;
|
|
236
458
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
459
|
+
/** Indentation strings by depth, built once and reused across documents. */
|
|
460
|
+
const INDENTS = [];
|
|
461
|
+
function indentFor(depth) {
|
|
462
|
+
let s = INDENTS[depth];
|
|
463
|
+
if (s === undefined) {
|
|
464
|
+
s = " ".repeat(depth);
|
|
465
|
+
INDENTS[depth] = s;
|
|
466
|
+
}
|
|
467
|
+
return s;
|
|
468
|
+
}
|
|
469
|
+
function writeElement(el, depth) {
|
|
470
|
+
const indent = indentFor(depth);
|
|
471
|
+
let out = `${indent}<${el.name}`;
|
|
472
|
+
const attributes = el.attributes;
|
|
473
|
+
for (const key in attributes) {
|
|
474
|
+
const value = attributes[key];
|
|
241
475
|
// el.attributes is typed Record<string, string>, but content built from
|
|
242
476
|
// generated/untyped code can leave a value undefined at runtime — skip
|
|
243
477
|
// rather than crash the whole serialization on one bad attribute.
|
|
244
478
|
if (typeof value !== "string")
|
|
245
479
|
continue;
|
|
246
|
-
|
|
480
|
+
out += ` ${key}="${escapeAttr(value)}"`;
|
|
247
481
|
}
|
|
248
|
-
const
|
|
482
|
+
const children = el.children;
|
|
483
|
+
const hasChildren = children.length > 0;
|
|
249
484
|
const hasText = el.text !== undefined;
|
|
250
|
-
if (!hasChildren && !hasText)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (hasText) {
|
|
256
|
-
parts.push(escapeText(el.text));
|
|
257
|
-
}
|
|
485
|
+
if (!hasChildren && !hasText)
|
|
486
|
+
return `${out}/>\n`;
|
|
487
|
+
out += ">";
|
|
488
|
+
if (hasText)
|
|
489
|
+
out += escapeText(el.text);
|
|
258
490
|
if (hasChildren) {
|
|
259
|
-
|
|
260
|
-
for (const child of
|
|
261
|
-
writeElement(
|
|
262
|
-
|
|
263
|
-
parts.push(indent);
|
|
491
|
+
out += "\n";
|
|
492
|
+
for (const child of children)
|
|
493
|
+
out += writeElement(child, depth + 1);
|
|
494
|
+
out += indent;
|
|
264
495
|
}
|
|
265
|
-
|
|
496
|
+
return `${out}</${el.name}>\n`;
|
|
497
|
+
}
|
|
498
|
+
// One regex test decides whether a value needs escaping at all; most do not,
|
|
499
|
+
// and the ones that do are rewritten in a single pass instead of six.
|
|
500
|
+
const ATTR_ESCAPE_RE = /[&<"\n\r\t]/;
|
|
501
|
+
const ATTR_ESCAPE_ALL_RE = /[&<"\n\r\t]/g;
|
|
502
|
+
const TEXT_ESCAPE_RE = /[&<>]/;
|
|
503
|
+
const TEXT_ESCAPE_ALL_RE = /[&<>]/g;
|
|
504
|
+
const ESCAPES = {
|
|
505
|
+
"&": "&",
|
|
506
|
+
"<": "<",
|
|
507
|
+
">": ">",
|
|
508
|
+
'"': """,
|
|
509
|
+
// Whitespace that XML parsers would normalize in attribute values.
|
|
510
|
+
"\n": " ",
|
|
511
|
+
"\r": " ",
|
|
512
|
+
"\t": "	",
|
|
513
|
+
};
|
|
514
|
+
function escapeChar(ch) {
|
|
515
|
+
return ESCAPES[ch] ?? ch;
|
|
266
516
|
}
|
|
267
517
|
function escapeAttr(value) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return s;
|
|
518
|
+
if (!ATTR_ESCAPE_RE.test(value))
|
|
519
|
+
return value;
|
|
520
|
+
return value.replace(ATTR_ESCAPE_ALL_RE, escapeChar);
|
|
272
521
|
}
|
|
273
522
|
function escapeText(value) {
|
|
274
|
-
|
|
523
|
+
if (!TEXT_ESCAPE_RE.test(value))
|
|
524
|
+
return value;
|
|
525
|
+
return value.replace(TEXT_ESCAPE_ALL_RE, escapeChar);
|
|
275
526
|
}
|
|
276
527
|
//# sourceMappingURL=xml-parser.js.map
|