Newer
Older
libxmljs-extra / src / Document.js
/* 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;