xmlbuilder2
Version:
An XML builder for node.js
684 lines • 28.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.XMLBuilderCBImpl = void 0;
const interfaces_1 = require("../interfaces");
const util_1 = require("@oozcitak/util");
const BuilderFunctions_1 = require("./BuilderFunctions");
const algorithm_1 = require("@oozcitak/dom/lib/algorithm");
const infra_1 = require("@oozcitak/infra");
const NamespacePrefixMap_1 = require("@oozcitak/dom/lib/serializer/NamespacePrefixMap");
const LocalNameSet_1 = require("@oozcitak/dom/lib/serializer/LocalNameSet");
const util_2 = require("@oozcitak/dom/lib/util");
const XMLCBWriter_1 = require("../writers/XMLCBWriter");
const JSONCBWriter_1 = require("../writers/JSONCBWriter");
const YAMLCBWriter_1 = require("../writers/YAMLCBWriter");
const events_1 = require("events");
/**
* Represents a readable XML document stream.
*/
class XMLBuilderCBImpl extends events_1.EventEmitter {
static _VoidElementNames = new Set(['area', 'base', 'basefont',
'bgsound', 'br', 'col', 'embed', 'frame', 'hr', 'img', 'input', 'keygen',
'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr']);
_options;
_builderOptions;
_writer;
_fragment;
_hasDeclaration = false;
_docTypeName = "";
_hasDocumentElement = false;
_currentElement;
_currentElementSerialized = false;
_openTags = [];
_prefixMap;
_prefixIndex;
_ended = false;
/**
* Initializes a new instance of `XMLStream`.
*
* @param options - stream writer options
* @param fragment - whether to create fragment stream or a document stream
*
* @returns XML stream
*/
constructor(options, fragment = false) {
super();
this._fragment = fragment;
// provide default options
this._options = (0, util_1.applyDefaults)(options || {}, interfaces_1.DefaultXMLBuilderCBOptions);
this._builderOptions = {
defaultNamespace: this._options.defaultNamespace,
namespaceAlias: this._options.namespaceAlias
};
if (this._options.format === "json") {
this._writer = new JSONCBWriter_1.JSONCBWriter(this._options);
}
else if (this._options.format === "yaml") {
this._writer = new YAMLCBWriter_1.YAMLCBWriter(this._options);
}
else {
this._writer = new XMLCBWriter_1.XMLCBWriter(this._options);
}
// automatically create listeners for callbacks passed via options
if (this._options.data !== undefined) {
this.on("data", this._options.data);
}
if (this._options.end !== undefined) {
this.on("end", this._options.end);
}
if (this._options.error !== undefined) {
this.on("error", this._options.error);
}
this._prefixMap = new NamespacePrefixMap_1.NamespacePrefixMap();
this._prefixMap.set("xml", infra_1.namespace.XML);
this._prefixIndex = { value: 1 };
this._push(this._writer.frontMatter());
}
/** @inheritdoc */
ele(p1, p2, p3) {
// parse if JS object or XML or JSON string
if ((0, util_1.isObject)(p1) || ((0, util_1.isString)(p1) && (/^\s*</.test(p1) || /^\s*[\{\[]/.test(p1) || /^(\s*|(#.*)|(%.*))*---/.test(p1)))) {
const frag = (0, BuilderFunctions_1.fragment)().set(this._options);
try {
frag.ele(p1);
}
catch (err) {
this.emit("error", err);
return this;
}
for (const node of frag.node.childNodes) {
this._fromNode(node);
}
return this;
}
this._serializeOpenTag(true);
if (!this._fragment && this._hasDocumentElement && this._writer.level === 0) {
this.emit("error", new Error("Document cannot have multiple document element nodes."));
return this;
}
try {
this._currentElement = (0, BuilderFunctions_1.fragment)(this._builderOptions).ele(p1, p2, p3);
}
catch (err) {
this.emit("error", err);
return this;
}
if (!this._fragment && !this._hasDocumentElement && this._docTypeName !== ""
&& this._currentElement.node._qualifiedName !== this._docTypeName) {
this.emit("error", new Error("Document element name does not match DocType declaration name."));
return this;
}
this._currentElementSerialized = false;
if (!this._fragment) {
this._hasDocumentElement = true;
}
return this;
}
/** @inheritdoc */
att(p1, p2, p3) {
if (this._currentElement === undefined) {
this.emit("error", new Error("Cannot insert an attribute node as child of a document node."));
return this;
}
try {
this._currentElement.att(p1, p2, p3);
}
catch (err) {
this.emit("error", err);
return this;
}
return this;
}
/** @inheritdoc */
com(content) {
this._serializeOpenTag(true);
let node;
try {
node = (0, BuilderFunctions_1.fragment)(this._builderOptions).com(content).first().node;
}
catch (err) {
/* istanbul ignore next */
this.emit("error", err);
/* istanbul ignore next */
return this;
}
if (this._options.wellFormed && (!(0, algorithm_1.xml_isLegalChar)(node.data) ||
node.data.indexOf("--") !== -1 || node.data.endsWith("-"))) {
this.emit("error", new Error("Comment data contains invalid characters (well-formed required)."));
return this;
}
this._push(this._writer.comment(node.data));
return this;
}
/** @inheritdoc */
txt(content) {
if (!this._fragment && this._currentElement === undefined) {
this.emit("error", new Error("Cannot insert a text node as child of a document node."));
return this;
}
this._serializeOpenTag(true);
let node;
try {
node = (0, BuilderFunctions_1.fragment)(this._builderOptions).txt(content).first().node;
}
catch (err) {
/* istanbul ignore next */
this.emit("error", err);
/* istanbul ignore next */
return this;
}
if (this._options.wellFormed && !(0, algorithm_1.xml_isLegalChar)(node.data)) {
this.emit("error", new Error("Text data contains invalid characters (well-formed required)."));
return this;
}
const markup = node.data.replace(/(?!&(lt|gt|amp|apos|quot);)&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
this._push(this._writer.text(markup));
const lastEl = this._openTags[this._openTags.length - 1];
// edge case: text on top level.
if (lastEl) {
lastEl[lastEl.length - 1] = true;
}
return this;
}
/** @inheritdoc */
ins(target, content = '') {
this._serializeOpenTag(true);
let node;
try {
node = (0, BuilderFunctions_1.fragment)(this._builderOptions).ins(target, content).first().node;
}
catch (err) {
/* istanbul ignore next */
this.emit("error", err);
/* istanbul ignore next */
return this;
}
if (this._options.wellFormed && (node.target.indexOf(":") !== -1 || (/^xml$/i).test(node.target))) {
this.emit("error", new Error("Processing instruction target contains invalid characters (well-formed required)."));
return this;
}
if (this._options.wellFormed && !(0, algorithm_1.xml_isLegalChar)(node.data)) {
this.emit("error", Error("Processing instruction data contains invalid characters (well-formed required)."));
return this;
}
this._push(this._writer.instruction(node.target, node.data));
return this;
}
/** @inheritdoc */
dat(content) {
this._serializeOpenTag(true);
let node;
try {
node = (0, BuilderFunctions_1.fragment)(this._builderOptions).dat(content).first().node;
}
catch (err) {
this.emit("error", err);
return this;
}
this._push(this._writer.cdata(node.data));
return this;
}
/** @inheritdoc */
dec(options = { version: "1.0" }) {
if (this._fragment) {
this.emit("error", Error("Cannot insert an XML declaration into a document fragment."));
return this;
}
if (this._hasDeclaration) {
this.emit("error", Error("XML declaration is already inserted."));
return this;
}
this._push(this._writer.declaration(options.version || "1.0", options.encoding, options.standalone));
this._hasDeclaration = true;
return this;
}
/** @inheritdoc */
dtd(options) {
if (this._fragment) {
this.emit("error", Error("Cannot insert a DocType declaration into a document fragment."));
return this;
}
if (this._docTypeName !== "") {
this.emit("error", new Error("DocType declaration is already inserted."));
return this;
}
if (this._hasDocumentElement) {
this.emit("error", new Error("Cannot insert DocType declaration after document element."));
return this;
}
let node;
try {
node = (0, BuilderFunctions_1.create)().dtd(options).first().node;
}
catch (err) {
this.emit("error", err);
return this;
}
if (this._options.wellFormed && !(0, algorithm_1.xml_isPubidChar)(node.publicId)) {
this.emit("error", new Error("DocType public identifier does not match PubidChar construct (well-formed required)."));
return this;
}
if (this._options.wellFormed &&
(!(0, algorithm_1.xml_isLegalChar)(node.systemId) ||
(node.systemId.indexOf('"') !== -1 && node.systemId.indexOf("'") !== -1))) {
this.emit("error", new Error("DocType system identifier contains invalid characters (well-formed required)."));
return this;
}
this._docTypeName = options.name;
this._push(this._writer.docType(options.name, node.publicId, node.systemId));
return this;
}
/** @inheritdoc */
import(node) {
const frag = (0, BuilderFunctions_1.fragment)().set(this._options);
try {
frag.import(node);
}
catch (err) {
this.emit("error", err);
return this;
}
for (const node of frag.node.childNodes) {
this._fromNode(node);
}
return this;
}
/** @inheritdoc */
up() {
this._serializeOpenTag(false);
this._serializeCloseTag();
return this;
}
/** @inheritdoc */
end() {
this._serializeOpenTag(false);
while (this._openTags.length > 0) {
this._serializeCloseTag();
}
this._push(null);
return this;
}
/**
* Serializes the opening tag of an element node.
*
* @param hasChildren - whether the element node has child nodes
*/
_serializeOpenTag(hasChildren) {
if (this._currentElementSerialized)
return;
if (this._currentElement === undefined)
return;
const node = this._currentElement.node;
if (this._options.wellFormed && (node.localName.indexOf(":") !== -1 ||
!(0, algorithm_1.xml_isName)(node.localName))) {
this.emit("error", new Error("Node local name contains invalid characters (well-formed required)."));
return;
}
let qualifiedName = "";
let ignoreNamespaceDefinitionAttribute = false;
let map = this._prefixMap.copy();
let localPrefixesMap = {};
let localDefaultNamespace = this._recordNamespaceInformation(node, map, localPrefixesMap);
let inheritedNS = this._openTags.length === 0 ? null : this._openTags[this._openTags.length - 1][1];
let ns = node.namespaceURI;
if (ns === null)
ns = inheritedNS;
if (inheritedNS === ns) {
if (localDefaultNamespace !== null) {
ignoreNamespaceDefinitionAttribute = true;
}
if (ns === infra_1.namespace.XML) {
qualifiedName = "xml:" + node.localName;
}
else {
qualifiedName = node.localName;
}
this._writer.beginElement(qualifiedName);
this._push(this._writer.openTagBegin(qualifiedName));
}
else {
let prefix = node.prefix;
let candidatePrefix = null;
if (prefix !== null || ns !== localDefaultNamespace) {
candidatePrefix = map.get(prefix, ns);
}
if (prefix === "xmlns") {
if (this._options.wellFormed) {
this.emit("error", new Error("An element cannot have the 'xmlns' prefix (well-formed required)."));
return;
}
candidatePrefix = prefix;
}
if (candidatePrefix !== null) {
qualifiedName = candidatePrefix + ':' + node.localName;
if (localDefaultNamespace !== null && localDefaultNamespace !== infra_1.namespace.XML) {
inheritedNS = localDefaultNamespace || null;
}
this._writer.beginElement(qualifiedName);
this._push(this._writer.openTagBegin(qualifiedName));
}
else if (prefix !== null) {
if (prefix in localPrefixesMap) {
prefix = this._generatePrefix(ns, map, this._prefixIndex);
}
map.set(prefix, ns);
qualifiedName += prefix + ':' + node.localName;
this._writer.beginElement(qualifiedName);
this._push(this._writer.openTagBegin(qualifiedName));
this._push(this._writer.attribute("xmlns:" + prefix, this._serializeAttributeValue(ns, this._options.wellFormed)));
if (localDefaultNamespace !== null) {
inheritedNS = localDefaultNamespace || null;
}
}
else if (localDefaultNamespace === null ||
(localDefaultNamespace !== null && localDefaultNamespace !== ns)) {
ignoreNamespaceDefinitionAttribute = true;
qualifiedName += node.localName;
inheritedNS = ns;
this._writer.beginElement(qualifiedName);
this._push(this._writer.openTagBegin(qualifiedName));
this._push(this._writer.attribute("xmlns", this._serializeAttributeValue(ns, this._options.wellFormed)));
}
else {
qualifiedName += node.localName;
inheritedNS = ns;
this._writer.beginElement(qualifiedName);
this._push(this._writer.openTagBegin(qualifiedName));
}
}
this._serializeAttributes(node, map, this._prefixIndex, localPrefixesMap, ignoreNamespaceDefinitionAttribute, this._options.wellFormed);
const isHTML = (ns === infra_1.namespace.HTML);
if (isHTML && !hasChildren &&
XMLBuilderCBImpl._VoidElementNames.has(node.localName)) {
this._push(this._writer.openTagEnd(qualifiedName, true, true));
this._writer.endElement(qualifiedName);
}
else if (!isHTML && !hasChildren) {
this._push(this._writer.openTagEnd(qualifiedName, true, false));
this._writer.endElement(qualifiedName);
}
else {
this._push(this._writer.openTagEnd(qualifiedName, false, false));
}
this._currentElementSerialized = true;
/**
* Save qualified name, original inherited ns, original prefix map, and
* hasChildren flag.
*/
this._openTags.push([qualifiedName, inheritedNS, this._prefixMap, hasChildren, undefined]);
/**
* New values of inherited namespace and prefix map will be used while
* serializing child nodes. They will be returned to their original values
* when this node is closed using the _openTags array item we saved above.
*/
if (this._isPrefixMapModified(this._prefixMap, map)) {
this._prefixMap = map;
}
/**
* Calls following this will either serialize child nodes or close this tag.
*/
this._writer.level++;
}
/**
* Serializes the closing tag of an element node.
*/
_serializeCloseTag() {
this._writer.level--;
const lastEle = this._openTags.pop();
/* istanbul ignore next */
if (lastEle === undefined) {
this.emit("error", new Error("Last element is undefined."));
return;
}
const [qualifiedName, ns, map, hasChildren, hasTextPayload] = lastEle;
/**
* Restore original values of inherited namespace and prefix map.
*/
this._prefixMap = map;
if (!hasChildren)
return;
this._push(this._writer.closeTag(qualifiedName, hasTextPayload));
this._writer.endElement(qualifiedName);
}
/**
* Pushes data to internal buffer.
*
* @param data - data
*/
_push(data) {
if (data === null) {
this._ended = true;
this.emit("end");
}
else if (this._ended) {
this.emit("error", new Error("Cannot push to ended stream."));
}
else if (data.length !== 0) {
this._writer.hasData = true;
this.emit("data", data, this._writer.level);
}
}
/**
* Reads and serializes an XML tree.
*
* @param node - root node
*/
_fromNode(node) {
if (util_2.Guard.isElementNode(node)) {
const name = node.prefix ? node.prefix + ":" + node.localName : node.localName;
if (node.namespaceURI !== null) {
this.ele(node.namespaceURI, name);
}
else {
this.ele(name);
}
for (const attr of node.attributes) {
const name = attr.prefix ? attr.prefix + ":" + attr.localName : attr.localName;
if (attr.namespaceURI !== null) {
this.att(attr.namespaceURI, name, attr.value);
}
else {
this.att(name, attr.value);
}
}
for (const child of node.childNodes) {
this._fromNode(child);
}
this.up();
}
else if (util_2.Guard.isExclusiveTextNode(node) && node.data) {
this.txt(node.data);
}
else if (util_2.Guard.isCommentNode(node)) {
this.com(node.data);
}
else if (util_2.Guard.isCDATASectionNode(node)) {
this.dat(node.data);
}
else if (util_2.Guard.isProcessingInstructionNode(node)) {
this.ins(node.target, node.data);
}
}
/**
* Produces an XML serialization of the attributes of an element node.
*
* @param node - node to serialize
* @param map - namespace prefix map
* @param prefixIndex - generated namespace prefix index
* @param localPrefixesMap - local prefixes map
* @param ignoreNamespaceDefinitionAttribute - whether to ignore namespace
* attributes
* @param requireWellFormed - whether to check conformance
*/
_serializeAttributes(node, map, prefixIndex, localPrefixesMap, ignoreNamespaceDefinitionAttribute, requireWellFormed) {
const localNameSet = requireWellFormed ? new LocalNameSet_1.LocalNameSet() : undefined;
for (const attr of node.attributes) {
// Optimize common case
if (!requireWellFormed && !ignoreNamespaceDefinitionAttribute && attr.namespaceURI === null) {
this._push(this._writer.attribute(attr.localName, this._serializeAttributeValue(attr.value, this._options.wellFormed)));
continue;
}
if (requireWellFormed && localNameSet && localNameSet.has(attr.namespaceURI, attr.localName)) {
this.emit("error", new Error("Element contains duplicate attributes (well-formed required)."));
return;
}
if (requireWellFormed && localNameSet)
localNameSet.set(attr.namespaceURI, attr.localName);
let attributeNamespace = attr.namespaceURI;
let candidatePrefix = null;
if (attributeNamespace !== null) {
candidatePrefix = map.get(attr.prefix, attributeNamespace);
if (attributeNamespace === infra_1.namespace.XMLNS) {
if (attr.value === infra_1.namespace.XML ||
(attr.prefix === null && ignoreNamespaceDefinitionAttribute) ||
(attr.prefix !== null && (!(attr.localName in localPrefixesMap) ||
localPrefixesMap[attr.localName] !== attr.value) &&
map.has(attr.localName, attr.value)))
continue;
if (requireWellFormed && attr.value === infra_1.namespace.XMLNS) {
this.emit("error", new Error("XMLNS namespace is reserved (well-formed required)."));
return;
}
if (requireWellFormed && attr.value === '') {
this.emit("error", new Error("Namespace prefix declarations cannot be used to undeclare a namespace (well-formed required)."));
return;
}
if (attr.prefix === 'xmlns')
candidatePrefix = 'xmlns';
/**
* _Note:_ The (candidatePrefix === null) check is not in the spec.
* We deviate from the spec here. Otherwise a prefix is generated for
* all attributes with namespaces.
*/
}
else if (candidatePrefix === null) {
if (attr.prefix !== null &&
(!map.hasPrefix(attr.prefix) ||
map.has(attr.prefix, attributeNamespace))) {
/**
* Check if we can use the attribute's own prefix.
* We deviate from the spec here.
* TODO: This is not an efficient way of searching for prefixes.
* Follow developments to the spec.
*/
candidatePrefix = attr.prefix;
}
else {
candidatePrefix = this._generatePrefix(attributeNamespace, map, prefixIndex);
}
this._push(this._writer.attribute("xmlns:" + candidatePrefix, this._serializeAttributeValue(attributeNamespace, this._options.wellFormed)));
}
}
if (requireWellFormed && (attr.localName.indexOf(":") !== -1 ||
!(0, algorithm_1.xml_isName)(attr.localName) ||
(attr.localName === "xmlns" && attributeNamespace === null))) {
this.emit("error", new Error("Attribute local name contains invalid characters (well-formed required)."));
return;
}
this._push(this._writer.attribute((candidatePrefix !== null ? candidatePrefix + ":" : "") + attr.localName, this._serializeAttributeValue(attr.value, this._options.wellFormed)));
}
}
/**
* Produces an XML serialization of an attribute value.
*
* @param value - attribute value
* @param requireWellFormed - whether to check conformance
*/
_serializeAttributeValue(value, requireWellFormed) {
if (requireWellFormed && value !== null && !(0, algorithm_1.xml_isLegalChar)(value)) {
this.emit("error", new Error("Invalid characters in attribute value."));
return "";
}
if (value === null)
return "";
return value.replace(/(?!&(lt|gt|amp|apos|quot);)&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
/**
* Records namespace information for the given element and returns the
* default namespace attribute value.
*
* @param node - element node to process
* @param map - namespace prefix map
* @param localPrefixesMap - local prefixes map
*/
_recordNamespaceInformation(node, map, localPrefixesMap) {
let defaultNamespaceAttrValue = null;
for (const attr of node.attributes) {
let attributeNamespace = attr.namespaceURI;
let attributePrefix = attr.prefix;
if (attributeNamespace === infra_1.namespace.XMLNS) {
if (attributePrefix === null) {
defaultNamespaceAttrValue = attr.value;
continue;
}
else {
let prefixDefinition = attr.localName;
let namespaceDefinition = attr.value;
if (namespaceDefinition === infra_1.namespace.XML) {
continue;
}
if (namespaceDefinition === '') {
namespaceDefinition = null;
}
if (map.has(prefixDefinition, namespaceDefinition)) {
continue;
}
map.set(prefixDefinition, namespaceDefinition);
localPrefixesMap[prefixDefinition] = namespaceDefinition || '';
}
}
}
return defaultNamespaceAttrValue;
}
/**
* Generates a new prefix for the given namespace.
*
* @param newNamespace - a namespace to generate prefix for
* @param prefixMap - namespace prefix map
* @param prefixIndex - generated namespace prefix index
*/
_generatePrefix(newNamespace, prefixMap, prefixIndex) {
let generatedPrefix = "ns" + prefixIndex.value;
prefixIndex.value++;
prefixMap.set(generatedPrefix, newNamespace);
return generatedPrefix;
}
/**
* Determines if the namespace prefix map was modified from its original.
*
* @param originalMap - original namespace prefix map
* @param newMap - new namespace prefix map
*/
_isPrefixMapModified(originalMap, newMap) {
const items1 = originalMap._items;
const items2 = newMap._items;
const nullItems1 = originalMap._nullItems;
const nullItems2 = newMap._nullItems;
for (const key in items2) {
const arr1 = items1[key];
if (arr1 === undefined)
return true;
const arr2 = items2[key];
if (arr1.length !== arr2.length)
return true;
for (let i = 0; i < arr1.length; i++) {
if (arr1[i] !== arr2[i])
return true;
}
}
if (nullItems1.length !== nullItems2.length)
return true;
for (let i = 0; i < nullItems1.length; i++) {
if (nullItems1[i] !== nullItems2[i])
return true;
}
return false;
}
}
exports.XMLBuilderCBImpl = XMLBuilderCBImpl;
//# sourceMappingURL=XMLBuilderCBImpl.js.map