Element.js

import AttributeList from './AttributeList';
import Container from './Container';
import NodeList from './NodeList';
import Result from './Result';

/** Return a new {@link Element} {@Link Node}.
* @extends Container
*/
class Element extends Container {
	/**
	* @param {ElementSettings|string} settings - Custom settings applied to the Element or its tag name.
	* @param {Array<Array|AttributeList,NodeList>} [args] - Custom settings applied to the Element or its tag name.
	* @example
	* new Element({ name: 'p' }) // returns an element representing <p></p>
	*
	* new Element({
	*   name: 'input',
	*   attrs: [{ name: 'type', value: 'search' }],
	*   isVoid: true
	* }) // returns an element representing <input type="search">
	* @example
	* new Element('p') // returns an element representing <p></p>
	*
	* new Element('p', null,
	*   new Element(
	*     'input',
	*     [{ name: 'type', value: 'search' }]
	*   )
	* ) // returns an element representing <p><input type="search"></p>
	*/
	constructor (settings, ...args) {
		super();

		if (settings !== Object(settings)) {
			settings = { name: String(settings == null ? 'span' : settings) };
		}

		if (args[0] === Object(args[0])) {
			settings.attrs = args[0];
		}

		if (args.length > 1) {
			settings.nodes = args.slice(1);
		}

		/** Type identifier of the Element
		* @type {'element'} */
		this.type = 'element';

		/** Tag name of the Element
		* @type {string} */
		this.name = settings.name;

		/** Whether the Element is self-closing
		* @type {boolean} */
		this.isSelfClosing = Boolean(settings.isSelfClosing);

		/** Whether the Element is self-closing
		* @type {boolean} */
		this.isVoid = 'isVoid' in settings
			? Boolean(settings.isVoid)
		: Result.voidElements.includes(settings.name);

		/** Whether the Element includes a closing tag
		* @type {boolean} */
		this.isWithoutEndTag = Boolean(settings.isWithoutEndTag);

		/** Attributes applied to the Element
		* @type {AttributeList} */
		this.attrs = AttributeList.from(settings.attrs);

		/** Nodes appended to the Element
		* @type {NodeList} */
		this.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);

		/** Source mapping of the Element
		* @type {ElementSource} */
		this.source = Object(settings.source);

		/** Current result applied to the Element
		* @type {Result} */
		this.result = settings.result;
	}

	/**
	* Return the outerHTML of the current {@link Element} as a String.
	* @returns {string}
	* @example
	* element.outerHTML // returns a string of outerHTML
	*/
	get outerHTML () {
		return `${getOpeningTagString(this)}${this.nodes.innerHTML}${getClosingTagString(this)}`;
	}

	/**
	* Replace the current {@link Element} from a String.
	* @param {string} input - Source being processed.
	* @returns {void}
	* @example
	* element.outerHTML = 'Hello <strong>world</strong>';
	*/
	set outerHTML (outerHTML) {
		Object.getOwnPropertyDescriptor(Container.prototype, 'outerHTML').set.call(this, outerHTML);
	}

	/**
	* Return a clone of the current {@link Element}.
	* @param {ElementSettings} settings - Custom settings applied to the cloned Element.
	* @param {boolean} isDeep - Whether the descendants of the current Element should also be cloned.
	*/
	clone (settings, isDeep) {
		const clone = new Element({
			...this,
			nodes: [],
			...Object(settings)
		});

		const didSetNodes = 'nodes' in Object(settings);

		if (isDeep && !didSetNodes) {
			clone.nodes = this.nodes.clone(clone);
		}

		return clone;
	}

	/**
	* Return the Element as a unique Object.
	* @returns {ElementJSON}
	*/
	toJSON () {
		/** @type {ElementJSON} */
		const object = { name: this.name };

		// conditionally disclose whether the Element is self-closing
		if (this.isSelfClosing) {
			object.isSelfClosing = true;
		}

		// conditionally disclose whether the Element is void
		if (this.isVoid) {
			object.isVoid = true;
		}

		// conditionally disclose Attributes applied to the Element
		if (this.attrs.length) {
			object.attrs = this.attrs.toJSON();
		}

		// conditionally disclose Nodes appended to the Element
		if (!this.isSelfClosing && !this.isVoid && this.nodes.length) {
			object.nodes = this.nodes.toJSON();
		}

		return object;
	}

	/**
	* Return the stringified Element.
	*/
	toString () {
		return `${getOpeningTagString(this)}${this.nodes || ''}${
			`${getClosingTagString(this)}`
		}`;
	}
}

export default Element;

function getClosingTagString (element) {
	return element.isSelfClosing || element.isVoid || element.isWithoutEndTag
		? ''
	: `</${element.name}${element.source.after || ''}>`;
}

function getOpeningTagString (element) {
	return `<${element.name}${element.attrs}${element.source.before || ''}>`;
}

/**
* @typedef {Object} ElementSettings - Custom settings applied to the Element or its tag name.
* @property {string} [name="span"] - Tag name of the Element.
* @property {boolean} [isSelfClosing=false] - Whether the Element is self-closing.
* @property {boolean} [isVoid=false] - Whether the Element is void.
* @property {boolean} [isWithoutEndTag=false] - Whether the Element includes a closing tag.
* @property {Array|AttributeList|Object} [attrs] - Attributes applied to the Element.
* @property {Array|NodeList} [nodes] - Nodes appended to the Element.
* @property {Object} [source] - Source mapping of the Element.
* @property {Result} result - Result applied to the Element.
*
* @typedef {Object} ElementSource - Source mapping of the Element.
* @property {string} before - Raw content before the inner-opening tag of the Element.
* @property {string} after - Raw content before the inner-closing tag of the Element.
*
* @typedef {Object} ElementJSON - JSON representation of the Element.
* @property {string} name - Tag name of the Element.
* @property {boolean} isSelfClosing - Whether the Element is self-closing.
* @property {boolean} isVoid - Whether the Element is void.
* @property {Object.<string,string|null>} attrs - Attributes applied to the Element.
* @property {Array<ElementJSON|string>} nodes - Nodes appended to the Element.
*/