@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.
- package/LICENSE.md +21 -0
- package/README.md +94 -0
- package/dist/collab/bridge.d.ts +13 -0
- package/dist/collab/bridge.js +2 -0
- package/dist/cst/grammar.d.ts +93 -0
- package/dist/cst/grammar.js +95 -0
- package/dist/cst/parser.d.ts +23 -0
- package/dist/cst/parser.js +161 -0
- package/dist/cst/xml-cst.d.ts +88 -0
- package/dist/cst/xml-cst.js +116 -0
- package/dist/cst/xml-grammar.d.ts +45 -0
- package/dist/cst/xml-grammar.js +366 -0
- package/dist/dom.d.ts +172 -0
- package/dist/dom.js +415 -0
- package/dist/engine/editor-state.d.ts +22 -0
- package/dist/engine/editor-state.js +32 -0
- package/dist/engine/sync-engine.d.ts +55 -0
- package/dist/engine/sync-engine.js +401 -0
- package/dist/engine/transaction.d.ts +30 -0
- package/dist/engine/transaction.js +52 -0
- package/dist/history-manager.d.ts +23 -0
- package/dist/history-manager.js +40 -0
- package/dist/model/formatter.d.ts +20 -0
- package/dist/model/formatter.js +112 -0
- package/dist/model/xml-api-model.d.ts +47 -0
- package/dist/model/xml-api-model.js +125 -0
- package/dist/model/xml-binder.d.ts +37 -0
- package/dist/model/xml-binder.js +484 -0
- package/dist/model/xml-schema.d.ts +9 -0
- package/dist/model/xml-schema.js +16 -0
- package/dist/xml-api-events.d.ts +43 -0
- package/dist/xml-api-events.js +29 -0
- package/dist/xml-api.d.ts +62 -0
- package/dist/xml-api.js +152 -0
- package/package.json +64 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SyncEngine = void 0;
|
|
4
|
+
const parser_1 = require("../cst/parser");
|
|
5
|
+
const xml_grammar_1 = require("../cst/xml-grammar");
|
|
6
|
+
const history_manager_1 = require("../history-manager");
|
|
7
|
+
const formatter_1 = require("../model/formatter");
|
|
8
|
+
const xml_api_model_1 = require("../model/xml-api-model");
|
|
9
|
+
const xml_binder_1 = require("../model/xml-binder");
|
|
10
|
+
const xml_api_events_1 = require("../xml-api-events");
|
|
11
|
+
const editor_state_1 = require("./editor-state");
|
|
12
|
+
const transaction_1 = require("./transaction");
|
|
13
|
+
class SyncEngine {
|
|
14
|
+
constructor(source, grammar = xml_grammar_1.grammar) {
|
|
15
|
+
this.isTransacting = false;
|
|
16
|
+
this.collabBridge = null;
|
|
17
|
+
this._state = editor_state_1.EditorState.create(source);
|
|
18
|
+
this.parser = new parser_1.Parser(grammar);
|
|
19
|
+
this.binder = new xml_binder_1.XMLBinder(source);
|
|
20
|
+
this.history = new history_manager_1.HistoryManager();
|
|
21
|
+
this.events = new xml_api_events_1.EventEmitter();
|
|
22
|
+
this.fullParse();
|
|
23
|
+
}
|
|
24
|
+
get state() {
|
|
25
|
+
return this._state;
|
|
26
|
+
}
|
|
27
|
+
get source() {
|
|
28
|
+
return this._state.source;
|
|
29
|
+
}
|
|
30
|
+
get model() {
|
|
31
|
+
return this._state.model;
|
|
32
|
+
}
|
|
33
|
+
get cst() {
|
|
34
|
+
return this._state.cst;
|
|
35
|
+
}
|
|
36
|
+
get grammar() {
|
|
37
|
+
return this.parser.grammar;
|
|
38
|
+
}
|
|
39
|
+
setCollabBridge(bridge) {
|
|
40
|
+
this.collabBridge = bridge;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Subscribe to model changes.
|
|
44
|
+
*/
|
|
45
|
+
on(handler) {
|
|
46
|
+
return this.events.on(handler);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Applies a transaction to the engine, updating the state and notifying listeners.
|
|
50
|
+
*/
|
|
51
|
+
dispatch(tr) {
|
|
52
|
+
if (!tr.docChanged)
|
|
53
|
+
return;
|
|
54
|
+
const oldState = this._state;
|
|
55
|
+
const newSource = tr.newSource;
|
|
56
|
+
// History Recording
|
|
57
|
+
if (!this.isTransacting) {
|
|
58
|
+
// In a real transaction system, we would store the inverse patches.
|
|
59
|
+
// For now, we rely on the single patch assumption for history or reconstruct it.
|
|
60
|
+
// Since Transaction can have multiple patches, simple history push is harder.
|
|
61
|
+
// We'll approximate by storing the full undo/redo for the range covered.
|
|
62
|
+
// Ideally, HistoryManager should handle Transaction objects.
|
|
63
|
+
// Temporary: Only support history for single-patch transactions or reconstruct simple history
|
|
64
|
+
if (tr.patches.length === 1) {
|
|
65
|
+
const p = tr.patches[0];
|
|
66
|
+
const oldText = oldState.source.slice(p.from, p.to);
|
|
67
|
+
const newEnd = p.from + p.text.length;
|
|
68
|
+
this.history.push({
|
|
69
|
+
redo: { from: p.from, to: p.to, text: p.text },
|
|
70
|
+
undo: { from: p.from, to: newEnd, text: oldText },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Fallback for multi-patch (clear history or just skip? Skipping is dangerous for undo)
|
|
75
|
+
// For Phase 3, we accept this limitation or clear history.
|
|
76
|
+
// Or we assume history only tracks updateSource calls which are single patch.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
this._state = this._state.update({ source: newSource });
|
|
80
|
+
// Core Update Logic (Parser / Binder)
|
|
81
|
+
this.binder = new xml_binder_1.XMLBinder(newSource);
|
|
82
|
+
// Optimization: If single patch, try incremental. Else full parse.
|
|
83
|
+
let handled = false;
|
|
84
|
+
if (tr.patches.length === 1 && oldState.cst) {
|
|
85
|
+
const p = tr.patches[0];
|
|
86
|
+
const delta = p.text.length - (p.to - p.from);
|
|
87
|
+
const incrementalResult = this.tryIncrementalUpdate(p.from, p.to, delta);
|
|
88
|
+
if (incrementalResult) {
|
|
89
|
+
if (oldState.model) {
|
|
90
|
+
this.updateModelIncremental(incrementalResult.oldNode, incrementalResult.newNode, tr);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
this.events.emit({ type: "full", transaction: tr });
|
|
94
|
+
}
|
|
95
|
+
if (this._state.cst && !this._state.cst.wellFormed) {
|
|
96
|
+
this._state = this._state.update({ model: null });
|
|
97
|
+
this.events.emit({ type: "full", transaction: tr });
|
|
98
|
+
}
|
|
99
|
+
handled = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!handled) {
|
|
103
|
+
this.fullParse();
|
|
104
|
+
this.events.emit({ type: "full", transaction: tr });
|
|
105
|
+
}
|
|
106
|
+
// Notify Collaboration Bridge if local change
|
|
107
|
+
if (!tr.isRemote && this.collabBridge) {
|
|
108
|
+
this.collabBridge.receiveLocalTransaction(tr);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Update the source code (e.g. from text editor).
|
|
113
|
+
* Handles history recording and incremental parsing.
|
|
114
|
+
*/
|
|
115
|
+
updateSource(from, to, text) {
|
|
116
|
+
const tr = new transaction_1.Transaction(this._state);
|
|
117
|
+
tr.replace(from, to, text);
|
|
118
|
+
this.dispatch(tr);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Apply a programmatic change derived from Model operations.
|
|
122
|
+
* This is the "Application -> Source" flow.
|
|
123
|
+
*/
|
|
124
|
+
applyPatch(start, end, text) {
|
|
125
|
+
// Uses dispatch via updateSource logic, but conceptually distinct
|
|
126
|
+
const tr = new transaction_1.Transaction(this._state);
|
|
127
|
+
tr.replace(start, end, text);
|
|
128
|
+
this.dispatch(tr);
|
|
129
|
+
}
|
|
130
|
+
// --- History Operations ---
|
|
131
|
+
undo() {
|
|
132
|
+
const tx = this.history.undo();
|
|
133
|
+
if (tx) {
|
|
134
|
+
this.isTransacting = true;
|
|
135
|
+
try {
|
|
136
|
+
this.updateSource(tx.undo.from, tx.undo.to, tx.undo.text);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
this.isTransacting = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
redo() {
|
|
144
|
+
const tx = this.history.redo();
|
|
145
|
+
if (tx) {
|
|
146
|
+
this.isTransacting = true;
|
|
147
|
+
try {
|
|
148
|
+
this.updateSource(tx.redo.from, tx.redo.to, tx.redo.text);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
this.isTransacting = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// --- High-Level Model Operations (delegated to Binder) ---
|
|
156
|
+
setAttribute(modelNode, key, value) {
|
|
157
|
+
if (!modelNode.cst)
|
|
158
|
+
throw new Error("Model node not linked to CST");
|
|
159
|
+
const patch = this.binder.calcSetAttributePatch(modelNode, key, value);
|
|
160
|
+
if (patch) {
|
|
161
|
+
this.applyPatch(patch.start, patch.end, patch.text);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
updateText(modelNode, text) {
|
|
165
|
+
if (!modelNode.cst)
|
|
166
|
+
throw new Error("Model node not linked to CST");
|
|
167
|
+
const patch = this.binder.calcUpdateTextPatch(modelNode, text);
|
|
168
|
+
if (patch) {
|
|
169
|
+
this.applyPatch(patch.start, patch.end, patch.text);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
replaceNode(target, content) {
|
|
173
|
+
var _a;
|
|
174
|
+
if (!target.cst)
|
|
175
|
+
throw new Error("Model node not linked to CST");
|
|
176
|
+
// Formatting logic
|
|
177
|
+
let indentUnit = " ";
|
|
178
|
+
let currentIndent = "";
|
|
179
|
+
if (target.cst) {
|
|
180
|
+
currentIndent = this.detectIndent(target.cst);
|
|
181
|
+
if ((_a = target.parent) === null || _a === void 0 ? void 0 : _a.cst) {
|
|
182
|
+
const parentIndent = this.detectIndent(target.parent.cst);
|
|
183
|
+
if (currentIndent.startsWith(parentIndent)) {
|
|
184
|
+
const diff = currentIndent.slice(parentIndent.length);
|
|
185
|
+
if (diff.length > 0 && !diff.includes("\n")) {
|
|
186
|
+
indentUnit = diff;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const formatter = new formatter_1.Formatter({ indent: indentUnit });
|
|
192
|
+
let newXml = formatter.format(content);
|
|
193
|
+
if (currentIndent && newXml.includes("\n")) {
|
|
194
|
+
newXml = newXml
|
|
195
|
+
.split("\n")
|
|
196
|
+
.map((line, index) => (index === 0 ? line : currentIndent + line))
|
|
197
|
+
.join("\n");
|
|
198
|
+
}
|
|
199
|
+
const patch = this.binder.calcReplaceNodePatch(target, newXml);
|
|
200
|
+
if (patch) {
|
|
201
|
+
this.applyPatch(patch.start, patch.end, patch.text);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
insertNode(parent, child, index) {
|
|
205
|
+
if (!parent.cst)
|
|
206
|
+
throw new Error("Parent node not linked to CST");
|
|
207
|
+
// Determine basic indentation (simplistic)
|
|
208
|
+
let indentUnit = " ";
|
|
209
|
+
if (parent.cst) {
|
|
210
|
+
const parentIndent = this.detectIndent(parent.cst);
|
|
211
|
+
// Try to find a child to detect indent step
|
|
212
|
+
// ... skipping complex logic for now
|
|
213
|
+
}
|
|
214
|
+
const formatter = new formatter_1.Formatter({ indent: indentUnit });
|
|
215
|
+
const insertText = formatter.format(child);
|
|
216
|
+
const patch = this.binder.calcInsertNodePatch(parent, index, insertText);
|
|
217
|
+
if (patch) {
|
|
218
|
+
this.applyPatch(patch.start, patch.end, patch.text);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
removeNode(parent, child) {
|
|
222
|
+
if (!child.cst)
|
|
223
|
+
throw new Error("Target node not linked to CST");
|
|
224
|
+
const patch = this.binder.calcRemoveNodePatch(child);
|
|
225
|
+
if (patch) {
|
|
226
|
+
this.applyPatch(patch.start, patch.end, patch.text);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// --- Internal Logic ---
|
|
230
|
+
fullParse() {
|
|
231
|
+
try {
|
|
232
|
+
const cst = this.parser.parse(this._state.source);
|
|
233
|
+
let model = null;
|
|
234
|
+
if (cst === null || cst === void 0 ? void 0 : cst.wellFormed) {
|
|
235
|
+
// Hydrate full model
|
|
236
|
+
const newModel = this.binder.hydrate(cst);
|
|
237
|
+
if (newModel instanceof xml_api_model_1.ModelElement) {
|
|
238
|
+
model = newModel;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
this._state = this._state.update({ cst, model });
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
console.error("Parse error:", e);
|
|
245
|
+
this._state = this._state.update({ cst: null, model: null });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
tryIncrementalUpdate(from, to, delta) {
|
|
249
|
+
const currentCst = this._state.cst;
|
|
250
|
+
if (!currentCst)
|
|
251
|
+
return null;
|
|
252
|
+
let target = this.findNodeAt(currentCst, from, to);
|
|
253
|
+
while (target) {
|
|
254
|
+
if (target.name) {
|
|
255
|
+
if (!this.binder.isHydratable(target.name)) {
|
|
256
|
+
target = target.parent;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const parseStart = target.parent ? target.start : 0;
|
|
260
|
+
const result = this.parser.parseAt(this._state.source, parseStart, target.name);
|
|
261
|
+
const expectedEnd = target.end + delta;
|
|
262
|
+
if (result && result.end === expectedEnd) {
|
|
263
|
+
if (target.parent) {
|
|
264
|
+
currentCst.shift(from, delta);
|
|
265
|
+
const index = target.parent.children.indexOf(target);
|
|
266
|
+
if (index !== -1) {
|
|
267
|
+
target.parent.children[index] = result.node;
|
|
268
|
+
result.node.parent = target.parent;
|
|
269
|
+
this.updateAncestorsWellFormed(result.node);
|
|
270
|
+
// State update is implicitly handled because we mutated the CST object
|
|
271
|
+
// which is referenced by this._state.cst
|
|
272
|
+
return { oldNode: target, newNode: result.node };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
this._state = this._state.update({ cst: result.node });
|
|
277
|
+
return { oldNode: target, newNode: result.node };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
target = target.parent;
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
updateModelIncremental(oldNode, newNode, tr) {
|
|
286
|
+
const currentModel = this._state.model;
|
|
287
|
+
if (!currentModel)
|
|
288
|
+
return;
|
|
289
|
+
if (currentModel.cst === oldNode) {
|
|
290
|
+
const newModelNode = this.binder.hydrate(newNode);
|
|
291
|
+
if (newModelNode instanceof xml_api_model_1.ModelElement) {
|
|
292
|
+
this._state = this._state.update({ model: newModelNode });
|
|
293
|
+
this.events.emit({
|
|
294
|
+
type: "full",
|
|
295
|
+
target: newModelNode,
|
|
296
|
+
transaction: tr,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const modelPath = this.findModelNodePath(currentModel, oldNode);
|
|
302
|
+
if (modelPath) {
|
|
303
|
+
const reconciledModel = this.binder.reconcile(modelPath.node, newNode);
|
|
304
|
+
if (reconciledModel) {
|
|
305
|
+
if (reconciledModel !== modelPath.node) {
|
|
306
|
+
modelPath.parent.children[modelPath.index] = reconciledModel;
|
|
307
|
+
reconciledModel.parent = modelPath.parent;
|
|
308
|
+
}
|
|
309
|
+
this.events.emit({
|
|
310
|
+
type: "structure",
|
|
311
|
+
target: reconciledModel,
|
|
312
|
+
transaction: tr,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// --- Helpers ---
|
|
318
|
+
findNodeAt(root, from, to) {
|
|
319
|
+
let current = root;
|
|
320
|
+
while (true) {
|
|
321
|
+
let foundChild = null;
|
|
322
|
+
// Binary search optimization
|
|
323
|
+
let left = 0;
|
|
324
|
+
let right = current.children.length - 1;
|
|
325
|
+
let candidateIndex = -1;
|
|
326
|
+
while (left <= right) {
|
|
327
|
+
const mid = (left + right) >>> 1;
|
|
328
|
+
if (current.children[mid].start <= from) {
|
|
329
|
+
candidateIndex = mid;
|
|
330
|
+
left = mid + 1;
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
right = mid - 1;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (candidateIndex !== -1) {
|
|
337
|
+
const candidate = current.children[candidateIndex];
|
|
338
|
+
if (candidate.end >= to) {
|
|
339
|
+
foundChild = candidate;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (foundChild) {
|
|
343
|
+
current = foundChild;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return current;
|
|
350
|
+
}
|
|
351
|
+
findModelNodePath(root, cstNode) {
|
|
352
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
353
|
+
const child = root.children[i];
|
|
354
|
+
if (child.cst === cstNode) {
|
|
355
|
+
return { parent: root, index: i, node: child };
|
|
356
|
+
}
|
|
357
|
+
if (child instanceof xml_api_model_1.ModelElement) {
|
|
358
|
+
const found = this.findModelNodePath(child, cstNode);
|
|
359
|
+
if (found)
|
|
360
|
+
return found;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
updateAncestorsWellFormed(node) {
|
|
366
|
+
let current = node.parent;
|
|
367
|
+
while (current) {
|
|
368
|
+
let childrenWellFormed = true;
|
|
369
|
+
for (const child of current.children) {
|
|
370
|
+
if (!child.wellFormed) {
|
|
371
|
+
childrenWellFormed = false;
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
let selfWellFormed = childrenWellFormed;
|
|
376
|
+
if (current.name && selfWellFormed) {
|
|
377
|
+
const validator = this.parser.grammar.validators[current.name];
|
|
378
|
+
if (validator && !validator(current, this._state.source)) {
|
|
379
|
+
selfWellFormed = false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
current.wellFormed = selfWellFormed;
|
|
383
|
+
current = current.parent;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
detectIndent(node) {
|
|
387
|
+
const input = this._state.source;
|
|
388
|
+
let i = node.start - 1;
|
|
389
|
+
while (i >= 0) {
|
|
390
|
+
if (input[i] === "\n") {
|
|
391
|
+
return input.slice(i + 1, node.start);
|
|
392
|
+
}
|
|
393
|
+
if (input[i] !== " " && input[i] !== "\t") {
|
|
394
|
+
return "";
|
|
395
|
+
}
|
|
396
|
+
i--;
|
|
397
|
+
}
|
|
398
|
+
return "";
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
exports.SyncEngine = SyncEngine;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { EditorState } from "./editor-state";
|
|
2
|
+
export interface TextPatch {
|
|
3
|
+
from: number;
|
|
4
|
+
to: number;
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Represents a unit of change to the editor state.
|
|
9
|
+
* Currently focuses on text changes (patches).
|
|
10
|
+
*/
|
|
11
|
+
export declare class Transaction {
|
|
12
|
+
readonly startState: EditorState;
|
|
13
|
+
readonly patches: TextPatch[];
|
|
14
|
+
docChanged: boolean;
|
|
15
|
+
/** Indicates if this transaction originated from a remote source (collaboration). */
|
|
16
|
+
isRemote: boolean;
|
|
17
|
+
constructor(startState: EditorState);
|
|
18
|
+
/**
|
|
19
|
+
* Adds a text change to the transaction.
|
|
20
|
+
* @param from Start index
|
|
21
|
+
* @param to End index
|
|
22
|
+
* @param text New text
|
|
23
|
+
*/
|
|
24
|
+
replace(from: number, to: number, text: string): this;
|
|
25
|
+
/**
|
|
26
|
+
* Calculates the new source text by applying patches.
|
|
27
|
+
* Handles multiple patches by sorting them in reverse order of position.
|
|
28
|
+
*/
|
|
29
|
+
get newSource(): string;
|
|
30
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Transaction = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Represents a unit of change to the editor state.
|
|
6
|
+
* Currently focuses on text changes (patches).
|
|
7
|
+
*/
|
|
8
|
+
class Transaction {
|
|
9
|
+
// Placeholder for future selection and metadata
|
|
10
|
+
// public selection: Selection | null = null;
|
|
11
|
+
// public meta: Map<string, any> = new Map();
|
|
12
|
+
constructor(startState) {
|
|
13
|
+
this.startState = startState;
|
|
14
|
+
this.patches = [];
|
|
15
|
+
this.docChanged = false;
|
|
16
|
+
/** Indicates if this transaction originated from a remote source (collaboration). */
|
|
17
|
+
this.isRemote = false;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Adds a text change to the transaction.
|
|
21
|
+
* @param from Start index
|
|
22
|
+
* @param to End index
|
|
23
|
+
* @param text New text
|
|
24
|
+
*/
|
|
25
|
+
replace(from, to, text) {
|
|
26
|
+
if (from < 0 || from > to) {
|
|
27
|
+
throw new Error(`Invalid patch range: ${from}-${to}`);
|
|
28
|
+
}
|
|
29
|
+
this.patches.push({ from, to, text });
|
|
30
|
+
this.docChanged = true;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Calculates the new source text by applying patches.
|
|
35
|
+
* Handles multiple patches by sorting them in reverse order of position.
|
|
36
|
+
*/
|
|
37
|
+
get newSource() {
|
|
38
|
+
let source = this.startState.source;
|
|
39
|
+
// Sort descending by position to apply from end to start
|
|
40
|
+
// This avoids index shifting issues for subsequent patches
|
|
41
|
+
const sorted = [...this.patches].sort((a, b) => b.from - a.from);
|
|
42
|
+
for (const p of sorted) {
|
|
43
|
+
// Validate range
|
|
44
|
+
if (p.from > source.length || p.to > source.length) {
|
|
45
|
+
throw new Error(`Invalid patch range: ${p.from}-${p.to} (Length: ${source.length})`);
|
|
46
|
+
}
|
|
47
|
+
source = source.slice(0, p.from) + p.text + source.slice(p.to);
|
|
48
|
+
}
|
|
49
|
+
return source;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.Transaction = Transaction;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface Transaction {
|
|
2
|
+
redo: {
|
|
3
|
+
from: number;
|
|
4
|
+
to: number;
|
|
5
|
+
text: string;
|
|
6
|
+
};
|
|
7
|
+
undo: {
|
|
8
|
+
from: number;
|
|
9
|
+
to: number;
|
|
10
|
+
text: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare class HistoryManager {
|
|
14
|
+
private undoStack;
|
|
15
|
+
private redoStack;
|
|
16
|
+
private maxHistory;
|
|
17
|
+
constructor(maxHistory?: number);
|
|
18
|
+
push(transaction: Transaction): void;
|
|
19
|
+
undo(): Transaction | null;
|
|
20
|
+
redo(): Transaction | null;
|
|
21
|
+
canUndo(): boolean;
|
|
22
|
+
canRedo(): boolean;
|
|
23
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HistoryManager = void 0;
|
|
4
|
+
class HistoryManager {
|
|
5
|
+
constructor(maxHistory = 100) {
|
|
6
|
+
this.undoStack = [];
|
|
7
|
+
this.redoStack = [];
|
|
8
|
+
this.maxHistory = maxHistory;
|
|
9
|
+
}
|
|
10
|
+
push(transaction) {
|
|
11
|
+
this.undoStack.push(transaction);
|
|
12
|
+
if (this.undoStack.length > this.maxHistory) {
|
|
13
|
+
this.undoStack.shift();
|
|
14
|
+
}
|
|
15
|
+
this.redoStack = []; // Clear redo stack on new operation
|
|
16
|
+
}
|
|
17
|
+
undo() {
|
|
18
|
+
const transaction = this.undoStack.pop();
|
|
19
|
+
if (transaction) {
|
|
20
|
+
this.redoStack.push(transaction);
|
|
21
|
+
return transaction;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
redo() {
|
|
26
|
+
const transaction = this.redoStack.pop();
|
|
27
|
+
if (transaction) {
|
|
28
|
+
this.undoStack.push(transaction);
|
|
29
|
+
return transaction;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
canUndo() {
|
|
34
|
+
return this.undoStack.length > 0;
|
|
35
|
+
}
|
|
36
|
+
canRedo() {
|
|
37
|
+
return this.redoStack.length > 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.HistoryManager = HistoryManager;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ModelNode } from "./xml-api-model";
|
|
2
|
+
export interface FormatterOptions {
|
|
3
|
+
indent?: string;
|
|
4
|
+
newline?: string;
|
|
5
|
+
force?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare class Formatter {
|
|
8
|
+
private indent;
|
|
9
|
+
private newline;
|
|
10
|
+
private force;
|
|
11
|
+
constructor(options?: FormatterOptions);
|
|
12
|
+
format(node: ModelNode): string;
|
|
13
|
+
private formatNode;
|
|
14
|
+
private formatAttributes;
|
|
15
|
+
private isInline;
|
|
16
|
+
private hasFormatting;
|
|
17
|
+
private getIndent;
|
|
18
|
+
private escape;
|
|
19
|
+
private escapeAttribute;
|
|
20
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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 {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
var _a, _b, _c;
|
|
8
|
+
this.indent = (_a = options.indent) !== null && _a !== void 0 ? _a : " ";
|
|
9
|
+
this.newline = (_b = options.newline) !== null && _b !== void 0 ? _b : "\n";
|
|
10
|
+
this.force = (_c = options.force) !== null && _c !== void 0 ? _c : false;
|
|
11
|
+
}
|
|
12
|
+
format(node) {
|
|
13
|
+
return this.formatNode(node, 0);
|
|
14
|
+
}
|
|
15
|
+
formatNode(node, level) {
|
|
16
|
+
if (node instanceof xml_api_model_1.ModelText) {
|
|
17
|
+
return this.escape(node.text);
|
|
18
|
+
}
|
|
19
|
+
if (node instanceof xml_api_model_1.ModelComment) {
|
|
20
|
+
return `<!--${node.content}-->`;
|
|
21
|
+
}
|
|
22
|
+
if (node instanceof xml_api_model_1.ModelCDATA) {
|
|
23
|
+
return `<![CDATA[${node.content}]]>`;
|
|
24
|
+
}
|
|
25
|
+
if (node instanceof xml_api_model_1.ModelElement) {
|
|
26
|
+
const tagName = node.tagName;
|
|
27
|
+
const attributes = this.formatAttributes(node.attributes);
|
|
28
|
+
const children = node.children;
|
|
29
|
+
if (children.length === 0) {
|
|
30
|
+
return `<${tagName}${attributes} />`;
|
|
31
|
+
}
|
|
32
|
+
const isInline = this.isInline(children);
|
|
33
|
+
const hasFormatting = this.force ? false : this.hasFormatting(children);
|
|
34
|
+
let result = `<${tagName}${attributes}>`;
|
|
35
|
+
if (isInline || hasFormatting) {
|
|
36
|
+
for (const child of children) {
|
|
37
|
+
if (this.force &&
|
|
38
|
+
child instanceof xml_api_model_1.ModelText &&
|
|
39
|
+
child.text.includes("\n") &&
|
|
40
|
+
child.text.trim().length === 0) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
result += this.formatNode(child, level + 1);
|
|
44
|
+
}
|
|
45
|
+
result += `</${tagName}>`;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
for (const child of children) {
|
|
49
|
+
if (this.force &&
|
|
50
|
+
child instanceof xml_api_model_1.ModelText &&
|
|
51
|
+
child.text.includes("\n") &&
|
|
52
|
+
child.text.trim().length === 0) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
result +=
|
|
56
|
+
this.newline +
|
|
57
|
+
this.getIndent(level + 1) +
|
|
58
|
+
this.formatNode(child, level + 1);
|
|
59
|
+
}
|
|
60
|
+
result += `${this.newline + this.getIndent(level)}</${tagName}>`;
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
formatAttributes(attributes) {
|
|
67
|
+
if (attributes.size === 0)
|
|
68
|
+
return "";
|
|
69
|
+
const entries = Array.from(attributes.entries());
|
|
70
|
+
return (" " +
|
|
71
|
+
entries
|
|
72
|
+
.map(([key, value]) => `${key}="${this.escapeAttribute(value)}"`) // Corrected: escaped " to \"
|
|
73
|
+
.join(" "));
|
|
74
|
+
}
|
|
75
|
+
isInline(children) {
|
|
76
|
+
for (const theChild of children) {
|
|
77
|
+
if (theChild instanceof xml_api_model_1.ModelText) {
|
|
78
|
+
// If there is any non-whitespace text, treat as inline.
|
|
79
|
+
if (theChild.text.trim().length > 0)
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (theChild instanceof xml_api_model_1.ModelCDATA) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
hasFormatting(children) {
|
|
89
|
+
for (const theChild of children) {
|
|
90
|
+
if (theChild instanceof xml_api_model_1.ModelText) {
|
|
91
|
+
// If it contains a newline and is otherwise whitespace, it's likely formatting.
|
|
92
|
+
if (theChild.text.includes("\n") && theChild.text.trim().length === 0) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
getIndent(level) {
|
|
100
|
+
return this.indent.repeat(level);
|
|
101
|
+
}
|
|
102
|
+
escape(str) {
|
|
103
|
+
return str
|
|
104
|
+
.replace(/&/g, "&")
|
|
105
|
+
.replace(/</g, "<")
|
|
106
|
+
.replace(/>/g, ">");
|
|
107
|
+
}
|
|
108
|
+
escapeAttribute(str) {
|
|
109
|
+
return this.escape(str).replace(/"/g, """);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
exports.Formatter = Formatter;
|