@miy2/xml-api 0.9.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.
@@ -0,0 +1,47 @@
1
+ import type { CST } from "../cst/xml-cst";
2
+ export type NodeId = string;
3
+ export declare enum ModelNodeType {
4
+ Element = "Element",
5
+ Text = "Text",
6
+ Comment = "Comment",
7
+ CDATA = "CDATA"
8
+ }
9
+ export declare abstract class ModelNode {
10
+ readonly id: NodeId;
11
+ parent: ModelElement | null;
12
+ cst: CST | null;
13
+ constructor();
14
+ abstract getType(): ModelNodeType;
15
+ abstract clone(preserveId?: boolean): ModelNode;
16
+ protected cloneBase(target: ModelNode, preserveId: boolean): void;
17
+ }
18
+ export declare class ModelElement extends ModelNode {
19
+ tagName: string;
20
+ attributes: Map<string, string>;
21
+ children: ModelNode[];
22
+ constructor(tagName: string);
23
+ getType(): ModelNodeType;
24
+ clone(preserveId?: boolean): ModelElement;
25
+ addChild(node: ModelNode): void;
26
+ setAttribute(key: string, value: string): void;
27
+ find(tagName: string): ModelElement[];
28
+ text(): string;
29
+ }
30
+ export declare class ModelText extends ModelNode {
31
+ text: string;
32
+ constructor(text: string);
33
+ getType(): ModelNodeType;
34
+ clone(preserveId?: boolean): ModelText;
35
+ }
36
+ export declare class ModelComment extends ModelNode {
37
+ content: string;
38
+ constructor(content: string);
39
+ getType(): ModelNodeType;
40
+ clone(preserveId?: boolean): ModelComment;
41
+ }
42
+ export declare class ModelCDATA extends ModelNode {
43
+ content: string;
44
+ constructor(content: string);
45
+ getType(): ModelNodeType;
46
+ clone(preserveId?: boolean): ModelCDATA;
47
+ }
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ModelCDATA = exports.ModelComment = exports.ModelText = exports.ModelElement = exports.ModelNode = exports.ModelNodeType = void 0;
4
+ var ModelNodeType;
5
+ (function (ModelNodeType) {
6
+ ModelNodeType["Element"] = "Element";
7
+ ModelNodeType["Text"] = "Text";
8
+ ModelNodeType["Comment"] = "Comment";
9
+ ModelNodeType["CDATA"] = "CDATA";
10
+ })(ModelNodeType || (exports.ModelNodeType = ModelNodeType = {}));
11
+ class ModelNode {
12
+ constructor() {
13
+ this.parent = null;
14
+ this.cst = null;
15
+ this.id = crypto.randomUUID();
16
+ }
17
+ cloneBase(target, preserveId) {
18
+ if (preserveId) {
19
+ // @ts-ignore
20
+ target.id = this.id;
21
+ }
22
+ target.cst = this.cst;
23
+ }
24
+ }
25
+ exports.ModelNode = ModelNode;
26
+ class ModelElement extends ModelNode {
27
+ constructor(tagName) {
28
+ super();
29
+ this.attributes = new Map();
30
+ this.children = [];
31
+ this.tagName = tagName;
32
+ }
33
+ getType() {
34
+ return ModelNodeType.Element;
35
+ }
36
+ clone(preserveId = false) {
37
+ const clone = new ModelElement(this.tagName);
38
+ this.cloneBase(clone, preserveId);
39
+ clone.attributes = new Map(this.attributes);
40
+ clone.children = this.children.map((c) => {
41
+ const cClone = c.clone(preserveId);
42
+ cClone.parent = clone;
43
+ return cClone;
44
+ });
45
+ return clone;
46
+ }
47
+ addChild(node) {
48
+ node.parent = this;
49
+ this.children.push(node);
50
+ }
51
+ setAttribute(key, value) {
52
+ this.attributes.set(key, value);
53
+ }
54
+ find(tagName) {
55
+ let results = [];
56
+ for (const child of this.children) {
57
+ if (child instanceof ModelElement) {
58
+ if (child.tagName === tagName) {
59
+ results.push(child);
60
+ }
61
+ results = results.concat(child.find(tagName));
62
+ }
63
+ }
64
+ return results;
65
+ }
66
+ text() {
67
+ return this.children
68
+ .map((c) => {
69
+ if (c instanceof ModelText)
70
+ return c.text;
71
+ if (c instanceof ModelCDATA)
72
+ return c.content;
73
+ if (c instanceof ModelElement)
74
+ return c.text();
75
+ return "";
76
+ })
77
+ .join("");
78
+ }
79
+ }
80
+ exports.ModelElement = ModelElement;
81
+ class ModelText extends ModelNode {
82
+ constructor(text) {
83
+ super();
84
+ this.text = text;
85
+ }
86
+ getType() {
87
+ return ModelNodeType.Text;
88
+ }
89
+ clone(preserveId = false) {
90
+ const clone = new ModelText(this.text);
91
+ this.cloneBase(clone, preserveId);
92
+ return clone;
93
+ }
94
+ }
95
+ exports.ModelText = ModelText;
96
+ class ModelComment extends ModelNode {
97
+ constructor(content) {
98
+ super();
99
+ this.content = content;
100
+ }
101
+ getType() {
102
+ return ModelNodeType.Comment;
103
+ }
104
+ clone(preserveId = false) {
105
+ const clone = new ModelComment(this.content);
106
+ this.cloneBase(clone, preserveId);
107
+ return clone;
108
+ }
109
+ }
110
+ exports.ModelComment = ModelComment;
111
+ class ModelCDATA extends ModelNode {
112
+ constructor(content) {
113
+ super();
114
+ this.content = content;
115
+ }
116
+ getType() {
117
+ return ModelNodeType.CDATA;
118
+ }
119
+ clone(preserveId = false) {
120
+ const clone = new ModelCDATA(this.content);
121
+ this.cloneBase(clone, preserveId);
122
+ return clone;
123
+ }
124
+ }
125
+ exports.ModelCDATA = ModelCDATA;
@@ -0,0 +1,37 @@
1
+ import type { CST } from "../cst/xml-cst";
2
+ import { ModelElement, type ModelNode } from "./xml-api-model";
3
+ export declare class XMLBinder {
4
+ private input;
5
+ constructor(input: string);
6
+ isHydratable(name: string | undefined): boolean;
7
+ hydrate(node: CST): ModelNode | null;
8
+ reconcile(currentModel: ModelNode, newCst: CST): ModelNode;
9
+ private canReconcile;
10
+ private applyReconciliation;
11
+ calcSetAttributePatch(model: ModelElement, key: string, value: string): {
12
+ start: number;
13
+ end: number;
14
+ text: string;
15
+ } | null;
16
+ calcUpdateTextPatch(model: ModelElement, text: string): {
17
+ start: number;
18
+ end: number;
19
+ text: string;
20
+ } | null;
21
+ calcReplaceNodePatch(model: ModelNode, newXml: string): {
22
+ start: number;
23
+ end: number;
24
+ text: string;
25
+ } | null;
26
+ calcInsertNodePatch(parent: ModelElement, index: number, insertText: string): {
27
+ start: number;
28
+ end: number;
29
+ text: string;
30
+ } | null;
31
+ calcRemoveNodePatch(child: ModelNode): {
32
+ start: number;
33
+ end: number;
34
+ text: string;
35
+ } | null;
36
+ private parseTag;
37
+ }
@@ -0,0 +1,484 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.XMLBinder = void 0;
4
+ const xml_api_model_1 = require("./xml-api-model");
5
+ class XMLBinder {
6
+ constructor(input) {
7
+ this.input = input;
8
+ }
9
+ isHydratable(name) {
10
+ if (!name)
11
+ return false;
12
+ return [
13
+ "element",
14
+ "document",
15
+ "CharData",
16
+ "Reference",
17
+ "CharRef",
18
+ "EntityRef",
19
+ "CDSect",
20
+ "Comment",
21
+ ].includes(name);
22
+ }
23
+ hydrate(node) {
24
+ let result = null;
25
+ // 1. Handle known rule names
26
+ if (node.name === "CharData") {
27
+ result = new xml_api_model_1.ModelText(node.getText(this.input));
28
+ }
29
+ else if (node.name === "Reference") {
30
+ const text = node.getText(this.input);
31
+ if (text.startsWith("&#")) {
32
+ result = new xml_api_model_1.ModelText(decodeCharRef(node, this.input));
33
+ }
34
+ else {
35
+ result = new xml_api_model_1.ModelText(text);
36
+ }
37
+ }
38
+ else if (node.name === "CharRef") {
39
+ result = new xml_api_model_1.ModelText(decodeCharRef(node, this.input));
40
+ }
41
+ else if (node.name === "EntityRef") {
42
+ result = new xml_api_model_1.ModelText(node.getText(this.input));
43
+ }
44
+ else if (node.name === "CDSect") {
45
+ const structural = node.unwrap();
46
+ if (structural.children.length === 3) {
47
+ result = new xml_api_model_1.ModelCDATA(structural.children[1].getText(this.input));
48
+ }
49
+ else {
50
+ result = new xml_api_model_1.ModelCDATA("");
51
+ }
52
+ }
53
+ else if (node.name === "Comment") {
54
+ const text = node.getText(this.input);
55
+ // Remove <!-- and -->
56
+ const content = text.substring(4, text.length - 3);
57
+ result = new xml_api_model_1.ModelComment(content);
58
+ }
59
+ else if (node.name === "PI") {
60
+ result = null;
61
+ }
62
+ else if (node.name === "element" || node.name === "document") {
63
+ const structural = node.unwrap();
64
+ // document rule: seq(prolog, element, Misc*)
65
+ if (node.name === "document" &&
66
+ structural.type === "sequence" &&
67
+ structural.children.length >= 2) {
68
+ const elementNode = structural.children[1];
69
+ result = this.hydrate(elementNode);
70
+ }
71
+ else if (structural.children.length === 3 &&
72
+ structural.children[0].name === "STag") {
73
+ // Case 1: Sequence [STag, content, ETag]
74
+ const stag = structural.children[0];
75
+ const content = structural.children[1];
76
+ const elem = this.parseTag(stag);
77
+ // elem.cst will be set at the end
78
+ const contentStructural = content.unwrap();
79
+ // content rule: seq(opt(CharData), rep(seq(alt(...), opt(CharData))))
80
+ // Initial CharData
81
+ const initialCharDataRep = contentStructural.children[0];
82
+ if (initialCharDataRep && initialCharDataRep.children.length > 0) {
83
+ const textNode = this.hydrate(initialCharDataRep.children[0]);
84
+ if (textNode)
85
+ elem.addChild(textNode);
86
+ }
87
+ // Repeating part
88
+ const repeatingPart = contentStructural.children[1];
89
+ if (repeatingPart) {
90
+ for (const seqNode of repeatingPart.children) {
91
+ // seq(alt(...), opt(CharData))
92
+ const choiceNode = seqNode.children[0];
93
+ const converted = this.hydrate(choiceNode);
94
+ if (converted) {
95
+ elem.addChild(converted);
96
+ }
97
+ const trailingCharDataRep = seqNode.children[1];
98
+ if (trailingCharDataRep &&
99
+ trailingCharDataRep.children.length > 0) {
100
+ const textNode = this.hydrate(trailingCharDataRep.children[0]);
101
+ if (textNode)
102
+ elem.addChild(textNode);
103
+ }
104
+ }
105
+ }
106
+ result = elem;
107
+ }
108
+ else if (structural.children.length >= 2 &&
109
+ structural.children[0].type === "literal" &&
110
+ structural.children[0].getText(this.input) === "<") {
111
+ // Case 2: EmptyElemTag
112
+ result = this.parseTag(node);
113
+ }
114
+ else if (structural !== node) {
115
+ // Wrapper check
116
+ result = this.hydrate(structural);
117
+ }
118
+ }
119
+ else if (node.type === "regex" || node.type === "literal") {
120
+ // 2. Handle structural nodes (literals/regex)
121
+ result = new xml_api_model_1.ModelText(node.getText(this.input));
122
+ }
123
+ else if (node.children.length === 1 &&
124
+ node.children[0].start === node.start &&
125
+ node.children[0].end === node.end) {
126
+ // 3. Fallback wrapper unwrap
127
+ result = this.hydrate(node.children[0]);
128
+ }
129
+ if (result) {
130
+ if (!result.cst) {
131
+ result.cst = node;
132
+ }
133
+ }
134
+ return result;
135
+ }
136
+ reconcile(currentModel, newCst) {
137
+ // 1. Try to hydrate the new CST to see what it *should* look like.
138
+ // This is inefficient (double parsing) but robust for a first implementation.
139
+ // A better way would be to traverse CST and update Model in one pass.
140
+ // But since `hydrate` logic is complex (handling grammar rules), duplicating it for reconcile is risky.
141
+ //
142
+ // Optimization: hydrate returns a NEW model tree.
143
+ // We then compare this new tree with currentModel.
144
+ // If they match in structure/identity-keys, we update currentModel and return it.
145
+ // If not, we return the new model.
146
+ const newModel = this.hydrate(newCst);
147
+ if (!newModel) {
148
+ // If hydration failed (e.g. comment), but we had a model, return null?
149
+ // Or if the node disappeared.
150
+ // For now, assume strict mapping.
151
+ // But hydrate returns null for Comments/PIs.
152
+ // If currentModel was something else, it's a replacement.
153
+ return newModel; // Should handle null better in caller?
154
+ }
155
+ if (this.canReconcile(currentModel, newModel)) {
156
+ this.applyReconciliation(currentModel, newModel);
157
+ return currentModel;
158
+ }
159
+ return newModel;
160
+ }
161
+ canReconcile(a, b) {
162
+ if (a.getType() !== b.getType())
163
+ return false;
164
+ if (a.getType() === xml_api_model_1.ModelNodeType.Element) {
165
+ return a.tagName === b.tagName;
166
+ }
167
+ // Text nodes can always be reconciled (updated)
168
+ return true;
169
+ }
170
+ applyReconciliation(target, source) {
171
+ target.cst = source.cst; // Update CST reference
172
+ if (target.getType() === xml_api_model_1.ModelNodeType.Text) {
173
+ target.text = source.text;
174
+ }
175
+ else if (target.getType() === xml_api_model_1.ModelNodeType.Comment) {
176
+ target.content = source.content;
177
+ }
178
+ else if (target.getType() === xml_api_model_1.ModelNodeType.CDATA) {
179
+ target.content = source.content;
180
+ }
181
+ else {
182
+ const t = target;
183
+ const s = source;
184
+ // Update Attributes
185
+ t.attributes = s.attributes;
186
+ // Reconcile Children with Key-based Matching
187
+ const newChildren = [];
188
+ // 1. Map existing children by ID
189
+ const keyedChildren = new Map();
190
+ const nonKeyedChildren = [];
191
+ for (const child of t.children) {
192
+ if (child.getType() === xml_api_model_1.ModelNodeType.Element) {
193
+ const el = child;
194
+ const id = el.attributes.get("id");
195
+ if (id) {
196
+ keyedChildren.set(id, el);
197
+ }
198
+ else {
199
+ nonKeyedChildren.push(child);
200
+ }
201
+ }
202
+ else {
203
+ nonKeyedChildren.push(child);
204
+ }
205
+ }
206
+ // 2. Iterate source children and try to match
207
+ for (const sChild of s.children) {
208
+ let matchedNode;
209
+ // Try Keyed Match
210
+ if (sChild.getType() === xml_api_model_1.ModelNodeType.Element) {
211
+ const sEl = sChild;
212
+ const id = sEl.attributes.get("id");
213
+ if (id && keyedChildren.has(id)) {
214
+ matchedNode = keyedChildren.get(id);
215
+ keyedChildren.delete(id);
216
+ }
217
+ }
218
+ // Try Non-Keyed Match (First compatible)
219
+ if (!matchedNode) {
220
+ for (let i = 0; i < nonKeyedChildren.length; i++) {
221
+ const candidate = nonKeyedChildren[i];
222
+ if (this.canReconcile(candidate, sChild)) {
223
+ matchedNode = candidate;
224
+ nonKeyedChildren.splice(i, 1);
225
+ break;
226
+ }
227
+ }
228
+ }
229
+ if (matchedNode) {
230
+ // Found a match (keyed or non-keyed)
231
+ // Use CST from new node to update existing node
232
+ const reconciled = this.reconcile(matchedNode, sChild.cst);
233
+ reconciled.parent = t;
234
+ newChildren.push(reconciled);
235
+ }
236
+ else {
237
+ // No match found, use new node
238
+ sChild.parent = t;
239
+ newChildren.push(sChild);
240
+ }
241
+ }
242
+ t.children = newChildren;
243
+ }
244
+ }
245
+ calcSetAttributePatch(model, key, value) {
246
+ if (!model.cst)
247
+ return null;
248
+ const structural = model.cst.unwrap();
249
+ let tagNode = null;
250
+ // Identify tag node (STag or EmptyElemTag)
251
+ // Case 1: element ::= STag content ETag
252
+ if (structural.children.length === 3 &&
253
+ structural.children[0].name === "STag") {
254
+ tagNode = structural.children[0];
255
+ }
256
+ // Case 2: element ::= EmptyElemTag
257
+ else if (structural.name === "EmptyElemTag") {
258
+ tagNode = structural;
259
+ }
260
+ // Case 3: Wrapper or other structure (try to find tag in children)
261
+ else {
262
+ for (const child of structural.children) {
263
+ if (child.name === "STag" || child.name === "EmptyElemTag") {
264
+ tagNode = child;
265
+ break;
266
+ }
267
+ }
268
+ // If structural is strictly EmptyElemTag but unwrap failed to show name? (Unlikely)
269
+ if (!tagNode &&
270
+ structural.children.length >= 2 &&
271
+ structural.children[0].getText(this.input) === "<") {
272
+ // Fallback: assume it matches EmptyElemTag structure directly
273
+ tagNode = structural;
274
+ }
275
+ }
276
+ if (!tagNode)
277
+ return null;
278
+ const tagStructural = tagNode.unwrap();
279
+ // Expected structure: < Name (S Attribute)* S? > (Length 5)
280
+ // Robust access to attributes
281
+ // Index 2 is rep(seq(S, Attribute))
282
+ const attrRep = tagStructural.children[2];
283
+ if (attrRep && attrRep.type === "repeat") {
284
+ for (const seqNode of attrRep.children) {
285
+ // seq(S, Attribute)
286
+ const attrNode = seqNode.children[1];
287
+ if (attrNode && attrNode.name === "Attribute") {
288
+ const attrStructural = attrNode.unwrap();
289
+ const nameNode = attrStructural.children[0];
290
+ const attrName = nameNode.getText(this.input);
291
+ if (attrName === key) {
292
+ // Found existing attribute
293
+ const attValueNode = attrStructural.children[2];
294
+ const oldText = attValueNode.getText(this.input);
295
+ const quote = oldText[0];
296
+ // Preserve quote style if possible
297
+ const newQuote = quote === "'" || quote === '"' ? quote : '"';
298
+ return {
299
+ start: attValueNode.start,
300
+ end: attValueNode.end,
301
+ text: `${newQuote}${escapeAttributeValue(value)}${newQuote}`,
302
+ };
303
+ }
304
+ }
305
+ }
306
+ }
307
+ // Attribute not found, insert new one.
308
+ // Insert before the closing sequence (S? > or S? />)
309
+ // The closing sequence starts after the attributes.
310
+ // We can insert at the end of attrRep?
311
+ // Or just look at the end of the tag and back up.
312
+ const len = tagStructural.children.length;
313
+ const closing = tagStructural.children[len - 1]; // > or />
314
+ const _optS = tagStructural.children[len - 2]; // S?
315
+ // Insert before closing bracket.
316
+ // If optS is present (has children or length > 0), we can insert before or after it?
317
+ // If we insert ` id="val"`, we provide the space.
318
+ // So inserting at `closing.start` is safe.
319
+ // Special case: If EmptyElemTag ends with `/>` (start is 2 chars before end).
320
+ // closing.start points to `/`.
321
+ return {
322
+ start: closing.start,
323
+ end: closing.start,
324
+ text: ` ${key}="${escapeAttributeValue(value)}"`,
325
+ };
326
+ }
327
+ calcUpdateTextPatch(model, text) {
328
+ if (!model.cst)
329
+ return null;
330
+ const structural = model.cst.unwrap();
331
+ // Case 1: Sequence [STag, content, ETag]
332
+ if (structural.children.length === 3 &&
333
+ structural.children[0].name === "STag") {
334
+ const contentNode = structural.children[1];
335
+ return {
336
+ start: contentNode.start,
337
+ end: contentNode.end,
338
+ text: escapeText(text),
339
+ };
340
+ }
341
+ // Case 2: EmptyElemTag
342
+ // <Name ... /> -> <Name ... >text</Name>
343
+ // We need to find "/>" at the end and replace it with ">text</Name>"
344
+ if (structural.name === "EmptyElemTag" ||
345
+ (structural.children.length >= 2 &&
346
+ structural.children[0].getText(this.input) === "<")) {
347
+ const len = structural.children.length;
348
+ const closing = structural.children[len - 1]; // "/>"
349
+ if (closing.getText(this.input) === "/>") {
350
+ return {
351
+ start: closing.start,
352
+ end: closing.end,
353
+ text: `>${escapeText(text)}</${model.tagName}>`,
354
+ };
355
+ }
356
+ }
357
+ return null;
358
+ }
359
+ calcReplaceNodePatch(model, newXml) {
360
+ if (!model.cst)
361
+ return null;
362
+ // Direct replacement of the CST range
363
+ return {
364
+ start: model.cst.start,
365
+ end: model.cst.end,
366
+ text: newXml,
367
+ };
368
+ }
369
+ calcInsertNodePatch(parent, index, insertText) {
370
+ if (!parent.cst)
371
+ return null;
372
+ const structural = parent.cst.unwrap();
373
+ // Case 1: Sequence [STag, content, ETag]
374
+ if (structural.children.length === 3 &&
375
+ structural.children[0].name === "STag") {
376
+ // content -> seq(opt(CharData), rep(seq(alt(...), opt(CharData))))
377
+ let insertPos;
378
+ // Look for the next sibling that has a CST (stable anchor)
379
+ // Since the model already contains the new node at 'index', we look from 'index + 1'
380
+ let anchorNode = null;
381
+ for (let i = index + 1; i < parent.children.length; i++) {
382
+ if (parent.children[i].cst) {
383
+ anchorNode = parent.children[i];
384
+ break;
385
+ }
386
+ }
387
+ if (anchorNode && anchorNode.cst) {
388
+ insertPos = anchorNode.cst.start;
389
+ }
390
+ else {
391
+ // No following stable anchor found, insert before ETag
392
+ const etag = structural.children[2];
393
+ insertPos = etag.start;
394
+ }
395
+ return {
396
+ start: insertPos,
397
+ end: insertPos,
398
+ text: insertText,
399
+ };
400
+ }
401
+ // Case 2: EmptyElemTag
402
+ // <Name ... /> -> <Name ... >insertText</Name>
403
+ if (structural.name === "EmptyElemTag" ||
404
+ (structural.children.length >= 2 &&
405
+ structural.children[0].getText(this.input) === "<")) {
406
+ const len = structural.children.length;
407
+ const closing = structural.children[len - 1]; // "/>"
408
+ if (closing.getText(this.input) === "/>") {
409
+ return {
410
+ start: closing.start,
411
+ end: closing.end,
412
+ text: `>${insertText}</${parent.tagName}>`,
413
+ };
414
+ }
415
+ }
416
+ return null;
417
+ }
418
+ calcRemoveNodePatch(child) {
419
+ if (!child.cst)
420
+ return null;
421
+ // TODO: Ideally we should remove surrounding whitespace if it becomes redundant (pretty print maintenance).
422
+ // For now, strict removal.
423
+ return {
424
+ start: child.cst.start,
425
+ end: child.cst.end,
426
+ text: "",
427
+ };
428
+ }
429
+ parseTag(node) {
430
+ const structural = node.unwrap();
431
+ const nameNode = structural.children[1];
432
+ const tagName = nameNode.getText(this.input);
433
+ const elem = new xml_api_model_1.ModelElement(tagName);
434
+ const attrRep = structural.children[2]; // rep(seq(S, Attribute))
435
+ for (const seq of attrRep.children) {
436
+ const attrNode = seq.children[1]; // Attribute
437
+ const attrStructural = attrNode.unwrap();
438
+ const attrName = attrStructural.children[0].getText(this.input);
439
+ const attValueNode = attrStructural.children[2];
440
+ const attValueStructural = attValueNode.unwrap();
441
+ const valRep = attValueStructural.children[1];
442
+ let valText = "";
443
+ for (const chunk of valRep.children) {
444
+ // Use hydrate to get text nodes from value chunks, or just getText?
445
+ // convert() recursively called convert, which returned AST|string.
446
+ // For attribute values, we expect string.
447
+ // Since hydrate returns ModelNode, we need to extract text.
448
+ // But attribute values might be complex? In current grammar, they are mostly text/refs.
449
+ const modelNode = this.hydrate(chunk);
450
+ if (modelNode instanceof xml_api_model_1.ModelText) {
451
+ valText += modelNode.text;
452
+ }
453
+ else if (modelNode) {
454
+ // Fallback if somehow it returned an Element (unlikely in AttValue)
455
+ // But wait, hydrate returns ModelNode.
456
+ // If it's a reference, it returns ModelText.
457
+ // If it's a literal/regex, it returns ModelText.
458
+ // So this should be fine.
459
+ // However, if hydrate returns null, we skip.
460
+ }
461
+ }
462
+ elem.setAttribute(attrName, valText);
463
+ }
464
+ return elem;
465
+ }
466
+ }
467
+ exports.XMLBinder = XMLBinder;
468
+ function decodeCharRef(node, input) {
469
+ const text = node.getText(input);
470
+ let code;
471
+ if (text.startsWith("&#x")) {
472
+ code = parseInt(text.slice(3, -1), 16);
473
+ }
474
+ else {
475
+ code = parseInt(text.slice(2, -1), 10);
476
+ }
477
+ return String.fromCodePoint(code);
478
+ }
479
+ function escapeText(str) {
480
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
481
+ }
482
+ function escapeAttributeValue(str) {
483
+ return escapeText(str).replace(/"/g, "&quot;");
484
+ }