@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.
- package/README.md +22 -2
- package/dist/collab/bridge.js +1 -2
- package/dist/cst/cst-utils.d.ts +11 -0
- package/dist/cst/cst-utils.js +32 -0
- package/dist/cst/grammar.js +11 -25
- package/dist/cst/parser.js +7 -11
- package/dist/cst/xml-cst.js +1 -5
- package/dist/cst/xml-grammar.js +83 -86
- package/dist/dom.d.ts +20 -1
- package/dist/dom.js +93 -60
- package/dist/engine/editor-state.js +1 -5
- package/dist/engine/sync-engine.d.ts +43 -9
- package/dist/engine/sync-engine.js +166 -122
- package/dist/engine/transaction-builder.d.ts +14 -0
- package/dist/engine/transaction-builder.js +143 -0
- package/dist/engine/transaction.d.ts +3 -0
- package/dist/engine/transaction.js +12 -8
- package/dist/history-manager.d.ts +4 -0
- package/dist/history-manager.js +75 -5
- package/dist/model/formatter.d.ts +2 -0
- package/dist/model/formatter.js +15 -18
- package/dist/model/xml-api-model.d.ts +17 -6
- package/dist/model/xml-api-model.js +40 -34
- package/dist/model/xml-binder.d.ts +8 -1
- package/dist/model/xml-binder.js +48 -34
- package/dist/model/xml-schema.js +1 -5
- package/dist/view/schema-view.d.ts +80 -0
- package/dist/view/schema-view.js +222 -0
- package/dist/view/view-binder.d.ts +47 -0
- package/dist/view/view-binder.js +163 -0
- package/dist/xml-api-events.d.ts +8 -4
- package/dist/xml-api-events.js +1 -5
- package/dist/xml-api.d.ts +36 -22
- package/dist/xml-api.js +77 -59
- package/package.json +4 -2
- package/LICENSE.md +0 -21
package/dist/history-manager.js
CHANGED
|
@@ -1,19 +1,90 @@
|
|
|
1
|
-
|
|
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;
|
package/dist/model/formatter.js
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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.
|
|
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
|
|
14
|
+
if (node instanceof ModelText) {
|
|
17
15
|
return this.escape(node.text);
|
|
18
16
|
}
|
|
19
|
-
if (node instanceof
|
|
17
|
+
if (node instanceof ModelComment) {
|
|
20
18
|
return `<!--${node.content}-->`;
|
|
21
19
|
}
|
|
22
|
-
if (node instanceof
|
|
20
|
+
if (node instanceof ModelCDATA) {
|
|
23
21
|
return `<![CDATA[${node.content}]]>`;
|
|
24
22
|
}
|
|
25
|
-
if (node instanceof
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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, """);
|
|
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
|
-
|
|
20
|
+
formatting: ModelFormatting;
|
|
21
|
+
constructor(id?: NodeId);
|
|
14
22
|
abstract getType(): ModelNodeType;
|
|
15
23
|
abstract clone(preserveId?: boolean): ModelNode;
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 || (
|
|
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.
|
|
12
|
+
this.formatting = { indent: null };
|
|
13
|
+
this.id = id !== null && id !== void 0 ? id : crypto.randomUUID();
|
|
16
14
|
}
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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):
|
|
15
|
+
reconcile(currentModel: ModelNode, newCst: CST): ReconcileResult;
|
|
9
16
|
private canReconcile;
|
|
10
17
|
private applyReconciliation;
|
|
11
18
|
calcSetAttributePatch(model: ModelElement, key: string, value: string): {
|
package/dist/model/xml-binder.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
32
|
+
result = new ModelText(decodeCharRef(node, this.input));
|
|
33
33
|
}
|
|
34
34
|
else {
|
|
35
|
-
result = new
|
|
35
|
+
result = new ModelText(text);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
else if (node.name === "CharRef") {
|
|
39
|
-
result = new
|
|
39
|
+
result = new ModelText(decodeCharRef(node, this.input));
|
|
40
40
|
}
|
|
41
41
|
else if (node.name === "EntityRef") {
|
|
42
|
-
result = new
|
|
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
|
|
47
|
+
result = new ModelCDATA(structural.children[1].getText(this.input));
|
|
48
48
|
}
|
|
49
49
|
else {
|
|
50
|
-
result = new
|
|
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
|
|
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
|
|
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
|
|
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() ===
|
|
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
|
-
|
|
173
|
-
|
|
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() ===
|
|
182
|
+
else if (target.getType() === ModelNodeType.Comment) {
|
|
176
183
|
target.content = source.content;
|
|
177
184
|
}
|
|
178
|
-
else if (target.getType() ===
|
|
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() ===
|
|
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() ===
|
|
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
|
|
233
|
-
reconciled
|
|
234
|
-
|
|
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
|
|
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
|
|
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
|
|
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;
|
package/dist/model/xml-schema.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
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;
|