UNPKG

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