@llm4ts/shell 2.11.0 → 2.13.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.
Files changed (61) hide show
  1. package/flows/epic-stories.js +25 -5
  2. package/flows/lib/epic-stories.js +76 -8
  3. package/kits/soap-ace/README.md +313 -0
  4. package/kits/soap-ace/fixtures/demo-bank-soap/RUNBOOK.md +102 -0
  5. package/kits/soap-ace/fixtures/demo-bank-soap/design/api-design.md +620 -0
  6. package/kits/soap-ace/fixtures/demo-bank-soap/responses/cercaConti.xml +3 -0
  7. package/kits/soap-ace/fixtures/demo-bank-soap/responses/cercaMovimenti.xml +3 -0
  8. package/kits/soap-ace/fixtures/demo-bank-soap/responses/confermaBonifico.xml +3 -0
  9. package/kits/soap-ace/fixtures/demo-bank-soap/responses/dettaglioConto.xml +3 -0
  10. package/kits/soap-ace/fixtures/demo-bank-soap/responses/inserisciBonifico.xml +3 -0
  11. package/kits/soap-ace/fixtures/demo-bank-soap/responses/revocaBonifico.xml +3 -0
  12. package/kits/soap-ace/fixtures/demo-bank-soap/soapui/DemoBank-soapui-project.xml +39 -0
  13. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/DemoBank.wsdl +186 -0
  14. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/LegacyRpc.wsdl +29 -0
  15. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/banking.xsd +10 -0
  16. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/bonifici.xsd +107 -0
  17. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/common.xsd +93 -0
  18. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/conti.xsd +71 -0
  19. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/movimenti.xsd +55 -0
  20. package/kits/soap-ace/flows/lib/soap/Analysis.js +508 -0
  21. package/kits/soap-ace/flows/lib/soap/Auth.js +179 -0
  22. package/kits/soap-ace/flows/lib/soap/Call.js +186 -0
  23. package/kits/soap-ace/flows/lib/soap/CallPolicy.js +50 -0
  24. package/kits/soap-ace/flows/lib/soap/Catalog.js +157 -0
  25. package/kits/soap-ace/flows/lib/soap/Classification.js +254 -0
  26. package/kits/soap-ace/flows/lib/soap/Design.js +497 -0
  27. package/kits/soap-ace/flows/lib/soap/DesignProposal.js +204 -0
  28. package/kits/soap-ace/flows/lib/soap/Discover.js +139 -0
  29. package/kits/soap-ace/flows/lib/soap/Envelope.js +132 -0
  30. package/kits/soap-ace/flows/lib/soap/Epic.js +239 -0
  31. package/kits/soap-ace/flows/lib/soap/Instance.js +675 -0
  32. package/kits/soap-ace/flows/lib/soap/Mapping.js +130 -0
  33. package/kits/soap-ace/flows/lib/soap/Masking.js +508 -0
  34. package/kits/soap-ace/flows/lib/soap/OpenApi.js +288 -0
  35. package/kits/soap-ace/flows/lib/soap/Samples.js +287 -0
  36. package/kits/soap-ace/flows/lib/soap/Scenarios.js +132 -0
  37. package/kits/soap-ace/flows/lib/soap/SoapUiImport.js +200 -0
  38. package/kits/soap-ace/flows/lib/soap/Stub.js +34 -0
  39. package/kits/soap-ace/flows/lib/soap/Transport.js +167 -0
  40. package/kits/soap-ace/flows/lib/soap/Wsdl.js +533 -0
  41. package/kits/soap-ace/flows/lib/soap/Xml.js +453 -0
  42. package/kits/soap-ace/flows/lib/soap/Xsd.js +471 -0
  43. package/kits/soap-ace/flows/lib/soap/Yaml.js +309 -0
  44. package/kits/soap-ace/flows/soap-design.js +167 -0
  45. package/kits/soap-ace/flows/soap-discover.js +85 -0
  46. package/kits/soap-ace/flows/soap-epic.js +204 -0
  47. package/kits/soap-ace/flows/soap-sample.js +265 -0
  48. package/kits/soap-ace/packs/ace12-rest/api-style.md +63 -0
  49. package/kits/soap-ace/packs/ace12-rest/pack.md +18 -0
  50. package/kits/soap-ace/packs/ace12-rest/prompts/implement.md +8 -0
  51. package/kits/soap-ace/packs/ace12-rest/reviewers/contract.md +5 -0
  52. package/kits/soap-ace/patterns/PAT-ACE-001-esito-to-problem.md +8 -0
  53. package/kits/soap-ace/patterns/PAT-ACE-002-fault-to-problem.md +8 -0
  54. package/kits/soap-ace/patterns/PAT-ACE-003-amounts-and-enums.md +8 -0
  55. package/kits/soap-ace/patterns/PAT-ACE-004-pagination.md +7 -0
  56. package/kits/soap-ace/patterns/PAT-ACE-005-nil-and-absence.md +7 -0
  57. package/kits/soap-ace/scaffolds/ace12-rest-api/CONTRIBUTING.md +56 -0
  58. package/kits/soap-ace/scaffolds/ace12-rest-api/README.md +16 -0
  59. package/kits/soap-ace/scaffolds/ace12-rest-api/scripts/ace-gates-container.sh +8 -0
  60. package/kits/soap-ace/scaffolds/ace12-rest-api/scripts/ace-gates.sh +41 -0
  61. package/package.json +4 -4
