/* eslint-disable no-dupe-class-members */ const libxmljs = require('libxmljs'); const utils = require('./utils'); class Document { static TYPE_XML = 0; static TYPE_HTML = 1; static TYPE_HTML_FRAGMENT = 2; /** * The internal `libxmljs.Document` instance. */ _original = null; /** * Tells whether the `Document` has content. */ _hasContent = false; /** * The object that holds the namespace information. */ namespace = null; /** * Creates a new `Document`. * @param {string} [source] The source as a string. * @param {number} [type] The type of the `Document`. * @param {libxmljs.ParserOptions} [options] The parser options. */ constructor(source = null, type = Document.TYPE_XML, options = null) { if (source !== null) { switch (type) { case Document.TYPE_XML: this._original = libxmljs.parseXml(source, options); break; case Document.TYPE_HTML: this._original = libxmljs.parseHtml(source, options); break; case Document.TYPE_HTML_FRAGMENT: this._original = libxmljs.parseHtmlFragment(source, options); break; default: break; } this._hasContent = true; } else { this._original = new libxmljs.Document(); } } /** * An array containing the errors in the current `Document`. */ get errors() { if (!this._original) return []; return this._original.errors; } /** * An array containing the validation errors in the current `Document`. */ get validationErrors() { if (!this._original) return undefined; return this._original.validationErrors; } /** * Gets the idxth child of the root node. * @param {number} idx The child index. * @returns {libxmljs.Element|null} A `libxmljs.Element` or `null`. */ child(idx) { this.assertNoContentError(); if (!idx) idx = 0; return this._original.child(idx); } /** * Gets all the children of the root node. * @returns {libxmljs.Element[]} An array of `libxmljs.Element`s. */ childNodes() { this.assertNoContentError(); return this._original.childNodes(); } /** * Counts the amount of results of the provided XPath. * @param {string} xpath The XPath to count. * @returns {number} The amount of results. */ count(xpath) { if (typeof xpath !== 'string') throw new Error('xpath must be a string.'); return this.get(`count(${xpath})`); } /** * Gets the `Document`'s encoding. * @returns {string} The `Document`'s encoding. */ encoding() { return this._original.encoding(); } /** * Sets the `Document`'s encoding. * @param {string} enc The encoding as a string. * @returns {Document} The `Document`. */ encoding(enc) { this._original.encoding(enc); return this; } /** * Finds the result of the provided XPath. * @param {string} xpath The XPath to find. * @returns {libxmljs.Element[]} An array of `libxmljs.Element`s. */ find(xpath) { this.assertNoContentError(); if (typeof xpath !== 'string') throw new Error('xpath must be a string.'); if (!this.namespace) return this._original.find(xpath); const fullXPath = utils.addNamespacePrefixInPath(this.namespace.alias, xpath); return this._original.find(fullXPath, { [this.namespace.alias]: this.namespace.url }); } /** * Parses an HTML document. * @param {string} html The HTML content. * @param {libxmljs.ParserOptions} [options] The parser options. * @returns {Document} The `Document`. */ fromHtml(html, options = {}) { if (typeof html !== 'string') throw new Error('html must be a string.'); if (typeof options !== 'object') throw new Error('options must be an object.'); this._original = libxmljs.Document.fromHtml(html, options); this._hasContent = true; return this; } /** * Parses an HTML fragment. * @param {string} htmlFragment The HTML content. * @param {libxmljs.ParserOptions} [options] The parser options. * @returns {Document} The `Document`. */ fromHtmlFragment(htmlFragment, options = {}) { if (typeof htmlFragment !== 'string') throw new Error('htmlFragment must be a string.'); if (typeof options !== 'object') throw new Error('options must be an object.'); this._original = libxmljs.Document.fromHtmlFragment(htmlFragment, options); this._hasContent = true; return this; } /** * Parses an XML document. * @param {string} xml The XML content. * @param {libxmljs.ParserOptions} [options] The parser options. * @returns {Document} The `Document`. */ fromXml(xml, options = {}) { if (typeof xml !== 'string') throw new Error('htmlFragment must be a string.'); if (typeof options !== 'object') throw new Error('options must be an object.'); this._original = libxmljs.Document.fromXml(xml, options); this._hasContent = true; return this; } /** * Gets the first result of the provided XPath. * @param {string} xpath The XPath to get. * @returns {libxmljs.Element} A `libxmljs.Element`. */ get(xpath) { const found = this.find(xpath); if (found.length === undefined) return found; // found is not an array so we just return it. if (found.length === 0) return null; // found is an empty array so we return null. return found[0]; // found is a non-empty array so we return the first element. } /** * Gets the `Document`'s DTD. * @returns {object} The DTD object. */ getDtd() { return this._original.getDtd(); } /** * Gets the `Document`'s namespaces. * @returns {libxmljs.Namespace[]} An array of `libxmljs.Namespace`s. */ namespaces() { this.assertNoContentError(); return this._original.namespaces(); } /** * Creates the root. * @param {string} name The root's tag name. * @param {string} content The root's text content. * @returns {libxmljs.Node} The created `libxmljs.Node`. */ node(name, content) { if (!this._hasContent) this._hasContent = true; return this._original.node(name, content); } /** * Checks whether the `Document` is valid using Relax NG. * @param {object} rng The Relax NG. * @returns {boolean} Whether the `Document` is valid using Relax NG. */ rngValidate(rng) { return this._original.rngValidate(rng); } /** * Gets the root. * @returns {libxmljs.Element|null} The root or `null`. */ root() { this.assertNoContentError(); return this._original.root(); } /** * Sets the root. * @param {libxmljs.Element} node The root `libxmljs.Element`. * @returns {libxmljs.Element} The created `libxmljs.Element`. */ root(node) { this.assertNoContentError(); return this._original.root(node); } /** * Sets the `Document`'s DTD. * @param {string} name The name of the DTD. * @param {string} ext The external ID for the DTD. * @param {string} sys The system ID for the DTD. * @returns {Document} The `Document`. */ setDtd(name, ext, sys) { this._original.setDtd(name, ext, sys); return this; } /** * Sets the `Document`'s namespace. * @param {string} alias The alias of the namespace. * @param {string} url The URL of the namespace. * @returns {Document} The `Document`. */ setNamespace(alias, url) { if (typeof alias !== 'string') throw new Error('alias must be a string.'); if (typeof url !== 'string') throw new Error('url must be a string.'); if (!utils.isValidURL(url)) throw new Error('url is not a valid URL.'); this.namespace = { alias, url, }; return this; } /** * Returns the `Document` as a string. * @param {boolean} [formatting] Tells whether the ouput is formatted. * @returns {string} The `Document` as a string. */ toString(formatting = true) { return this._original.toString(formatting); } /** * Returns the string "document". * @returns {string} "document". */ type() { return this._original.type(); } /** * Validates the `Document` against a XSD document. * @param {Document} xsdDoc The XSD `Document`. * @returns {boolean} Whether the `Document` is valid. `validationErrors` contains the errors if any. */ validate(xsdDoc) { this.assertNoContentError(); if (!xsdDoc) throw new Error('No XSD document to validate against.'); return this._original.validate(xsdDoc); } /** * Gets the `Document`'s version. * @returns {string} The `Document`'s version. */ version() { return this._original.version(); } /** * Throws an error if the `Document` has no content. */ assertNoContentError() { if (!this._hasContent) { const err = new Error('No parsed content found.'); err.name = 'NoContentError'; throw err; } } } module.exports = Document;