@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,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, "&amp;")
105
+ .replace(/</g, "&lt;")
106
+ .replace(/>/g, "&gt;");
107
+ }
108
+ escapeAttribute(str) {
109
+ return this.escape(str).replace(/"/g, "&quot;");
110
+ }
111
+ }
112
+ exports.Formatter = Formatter;