1 | import HTMLParser from 'parse5/lib/parser';
|
2 | import defaultTreeAdapter from 'parse5/lib/tree-adapters/default';
|
3 | import Tokenizer from 'parse5/lib/tokenizer';
|
4 |
|
5 | /**
|
6 | * @name AttributeList
|
7 | * @class
|
8 | * @extends Array
|
9 | * @classdesc Return a new list of {@link Element} attributes.
|
10 | * @param {...Array|AttributeList|Object} attrs - An array or object of attributes.
|
11 | * @returns {AttributeList}
|
12 | * @example
|
13 | * new AttributeList([{ name: 'class', value: 'foo' }, { name: 'id', value: 'bar' }])
|
14 | * @example
|
15 | * new AttributeList({ class: 'foo', id: 'bar' })
|
16 | */
|
17 | class AttributeList extends Array {
|
18 | constructor(attrs) {
|
19 | super();
|
20 |
|
21 | if (attrs === Object(attrs)) {
|
22 | this.push(...getAttributeListArray(attrs));
|
23 | }
|
24 | }
|
25 | /**
|
26 | * Add an attribute or attributes to the current {@link AttributeList}.
|
27 | * @param {Array|Object|RegExp|String} name - The attribute to remove.
|
28 | * @param {String} [value] - The value of the attribute being added.
|
29 | * @returns {Boolean} - Whether the attribute or attributes were added to the current {@link AttributeList}.
|
30 | * @example <caption>Add an empty "id" attribute.</caption>
|
31 | * attrs.add('id')
|
32 | * @example <caption>Add an "id" attribute with a value of "bar".</caption>
|
33 | * attrs.add({ id: 'bar' })
|
34 | * @example
|
35 | * attrs.add([{ name: 'id', value: 'bar' }])
|
36 | */
|
37 |
|
38 |
|
39 | add(name, ...args) {
|
40 | return toggle(this, getAttributeListArray(name, ...args), true).attributeAdded;
|
41 | }
|
42 | /**
|
43 | * Return a new clone of the current {@link AttributeList} while conditionally applying additional attributes.
|
44 | * @param {...Array|AttributeList|Object} attrs - Additional attributes to be added to the new {@link AttributeList}.
|
45 | * @returns {Element} - The cloned Element.
|
46 | * @example
|
47 | * attrs.clone()
|
48 | * @example <caption>Clone the current attribute and add an "id" attribute with a value of "bar".</caption>
|
49 | * attrs.clone({ name: 'id', value: 'bar' })
|
50 | */
|
51 |
|
52 |
|
53 | clone(...attrs) {
|
54 | return new AttributeList(Array.from(this).concat(getAttributeListArray(attrs)));
|
55 | }
|
56 | /**
|
57 | * Return whether an attribute or attributes exists in the current {@link AttributeList}.
|
58 | * @param {String} name - The name or attribute object being accessed.
|
59 | * @returns {Boolean} - Whether the attribute exists.
|
60 | * @example <caption>Return whether there is an "id" attribute.</caption>
|
61 | * attrs.contains('id')
|
62 | * @example
|
63 | * attrs.contains({ id: 'bar' })
|
64 | * @example <caption>Return whether there is an "id" attribute with a value of "bar".</caption>
|
65 | * attrs.contains([{ name: 'id': value: 'bar' }])
|
66 | */
|
67 |
|
68 |
|
69 | contains(name) {
|
70 | return this.indexOf(name) !== -1;
|
71 | }
|
72 | /**
|
73 | * Return an attribute value by name from the current {@link AttributeList}.
|
74 | * @description If the attribute exists with a value then a String is returned. If the attribute exists with no value then `null` is returned. If the attribute does not exist then `false` is returned.
|
75 | * @param {RegExp|String} name - The name of the attribute being accessed.
|
76 | * @returns {Boolean|Null|String} - The value of the attribute (a string or null) or false (if the attribute does not exist).
|
77 | * @example <caption>Return the value of "id" or `false`.</caption>
|
78 | * // <div>this element has no "id" attribute</div>
|
79 | * attrs.get('id') // returns false
|
80 | * // <div id>this element has an "id" attribute with no value</div>
|
81 | * attrs.get('id') // returns null
|
82 | * // <div id="">this element has an "id" attribute with a value</div>
|
83 | * attrs.get('id') // returns ''
|
84 | */
|
85 |
|
86 |
|
87 | get(name) {
|
88 | const index = this.indexOf(name);
|
89 | return index === -1 ? false : this[index].value;
|
90 | }
|
91 | /**
|
92 | * Return the position of an attribute by name or attribute object in the current {@link AttributeList}.
|
93 | * @param {Array|Object|RegExp|String} name - The attribute to locate.
|
94 | * @returns {Number} - The index of the attribute or -1.
|
95 | * @example <caption>Return the index of "id".</caption>
|
96 | * attrs.indexOf('id')
|
97 | * @example <caption>Return the index of /d$/.</caption>
|
98 | * attrs.indexOf(/d$/i)
|
99 | * @example <caption>Return the index of "foo" with a value of "bar".</caption>
|
100 | * attrs.indexOf({ foo: 'bar' })
|
101 | * @example <caption>Return the index of "ariaLabel" or "aria-label" matching /^open/.</caption>
|
102 | * attrs.indexOf({ ariaLabel: /^open/ })
|
103 | * @example <caption>Return the index of an attribute whose name matches `/^foo/`.</caption>
|
104 | * attrs.indexOf([{ name: /^foo/ })
|
105 | */
|
106 |
|
107 |
|
108 | indexOf(name, ...args) {
|
109 | return this.findIndex(Array.isArray(name) ? findIndexByArray : isRegExp(name) ? findIndexByRegExp : name === Object(name) ? findIndexByObject : findIndexByString);
|
110 |
|
111 | function findIndexByArray(attr) {
|
112 | return name.some(innerAttr => ('name' in Object(innerAttr) ? isRegExp(innerAttr.name) ? innerAttr.name.test(attr.name) : String(innerAttr.name) === attr.name : true) && ('value' in Object(innerAttr) ? isRegExp(innerAttr.value) ? innerAttr.value.test(attr.value) : getAttributeValue(innerAttr.value) === attr.value : true));
|
113 | }
|
114 |
|
115 | function findIndexByObject(attr) {
|
116 | const innerAttr = name[attr.name] || name[toCamelCaseString(attr.name)];
|
117 | return innerAttr ? isRegExp(innerAttr) ? innerAttr.test(attr.value) : attr.value === innerAttr : false;
|
118 | }
|
119 |
|
120 | function findIndexByRegExp(attr) {
|
121 | return name.test(attr.name) && (args.length ? isRegExp(args[0]) ? args[0].test(attr.value) : attr.value === getAttributeValue(args[0]) : true);
|
122 | }
|
123 |
|
124 | function findIndexByString(attr) {
|
125 | return (attr.name === String(name) || attr.name === toKebabCaseString(name)) && (args.length ? isRegExp(args[0]) ? args[0].test(attr.value) : attr.value === getAttributeValue(args[0]) : true);
|
126 | }
|
127 | }
|
128 | /**
|
129 | * Remove an attribute or attributes from the current {@link AttributeList}.
|
130 | * @param {Array|Object|RegExp|String} name - The attribute to remove.
|
131 | * @param {String} [value] - The value of the attribute being removed.
|
132 | * @returns {Boolean} - Whether the attribute or attributes were removed from the {@link AttributeList}.
|
133 | * @example <caption>Remove the "id" attribute.</caption>
|
134 | * attrs.remove('id')
|
135 | * @example <caption>Remove the "id" attribute when it has a value of "bar".</caption>
|
136 | * attrs.remove('id', 'bar')
|
137 | * @example
|
138 | * attrs.remove({ id: 'bar' })
|
139 | * @example
|
140 | * attrs.remove([{ name: 'id', value: 'bar' }])
|
141 | * @example <caption>Remove the "id" and "class" attributes.</caption>
|
142 | * attrs.remove(['id', 'class'])
|
143 | */
|
144 |
|
145 |
|
146 | remove(name, ...args) {
|
147 | return toggle(this, getAttributeListArray(name, ...args), false).attributeRemoved;
|
148 | }
|
149 | /**
|
150 | * Toggle an attribute or attributes from the current {@link AttributeList}.
|
151 | * @param {String|Object} name_or_attrs - The name of the attribute being toggled, or an object of attributes being toggled.
|
152 | * @param {String|Boolean} [value_or_force] - The value of the attribute being toggled when the first argument is not an object, or attributes should be exclusively added (true) or removed (false).
|
153 | * @param {Boolean} [force] - Whether attributes should be exclusively added (true) or removed (false).
|
154 | * @returns {Boolean} - Whether any attribute was added to the current {@link AttributeList}.
|
155 | * @example <caption>Toggle the "id" attribute.</caption>
|
156 | * attrs.toggle('id')
|
157 | * @example <caption>Toggle the "id" attribute with a value of "bar".</caption>
|
158 | * attrs.toggle('id', 'bar')
|
159 | * @example
|
160 | * attrs.toggle({ id: 'bar' })
|
161 | * @example
|
162 | * attrs.toggle([{ name: 'id', value: 'bar' }])
|
163 | */
|
164 |
|
165 |
|
166 | toggle(name, ...args) {
|
167 | const attrs = getAttributeListArray(name, ...args);
|
168 | const force = name === Object(name) ? args[0] == null ? null : Boolean(args[0]) : args[1] == null ? null : Boolean(args[1]);
|
169 | const result = toggle(this, attrs, force);
|
170 | return result.attributeAdded || result.atttributeModified;
|
171 | }
|
172 | /**
|
173 | * Return the current {@link AttributeList} as a String.
|
174 | * @returns {String} A string version of the current {@link AttributeList}
|
175 | * @example
|
176 | * attrs.toString() // returns 'class="foo" data-foo="bar"'
|
177 | */
|
178 |
|
179 |
|
180 | toString() {
|
181 | return this.length ? `${this.map(attr => `${Object(attr.source).before || ' '}${attr.name}${attr.value === null ? '' : `=${Object(attr.source).quote || '"'}${attr.value}${Object(attr.source).quote || '"'}`}`).join('')}` : '';
|
182 | }
|
183 | /**
|
184 | * Return the current {@link AttributeList} as an Object.
|
185 | * @returns {Object} point - An object version of the current {@link AttributeList}
|
186 | * @example
|
187 | * attrs.toJSON() // returns { class: 'foo', dataFoo: 'bar' } when <x class="foo" data-foo: "bar" />
|
188 | */
|
189 |
|
190 |
|
191 | toJSON() {
|
192 | return this.reduce((object, attr) => Object.assign(object, {
|
193 | [toCamelCaseString(attr.name)]: attr.value
|
194 | }), {});
|
195 | }
|
196 | /**
|
197 | * Return a new {@link AttributeList} from an array or object.
|
198 | * @param {Array|AttributeList|Object} nodes - An array or object of attributes.
|
199 | * @returns {AttributeList} A new {@link AttributeList}
|
200 | * @example <caption>Return an array of attributes from a regular object.</caption>
|
201 | * AttributeList.from({ dataFoo: 'bar' }) // returns AttributeList [{ name: 'data-foo', value: 'bar' }]
|
202 | * @example <caption>Return a normalized array of attributes from an impure array of attributes.</caption>
|
203 | * AttributeList.from([{ name: 'data-foo', value: true, foo: 'bar' }]) // returns AttributeList [{ name: 'data-foo', value: 'true' }]
|
204 | */
|
205 |
|
206 |
|
207 | static from(attrs) {
|
208 | return new AttributeList(getAttributeListArray(attrs));
|
209 | }
|
210 |
|
211 | }
|
212 | /**
|
213 | * Toggle an attribute or attributes from an {@link AttributeList}.
|
214 | * @param {AttributeList} attrs - The {@link AttributeList} being modified.
|
215 | * @param {String|Object} toggles - The attributes being toggled.
|
216 | * @param {Boolean} [force] - Whether attributes should be exclusively added (true) or removed (false)
|
217 | * @returns {Object} An object specifying whether any attributes were added, removed, and/or modified.
|
218 | * @private
|
219 | */
|
220 |
|
221 |
|
222 | function toggle(attrs, toggles, force) {
|
223 | let attributeAdded = false;
|
224 | let attributeRemoved = false;
|
225 | let atttributeModified = false;
|
226 | toggles.forEach(toggleAttr => {
|
227 | const index = attrs.indexOf(toggleAttr.name);
|
228 |
|
229 | if (index === -1) {
|
230 | if (force !== false) {
|
231 | // add the attribute (if not exclusively removing attributes)
|
232 | attrs.push(toggleAttr);
|
233 | attributeAdded = true;
|
234 | }
|
235 | } else if (force !== true) {
|
236 | // remove the attribute (if not exclusively adding attributes)
|
237 | attrs.splice(index, 1);
|
238 | attributeRemoved = true;
|
239 | } else if (toggleAttr.value !== undefined && attrs[index].value !== toggleAttr.value) {
|
240 | // change the value of the attribute (if exclusively adding attributes)
|
241 | attrs[index].value = toggleAttr.value;
|
242 | atttributeModified = true;
|
243 | }
|
244 | });
|
245 | return {
|
246 | attributeAdded,
|
247 | attributeRemoved,
|
248 | atttributeModified
|
249 | };
|
250 | }
|
251 | /**
|
252 | * Return an AttributeList-compatible array from an array or object.
|
253 | * @private
|
254 | */
|
255 |
|
256 |
|
257 | function getAttributeListArray(attrs, value) {
|
258 | return attrs === null || attrs === undefined // void values are omitted
|
259 | ? [] : Array.isArray(attrs) // arrays are sanitized as a name or value, and then optionally a source
|
260 | ? attrs.reduce((attrs, rawattr) => {
|
261 | const attr = {};
|
262 |
|
263 | if ('name' in Object(rawattr)) {
|
264 | attr.name = String(rawattr.name);
|
265 | }
|
266 |
|
267 | if ('value' in Object(rawattr)) {
|
268 | attr.value = getAttributeValue(rawattr.value);
|
269 | }
|
270 |
|
271 | if ('source' in Object(rawattr)) {
|
272 | attr.source = rawattr.source;
|
273 | }
|
274 |
|
275 | if ('name' in attr || 'value' in attr) {
|
276 | attrs.push(attr);
|
277 | }
|
278 |
|
279 | return attrs;
|
280 | }, []) : attrs === Object(attrs) // objects are sanitized as a name and value
|
281 | ? Object.keys(attrs).map(name => ({
|
282 | name: toKebabCaseString(name),
|
283 | value: getAttributeValue(attrs[name])
|
284 | })) : 1 in arguments // both name and value arguments are sanitized as a name and value
|
285 | ? [{
|
286 | name: attrs,
|
287 | value: getAttributeValue(value)
|
288 | }] // one name argument is sanitized as a name
|
289 | : [{
|
290 | name: attrs
|
291 | }];
|
292 | }
|
293 | /**
|
294 | * Return a value transformed into an attribute value.
|
295 | * @description Expected values are strings. Unexpected values are null, objects, and undefined. Nulls returns null, Objects with the default toString return their JSON.stringify’d value otherwise toString’d, and Undefineds return an empty string.
|
296 | * @example <caption>Expected values.</caption>
|
297 | * getAttributeValue('foo') // returns 'foo'
|
298 | * getAttributeValue('') // returns ''
|
299 | * @example <caption>Unexpected values.</caption>
|
300 | * getAttributeValue(null) // returns null
|
301 | * getAttributeValue(undefined) // returns ''
|
302 | * getAttributeValue(['foo']) // returns '["foo"]'
|
303 | * getAttributeValue({ toString() { return 'bar' }}) // returns 'bar'
|
304 | * getAttributeValue({ toString: 'bar' }) // returns '{"toString":"bar"}'
|
305 | * @private
|
306 | */
|
307 |
|
308 |
|
309 | function getAttributeValue(value) {
|
310 | return value === null ? null : value === undefined ? '' : value === Object(value) ? value.toString === Object.prototype.toString ? JSON.stringify(value) : String(value) : String(value);
|
311 | }
|
312 | /**
|
313 | * Return a string formatted using camelCasing.
|
314 | * @param {String} value - The value being formatted.
|
315 | * @example
|
316 | * toCamelCaseString('hello-world') // returns 'helloWorld'
|
317 | * @private
|
318 | */
|
319 |
|
320 |
|
321 | function toCamelCaseString(value) {
|
322 | return isKebabCase(value) ? String(value).replace(/-[a-z]/g, $0 => $0.slice(1).toUpperCase()) : String(value);
|
323 | }
|
324 | /**
|
325 | * Return a string formatted using kebab-casing.
|
326 | * @param {String} value - The value being formatted.
|
327 | * @description Expected values do not already contain dashes.
|
328 | * @example <caption>Expected values.</caption>
|
329 | * toKebabCaseString('helloWorld') // returns 'hello-world'
|
330 | * toKebabCaseString('helloworld') // returns 'helloworld'
|
331 | * @example <caption>Unexpected values.</caption>
|
332 | * toKebabCaseString('hello-World') // returns 'hello-World'
|
333 | * @private
|
334 | */
|
335 |
|
336 |
|
337 | function toKebabCaseString(value) {
|
338 | return isCamelCase(value) ? String(value).replace(/[A-Z]/g, $0 => `-${$0.toLowerCase()}`) : String(value);
|
339 | }
|
340 | /**
|
341 | * Return whether a value is formatted camelCase.
|
342 | * @example
|
343 | * isCamelCase('helloWorld') // returns true
|
344 | * isCamelCase('hello-world') // returns false
|
345 | * isCamelCase('helloworld') // returns false
|
346 | * @private
|
347 | */
|
348 |
|
349 |
|
350 | function isCamelCase(value) {
|
351 | return /^\w+[A-Z]\w*$/.test(value);
|
352 | }
|
353 | /**
|
354 | * Return whether a value is formatted kebab-case.
|
355 | * @example
|
356 | * isKebabCase('hello-world') // returns true
|
357 | * isKebabCase('helloworld') // returns false
|
358 | * isKebabCase('helloWorld') // returns false
|
359 | * @private
|
360 | */
|
361 |
|
362 |
|
363 | function isKebabCase(value) {
|
364 | return /^\w+[-]\w+$/.test(value);
|
365 | }
|
366 | /**
|
367 | * Return whether a value is a Regular Expression.
|
368 | * @example
|
369 | * isRegExp(/hello-world/) // returns true
|
370 | * isRegExp('/hello-world/') // returns false
|
371 | * isRegExp(new RegExp('hello-world')) // returns true
|
372 | * @private
|
373 | */
|
374 |
|
375 |
|
376 | function isRegExp(value) {
|
377 | return Object.prototype.toString.call(value) === '[object RegExp]';
|
378 | }
|
379 |
|
380 | /**
|
381 | * Transform a {@link Node} and any descendants using visitors.
|
382 | * @param {Node} node - The {@link Node} to be visited.
|
383 | * @param {Result} result - The {@link Result} to be used by visitors.
|
384 | * @param {Object} [overrideVisitors] - Alternative visitors to be used in place of {@link Result} visitors.
|
385 | * @returns {ResultPromise}
|
386 | * @private
|
387 | */
|
388 | function visit(node, result, overrideVisitors) {
|
389 | // get visitors as an object
|
390 | const visitors = Object(overrideVisitors || Object(result).visitors); // get node types
|
391 |
|
392 | const beforeType = getTypeFromNode(node);
|
393 | const beforeSubType = getSubTypeFromNode(node);
|
394 | const beforeNodeType = 'Node';
|
395 | const beforeRootType = 'Root';
|
396 | const afterType = `after${beforeType}`;
|
397 | const afterSubType = `after${beforeSubType}`;
|
398 | const afterNodeType = 'afterNode';
|
399 | const afterRootType = 'afterRoot';
|
400 | let promise = Promise.resolve(); // fire "before" visitors
|
401 |
|
402 | if (visitors[beforeNodeType]) {
|
403 | promise = promise.then(() => runAll(visitors[beforeNodeType], node, result));
|
404 | }
|
405 |
|
406 | if (visitors[beforeType]) {
|
407 | promise = promise.then(() => runAll(visitors[beforeType], node, result));
|
408 | }
|
409 |
|
410 | if (beforeSubType !== beforeType && visitors[beforeSubType]) {
|
411 | promise = promise.then(() => runAll(visitors[beforeSubType], node, result));
|
412 | } // dispatch before root event
|
413 |
|
414 |
|
415 | if (visitors[beforeRootType] && node === result.root) {
|
416 | promise = promise.then(() => runAll(visitors[beforeRootType], node, result));
|
417 | } // walk children
|
418 |
|
419 |
|
420 | if (Array.isArray(node.nodes)) {
|
421 | node.nodes.slice(0).forEach(childNode => {
|
422 | promise = promise.then(() => childNode.parent === node && visit(childNode, result, overrideVisitors));
|
423 | });
|
424 | } // fire "after" visitors
|
425 |
|
426 |
|
427 | if (visitors[afterNodeType]) {
|
428 | promise = promise.then(() => runAll(visitors[afterNodeType], node, result));
|
429 | }
|
430 |
|
431 | if (visitors[afterType]) {
|
432 | promise = promise.then(() => runAll(visitors[afterType], node, result));
|
433 | }
|
434 |
|
435 | if (afterType !== afterSubType && visitors[afterSubType]) {
|
436 | promise = promise.then(() => runAll(visitors[afterSubType], node, result));
|
437 | } // dispatch root event
|
438 |
|
439 |
|
440 | if (visitors[afterRootType] && node === result.root) {
|
441 | promise = promise.then(() => runAll(visitors[afterRootType], node, result));
|
442 | }
|
443 |
|
444 | return promise.then(() => result);
|
445 | }
|
446 |
|
447 | function runAll(plugins, node, result) {
|
448 | let promise = Promise.resolve();
|
449 | [].concat(plugins || []).forEach(plugin => {
|
450 | // run the current plugin
|
451 | promise = promise.then(() => {
|
452 | // update the current plugin
|
453 | result.currentPlugin = plugin;
|
454 | return plugin(node, result);
|
455 | }).then(() => {
|
456 | // clear the current plugin
|
457 | result.currentPlugin = null;
|
458 | });
|
459 | });
|
460 | return promise;
|
461 | } // return normalized plugins and visitors
|
462 |
|
463 | function getVisitors(rawplugins) {
|
464 | const visitors = {}; // initialize plugins and visitors
|
465 |
|
466 | [].concat(rawplugins || []).forEach(plugin => {
|
467 | const initializedPlugin = Object(plugin).type === 'plugin' ? plugin() : plugin;
|
468 |
|
469 | if (initializedPlugin instanceof Function) {
|
470 | if (!visitors.afterRoot) {
|
471 | visitors.afterRoot = [];
|
472 | }
|
473 |
|
474 | visitors.afterRoot.push(initializedPlugin);
|
475 | } else if (Object(initializedPlugin) === initializedPlugin && Object.keys(initializedPlugin).length) {
|
476 | Object.keys(initializedPlugin).forEach(key => {
|
477 | const fn = initializedPlugin[key];
|
478 |
|
479 | if (fn instanceof Function) {
|
480 | if (!visitors[key]) {
|
481 | visitors[key] = [];
|
482 | }
|
483 |
|
484 | visitors[key].push(initializedPlugin[key]);
|
485 | }
|
486 | });
|
487 | }
|
488 | });
|
489 | return visitors;
|
490 | }
|
491 |
|
492 | function getTypeFromNode(node) {
|
493 | return {
|
494 | 'comment': 'Comment',
|
495 | 'text': 'Text',
|
496 | 'doctype': 'Doctype',
|
497 | 'fragment': 'Fragment'
|
498 | }[node.type] || 'Element';
|
499 | }
|
500 |
|
501 | function getSubTypeFromNode(node) {
|
502 | return {
|
503 | 'comment': 'Comment',
|
504 | 'text': 'Text',
|
505 | 'doctype': 'Doctype',
|
506 | 'fragment': 'Fragment'
|
507 | }[node.type] || (!node.name ? 'FragmentElement' : `${node.name[0].toUpperCase()}${node.name.slice(1)}Element`);
|
508 | }
|
509 |
|
510 | /**
|
511 | * @name Node
|
512 | * @class
|
513 | * @extends Node
|
514 | * @classdesc Create a new {@link Node}.
|
515 | * @returns {Node}
|
516 | */
|
517 |
|
518 | class Node {
|
519 | /**
|
520 | * The position of the current {@link Node} from its parent.
|
521 | * @returns {Number}
|
522 | * @example
|
523 | * node.index // returns the index of the node or -1
|
524 | */
|
525 | get index() {
|
526 | if (this.parent === Object(this.parent) && this.parent.nodes && this.parent.nodes.length) {
|
527 | return this.parent.nodes.indexOf(this);
|
528 | }
|
529 |
|
530 | return -1;
|
531 | }
|
532 | /**
|
533 | * The next {@link Node} after the current {@link Node}, or `null` if there is none.
|
534 | * @returns {Node|Null} - The next Node or null.
|
535 | * @example
|
536 | * node.next // returns null
|
537 | */
|
538 |
|
539 |
|
540 | get next() {
|
541 | const index = this.index;
|
542 |
|
543 | if (index !== -1) {
|
544 | return this.parent.nodes[index + 1] || null;
|
545 | }
|
546 |
|
547 | return null;
|
548 | }
|
549 | /**
|
550 | * The next {@link Element} after the current {@link Node}, or `null` if there is none.
|
551 | * @returns {Element|Null}
|
552 | * @example
|
553 | * node.nextElement // returns an element or null
|
554 | */
|
555 |
|
556 |
|
557 | get nextElement() {
|
558 | const index = this.index;
|
559 |
|
560 | if (index !== -1) {
|
561 | return this.parent.nodes.slice(index).find(hasNodes);
|
562 | }
|
563 |
|
564 | return null;
|
565 | }
|
566 | /**
|
567 | * The previous {@link Node} before the current {@link Node}, or `null` if there is none.
|
568 | * @returns {Node|Null}
|
569 | * @example
|
570 | * node.previous // returns a node or null
|
571 | */
|
572 |
|
573 |
|
574 | get previous() {
|
575 | const index = this.index;
|
576 |
|
577 | if (index !== -1) {
|
578 | return this.parent.nodes[index - 1] || null;
|
579 | }
|
580 |
|
581 | return null;
|
582 | }
|
583 | /**
|
584 | * The previous {@link Element} before the current {@link Node}, or `null` if there is none.
|
585 | * @returns {Element|Null}
|
586 | * @example
|
587 | * node.previousElement // returns an element or null
|
588 | */
|
589 |
|
590 |
|
591 | get previousElement() {
|
592 | const index = this.index;
|
593 |
|
594 | if (index !== -1) {
|
595 | return this.parent.nodes.slice(0, index).reverse().find(hasNodes);
|
596 | }
|
597 |
|
598 | return null;
|
599 | }
|
600 | /**
|
601 | * The top-most ancestor from the current {@link Node}.
|
602 | * @returns {Node}
|
603 | * @example
|
604 | * node.root // returns the top-most node or the current node itself
|
605 | */
|
606 |
|
607 |
|
608 | get root() {
|
609 | let parent = this;
|
610 |
|
611 | while (parent.parent) {
|
612 | parent = parent.parent;
|
613 | }
|
614 |
|
615 | return parent;
|
616 | }
|
617 | /**
|
618 | * Insert one ore more {@link Node}s after the current {@link Node}.
|
619 | * @param {...Node|String} nodes - Any nodes to be inserted after the current {@link Node}.
|
620 | * @returns {Node} - The current {@link Node}.
|
621 | * @example
|
622 | * node.after(new Text({ data: 'Hello World' }))
|
623 | */
|
624 |
|
625 |
|
626 | after(...nodes) {
|
627 | if (nodes.length) {
|
628 | const index = this.index;
|
629 |
|
630 | if (index !== -1) {
|
631 | this.parent.nodes.splice(index + 1, 0, ...nodes);
|
632 | }
|
633 | }
|
634 |
|
635 | return this;
|
636 | }
|
637 | /**
|
638 | * Append Nodes or new Text Nodes to the current {@link Node}.
|
639 | * @param {...Node|String} nodes - Any nodes to be inserted after the last child of the current {@link Node}.
|
640 | * @returns {Node} - The current {@link Node}.
|
641 | * @example
|
642 | * node.append(someOtherNode)
|
643 | */
|
644 |
|
645 |
|
646 | append(...nodes) {
|
647 | if (this.nodes) {
|
648 | this.nodes.splice(this.nodes.length, 0, ...nodes);
|
649 | }
|
650 |
|
651 | return this;
|
652 | }
|
653 | /**
|
654 | * Append the current {@link Node} to another Node.
|
655 | * @param {Container} parent - The {@link Container} for the current {@link Node}.
|
656 | * @returns {Node} - The current {@link Node}.
|
657 | */
|
658 |
|
659 |
|
660 | appendTo(parent) {
|
661 | if (parent && parent.nodes) {
|
662 | parent.nodes.splice(parent.nodes.length, 0, this);
|
663 | }
|
664 |
|
665 | return this;
|
666 | }
|
667 | /**
|
668 | * Insert Nodes or new Text Nodes before the Node if it has a parent.
|
669 | * @param {...Node|String} nodes - Any nodes to be inserted before the current {@link Node}.
|
670 | * @returns {Node}
|
671 | * @example
|
672 | * node.before(new Text({ data: 'Hello World' })) // returns the current node
|
673 | */
|
674 |
|
675 |
|
676 | before(...nodes) {
|
677 | if (nodes.length) {
|
678 | const index = this.index;
|
679 |
|
680 | if (index !== -1) {
|
681 | this.parent.nodes.splice(index, 0, ...nodes);
|
682 | }
|
683 | }
|
684 |
|
685 | return this;
|
686 | }
|
687 | /**
|
688 | * Prepend Nodes or new Text Nodes to the current {@link Node}.
|
689 | * @param {...Node|String} nodes - Any nodes inserted before the first child of the current {@link Node}.
|
690 | * @returns {Node} - The current {@link Node}.
|
691 | * @example
|
692 | * node.prepend(someOtherNode)
|
693 | */
|
694 |
|
695 |
|
696 | prepend(...nodes) {
|
697 | if (this.nodes) {
|
698 | this.nodes.splice(0, 0, ...nodes);
|
699 | }
|
700 |
|
701 | return this;
|
702 | }
|
703 | /**
|
704 | * Remove the current {@link Node} from its parent.
|
705 | * @returns {Node}
|
706 | * @example
|
707 | * node.remove() // returns the current node
|
708 | */
|
709 |
|
710 |
|
711 | remove() {
|
712 | const index = this.index;
|
713 |
|
714 | if (index !== -1) {
|
715 | this.parent.nodes.splice(index, 1);
|
716 | }
|
717 |
|
718 | return this;
|
719 | }
|
720 | /**
|
721 | * Replace the current {@link Node} with another Node or Nodes.
|
722 | * @param {...Node} nodes - Any nodes replacing the current {@link Node}.
|
723 | * @returns {Node} - The current {@link Node}
|
724 | * @example
|
725 | * node.replaceWith(someOtherNode) // returns the current node
|
726 | */
|
727 |
|
728 |
|
729 | replaceWith(...nodes) {
|
730 | const index = this.index;
|
731 |
|
732 | if (index !== -1) {
|
733 | this.parent.nodes.splice(index, 1, ...nodes);
|
734 | }
|
735 |
|
736 | return this;
|
737 | }
|
738 | /**
|
739 | * Transform the current {@link Node} and any descendants using visitors.
|
740 | * @param {Result} result - The {@link Result} to be used by visitors.
|
741 | * @param {Object} [overrideVisitors] - Alternative visitors to be used in place of {@link Result} visitors.
|
742 | * @returns {ResultPromise}
|
743 | * @example
|
744 | * await node.visit(result)
|
745 | * @example
|
746 | * await node.visit() // visit using the result of the current node
|
747 | * @example
|
748 | * await node.visit(result, {
|
749 | * Element () {
|
750 | * // do something to an element
|
751 | * }
|
752 | * })
|
753 | */
|
754 |
|
755 |
|
756 | visit(result, overrideVisitors) {
|
757 | const resultToUse = 0 in arguments ? result : this.result;
|
758 | return visit(this, resultToUse, overrideVisitors);
|
759 | }
|
760 | /**
|
761 | * Add a warning from the current {@link Node}.
|
762 | * @param {Result} result - The {@link Result} the warning is being added to.
|
763 | * @param {String} text - The message being sent as the warning.
|
764 | * @param {Object} [opts] - Additional information about the warning.
|
765 | * @example
|
766 | * node.warn(result, 'Something went wrong')
|
767 | * @example
|
768 | * node.warn(result, 'Something went wrong', {
|
769 | * node: someOtherNode,
|
770 | * plugin: someOtherPlugin
|
771 | * })
|
772 | */
|
773 |
|
774 |
|
775 | warn(result, text, opts) {
|
776 | const data = Object.assign({
|
777 | node: this
|
778 | }, opts);
|
779 | return result.warn(text, data);
|
780 | }
|
781 |
|
782 | }
|
783 |
|
784 | function hasNodes(node) {
|
785 | return node.nodes;
|
786 | }
|
787 |
|
788 | /**
|
789 | * @name Comment
|
790 | * @class
|
791 | * @extends Node
|
792 | * @classdesc Return a new {@link Comment} {@link Node}.
|
793 | * @param {Object|String} settings - Custom settings applied to the Comment, or the content of the {@link Comment}.
|
794 | * @param {String} settings.comment - Content of the Comment.
|
795 | * @param {Object} settings.source - Source mapping of the Comment.
|
796 | * @returns {Comment}
|
797 | * @example
|
798 | * new Comment({ comment: ' Hello World ' })
|
799 | */
|
800 |
|
801 | class Comment extends Node {
|
802 | constructor(settings) {
|
803 | super();
|
804 |
|
805 | if (typeof settings === 'string') {
|
806 | settings = {
|
807 | comment: settings
|
808 | };
|
809 | }
|
810 |
|
811 | Object.assign(this, {
|
812 | type: 'comment',
|
813 | name: '#comment',
|
814 | comment: String(Object(settings).comment || ''),
|
815 | source: Object(Object(settings).source)
|
816 | });
|
817 | }
|
818 | /**
|
819 | * Return the stringified innerHTML of the current {@link Comment}.
|
820 | * @returns {String}
|
821 | * @example
|
822 | * attrs.innerHTML // returns ' Hello World '
|
823 | */
|
824 |
|
825 |
|
826 | get innerHTML() {
|
827 | return String(this.comment);
|
828 | }
|
829 | /**
|
830 | * Return the stringified outerHTML of the current {@link Comment}.
|
831 | * @returns {String}
|
832 | * @example
|
833 | * attrs.outerHTML // returns '<!-- Hello World -->'
|
834 | */
|
835 |
|
836 |
|
837 | get outerHTML() {
|
838 | return String(this);
|
839 | }
|
840 | /**
|
841 | * Return the stringified innerHTML from the source input.
|
842 | * @returns {String}
|
843 | * @example
|
844 | * attrs.sourceInnerHTML // returns ' Hello World '
|
845 | */
|
846 |
|
847 |
|
848 | get sourceInnerHTML() {
|
849 | return typeof Object(this.source.input).html !== 'string' ? '' : this.source.input.html.slice(this.source.startOffset + 4, this.source.endOffset - 3);
|
850 | }
|
851 | /**
|
852 | * Return the stringified outerHTML from the source input.
|
853 | * @returns {String}
|
854 | * @example
|
855 | * attrs.sourceOuterHTML // returns '<!-- Hello World -->'
|
856 | */
|
857 |
|
858 |
|
859 | get sourceOuterHTML() {
|
860 | return typeof Object(this.source.input).html !== 'string' ? '' : this.source.input.html.slice(this.source.startOffset, this.source.endOffset);
|
861 | }
|
862 | /**
|
863 | * Return a clone of the current {@link Comment}.
|
864 | * @param {Object} settings - Custom settings applied to the cloned {@link Comment}.
|
865 | * @returns {Comment} - The cloned {@link Comment}
|
866 | * @example
|
867 | * comment.clone()
|
868 | * @example <caption>Clone the current text with new source.</caption>
|
869 | * comment.clone({ source: { input: 'modified source' } })
|
870 | */
|
871 |
|
872 |
|
873 | clone(settings) {
|
874 | return new this.constructor(Object.assign({}, this, settings, {
|
875 | source: Object.assign({}, this.source, Object(settings).source)
|
876 | }));
|
877 | }
|
878 | /**
|
879 | * Return the current {@link Comment} as a String.
|
880 | * @returns {String} A string version of the current {@link Comment}
|
881 | * @example
|
882 | * attrs.toJSON() // returns '<!-- Hello World -->'
|
883 | */
|
884 |
|
885 |
|
886 | toString() {
|
887 | return `<!--${this.comment}-->`;
|
888 | }
|
889 | /**
|
890 | * Return the current {@link Comment} as a Object.
|
891 | * @returns {Object} An object version of the current {@link Comment}
|
892 | * @example
|
893 | * attrs.toJSON() // returns { comment: ' Hello World ' }
|
894 | */
|
895 |
|
896 |
|
897 | toJSON() {
|
898 | return {
|
899 | comment: this.comment
|
900 | };
|
901 | }
|
902 |
|
903 | }
|
904 |
|
905 | /**
|
906 | * @name Container
|
907 | * @class
|
908 | * @extends Node
|
909 | * @classdesc Return a new {@link Container} {@link Node}.
|
910 | * @returns {Container}
|
911 | */
|
912 |
|
913 | class Container extends Node {
|
914 | /**
|
915 | * Return the first child {@link Node} of the current {@link Container}, or `null` if there is none.
|
916 | * @returns {Node|Null}
|
917 | * @example
|
918 | * container.first // returns a Node or null
|
919 | */
|
920 | get first() {
|
921 | return this.nodes[0] || null;
|
922 | }
|
923 | /**
|
924 | * Return the first child {@link Element} of the current {@link Container}, or `null` if there is none.
|
925 | * @returns {Node|Null}
|
926 | * @example
|
927 | * container.firstElement // returns an Element or null
|
928 | */
|
929 |
|
930 |
|
931 | get firstElement() {
|
932 | return this.nodes.find(hasNodes$1) || null;
|
933 | }
|
934 | /**
|
935 | * Return the last child {@link Node} of the current {@link Container}, or `null` if there is none.
|
936 | * @returns {Node|Null}
|
937 | * @example
|
938 | * container.last // returns a Node or null
|
939 | */
|
940 |
|
941 |
|
942 | get last() {
|
943 | return this.nodes[this.nodes.length - 1] || null;
|
944 | }
|
945 | /**
|
946 | * Return the last child {@link Element} of the current {@link Container}, or `null` if there is none.
|
947 | * @returns {Node|Null}
|
948 | * @example
|
949 | * container.lastElement // returns an Element or null
|
950 | */
|
951 |
|
952 |
|
953 | get lastElement() {
|
954 | return this.nodes.slice().reverse().find(hasNodes$1) || null;
|
955 | }
|
956 | /**
|
957 | * Return a child {@link Element} {@link NodeList} of the current {@link Container}.
|
958 | * @returns {Array}
|
959 | * @example
|
960 | * container.elements // returns an array of Elements
|
961 | */
|
962 |
|
963 |
|
964 | get elements() {
|
965 | return this.nodes.filter(hasNodes$1) || [];
|
966 | }
|
967 | /**
|
968 | * Return the innerHTML of the current {@link Container} as a String.
|
969 | * @returns {String}
|
970 | * @example
|
971 | * container.innerHTML // returns a string of innerHTML
|
972 | */
|
973 |
|
974 |
|
975 | get innerHTML() {
|
976 | return this.nodes.innerHTML;
|
977 | }
|
978 | /**
|
979 | * Define the nodes of the current {@link Container} from a String.
|
980 | * @param {String} innerHTML - Source being processed.
|
981 | * @returns {Void}
|
982 | * @example
|
983 | * container.innerHTML = 'Hello <strong>world</strong>';
|
984 | * container.nodes.length; // 2
|
985 | */
|
986 |
|
987 |
|
988 | set innerHTML(innerHTML) {
|
989 | this.nodes.innerHTML = innerHTML;
|
990 | }
|
991 | /**
|
992 | * Return the outerHTML of the current {@link Container} as a String.
|
993 | * @returns {String}
|
994 | * @example
|
995 | * container.outerHTML // returns a string of outerHTML
|
996 | */
|
997 |
|
998 |
|
999 | get outerHTML() {
|
1000 | return this.nodes.innerHTML;
|
1001 | }
|
1002 | /**
|
1003 | * Replace the current {@link Container} from a String.
|
1004 | * @param {String} input - Source being processed.
|
1005 | * @returns {Void}
|
1006 | * @example
|
1007 | * container.outerHTML = 'Hello <strong>world</strong>';
|
1008 | */
|
1009 |
|
1010 |
|
1011 | set outerHTML(outerHTML) {
|
1012 | const Result = Object(this.result).constructor;
|
1013 |
|
1014 | if (Result) {
|
1015 | const childNodes = new Result(outerHTML).root.nodes;
|
1016 | this.replaceWith(...childNodes);
|
1017 | }
|
1018 | }
|
1019 | /**
|
1020 | * Return the stringified innerHTML from the source input.
|
1021 | * @returns {String}
|
1022 | */
|
1023 |
|
1024 |
|
1025 | get sourceInnerHTML() {
|
1026 | return this.isSelfClosing || this.isVoid || typeof Object(this.source.input).html !== 'string' ? '' : 'startInnerOffset' in this.source && 'endInnerOffset' in this.source ? this.source.input.html.slice(this.source.startInnerOffset, this.source.endInnerOffset) : this.sourceOuterHTML;
|
1027 | }
|
1028 | /**
|
1029 | * Return the stringified outerHTML from the source input.
|
1030 | * @returns {String}
|
1031 | */
|
1032 |
|
1033 |
|
1034 | get sourceOuterHTML() {
|
1035 | return typeof Object(this.source.input).html !== 'string' ? '' : this.source.input.html.slice(this.source.startOffset, this.source.endOffset);
|
1036 | }
|
1037 | /**
|
1038 | * Return the text content of the current {@link Container} as a String.
|
1039 | * @returns {String}
|
1040 | */
|
1041 |
|
1042 |
|
1043 | get textContent() {
|
1044 | return this.nodes.textContent;
|
1045 | }
|
1046 | /**
|
1047 | * Define the content of the current {@link Container} as a new {@link Text} {@link Node}.
|
1048 | * @returns {String}
|
1049 | */
|
1050 |
|
1051 |
|
1052 | set textContent(textContent) {
|
1053 | this.nodes.textContent = textContent;
|
1054 | }
|
1055 | /**
|
1056 | * Return a child {@link Node} of the current {@link Container} by last index, or `null` if there is none.
|
1057 | * @returns {Node|Null}
|
1058 | * @example
|
1059 | * container.lastNth(0) // returns a Node or null
|
1060 | */
|
1061 |
|
1062 |
|
1063 | lastNth(index) {
|
1064 | return this.nodes.slice().reverse()[index] || null;
|
1065 | }
|
1066 | /**
|
1067 | * Return a child {@link Element} of the current {@link Container} by last index, or `null` if there is none.
|
1068 | * @returns {Element|Null}
|
1069 | * @example
|
1070 | * container.lastNthElement(0) // returns an Element or null
|
1071 | */
|
1072 |
|
1073 |
|
1074 | lastNthElement(index) {
|
1075 | return this.elements.reverse()[index] || null;
|
1076 | }
|
1077 | /**
|
1078 | * Return a child {@link Node} of the current {@link Container} by index, or `null` if there is none.
|
1079 | * @returns {Node|Null}
|
1080 | * @example
|
1081 | * container.nth(0) // returns a Node or null
|
1082 | */
|
1083 |
|
1084 |
|
1085 | nth(index) {
|
1086 | return this.nodes[index] || null;
|
1087 | }
|
1088 | /**
|
1089 | * Return an {@link Element} child of the current Container by index, or `null` if there is none.
|
1090 | * @returns {Element|Null}
|
1091 | * @example
|
1092 | * container.nthElement(0) // returns an Element or null
|
1093 | */
|
1094 |
|
1095 |
|
1096 | nthElement(index) {
|
1097 | return this.elements[index] || null;
|
1098 | }
|
1099 | /**
|
1100 | * Replace all of the children of the current {@link Container}.
|
1101 | * @param {...Node} nodes - Any nodes replacing the current children of the {@link Container}.
|
1102 | * @returns {Container} - The current {@link Container}.
|
1103 | * @example
|
1104 | * container.replaceAll(new Text({ data: 'Hello World' }))
|
1105 | */
|
1106 |
|
1107 |
|
1108 | replaceAll(...nodes) {
|
1109 | if (this.nodes) {
|
1110 | this.nodes.splice(0, this.nodes.length, ...nodes);
|
1111 | }
|
1112 |
|
1113 | return this;
|
1114 | }
|
1115 | /**
|
1116 | * Traverse the descendant {@link Node}s of the current {@link Container} with a callback function.
|
1117 | * @param {Function|String|RegExp} callback_or_filter - A callback function, or a filter to reduce {@link Node}s the callback is applied to.
|
1118 | * @param {Function|String|RegExp} callback - A callback function when a filter is also specified.
|
1119 | * @returns {Container} - The current {@link Container}.
|
1120 | * @example
|
1121 | * container.walk(node => {
|
1122 | * console.log(node);
|
1123 | * })
|
1124 | * @example
|
1125 | * container.walk('*', node => {
|
1126 | * console.log(node);
|
1127 | * })
|
1128 | * @example <caption>Walk only "section" {@link Element}s.</caption>
|
1129 | * container.walk('section', node => {
|
1130 | * console.log(node); // logs only Section Elements
|
1131 | * })
|
1132 | * @example
|
1133 | * container.walk(/^section$/, node => {
|
1134 | * console.log(node); // logs only Section Elements
|
1135 | * })
|
1136 | * @example
|
1137 | * container.walk(
|
1138 | * node => node.name.toLowerCase() === 'section',
|
1139 | * node => {
|
1140 | * console.log(node); // logs only Section Elements
|
1141 | * })
|
1142 | * @example <caption>Walk only {@link Text}.</caption>
|
1143 | * container.walk('#text', node => {
|
1144 | * console.log(node); // logs only Text Nodes
|
1145 | * })
|
1146 | */
|
1147 |
|
1148 |
|
1149 | walk() {
|
1150 | const [cb, filter] = getCbAndFilterFromArgs(arguments);
|
1151 | walk(this, cb, filter);
|
1152 | return this;
|
1153 | }
|
1154 |
|
1155 | }
|
1156 |
|
1157 | function walk(node, cb, filter) {
|
1158 | if (typeof cb === 'function' && node.nodes) {
|
1159 | node.nodes.slice(0).forEach(child => {
|
1160 | if (Object(child).parent === node) {
|
1161 | if (testWithFilter(child, filter)) {
|
1162 | cb(child); // eslint-disable-line callback-return
|
1163 | }
|
1164 |
|
1165 | if (child.nodes) {
|
1166 | walk(child, cb, filter);
|
1167 | }
|
1168 | }
|
1169 | });
|
1170 | }
|
1171 | }
|
1172 |
|
1173 | function getCbAndFilterFromArgs(args) {
|
1174 | const [cbOrFilter, onlyCb] = args;
|
1175 | const cb = onlyCb || cbOrFilter;
|
1176 | const filter = onlyCb ? cbOrFilter : undefined;
|
1177 | return [cb, filter];
|
1178 | }
|
1179 |
|
1180 | function testWithFilter(node, filter) {
|
1181 | if (!filter) {
|
1182 | return true;
|
1183 | } else if (filter === '*') {
|
1184 | return Object(node).constructor.name === 'Element';
|
1185 | } else if (typeof filter === 'string') {
|
1186 | return node.name === filter;
|
1187 | } else if (filter instanceof RegExp) {
|
1188 | return filter.test(node.name);
|
1189 | } else if (filter instanceof Function) {
|
1190 | return filter(node);
|
1191 | } else {
|
1192 | return false;
|
1193 | }
|
1194 | }
|
1195 |
|
1196 | function hasNodes$1(node) {
|
1197 | return node.nodes;
|
1198 | }
|
1199 |
|
1200 | /**
|
1201 | * @name Doctype
|
1202 | * @class
|
1203 | * @extends Node
|
1204 | * @classdesc Create a new {@link Doctype} {@link Node}.
|
1205 | * @param {Object|String} settings - Custom settings applied to the {@link Doctype}, or the name of the {@link Doctype}.
|
1206 | * @param {String} settings.name - Name of the {@link Doctype}.
|
1207 | * @param {String} settings.publicId - Public identifier portion of the {@link Doctype}.
|
1208 | * @param {String} settings.systemId - System identifier portion of the {@link Doctype}.
|
1209 | * @param {Object} settings.source - Source mapping of the {@link Doctype}.
|
1210 | * @returns {Doctype}
|
1211 | * @example
|
1212 | * new Doctype({ name: 'html' }) // <!doctype html>
|
1213 | */
|
1214 |
|
1215 | class Doctype extends Node {
|
1216 | constructor(settings) {
|
1217 | super();
|
1218 |
|
1219 | if (typeof settings === 'string') {
|
1220 | settings = {
|
1221 | name: settings
|
1222 | };
|
1223 | }
|
1224 |
|
1225 | Object.assign(this, {
|
1226 | type: 'doctype',
|
1227 | doctype: String(Object(settings).doctype || 'doctype'),
|
1228 | name: String(Object(settings).name || 'html'),
|
1229 | publicId: Object(settings).publicId || null,
|
1230 | systemId: Object(settings).systemId || null,
|
1231 | source: Object.assign({
|
1232 | before: Object(Object(settings).source).before || ' ',
|
1233 | after: Object(Object(settings).source).after || '',
|
1234 | beforePublicId: Object(Object(settings).source).beforePublicId || null,
|
1235 | beforeSystemId: Object(Object(settings).source).beforeSystemId || null
|
1236 | }, Object(settings).source)
|
1237 | });
|
1238 | }
|
1239 | /**
|
1240 | * Return a clone of the current {@link Doctype}.
|
1241 | * @param {Object} settings - Custom settings applied to the cloned {@link Doctype}.
|
1242 | * @returns {Doctype} - The cloned {@link Doctype}
|
1243 | * @example
|
1244 | * doctype.clone()
|
1245 | * @example <caption>Clone the current text with new source.</caption>
|
1246 | * doctype.clone({ source: { input: 'modified source' } })
|
1247 | */
|
1248 |
|
1249 |
|
1250 | clone(settings) {
|
1251 | return new this.constructor(Object.assign({}, this, settings, {
|
1252 | source: Object.assign({}, this.source, Object(settings).source)
|
1253 | }));
|
1254 | }
|
1255 | /**
|
1256 | * Return the current {@link Doctype} as a String.
|
1257 | * @returns {String}
|
1258 | */
|
1259 |
|
1260 |
|
1261 | toString() {
|
1262 | const publicId = this.publicId ? `${this.source.beforePublicId || ' '}${this.publicId}` : '';
|
1263 | const systemId = this.systemId ? `${this.source.beforeSystemId || ' '}${this.systemId}` : '';
|
1264 | return `<!${this.doctype}${this.source.before}${this.name}${this.source.after}${publicId}${systemId}>`;
|
1265 | }
|
1266 | /**
|
1267 | * Return the current {@link Doctype} as an Object.
|
1268 | * @returns {Object}
|
1269 | */
|
1270 |
|
1271 |
|
1272 | toJSON() {
|
1273 | return {
|
1274 | name: this.name,
|
1275 | publicId: this.publicId,
|
1276 | systemId: this.systemId
|
1277 | };
|
1278 | }
|
1279 |
|
1280 | }
|
1281 |
|
1282 | /**
|
1283 | * @name Fragment
|
1284 | * @class
|
1285 | * @extends Container
|
1286 | * @classdesc Create a new {@link Fragment} {@Link Node}.
|
1287 | * @param {Object} settings - Custom settings applied to the {@link Fragment}.
|
1288 | * @param {Array|NodeList} settings.nodes - Nodes appended to the {@link Fragment}.
|
1289 | * @param {Object} settings.source - Source mapping of the {@link Fragment}.
|
1290 | * @returns {Fragment}
|
1291 | * @example
|
1292 | * new Fragment() // returns an empty fragment
|
1293 | *
|
1294 | * new Fragment({ nodes: [ new Element('span') ] }) // returns a fragment with a <span>
|
1295 | */
|
1296 |
|
1297 | class Fragment extends Container {
|
1298 | constructor(settings) {
|
1299 | super();
|
1300 | Object.assign(this, settings, {
|
1301 | type: 'fragment',
|
1302 | name: '#document-fragment',
|
1303 | nodes: Array.isArray(Object(settings).nodes) ? new NodeList(this, ...Array.from(settings.nodes)) : Object(settings).nodes !== null && Object(settings).nodes !== undefined ? new NodeList(this, settings.nodes) : new NodeList(this),
|
1304 | source: Object(Object(settings).source)
|
1305 | });
|
1306 | }
|
1307 | /**
|
1308 | * Return a clone of the current {@link Fragment}.
|
1309 | * @param {Boolean} isDeep - Whether the descendants of the current Fragment should also be cloned.
|
1310 | * @returns {Fragment} - The cloned Fragment
|
1311 | */
|
1312 |
|
1313 |
|
1314 | clone(isDeep) {
|
1315 | const clone = new Fragment(Object.assign({}, this, {
|
1316 | nodes: []
|
1317 | }));
|
1318 |
|
1319 | if (isDeep) {
|
1320 | clone.nodes = this.nodes.clone(clone);
|
1321 | }
|
1322 |
|
1323 | return clone;
|
1324 | }
|
1325 | /**
|
1326 | * Return the current {@link Fragment} as an Array.
|
1327 | * @returns {Array}
|
1328 | * @example
|
1329 | * fragment.toJSON() // returns []
|
1330 | */
|
1331 |
|
1332 |
|
1333 | toJSON() {
|
1334 | return this.nodes.toJSON();
|
1335 | }
|
1336 | /**
|
1337 | * Return the current {@link Fragment} as a String.
|
1338 | * @returns {String}
|
1339 | * @example
|
1340 | * fragment.toJSON() // returns ''
|
1341 | */
|
1342 |
|
1343 |
|
1344 | toString() {
|
1345 | return String(this.nodes);
|
1346 | }
|
1347 |
|
1348 | }
|
1349 |
|
1350 | /**
|
1351 | * @name Text
|
1352 | * @class
|
1353 | * @extends Node
|
1354 | * @classdesc Create a new {@link Text} {@link Node}.
|
1355 | * @param {Object|String} settings - Custom settings applied to the {@link Text}, or the content of the {@link Text}.
|
1356 | * @param {String} settings.data - Content of the {@link Text}.
|
1357 | * @param {Object} settings.source - Source mapping of the {@link Text}.
|
1358 | * @returns {Text}
|
1359 | * @example
|
1360 | * new Text({ data: 'Hello World' })
|
1361 | */
|
1362 |
|
1363 | class Text extends Node {
|
1364 | constructor(settings) {
|
1365 | super();
|
1366 |
|
1367 | if (typeof settings === 'string') {
|
1368 | settings = {
|
1369 | data: settings
|
1370 | };
|
1371 | }
|
1372 |
|
1373 | Object.assign(this, {
|
1374 | type: 'text',
|
1375 | name: '#text',
|
1376 | data: String(Object(settings).data || ''),
|
1377 | source: Object(Object(settings).source)
|
1378 | });
|
1379 | }
|
1380 | /**
|
1381 | * Return the stringified innerHTML from the source input.
|
1382 | * @returns {String}
|
1383 | */
|
1384 |
|
1385 |
|
1386 | get sourceInnerHTML() {
|
1387 | return typeof Object(this.source.input).html !== 'string' ? '' : this.source.input.html.slice(this.source.startOffset, this.source.endOffset);
|
1388 | }
|
1389 | /**
|
1390 | * Return the stringified outerHTML from the source input.
|
1391 | * @returns {String}
|
1392 | */
|
1393 |
|
1394 |
|
1395 | get sourceOuterHTML() {
|
1396 | return typeof Object(this.source.input).html !== 'string' ? '' : this.source.input.html.slice(this.source.startOffset, this.source.endOffset);
|
1397 | }
|
1398 | /**
|
1399 | * Return the current {@link Text} as a String.
|
1400 | * @returns {String}
|
1401 | * @example
|
1402 | * text.textContent // returns ''
|
1403 | */
|
1404 |
|
1405 |
|
1406 | get textContent() {
|
1407 | return String(this.data);
|
1408 | }
|
1409 | /**
|
1410 | * Define the current {@link Text} from a String.
|
1411 | * @returns {Void}
|
1412 | * @example
|
1413 | * text.textContent = 'Hello World'
|
1414 | * text.textContent // 'Hello World'
|
1415 | */
|
1416 |
|
1417 |
|
1418 | set textContent(textContent) {
|
1419 | this.data = String(textContent);
|
1420 | }
|
1421 | /**
|
1422 | * Return a clone of the current {@link Text}.
|
1423 | * @param {Object} settings - Custom settings applied to the cloned {@link Text}.
|
1424 | * @returns {Text} - The cloned {@link Text}
|
1425 | * @example
|
1426 | * text.clone()
|
1427 | * @example <caption>Clone the current text with new source.</caption>
|
1428 | * text.clone({ source: { input: 'modified source' } })
|
1429 | */
|
1430 |
|
1431 |
|
1432 | clone(settings) {
|
1433 | return new Text(Object.assign({}, this, settings, {
|
1434 | source: Object.assign({}, this.source, Object(settings).source)
|
1435 | }));
|
1436 | }
|
1437 | /**
|
1438 | * Return the current {@link Text} as a String.
|
1439 | * @returns {String}
|
1440 | * @example
|
1441 | * text.toString() // returns ''
|
1442 | */
|
1443 |
|
1444 |
|
1445 | toString() {
|
1446 | return String(this.data);
|
1447 | }
|
1448 | /**
|
1449 | * Return the current {@link Text} as a String.
|
1450 | * @returns {String}
|
1451 | * @example
|
1452 | * text.toJSON() // returns ''
|
1453 | */
|
1454 |
|
1455 |
|
1456 | toJSON() {
|
1457 | return String(this.data);
|
1458 | }
|
1459 |
|
1460 | }
|
1461 |
|
1462 | function normalize(node) {
|
1463 | const nodeTypes = {
|
1464 | comment: Comment,
|
1465 | doctype: Doctype,
|
1466 | element: Element,
|
1467 | fragment: Fragment,
|
1468 | text: Text
|
1469 | };
|
1470 | return node instanceof Node // Nodes are unchanged
|
1471 | ? node : node.type in nodeTypes // Strings are converted into Text nodes
|
1472 | ? new nodeTypes[node.type](node) // Node-like Objects with recognized types are normalized
|
1473 | : new Text({
|
1474 | data: String(node)
|
1475 | });
|
1476 | }
|
1477 |
|
1478 | const parents = new WeakMap();
|
1479 | /**
|
1480 | * @name NodeList
|
1481 | * @class
|
1482 | * @extends Array
|
1483 | * @classdesc Create a new {@link NodeList}.
|
1484 | * @param {Container} parent - Parent containing the current {@link NodeList}.
|
1485 | * @param {...Node} nodes - {@link Node}s belonging to the current {@link NodeList}.
|
1486 | * @returns {NodeList}
|
1487 | */
|
1488 |
|
1489 | class NodeList extends Array {
|
1490 | constructor(parent, ...nodes) {
|
1491 | super();
|
1492 | parents.set(this, parent);
|
1493 |
|
1494 | if (nodes.length) {
|
1495 | this.push(...nodes);
|
1496 | }
|
1497 | }
|
1498 | /**
|
1499 | * Return the innerHTML of the current {@link Container} as a String.
|
1500 | * @returns {String}
|
1501 | * @example
|
1502 | * container.innerHTML // returns a string of innerHTML
|
1503 | */
|
1504 |
|
1505 |
|
1506 | get innerHTML() {
|
1507 | return this.map(node => node.type === 'text' ? getInnerHtmlEncodedString(node.data) : 'outerHTML' in node ? node.outerHTML : String(node)).join('');
|
1508 | }
|
1509 | /**
|
1510 | * Define the nodes of the current {@link NodeList} from a String.
|
1511 | * @param {String} innerHTML - Source being processed.
|
1512 | * @returns {Void}
|
1513 | * @example
|
1514 | * nodeList.innerHTML = 'Hello <strong>world</strong>';
|
1515 | * nodeList.length; // 2
|
1516 | */
|
1517 |
|
1518 |
|
1519 | set innerHTML(innerHTML) {
|
1520 | const parent = this.parent;
|
1521 | const Result = Object(parent.result).constructor;
|
1522 |
|
1523 | if (Result) {
|
1524 | const nodes = new Result(innerHTML).root.nodes;
|
1525 | this.splice(0, this.length, ...nodes);
|
1526 | }
|
1527 | }
|
1528 | /**
|
1529 | * Return the parent of the current {@link NodeList}.
|
1530 | * @returns {Container}
|
1531 | */
|
1532 |
|
1533 |
|
1534 | get parent() {
|
1535 | return parents.get(this);
|
1536 | }
|
1537 | /**
|
1538 | * Return the text content of the current {@link NodeList} as a String.
|
1539 | * @returns {String}
|
1540 | */
|
1541 |
|
1542 |
|
1543 | get textContent() {
|
1544 | return this.map(node => Object(node).textContent || '').join('');
|
1545 | }
|
1546 | /**
|
1547 | * Define the content of the current {@link NodeList} as a new {@link Text} {@link Node}.
|
1548 | * @returns {String}
|
1549 | */
|
1550 |
|
1551 |
|
1552 | set textContent(textContent) {
|
1553 | this.splice(0, this.length, new Text({
|
1554 | data: textContent
|
1555 | }));
|
1556 | }
|
1557 | /**
|
1558 | * Return a clone of the current {@link NodeList}.
|
1559 | * @param {Object} parent - New parent containing the cloned {@link NodeList}.
|
1560 | * @returns {NodeList} - The cloned NodeList
|
1561 | */
|
1562 |
|
1563 |
|
1564 | clone(parent) {
|
1565 | return new NodeList(parent, ...this.map(node => node.clone({}, true)));
|
1566 | }
|
1567 | /**
|
1568 | * Remove and return the last {@link Node} in the {@link NodeList}.
|
1569 | * @returns {Node}
|
1570 | */
|
1571 |
|
1572 |
|
1573 | pop() {
|
1574 | const [remove] = this.splice(this.length - 1, 1);
|
1575 | return remove;
|
1576 | }
|
1577 | /**
|
1578 | * Add {@link Node}s to the end of the {@link NodeList} and return the new length of the {@link NodeList}.
|
1579 | * @returns {Number}
|
1580 | */
|
1581 |
|
1582 |
|
1583 | push(...nodes) {
|
1584 | const parent = this.parent;
|
1585 | const inserts = nodes.filter(node => node !== parent);
|
1586 | this.splice(this.length, 0, ...inserts);
|
1587 | return this.length;
|
1588 | }
|
1589 | /**
|
1590 | * Remove and return the first {@link Node} in the {@link NodeList}.
|
1591 | * @returns {Node}
|
1592 | */
|
1593 |
|
1594 |
|
1595 | shift() {
|
1596 | const [remove] = this.splice(0, 1);
|
1597 | return remove;
|
1598 | }
|
1599 | /**
|
1600 | * Add and remove {@link Node}s to and from the {@link NodeList}.
|
1601 | * @returns {Array}
|
1602 | */
|
1603 |
|
1604 |
|
1605 | splice(start, ...args) {
|
1606 | const {
|
1607 | length,
|
1608 | parent
|
1609 | } = this;
|
1610 | const startIndex = start > length ? length : start < 0 ? Math.max(length + start, 0) : Number(start) || 0;
|
1611 | const deleteCount = 0 in args ? Number(args[0]) || 0 : length;
|
1612 | const inserts = getNodeListArray(args.slice(1).filter(node => node !== parent));
|
1613 |
|
1614 | for (let _i = 0, _length = inserts == null ? 0 : inserts.length; _i < _length; _i++) {
|
1615 | let insert = inserts[_i];
|
1616 | insert.remove();
|
1617 | insert.parent = parent;
|
1618 | }
|
1619 |
|
1620 | const removes = Array.prototype.splice.call(this, startIndex, deleteCount, ...inserts);
|
1621 |
|
1622 | for (let _i2 = 0, _length2 = removes == null ? 0 : removes.length; _i2 < _length2; _i2++) {
|
1623 | let remove = removes[_i2];
|
1624 | delete remove.parent;
|
1625 | }
|
1626 |
|
1627 | return removes;
|
1628 | }
|
1629 | /**
|
1630 | * Add {@link Node}s to the beginning of the {@link NodeList} and return the new length of the {@link NodeList}.
|
1631 | * @returns {Number}
|
1632 | */
|
1633 |
|
1634 |
|
1635 | unshift(...nodes) {
|
1636 | const parent = this.parent;
|
1637 | const inserts = nodes.filter(node => node !== parent);
|
1638 | this.splice(0, 0, ...inserts);
|
1639 | return this.length;
|
1640 | }
|
1641 | /**
|
1642 | * Return the current {@link NodeList} as a String.
|
1643 | * @returns {String}
|
1644 | * @example
|
1645 | * nodeList.toString() // returns ''
|
1646 | */
|
1647 |
|
1648 |
|
1649 | toString() {
|
1650 | return this.join('');
|
1651 | }
|
1652 | /**
|
1653 | * Return the current {@link NodeList} as an Array.
|
1654 | * @returns {Array}
|
1655 | * @example
|
1656 | * nodeList.toJSON() // returns []
|
1657 | */
|
1658 |
|
1659 |
|
1660 | toJSON() {
|
1661 | return Array.from(this).map(node => node.toJSON());
|
1662 | }
|
1663 | /**
|
1664 | * Return a new {@link NodeList} from an object.
|
1665 | * @param {Array|Node} nodes - An array or object of nodes.
|
1666 | * @returns {NodeList} A new {@link NodeList}
|
1667 | * @example <caption>Return a NodeList from an array of text.</caption>
|
1668 | * NodeList.from([ 'test' ]) // returns NodeList [ Text { data: 'test' } ]
|
1669 | */
|
1670 |
|
1671 |
|
1672 | static from(nodes) {
|
1673 | return new NodeList(new Fragment(), ...getNodeListArray(nodes));
|
1674 | }
|
1675 |
|
1676 | }
|
1677 | /**
|
1678 | * Return an NodeList-compatible array from an array.
|
1679 | * @private
|
1680 | */
|
1681 |
|
1682 | function getNodeListArray(nodes) {
|
1683 | // coerce nodes into an array
|
1684 | return Object(nodes).length ? Array.from(nodes).filter(node => node != null).map(normalize) : [];
|
1685 | }
|
1686 | /**
|
1687 | * Return an innerHTML-encoded string.
|
1688 | * @private
|
1689 | */
|
1690 |
|
1691 |
|
1692 | function getInnerHtmlEncodedString(string) {
|
1693 | return string.replace(/&|<|>/g, match => match === '&' ? '&' : match === '<' ? '<' : '>');
|
1694 | }
|
1695 |
|
1696 | defaultTreeAdapter.isElementNode = node => 'tagName' in node; // patch defaultTreeAdapter.createCommentNode to support doctype nodes
|
1697 |
|
1698 |
|
1699 | defaultTreeAdapter.createCommentNode = function createCommentNode(data) {
|
1700 | return typeof data === 'string' ? {
|
1701 | nodeName: '#comment',
|
1702 | data,
|
1703 | parentNode: null
|
1704 | } : Object.assign({
|
1705 | nodeName: '#documentType',
|
1706 | name: data
|
1707 | }, data);
|
1708 | };
|
1709 |
|
1710 | Tokenizer.prototype._createDoctypeToken = function _createDoctypeToken() {
|
1711 | const doctypeRegExp = /^(doctype)(\s+)(html)(?:(\s+)(public\s+(["']).*?\6))?(?:(\s+)((["']).*\9))?(\s*)/i;
|
1712 | const doctypeStartRegExp = /doctype\s*$/i;
|
1713 | const offset = this.preprocessor.html.slice(0, this.preprocessor.pos).match(doctypeStartRegExp, '')[0].length;
|
1714 | const innerHTML = this.preprocessor.html.slice(this.preprocessor.pos - offset);
|
1715 | const [, doctype, before, name, beforePublicId, publicId,, beforeSystemId, systemId,, after] = Object(innerHTML.match(doctypeRegExp));
|
1716 | this.currentToken = {
|
1717 | type: Tokenizer.COMMENT_TOKEN,
|
1718 | data: {
|
1719 | doctype,
|
1720 | name,
|
1721 | publicId,
|
1722 | systemId,
|
1723 | source: {
|
1724 | before,
|
1725 | beforePublicId,
|
1726 | beforeSystemId,
|
1727 | after
|
1728 | }
|
1729 | },
|
1730 | forceQuirks: false,
|
1731 | publicId: null,
|
1732 | systemId: null
|
1733 | };
|
1734 | }; // patch _createAttr to include the start offset position for name
|
1735 |
|
1736 |
|
1737 | Tokenizer.prototype._createAttr = function _createAttr(attrNameFirstCh) {
|
1738 | this.currentAttr = {
|
1739 | name: attrNameFirstCh,
|
1740 | value: '',
|
1741 | startName: this.preprocessor.pos
|
1742 | };
|
1743 | }; // patch _leaveAttrName to support duplicate attributes
|
1744 |
|
1745 |
|
1746 | Tokenizer.prototype._leaveAttrName = function _leaveAttrName(toState) {
|
1747 | const startName = this.currentAttr.startName;
|
1748 | const endName = this.currentAttr.endName = this.preprocessor.pos;
|
1749 | this.currentToken.attrs.push(this.currentAttr);
|
1750 | const before = this.preprocessor.html.slice(0, startName).match(/\s*$/)[0];
|
1751 | this.currentAttr.raw = {
|
1752 | name: this.preprocessor.html.slice(startName, endName),
|
1753 | value: null,
|
1754 | source: {
|
1755 | startName,
|
1756 | endName,
|
1757 | startValue: null,
|
1758 | endValue: null,
|
1759 | before,
|
1760 | quote: ''
|
1761 | }
|
1762 | };
|
1763 | this.state = toState;
|
1764 | }; // patch _leaveAttrValue to include the offset end position for value
|
1765 |
|
1766 |
|
1767 | Tokenizer.prototype._leaveAttrValue = function _leaveAttrValue(toState) {
|
1768 | const startValue = this.currentAttr.endName + 1;
|
1769 | const endValue = this.preprocessor.pos;
|
1770 | const quote = endValue - this.currentAttr.value.length === startValue ? '' : this.preprocessor.html[startValue];
|
1771 | const currentAttrValue = this.preprocessor.html.slice(startValue + quote.length, endValue - quote.length);
|
1772 | this.currentAttr.raw.value = currentAttrValue;
|
1773 | this.currentAttr.raw.source.startValue = startValue;
|
1774 | this.currentAttr.raw.source.endValue = endValue;
|
1775 | this.currentAttr.raw.source.quote = quote;
|
1776 | this.state = toState;
|
1777 | }; // patch TAG_OPEN_STATE to support jsx-style fragments
|
1778 |
|
1779 |
|
1780 | const originalTAG_OPEN_STATE = Tokenizer.prototype.TAG_OPEN_STATE;
|
1781 |
|
1782 | Tokenizer.prototype.TAG_OPEN_STATE = function TAG_OPEN_STATE(cp) {
|
1783 | const isReactOpeningElement = this.preprocessor.html.slice(this.preprocessor.pos - 1, this.preprocessor.pos + 1) === '<>';
|
1784 | const isReactClosingElement = this.preprocessor.html.slice(this.preprocessor.pos - 1, this.preprocessor.pos + 2) === '</>';
|
1785 |
|
1786 | if (isReactOpeningElement) {
|
1787 | this._createStartTagToken();
|
1788 |
|
1789 | this._reconsumeInState('TAG_NAME_STATE');
|
1790 | } else if (isReactClosingElement) {
|
1791 | this._createEndTagToken();
|
1792 |
|
1793 | this._reconsumeInState('TAG_NAME_STATE');
|
1794 | } else {
|
1795 | originalTAG_OPEN_STATE.call(this, cp);
|
1796 | }
|
1797 | };
|
1798 |
|
1799 | function parseLoose(html, parseOpts) {
|
1800 | this.tokenizer = new Tokenizer(this.options);
|
1801 | const document = defaultTreeAdapter.createDocumentFragment();
|
1802 | const template = defaultTreeAdapter.createDocumentFragment();
|
1803 |
|
1804 | this._bootstrap(document, template);
|
1805 |
|
1806 | this._pushTmplInsertionMode('IN_TEMPLATE_MODE');
|
1807 |
|
1808 | this._initTokenizerForFragmentParsing();
|
1809 |
|
1810 | this._insertFakeRootElement();
|
1811 |
|
1812 | this.tokenizer.write(html, true);
|
1813 |
|
1814 | this._runParsingLoop(null);
|
1815 |
|
1816 | document.childNodes = filter(document.childNodes, parseOpts);
|
1817 | return document;
|
1818 | }
|
1819 |
|
1820 | function parseHTML(input, parseOpts) {
|
1821 | const htmlParser = new HTMLParser({
|
1822 | treeAdapter: defaultTreeAdapter,
|
1823 | sourceCodeLocationInfo: true
|
1824 | });
|
1825 | return parseLoose.call(htmlParser, input, parseOpts);
|
1826 | } // filter out generated elements
|
1827 |
|
1828 | function filter(childNodes, parseOpts) {
|
1829 | return childNodes.reduce((nodes, childNode) => {
|
1830 | const isVoidElement = parseOpts.voidElements.includes(childNode.nodeName);
|
1831 | const grandChildNodes = childNode.childNodes ? filter(childNode.childNodes, parseOpts) : []; // filter child nodes
|
1832 |
|
1833 | if (isVoidElement) {
|
1834 | delete childNode.childNodes;
|
1835 | } else {
|
1836 | childNode.childNodes = grandChildNodes;
|
1837 | } // push nodes with source
|
1838 |
|
1839 |
|
1840 | if (childNode.sourceCodeLocation) {
|
1841 | nodes.push(childNode);
|
1842 | }
|
1843 |
|
1844 | if (!childNode.sourceCodeLocation || !childNode.childNodes) {
|
1845 | nodes.push(...grandChildNodes);
|
1846 | }
|
1847 |
|
1848 | return nodes;
|
1849 | }, []);
|
1850 | }
|
1851 |
|
1852 | /**
|
1853 | * @name Result
|
1854 | * @class
|
1855 | * @classdesc Create a new syntax tree {@link Result} from a processed input.
|
1856 | * @param {Object} processOptions - Custom settings applied to the {@link Result}.
|
1857 | * @param {String} processOptions.from - Source input location.
|
1858 | * @param {String} processOptions.to - Destination output location.
|
1859 | * @param {Array} processOptions.voidElements - Void elements.
|
1860 | * @returns {Result}
|
1861 | * @property {Result} result - Result of pHTML transformations.
|
1862 | * @property {String} result.from - Path to the HTML source file. You should always set from, because it is used in source map generation and syntax error messages.
|
1863 | * @property {String} result.to - Path to the HTML output file.
|
1864 | * @property {Fragment} result.root - Object representing the parsed nodes of the HTML file.
|
1865 | * @property {Array} result.messages - List of the messages gathered during transformations.
|
1866 | * @property {Array} result.voidElements - List of the elements that only have a start tag, as they cannot have any content.
|
1867 | */
|
1868 |
|
1869 | class Result {
|
1870 | constructor(html, processOptions) {
|
1871 | // the "to" and "from" locations are always string values
|
1872 | const from = 'from' in Object(processOptions) && processOptions.from !== undefined && processOptions.from !== null ? String(processOptions.from) : '';
|
1873 | const to = 'to' in Object(processOptions) && processOptions.to !== undefined && processOptions.to !== null ? String(processOptions.to) : from;
|
1874 | const voidElements = 'voidElements' in Object(processOptions) ? [].concat(Object(processOptions).voidElements || []) : Result.voidElements; // prepare visitors (which may be functions or visitors)
|
1875 |
|
1876 | const visitors = getVisitors(Object(processOptions).visitors); // prepare the result object
|
1877 |
|
1878 | Object.assign(this, {
|
1879 | type: 'result',
|
1880 | from,
|
1881 | to,
|
1882 | input: {
|
1883 | html,
|
1884 | from,
|
1885 | to
|
1886 | },
|
1887 | root: null,
|
1888 | voidElements,
|
1889 | visitors,
|
1890 | messages: []
|
1891 | }); // parse the html and transform it into nodes
|
1892 |
|
1893 | const documentFragment = parseHTML(html, {
|
1894 | voidElements
|
1895 | });
|
1896 | this.root = transform(documentFragment, this);
|
1897 | }
|
1898 | /**
|
1899 | * Current {@link Root} as a String.
|
1900 | * @returns {String}
|
1901 | */
|
1902 |
|
1903 |
|
1904 | get html() {
|
1905 | return String(this.root);
|
1906 | }
|
1907 | /**
|
1908 | * Messages that are warnings.
|
1909 | * @returns {String}
|
1910 | */
|
1911 |
|
1912 |
|
1913 | get warnings() {
|
1914 | return this.messages.filter(message => Object(message).type === 'warning');
|
1915 | }
|
1916 | /**
|
1917 | * Return a normalized node whose instances match the current {@link Result}.
|
1918 | * @param {Node} [node] - the node to be normalized.
|
1919 | * @returns {Node}
|
1920 | * @example
|
1921 | * result.normalize(someNode)
|
1922 | */
|
1923 |
|
1924 |
|
1925 | normalize(node) {
|
1926 | return normalize(node);
|
1927 | }
|
1928 | /**
|
1929 | * The current {@link Root} as an Object.
|
1930 | * @returns {Object}
|
1931 | */
|
1932 |
|
1933 |
|
1934 | toJSON() {
|
1935 | return this.root.toJSON();
|
1936 | }
|
1937 | /**
|
1938 | * Transform the current {@link Node} and any descendants using visitors.
|
1939 | * @param {Node} node - The {@link Node} to be visited.
|
1940 | * @param {Object} [overrideVisitors] - Alternative visitors to be used in place of {@link Result} visitors.
|
1941 | * @returns {ResultPromise}
|
1942 | * @example
|
1943 | * await result.visit(someNode)
|
1944 | * @example
|
1945 | * await result.visit() // visit using the root of the current result
|
1946 | * @example
|
1947 | * await result.visit(root, {
|
1948 | * Element () {
|
1949 | * // do something to an element
|
1950 | * }
|
1951 | * })
|
1952 | */
|
1953 |
|
1954 |
|
1955 | visit(node, overrideVisitors) {
|
1956 | const nodeToUse = 0 in arguments ? node : this.root;
|
1957 | return visit(nodeToUse, this, overrideVisitors);
|
1958 | }
|
1959 | /**
|
1960 | * Add a warning to the current {@link Root}.
|
1961 | * @param {String} text - The message being sent as the warning.
|
1962 | * @param {Object} [opts] - Additional information about the warning.
|
1963 | * @example
|
1964 | * result.warn('Something went wrong')
|
1965 | * @example
|
1966 | * result.warn('Something went wrong', {
|
1967 | * node: someNode,
|
1968 | * plugin: somePlugin
|
1969 | * })
|
1970 | */
|
1971 |
|
1972 |
|
1973 | warn(text, rawopts) {
|
1974 | const opts = Object(rawopts);
|
1975 |
|
1976 | if (!opts.plugin) {
|
1977 | if (Object(this.currentPlugin).name) {
|
1978 | opts.plugin = this.currentPlugin.name;
|
1979 | }
|
1980 | }
|
1981 |
|
1982 | this.messages.push({
|
1983 | type: 'warning',
|
1984 | text,
|
1985 | opts
|
1986 | });
|
1987 | }
|
1988 |
|
1989 | }
|
1990 |
|
1991 | Result.voidElements = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
1992 |
|
1993 | function transform(node, result) {
|
1994 | const hasSource = node.sourceCodeLocation === Object(node.sourceCodeLocation);
|
1995 | const source = hasSource ? {
|
1996 | startOffset: node.sourceCodeLocation.startOffset,
|
1997 | endOffset: node.sourceCodeLocation.endOffset,
|
1998 | startInnerOffset: Object(node.sourceCodeLocation.startTag).endOffset || node.sourceCodeLocation.startOffset,
|
1999 | endInnerOffset: Object(node.sourceCodeLocation.endTag).startOffset || node.sourceCodeLocation.endOffset,
|
2000 | input: result.input
|
2001 | } : {
|
2002 | startOffset: 0,
|
2003 | startInnerOffset: 0,
|
2004 | endInnerOffset: result.input.html.length,
|
2005 | endOffset: result.input.html.length,
|
2006 | input: result.input
|
2007 | };
|
2008 |
|
2009 | if (Object(node.sourceCodeLocation).startTag) {
|
2010 | source.before = result.input.html.slice(source.startOffset, source.startInnerOffset - 1).match(/\s*\/?$/)[0];
|
2011 | }
|
2012 |
|
2013 | if (Object(node.sourceCodeLocation).endTag) {
|
2014 | source.after = result.input.html.slice(source.endInnerOffset + 2 + node.nodeName.length, source.endOffset - 1);
|
2015 | }
|
2016 |
|
2017 | const $node = defaultTreeAdapter.isCommentNode(node) ? new Comment({
|
2018 | comment: node.data,
|
2019 | source,
|
2020 | result
|
2021 | }) : defaultTreeAdapter.isDocumentTypeNode(node) ? new Doctype(Object.assign(node, {
|
2022 | result,
|
2023 | source: Object.assign({}, node.source, source)
|
2024 | })) : defaultTreeAdapter.isElementNode(node) ? new Element({
|
2025 | name: result.input.html.slice(source.startOffset + 1, source.startOffset + 1 + node.nodeName.length),
|
2026 | attrs: node.attrs.map(attr => attr.raw),
|
2027 | nodes: node.childNodes instanceof Array ? node.childNodes.map(child => transform(child, result)) : null,
|
2028 | isSelfClosing: /\//.test(source.before),
|
2029 | isWithoutEndTag: !Object(node.sourceCodeLocation).endTag,
|
2030 | isVoid: result.voidElements.includes(node.tagName),
|
2031 | result,
|
2032 | source
|
2033 | }) : defaultTreeAdapter.isTextNode(node) ? new Text({
|
2034 | data: hasSource ? source.input.html.slice(source.startInnerOffset, source.endInnerOffset) : node.value,
|
2035 | result,
|
2036 | source
|
2037 | }) : new Fragment({
|
2038 | nodes: node.childNodes instanceof Array ? node.childNodes.map(child => transform(child, result)) : null,
|
2039 | result,
|
2040 | source
|
2041 | });
|
2042 | return $node;
|
2043 | }
|
2044 | /**
|
2045 | * A promise to return a syntax tree.
|
2046 | * @typedef {Promise} ResultPromise
|
2047 | * @example
|
2048 | * resultPromise.then(result => {
|
2049 | * // do something with the result
|
2050 | * })
|
2051 | */
|
2052 |
|
2053 | /**
|
2054 | * A promise to return a syntax tree.
|
2055 | * @typedef {Object} ProcessOptions
|
2056 | * @property {Object} ProcessOptions - Custom settings applied to the {@link Result}.
|
2057 | * @property {String} ProcessOptions.from - Source input location.
|
2058 | * @property {String} ProcessOptions.to - Destination output location.
|
2059 | * @property {Array} ProcessOptions.voidElements - Void elements.
|
2060 | */
|
2061 |
|
2062 | /**
|
2063 | * @name Element
|
2064 | * @class
|
2065 | * @extends Container
|
2066 | * @classdesc Create a new {@link Element} {@Link Node}.
|
2067 | * @param {Object|String} settings - Custom settings applied to the {@link Element} or its tag name.
|
2068 | * @param {String} settings.name - Tag name of the {@link Element}.
|
2069 | * @param {Boolean} settings.isSelfClosing - Whether the {@link Element} is self-closing.
|
2070 | * @param {Boolean} settings.isVoid - Whether the {@link Element} is void.
|
2071 | * @param {Boolean} settings.isWithoutEndTag - Whether the {@link Element} uses a closing tag.
|
2072 | * @param {Array|AttributeList|Object} settings.attrs - Attributes applied to the {@link Element}.
|
2073 | * @param {Array|NodeList} settings.nodes - Nodes appended to the {@link Element}.
|
2074 | * @param {Object} settings.source - Source mapping of the {@link Element}.
|
2075 | * @param {Array|AttributeList|Object} [attrs] - Conditional override attributes applied to the {@link Element}.
|
2076 | * @param {Array|NodeList} [nodes] - Conditional override nodes appended to the {@link Element}.
|
2077 | * @returns {Element} A new {@link Element} {@Link Node}
|
2078 | * @example
|
2079 | * new Element({ name: 'p' }) // returns an element representing <p></p>
|
2080 | *
|
2081 | * new Element({
|
2082 | * name: 'input',
|
2083 | * attrs: [{ name: 'type', value: 'search' }],
|
2084 | * isVoid: true
|
2085 | * }) // returns an element representing <input type="search">
|
2086 | * @example
|
2087 | * new Element('p') // returns an element representing <p></p>
|
2088 | *
|
2089 | * new Element('p', null,
|
2090 | * new Element(
|
2091 | * 'input',
|
2092 | * [{ name: 'type', value: 'search' }]
|
2093 | * )
|
2094 | * ) // returns an element representing <p><input type="search"></p>
|
2095 | */
|
2096 |
|
2097 | class Element extends Container {
|
2098 | constructor(settings, ...args) {
|
2099 | super();
|
2100 |
|
2101 | if (settings !== Object(settings)) {
|
2102 | settings = {
|
2103 | name: String(settings == null ? 'span' : settings)
|
2104 | };
|
2105 | }
|
2106 |
|
2107 | if (args[0] === Object(args[0])) {
|
2108 | settings.attrs = args[0];
|
2109 | }
|
2110 |
|
2111 | if (args.length > 1) {
|
2112 | settings.nodes = args.slice(1);
|
2113 | }
|
2114 |
|
2115 | Object.assign(this, settings, {
|
2116 | type: 'element',
|
2117 | name: settings.name,
|
2118 | isSelfClosing: Boolean(settings.isSelfClosing),
|
2119 | isVoid: 'isVoid' in settings ? Boolean(settings.isVoid) : Result.voidElements.includes(settings.name),
|
2120 | isWithoutEndTag: Boolean(settings.isWithoutEndTag),
|
2121 | attrs: AttributeList.from(settings.attrs),
|
2122 | nodes: Array.isArray(settings.nodes) ? new NodeList(this, ...Array.from(settings.nodes)) : settings.nodes !== null && settings.nodes !== undefined ? new NodeList(this, settings.nodes) : new NodeList(this),
|
2123 | source: Object(settings.source)
|
2124 | });
|
2125 | }
|
2126 | /**
|
2127 | * Return the outerHTML of the current {@link Element} as a String.
|
2128 | * @returns {String}
|
2129 | * @example
|
2130 | * element.outerHTML // returns a string of outerHTML
|
2131 | */
|
2132 |
|
2133 |
|
2134 | get outerHTML() {
|
2135 | return `${getOpeningTagString(this)}${this.nodes.innerHTML}${getClosingTagString(this)}`;
|
2136 | }
|
2137 | /**
|
2138 | * Replace the current {@link Element} from a String.
|
2139 | * @param {String} input - Source being processed.
|
2140 | * @returns {Void}
|
2141 | * @example
|
2142 | * element.outerHTML = 'Hello <strong>world</strong>';
|
2143 | */
|
2144 |
|
2145 |
|
2146 | set outerHTML(outerHTML) {
|
2147 | Object.getOwnPropertyDescriptor(Container.prototype, 'outerHTML').set.call(this, outerHTML);
|
2148 | }
|
2149 | /**
|
2150 | * Return a clone of the current {@link Element}.
|
2151 | * @param {Object} settings - Custom settings applied to the cloned {@link Element}.
|
2152 | * @param {Boolean} isDeep - Whether the descendants of the current {@link Element} should also be cloned.
|
2153 | * @returns {Element} - The cloned Element
|
2154 | */
|
2155 |
|
2156 |
|
2157 | clone(settings, isDeep) {
|
2158 | const clone = new Element(Object.assign({}, this, {
|
2159 | nodes: []
|
2160 | }, Object(settings)));
|
2161 | const didSetNodes = 'nodes' in Object(settings);
|
2162 |
|
2163 | if (isDeep && !didSetNodes) {
|
2164 | clone.nodes = this.nodes.clone(clone);
|
2165 | }
|
2166 |
|
2167 | return clone;
|
2168 | }
|
2169 | /**
|
2170 | * Return the Element as a unique Object.
|
2171 | * @returns {Object}
|
2172 | */
|
2173 |
|
2174 |
|
2175 | toJSON() {
|
2176 | const object = {
|
2177 | name: this.name
|
2178 | }; // conditionally disclose whether the Element is self-closing
|
2179 |
|
2180 | if (this.isSelfClosing) {
|
2181 | object.isSelfClosing = true;
|
2182 | } // conditionally disclose whether the Element is void
|
2183 |
|
2184 |
|
2185 | if (this.isVoid) {
|
2186 | object.isVoid = true;
|
2187 | } // conditionally disclose Attributes applied to the Element
|
2188 |
|
2189 |
|
2190 | if (this.attrs.length) {
|
2191 | object.attrs = this.attrs.toJSON();
|
2192 | } // conditionally disclose Nodes appended to the Element
|
2193 |
|
2194 |
|
2195 | if (!this.isSelfClosing && !this.isVoid && this.nodes.length) {
|
2196 | object.nodes = this.nodes.toJSON();
|
2197 | }
|
2198 |
|
2199 | return object;
|
2200 | }
|
2201 | /**
|
2202 | * Return the stringified Element.
|
2203 | * @returns {String}
|
2204 | */
|
2205 |
|
2206 |
|
2207 | toString() {
|
2208 | return `${getOpeningTagString(this)}${this.nodes || ''}${`${getClosingTagString(this)}`}`;
|
2209 | }
|
2210 |
|
2211 | }
|
2212 |
|
2213 | function getClosingTagString(element) {
|
2214 | return element.isSelfClosing || element.isVoid || element.isWithoutEndTag ? '' : `</${element.name}${element.source.after || ''}>`;
|
2215 | }
|
2216 |
|
2217 | function getOpeningTagString(element) {
|
2218 | return `<${element.name}${element.attrs}${element.source.before || ''}>`;
|
2219 | }
|
2220 |
|
2221 | /**
|
2222 | * @name Plugin
|
2223 | * @class
|
2224 | * @classdesc Create a new Plugin.
|
2225 | * @param {String} name - Name of the Plugin.
|
2226 | * @param {Function} pluginFunction - Function executed by the Plugin during initialization.
|
2227 | * @returns {Plugin}
|
2228 | * @example
|
2229 | * new Plugin('phtml-test', pluginOptions => {
|
2230 | * // initialization logic
|
2231 | *
|
2232 | * return {
|
2233 | * Element (element, result) {
|
2234 | * // runtime logic, do something with an element
|
2235 | * },
|
2236 | * Root (root, result) {
|
2237 | * // runtime logic, do something with the root
|
2238 | * }
|
2239 | * }
|
2240 | * })
|
2241 | * @example
|
2242 | * new Plugin('phtml-test', pluginOptions => {
|
2243 | * // initialization logic
|
2244 | *
|
2245 | * return (root, result) => {
|
2246 | * // runtime logic, do something with the root
|
2247 | * }
|
2248 | * })
|
2249 | */
|
2250 |
|
2251 | class Plugin extends Function {
|
2252 | constructor(name, pluginFunction) {
|
2253 | return Object.defineProperties(pluginFunction, {
|
2254 | constructor: {
|
2255 | value: Plugin,
|
2256 | configurable: true
|
2257 | },
|
2258 | type: {
|
2259 | value: 'plugin',
|
2260 | configurable: true
|
2261 | },
|
2262 | name: {
|
2263 | value: String(name || 'phtml-plugin'),
|
2264 | configurable: true
|
2265 | },
|
2266 | pluginFunction: {
|
2267 | value: typeof pluginFunction === 'function' ? pluginFunction : () => pluginFunction,
|
2268 | configurable: true
|
2269 | },
|
2270 | process: {
|
2271 | value(...args) {
|
2272 | return Plugin.prototype.process.apply(this, args);
|
2273 | },
|
2274 |
|
2275 | configurable: true
|
2276 | }
|
2277 | });
|
2278 | }
|
2279 | /**
|
2280 | * Process input with options and plugin options and return the result.
|
2281 | * @param {String} input - Source being processed.
|
2282 | * @param {ProcessOptions} processOptions - Custom settings applied to the Result.
|
2283 | * @param {Object} pluginOptions - Options passed to the Plugin.
|
2284 | * @returns {ResultPromise}
|
2285 | * @example
|
2286 | * plugin.process('some html', processOptions, pluginOptions)
|
2287 | */
|
2288 |
|
2289 |
|
2290 | process(input, processOptions, pluginOptions) {
|
2291 | const initializedPlugin = this.pluginFunction(pluginOptions);
|
2292 | const result = new Result(input, Object.assign({
|
2293 | visitors: [initializedPlugin]
|
2294 | }, Object(processOptions)));
|
2295 | return result.visit(result.root);
|
2296 | }
|
2297 |
|
2298 | }
|
2299 |
|
2300 | /**
|
2301 | * @name PHTML
|
2302 | * @class
|
2303 | * @classdesc Create a new instance of {@link PHTML}.
|
2304 | * @param {Array|Object|Plugin|Function} plugins - Plugin or plugins being added.
|
2305 | * @returns {PHTML}
|
2306 | * @example
|
2307 | * new PHTML(plugin)
|
2308 | * @example
|
2309 | * new PHTML([ somePlugin, anotherPlugin ])
|
2310 | */
|
2311 |
|
2312 | class PHTML {
|
2313 | constructor(pluginOrPlugins) {
|
2314 | Object.assign(this, {
|
2315 | plugins: []
|
2316 | });
|
2317 | this.use(pluginOrPlugins);
|
2318 | }
|
2319 | /**
|
2320 | * Process input using plugins and return the result
|
2321 | * @param {String} input - Source being processed.
|
2322 | * @param {ProcessOptions} processOptions - Custom settings applied to the Result.
|
2323 | * @returns {ResultPromise}
|
2324 | * @example
|
2325 | * phtml.process('some html', processOptions)
|
2326 | */
|
2327 |
|
2328 |
|
2329 | process(input, processOptions) {
|
2330 | const result = new Result(input, Object.assign({
|
2331 | visitors: this.plugins
|
2332 | }, Object(processOptions))); // dispatch visitors and promise the result
|
2333 |
|
2334 | return result.visit();
|
2335 | }
|
2336 | /**
|
2337 | * Add plugins to the existing instance of PHTML
|
2338 | * @param {Array|Object|Plugin|Function} plugins - Plugin or plugins being added.
|
2339 | * @returns {PHTML}
|
2340 | * @example
|
2341 | * phtml.use(plugin)
|
2342 | * @example
|
2343 | * phtml.use([ somePlugin, anotherPlugin ])
|
2344 | * @example
|
2345 | * phtml.use(somePlugin, anotherPlugin)
|
2346 | */
|
2347 |
|
2348 |
|
2349 | use(pluginOrPlugins, ...additionalPlugins) {
|
2350 | const plugins = [pluginOrPlugins, ...additionalPlugins].reduce((flattenedPlugins, plugin) => flattenedPlugins.concat(plugin), []).filter( // Plugins are either a function or an object with keys
|
2351 | plugin => typeof plugin === 'function' || Object(plugin) === plugin && Object.keys(plugin).length);
|
2352 | this.plugins.push(...plugins);
|
2353 | return this;
|
2354 | }
|
2355 | /**
|
2356 | * Process input and return the new {@link Result}
|
2357 | * @param {ProcessOptions} [processOptions] - Custom settings applied to the {@link Result}.
|
2358 | * @param {Array|Object|Plugin|Function} [plugins] - Custom settings applied to the {@link Result}.
|
2359 | * @returns {ResultPromise}
|
2360 | * @example
|
2361 | * PHTML.process('some html', processOptions)
|
2362 | * @example <caption>Process HTML with plugins.</caption>
|
2363 | * PHTML.process('some html', processOptions, plugins) // returns a new PHTML instance
|
2364 | */
|
2365 |
|
2366 |
|
2367 | static process(input, processOptions, pluginOrPlugins) {
|
2368 | const phtml = new PHTML(pluginOrPlugins);
|
2369 | return phtml.process(input, processOptions);
|
2370 | }
|
2371 | /**
|
2372 | * Return a new {@link PHTML} instance which will use plugins
|
2373 | * @param {Array|Object|Plugin|Function} plugin - Plugin or plugins being added.
|
2374 | * @returns {PHTML} - New {@link PHTML} instance
|
2375 | * @example
|
2376 | * PHTML.use(plugin) // returns a new PHTML instance
|
2377 | * @example
|
2378 | * PHTML.use([ somePlugin, anotherPlugin ]) // returns a new PHTML instance
|
2379 | * @example
|
2380 | * PHTML.use(somePlugin, anotherPlugin) // returns a new PHTML instance
|
2381 | */
|
2382 |
|
2383 |
|
2384 | static use(pluginOrPlugins, ...additionalPlugins) {
|
2385 | return new PHTML().use(pluginOrPlugins, ...additionalPlugins);
|
2386 | }
|
2387 |
|
2388 | }
|
2389 |
|
2390 | PHTML.AttributeList = AttributeList;
|
2391 | PHTML.Comment = Comment;
|
2392 | PHTML.Container = Container;
|
2393 | PHTML.Doctype = Doctype;
|
2394 | PHTML.Element = Element;
|
2395 | PHTML.Fragment = Fragment;
|
2396 | PHTML.Node = Node;
|
2397 | PHTML.NodeList = NodeList;
|
2398 | PHTML.Plugin = Plugin;
|
2399 | PHTML.Result = Result;
|
2400 | PHTML.Text = Text;
|
2401 |
|
2402 | export default PHTML;
|
2403 | //# sourceMappingURL=index.mjs.map
|