UNPKG

docutils-ts

Version:

Port of the Python Docutils library to TypeScript

1,458 lines (1,448 loc) 74.8 kB
/** * Docutils document tree element class library. * * Classes in CamelCase are abstract base classes or auxiliary classes. The one * exception is `Text`, for a text (PCDATA) node; uppercase is used to * differentiate from element classes. Classes in lower_case_with_underscores * are element classes, matching the XML element generic identifiers in the DTD_. * * The position of each node (the level at which it can occur) is significant and * is represented by abstract base classes (`Root`, `Structural`, `Body`, * `Inline`, etc.). Certain transformations will be easier because we can use * ``isinstance(node, base_class)`` to determine the position of the node in the * hierarchy. * * .. _DTD: http://docutils.sourceforge.net/docs/ref/docutils.dtd * */ import { xmlescape } from "./xml-escape.js"; import Transformer from "./transformer.js"; import { ApplicationError, InvalidArgumentsError, InvalidStateError, UnimplementedError } from "./exceptions.js"; import unescape from "./utils/unescape.js"; import { checkDocumentArg, isIterable, pySplit } from "./utils.js"; import { fullyNormalizeName, whitespaceNormalizeName } from "./nodeUtils.js"; import { nodeBasicAttributes } from './constants.js'; const _nonIdChars = /[^a-z0-9]+/ig; const _nonIdAtEnds = /^[-0-9]+|-+$/; const _nonIdTranslate = { 0x00f8: "o", // o with stroke 0x0111: "d", // d with stroke 0x0127: "h", // h with stroke 0x0131: "i", // dotless i 0x0142: "l", // l with stroke 0x0167: "t", // t with stroke 0x0180: "b", // b with stroke 0x0183: "b", // b with topbar 0x0188: "c", // c with hook 0x018c: "d", // d with topbar 0x0192: "f", // f with hook 0x0199: "k", // k with hook 0x019a: "l", // l with bar 0x019e: "n", // n with long right leg 0x01a5: "p", // p with hook 0x01ab: "t", // t with palatal hook 0x01ad: "t", // t with hook 0x01b4: "y", // y with hook 0x01b6: "z", // z with stroke 0x01e5: "g", // g with stroke 0x0225: "z", // z with hook 0x0234: "l", // l with curl 0x0235: "n", // n with curl 0x0236: "t", // t with curl 0x0237: "j", // dotless j 0x023c: "c", // c with stroke 0x023f: "s", // s with swash tail 0x0240: "z", // z with swash tail 0x0247: "e", // e with stroke 0x0249: "j", // j with stroke 0x024b: "q", // q with hook tail 0x024d: "r", // r with stroke 0x024f: "y" // y with stroke }; const _nonIdTranslateDigraphs = { 0x00df: "sz", // ligature sz 0x00e6: "ae", // ae 0x0153: "oe", // ligature oe 0x0238: "db", // db digraph 0x0239: "qp" // qp digraph }; /** * +------+------+ * | this | is a | * +------+------+ * |ridiculous | * |test | * +-------------+ */ function dupname(node, name) { /* What is the intention of this function? */ node.attributes.dupnames.push(name); node.attributes.names.splice(node.attributes.names.indexOf(name), 1); // Assume that this method is referenced, even though it isn't; we // don't want to throw unnecessary system_messages. node.referenced = true; } /** * Escape string values that are elements of a list, for serialization. * @param {String} value - Value to escape. */ function serialEscape(value) { return value.replace(/\\/g, "\\\\").replace(/ /g, "\\ "); } /* We don't do 'psuedo-xml' but perhaps we should */ function pseudoQuoteattr(value) { return `"${xmlescape(value)}"`; } function setupBacklinkable(o) { o.addBackref = (refid) => { o.attributes.backrefs.push(refid); }; } /** * Convert `string` into an identifier and return it. * * Docutils identifiers will conform to the regular expression * ``[a-z](-?[a-z0-9]+)*``. For CSS compatibility, identifiers (the "class" * and "id" attributes) should have no underscores, colons, or periods. * Hyphens may be used. * * - The `HTML 4.01 spec`_ defines identifiers based on SGML tokens: * * ID and NAME tokens must begin with a letter ([A-Za-z]) and may be * followed by any number of letters, digits ([0-9]), hyphens ("-"), * underscores ("_"), colons (":"), and periods ("."). * * - However the `CSS1 spec`_ defines identifiers based on the "name" token, * a tighter interpretation ("flex" tokenizer notation; "latin1" and * "escape" 8-bit characters have been replaced with entities):: * * unicode \\[0-9a-f]{1,4} * latin1 [&iexcl;-&yuml;] * escape {unicode}|\\[ -~&iexcl;-&yuml;] * nmchar [-a-z0-9]|{latin1}|{escape} * name {nmchar}+ * * The CSS1 "nmchar" rule does not include underscores ("_"), colons (":"), * or periods ("."), therefore "class" and "id" attributes should not contain * these characters. They should be replaced with hyphens ("-"). Combined * with HTML's requirements (the first character must be a letter; no * "unicode", "latin1", or "escape" characters), this results in the * ``[a-z](-?[a-z0-9]+)*`` pattern. * * .. _HTML 4.01 spec: http://www.w3.org/TR/html401 * .. _CSS1 spec: http://www.w3.org/TR/REC-CSS1 */ function makeId(strVal) { let id = strVal.toLowerCase(); // This is for unicode, I believe? //if not isinstance(id, str): //id = id.decode() // id = translate(_nonIdTranslateDigraphs); //id = id.translate(_nonIdTranslate); // get rid of non-ascii characters. // 'ascii' lowercase to prevent problems with turkish locale. //id = unicodedata.normalize('NFKD', id). // encode('ascii', 'ignore').decode('ascii'); // shrink runs of whitespace and replace by hyphen id = pySplit(id).join(' ').replace(_nonIdChars, '-'); id = id.replace(_nonIdAtEnds, ''); return id; } function _callDefaultVisit(node) { // @ts-ignore return this.default_visit(node); } function _callDefaultDeparture(node) { // @ts-ignore return this.default_departure(node); } /* This is designed to be called later, a-nd not with an object. hmm */ function _addNodeClassNames(names, o) { names.forEach((_name) => { const v = `visit_${_name}`; if (!o[v]) { o[v] = _callDefaultVisit.bind(o); } const d = `depart_${_name}`; if (!o[d]) { o[d] = _callDefaultDeparture.bind(o); } }); } const nodeClassNames = ["Text", "abbreviation", "acronym", "address", "admonition", "attention", "attribution", "author", "authors", "block_quote", "bullet_list", "caption", "caution", "citation", "citation_reference", "classifier", "colspec", "comment", "compound", "contact", "container", "copyright", "danger", "date", "decoration", "definition", "definition_list", "definition_list_item", "description", "docinfo", "doctest_block", "document", "emphasis", "entry", "enumerated_list", "error", "field", "field_body", "field_list", "field_name", "figure", "footer", "footnote", "footnote_reference", "generated", "header", "hint", "image", "important", "inline", "label", "legend", "line", "line_block", "list_item", "literal", "literal_block", "math", "math_block", "note", "option", "option_argument", "option_group", "option_list", "option_list_item", "option_string", "organization", "paragraph", "pending", "problematic", "raw", "reference", "revision", "row", "rubric", "section", "sidebar", "status", "strong", "subscript", "substitution_definition", "substitution_reference", "subtitle", "superscript", "system_message", "table", "target", "tbody", "term", "tgroup", "thead", "tip", "title", "title_reference", "topic", "transition", "version", "warning"]; const SkipChildren = class { }; const StopTraversal = class { }; class SkipNode extends Error { } const SkipDeparture = class { }; const SkipSiblings = class { }; const NodeFound = class { }; /** * "Visitor" pattern [GoF95]_ abstract superclass implementation for * document tree traversals. * * Each node class has corresponding methods, doing nothing by * default; override individual methods for specific and useful * behaviour. The `dispatch_visit()` method is called by * `Node.walk()` upon entering a node. `Node.walkabout()` also calls * the `dispatch_departure()` method before exiting a node. * * The dispatch methods call "``visit_`` + node class name" or * "``depart_`` + node class name", resp. * * This is a base class for visitors whose ``visit_...`` & ``depart_...`` * methods should be implemented for *all* node types encountered (such as * for `docutils.writers.Writer` subclasses). Unimplemented methods will * raise exceptions. * * For sparse traversals, where only certain node types are of interest, * subclass `SparseNodeVisitor` instead. When (mostly or entirely) uniform * processing is desired, subclass `GenericNodeVisitor`. * * .. [GoF95] Gamma, Helm, Johnson, Vlissides. *Design Patterns: Elements of * Reusable Object-Oriented Software*. Addison-Wesley, Reading, MA, USA, * 1995. */ class NodeVisitor { /** * Create a NodeVisitor. * @param {nodes.document} document - document to visit */ constructor(document) { if (!checkDocumentArg(document)) { throw new Error(`Invalid document arg: ${document}`); } this.document = document; const core = document.settings; this.strictVisitor = core.strictVisitor; this.optional = []; } /** * Call this."``visit_`` + node class name" with `node` as * parameter. If the ``visit_...`` method does not exist, call * this.unknown_visit. */ dispatchVisit(node) { const nodeName = node.tagname; const methodName = `visit_${nodeName}`; let method = (this)[methodName]; if (!method) { method = this.unknownVisit; } this.document.reporter.debug(`docutils.nodes.NodeVisitor.dispatch_visit calling for ${nodeName}`); return method.bind(this)(node); } /* * Call this."``depart_`` + node class name" with `node` as * parameter. If the ``depart_...`` method does not exist, call * this.unknown_departure. */ dispatchDeparture(node) { const nodeName = node.tagname; const method = (this)[`depart_${nodeName}`] || this.unknownDeparture; this.document.reporter.debug(`docutils.nodes.NodeVisitor.dispatch_departure calling for ${node}`); return method.bind(this)(node); } /** * Called when entering unknown `Node` types. * * Raise an exception unless overridden. */ unknownVisit(node) { if (this.strictVisitor || !(this.optional.includes(node.tagname))) { throw new Error(`visiting unknown node type:${node.tagname}`); } } /** * Called before exiting unknown `Node` types. * * Raise exception unless overridden. */ unknownDeparture(node) { if (this.strictVisitor || !(this.optional.includes(node.tagname))) { throw new Error(`departing unknown node type: ${node.tagname}`); } } } /** * Base class for sparse traversals, where only certain node types are of * interest. When ``visit_...`` & ``depart_...`` methods should be * implemented for *all* node types (such as for `docutils.writers.Writer` * subclasses), subclass `NodeVisitor` instead. */ class SparseNodeVisitor extends NodeVisitor { } /** * Generic "Visitor" abstract superclass, for simple traversals. * * Unless overridden, each ``visit_...`` method calls `default_visit()`, and * each ``depart_...`` method (when using `Node.walkabout()`) calls * `default_departure()`. `default_visit()` (and `default_departure()`) must * be overridden in subclasses. * * Define fully generic visitors by overriding `default_visit()` (and * `default_departure()`) only. Define semi-generic visitors by overriding * individual ``visit_...()`` (and ``depart_...()``) methods also. * * `NodeVisitor.unknown_visit()` (`NodeVisitor.unknown_departure()`) should * be overridden for default behavior. */ class GenericNodeVisitor extends NodeVisitor { constructor(document) { super(document); // document this/ _addNodeClassNames(nodeClassNames, this); } default_visit(node) { throw new Error("not implemented"); } default_departure(node) { throw new Error("not implemented"); } } GenericNodeVisitor.nodeClassNames = []; // fixme // GenericNodeVisitor.nodeClassNames = nodeClassNames; // ======== // Mixins // ======== class Resolvable { } class BackLinkable { constructor() { this.backrefs = []; } addBackref(refid) { this.backrefs.push(refid); } } // ==================== // Element Categories // ==================== class Root { } class Titular { } /** * Category of Node which may occur before Bibliographic Nodes. */ class PreBibliographic { } class Bibliographic { } class Decorative extends PreBibliographic { } class Structural { } class Body { } class General extends Body { } /** List-like elements. */ class Sequential extends Body { } class Admonition extends Body { } /** Special internal body elements. */ class Special extends Body { } /** Internal elements that don't appear in output. */ class Invisible extends PreBibliographic { } class Part { } class Inline { } class Referential extends Resolvable { } class Targetable extends Resolvable { } /** Contains a `label` as its first element. */ class Labeled { } // ============================== // Functional Node: NodeInterface Base Classes // ============================== /** * Node class. * * The base class for all docutils nodes. */ class Node { removeChild(index) { this._children.splice(index, 1); } append(item) { throw new Error('Cant append to underived Node'); } getChild(index) { if (index < 0 || index >= this._children.length) { throw new ApplicationError('index out of range'); } return this._children[index]; } hasChildren() { return this._children.length > 0; } getNumChildren() { return this._children.length; } clearChildren() { this._children.length = 0; } getChildren() { return [...this._children]; } get children() { return this._children; } set children(value) { this._children = value; } get parent() { if (this._parent === undefined) { throw new ApplicationError('Attempt to access parent property of node without parent.'); } return this._parent; } /** * Create a node */ constructor() { this.isSetup = false; /** * List attributes which are defined for every Element-derived class * instance and can be safely transferred to a different node. */ this.basicAttributes = nodeBasicAttributes; /** * List attributes, automatically initialized to empty lists for * all nodes. */ this.listAttributes = []; /** List attributes that are known to the Element base class. */ this.knownAttributes = []; this.childTextSeparator = ""; this.referenced = false; this.names = []; this.currentSource = ""; this.currentLine = 0; this.rawsource = ""; this.line = 0; this.classTypes = []; this._children = []; this.attributes = {}; this.tagname = this.constructor.name; this.classTypes = []; this._init(); } _init() { } /** Return the first node in the iterable returned by traverse(), or None if the iterable is empty. Parameter list is the same as of traverse. Note that include_self defaults to 0, though. */ nextNode(args) { const iterable = this.traverse(args); if (iterable.length) { return iterable[0]; } return undefined; } hasClassType(classType) { return this.classTypes.findIndex((c) => c.prototype instanceof classType || c === classType) !== -1; } isInline() { return this.classTypes.findIndex((c) => c.prototype instanceof Inline || c === Inline) !== -1; } isAdmonition() { return this.classTypes.findIndex((c) => c.prototype instanceof Admonition || c === Admonition) !== -1; } asDOM(dom) { return {}; } setupChild(child) { child._parent = this; if (this.document) { child.document = this.document; if (child.source == null) { child.source = this.document.currentSource; } if (child.line == null) { child.line = this.document.currentLine; } } child.isSetup = true; } /** * Traverse a tree of `Node` objects, calling the * `dispatch_visit()` method of `visitor` when entering each * node. (The `walkabout()` method is similar, except it also * calls the `dispatch_departure()` method before exiting each * node.) * * This tree traversal supports limited in-place tree * modifications. Replacing one node with one or more nodes is * OK, as is removing an element. However, if the node removed * or replaced occurs after the current node, the old node will * still be traversed, and any new nodes will not. * * Within ``visit`` methods (and ``depart`` methods for * `walkabout()`), `TreePruningException` subclasses may be raised * (`SkipChildren`, `SkipSiblings`, `SkipNode`, `SkipDeparture`). * * Parameter `visitor`: A `NodeVisitor` object, containing a * ``visit`` implementation for each `Node` subclass encountered. * * Return true if we should stop the traversal. */ walk(visitor) { let stop = false; visitor.document.reporter.debug("docutils.nodes.Node.walk calling dispatch_visit for fixme"); try { try { visitor.dispatchVisit(this); } catch (error) { if (error instanceof SkipChildren || error instanceof SkipNode) { return stop; } if (error instanceof SkipDeparture) { // do nothing } throw error; } const children = [...this._children]; let skipSiblings = false; children.forEach((child) => { try { if (!stop && !skipSiblings) { if (child.walk(visitor)) { stop = true; } } } catch (error) { if (error instanceof SkipSiblings) { skipSiblings = true; } else { throw error; } } }); } catch (error) { if (error instanceof StopTraversal) { stop = true; } throw error; } return stop; } walkabout(visitor) { let callDepart = true; let stop = false; visitor.document.reporter.debug("docutils.nodes.Node.walkabout calling dispatch_visit"); try { try { visitor.dispatchVisit(this); } catch (error) { if (error instanceof SkipNode || error instanceof SkipChildren) { return stop; } if (error instanceof SkipDeparture) { callDepart = false; } else { throw error; } } try { for (const child of [...this.children]) { // console.log(typeof child); // console.log(Object.keys(child)); //console.log(`calling walkabout on ${child.tagname}`); if (child.walkabout(visitor)) { stop = true; break; } } } catch (error) { if (!(error instanceof SkipSiblings)) { throw error; } } } catch (error) { if (error instanceof StopTraversal) { stop = true; } else { throw error; } } if (callDepart) { visitor.document.reporter.debug(`docutils.nodes.Node.walkabout calling dispatch_departure for ${this}`); visitor.dispatchDeparture(this); } return stop; } _fastTraverse(cls) { // Specialized traverse() that only supports instance checks. const result = []; if (this instanceof cls) { result.push(this); } const myNode = this; myNode._children.forEach((child) => { if (typeof child === "undefined") { throw new Error("child is undefined"); } // @ts-ignore if (typeof child._fastTraverse === "undefined") { throw new Error(`${child} does not have _fastTraverse`); } // @ts-ignore result.push(...child._fastTraverse(cls)); }); return result; } _allTraverse() { // Specialized traverse() that doesn't check for a condition. const result = []; result.push(this); this._children.forEach((child) => { // @ts-ignore result.push(...child._allTraverse()); }); return result; } traverse(args) { const { condition, includeSelf = true, descend = true, siblings = false, ascend = false } = args; const mySiblings = ascend ? true : siblings; if (includeSelf && descend && !mySiblings) { if (!condition) { return this._allTraverse(); } if ((condition.prototype instanceof Node) || condition === Node) { return this._fastTraverse(condition); } } if (typeof condition !== "undefined" && (condition.prototype instanceof Node || condition === Node)) { const nodeClass = condition; const myCondition = (node, nodeClassArg) => ((node instanceof nodeClassArg) || (node instanceof nodeClass)); throw new Error("unimplemented"); } /* if isinstance(condition, (types.ClassType, type)): node_class = condition def condition(node, node_class=node_class): return isinstance(node, node_class) */ const r = []; if (includeSelf && (condition == null || condition(this))) { r.push(this); } if (descend && this._children.length) { this._children.forEach((child) => { r.push(...child.traverse({ includeSelf: true, descend: true, siblings: false, ascend: false, condition })); }); } if (siblings || ascend) { let node = this; while (node != null && node.parent != null) { const index = node.parent.getChildren().indexOf(node); node.parent.getChildren().slice(index + 1).forEach((sibling) => { r.push(...sibling.traverse({ includeSelf: true, descend, siblings: false, ascend: false, condition })); }); if (!ascend) { node = undefined; } else { node = node.parent; } } } return r; } add(iNodes) { throw new UnimplementedError(""); } endtag() { return ""; } starttag(quoteattr) { return ""; } addBackref(prbid) { } updateBasicAtts(dict_) { const dict2 = dict_ instanceof Node ? dict_.attributes : dict_; this.basicAttributes.forEach((att) => { const v = att in dict2 ? dict2[att] : []; this.appendAttrList(att, v); }); } appendAttrList(attr, values) { // List Concatenation values.forEach((value) => { if ((this.attributes[attr].filter((v) => v === value)).length === 0) { this.attributes[attr].push(value); } }); } replaceAttr(attr, value, force = true) { // One or the other if (force || this.attributes[attr] == null) { this.attributes[attr] = value; } } copyAttrConsistent(attr, value, replace) { if (this.attributes[attr] !== value) { this.replaceAttr(attr, value, replace); } } updateAllAtts(dict_, updateFun = this.copyAttrConsistent, replace = true, andSource = false) { const dict2 = dict_ instanceof Node ? dict_.attributes : dict_; // Include the source attribute when copying? let filterFun; if (andSource) { filterFun = this.isNotListAttribute.bind(this); } else { filterFun = this.isNotKnownAttribute.bind(this); } // Copy the basic attributes this.updateBasicAtts(dict2); // Grab other attributes in dict_ not in self except the // (All basic attributes should be copied already) const atts = Object.keys(dict2).filter(filterFun); atts.forEach((att) => { updateFun.bind(this)(att, dict2[att], replace); }); } /** Updates all attributes from node or dictionary `dict_`. Appends the basic attributes ('ids', 'names', 'classes', 'dupnames', but not 'source') and then, for all other attributes in dict_, updates the same attribute in self. When attributes with the same identifier appear in both self and dict_ whose values aren't each lists and replace is True, the values in self are replaced with the values in dict_; if the values from self and dict_ for the given identifier are both of list type, then the two lists are concatenated and the result stored in self; otherwise, the values in self are preserved. When and_source is True, the 'source' attribute is included in the copy. NOTE: When replace is False, and self contains a 'source' attribute, 'source' is not replaced even when dict_ has a 'source' attribute, though it may still be merged into a list depending on the value of update_fun. */ updateAllAttsConcatenating(dict_, replace = true, andSource = false) { this.updateAllAtts(dict_, this.copyAttrConcatenate, replace, andSource); } /** Returns True if and only if the given attribute is NOT one of the basic list attributes defined for all Elements. */ isNotListAttribute(attr) { return !(attr in this.listAttributes); } /** Returns True if and only if the given attribute is NOT recognized by this class. */ isNotKnownAttribute(attr) { return !(attr in this.knownAttributes); } copyAttrConcatenate(attr, value, replace) { /* """ If attr is an attribute of self and both self[attr] and value are lists, concatenate the two sequences, setting the result to self[attr]. If either self[attr] or value are non-sequences and replace is True or self[attr] is None, replace self[attr] with value. Otherwise, do nothing. """ */ if (this.attributes[attr] !== value) { if (Array.isArray(this.attributes[attr]) && Array.isArray(value)) { this.appendAttrList(attr, value); } else { this.replaceAttr(attr, value, replace); } } } getCustomAttr(attrName) { return undefined; } } /* * `Element` is the superclass to all specific elements. * Elements contain attributes and child nodes. Elements emulate * dictionaries for attributes, indexing by attribute name (a string). To * set the attribute 'att' to 'value', do:: * * element['att'] = 'value' * * There are two special attributes: 'ids' and 'names'. Both are * lists of unique identifiers, and names serve as human interfaces * to IDs. Names are case- and whitespace-normalized (see the * fully_normalize_name() function), and IDs conform to the regular * expression ``[a-z](-?[a-z0-9]+)*`` (see the make_id() function). * * Elements also emulate lists for child nodes (element nodes and/or text * nodes), indexing by integer. To get the first child node, use:: * * element[0] * * Elements may be constructed using the ``+=`` operator. To add one new * child node to element, do:: * * element += node * * This is equivalent to ``element.append(node: NodeInterface)``. * * To add a list of multiple child nodes at once, use the same ``+=`` * operator:: * * element += [node1, node2] * * This is equivalent to ``element.extend([node1, node2])``. * * @extends module:nodes~Node */ class Element extends Node { /** * Create element. * @classdesc Abstracts a docutils Element. * @extends module:nodes~Node */ constructor(rawsource, children = [], attributes = {}) { super(); /** * A list of class-specific attributes that should not be copied with the * standard attributes when replacing a node. * * NOTE: Derived classes should override this value to prevent any of its * attributes being copied by adding to the value in its parent class. */ this.localAttributes = ["backrefs"]; /** * The element generic identifier. If None, it is set as an instance * attribute to the name of the class. */ this.tagname = ""; this.nodeName = Symbol.for("Element"); children.forEach((child) => this.append(child)); this.attributes = {}; this.listAttributes.forEach((x) => { this.attributes[x] = []; }); Object.keys(attributes).forEach((att) => { const value = attributes[att]; const attKey = att.toLowerCase(); /* This if path never taken... why? FIXME */ if (attKey in this.listAttributes) { if (!isIterable(value)) { throw new Error(); } const a = Array.isArray(value) ? value : [value]; this.attributes[attKey] = [...a]; } else { this.attributes[attKey] = value; } }); this.tagname = this.constructor.name; } _init() { super._init(); /* List attributes which are defined for every Element-derived class instance and can be safely transferred to a different node. */ this.basicAttributes = ["ids", "classes", "names", "dupnames"]; /* "A list of class-specific attributes that should not be copied with the standard attributes when replacing a node. NOTE: Derived classes should override this value to prevent any of its attributes being copied by adding to the value in its parent class. */ this.localAttributes = ["backrefs"]; /* List attributes, automatically initialized to empty lists for all nodes. */ this.listAttributes = [...this.basicAttributes, ...this.localAttributes]; /* List attributes that are known to the Element base class. */ this.knownAttributes = [...this.listAttributes, "source", "rawsource"]; /* The element generic identifier. If None, it is set as an instance attribute to the name of the class. */ // this.tagname = undefined; (already set in Node.constructor) /* Separator for child nodes, used by `astext()` method. */ this.childTextSeparator = "\n\n"; } _domNode(domroot) { const element = domroot.createElement(this.tagname); const l = this.attlist(); Object.keys(l).forEach((attribute) => { const value = l[attribute]; let myVal; if (Array.isArray(value)) { myVal = value.map((v) => serialEscape(v.toString())).join(" "); } else { myVal = value.toString(); } element.setAttribute(attribute, myVal); }); this.children.forEach((child) => { // @ts-ignore element.appendChild(child._domNode(domroot)); }); return element; } emptytag() { return `<${[this.tagname, ...Object.entries(this.attlist()) .map(([n, v]) => `${n}="${v}"`)].join(" ")}/>`; } astext() { return this.children.map((x) => x.astext()).join(this.childTextSeparator); } extend(...items) { items.forEach(this.append.bind(this)); } append(item) { this.setupChild(item); this.children.push(item); } add(item) { if (Array.isArray(item)) { this.extend(...item); } else { this.append(item); } } setupChild(child) { if (!(child instanceof Node)) { throw new InvalidArgumentsError(`Expecting node instance ${child}`); } if (!child) { throw new InvalidArgumentsError("need child"); } child._parent = this; if (this.document) { child.document = this.document; if (typeof child.source === "undefined") { child.source = this.document.currentSource; } if (typeof child.line === "undefined") { child.line = this.document.currentLine; } } } starttag(quoteAttr) { const q = quoteAttr || pseudoQuoteattr; const parts = [this.tagname]; const attlist = this.attlist(); Object.keys(attlist).forEach((name) => { const value = attlist[name]; let myVal = value; let gotPart = false; if (myVal === undefined) { parts.push(`${name}="True"`); gotPart = true; } else if (Array.isArray(myVal)) { const values = myVal.map((v) => serialEscape(v.toString())); myVal = values.join(" "); } else { myVal = value.toString(); } if (!gotPart) { myVal = q(myVal); parts.push(`${name}=${myVal}`); } }); return `<${parts.join(" ")}>`; } endtag() { return `</${this.tagname}>`; } attlist() { const attlist = this.nonDefaultAttributes(); return attlist; } nonDefaultAttributes() { const atts = {}; Object.entries(this.attributes).forEach(([key, value]) => { if (this.isNotDefault(key)) { atts[key] = value; } }); return atts; } isNotDefault(key) { if (Array.isArray(this.attributes[key]) && this.attributes[key].length === 0 && this.listAttributes.includes(key)) { return false; } return true; } /* Return the index of the first child whose class does *not* match. Parameters: - `childclass`: A `Node` subclass to skip, or a tuple of `Node` classes. If a tuple, none of the classes may match. - `start`: Initial index to check. - `end`: Initial index to *not* check. */ firstChildNotMatchingClass(childClass, start = 0, end = this.children.length) { const myChildClass = Array.isArray(childClass) ? childClass : [childClass]; const r = this.children.slice(start, Math.min(this.children.length, end)) .findIndex((child, index) => { if (myChildClass.findIndex((c) => { // if (typeof child === 'undefined') { // throw new Error(`child should not be undefined, index ${index}`); // } if (child instanceof c || (this.children[index].classTypes.filter(((c2) => c2.prototype instanceof c || c2 === c))) .length) { return true; } return false; }) === -1) { // console.log(`returning index ${index} ${nodeToXml(this.children[index])}`); return true; } return false; }); if (r !== -1) { return r; } return undefined; } pformat(indent = ' ', level = 0) { return `${indent.repeat(level)}${this.starttag()}\n${this.children.map((c) => c.pformat(indent, level + 1)).join("")}`; } copy() { const ctor = this.constructor; return new ctor(this.rawsource, this.children, this.attributes); } deepcopy() { return this.copy(); } /* Update basic attributes ('ids', 'names', 'classes', 'dupnames', but not 'source') from node or dictionary `dict_`. */ /* For each element in values, if it does not exist in self[attr], append it. NOTE: Requires self[attr] and values to be sequence type and the former should specifically be a list. */ /* If self[attr] does not exist or force is True or omitted, set self[attr] to value, otherwise do nothing. */ /* If replace is true or this.attributes[attr] is null, replace this.attributes[attr] with value. Otherwise, do nothing. */ /* Updates all attributes from node or dictionary `dict_`. Appends the basic attributes ('ids', 'names', 'classes', 'dupnames', but not 'source') and then, for all other attributes in dict_, updates the same attribute in self. When attributes with the same identifier appear in both self and dict_, the two values are merged based on the value of update_fun. Generally, when replace is True, the values in self are replaced or merged with the values in dict_; otherwise, the values in self may be preserved or merged. When and_source is True, the 'source' attribute is included in the copy. NOTE: When replace is False, and self contains a 'source' attribute, 'source' is not replaced even when dict_ has a 'source' attribute, though it may still be merged into a list depending on the value of update_fun. NOTE: It is easier to call the update-specific methods then to pass the update_fun method to this function. */ /** Note that this Element has been referenced by its name `name` or id `id`. */ noteReferencedBy(name, id) { this.referenced = true; const byName = this.attributes.expect_referenced_by_name[name]; const byId = this.attributes.expect_referenced_by_name[id]; if (byName != null) { byName.referenced = true; } if (byId != null) { byId.referenced = 1; } } } // ===================== // Decorative Elements // ===================== class header extends Element { constructor(rawsource, children, attributes) { super(rawsource, children, attributes); this.classTypes = [Decorative]; } } class footer extends Element { constructor(rawsource, children = [], attributes = {}) { super(rawsource, children, attributes); this.classTypes = [Decorative]; } } class decoration extends Element { constructor(rawsource, children = [], attributes = {}) { super(rawsource, children, attributes); this.classTypes = [Decorative]; } getHeader() { if (!this.children.length || !(this.children[0] instanceof header)) { this.children.splice(0, 0, new header()); } return this.children[0]; } getFooter() { if (!this.children.length || !(this.children[this.children.length - 1] instanceof footer)) { this.add(new footer()); } return this.children[this.children.length - 1]; } } class Text extends Node { pformat(indent = ' ', level = 0) { const indentStr = indent.repeat(level); const lines = this.astext().split('\n').map(line => `${indentStr}${line}`); if (lines.length === 0) { return ''; } return `${lines.join('\n')}\n`; } copy() { return this.constructor(this.data, this.rawsource); } deepcopy() { return this.copy(); } walk(visitor) { throw new Error("Method not implemented."); } constructor(data, rawsource = "") { super(); if (typeof data === "undefined") { throw new Error("data should not be undefined"); } this.rawsource = rawsource; this.data = data; this.children = []; } _domNode(domroot) { return domroot.createTextNode(this.data); } astext() { return unescape(this.data); } toString() { return this.astext(); } toSource() { return this.toString(); } add(iNodes) { throw new UnimplementedError(""); } emptytag() { return ""; } } class TextElement extends Element { constructor(rawsource, text, children, attributes) { const cAry = children || []; if (Array.isArray(text)) { throw new InvalidArgumentsError("text should not be an array"); } super(rawsource, (typeof text !== "undefined" && text !== "") ? [new Text(text), ...cAry] : cAry, attributes); } } /** * Document class. Do not call the constructor, rather call * {@link newDocument} to obtain a new document instance * * Implements {@link Document} * @extends Element * @implements Document */ class document extends Element { /** Private constructor */ constructor(settings, reporter, logger, rawsource, children, attributes) { super(rawsource, children, attributes); this.idPrefix = ""; this.autoIdPrefix = ""; this.classTypes = [Root, Structural]; this.logger = logger; this.tagname = "document"; this.settings = settings; this.reporter = reporter; this.indirectTargets = []; this.substitutionDefs = {}; this.substitutionNames = {}; this.refNames = {}; this.refIds = {}; this.nameIds = {}; this.nameTypes = {}; this.ids = {}; this.footnoteRefs = {}; this.citationRefs = {}; this.autofootnotes = []; this.autofootnoteRefs = []; this.symbolFootnotes = []; this.symbolFootnoteRefs = []; this.footnotes = []; this.citations = []; this.autofootnoteStart = 1; this.symbolFootnoteStart = 0; this.idStart = 1; this.parseMessages = []; this.transformMessages = []; this.transformer = new Transformer(this, logger); this.decoration = undefined; this.document = this; } setId(node, msgnode) { let msg; let id = ''; node.attributes.ids.forEach((myId) => { if (myId in this.ids && this.ids[myId] !== node) { msg = this.reporter.severe(`Duplicate ID: "${myId}".`); if (msgnode !== undefined && msg !== undefined) { msgnode.add(msg); } } }); if (node.attributes.ids.length === 0) { let myBreak = false; for (const name of node.attributes.names) { id = this.idPrefix + makeId(name); if (id && this.attributes.ids.indexOf(id) === -1) { myBreak = true; break; } } if (!myBreak) { id = ""; while (!id || (id in this.attributes.ids)) { id = (this.idPrefix + this.autoIdPrefix + this.idStart); this.idStart += 1; } } node.attributes.ids.push(id); } this.ids[id] = node; return id; } setNameIdMap(node, id, msgnode, explicit) { node.attributes.names.forEach((name) => { if (name in this.nameIds) { this.setDuplicateNameId(node, id, name, msgnode, explicit); } else { this.nameIds[name] = id; this.nameTypes[name] = explicit || false; } }); } setDuplicateNameId(node, id, name, msgnode, explicit) { const oldId = this.nameIds[name]; const oldExplicit = this.nameTypes[name]; this.nameTypes[name] = oldExplicit || explicit || false; let oldNode; if (explicit) { if (oldExplicit) { let level = 2; if (oldId != null) { oldNode = this.ids[oldId]; if ("refuri" in node.attributes) { const { refuri } = node.attributes; if (oldNode.attributes.names.length && "refuri" in oldNode.attributes && oldNode.attributes.refuri === refuri) { level = 1; // just inform if refuri's identical } } if (level > 1) { dupname(oldNode, name); // worth seeing if the same behavior can be obtained // via deletion this.nameIds[name] = undefined; } } const msg = this.reporter.systemMessage(level, `Duplicate explicit target name: "${name}".`, [], { backrefs: [id], base_node: node }); if (msgnode != null) { msgnode.add(msg); } dupname(node, name); } else { this.nameIds[name] = id; if (oldId != null) { oldNode = this.ids[oldId]; dupname(oldNode, name); } } } else { if (oldId != null && !oldExplicit) { this.nameIds[name] = undefined; oldNode = this.ids[oldId]; dupname(oldNode, name); } dupname(node, name); } if (!explicit || (!oldExplicit && oldId != null)) { const msg = this.reporter.info(`Duplicate implicit target name: "${name}".`, [], { backrefs: [id], base_node: node }); if (msgnode != null && msg !== undefined) { msgnode.add(msg); } } } hasName(name) { return Object.keys(this.nameIds).includes(name); } noteImplicitTarget(target, msgnode) { const id = this.setId(target, msgnode); this.setNameIdMap(target, id, msgnode); } noteExplicitTarget(target, msgnode) { if (msgnode !== undefined) { const id = this.setId(target, msgnode); this.setNameIdMap(target, id, msgnode, true); } } noteRefname(node) { if (node === undefined || node.attributes.refname === undefined) { throw new InvalidStateError(); } const a = [node]; if (this.refNames[node.attributes.refname]) { this.refNames[node.attributes.refname].push(node); } else { this.refNames[node.attributes.refname] = a; } } noteRefId(node) { if (node === undefined || node.attributes.refid === undefined) { throw new InvalidStateError(); } const a = [node]; if (this.refIds[node.attributes.refid]) { this.refIds[node.attributes.refid].push(node); } else { this.refIds[node.attributes.refid] = a; } } noteIndirectTarget(target) { this.indirectTargets.push(target); // check this fixme if (target.names) { this.noteRefname(target); } } noteAnonymousTarget(target) { this.setId(target); } noteAutofootnote(footnote) { this.setId(footnote); this.autofootnotes.push(footnote); } noteAutofootnoteRef(ref) { this.setId(ref); this.autofootnoteRefs.push(ref); } noteSymbolFootnote(footnote) { this.setId(footnote); this.symbolFootnotes.push(footnote); } noteSymbolFootnoteRef(ref) { this.setId(ref); this.symbolFootnoteRefs.push(ref); } noteFootnote(footnote) { this.setId(footnote); this.footnotes.push(footnote); } noteFootnoteRef(ref) { if (ref === undefined || ref.attributes.refname === undefined) { throw new InvalidStateError(); } this.setId(ref); const a = [ref]; if (this.footnoteRefs[ref.attributes.refname]) { this.footnoteRefs[ref.attributes.refname].push(ref); } else { this.footnoteRefs[ref.attributes.refname] = a; } this.noteRefname(ref); } noteCitation(citation) { this.citations.push(citation); } noteCitationRef(ref) { if (ref === undefined || ref.attributes.refname === undefined) { throw new InvalidStateError(); } this.setId(ref);