UNPKG

asciidoctor-chunker

Version:

Creates chunked (multi-page) HTML from Asciidoctor's single HTML file with supporting the fine-tuned splits in chapters, sections and any depth of subsections.

463 lines (426 loc) 14.5 kB
/* * This file is a part of Asciidoctor Chunker project. * Copyright (c) 2022 Wataru Shito (@waterloo_jp) */ 'use strict'; import fs from 'fs'; import * as cheerio from 'cheerio'; /** * Class to hold a currently selected DOM node or multiple DOM nodes * while providing the various DOM manipulation interfaces. * Use the static method `getInstanceFrom[File|HTML]()` to constuct * an instance. * * Basically all the instance methods return a new Node instance without * changing the selection of the Node instance passed in the argument. * The methods with `$` suffix returns the original Node instance without * creating new one. * * However, the returned Node instance may share the DOM tree with * the one in the argument. Please refer to the method documentation * for details. */ class Node { /** * $: CheerioAPI * Provides the various static methods. */ $; /** * root: Cheerio<Document> * The root Element that this node belongs in order * to access the root context for html serialization * and tree traversal. */ rootNode; /** * Cheerio<Document | Element> * The current selections in the DOM. */ context; /** * Returns the length of current selections. */ get length() { return this.context.length; } /** * Returns the name of the tag currenlty selected. * If there are multiple emelements are selected * this returns the name of the first selection. */ get tagName() { return this.context[0].name; } /** * Appends multiple nodes sequently to the target node. * This causes side effects that change the state of the target node. * The appending nodes are cloned and untouched. * * @param {[Node]} appendingNodes The array of appending nodes. * These nodes are cloned before appending in `appendNode$()` method. * @param {Node} target The target node where nodes are appended to. */ static appendNodesToTarget$ = (...appendingNodes) => target => { appendingNodes.forEach(ele => target.appendNode$(ele)); return target; } /** * Static method thaat instantiates the new DOM from the filename. * * @param {String} filename */ static getInstanceFromFile(filename) { const $ = cheerio.load(fs.readFileSync(filename)); return new Node($, $.root(), $.root()); }; /** * Static method that instantiates the new DOM node from the the html text. * * @param {String} htmlText * @param {Option} options object of Cheerio, default is `null`. * @param {boolean} isDocument true to have a DOM with html element as a root * `false` to make DOM as given in `htmlText`. Default is `false`. * @returns {Node} the newly constructed Node instance. */ static getInstanceFromHTML(htmlText, options = null, isDocument = false) { const $ = cheerio.load(htmlText, options, isDocument); return new Node($, $.root(), $.root()); } /** * Static method that inserts the given html string before the target node * and returns the new Node instance that selects the inserted node within * the DOM tree where target node belongs. This does not change the `targetNode` * selections. * * @param {String} html * @param {Node} targetNode * @returns {Node} the new Node instance of which selection is the inserted node. * The returned Node instance shares the DOM and the root with that of `targetNode`. */ static insertHtmlBefore(html, targetNode) { targetNode.$(html).insertBefore(targetNode.context); return new Node(targetNode.$, targetNode.rootNode, targetNode.context.prev()); } /** * Static method that inserts the given html string after the target node * and returns the new Node instance that selects the inserted node within * the DOM tree where target node belongs. This does not change the `targetNode` * selections. * * @param {String} html * @param {Node} targetNode * @returns {Node} the new Node instance of which selection is the inserted node. * The returned Node instance shares the DOM and the root with that of `targetNode`. */ static insertHtmlAfter(html, targetNode) { targetNode.$(html).insertAfter(targetNode.context); return new Node(targetNode.$, targetNode.rootNode, targetNode.context.next()); } /** [FOR INTERNAL USE] */ constructor($, root, context) { this.$ = $; this.rootNode = root; this.context = context; } /** * Adds the class value to this node and returns * `this` node. If the current node is * `<div class="abc">` and invoking `addClass$("next")` * the node becomes `<div class="abc next">`. * * @param {string} classValue * @returns {Node} returns `this` Node instance for the * method chain. */ addClass$(classValue) { this.context.addClass(classValue); return this; } /** * Inserts content in HTML string as the last child of each * of the current node and returns `this` node instance * for the method chain. * * @param {string} htmlStr * @returns {this} for method chain */ appendHTML$(htmlStr) { this.$(this.context).append(htmlStr); return this; } /** * Inserts clone of the given node as the last child of each * of the current node and returns `this` node instance for * the method chain. * * Note that the appending node is cloned for safety because the * node cannot be belonged mutlple DOM trees. Thus, it is redundant * to clone by the caller although it is not harmful. * * @param {Node | string} node Appending node which is cloned * before appending. * @returns {this} for method chain */ appendNode$(node) { if (typeof node === 'string') this.context.append(node); // node is HTML string else { const copy = node.context.clone(); // this.$(this.context).append(copy); this.context.append(copy); } return this; } /** * @returns {Node} the node instance pointing the children. */ children() { return new Node(this.$, this.root, this.context.children()); } /** * Creates the completely independent DOM tree from the * current node. The returned instance points the currently * selected node as the root node. * * If you want to clone the whole DOM tree, `node.root().clone()` * will make one. * * @returns {Node} the cloned node. */ clone() { const node = new Node(); // $ is the CheerioAPI that provides the various static methods // CheerioAPI cannot be instantiated other than from load() so // we reuse the $ instance. node.$ = this.$; const copy = this.$(this.context).clone(); node.rootNode = copy; // the cloned node is now the new root! node.context = copy; return node; } /** * Returns the children of this node's context, including text and comment nodes. * * @returns {Node} the new instance of contents nodes. */ contents() { return new Node(this.$, this.rootNode, this.context.contents()); } /** * Iterates the current selections with the callback. To break * out the `each` loop, return `false` from the callback. * * @param {(Node, Number) => boolean} callback The callback * function to be invoked to each node. */ each(callback) { this.context.each((i, ele) => { return callback(new Node(this.$, this.context, this.$(ele)), i); }); } /** * Removes all the children and returns `this` Node instance. * * @returns {Node} `this` object of which children are removed. */ empty$() { this.context.empty(); return this; } /** * Query the selectorAll under the current node and * returns the selections encapsulated in a new Node instance. * The returned Node instance shares the root node and DOM * tree from this one. * * @param {*} selector * @returns {Node} returns the new Node instance encapsulating * the selected nodes. * */ find(selector) { const matched = this.$(this.context).find(selector); const node = new Node(this.$, this.rootNode, matched); this.context = matched.end(); // keep the original contex for the current node return node; // returns new Node instance that holds the selections as context } /** * Returns the new Node instance that selects the first element * of current selections (or context). * * @returns {Node} the new Node instance with the first element * selected as the context. */ first() { return new Node(this.$, this.rootNode, this.context.first()); } /** * Retrieve the ith element from the current context. * * @param {number} ith the index of current selections. * @returns {Node} the new Node instance with the retrieved * element set as the context. */ get(ith) { // Cheerio.get() does not return Cheerio object. Wrap it with $(). return new Node(this.$, this.rootNode, this.$(this.context.get(ith))); } /** * Gets the attribute of this node. * Only the first element of the attribute in the * current node set is returned. * * @param {string} attrName * @returns {string} the attribute name */ getAttr(attrName) { return this.context.attr(attrName); } /** * Check to see if `this` node has the given className. * @param {String} className * @returns {boolean} */ hasClass(className) { return this.context.hasClass(className); } html() { return this.$(this.context).html(); } /** * Inserts the copy of this node after the given node and returns * the target node where this node is inserted after. This method * will modify the DOM tree where the target node belongs to. * * Note that this method returns the target node. If you want * the inserted node to be returned, use the corresponding static * method: `insertHtmlAfter(html, targetNode)` * * @param {Node} targetNode The target node where `this` node is * inserted to after. The `this` node's context is unmodified. * @returns {Node} the target node, i.e. the passed argument, * where `this` node is inserted after the target. */ insertMeAfter$(targetNode) { targetNode.$(this.html()).insertAfter(targetNode.context); return targetNode; } /** * Inserts the copy of this node before the given node and returns * the target node where `this` node is inserted before. This method * will modify the DOM tree where the target node belongs to. * * Note that this method returns the target node. If you want * the inserted node to be returned, use the corresponding static * method: `insertHtmlBefore(html, targetNode)`. * * @param {Node} targetNode The target node where `this` node is * inserted to before. The `this` node's context is unmodified. * @returns {Node} the target node, i.e. the given parameter, * where `this` node is inserted before the target. */ insertMeBefore$(targetNode) { targetNode.$(this.html()).insertBefore(targetNode.context); return targetNode; } /** * Returns the new Node instance with the current context * (or the selection)set to the next node within the siblings. * If there is not more next siblings, the current context * is the cheerio object with zero selection. * * The new Node instance shares the root node with `this` object. * The `this` object's context (or the selection) is unchanged. * * @returns {Node} returns the new Node instance with the current * context set to the next sibling node. */ next() { // this.context.next() does not change the current selection // this.context.next() returns the new cheerio instance with // next node selected return new Node(this.$, this.rootNode, this.context.next()); } /** * Returns the new Node instance of this parent. */ parent() { return new Node(this.$, this.rootNode, this.context.parent()); } /** * Returns the new Node instance with the current context * (or the selection)set to the previous node within the siblings. * If there is not more previous siblings, the current context * is the cheerio object with zero selection. * * The new Node instance shares the root node with `this` object. * The `this` object's context (or the selection) is unchanged. * * @returns {Node} returns the new Node instance with the current * context set to the previous sibling node. */ prev() { // this.context.prev() does not change the current selection // this.context.prev() returns the new cheerio instance with // the previous node selected return new Node(this.$, this.rootNode, this.context.prev()); } /** * Removes the selections given in the parameter under the current * node and returns `this` Node instance. * * Note that if given `selector` points itself, the returned Node * instance holds removed elements. * * @param {String} selector CSS selector to match the removing nodes. * @returns {this} `this` Node instance. */ remove$(selector) { this.context.find(selector).remove().end(); return this; } /** * Replaces the current selections with the given HTML * string and returns the replaced node as a current * current selection. * * @param {String} html The replacing HTML string. * @returns {Node} the new Node instance with the root node * set as a current context. */ replaceWithHTML$(html) { const rep = this.$(html); this.context.replaceWith(html); return new Node(this.$, this.rootNode, this.rootNode); } root() { return new Node(this.$, this.rootNode, this.rootNode); } /** * Sets the attribute of this node and returns `this` node. * * @param {string} attrName * @param {string} value * @returns {Node} returns `this` Node instance for the method chain. */ setAttr$(attrName, value) { this.context.attr(attrName, value); return this; } /** * Gets or sets the text node of current node. * If current node consists of multiple nodes, all the texts are joined without * delimiter. * @param {*} str * @returns {string | Node} the text content of this node for getter * and this Node instance if setter for method chain. */ text(str) { if (str) { this.$(this.context).text(str); return this; } else return this.$(this.context).text(); } } export default Node;