UNPKG

libxmljs

Version:

libxml bindings for v8 javascript engine

559 lines (433 loc) 15.3 kB
import { XMLElement, XMLDTD, XPathNamespace, XMLNode, XMLText, XMLNodeError, XMLNamespace } from "./node"; import { parseHtml, parseXml, parseXmlAsync, parseHtmlAsync, } from "./parse"; import { XMLParseOptions, HTMLParseOptions, XMLStructuredError, XMLSaveOptions, XMLSaveFlags, XMLDocumentError, DEFAULT_XML_PARSE_OPTIONS, DEFAULT_HTML_PARSE_OPTIONS, } from "./types"; import { XMLReference, createXMLReference, createXMLReferenceOrThrow } from "./bindings"; import { xmlDocPtr, xmlNodePtr, xmlNsPtr, XMLReferenceType, xmlSaveCtxtPtr } from "./bindings/types"; import { xmlRelaxNGFree, xmlRelaxNGFreeParserCtxt, xmlRelaxNGFreeValidCtxt, xmlRelaxNGNewDocParserCtxt, xmlRelaxNGNewValidCtxt, xmlRelaxNGParse, xmlRelaxNGValidateDoc, xmlResetLastError, xmlDocGetRootElement, xmlGetIntSubset, xmlUnlinkNode, xmlCreateIntSubset, xmlBufferCreate, xmlSaveToBuffer, xmlSaveTree, xmlSaveFlush, xmlSaveClose, xmlBufferContent, xmlBufferFree, xmlEncodeSpecialChars, xmlNewDocNode, xmlDocSetRootElement, xmlSchemaNewDocParserCtxt, xmlSchemaParse, xmlSchemaNewValidCtxt, xmlSchemaValidateDoc, xmlSchemaFreeValidCtxt, xmlSchemaFree, xmlSchemaFreeParserCtxt, xmlGetLastError, withStructuredErrors, xmlNewDoc, xmlNewDocText, xmlNewDocComment, xmlNewDocPI, xmlSearchNs, xmlSearchNsByHref, xmlNewNs, xmlSetNs, } from "./bindings/functions"; export const DEFAULT_XML_SAVE_OPTIONS: XMLSaveOptions = { format: true, }; const flagsToOptions = (array: XMLSaveFlags[]): number => { let options = 0; array.forEach((v: XMLSaveFlags) => { options += v; }); return options; }; export class XMLDocument extends XMLReference<xmlDocPtr> { public errors: XMLStructuredError[]; public validationErrors: any[]; /** * @private * @param _ref */ constructor(_ref: any) { super(_ref); this.errors = []; this.validationErrors = []; } /** * @private * @param _ref * @param encoding * @returns */ public static createDocument(_ref: null | string | Buffer = null, encoding: string = "utf8"): XMLDocument { let _docRef: xmlDocPtr | null; if (_ref === null) { _docRef = xmlNewDoc("1.0"); } else if (typeof _ref === "string" || _ref instanceof Buffer) { _docRef = xmlNewDoc(_ref); } else { throw new Error(XMLDocumentError.NO_REF); } if (_docRef && encoding) { _docRef.encoding = encoding; } return createXMLReferenceOrThrow(XMLDocument, _docRef, XMLDocumentError.NO_REF); } public createText(content?: string, encode: boolean = true): XMLText { return createXMLReferenceOrThrow( XMLText, xmlNewDocText(this.getNativeReference(), encode ? this.encode(content || "") : content || ""), XMLDocumentError.NO_REF ); } public createComment(content?: string): XMLText { return createXMLReferenceOrThrow( XMLText, xmlNewDocComment(this.getNativeReference(), this.encode(content || "")), XMLDocumentError.NO_REF ); } public createProcessingInstruction( name: string, content?: string): XMLText { return createXMLReferenceOrThrow( XMLText, xmlNewDocPI(this.getNativeReference(), name, this.encode(content || "")), XMLDocumentError.NO_REF ); } /** * @private * @param _nodeRef * @param prefix * @param href */ public _findNamespace(_nodeRef: xmlNodePtr, prefix?: string | null, href?: string | null) { const _docRef = this.doc().getNativeReference(); let _nsRef: xmlNsPtr | null = null; if (typeof prefix === "string") { _nsRef = xmlSearchNs(_docRef, _nodeRef, prefix); } if (!_nsRef && typeof href === "string") { _nsRef = xmlSearchNsByHref(_docRef, _nodeRef, href); } return createXMLReference(XMLNamespace, _nsRef) } /** * Get or set the document root * * @param node the node to set as the document root * @returns the root node for the document */ public root(node?: XMLElement): XMLElement | null { const _docRef = this.getNativeReference(); if (node !== undefined) { node.setDocumentRoot(_docRef); } return createXMLReference(XMLElement, xmlDocGetRootElement(_docRef)); } /** * @see XMLNode.doc */ public doc() { return this; } /** * @see XMLNode.childNodes */ public childNodes() { const root = this.root(); if (root === null) { throw new Error(XMLDocumentError.NO_ROOT); } return root.childNodes(); } /** * @see XMLNode.find */ public find(xpath: string, namespace?: XPathNamespace) { const root = this.root(); if (root === null) { throw new Error(XMLDocumentError.NO_ROOT); } return root.find(xpath, namespace); } /** * see XMLNode.get */ public get(xpath: string, namespace?: XPathNamespace) { const root = this.root(); if (root === null) { throw new Error(XMLDocumentError.NO_ROOT); } return root.get(xpath, namespace); } /** * @see XMLNode.child */ public child(index: number) { const root = this.root(); if (root === null) { throw new Error(XMLDocumentError.NO_ROOT); } return root.child(index) || null; } public namespaces() { const root = this.root(); if (root === null) { throw new Error(XMLDocumentError.NO_ROOT); } return root.namespaces(); } /** * Validate the current document against the given schema * * @param schemaDoc document to validate against * @returns {boolean} valid */ public validate(schemaDoc: XMLDocument) { xmlResetLastError(); return withStructuredErrors((errors) => { const parser_ctxt = xmlSchemaNewDocParserCtxt(schemaDoc.getNativeReference()); if (parser_ctxt === null) { throw new Error("Could not create context for schema parser"); } const schema = xmlSchemaParse(parser_ctxt); if (schema === null) { throw new Error("Invalid XSD schema"); } const valid_ctxt = xmlSchemaNewValidCtxt(schema); if (valid_ctxt === null) { throw new Error("Unable to create a validation context for the schema"); } const valid = xmlSchemaValidateDoc(valid_ctxt, this.getNativeReference()) == 0; xmlSchemaFree(schema); xmlSchemaFreeValidCtxt(valid_ctxt); xmlSchemaFreeParserCtxt(parser_ctxt); this.validationErrors = errors; return valid; }); } public rngValidate(schemaDoc: XMLDocument): boolean { xmlResetLastError(); return withStructuredErrors((errors) => { const parser_ctxt = xmlRelaxNGNewDocParserCtxt( schemaDoc.getNativeReference() ); if (parser_ctxt === null) { throw new Error("Could not create context for schema parser"); } const schema = xmlRelaxNGParse(parser_ctxt); if (schema === null) { throw new Error("Invalid XSD schema"); } const valid_ctxt = xmlRelaxNGNewValidCtxt(schema); if (valid_ctxt === null) { throw new Error("Unable to create a validation context for the schema"); } const valid = xmlRelaxNGValidateDoc(valid_ctxt, this.getNativeReference()) == 0; xmlRelaxNGFree(schema); xmlRelaxNGFreeValidCtxt(valid_ctxt); xmlRelaxNGFreeParserCtxt(parser_ctxt); this.validationErrors = errors; return valid; }); } public name(): string { return "document"; } public node(name: string, content?: string): XMLElement { const node = this.root(this.createElement(name, content)); if (node === null) { throw new Error("Couldn't create root node"); } return node; } public encode(data: string): string { const _ref = this.getNativeReference(), content = xmlEncodeSpecialChars(_ref, data); if (content === null) { throw new Error("Couldn't encode, document is NULL"); } return content; } public createElement(name: string, content: string = ""): XMLElement { const encodedContent = this.encode(content); const node = createXMLReferenceOrThrow( XMLElement, xmlNewDocNode(this.getNativeReference(), null, name, encodedContent), XMLDocumentError.NO_REF ); // TODO: enable // xmlFree(encodedContent); return node; } public getDtd(): XMLDTD | null { const _ref = this.getNativeReference(); return createXMLReference(XMLDTD, xmlGetIntSubset(_ref)); } public setDtd(name: string, externalId: string | null = null, systemId: string | null = null): XMLDTD | null { const dtd = this.getDtd(); if (typeof name !== "string") { throw new Error("Invalid DTD name, must be a string"); } const _ref = this.getNativeReference(); dtd?.unlink(); const _newDtdRef = xmlCreateIntSubset(_ref, name, externalId, systemId); return createXMLReference(XMLDTD, _newDtdRef); } public fromHtml(buffer: string, options: HTMLParseOptions = DEFAULT_HTML_PARSE_OPTIONS): HTMLDocument { const _ref = parseHtml(buffer, options).getNativeReference(); this.setNativeReference(_ref); return this; } public fromXml(buffer: string, options: XMLParseOptions = DEFAULT_XML_PARSE_OPTIONS): HTMLDocument { const _ref = parseXml(buffer, options).getNativeReference(); this.setNativeReference(_ref); return this; } public version(): string | null { const _ref = this.getNativeReference(); return _ref.version || ""; } public encoding(value?: string): string { const _ref = this.getNativeReference(); if (value) { _ref.encoding = value; } return _ref.encoding || ""; } public getParseFlags(): number { return this.getNativeReference().parseFlags; } public type(): string { return "document"; } public toString(options: XMLSaveOptions | boolean = DEFAULT_XML_SAVE_OPTIONS): string { if (typeof options === "boolean") { return this.toString({ ...DEFAULT_XML_SAVE_OPTIONS, format: options, }); } return XMLDocument.toString(this, { ...DEFAULT_XML_SAVE_OPTIONS, ...options, }) || ""; } public static toString(node: XMLNode | XMLDocument, options: XMLSaveOptions | boolean = {}): string { if (typeof options === "boolean") { return this.toString(node, { format: options, }); } const flags: XMLSaveFlags[] = options.flags || []; if (options.declaration === false && flags.indexOf(XMLSaveFlags.XML_SAVE_NO_DECL) === -1) { flags.push(XMLSaveFlags.XML_SAVE_NO_DECL); } if (options.format === true && flags.indexOf(XMLSaveFlags.XML_SAVE_FORMAT) === -1) { flags.push(XMLSaveFlags.XML_SAVE_FORMAT); } if (options.selfCloseEmpty === false && flags.indexOf(XMLSaveFlags.XML_SAVE_NO_EMPTY) === -1) { flags.push(XMLSaveFlags.XML_SAVE_NO_EMPTY); } if (options.whitespace === true && flags.indexOf(XMLSaveFlags.XML_SAVE_WSNONSIG) === -1) { flags.push(XMLSaveFlags.XML_SAVE_WSNONSIG); } if (/^html$/i.test(options.type || "") && flags.indexOf(XMLSaveFlags.XML_SAVE_AS_HTML) === -1) { flags.push(XMLSaveFlags.XML_SAVE_AS_HTML); // if the document is XML and we want formatted HTML output // we must use the XHTML serializer because the default HTML // serializer only formats node->type = HTML_NODE and not XML_NODEs if (flags.indexOf(XMLSaveFlags.XML_SAVE_FORMAT) > -1 && flags.indexOf(XMLSaveFlags.XML_SAVE_XHTML) === -1) { flags.push(XMLSaveFlags.XML_SAVE_XHTML); } } else if (/^xhtml$/i.test(options.type || "") && flags.indexOf(XMLSaveFlags.XML_SAVE_XHTML) === -1) { flags.push(XMLSaveFlags.XML_SAVE_XHTML); } else if (/^xml$/i.test(options.type || "") && flags.indexOf(XMLSaveFlags.XML_SAVE_AS_XML) === -1) { flags.push(XMLSaveFlags.XML_SAVE_AS_XML); } const encoding = options.encoding || "UTF-8"; const buffer = xmlBufferCreate(); const context = xmlSaveToBuffer(buffer, encoding, flagsToOptions(flags)); node._xmlSaveTree(context); xmlSaveFlush(context); const content = xmlBufferContent(buffer); xmlSaveClose(context); xmlBufferFree(buffer); return content || ""; } /** * @private * @param context */ public _xmlSaveTree(context: xmlSaveCtxtPtr) { xmlSaveTree(context, this.getNativeReference() as any); } public static fromXml(buffer: string | Buffer, options?: XMLParseOptions): XMLDocument { return parseXml(buffer, options); } public static async fromXmlAsync( buffer: string | Buffer, options: XMLParseOptions = DEFAULT_XML_PARSE_OPTIONS ) { return parseXmlAsync(buffer, options) } public static fromHtml(buffer: string | Buffer, options?: HTMLParseOptions): HTMLDocument { return parseHtml(buffer, options); } public static async fromHtmlAsync( buffer: string | Buffer, options: HTMLParseOptions = DEFAULT_HTML_PARSE_OPTIONS ) { return parseHtmlAsync(buffer, options) } public static fromHtmlFragment(buffer: string, options?: HTMLParseOptions): HTMLDocument { if (options === undefined) { return HTMLDocument.fromHtmlFragment(buffer, {}); } options.doctype = false; options.implied = false; return parseHtml(buffer, options); } } export class HTMLDocument extends XMLDocument { public toString(options: XMLSaveOptions | boolean = {}): string { if (typeof options === "boolean") { return this.toString({ format: options, }); } if (!options?.type) { options.type = "html"; } return super.toString(options); } }