phtml
Version:
A tool for transforming HTML with JavaScript
1,729 lines (1,440 loc) • 394 kB
JavaScript
!function(){/**
* @name AttributeList
* @class
* @extends Array
* @classdesc Return a new list of {@link Element} attributes.
* @param {...Array|AttributeList|Object} attrs - An array or object of attributes.
* @returns {AttributeList}
* @example
* new AttributeList([{ name: 'class', value: 'foo' }, { name: 'id', value: 'bar' }])
* @example
* new AttributeList({ class: 'foo', id: 'bar' })
*/
class AttributeList extends Array {
constructor(attrs) {
super();
if (attrs === Object(attrs)) {
this.push(...getAttributeListArray(attrs));
}
}
/**
* Add an attribute or attributes to the current {@link AttributeList}.
* @param {Array|Object|RegExp|String} name - The attribute to remove.
* @param {String} [value] - The value of the attribute being added.
* @returns {Boolean} - Whether the attribute or attributes were added to the current {@link AttributeList}.
* @example <caption>Add an empty "id" attribute.</caption>
* attrs.add('id')
* @example <caption>Add an "id" attribute with a value of "bar".</caption>
* attrs.add({ id: 'bar' })
* @example
* attrs.add([{ name: 'id', value: 'bar' }])
*/
add(name) {
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
return toggle(this, getAttributeListArray(name, ...args), true).attributeAdded;
}
/**
* Return a new clone of the current {@link AttributeList} while conditionally applying additional attributes.
* @param {...Array|AttributeList|Object} attrs - Additional attributes to be added to the new {@link AttributeList}.
* @returns {Element} - The cloned Element.
* @example
* attrs.clone()
* @example <caption>Clone the current attribute and add an "id" attribute with a value of "bar".</caption>
* attrs.clone({ name: 'id', value: 'bar' })
*/
clone() {
for (var _len2 = arguments.length, attrs = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
attrs[_key2] = arguments[_key2];
}
return new AttributeList(Array.from(this).concat(getAttributeListArray(attrs)));
}
/**
* Return whether an attribute or attributes exists in the current {@link AttributeList}.
* @param {String} name - The name or attribute object being accessed.
* @returns {Boolean} - Whether the attribute exists.
* @example <caption>Return whether there is an "id" attribute.</caption>
* attrs.contains('id')
* @example
* attrs.contains({ id: 'bar' })
* @example <caption>Return whether there is an "id" attribute with a value of "bar".</caption>
* attrs.contains([{ name: 'id': value: 'bar' }])
*/
contains(name) {
return this.indexOf(name) !== -1;
}
/**
* Return an attribute value by name from the current {@link AttributeList}.
* @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.
* @param {RegExp|String} name - The name of the attribute being accessed.
* @returns {Boolean|Null|String} - The value of the attribute (a string or null) or false (if the attribute does not exist).
* @example <caption>Return the value of "id" or `false`.</caption>
* // <div>this element has no "id" attribute</div>
* attrs.get('id') // returns false
* // <div id>this element has an "id" attribute with no value</div>
* attrs.get('id') // returns null
* // <div id="">this element has an "id" attribute with a value</div>
* attrs.get('id') // returns ''
*/
get(name) {
const index = this.indexOf(name);
return index === -1 ? false : this[index].value;
}
/**
* Return the position of an attribute by name or attribute object in the current {@link AttributeList}.
* @param {Array|Object|RegExp|String} name - The attribute to locate.
* @returns {Number} - The index of the attribute or -1.
* @example <caption>Return the index of "id".</caption>
* attrs.indexOf('id')
* @example <caption>Return the index of /d$/.</caption>
* attrs.indexOf(/d$/i)
* @example <caption>Return the index of "foo" with a value of "bar".</caption>
* attrs.indexOf({ foo: 'bar' })
* @example <caption>Return the index of "ariaLabel" or "aria-label" matching /^open/.</caption>
* attrs.indexOf({ ariaLabel: /^open/ })
* @example <caption>Return the index of an attribute whose name matches `/^foo/`.</caption>
* attrs.indexOf([{ name: /^foo/ })
*/
indexOf(name) {
for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
args[_key3 - 1] = arguments[_key3];
}
return this.findIndex(Array.isArray(name) ? findIndexByArray : isRegExp(name) ? findIndexByRegExp : name === Object(name) ? findIndexByObject : findIndexByString);
function findIndexByArray(attr) {
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));
}
function findIndexByObject(attr) {
const innerAttr = name[attr.name] || name[toCamelCaseString(attr.name)];
return innerAttr ? isRegExp(innerAttr) ? innerAttr.test(attr.value) : attr.value === innerAttr : false;
}
function findIndexByRegExp(attr) {
return name.test(attr.name) && (args.length ? isRegExp(args[0]) ? args[0].test(attr.value) : attr.value === getAttributeValue(args[0]) : true);
}
function findIndexByString(attr) {
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);
}
}
/**
* Remove an attribute or attributes from the current {@link AttributeList}.
* @param {Array|Object|RegExp|String} name - The attribute to remove.
* @param {String} [value] - The value of the attribute being removed.
* @returns {Boolean} - Whether the attribute or attributes were removed from the {@link AttributeList}.
* @example <caption>Remove the "id" attribute.</caption>
* attrs.remove('id')
* @example <caption>Remove the "id" attribute when it has a value of "bar".</caption>
* attrs.remove('id', 'bar')
* @example
* attrs.remove({ id: 'bar' })
* @example
* attrs.remove([{ name: 'id', value: 'bar' }])
* @example <caption>Remove the "id" and "class" attributes.</caption>
* attrs.remove(['id', 'class'])
*/
remove(name) {
for (var _len4 = arguments.length, args = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
args[_key4 - 1] = arguments[_key4];
}
return toggle(this, getAttributeListArray(name, ...args), false).attributeRemoved;
}
/**
* Toggle an attribute or attributes from the current {@link AttributeList}.
* @param {String|Object} name_or_attrs - The name of the attribute being toggled, or an object of attributes being toggled.
* @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).
* @param {Boolean} [force] - Whether attributes should be exclusively added (true) or removed (false).
* @returns {Boolean} - Whether any attribute was added to the current {@link AttributeList}.
* @example <caption>Toggle the "id" attribute.</caption>
* attrs.toggle('id')
* @example <caption>Toggle the "id" attribute with a value of "bar".</caption>
* attrs.toggle('id', 'bar')
* @example
* attrs.toggle({ id: 'bar' })
* @example
* attrs.toggle([{ name: 'id', value: 'bar' }])
*/
toggle(name) {
for (var _len5 = arguments.length, args = new Array(_len5 > 1 ? _len5 - 1 : 0), _key5 = 1; _key5 < _len5; _key5++) {
args[_key5 - 1] = arguments[_key5];
}
const attrs = getAttributeListArray(name, ...args);
const force = name === Object(name) ? args[0] == null ? null : Boolean(args[0]) : args[1] == null ? null : Boolean(args[1]);
const result = toggle(this, attrs, force);
return result.attributeAdded || result.atttributeModified;
}
/**
* Return the current {@link AttributeList} as a String.
* @returns {String} A string version of the current {@link AttributeList}
* @example
* attrs.toString() // returns 'class="foo" data-foo="bar"'
*/
toString() {
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('') : '';
}
/**
* Return the current {@link AttributeList} as an Object.
* @returns {Object} point - An object version of the current {@link AttributeList}
* @example
* attrs.toJSON() // returns { class: 'foo', dataFoo: 'bar' } when <x class="foo" data-foo: "bar" />
*/
toJSON() {
return this.reduce((object, attr) => Object.assign(object, {
[toCamelCaseString(attr.name)]: attr.value
}), {});
}
/**
* Return a new {@link AttributeList} from an array or object.
* @param {Array|AttributeList|Object} nodes - An array or object of attributes.
* @returns {AttributeList} A new {@link AttributeList}
* @example <caption>Return an array of attributes from a regular object.</caption>
* AttributeList.from({ dataFoo: 'bar' }) // returns AttributeList [{ name: 'data-foo', value: 'bar' }]
* @example <caption>Return a normalized array of attributes from an impure array of attributes.</caption>
* AttributeList.from([{ name: 'data-foo', value: true, foo: 'bar' }]) // returns AttributeList [{ name: 'data-foo', value: 'true' }]
*/
static from(attrs) {
return new AttributeList(getAttributeListArray(attrs));
}
}
/**
* Toggle an attribute or attributes from an {@link AttributeList}.
* @param {AttributeList} attrs - The {@link AttributeList} being modified.
* @param {String|Object} toggles - The attributes being toggled.
* @param {Boolean} [force] - Whether attributes should be exclusively added (true) or removed (false)
* @returns {Object} An object specifying whether any attributes were added, removed, and/or modified.
* @private
*/
function toggle(attrs, toggles, force) {
let attributeAdded = false;
let attributeRemoved = false;
let atttributeModified = false;
toggles.forEach(toggleAttr => {
const index = attrs.indexOf(toggleAttr.name);
if (index === -1) {
if (force !== false) {
// add the attribute (if not exclusively removing attributes)
attrs.push(toggleAttr);
attributeAdded = true;
}
} else if (force !== true) {
// remove the attribute (if not exclusively adding attributes)
attrs.splice(index, 1);
attributeRemoved = true;
} else if (toggleAttr.value !== undefined && attrs[index].value !== toggleAttr.value) {
// change the value of the attribute (if exclusively adding attributes)
attrs[index].value = toggleAttr.value;
atttributeModified = true;
}
});
return {
attributeAdded,
attributeRemoved,
atttributeModified
};
}
/**
* Return an AttributeList-compatible array from an array or object.
* @private
*/
function getAttributeListArray(attrs, value) {
return attrs === null || attrs === undefined // void values are omitted
? [] : Array.isArray(attrs) // arrays are sanitized as a name or value, and then optionally a source
? attrs.reduce((attrs, rawattr) => {
const attr = {};
if ('name' in Object(rawattr)) {
attr.name = String(rawattr.name);
}
if ('value' in Object(rawattr)) {
attr.value = getAttributeValue(rawattr.value);
}
if ('source' in Object(rawattr)) {
attr.source = rawattr.source;
}
if ('name' in attr || 'value' in attr) {
attrs.push(attr);
}
return attrs;
}, []) : attrs === Object(attrs) // objects are sanitized as a name and value
? Object.keys(attrs).map(name => ({
name: toKebabCaseString(name),
value: getAttributeValue(attrs[name])
})) : 1 in arguments // both name and value arguments are sanitized as a name and value
? [{
name: attrs,
value: getAttributeValue(value)
}] // one name argument is sanitized as a name
: [{
name: attrs
}];
}
/**
* Return a value transformed into an attribute value.
* @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.
* @example <caption>Expected values.</caption>
* getAttributeValue('foo') // returns 'foo'
* getAttributeValue('') // returns ''
* @example <caption>Unexpected values.</caption>
* getAttributeValue(null) // returns null
* getAttributeValue(undefined) // returns ''
* getAttributeValue(['foo']) // returns '["foo"]'
* getAttributeValue({ toString() { return 'bar' }}) // returns 'bar'
* getAttributeValue({ toString: 'bar' }) // returns '{"toString":"bar"}'
* @private
*/
function getAttributeValue(value) {
return value === null ? null : value === undefined ? '' : value === Object(value) ? value.toString === Object.prototype.toString ? JSON.stringify(value) : String(value) : String(value);
}
/**
* Return a string formatted using camelCasing.
* @param {String} value - The value being formatted.
* @example
* toCamelCaseString('hello-world') // returns 'helloWorld'
* @private
*/
function toCamelCaseString(value) {
return isKebabCase(value) ? String(value).replace(/-[a-z]/g, $0 => $0.slice(1).toUpperCase()) : String(value);
}
/**
* Return a string formatted using kebab-casing.
* @param {String} value - The value being formatted.
* @description Expected values do not already contain dashes.
* @example <caption>Expected values.</caption>
* toKebabCaseString('helloWorld') // returns 'hello-world'
* toKebabCaseString('helloworld') // returns 'helloworld'
* @example <caption>Unexpected values.</caption>
* toKebabCaseString('hello-World') // returns 'hello-World'
* @private
*/
function toKebabCaseString(value) {
return isCamelCase(value) ? String(value).replace(/[A-Z]/g, $0 => "-" + $0.toLowerCase()) : String(value);
}
/**
* Return whether a value is formatted camelCase.
* @example
* isCamelCase('helloWorld') // returns true
* isCamelCase('hello-world') // returns false
* isCamelCase('helloworld') // returns false
* @private
*/
function isCamelCase(value) {
return /^\w+[A-Z]\w*$/.test(value);
}
/**
* Return whether a value is formatted kebab-case.
* @example
* isKebabCase('hello-world') // returns true
* isKebabCase('helloworld') // returns false
* isKebabCase('helloWorld') // returns false
* @private
*/
function isKebabCase(value) {
return /^\w+[-]\w+$/.test(value);
}
/**
* Return whether a value is a Regular Expression.
* @example
* isRegExp(/hello-world/) // returns true
* isRegExp('/hello-world/') // returns false
* isRegExp(new RegExp('hello-world')) // returns true
* @private
*/
function isRegExp(value) {
return Object.prototype.toString.call(value) === '[object RegExp]';
}
/**
* Transform a {@link Node} and any descendants using visitors.
* @param {Node} node - The {@link Node} to be visited.
* @param {Result} result - The {@link Result} to be used by visitors.
* @param {Object} [overrideVisitors] - Alternative visitors to be used in place of {@link Result} visitors.
* @returns {ResultPromise}
* @private
*/
function visit(node, result, overrideVisitors) {
// get visitors as an object
const visitors = Object(overrideVisitors || Object(result).visitors); // get node types
const beforeType = getTypeFromNode(node);
const beforeSubType = getSubTypeFromNode(node);
const beforeNodeType = 'Node';
const beforeRootType = 'Root';
const afterType = "after" + beforeType;
const afterSubType = "after" + beforeSubType;
const afterNodeType = 'afterNode';
const afterRootType = 'afterRoot';
let promise = Promise.resolve(); // fire "before" visitors
if (visitors[beforeNodeType]) {
promise = promise.then(() => runAll(visitors[beforeNodeType], node, result));
}
if (visitors[beforeType]) {
promise = promise.then(() => runAll(visitors[beforeType], node, result));
}
if (beforeSubType !== beforeType && visitors[beforeSubType]) {
promise = promise.then(() => runAll(visitors[beforeSubType], node, result));
} // dispatch before root event
if (visitors[beforeRootType] && node === result.root) {
promise = promise.then(() => runAll(visitors[beforeRootType], node, result));
} // walk children
if (Array.isArray(node.nodes)) {
node.nodes.slice(0).forEach(childNode => {
promise = promise.then(() => childNode.parent === node && visit(childNode, result, overrideVisitors));
});
} // fire "after" visitors
if (visitors[afterNodeType]) {
promise = promise.then(() => runAll(visitors[afterNodeType], node, result));
}
if (visitors[afterType]) {
promise = promise.then(() => runAll(visitors[afterType], node, result));
}
if (afterType !== afterSubType && visitors[afterSubType]) {
promise = promise.then(() => runAll(visitors[afterSubType], node, result));
} // dispatch root event
if (visitors[afterRootType] && node === result.root) {
promise = promise.then(() => runAll(visitors[afterRootType], node, result));
}
return promise.then(() => result);
}
function runAll(plugins, node, result) {
let promise = Promise.resolve();
[].concat(plugins || []).forEach(plugin => {
// run the current plugin
promise = promise.then(() => {
// update the current plugin
result.currentPlugin = plugin;
return plugin(node, result);
}).then(() => {
// clear the current plugin
result.currentPlugin = null;
});
});
return promise;
} // return normalized plugins and visitors
function getVisitors(rawplugins) {
const visitors = {}; // initialize plugins and visitors
[].concat(rawplugins || []).forEach(plugin => {
const initializedPlugin = Object(plugin).type === 'plugin' ? plugin() : plugin;
if (initializedPlugin instanceof Function) {
if (!visitors.afterRoot) {
visitors.afterRoot = [];
}
visitors.afterRoot.push(initializedPlugin);
} else if (Object(initializedPlugin) === initializedPlugin && Object.keys(initializedPlugin).length) {
Object.keys(initializedPlugin).forEach(key => {
const fn = initializedPlugin[key];
if (fn instanceof Function) {
if (!visitors[key]) {
visitors[key] = [];
}
visitors[key].push(initializedPlugin[key]);
}
});
}
});
return visitors;
}
function getTypeFromNode(node) {
return {
'comment': 'Comment',
'text': 'Text',
'doctype': 'Doctype',
'fragment': 'Fragment'
}[node.type] || 'Element';
}
function getSubTypeFromNode(node) {
return {
'comment': 'Comment',
'text': 'Text',
'doctype': 'Doctype',
'fragment': 'Fragment'
}[node.type] || (!node.name ? 'FragmentElement' : "" + node.name[0].toUpperCase() + node.name.slice(1) + "Element");
}
/**
* @name Node
* @class
* @extends Node
* @classdesc Create a new {@link Node}.
* @returns {Node}
*/
class Node {
/**
* The position of the current {@link Node} from its parent.
* @returns {Number}
* @example
* node.index // returns the index of the node or -1
*/
get index() {
if (this.parent === Object(this.parent) && this.parent.nodes && this.parent.nodes.length) {
return this.parent.nodes.indexOf(this);
}
return -1;
}
/**
* The next {@link Node} after the current {@link Node}, or `null` if there is none.
* @returns {Node|Null} - The next Node or null.
* @example
* node.next // returns null
*/
get next() {
const index = this.index;
if (index !== -1) {
return this.parent.nodes[index + 1] || null;
}
return null;
}
/**
* The next {@link Element} after the current {@link Node}, or `null` if there is none.
* @returns {Element|Null}
* @example
* node.nextElement // returns an element or null
*/
get nextElement() {
const index = this.index;
if (index !== -1) {
return this.parent.nodes.slice(index).find(hasNodes);
}
return null;
}
/**
* The previous {@link Node} before the current {@link Node}, or `null` if there is none.
* @returns {Node|Null}
* @example
* node.previous // returns a node or null
*/
get previous() {
const index = this.index;
if (index !== -1) {
return this.parent.nodes[index - 1] || null;
}
return null;
}
/**
* The previous {@link Element} before the current {@link Node}, or `null` if there is none.
* @returns {Element|Null}
* @example
* node.previousElement // returns an element or null
*/
get previousElement() {
const index = this.index;
if (index !== -1) {
return this.parent.nodes.slice(0, index).reverse().find(hasNodes);
}
return null;
}
/**
* The top-most ancestor from the current {@link Node}.
* @returns {Node}
* @example
* node.root // returns the top-most node or the current node itself
*/
get root() {
let parent = this;
while (parent.parent) {
parent = parent.parent;
}
return parent;
}
/**
* Insert one ore more {@link Node}s after the current {@link Node}.
* @param {...Node|String} nodes - Any nodes to be inserted after the current {@link Node}.
* @returns {Node} - The current {@link Node}.
* @example
* node.after(new Text({ data: 'Hello World' }))
*/
after() {
for (var _len = arguments.length, nodes = new Array(_len), _key = 0; _key < _len; _key++) {
nodes[_key] = arguments[_key];
}
if (nodes.length) {
const index = this.index;
if (index !== -1) {
this.parent.nodes.splice(index + 1, 0, ...nodes);
}
}
return this;
}
/**
* Append Nodes or new Text Nodes to the current {@link Node}.
* @param {...Node|String} nodes - Any nodes to be inserted after the last child of the current {@link Node}.
* @returns {Node} - The current {@link Node}.
* @example
* node.append(someOtherNode)
*/
append() {
if (this.nodes) {
for (var _len2 = arguments.length, nodes = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
nodes[_key2] = arguments[_key2];
}
this.nodes.splice(this.nodes.length, 0, ...nodes);
}
return this;
}
/**
* Append the current {@link Node} to another Node.
* @param {Container} parent - The {@link Container} for the current {@link Node}.
* @returns {Node} - The current {@link Node}.
*/
appendTo(parent) {
if (parent && parent.nodes) {
parent.nodes.splice(parent.nodes.length, 0, this);
}
return this;
}
/**
* Insert Nodes or new Text Nodes before the Node if it has a parent.
* @param {...Node|String} nodes - Any nodes to be inserted before the current {@link Node}.
* @returns {Node}
* @example
* node.before(new Text({ data: 'Hello World' })) // returns the current node
*/
before() {
for (var _len3 = arguments.length, nodes = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
nodes[_key3] = arguments[_key3];
}
if (nodes.length) {
const index = this.index;
if (index !== -1) {
this.parent.nodes.splice(index, 0, ...nodes);
}
}
return this;
}
/**
* Prepend Nodes or new Text Nodes to the current {@link Node}.
* @param {...Node|String} nodes - Any nodes inserted before the first child of the current {@link Node}.
* @returns {Node} - The current {@link Node}.
* @example
* node.prepend(someOtherNode)
*/
prepend() {
if (this.nodes) {
for (var _len4 = arguments.length, nodes = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
nodes[_key4] = arguments[_key4];
}
this.nodes.splice(0, 0, ...nodes);
}
return this;
}
/**
* Remove the current {@link Node} from its parent.
* @returns {Node}
* @example
* node.remove() // returns the current node
*/
remove() {
const index = this.index;
if (index !== -1) {
this.parent.nodes.splice(index, 1);
}
return this;
}
/**
* Replace the current {@link Node} with another Node or Nodes.
* @param {...Node} nodes - Any nodes replacing the current {@link Node}.
* @returns {Node} - The current {@link Node}
* @example
* node.replaceWith(someOtherNode) // returns the current node
*/
replaceWith() {
const index = this.index;
if (index !== -1) {
for (var _len5 = arguments.length, nodes = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
nodes[_key5] = arguments[_key5];
}
this.parent.nodes.splice(index, 1, ...nodes);
}
return this;
}
/**
* Transform the current {@link Node} and any descendants using visitors.
* @param {Result} result - The {@link Result} to be used by visitors.
* @param {Object} [overrideVisitors] - Alternative visitors to be used in place of {@link Result} visitors.
* @returns {ResultPromise}
* @example
* await node.visit(result)
* @example
* await node.visit() // visit using the result of the current node
* @example
* await node.visit(result, {
* Element () {
* // do something to an element
* }
* })
*/
visit(result, overrideVisitors) {
const resultToUse = 0 in arguments ? result : this.result;
return visit(this, resultToUse, overrideVisitors);
}
/**
* Add a warning from the current {@link Node}.
* @param {Result} result - The {@link Result} the warning is being added to.
* @param {String} text - The message being sent as the warning.
* @param {Object} [opts] - Additional information about the warning.
* @example
* node.warn(result, 'Something went wrong')
* @example
* node.warn(result, 'Something went wrong', {
* node: someOtherNode,
* plugin: someOtherPlugin
* })
*/
warn(result, text, opts) {
const data = Object.assign({
node: this
}, opts);
return result.warn(text, data);
}
}
function hasNodes(node) {
return node.nodes;
}
/**
* @name Comment
* @class
* @extends Node
* @classdesc Return a new {@link Comment} {@link Node}.
* @param {Object|String} settings - Custom settings applied to the Comment, or the content of the {@link Comment}.
* @param {String} settings.comment - Content of the Comment.
* @param {Object} settings.source - Source mapping of the Comment.
* @returns {Comment}
* @example
* new Comment({ comment: ' Hello World ' })
*/
class Comment extends Node {
constructor(settings) {
super();
if (typeof settings === 'string') {
settings = {
comment: settings
};
}
Object.assign(this, {
type: 'comment',
name: '#comment',
comment: String(Object(settings).comment || ''),
source: Object(Object(settings).source)
});
}
/**
* Return the stringified innerHTML of the current {@link Comment}.
* @returns {String}
* @example
* attrs.innerHTML // returns ' Hello World '
*/
get innerHTML() {
return String(this.comment);
}
/**
* Return the stringified outerHTML of the current {@link Comment}.
* @returns {String}
* @example
* attrs.outerHTML // returns '<!-- Hello World -->'
*/
get outerHTML() {
return String(this);
}
/**
* Return the stringified innerHTML from the source input.
* @returns {String}
* @example
* attrs.sourceInnerHTML // returns ' Hello World '
*/
get sourceInnerHTML() {
return typeof Object(this.source.input).html !== 'string' ? '' : this.source.input.html.slice(this.source.startOffset + 4, this.source.endOffset - 3);
}
/**
* Return the stringified outerHTML from the source input.
* @returns {String}
* @example
* attrs.sourceOuterHTML // returns '<!-- Hello World -->'
*/
get sourceOuterHTML() {
return typeof Object(this.source.input).html !== 'string' ? '' : this.source.input.html.slice(this.source.startOffset, this.source.endOffset);
}
/**
* Return a clone of the current {@link Comment}.
* @param {Object} settings - Custom settings applied to the cloned {@link Comment}.
* @returns {Comment} - The cloned {@link Comment}
* @example
* comment.clone()
* @example <caption>Clone the current text with new source.</caption>
* comment.clone({ source: { input: 'modified source' } })
*/
clone(settings) {
return new this.constructor(Object.assign({}, this, settings, {
source: Object.assign({}, this.source, Object(settings).source)
}));
}
/**
* Return the current {@link Comment} as a String.
* @returns {String} A string version of the current {@link Comment}
* @example
* attrs.toJSON() // returns '<!-- Hello World -->'
*/
toString() {
return "<!--" + this.comment + "-->";
}
/**
* Return the current {@link Comment} as a Object.
* @returns {Object} An object version of the current {@link Comment}
* @example
* attrs.toJSON() // returns { comment: ' Hello World ' }
*/
toJSON() {
return {
comment: this.comment
};
}
}
/**
* @name Container
* @class
* @extends Node
* @classdesc Return a new {@link Container} {@link Node}.
* @returns {Container}
*/
class Container extends Node {
/**
* Return the first child {@link Node} of the current {@link Container}, or `null` if there is none.
* @returns {Node|Null}
* @example
* container.first // returns a Node or null
*/
get first() {
return this.nodes[0] || null;
}
/**
* Return the first child {@link Element} of the current {@link Container}, or `null` if there is none.
* @returns {Node|Null}
* @example
* container.firstElement // returns an Element or null
*/
get firstElement() {
return this.nodes.find(hasNodes$1) || null;
}
/**
* Return the last child {@link Node} of the current {@link Container}, or `null` if there is none.
* @returns {Node|Null}
* @example
* container.last // returns a Node or null
*/
get last() {
return this.nodes[this.nodes.length - 1] || null;
}
/**
* Return the last child {@link Element} of the current {@link Container}, or `null` if there is none.
* @returns {Node|Null}
* @example
* container.lastElement // returns an Element or null
*/
get lastElement() {
return this.nodes.slice().reverse().find(hasNodes$1) || null;
}
/**
* Return a child {@link Element} {@link NodeList} of the current {@link Container}.
* @returns {Array}
* @example
* container.elements // returns an array of Elements
*/
get elements() {
return this.nodes.filter(hasNodes$1) || [];
}
/**
* Return the innerHTML of the current {@link Container} as a String.
* @returns {String}
* @example
* container.innerHTML // returns a string of innerHTML
*/
get innerHTML() {
return this.nodes.innerHTML;
}
/**
* Define the nodes of the current {@link Container} from a String.
* @param {String} innerHTML - Source being processed.
* @returns {Void}
* @example
* container.innerHTML = 'Hello <strong>world</strong>';
* container.nodes.length; // 2
*/
set innerHTML(innerHTML) {
this.nodes.innerHTML = innerHTML;
}
/**
* Return the outerHTML of the current {@link Container} as a String.
* @returns {String}
* @example
* container.outerHTML // returns a string of outerHTML
*/
get outerHTML() {
return this.nodes.innerHTML;
}
/**
* Replace the current {@link Container} from a String.
* @param {String} input - Source being processed.
* @returns {Void}
* @example
* container.outerHTML = 'Hello <strong>world</strong>';
*/
set outerHTML(outerHTML) {
const Result = Object(this.result).constructor;
if (Result) {
const childNodes = new Result(outerHTML).root.nodes;
this.replaceWith(...childNodes);
}
}
/**
* Return the stringified innerHTML from the source input.
* @returns {String}
*/
get sourceInnerHTML() {
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;
}
/**
* Return the stringified outerHTML from the source input.
* @returns {String}
*/
get sourceOuterHTML() {
return typeof Object(this.source.input).html !== 'string' ? '' : this.source.input.html.slice(this.source.startOffset, this.source.endOffset);
}
/**
* Return the text content of the current {@link Container} as a String.
* @returns {String}
*/
get textContent() {
return this.nodes.textContent;
}
/**
* Define the content of the current {@link Container} as a new {@link Text} {@link Node}.
* @returns {String}
*/
set textContent(textContent) {
this.nodes.textContent = textContent;
}
/**
* Return a child {@link Node} of the current {@link Container} by last index, or `null` if there is none.
* @returns {Node|Null}
* @example
* container.lastNth(0) // returns a Node or null
*/
lastNth(index) {
return this.nodes.slice().reverse()[index] || null;
}
/**
* Return a child {@link Element} of the current {@link Container} by last index, or `null` if there is none.
* @returns {Element|Null}
* @example
* container.lastNthElement(0) // returns an Element or null
*/
lastNthElement(index) {
return this.elements.reverse()[index] || null;
}
/**
* Return a child {@link Node} of the current {@link Container} by index, or `null` if there is none.
* @returns {Node|Null}
* @example
* container.nth(0) // returns a Node or null
*/
nth(index) {
return this.nodes[index] || null;
}
/**
* Return an {@link Element} child of the current Container by index, or `null` if there is none.
* @returns {Element|Null}
* @example
* container.nthElement(0) // returns an Element or null
*/
nthElement(index) {
return this.elements[index] || null;
}
/**
* Replace all of the children of the current {@link Container}.
* @param {...Node} nodes - Any nodes replacing the current children of the {@link Container}.
* @returns {Container} - The current {@link Container}.
* @example
* container.replaceAll(new Text({ data: 'Hello World' }))
*/
replaceAll() {
if (this.nodes) {
for (var _len = arguments.length, nodes = new Array(_len), _key = 0; _key < _len; _key++) {
nodes[_key] = arguments[_key];
}
this.nodes.splice(0, this.nodes.length, ...nodes);
}
return this;
}
/**
* Traverse the descendant {@link Node}s of the current {@link Container} with a callback function.
* @param {Function|String|RegExp} callback_or_filter - A callback function, or a filter to reduce {@link Node}s the callback is applied to.
* @param {Function|String|RegExp} callback - A callback function when a filter is also specified.
* @returns {Container} - The current {@link Container}.
* @example
* container.walk(node => {
* console.log(node);
* })
* @example
* container.walk('*', node => {
* console.log(node);
* })
* @example <caption>Walk only "section" {@link Element}s.</caption>
* container.walk('section', node => {
* console.log(node); // logs only Section Elements
* })
* @example
* container.walk(/^section$/, node => {
* console.log(node); // logs only Section Elements
* })
* @example
* container.walk(
* node => node.name.toLowerCase() === 'section',
* node => {
* console.log(node); // logs only Section Elements
* })
* @example <caption>Walk only {@link Text}.</caption>
* container.walk('#text', node => {
* console.log(node); // logs only Text Nodes
* })
*/
walk() {
const _getCbAndFilterFromAr = getCbAndFilterFromArgs(arguments),
cb = _getCbAndFilterFromAr[0],
filter = _getCbAndFilterFromAr[1];
walk(this, cb, filter);
return this;
}
}
function walk(node, cb, filter) {
if (typeof cb === 'function' && node.nodes) {
node.nodes.slice(0).forEach(child => {
if (Object(child).parent === node) {
if (testWithFilter(child, filter)) {
cb(child); // eslint-disable-line callback-return
}
if (child.nodes) {
walk(child, cb, filter);
}
}
});
}
}
function getCbAndFilterFromArgs(args) {
const cbOrFilter = args[0],
onlyCb = args[1];
const cb = onlyCb || cbOrFilter;
const filter = onlyCb ? cbOrFilter : undefined;
return [cb, filter];
}
function testWithFilter(node, filter) {
if (!filter) {
return true;
} else if (filter === '*') {
return Object(node).constructor.name === 'Element';
} else if (typeof filter === 'string') {
return node.name === filter;
} else if (filter instanceof RegExp) {
return filter.test(node.name);
} else if (filter instanceof Function) {
return filter(node);
} else {
return false;
}
}
function hasNodes$1(node) {
return node.nodes;
}
/**
* @name Doctype
* @class
* @extends Node
* @classdesc Create a new {@link Doctype} {@link Node}.
* @param {Object|String} settings - Custom settings applied to the {@link Doctype}, or the name of the {@link Doctype}.
* @param {String} settings.name - Name of the {@link Doctype}.
* @param {String} settings.publicId - Public identifier portion of the {@link Doctype}.
* @param {String} settings.systemId - System identifier portion of the {@link Doctype}.
* @param {Object} settings.source - Source mapping of the {@link Doctype}.
* @returns {Doctype}
* @example
* new Doctype({ name: 'html' }) // <!doctype html>
*/
class Doctype extends Node {
constructor(settings) {
super();
if (typeof settings === 'string') {
settings = {
name: settings
};
}
Object.assign(this, {
type: 'doctype',
doctype: String(Object(settings).doctype || 'doctype'),
name: String(Object(settings).name || 'html'),
publicId: Object(settings).publicId || null,
systemId: Object(settings).systemId || null,
source: Object.assign({
before: Object(Object(settings).source).before || ' ',
after: Object(Object(settings).source).after || '',
beforePublicId: Object(Object(settings).source).beforePublicId || null,
beforeSystemId: Object(Object(settings).source).beforeSystemId || null
}, Object(settings).source)
});
}
/**
* Return a clone of the current {@link Doctype}.
* @param {Object} settings - Custom settings applied to the cloned {@link Doctype}.
* @returns {Doctype} - The cloned {@link Doctype}
* @example
* doctype.clone()
* @example <caption>Clone the current text with new source.</caption>
* doctype.clone({ source: { input: 'modified source' } })
*/
clone(settings) {
return new this.constructor(Object.assign({}, this, settings, {
source: Object.assign({}, this.source, Object(settings).source)
}));
}
/**
* Return the current {@link Doctype} as a String.
* @returns {String}
*/
toString() {
const publicId = this.publicId ? "" + (this.source.beforePublicId || ' ') + this.publicId : '';
const systemId = this.systemId ? "" + (this.source.beforeSystemId || ' ') + this.systemId : '';
return "<!" + this.doctype + this.source.before + this.name + this.source.after + publicId + systemId + ">";
}
/**
* Return the current {@link Doctype} as an Object.
* @returns {Object}
*/
toJSON() {
return {
name: this.name,
publicId: this.publicId,
systemId: this.systemId
};
}
}
/**
* @name Fragment
* @class
* @extends Container
* @classdesc Create a new {@link Fragment} {@Link Node}.
* @param {Object} settings - Custom settings applied to the {@link Fragment}.
* @param {Array|NodeList} settings.nodes - Nodes appended to the {@link Fragment}.
* @param {Object} settings.source - Source mapping of the {@link Fragment}.
* @returns {Fragment}
* @example
* new Fragment() // returns an empty fragment
*
* new Fragment({ nodes: [ new Element('span') ] }) // returns a fragment with a <span>
*/
class Fragment extends Container {
constructor(settings) {
super();
Object.assign(this, settings, {
type: 'fragment',
name: '#document-fragment',
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),
source: Object(Object(settings).source)
});
}
/**
* Return a clone of the current {@link Fragment}.
* @param {Boolean} isDeep - Whether the descendants of the current Fragment should also be cloned.
* @returns {Fragment} - The cloned Fragment
*/
clone(isDeep) {
const clone = new Fragment(Object.assign({}, this, {
nodes: []
}));
if (isDeep) {
clone.nodes = this.nodes.clone(clone);
}
return clone;
}
/**
* Return the current {@link Fragment} as an Array.
* @returns {Array}
* @example
* fragment.toJSON() // returns []
*/
toJSON() {
return this.nodes.toJSON();
}
/**
* Return the current {@link Fragment} as a String.
* @returns {String}
* @example
* fragment.toJSON() // returns ''
*/
toString() {
return String(this.nodes);
}
}
/**
* @name Text
* @class
* @extends Node
* @classdesc Create a new {@link Text} {@link Node}.
* @param {Object|String} settings - Custom settings applied to the {@link Text}, or the content of the {@link Text}.
* @param {String} settings.data - Content of the {@link Text}.
* @param {Object} settings.source - Source mapping of the {@link Text}.
* @returns {Text}
* @example
* new Text({ data: 'Hello World' })
*/
class Text extends Node {
constructor(settings) {
super();
if (typeof settings === 'string') {
settings = {
data: settings
};
}
Object.assign(this, {
type: 'text',
name: '#text',
data: String(Object(settings).data || ''),
source: Object(Object(settings).source)
});
}
/**
* Return the stringified innerHTML from the source input.
* @returns {String}
*/
get sourceInnerHTML() {
return typeof Object(this.source.input).html !== 'string' ? '' : this.source.input.html.slice(this.source.startOffset, this.source.endOffset);
}
/**
* Return the stringified outerHTML from the source input.
* @returns {String}
*/
get sourceOuterHTML() {
return typeof Object(this.source.input).html !== 'string' ? '' : this.source.input.html.slice(this.source.startOffset, this.source.endOffset);
}
/**
* Return the current {@link Text} as a String.
* @returns {String}
* @example
* text.textContent // returns ''
*/
get textContent() {
return String(this.data);
}
/**
* Define the current {@link Text} from a String.
* @returns {Void}
* @example
* text.textContent = 'Hello World'
* text.textContent // 'Hello World'
*/
set textContent(textContent) {
this.data = String(textContent);
}
/**
* Return a clone of the current {@link Text}.
* @param {Object} settings - Custom settings applied to the cloned {@link Text}.
* @returns {Text} - The cloned {@link Text}
* @example
* text.clone()
* @example <caption>Clone the current text with new source.</caption>
* text.clone({ source: { input: 'modified source' } })
*/
clone(settings) {
return new Text(Object.assign({}, this, settings, {
source: Object.assign({}, this.source, Object(settings).source)
}));
}
/**
* Return the current {@link Text} as a String.
* @returns {String}
* @example
* text.toString() // returns ''
*/
toString() {
return String(this.data);
}
/**
* Return the current {@link Text} as a String.
* @returns {String}
* @example
* text.toJSON() // returns ''
*/
toJSON() {
return String(this.data);
}
}
function normalize(node) {
const nodeTypes = {
comment: Comment,
doctype: Doctype,
element: Element,
fragment: Fragment,
text: Text
};
return node instanceof Node // Nodes are unchanged
? node : node.type in nodeTypes // Strings are converted into Text nodes
? new nodeTypes[node.type](node) // Node-like Objects with recognized types are normalized
: new Text({
data: String(node)
});
}
const parents = new WeakMap();
/**
* @name NodeList
* @class
* @extends Array
* @classdesc Create a new {@link NodeList}.
* @param {Container} parent - Parent containing the current {@link NodeList}.
* @param {...Node} nodes - {@link Node}s belonging to the current {@link NodeList}.
* @returns {NodeList}
*/
class NodeList extends Array {
constructor(parent) {
super();
parents.set(this, parent);
for (var _len = arguments.length, nodes = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
nodes[_key - 1] = arguments[_key];
}
if (nodes.length) {
this.push(...nodes);
}
}
/**
* Return the innerHTML of the current {@link Container} as a String.
* @returns {String}
* @example
* container.innerHTML // returns a string of innerHTML
*/
get innerHTML() {
return this.map(node => node.type === 'text' ? getInnerHtmlEncodedString(node.data) : 'outerHTML' in node ? node.outerHTML : String(node)).join('');
}
/**
* Define the nodes of the current {@link NodeList} from a String.
* @param {String} innerHTML - Source being processed.
* @returns {Void}
* @example
* nodeList.innerHTML = 'Hello <strong>world</strong>';
* nodeList.length; // 2
*/
set innerHTML(innerHTML) {
const parent = this.parent;
const Result = Object(parent.result).constructor;
if (Result) {
const nodes = new Result(innerHTML).root.nodes;
this.splice(0, this.length, ...nodes);
}
}
/**
* Return the parent of the current {@link NodeList}.
* @returns {Container}
*/
get parent() {
return parents.get(this);
}
/**
* Return the text content of the current {@link NodeList} as a String.
* @returns {String}
*/
get textContent() {
return this.map(node => Object(node).textContent || '').join('');
}
/**
* Define the content of the current {@link NodeList} as a new {@link Text} {@link Node}.
* @returns {String}
*/
set textContent(textContent) {
this.splice(0, this.length, new Text({
data: textContent
}));
}
/**
* Return a clone of the current {@link NodeList}.
* @param {Object} parent - New parent containing the cloned {@link NodeList}.
* @returns {NodeList} - The cloned NodeList
*/
clone(parent) {
return new NodeList(parent, ...this.map(node => node.clone({}, true)));
}
/**
* Remove and return the last {@link Node} in the {@link NodeList}.
* @returns {Node}
*/
pop() {
const _this$splice = this.splice(this.length - 1, 1),
remove = _this$splice[0];
return remove;
}
/**
* Add {@link Node}s to the end of the {@link NodeList} and return the new length of the {@link NodeList}.
* @returns {Number}
*/
push() {
const parent = this.parent;
for (var _len2 = arguments.length, nodes = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
nodes[_key2] = arguments[_key2];
}
const inserts = nodes.filter(node => node !== parent);
this.splice(this.length, 0, ...inserts);
return this.length;
}
/**
* Remove and return the first {@link Node} in the {@link NodeList}.
* @returns {Node}
*/
shift() {
const _this$splice2 = this.splice(0, 1),
remove = _this$splice2[0];
return remove;
}
/**
* Add and remove {@link Node}s to and from the {@link NodeList}.
* @returns {Array}
*/
splice(start) {
const length = this.length,
parent = this.parent;
const startIndex = start > length ? length : start < 0 ? Math.max(length + start, 0) : Number(start) || 0;
for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
args[_key3 - 1] = arguments[_key3];
}
const deleteCount = 0 in args ? Number(args[0]) || 0 : length;
const inserts = getNodeListArray(args.slice(1).filter(node => node !== parent));
for (let _i = 0, _length = inserts == null ? 0 : inserts.length; _i < _length; _i++) {
let insert = inserts[_i];
insert.remove();
insert.parent = parent;
}
const removes = Array.prototype.splice.call(this, startIndex, deleteCount, ...inserts);
for (let _i2 = 0, _length2 = removes == null ? 0 : removes.length; _i2 < _length2; _i2++) {
let remove = removes[_i2];
delete remove.parent;
}
return removes;
}
/**
* Add {@link Node}s to the beginning of the {@link NodeList} and return the new length of the {@link NodeList}.
* @returns {Number}
*/
unshift() {
const parent = this.parent;
for (var _len4 = arguments.length, nodes = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
nodes[_key4] = arguments[_key4];
}
const inserts = nodes.filter(node => node !== parent);
this.splice(0, 0, ...inserts);
return this.length;
}
/**
* Return the current {@link NodeList} as a String.
* @returns {String}
* @example
* nodeList.toString() // returns ''
*/
toString() {
return this.join('');
}
/**
* Return the current {@link NodeList} as an Array.
* @returns {Array}
* @example
* nodeList.toJSON() // returns []
*/
toJSON() {
return Array.from(this).map(node => node.toJSON());
}
/**
* Return a new {@link N