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.
*/