libxmljs
Version:
libxml bindings for v8 javascript engine
874 lines (706 loc) • 25 kB
text/typescript
import { xmlDocPtr, xmlNodePtr, xmlDtdPtr, xmlNsPtr, XMLReferenceType, xmlXPathObjectPtr } from "./bindings/types";
import { XMLReference, createXMLReference, createXMLReferenceOrThrow } from "./bindings";
import {
xmlXPathNewContext,
xmlXPathNodeEval,
xmlUnlinkNode,
xmlHasProp,
xmlSetProp,
xmlAddChild,
xmlDocCopyNode,
xmlNewCDataBlock,
xmlNodeGetContent,
xmlNodeSetContent,
xmlNodeSetName,
xmlGetNodePath,
xmlCopyNode,
xmlAddNextSibling,
xmlAddPrevSibling,
xmlReplaceNode,
xmlNewNs,
xmlSetNs,
xmlEncodeEntitiesReentrant,
xmlStringGetNodeList,
xmlGetNsList,
xmlXPathRegisterNs,
xmlGetLineNo,
xmlDocSetRootElement,
xmlSaveTree,
xmlReconciliateNs,
xmlSetTreeDoc,
xmlXPathFreeObject,
xmlXPathFreeContext,
} from "./bindings/functions";
import { xmlSaveCtxtPtr }from "./bindings/types"
import bindings from "./bindings";
export enum XMLNodeError {
NO_REF = "Node has no native reference",
NO_DOC = "Node has no document",
}
export type XPathNamespace = string | XMLNamespace | { [key: string]: string };
export type XMLAttributeMap = {
[key: string]: string | number;
};
import { XMLDocument } from "./document";
import { XMLElementType, XMLSaveOptions } from "./types";
export type XMLXPathNode = XMLNode | XMLAttribute | XMLElement;
enum xmlXPathObjectType {
XPATH_UNDEFINED = bindings.XPATH_UNDEFINED, // 0
XPATH_NODESET = bindings.XPATH_NODESET, // 1
XPATH_BOOLEAN = bindings.XPATH_BOOLEAN, // 2
XPATH_NUMBER = bindings.XPATH_NUMBER, // 3
XPATH_STRING = bindings.XPATH_STRING, // 4
XPATH_POINT = bindings.XPATH_POINT, // 5
XPATH_RANGE = bindings.XPATH_RANGE, // 6
XPATH_LOCATIONSET = bindings.XPATH_LOCATIONSET, // 7
XPATH_USERS = bindings.XPATH_USERS, // 8
XPATH_XSLT_TREE = bindings.XPATH_XSLT_TREE, // 9 : An XSLT value tree, non modifiable
}
function refToNodeType(node: xmlNodePtr | xmlDocPtr | null): XMLNode | XMLAttribute | XMLElement | XMLDocument | null {
if (node === null) {
return null;
}
if (
node.type === XMLElementType.XML_DOCUMENT_NODE ||
node.type === XMLElementType.XML_DOCB_DOCUMENT_NODE ||
node.type === XMLElementType.XML_HTML_DOCUMENT_NODE
) {
return createXMLReference(XMLDocument, node);
}
if (node.type === XMLElementType.XML_ATTRIBUTE_NODE) {
return createXMLReference(XMLAttribute, node);
}
if (node.type === XMLElementType.XML_ELEMENT_NODE) {
return createXMLReference(XMLElement, node);
}
if (node.type === XMLElementType.XML_TEXT_NODE) {
return createXMLReference(XMLText, node);
}
return createXMLReference(XMLNode, node);
}
export class XMLNode extends XMLReference<xmlNodePtr> {
/**
* @private
* @param _ref
*/
constructor(_ref: any) {
super(_ref);
}
/**
* Get the parent node for the current
*
* @returns {XMLElement} parent node
*/
public parent() {
const _ref = this.getNativeReference();
if (_ref.parent === null) {
return refToNodeType(_ref.doc);
}
return refToNodeType(_ref.parent);
}
/**
* Get the tag name of current node
*
* @returns {string} name
*/
public name(): string {
const _ref = this.getNativeReference();
if (
_ref.type === XMLElementType.XML_DOCUMENT_NODE ||
_ref.type === XMLElementType.XML_DOCB_DOCUMENT_NODE ||
_ref.type === XMLElementType.XML_HTML_DOCUMENT_NODE
) {
return createXMLReferenceOrThrow(XMLDocument, _ref, XMLNodeError.NO_REF).name();
}
if (
_ref.type === XMLElementType.XML_ATTRIBUTE_NODE ||
_ref.type === XMLElementType.XML_ELEMENT_NODE ||
_ref.type === XMLElementType.XML_PI_NODE
) {
return _ref.name;
}
if (_ref.type === XMLElementType.XML_NAMESPACE_DECL) {
return createXMLReferenceOrThrow(XMLNamespace, _ref, XMLNodeError.NO_REF).name();
}
return this.type();
}
/**
* Get the line number that this node starts on in the original parsed document
*
* @returns {number} line number
*/
public line() {
const _ref = this.getNativeReference();
return xmlGetLineNo(_ref);
}
/**
* Get the type of element (XMLElementType)
*
* "text" | "cdata" | "comment" | "element" | "node"
*
* @returns {string} the node type
*/
public type() {
const _ref = this.getNativeReference();
switch (_ref.type) {
case XMLElementType.XML_ELEMENT_NODE:
return "element";
case XMLElementType.XML_ATTRIBUTE_NODE:
return "attribute";
case XMLElementType.XML_TEXT_NODE:
return "text";
case XMLElementType.XML_CDATA_SECTION_NODE:
return "cdata";
case XMLElementType.XML_ENTITY_REF_NODE:
return "entity_ref";
case XMLElementType.XML_ENTITY_NODE:
return "entity";
case XMLElementType.XML_PI_NODE:
return "pi";
case XMLElementType.XML_COMMENT_NODE:
return "comment";
case XMLElementType.XML_DOCUMENT_NODE:
return "document";
case XMLElementType.XML_DOCUMENT_TYPE_NODE:
return "document_type";
case XMLElementType.XML_DOCUMENT_FRAG_NODE:
return "document_frag";
case XMLElementType.XML_NOTATION_NODE:
return "notation";
case XMLElementType.XML_HTML_DOCUMENT_NODE:
return "html_document";
case XMLElementType.XML_DTD_NODE:
return "dtd";
case XMLElementType.XML_ELEMENT_DECL:
return "element_decl";
case XMLElementType.XML_ATTRIBUTE_DECL:
return "attribute_decl";
case XMLElementType.XML_ENTITY_DECL:
return "entity_decl";
case XMLElementType.XML_NAMESPACE_DECL:
return "namespace_decl";
case XMLElementType.XML_XINCLUDE_START:
return "xinclude_start";
case XMLElementType.XML_XINCLUDE_END:
return "xinclude_end";
case XMLElementType.XML_DOCB_DOCUMENT_NODE:
return "docb_document";
}
return "node";
}
/**
* Create a new element with the given name and content,
* then append it as a child of the current node
*
* @see XMLNode.createElement
* @param name
* @param content
* @returns the appended element
*/
public node(name: string, content?: string): XMLElement {
return this.addChild(this.createElement(name, content));
}
/**
* Create a new element with the given name and content
*
* @param name
* @param content
* @returns the created element
*/
public createElement(name: string, content?: string): XMLElement {
const _ref = this.getNativeReference();
if (_ref.doc === null) {
throw new Error(XMLNodeError.NO_DOC);
}
return createXMLReferenceOrThrow(XMLDocument, this.getNativeReference().doc, XMLNodeError.NO_REF).createElement(
name,
content
);
}
public defineNamespace(prefix: string | null, href?: string | null): XMLNamespace {
if (!href) {
href = prefix;
prefix = null;
}
return createXMLReferenceOrThrow(
XMLNamespace,
xmlNewNs(this.getNativeReference(), href, prefix),
XMLNodeError.NO_REF
);
}
/**
* @private
* @param _docRef
*/
public setDocumentRoot(_docRef: xmlDocPtr) {
xmlDocSetRootElement(_docRef, this.getNativeReference());
}
/**
* Get the previous sibling relative to the current node
*
* @returns {XMLElement | null} previous element
*/
public prevElement() {
let node = this.prevSibling();
while (node !== null && node.type() !== "element") {
node = node.prevSibling();
}
return node;
}
/**
* Get the next sibling relative to the current node
*
* @returns {XMLElement | null} next element
*/
public nextElement() {
let node = this.nextSibling();
while (node !== null && node.type() !== "element") {
node = node.nextSibling();
}
return node;
}
protected importNode(node: xmlNodePtr): XMLNode {
const _ref = this.getNativeReference();
if (_ref.doc === node.doc) {
if (node.parent !== null) {
xmlUnlinkNode(node);
}
return createXMLReferenceOrThrow(XMLNode, node, XMLNodeError.NO_REF);
}
return createXMLReferenceOrThrow(XMLNode, xmlDocCopyNode(node, _ref.doc, 1), XMLNodeError.NO_REF);
}
private childWillMerge(_nodeRef: xmlNodePtr) {
const _selfRef = this.getNativeReference();
return (
_nodeRef.type === XMLElementType.XML_TEXT_NODE &&
_selfRef.last !== null &&
_selfRef.last.type === XMLElementType.XML_TEXT_NODE &&
_selfRef.last.name === _nodeRef.name &&
_selfRef.last !== _nodeRef
);
}
/**
* Append node after the last node child
*
* @param node the XMLElement to append
* @returns the appended child
*/
public addChild(node: XMLElement): XMLElement {
const _parentRef = this.getNativeReference();
const _childRef = node.getNativeReference();
let childNode: xmlNodePtr | null = this.importNode(_childRef).getNativeReference();
if (childNode === _childRef && this.childWillMerge(childNode)) {
childNode = xmlAddChild(_parentRef, xmlCopyNode(childNode, 0));
} else {
childNode = xmlAddChild(_parentRef, childNode);
}
return createXMLReferenceOrThrow(XMLElement, childNode, XMLNodeError.NO_REF);
}
private nextSiblingWillMerge(node: XMLElement) {
const _selfRef = this.getNativeReference();
const _nodeRef = node.getNativeReference();
return (
_nodeRef.type === XMLElementType.XML_TEXT_NODE &&
(_selfRef.type === XMLElementType.XML_TEXT_NODE ||
(_selfRef.next !== null &&
_selfRef.next.type === XMLElementType.XML_TEXT_NODE &&
_selfRef.name === _selfRef.next.name))
); // libxml2 bug?
}
/**
* Insert a node directly after the current node
*
* @param node the node to be inserted
* @returns the inserted sibling
*/
public addNextSibling(node: XMLElement) {
const _parentRef = this.getNativeReference();
const _childRef = node.getNativeReference();
let childNode: xmlNodePtr | null = this.importNode(_childRef).getNativeReference();
if (childNode === _childRef && this.nextSiblingWillMerge(node)) {
childNode = xmlCopyNode(childNode, 0);
}
return createXMLReference(XMLElement, xmlAddNextSibling(_parentRef, childNode));
}
private prevSiblingWillMerge(node: XMLElement) {
const _selfRef = this.getNativeReference();
const _nodeRef = node.getNativeReference();
return (
_nodeRef.type === XMLElementType.XML_TEXT_NODE &&
(_selfRef.type === XMLElementType.XML_TEXT_NODE ||
(_selfRef.prev !== null &&
_selfRef.prev.type === XMLElementType.XML_TEXT_NODE &&
_selfRef.name === _selfRef.prev.name))
);
}
/**
* Insert a node directly before the current node
*
* @param node the node to be inserted
* @returns the inserted sibling
*/
public addPrevSibling(node: XMLElement) {
const _parentRef = this.getNativeReference();
const _childRef = node.getNativeReference();
let childNode: xmlNodePtr | null = this.importNode(_childRef).getNativeReference();
if (childNode === _childRef && this.prevSiblingWillMerge(node)) {
childNode = xmlCopyNode(childNode, 0);
}
return createXMLReferenceOrThrow(XMLElement, xmlAddPrevSibling(_parentRef, childNode), XMLNodeError.NO_REF);
}
/**
* Get an array of child nodes
*
* @returns {XMLElement[]} array of child nodes
*/
public childNodes() {
const _ref = this.getNativeReference()
const children: XMLElement[] = [];
let child = _ref.children;
while (child !== null) {
children.push(createXMLReferenceOrThrow(XMLElement, child, XMLNodeError.NO_REF));
child = child.next;
}
return children;
}
public prevSibling() {
return createXMLReference(XMLNode, this.getNativeReference().prev);
}
public nextSibling() {
return createXMLReference(XMLNode, this.getNativeReference().next);
}
public evaluateXPath(xpath: string, namespace?: XPathNamespace): xmlXPathObjectPtr {
const _ref = this.getNativeReference();
const xpathContext = xmlXPathNewContext(_ref.doc);
if (namespace) {
if (namespace instanceof XMLNamespace) {
xmlXPathRegisterNs(xpathContext, namespace.prefix(), namespace.href());
} else if (typeof namespace === "string") {
xmlXPathRegisterNs(xpathContext, "xmlns", namespace);
} else {
Object.keys(namespace).forEach((prefix) => {
const href = namespace[prefix];
xmlXPathRegisterNs(xpathContext, prefix, href || null);
});
}
}
const ret = xmlXPathNodeEval(_ref, xpath, xpathContext);
xmlXPathFreeContext(xpathContext);
return ret;
}
/**
* Find decendant nodes matching the given xpath selector
*
* @param xpath XPath selector
* @param namespace optional namespace
* @returns {XMLXPathNodeSet} array of matching decendant nodes
*/
public find(xpath: string, namespace?: XPathNamespace): XMLXPathNode[] {
const result = this.evaluateXPath(xpath, namespace);
const nodeSet: XMLXPathNode[] = [];
result.nodesetval.forEach((node) => {
const instance = refToNodeType(node);
if (instance !== null && !(instance instanceof XMLDocument)) {
nodeSet.push(instance);
}
});
xmlXPathFreeObject(result);
return nodeSet;
}
/**
* Find the first decendant node matching the given xpath selector
*
* @param xpath XPath selector
* @param namespace optional namespace
* @returns {XMLElement} first matching decendant node
*/
public get(xpath: string, namespace?: XPathNamespace): XMLXPathNode | boolean | number | string | null {
const result = this.evaluateXPath(xpath, namespace);
let ret: ReturnType<typeof XMLNode.prototype.get> = null;
if (result.type === xmlXPathObjectType.XPATH_BOOLEAN) {
ret = !!result.boolval;
} else if (result.type === xmlXPathObjectType.XPATH_NUMBER) {
ret = result.floatval;
} else if (result.type === xmlXPathObjectType.XPATH_STRING) {
ret = result.stringval;
} else if (result.type === xmlXPathObjectType.XPATH_NODESET) {
const _ref = result.nodesetval[0];
if (_ref) {
const node = refToNodeType(_ref);
if (node !== null && !(node instanceof XMLDocument)) {
ret = node;
}
}
}
xmlXPathFreeObject(result);
return ret;
}
/**
* Return the child at the given index
* @param index the index of the child
* @returns {XMLElement | null} the child to return
*/
public child(index: number) {
let currIndex = 0;
let _childRef = this.getNativeReference().children;
while (_childRef != null) {
if (currIndex >= index) {
break;
}
currIndex++;
_childRef = _childRef.next;
}
return createXMLReference(XMLElement, _childRef);
}
public remove() {
xmlUnlinkNode(this.getNativeReference());
}
/**
* Get the associated XMLDocument for the current node
*
* @returns {XMLDocument | null}
*/
public doc() {
const _ref = this.getNativeReference();
return createXMLReference(XMLDocument, _ref.doc);
}
public toString(options: XMLSaveOptions | boolean = {}): string {
return XMLDocument.toString(this, options) || "";
}
/**
* Get or set the namespace for the current node
*
* @param prefix namespace prefix
* @param href namespace URL
* @returns {XMLNamespace} the current namespace
*/
public namespace(prefix?: string | XMLNamespace | null, href?: string | null): XMLNamespace | null {
const _ref = this.getNativeReference();
if (prefix === null) {
_ref.ns = null;
} else if (prefix instanceof XMLNamespace) {
prefix._applyToNode(_ref);
} else if (typeof prefix === "string") {
if (!href) {
href = prefix;
prefix = null;
}
const namespace = this.doc()?._findNamespace(_ref, prefix, href) || this.defineNamespace(prefix, href);
if (namespace) {
namespace._applyToNode(_ref);
}
}
return createXMLReference(XMLNamespace, _ref.ns);
}
/**
* Get an array of namespaces that appy to the current node
*
* @param onlyLocal whether to include inherited namespaces
* @returns {XMLNamespace[]} an array of namespaces for the current node
*/
public namespaces(onlyLocal: boolean = false) {
const namespaces: XMLNamespace[] = [];
const _ref = this.getNativeReference();
if (onlyLocal === true) {
let namespace = _ref.nsDef;
while (namespace !== null) {
namespaces.push(createXMLReferenceOrThrow(XMLNamespace, namespace, XMLNodeError.NO_REF));
namespace = namespace.next;
}
} else {
xmlGetNsList(_ref.doc, _ref).forEach((namespace) => {
namespaces.push(createXMLReferenceOrThrow(XMLNamespace, namespace, XMLNodeError.NO_REF));
});
}
return namespaces;
}
public clone() {
const _ref = this.getNativeReference();
return refToNodeType(xmlDocCopyNode(_ref, _ref.doc, 1));
}
/**
* @private
* @param context
*/
public _xmlSaveTree(context: xmlSaveCtxtPtr) {
xmlSaveTree(context, this.getNativeReference());
}
}
export class XMLElement extends XMLNode {
public name(value?: string): string {
if (value !== undefined) {
xmlNodeSetName(this.getNativeReference(), value);
}
return super.name();
}
public getAttribute(key: string): XMLAttribute | null {
const _ref = this.getNativeReference();
return createXMLReference(XMLAttribute, xmlHasProp(_ref, key));
}
public setAttribute(key: string, value: string | number): XMLAttribute | null {
const _ref = this.getNativeReference();
return createXMLReference(XMLAttribute, xmlSetProp(_ref, key, value.toString()));
}
/**
* set multiple attributes
* BREAKING CHANGE: no longer overloaded for setting single attr
* @param attributes
* @returns
*/
public attr(attributes: XMLAttributeMap): XMLElement {
if (typeof attributes === "string") {
console.error("Use XMLElement.setAttribute instead");
return this;
}
Object.keys(attributes).forEach((k) => {
this.setAttribute(k, attributes[k] || "");
});
return this;
}
public attrs(): XMLAttribute[] {
const attrs: XMLAttribute[] = [];
const _ref = this.getNativeReference();
let attr = _ref.properties;
while (attr !== null) {
attrs.push(createXMLReferenceOrThrow(XMLAttribute, attr, XMLNodeError.NO_REF));
attr = attr.next;
}
return attrs;
}
public cdata(content: string) {
const _ref = this.getNativeReference();
const cdata = xmlNewCDataBlock(_ref.doc, content, content.length);
this.addChild(createXMLReferenceOrThrow(XMLElement, cdata, XMLNodeError.NO_REF));
return this;
}
public text(content?: string) {
const _ref = this.getNativeReference();
if (content === undefined) {
return xmlNodeGetContent(_ref);
}
const doc = this.doc();
if (doc && this.type() !== "comment") {
content = doc.encode(content);
}
this.childNodes().forEach((child) => {
xmlUnlinkNode(child.getNativeReference());
});
xmlNodeSetContent(_ref, content);
return content;
}
public path() {
return xmlGetNodePath(this.getNativeReference());
}
public replace(value: XMLElement | string) {
const _ref = this.getNativeReference();
let element: XMLNode | null = null;
const doc = this.doc();
if (doc && typeof value === "string") {
element = doc.createText(value, false);
} else if (value instanceof XMLNode) {
element = this.importNode(value.getNativeReference());
}
const _newRef = xmlReplaceNode(_ref, (element as XMLElement).getNativeReference());
if (_newRef !== null) {
this.setNativeReference(_newRef);
}
}
}
export class XMLNamespace extends XMLReference<xmlNsPtr> {
/**
* @private
* @param _ref
*/
constructor(_ref: any) {
super(_ref);
}
public name() {
return this.prefix() || "";
}
public prefix() {
return this.getNativeReference().prefix || null;
}
public href(): string {
return this.getNativeReference().href;
}
/**
* @private
* @param _nodeRef
*/
public _applyToNode(_nodeRef: xmlNodePtr) {
xmlSetNs(_nodeRef, this.getNativeReference());
}
}
export class XMLAttribute extends XMLNode {
/**
* @private
* @param _ref
*/
constructor(_ref: any) {
super(_ref);
}
public name(): string {
return this.getNativeReference().name;
}
public defineNamespace(prefix: string | null, href?: string | null): XMLNamespace {
// should probably do xmlSetNsProp
return this.node().defineNamespace(prefix, href);
}
public value(value?: string): string {
const _ref = this.getNativeReference();
if (typeof value === "string") {
const content = xmlEncodeEntitiesReentrant(_ref.doc, value);
_ref.children = xmlStringGetNodeList(_ref.doc, content);
_ref.last = null;
let child = _ref.children;
while (child !== null) {
child.parent = _ref;
child.doc = _ref.doc;
if (child.next === null) {
_ref.last = child;
}
child = child.next;
}
}
return _ref.children?.content || "";
}
public node(): XMLElement {
return createXMLReferenceOrThrow(XMLElement, this.getNativeReference().parent, XMLNodeError.NO_REF);
}
}
export class XMLDTD extends XMLReference<xmlDtdPtr> {
public name: string;
public externalId: string | null;
public systemId: string | null;
/**
* @private
* @param _ref
*/
constructor(_ref: any) {
super(_ref);
this.name = this.getName();
this.externalId = this.getExternalID();
this.systemId = this.getSystemID();
}
public getName(): string {
return this.getNativeReference()?.name || "";
}
public getExternalID(): string | null {
return this.getNativeReference()?.ExternalID || null;
}
public getSystemID(): string | null {
return this.getNativeReference()?.SystemID || null;
}
public unlink() {
xmlUnlinkNode(this.getNativeReference() as any);
}
}
export class XMLText extends XMLElement {
/**
* @private
* @param _ref
*/
constructor(_ref: any) {
super(_ref);
}
}