@miy2/xml-api 0.9.0 → 0.9.1

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,19 +1,90 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HistoryManager = void 0;
4
- class HistoryManager {
1
+ export class HistoryManager {
5
2
  constructor(maxHistory = 100) {
6
3
  this.undoStack = [];
7
4
  this.redoStack = [];
5
+ this.mergeThreshold = 1000; // ms
8
6
  this.maxHistory = maxHistory;
9
7
  }
10
8
  push(transaction) {
9
+ const last = this.undoStack[this.undoStack.length - 1];
10
+ if (last && this.shouldMerge(last, transaction)) {
11
+ this.merge(last, transaction);
12
+ return;
13
+ }
11
14
  this.undoStack.push(transaction);
12
15
  if (this.undoStack.length > this.maxHistory) {
13
16
  this.undoStack.shift();
14
17
  }
15
18
  this.redoStack = []; // Clear redo stack on new operation
16
19
  }
20
+ shouldMerge(last, next) {
21
+ // Check time threshold
22
+ if (next.timestamp - last.timestamp > this.mergeThreshold)
23
+ return false;
24
+ // Detect type of operation
25
+ // Pattern 1: Incremental Insertion (CodeEditor / Typing)
26
+ // Last: Insert "A" at 10. (from: 10, to: 10, text: "A")
27
+ // Next: Insert "B" at 11. (from: 11, to: 11, text: "B")
28
+ const isInsertion = last.redo.from === last.redo.to &&
29
+ next.redo.from === next.redo.to &&
30
+ next.redo.from === last.redo.from + last.redo.text.length;
31
+ if (isInsertion)
32
+ return true;
33
+ // Pattern 2: Replacement Extension (WYSIWYG / ViewBinder)
34
+ // Last: Replace "A" with "AB" at 10. (from: 10, to: 11, text: "AB")
35
+ // Next: Replace "AB" with "ABC" at 10. (from: 10, to: 12, text: "ABC")
36
+ // Condition: Start position same.
37
+ const isReplacementExtension = last.redo.from === next.redo.from &&
38
+ // Check if next is extending last
39
+ next.redo.text.startsWith(last.redo.text) &&
40
+ // Check if the previous state of next matches the current state of last
41
+ next.undo.text === last.redo.text;
42
+ // Note: next.undo.text is the text that was replaced by next.
43
+ // In Pattern 2, we replaced "AB" (last.redo.text) with "ABC".
44
+ // So next.undo.text should be "AB".
45
+ if (isReplacementExtension)
46
+ return true;
47
+ return false;
48
+ }
49
+ merge(last, next) {
50
+ last.timestamp = next.timestamp;
51
+ if (last.redo.from === last.redo.to && next.redo.from === next.redo.to) {
52
+ // Pattern 1: Incremental Insertion
53
+ // Last: Insert "A" at 10.
54
+ // Next: Insert "B" at 11.
55
+ // Merged: Insert "AB" at 10.
56
+ last.redo.text += next.redo.text;
57
+ // Undo:
58
+ // Last Undo: Delete 10-11 ("A").
59
+ // Next Undo: Delete 11-12 ("B").
60
+ // Merged Undo: Delete 10-12.
61
+ // The `to` of undo represents the end of the range to be replaced/deleted in the current doc.
62
+ // last.undo.to needs to expand by the length of the new insertion.
63
+ last.undo.to += next.redo.text.length;
64
+ // last.undo.text remains same (usually empty for insertion)
65
+ }
66
+ else {
67
+ // Pattern 2: Replacement Extension
68
+ // Last: Replace "A" (10-11) with "AB". Redo: "AB". Undo: "A" (from 10, to 12).
69
+ // Next: Replace "AB" (10-12) with "ABC". Redo: "ABC". Undo: "AB" (from 10, to 13).
70
+ // Merged: Replace "A" (10-11) with "ABC".
71
+ // Update Redo
72
+ last.redo.text = next.redo.text;
73
+ // last.redo.from/to remain as the original range of "A" (10-11).
74
+ // Update Undo
75
+ // We want undo to restore "A".
76
+ // Current state is "ABC" (10-13).
77
+ // Undo operation should be: replace 10-13 with "A".
78
+ // last.undo.from is 10. Correct.
79
+ // last.undo.text is "A". Correct.
80
+ // last.undo.to needs to be 13.
81
+ // next.undo.to is 13. (SyncEngine calculates this as from + text.length? No, SyncEngine calculates undo.to as newEnd)
82
+ // SyncEngine: undo: { from: p.from, to: newEnd, text: oldText }
83
+ // newEnd is the end of the newly inserted text.
84
+ // So yes, next.undo.to is 13.
85
+ last.undo.to = next.undo.to;
86
+ }
87
+ }
17
88
  undo() {
18
89
  const transaction = this.undoStack.pop();
19
90
  if (transaction) {
@@ -37,4 +108,3 @@ class HistoryManager {
37
108
  return this.redoStack.length > 0;
38
109
  }
39
110
  }
40
- exports.HistoryManager = HistoryManager;
@@ -2,11 +2,13 @@ import { type ModelNode } from "./xml-api-model";
2
2
  export interface FormatterOptions {
3
3
  indent?: string;
4
4
  newline?: string;
5
+ baseIndent?: string;
5
6
  force?: boolean;
6
7
  }
7
8
  export declare class Formatter {
8
9
  private indent;
9
10
  private newline;
11
+ private baseIndent;
10
12
  private force;
11
13
  constructor(options?: FormatterOptions);
12
14
  format(node: ModelNode): string;
@@ -1,28 +1,26 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Formatter = void 0;
4
- const xml_api_model_1 = require("./xml-api-model");
5
- class Formatter {
1
+ import { ModelCDATA, ModelComment, ModelElement, ModelText, } from "./xml-api-model";
2
+ export class Formatter {
6
3
  constructor(options = {}) {
7
- var _a, _b, _c;
4
+ var _a, _b, _c, _d;
8
5
  this.indent = (_a = options.indent) !== null && _a !== void 0 ? _a : " ";
9
6
  this.newline = (_b = options.newline) !== null && _b !== void 0 ? _b : "\n";
10
- this.force = (_c = options.force) !== null && _c !== void 0 ? _c : false;
7
+ this.baseIndent = (_c = options.baseIndent) !== null && _c !== void 0 ? _c : "";
8
+ this.force = (_d = options.force) !== null && _d !== void 0 ? _d : false;
11
9
  }
12
10
  format(node) {
13
11
  return this.formatNode(node, 0);
14
12
  }
15
13
  formatNode(node, level) {
16
- if (node instanceof xml_api_model_1.ModelText) {
14
+ if (node instanceof ModelText) {
17
15
  return this.escape(node.text);
18
16
  }
19
- if (node instanceof xml_api_model_1.ModelComment) {
17
+ if (node instanceof ModelComment) {
20
18
  return `<!--${node.content}-->`;
21
19
  }
22
- if (node instanceof xml_api_model_1.ModelCDATA) {
20
+ if (node instanceof ModelCDATA) {
23
21
  return `<![CDATA[${node.content}]]>`;
24
22
  }
25
- if (node instanceof xml_api_model_1.ModelElement) {
23
+ if (node instanceof ModelElement) {
26
24
  const tagName = node.tagName;
27
25
  const attributes = this.formatAttributes(node.attributes);
28
26
  const children = node.children;
@@ -35,7 +33,7 @@ class Formatter {
35
33
  if (isInline || hasFormatting) {
36
34
  for (const child of children) {
37
35
  if (this.force &&
38
- child instanceof xml_api_model_1.ModelText &&
36
+ child instanceof ModelText &&
39
37
  child.text.includes("\n") &&
40
38
  child.text.trim().length === 0) {
41
39
  continue;
@@ -47,7 +45,7 @@ class Formatter {
47
45
  else {
48
46
  for (const child of children) {
49
47
  if (this.force &&
50
- child instanceof xml_api_model_1.ModelText &&
48
+ child instanceof ModelText &&
51
49
  child.text.includes("\n") &&
52
50
  child.text.trim().length === 0) {
53
51
  continue;
@@ -74,12 +72,12 @@ class Formatter {
74
72
  }
75
73
  isInline(children) {
76
74
  for (const theChild of children) {
77
- if (theChild instanceof xml_api_model_1.ModelText) {
75
+ if (theChild instanceof ModelText) {
78
76
  // If there is any non-whitespace text, treat as inline.
79
77
  if (theChild.text.trim().length > 0)
80
78
  return true;
81
79
  }
82
- if (theChild instanceof xml_api_model_1.ModelCDATA) {
80
+ if (theChild instanceof ModelCDATA) {
83
81
  return true;
84
82
  }
85
83
  }
@@ -87,7 +85,7 @@ class Formatter {
87
85
  }
88
86
  hasFormatting(children) {
89
87
  for (const theChild of children) {
90
- if (theChild instanceof xml_api_model_1.ModelText) {
88
+ if (theChild instanceof ModelText) {
91
89
  // If it contains a newline and is otherwise whitespace, it's likely formatting.
92
90
  if (theChild.text.includes("\n") && theChild.text.trim().length === 0) {
93
91
  return true;
@@ -97,7 +95,7 @@ class Formatter {
97
95
  return false;
98
96
  }
99
97
  getIndent(level) {
100
- return this.indent.repeat(level);
98
+ return this.baseIndent + this.indent.repeat(level);
101
99
  }
102
100
  escape(str) {
103
101
  return str
@@ -109,4 +107,3 @@ class Formatter {
109
107
  return this.escape(str).replace(/"/g, "&quot;");
110
108
  }
111
109
  }
112
- exports.Formatter = Formatter;
@@ -6,42 +6,53 @@ export declare enum ModelNodeType {
6
6
  Comment = "Comment",
7
7
  CDATA = "CDATA"
8
8
  }
9
+ export interface ModelFormatting {
10
+ /**
11
+ * The whitespace preceding the node, if it starts on a new line.
12
+ * Null if the node is inline (preceded by non-whitespace content).
13
+ */
14
+ indent: string | null;
15
+ }
9
16
  export declare abstract class ModelNode {
10
17
  readonly id: NodeId;
11
18
  parent: ModelElement | null;
12
19
  cst: CST | null;
13
- constructor();
20
+ formatting: ModelFormatting;
21
+ constructor(id?: NodeId);
14
22
  abstract getType(): ModelNodeType;
15
23
  abstract clone(preserveId?: boolean): ModelNode;
16
- protected cloneBase(target: ModelNode, preserveId: boolean): void;
24
+ findNodeById(id: string): ModelNode | null;
25
+ protected cloneBase(target: ModelNode, _preserveId: boolean): void;
17
26
  }
18
27
  export declare class ModelElement extends ModelNode {
19
28
  tagName: string;
20
29
  attributes: Map<string, string>;
21
30
  children: ModelNode[];
22
- constructor(tagName: string);
31
+ constructor(tagName: string, id?: NodeId);
23
32
  getType(): ModelNodeType;
24
33
  clone(preserveId?: boolean): ModelElement;
25
34
  addChild(node: ModelNode): void;
26
35
  setAttribute(key: string, value: string): void;
27
36
  find(tagName: string): ModelElement[];
37
+ findNodeById(id: string): ModelNode | null;
28
38
  text(): string;
29
39
  }
30
40
  export declare class ModelText extends ModelNode {
31
41
  text: string;
32
- constructor(text: string);
42
+ kind: "text" | "whitespace";
43
+ constructor(text: string, id?: NodeId, kind?: "text" | "whitespace");
33
44
  getType(): ModelNodeType;
34
45
  clone(preserveId?: boolean): ModelText;
35
46
  }
36
47
  export declare class ModelComment extends ModelNode {
37
48
  content: string;
38
- constructor(content: string);
49
+ constructor(content: string, id?: NodeId);
39
50
  getType(): ModelNodeType;
40
51
  clone(preserveId?: boolean): ModelComment;
41
52
  }
42
53
  export declare class ModelCDATA extends ModelNode {
43
54
  content: string;
44
- constructor(content: string);
55
+ constructor(content: string, id?: NodeId);
45
56
  getType(): ModelNodeType;
46
57
  clone(preserveId?: boolean): ModelCDATA;
47
58
  }
@@ -1,31 +1,30 @@
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;
1
+ export var ModelNodeType;
5
2
  (function (ModelNodeType) {
6
3
  ModelNodeType["Element"] = "Element";
7
4
  ModelNodeType["Text"] = "Text";
8
5
  ModelNodeType["Comment"] = "Comment";
9
6
  ModelNodeType["CDATA"] = "CDATA";
10
- })(ModelNodeType || (exports.ModelNodeType = ModelNodeType = {}));
11
- class ModelNode {
12
- constructor() {
7
+ })(ModelNodeType || (ModelNodeType = {}));
8
+ export class ModelNode {
9
+ constructor(id) {
13
10
  this.parent = null;
14
11
  this.cst = null;
15
- this.id = crypto.randomUUID();
12
+ this.formatting = { indent: null };
13
+ this.id = id !== null && id !== void 0 ? id : crypto.randomUUID();
16
14
  }
17
- cloneBase(target, preserveId) {
18
- if (preserveId) {
19
- // @ts-ignore
20
- target.id = this.id;
21
- }
15
+ findNodeById(id) {
16
+ if (this.id === id)
17
+ return this;
18
+ return null;
19
+ }
20
+ cloneBase(target, _preserveId) {
22
21
  target.cst = this.cst;
22
+ target.formatting = { ...this.formatting };
23
23
  }
24
24
  }
25
- exports.ModelNode = ModelNode;
26
- class ModelElement extends ModelNode {
27
- constructor(tagName) {
28
- super();
25
+ export class ModelElement extends ModelNode {
26
+ constructor(tagName, id) {
27
+ super(id);
29
28
  this.attributes = new Map();
30
29
  this.children = [];
31
30
  this.tagName = tagName;
@@ -34,7 +33,7 @@ class ModelElement extends ModelNode {
34
33
  return ModelNodeType.Element;
35
34
  }
36
35
  clone(preserveId = false) {
37
- const clone = new ModelElement(this.tagName);
36
+ const clone = new ModelElement(this.tagName, preserveId ? this.id : undefined);
38
37
  this.cloneBase(clone, preserveId);
39
38
  clone.attributes = new Map(this.attributes);
40
39
  clone.children = this.children.map((c) => {
@@ -63,6 +62,16 @@ class ModelElement extends ModelNode {
63
62
  }
64
63
  return results;
65
64
  }
65
+ findNodeById(id) {
66
+ if (this.id === id)
67
+ return this;
68
+ for (const child of this.children) {
69
+ const found = child.findNodeById(id);
70
+ if (found)
71
+ return found;
72
+ }
73
+ return null;
74
+ }
66
75
  text() {
67
76
  return this.children
68
77
  .map((c) => {
@@ -77,49 +86,46 @@ class ModelElement extends ModelNode {
77
86
  .join("");
78
87
  }
79
88
  }
80
- exports.ModelElement = ModelElement;
81
- class ModelText extends ModelNode {
82
- constructor(text) {
83
- super();
89
+ export class ModelText extends ModelNode {
90
+ constructor(text, id, kind = "text") {
91
+ super(id);
84
92
  this.text = text;
93
+ this.kind = kind;
85
94
  }
86
95
  getType() {
87
96
  return ModelNodeType.Text;
88
97
  }
89
98
  clone(preserveId = false) {
90
- const clone = new ModelText(this.text);
99
+ const clone = new ModelText(this.text, preserveId ? this.id : undefined, this.kind);
91
100
  this.cloneBase(clone, preserveId);
92
101
  return clone;
93
102
  }
94
103
  }
95
- exports.ModelText = ModelText;
96
- class ModelComment extends ModelNode {
97
- constructor(content) {
98
- super();
104
+ export class ModelComment extends ModelNode {
105
+ constructor(content, id) {
106
+ super(id);
99
107
  this.content = content;
100
108
  }
101
109
  getType() {
102
110
  return ModelNodeType.Comment;
103
111
  }
104
112
  clone(preserveId = false) {
105
- const clone = new ModelComment(this.content);
113
+ const clone = new ModelComment(this.content, preserveId ? this.id : undefined);
106
114
  this.cloneBase(clone, preserveId);
107
115
  return clone;
108
116
  }
109
117
  }
110
- exports.ModelComment = ModelComment;
111
- class ModelCDATA extends ModelNode {
112
- constructor(content) {
113
- super();
118
+ export class ModelCDATA extends ModelNode {
119
+ constructor(content, id) {
120
+ super(id);
114
121
  this.content = content;
115
122
  }
116
123
  getType() {
117
124
  return ModelNodeType.CDATA;
118
125
  }
119
126
  clone(preserveId = false) {
120
- const clone = new ModelCDATA(this.content);
127
+ const clone = new ModelCDATA(this.content, preserveId ? this.id : undefined);
121
128
  this.cloneBase(clone, preserveId);
122
129
  return clone;
123
130
  }
124
131
  }
125
- exports.ModelCDATA = ModelCDATA;
@@ -1,11 +1,18 @@
1
1
  import type { CST } from "../cst/xml-cst";
2
2
  import { ModelElement, type ModelNode } from "./xml-api-model";
3
+ export interface ReconcileResult {
4
+ node: ModelNode | null;
5
+ diff?: {
6
+ addedNodes: ModelNode[];
7
+ removedNodes: ModelNode[];
8
+ };
9
+ }
3
10
  export declare class XMLBinder {
4
11
  private input;
5
12
  constructor(input: string);
6
13
  isHydratable(name: string | undefined): boolean;
7
14
  hydrate(node: CST): ModelNode | null;
8
- reconcile(currentModel: ModelNode, newCst: CST): ModelNode;
15
+ reconcile(currentModel: ModelNode, newCst: CST): ReconcileResult;
9
16
  private canReconcile;
10
17
  private applyReconciliation;
11
18
  calcSetAttributePatch(model: ModelElement, key: string, value: string): {
@@ -1,8 +1,6 @@
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 {
1
+ import { detectIndent } from "../cst/cst-utils";
2
+ import { ModelCDATA, ModelComment, ModelElement, ModelNodeType, ModelText, } from "./xml-api-model";
3
+ export class XMLBinder {
6
4
  constructor(input) {
7
5
  this.input = input;
8
6
  }
@@ -24,37 +22,39 @@ class XMLBinder {
24
22
  let result = null;
25
23
  // 1. Handle known rule names
26
24
  if (node.name === "CharData") {
27
- result = new xml_api_model_1.ModelText(node.getText(this.input));
25
+ const text = node.getText(this.input);
26
+ const kind = /^\s*$/.test(text) ? "whitespace" : "text";
27
+ result = new ModelText(text, undefined, kind);
28
28
  }
29
29
  else if (node.name === "Reference") {
30
30
  const text = node.getText(this.input);
31
31
  if (text.startsWith("&#")) {
32
- result = new xml_api_model_1.ModelText(decodeCharRef(node, this.input));
32
+ result = new ModelText(decodeCharRef(node, this.input));
33
33
  }
34
34
  else {
35
- result = new xml_api_model_1.ModelText(text);
35
+ result = new ModelText(text);
36
36
  }
37
37
  }
38
38
  else if (node.name === "CharRef") {
39
- result = new xml_api_model_1.ModelText(decodeCharRef(node, this.input));
39
+ result = new ModelText(decodeCharRef(node, this.input));
40
40
  }
41
41
  else if (node.name === "EntityRef") {
42
- result = new xml_api_model_1.ModelText(node.getText(this.input));
42
+ result = new ModelText(node.getText(this.input));
43
43
  }
44
44
  else if (node.name === "CDSect") {
45
45
  const structural = node.unwrap();
46
46
  if (structural.children.length === 3) {
47
- result = new xml_api_model_1.ModelCDATA(structural.children[1].getText(this.input));
47
+ result = new ModelCDATA(structural.children[1].getText(this.input));
48
48
  }
49
49
  else {
50
- result = new xml_api_model_1.ModelCDATA("");
50
+ result = new ModelCDATA("");
51
51
  }
52
52
  }
53
53
  else if (node.name === "Comment") {
54
54
  const text = node.getText(this.input);
55
55
  // Remove <!-- and -->
56
56
  const content = text.substring(4, text.length - 3);
57
- result = new xml_api_model_1.ModelComment(content);
57
+ result = new ModelComment(content);
58
58
  }
59
59
  else if (node.name === "PI") {
60
60
  result = null;
@@ -118,7 +118,7 @@ class XMLBinder {
118
118
  }
119
119
  else if (node.type === "regex" || node.type === "literal") {
120
120
  // 2. Handle structural nodes (literals/regex)
121
- result = new xml_api_model_1.ModelText(node.getText(this.input));
121
+ result = new ModelText(node.getText(this.input));
122
122
  }
123
123
  else if (node.children.length === 1 &&
124
124
  node.children[0].start === node.start &&
@@ -130,11 +130,14 @@ class XMLBinder {
130
130
  if (!result.cst) {
131
131
  result.cst = node;
132
132
  }
133
+ if (result.cst) {
134
+ result.formatting.indent = detectIndent(result.cst, this.input);
135
+ }
133
136
  }
134
137
  return result;
135
138
  }
139
+ // ... (rest of class)
136
140
  reconcile(currentModel, newCst) {
137
- // 1. Try to hydrate the new CST to see what it *should* look like.
138
141
  // This is inefficient (double parsing) but robust for a first implementation.
139
142
  // A better way would be to traverse CST and update Model in one pass.
140
143
  // But since `hydrate` logic is complex (handling grammar rules), duplicating it for reconcile is risky.
@@ -150,18 +153,18 @@ class XMLBinder {
150
153
  // For now, assume strict mapping.
151
154
  // But hydrate returns null for Comments/PIs.
152
155
  // If currentModel was something else, it's a replacement.
153
- return newModel; // Should handle null better in caller?
156
+ return { node: newModel, diff: undefined };
154
157
  }
155
158
  if (this.canReconcile(currentModel, newModel)) {
156
- this.applyReconciliation(currentModel, newModel);
157
- return currentModel;
159
+ const diff = this.applyReconciliation(currentModel, newModel);
160
+ return { node: currentModel, diff };
158
161
  }
159
- return newModel;
162
+ return { node: newModel, diff: undefined };
160
163
  }
161
164
  canReconcile(a, b) {
162
165
  if (a.getType() !== b.getType())
163
166
  return false;
164
- if (a.getType() === xml_api_model_1.ModelNodeType.Element) {
167
+ if (a.getType() === ModelNodeType.Element) {
165
168
  return a.tagName === b.tagName;
166
169
  }
167
170
  // Text nodes can always be reconciled (updated)
@@ -169,13 +172,17 @@ class XMLBinder {
169
172
  }
170
173
  applyReconciliation(target, source) {
171
174
  target.cst = source.cst; // Update CST reference
172
- if (target.getType() === xml_api_model_1.ModelNodeType.Text) {
173
- target.text = source.text;
175
+ target.formatting = { ...source.formatting };
176
+ if (target.getType() === ModelNodeType.Text) {
177
+ const t = target;
178
+ const s = source;
179
+ t.text = s.text;
180
+ t.kind = s.kind;
174
181
  }
175
- else if (target.getType() === xml_api_model_1.ModelNodeType.Comment) {
182
+ else if (target.getType() === ModelNodeType.Comment) {
176
183
  target.content = source.content;
177
184
  }
178
- else if (target.getType() === xml_api_model_1.ModelNodeType.CDATA) {
185
+ else if (target.getType() === ModelNodeType.CDATA) {
179
186
  target.content = source.content;
180
187
  }
181
188
  else {
@@ -185,11 +192,12 @@ class XMLBinder {
185
192
  t.attributes = s.attributes;
186
193
  // Reconcile Children with Key-based Matching
187
194
  const newChildren = [];
195
+ const oldChildrenSet = new Set(t.children);
188
196
  // 1. Map existing children by ID
189
197
  const keyedChildren = new Map();
190
198
  const nonKeyedChildren = [];
191
199
  for (const child of t.children) {
192
- if (child.getType() === xml_api_model_1.ModelNodeType.Element) {
200
+ if (child.getType() === ModelNodeType.Element) {
193
201
  const el = child;
194
202
  const id = el.attributes.get("id");
195
203
  if (id) {
@@ -207,7 +215,7 @@ class XMLBinder {
207
215
  for (const sChild of s.children) {
208
216
  let matchedNode;
209
217
  // Try Keyed Match
210
- if (sChild.getType() === xml_api_model_1.ModelNodeType.Element) {
218
+ if (sChild.getType() === ModelNodeType.Element) {
211
219
  const sEl = sChild;
212
220
  const id = sEl.attributes.get("id");
213
221
  if (id && keyedChildren.has(id)) {
@@ -226,12 +234,15 @@ class XMLBinder {
226
234
  }
227
235
  }
228
236
  }
229
- if (matchedNode) {
237
+ if (matchedNode && sChild.cst) {
230
238
  // Found a match (keyed or non-keyed)
231
239
  // 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);
240
+ const result = this.reconcile(matchedNode, sChild.cst);
241
+ const reconciled = result.node;
242
+ if (reconciled) {
243
+ newChildren.push(reconciled);
244
+ reconciled.parent = t;
245
+ }
235
246
  }
236
247
  else {
237
248
  // No match found, use new node
@@ -240,7 +251,11 @@ class XMLBinder {
240
251
  }
241
252
  }
242
253
  t.children = newChildren;
254
+ const addedNodes = newChildren.filter((c) => !oldChildrenSet.has(c));
255
+ const removedNodes = Array.from(oldChildrenSet).filter((c) => !newChildren.includes(c));
256
+ return { addedNodes, removedNodes };
243
257
  }
258
+ return undefined;
244
259
  }
245
260
  calcSetAttributePatch(model, key, value) {
246
261
  if (!model.cst)
@@ -384,7 +399,7 @@ class XMLBinder {
384
399
  break;
385
400
  }
386
401
  }
387
- if (anchorNode && anchorNode.cst) {
402
+ if (anchorNode === null || anchorNode === void 0 ? void 0 : anchorNode.cst) {
388
403
  insertPos = anchorNode.cst.start;
389
404
  }
390
405
  else {
@@ -430,7 +445,7 @@ class XMLBinder {
430
445
  const structural = node.unwrap();
431
446
  const nameNode = structural.children[1];
432
447
  const tagName = nameNode.getText(this.input);
433
- const elem = new xml_api_model_1.ModelElement(tagName);
448
+ const elem = new ModelElement(tagName);
434
449
  const attrRep = structural.children[2]; // rep(seq(S, Attribute))
435
450
  for (const seq of attrRep.children) {
436
451
  const attrNode = seq.children[1]; // Attribute
@@ -447,7 +462,7 @@ class XMLBinder {
447
462
  // Since hydrate returns ModelNode, we need to extract text.
448
463
  // But attribute values might be complex? In current grammar, they are mostly text/refs.
449
464
  const modelNode = this.hydrate(chunk);
450
- if (modelNode instanceof xml_api_model_1.ModelText) {
465
+ if (modelNode instanceof ModelText) {
451
466
  valText += modelNode.text;
452
467
  }
453
468
  else if (modelNode) {
@@ -464,7 +479,6 @@ class XMLBinder {
464
479
  return elem;
465
480
  }
466
481
  }
467
- exports.XMLBinder = XMLBinder;
468
482
  function decodeCharRef(node, input) {
469
483
  const text = node.getText(input);
470
484
  let code;
@@ -1,7 +1,4 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.XMLSchema = void 0;
4
- class XMLSchema {
1
+ export class XMLSchema {
5
2
  constructor(definitions = []) {
6
3
  this.elements = new Map();
7
4
  for (const def of definitions) {
@@ -13,4 +10,3 @@ class XMLSchema {
13
10
  return (_b = (_a = this.elements.get(tagName)) === null || _a === void 0 ? void 0 : _a.isVoid) !== null && _b !== void 0 ? _b : false;
14
11
  }
15
12
  }
16
- exports.XMLSchema = XMLSchema;