1 | const DEFAULT_IGNORE_TAGS = ['script', 'style', 'svg'];
|
2 | const DEFAULT_EMPTY_ATTRS = ['class', 'id'];
|
3 | const VOID_ELEMENTS = [
|
4 | 'area',
|
5 | 'base',
|
6 | 'br',
|
7 | 'col',
|
8 | 'embed',
|
9 | 'hr',
|
10 | 'img',
|
11 | 'input',
|
12 | 'keygen',
|
13 | 'link',
|
14 | 'menuitem',
|
15 | 'meta',
|
16 | 'param',
|
17 | 'source',
|
18 | 'track',
|
19 | 'wbr',
|
20 | ];
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 | const not = p => (...args) => !p(...args);
|
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 | export function getDiffableHTML(html, options = {}) {
|
70 | const ignoreAttributes = (options.ignoreAttributes
|
71 | ? options.ignoreAttributes.filter(e => typeof e === 'string')
|
72 | : []);
|
73 | const ignoreAttributesForTags = (options.ignoreAttributes
|
74 | ? options.ignoreAttributes.filter(e => typeof e !== 'string')
|
75 | : []);
|
76 | const ignoreTags = [...(options.ignoreTags || []), ...DEFAULT_IGNORE_TAGS];
|
77 | const ignoreChildren = options.ignoreChildren || [];
|
78 | const stripEmptyAttributes = options.stripEmptyAttributes || DEFAULT_EMPTY_ATTRS;
|
79 | const escapeAttributes = /(&|")/g;
|
80 |
|
81 | const escapeAttributesFn = match => (match === '&' ? '&' : '"');
|
82 |
|
83 | let text = '';
|
84 | let depth = -1;
|
85 |
|
86 | const handledChildrenForNode = new Set();
|
87 |
|
88 | const handledNodeStarted = new Set();
|
89 |
|
90 |
|
91 | function getIndentation() {
|
92 | return ' '.repeat(depth);
|
93 | }
|
94 |
|
95 |
|
96 | function printText(textNode) {
|
97 | const value = textNode.nodeValue.trim();
|
98 |
|
99 | if (value !== '') {
|
100 | text += `${getIndentation()}${value}\n`;
|
101 | }
|
102 | }
|
103 |
|
104 |
|
105 | function shouldProcessChildren(node) {
|
106 | const name = node.nodeName.toLowerCase();
|
107 | return (
|
108 | !ignoreTags.includes(name) &&
|
109 | !ignoreChildren.includes(name) &&
|
110 | !handledChildrenForNode.has(node)
|
111 | );
|
112 | }
|
113 |
|
114 | |
115 |
|
116 |
|
117 |
|
118 |
|
119 | function getClassListValueString(el) {
|
120 |
|
121 | return [...el.classList.values()].sort().join(' ');
|
122 | }
|
123 |
|
124 | function shouldStripAttribute({ name, value }) {
|
125 | return stripEmptyAttributes.includes(name) && value.trim() === '';
|
126 | }
|
127 |
|
128 | |
129 |
|
130 |
|
131 |
|
132 | function getAttributeString(el, { name, value }) {
|
133 | if (shouldStripAttribute({ name, value })) return '';
|
134 | if (name === 'class') return ` class="${getClassListValueString(el)}"`;
|
135 | return ` ${name}="${value.replace(escapeAttributes, escapeAttributesFn)}"`;
|
136 | }
|
137 |
|
138 | |
139 |
|
140 |
|
141 |
|
142 | function isIgnoredAttribute(el) {
|
143 | return function isIgnoredElementAttibute(attr) {
|
144 | if (ignoreAttributes.includes(attr.name) || shouldStripAttribute(attr)) {
|
145 | return true;
|
146 | }
|
147 |
|
148 | return !!ignoreAttributesForTags.find(e => {
|
149 | if (!e.tags || !e.attributes) {
|
150 | throw new Error(
|
151 | `An object entry to ignoreAttributes should contain a 'tags' and an 'attributes' property.`,
|
152 | );
|
153 | }
|
154 | return e.tags.includes(el.nodeName.toLowerCase()) && e.attributes.includes(attr.name);
|
155 | });
|
156 | };
|
157 | }
|
158 |
|
159 | const sortAttribute = (a, b) => a.name.localeCompare(b.name);
|
160 |
|
161 |
|
162 | function getAttributesString(el) {
|
163 | let attrStr = '';
|
164 | const attributes = Array.from(el.attributes)
|
165 | .filter(not(isIgnoredAttribute(el)))
|
166 | .sort(sortAttribute);
|
167 |
|
168 | if (attributes.length === 1) {
|
169 | attrStr = getAttributeString(el, attributes[0]);
|
170 | } else if (attributes.length > 1) {
|
171 | for (let i = 0; i < attributes.length; i += 1) {
|
172 | attrStr += `\n${getIndentation()} ${getAttributeString(el, attributes[i])}`;
|
173 | }
|
174 | attrStr += `\n${getIndentation()}`;
|
175 | }
|
176 |
|
177 | return attrStr;
|
178 | }
|
179 |
|
180 |
|
181 | function getLocalName(el) {
|
182 |
|
183 |
|
184 | return el.getAttribute('data-tag-name') || el.localName;
|
185 | }
|
186 |
|
187 |
|
188 | function printOpenElement(el) {
|
189 | text += `${getIndentation()}<${getLocalName(el)}${getAttributesString(el)}>\n`;
|
190 | }
|
191 |
|
192 |
|
193 | function onNodeStart(node) {
|
194 |
|
195 | if (node.nodeName === 'DIFF-CONTAINER' || ignoreTags.includes(node.nodeName.toLowerCase())) {
|
196 | return;
|
197 | }
|
198 |
|
199 |
|
200 |
|
201 | if (handledNodeStarted.has(node)) {
|
202 | return;
|
203 | }
|
204 | handledNodeStarted.add(node);
|
205 |
|
206 | if (node instanceof Text) {
|
207 | printText(node);
|
208 | } else if (node instanceof Element) {
|
209 | printOpenElement(node);
|
210 | } else {
|
211 | throw new Error(`Unknown node type: ${node}`);
|
212 | }
|
213 | }
|
214 |
|
215 |
|
216 | function printCloseElement(el) {
|
217 | if (getLocalName(el) === 'diff-container' || VOID_ELEMENTS.includes(getLocalName(el))) {
|
218 | return;
|
219 | }
|
220 |
|
221 | text += `${getIndentation()}</${getLocalName(el)}>\n`;
|
222 | }
|
223 |
|
224 |
|
225 | function onNodeEnd(node) {
|
226 |
|
227 | if (ignoreTags.includes(node.nodeName.toLowerCase())) {
|
228 | return;
|
229 | }
|
230 |
|
231 | if (node instanceof Element) {
|
232 | printCloseElement(node);
|
233 | }
|
234 | }
|
235 |
|
236 | let container;
|
237 |
|
238 | if (typeof html === 'string') {
|
239 | container = document.createElement('diff-container');
|
240 | container.innerHTML = html;
|
241 | depth = -1;
|
242 | } else if (html instanceof Node) {
|
243 | container = html;
|
244 | depth = 0;
|
245 | } else {
|
246 | throw new Error(`Cannot create diffable HTML from: ${html}`);
|
247 | }
|
248 |
|
249 | const walker = document.createTreeWalker(
|
250 | container,
|
251 | NodeFilter.SHOW_TEXT + NodeFilter.SHOW_ELEMENT,
|
252 | null,
|
253 | false,
|
254 | );
|
255 |
|
256 |
|
257 | while (walker.currentNode) {
|
258 | const current = walker.currentNode;
|
259 | onNodeStart(current);
|
260 |
|
261 |
|
262 | if (shouldProcessChildren(current) && walker.firstChild()) {
|
263 | depth += 1;
|
264 | } else {
|
265 |
|
266 | onNodeEnd(current);
|
267 |
|
268 |
|
269 | const sibling = walker.nextSibling();
|
270 |
|
271 |
|
272 | if (!sibling) {
|
273 | depth -= 1;
|
274 |
|
275 | const parent = walker.parentNode();
|
276 |
|
277 | if (!parent) {
|
278 | break;
|
279 | }
|
280 |
|
281 |
|
282 |
|
283 | handledChildrenForNode.add(parent);
|
284 | }
|
285 | }
|
286 | }
|
287 |
|
288 | return text;
|
289 | }
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 | export function isDiffOptions(arg) {
|
296 | return (
|
297 | arg &&
|
298 | arg !== null &&
|
299 | typeof arg === 'object' &&
|
300 | ('ignoreAttributes' in arg ||
|
301 | 'ignoreTags' in arg ||
|
302 | 'ignoreChildren' in arg ||
|
303 | 'stripEmptyAttributes' in arg)
|
304 | );
|
305 | }
|