docutils-ts
Version:
Port of the Python Docutils library to TypeScript
1,458 lines (1,448 loc) • 74.8 kB
JavaScript
/**
* 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 [¡-ÿ]
* escape {unicode}|\\[ -~¡-ÿ]
* 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);