@@ -0,0 +1,453 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+ // A strict reader for the narrow XML profile SOAP estates use: WSDL, XSD,
4
+ // SOAP envelopes, and SoapUI project files. It is deliberately not a general
5
+ // XML parser. Document type declarations are refused outright, so external
6
+ // entities (XXE) and entity expansion attacks are impossible by construction;
7
+ // only the five predefined entities and numeric character references are
8
+ // understood. Namespaces are resolved while parsing, and every element keeps
9
+ // its in-scope bindings so QName-valued attributes (`type="tns:Conto"`) can be
10
+ // resolved later.
11
+ export const XmlNamespace = "http://www.w3.org/XML/1998/namespace";
12
+ const XmlnsNamespace = "http://www.w3.org/2000/xmlns/";
13
+ export class XmlError extends Schema.TaggedError()("XmlError", {
14
+ reason: Schema.Literals(["doctype", "encoding", "syntax", "namespace", "limit"]),
15
+ detail: Schema.String,
16
+ line: Schema.optionalKey(Schema.Int),
17
+ source: Schema.optionalKey(Schema.String)
18
+ }) {
19
+ get message() {
20
+ const where = this.source === undefined ? "" : `${this.source}`;
21
+ const line = this.line === undefined ? "" : `:${this.line}`;
22
+ const prefix = where === "" && line === "" ? "" : `${where}${line}: `;
23
+ return `${prefix}${this.reason}: ${this.detail}`;
24
+ }
25
+ }
26
+ export const defaultXmlLimits = { maxDepth: 256, maxChars: 32 * 1024 * 1024 };
27
+ // ---------------------------------------------------------------------------
28
+ // Decoding
29
+ const encodingDeclaration = /^<\?xml[^>]*?\sencoding\s*=\s*["']([A-Za-z0-9._-]+)["']/;
30
+ const supportedEncodings = new Map([
31
+ ["utf-8", "utf-8"],
32
+ ["utf8", "utf-8"],
33
+ ["us-ascii", "utf-8"],
34
+ ["ascii", "utf-8"],
35
+ ["iso-8859-1", "latin1"],
36
+ ["latin1", "latin1"],
37
+ ["latin-1", "latin1"],
38
+ ["iso-8859-15", "iso-8859-15"],
39
+ ["windows-1252", "windows-1252"],
40
+ ["cp1252", "windows-1252"],
41
+ ["utf-16", "utf-16le"],
42
+ ["utf-16le", "utf-16le"],
43
+ ["utf-16be", "utf-16be"]
44
+ ]);
45
+ const startsWith = (bytes, prefix) => prefix.every((value, index) => bytes[index] === value);
46
+ /**
47
+ * Decode raw document bytes by BOM, then by the XML declaration's
48
+ * `encoding`, defaulting to UTF-8. Undecodable input is an `XmlError`, never
49
+ * replacement characters.
50
+ */
51
+ export const decodeXml = (bytes, source) => Effect.suspend(() => {
52
+ const at = source === undefined ? {} : { source };
53
+ let label = "utf-8";
54
+ let offset = 0;
55
+ if (startsWith(bytes, [0xef, 0xbb, 0xbf])) {
56
+ offset = 3;
57
+ }
58
+ else if (startsWith(bytes, [0xff, 0xfe])) {
59
+ label = "utf-16le";
60
+ offset = 2;
61
+ }
62
+ else if (startsWith(bytes, [0xfe, 0xff])) {
63
+ label = "utf-16be";
64
+ offset = 2;
65
+ }
66
+ else {
67
+ const head = String.fromCharCode(...bytes.subarray(0, 200));
68
+ const declared = encodingDeclaration.exec(head)?.[1]?.toLowerCase();
69
+ if (declared !== undefined) {
70
+ const mapped = supportedEncodings.get(declared);
71
+ if (mapped === undefined) {
72
+ return Effect.fail(new XmlError({ reason: "encoding", detail: `unsupported encoding ${declared}`, ...at }));
73
+ }
74
+ label = mapped;
75
+ }
76
+ }
77
+ return Effect.try({
78
+ try: () => new TextDecoder(label, { fatal: true }).decode(bytes.subarray(offset)),
79
+ catch: () => new XmlError({ reason: "encoding", detail: `bytes are not valid ${label}`, ...at })
80
+ });
81
+ });
82
+ // ---------------------------------------------------------------------------
83
+ // Parsing
84
+ const namePattern = /[A-Za-z_À-￿][A-Za-z0-9._\-·À-￿]*(?::[A-Za-z_À-￿][A-Za-z0-9._\-·À-￿]*)?/y;
85
+ const predefinedEntities = {
86
+ lt: "<",
87
+ gt: ">",
88
+ amp: "&",
89
+ quot: '"',
90
+ apos: "'"
91
+ };
92
+ const splitQName = (qname) => {
93
+ const colon = qname.indexOf(":");
94
+ return colon < 0
95
+ ? { prefix: "", local: qname }
96
+ : { prefix: qname.slice(0, colon), local: qname.slice(colon + 1) };
97
+ };
98
+ class Parser {
99
+ position = 0;
100
+ text;
101
+ limits;
102
+ source;
103
+ constructor(text, limits, source) {
104
+ this.text = text;
105
+ this.limits = limits;
106
+ this.source = source;
107
+ }
108
+ fail(reason, detail, at = this.position) {
109
+ throw new XmlError({
110
+ reason,
111
+ detail,
112
+ line: this.lineAt(Math.min(at, this.text.length)),
113
+ ...(this.source === undefined ? {} : { source: this.source })
114
+ });
115
+ }
116
+ lineAt(at) {
117
+ let line = 1;
118
+ for (let index = 0; index < at; index++) {
119
+ if (this.text.charCodeAt(index) === 10)
120
+ line++;
121
+ }
122
+ return line;
123
+ }
124
+ parseDocument() {
125
+ if (this.text.length > this.limits.maxChars) {
126
+ this.fail("limit", `document exceeds ${this.limits.maxChars} characters`, 0);
127
+ }
128
+ this.skipMisc();
129
+ if (!this.text.startsWith("<", this.position) || this.position >= this.text.length) {
130
+ this.fail("syntax", "expected a root element");
131
+ }
132
+ const root = this.parseElement(new Map([
133
+ ["xml", XmlNamespace],
134
+ ["", ""]
135
+ ]), 1);
136
+ this.skipMisc();
137
+ if (this.position < this.text.length) {
138
+ this.fail("syntax", "content after the root element");
139
+ }
140
+ return root;
141
+ }
142
+ /** Whitespace, comments, and processing instructions (including the XML declaration). */
143
+ skipMisc() {
144
+ for (;;) {
145
+ this.skipWhitespace();
146
+ if (this.text.startsWith("<?", this.position)) {
147
+ this.skipProcessingInstruction();
148
+ }
149
+ else if (this.text.startsWith("<!--", this.position)) {
150
+ this.skipComment();
151
+ }
152
+ else if (this.text.startsWith("<!", this.position)) {
153
+ this.rejectDeclaration();
154
+ }
155
+ else {
156
+ return;
157
+ }
158
+ }
159
+ }
160
+ rejectDeclaration() {
161
+ if (this.text.startsWith("<!DOCTYPE", this.position)) {
162
+ this.fail("doctype", "document type declarations are not accepted");
163
+ }
164
+ return this.fail("syntax", "unexpected markup declaration");
165
+ }
166
+ skipWhitespace() {
167
+ while (this.position < this.text.length && /\s/.test(this.text[this.position] ?? "")) {
168
+ this.position++;
169
+ }
170
+ }
171
+ skipProcessingInstruction() {
172
+ const end = this.text.indexOf("?>", this.position + 2);
173
+ if (end < 0)
174
+ this.fail("syntax", "unterminated processing instruction");
175
+ this.position = end + 2;
176
+ }
177
+ skipComment() {
178
+ const end = this.text.indexOf("-->", this.position + 4);
179
+ if (end < 0)
180
+ this.fail("syntax", "unterminated comment");
181
+ this.position = end + 3;
182
+ }
183
+ readName() {
184
+ namePattern.lastIndex = this.position;
185
+ const match = namePattern.exec(this.text);
186
+ if (match === null)
187
+ this.fail("syntax", "expected a name");
188
+ this.position += match[0].length;
189
+ return match[0];
190
+ }
191
+ decodeText(raw, start) {
192
+ if (!raw.includes("&"))
193
+ return raw;
194
+ return raw.replace(/&([^;&\s]*);?/g, (whole, body) => {
195
+ if (!whole.endsWith(";"))
196
+ this.fail("syntax", `unterminated reference &${body}`, start);
197
+ if (/^#x[0-9A-Fa-f]+$/.test(body)) {
198
+ return this.codePoint(Number.parseInt(body.slice(2), 16), whole, start);
199
+ }
200
+ if (/^#[0-9]+$/.test(body)) {
201
+ return this.codePoint(Number.parseInt(body.slice(1), 10), whole, start);
202
+ }
203
+ if (body.startsWith("#"))
204
+ this.fail("syntax", `invalid character reference ${whole}`, start);
205
+ const value = predefinedEntities[body];
206
+ if (value === undefined)
207
+ this.fail("syntax", `undeclared entity ${whole}`, start);
208
+ return value;
209
+ });
210
+ }
211
+ codePoint(code, whole, start) {
212
+ if (!Number.isInteger(code) || code <= 0 || code > 0x10ffff) {
213
+ this.fail("syntax", `invalid character reference ${whole}`, start);
214
+ }
215
+ return String.fromCodePoint(code);
216
+ }
217
+ readAttributes() {
218
+ const attributes = [];
219
+ for (;;) {
220
+ const before = this.position;
221
+ this.skipWhitespace();
222
+ const next = this.text[this.position];
223
+ if (next === ">" || next === "/" || next === undefined)
224
+ return attributes;
225
+ if (this.position === before)
226
+ this.fail("syntax", "expected whitespace before attribute");
227
+ const qname = this.readName();
228
+ this.skipWhitespace();
229
+ if (this.text[this.position] !== "=")
230
+ this.fail("syntax", `expected = after ${qname}`);
231
+ this.position++;
232
+ this.skipWhitespace();
233
+ const quote = this.text[this.position];
234
+ if (quote !== '"' && quote !== "'")
235
+ this.fail("syntax", `expected quoted value for ${qname}`);
236
+ const end = this.text.indexOf(quote, this.position + 1);
237
+ if (end < 0)
238
+ this.fail("syntax", `unterminated value for ${qname}`);
239
+ const raw = this.text.slice(this.position + 1, end);
240
+ if (raw.includes("<"))
241
+ this.fail("syntax", `'<' in value of ${qname}`);
242
+ const value = this.decodeText(raw, this.position);
243
+ this.position = end + 1;
244
+ if (attributes.some((attribute) => attribute.qname === qname)) {
245
+ this.fail("syntax", `duplicate attribute ${qname}`);
246
+ }
247
+ attributes.push({ qname, value });
248
+ }
249
+ }
250
+ parseElement(parentScope, depth) {
251
+ if (depth > this.limits.maxDepth) {
252
+ this.fail("limit", `nesting exceeds ${this.limits.maxDepth} levels`);
253
+ }
254
+ const start = this.position;
255
+ this.position++; // "<"
256
+ const qname = this.readName();
257
+ const raw = this.readAttributes();
258
+ const declarations = raw.filter((attribute) => attribute.qname === "xmlns" || attribute.qname.startsWith("xmlns:"));
259
+ let scope = parentScope;
260
+ if (declarations.length > 0) {
261
+ const extended = new Map(parentScope);
262
+ for (const attribute of declarations) {
263
+ const prefix = attribute.qname === "xmlns" ? "" : attribute.qname.slice(6);
264
+ if (prefix !== "" && attribute.value === "") {
265
+ this.fail("namespace", `prefix ${prefix} cannot be unbound`, start);
266
+ }
267
+ extended.set(prefix, attribute.value);
268
+ }
269
+ scope = extended;
270
+ }
271
+ const resolve = (name, isAttribute) => {
272
+ const { prefix, local } = splitQName(name);
273
+ if (prefix === "") {
274
+ return { name: { namespace: isAttribute ? "" : (scope.get("") ?? ""), local }, prefix };
275
+ }
276
+ if (prefix === "xmlns")
277
+ return { name: { namespace: XmlnsNamespace, local }, prefix };
278
+ const namespace = scope.get(prefix);
279
+ if (namespace === undefined)
280
+ this.fail("namespace", `unbound prefix ${prefix}`, start);
281
+ return { name: { namespace, local }, prefix };
282
+ };
283
+ const element = resolve(qname, false);
284
+ const attributes = raw
285
+ .filter((attribute) => !declarations.includes(attribute))
286
+ .map((attribute) => {
287
+ const resolved = resolve(attribute.qname, true);
288
+ return { name: resolved.name, prefix: resolved.prefix, value: attribute.value };
289
+ });
290
+ const line = this.lineAt(start);
291
+ if (this.text.startsWith("/>", this.position)) {
292
+ this.position += 2;
293
+ return {
294
+ _tag: "element",
295
+ name: element.name,
296
+ prefix: element.prefix,
297
+ attributes,
298
+ children: [],
299
+ scope,
300
+ line
301
+ };
302
+ }
303
+ if (this.text[this.position] !== ">")
304
+ this.fail("syntax", `malformed start tag <${qname}>`);
305
+ this.position++;
306
+ const children = [];
307
+ let text = "";
308
+ const flush = () => {
309
+ if (text !== "") {
310
+ children.push({ _tag: "text", value: text });
311
+ text = "";
312
+ }
313
+ };
314
+ for (;;) {
315
+ if (this.position >= this.text.length)
316
+ this.fail("syntax", `unclosed element <${qname}>`);
317
+ const next = this.text.indexOf("<", this.position);
318
+ if (next < 0)
319
+ this.fail("syntax", `unclosed element <${qname}>`);
320
+ if (next > this.position) {
321
+ const chunk = this.text.slice(this.position, next);
322
+ text += this.decodeText(chunk, this.position);
323
+ this.position = next;
324
+ }
325
+ if (this.text.startsWith("</", this.position)) {
326
+ this.position += 2;
327
+ const closing = this.readName();
328
+ this.skipWhitespace();
329
+ if (this.text[this.position] !== ">")
330
+ this.fail("syntax", `malformed end tag </${closing}>`);
331
+ this.position++;
332
+ if (closing !== qname) {
333
+ this.fail("syntax", `</${closing}> does not close <${qname}>`);
334
+ }
335
+ flush();
336
+ return {
337
+ _tag: "element",
338
+ name: element.name,
339
+ prefix: element.prefix,
340
+ attributes,
341
+ children,
342
+ scope,
343
+ line
344
+ };
345
+ }
346
+ if (this.text.startsWith("<![CDATA[", this.position)) {
347
+ const end = this.text.indexOf("]]>", this.position + 9);
348
+ if (end < 0)
349
+ this.fail("syntax", "unterminated CDATA section");
350
+ text += this.text.slice(this.position + 9, end);
351
+ this.position = end + 3;
352
+ }
353
+ else if (this.text.startsWith("<!--", this.position)) {
354
+ this.skipComment();
355
+ }
356
+ else if (this.text.startsWith("<?", this.position)) {
357
+ this.skipProcessingInstruction();
358
+ }
359
+ else if (this.text.startsWith("<!", this.position)) {
360
+ this.rejectDeclaration();
361
+ }
362
+ else {
363
+ flush();
364
+ children.push(this.parseElement(scope, depth + 1));
365
+ }
366
+ }
367
+ }
368
+ }
369
+ /** Parse an already-decoded document into its root element. */
370
+ export const parseXml = (text, options = {}) => Effect.try({
371
+ // XML end-of-line handling: CRLF and CR become LF before parsing.
372
+ try: () => new Parser(text.replace(/\r\n?/g, "\n"), options.limits ?? defaultXmlLimits, options.source).parseDocument(),
373
+ catch: (cause) => cause instanceof XmlError
374
+ ? cause
375
+ : new XmlError({
376
+ reason: "syntax",
377
+ detail: "unreadable document",
378
+ ...(options.source === undefined ? {} : { source: options.source })
379
+ })
380
+ });
381
+ /** Decode bytes by their declared encoding, then parse. */
382
+ export const parseXmlBytes = (bytes, options = {}) => Effect.flatMap(decodeXml(bytes, options.source), (text) => parseXml(text, options));
383
+ // ---------------------------------------------------------------------------
384
+ // Navigation helpers
385
+ export const isElement = (node) => node._tag === "element";
386
+ export const elements = (element) => element.children.filter(isElement);
387
+ export const is = (element, namespace, local) => element.name.namespace === namespace && element.name.local === local;
388
+ export const childrenNamed = (element, namespace, local) => elements(element).filter((child) => is(child, namespace, local));
389
+ export const firstChild = (element, namespace, local) => elements(element).find((child) => is(child, namespace, local));
390
+ /** An unqualified attribute's value (the usual case in WSDL and XSD). */
391
+ export const attribute = (element, local, namespace = "") => element.attributes.find((candidate) => candidate.name.local === local && candidate.name.namespace === namespace)?.value;
392
+ /** Concatenated text content of the element's direct text children. */
393
+ export const textOf = (element) => element.children
394
+ .filter((child) => child._tag === "text")
395
+ .map((child) => child.value)
396
+ .join("");
397
+ /**
398
+ * Resolve a QName-valued attribute (`tns:Conto`) against the element's
399
+ * in-scope bindings; an unprefixed value takes the default namespace, as
400
+ * XSD's `type`/`ref`/`base` do.
401
+ */
402
+ export const resolveQName = (element, value) => {
403
+ const { prefix, local } = splitQName(value.trim());
404
+ const namespace = element.scope.get(prefix);
405
+ return namespace === undefined ? undefined : { namespace, local };
406
+ };
407
+ // ---------------------------------------------------------------------------
408
+ // Writing
409
+ export const escapeText = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
410
+ export const escapeAttribute = (value) => escapeText(value).replace(/"/g, "&quot;");
411
+ const qualifiedName = (prefix, local) => prefix === "" ? local : `${prefix}:${local}`;
412
+ /**
413
+ * Serialize an element tree. Namespace declarations are re-emitted where an
414
+ * element's scope differs from its parent's, so a parsed tree round-trips to
415
+ * an equivalent document (whitespace and prefixes kept, comments dropped).
416
+ */
417
+ export const renderXml = (root, options = {}) => {
418
+ const out = [];
419
+ if (options.declaration === true)
420
+ out.push('<?xml version="1.0" encoding="UTF-8"?>\n');
421
+ const walk = (element, parentScope) => {
422
+ const declarations = [];
423
+ for (const [prefix, namespace] of element.scope) {
424
+ if (prefix === "xml")
425
+ continue;
426
+ if (parentScope.get(prefix) !== namespace) {
427
+ declarations.push(prefix === ""
428
+ ? ` xmlns="${escapeAttribute(namespace)}"`
429
+ : ` xmlns:${prefix}="${escapeAttribute(namespace)}"`);
430
+ }
431
+ }
432
+ const attributes = element.attributes.map((attribute) => ` ${qualifiedName(attribute.prefix, attribute.name.local)}="${escapeAttribute(attribute.value)}"`);
433
+ const name = qualifiedName(element.prefix, element.name.local);
434
+ out.push(`<${name}${declarations.join("")}${attributes.join("")}`);
435
+ if (element.children.length === 0) {
436
+ out.push("/>");
437
+ return;
438
+ }
439
+ out.push(">");
440
+ for (const child of element.children) {
441
+ if (child._tag === "text")
442
+ out.push(escapeText(child.value));
443
+ else
444
+ walk(child, element.scope);
445
+ }
446
+ out.push(`</${name}>`);
447
+ };
448
+ walk(root, new Map([
449
+ ["xml", XmlNamespace],
450
+ ["", ""]
451
+ ]));
452
+ return out.join("");
453
+ };