UNPKG

70.7 kBJavaScriptView Raw
1import HTMLParser from 'parse5/lib/parser';
2import defaultTreeAdapter from 'parse5/lib/tree-adapters/default';
3import 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*/
17class 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
222function 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
257function 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
309function 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
321function 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
337function 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
350function 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
363function 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
376function 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*/
388function 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
447function 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
463function 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
492function getTypeFromNode(node) {
493 return {
494 'comment': 'Comment',
495 'text': 'Text',
496 'doctype': 'Doctype',
497 'fragment': 'Fragment'
498 }[node.type] || 'Element';
499}
500
501function 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
518class 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
784function 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
801class 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
913class 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
1157function 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
1173function getCbAndFilterFromArgs(args) {
1174 const [cbOrFilter, onlyCb] = args;
1175 const cb = onlyCb || cbOrFilter;
1176 const filter = onlyCb ? cbOrFilter : undefined;
1177 return [cb, filter];
1178}
1179
1180function 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
1196function 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
1215class 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
1297class 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
1363class 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
1462function 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
1478const 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
1489class 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
1682function 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
1692function getInnerHtmlEncodedString(string) {
1693 return string.replace(/&|<|>/g, match => match === '&' ? '&amp;' : match === '<' ? '&lt;' : '&gt;');
1694}
1695
1696defaultTreeAdapter.isElementNode = node => 'tagName' in node; // patch defaultTreeAdapter.createCommentNode to support doctype nodes
1697
1698
1699defaultTreeAdapter.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
1710Tokenizer.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
1737Tokenizer.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
1746Tokenizer.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
1767Tokenizer.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
1780const originalTAG_OPEN_STATE = Tokenizer.prototype.TAG_OPEN_STATE;
1781
1782Tokenizer.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
1799function 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
1820function 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
1828function 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
1869class 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
1991Result.voidElements = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
1992
1993function 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
2097class 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
2213function getClosingTagString(element) {
2214 return element.isSelfClosing || element.isVoid || element.isWithoutEndTag ? '' : `</${element.name}${element.source.after || ''}>`;
2215}
2216
2217function 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
2251class 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
2312class 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
2390PHTML.AttributeList = AttributeList;
2391PHTML.Comment = Comment;
2392PHTML.Container = Container;
2393PHTML.Doctype = Doctype;
2394PHTML.Element = Element;
2395PHTML.Fragment = Fragment;
2396PHTML.Node = Node;
2397PHTML.NodeList = NodeList;
2398PHTML.Plugin = Plugin;
2399PHTML.Result = Result;
2400PHTML.Text = Text;
2401
2402export default PHTML;
2403//# sourceMappingURL=index.mjs.map