-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathhandleEdit.ts
179 lines (160 loc) · 4.76 KB
/
handleEdit.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import {
EditV2,
Insert,
isComplex,
isInsert,
isRemove,
isSetAttributes,
isSetTextContent,
Remove,
SetAttributes,
SetTextContent,
} from "./editv2.js";
function handleSetTextContent({
element,
textContent,
}: SetTextContent): (SetTextContent | Insert)[] {
const { childNodes } = element;
const restoreChildNodes: Insert[] = Array.from(childNodes).map((node) => ({
parent: element,
node,
reference: null,
}));
element.textContent = textContent;
const undoTextContent: SetTextContent = { element, textContent: "" };
return [undoTextContent, ...restoreChildNodes];
}
function uniqueNSPrefix(element: Element, ns: string): string {
let i = 1;
const attributes = Array.from(element.attributes);
const hasSamePrefix = (attribute: Attr) =>
attribute.prefix === `ens${i}` && attribute.namespaceURI !== ns;
const nsOrNull = new Set([null, ns]);
const differentNamespace = (prefix: string) =>
!nsOrNull.has(element.lookupNamespaceURI(prefix));
while (differentNamespace(`ens${i}`) || attributes.find(hasSamePrefix))
i += 1;
return `ens${i}`;
}
const xmlAttributeName =
/^(?!xml|Xml|xMl|xmL|XMl|xML|XmL|XML)[A-Za-z_][A-Za-z0-9-_.]*(:[A-Za-z_][A-Za-z0-9-_.]*)?$/;
function validName(name: string): boolean {
return xmlAttributeName.test(name);
}
function handleSetAttributes({
element,
attributes,
attributesNS,
}: SetAttributes): SetAttributes {
const oldAttributes = { ...attributes };
const oldAttributesNS = { ...attributesNS };
// save element's non-prefixed attributes for undo
Object.keys(attributes)
.reverse()
.forEach((name) => {
oldAttributes[name] = element.getAttribute(name);
});
// change element's non-prefixed attributes
for (const entry of Object.entries(attributes)) {
try {
const [name, value] = entry as [string, string | null];
if (value === null) element.removeAttribute(name);
else element.setAttribute(name, value);
} catch (_e) {
// undo nothing if update didn't work on this attribute
delete oldAttributes[entry[0]];
}
}
// save element's namespaced attributes for undo
Object.entries(attributesNS).forEach(([ns, attrs]) => {
Object.keys(attrs!)
.filter(validName)
.reverse()
.forEach((name) => {
oldAttributesNS[ns] = {
...oldAttributesNS[ns],
[name]: element.getAttributeNS(ns, name.split(":").pop()!),
};
});
Object.keys(attrs!)
.filter((name) => !validName(name))
.forEach((name) => {
delete oldAttributesNS[ns]![name];
});
});
// change element's namespaced attributes
for (const nsEntry of Object.entries(attributesNS)) {
const [ns, attrs] = nsEntry as [
string,
Partial<Record<string, string | null>>,
];
for (const entry of Object.entries(attrs).filter(([name]) =>
validName(name),
)) {
try {
const [name, value] = entry as [string, string | null];
if (value === null) {
element.removeAttributeNS(ns, name.split(":").pop()!);
} else {
let qualifiedName = name;
if (!qualifiedName.includes(":")) {
let prefix = element.lookupPrefix(ns);
if (!prefix) prefix = uniqueNSPrefix(element, ns);
qualifiedName = `${prefix}:${name}`;
}
element.setAttributeNS(ns, qualifiedName, value);
}
} catch (_e) {
delete oldAttributesNS[ns]![entry[0]];
}
}
}
return {
element,
attributes: oldAttributes,
attributesNS: oldAttributesNS,
};
}
function handleRemove({ node }: Remove): Insert | [] {
const { parentNode: parent, nextSibling: reference } = node;
node.parentNode?.removeChild(node);
if (parent)
return {
node,
parent,
reference,
};
return [];
}
function handleInsert({
parent,
node,
reference,
}: Insert): Insert | Remove | [] {
try {
const { parentNode, nextSibling } = node;
parent.insertBefore(node, reference);
if (parentNode)
// undo: move child node back to original place
return {
node,
parent: parentNode,
reference: nextSibling,
};
// undo: remove orphaned node
return { node };
} catch (_e) {
// undo nothing if insert doesn't work on these nodes
return [];
}
}
/** Applies an EditV2, returning the corresponding "undo" EditV2. */
export function handleEdit(edit: EditV2): EditV2 {
if (isInsert(edit)) return handleInsert(edit);
if (isRemove(edit)) return handleRemove(edit);
if (isSetAttributes(edit)) return handleSetAttributes(edit);
if (isSetTextContent(edit)) return handleSetTextContent(edit);
if (isComplex(edit)) return edit.map((edit) => handleEdit(edit)).reverse();
console.error(`Invalid edit provided: ${edit}`);
return [];
}