typesxml
Version:
Open source XML library written in TypeScript
681 lines • 26.5 kB
JavaScript
"use strict";
/*******************************************************************************
* Copyright (c) 2023-2026 Maxprograms.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse License 1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/org/documents/epl-v10.html
*
* Contributors:
* Maxprograms - initial API and implementation
*******************************************************************************/
Object.defineProperty(exports, "__esModule", { value: true });
exports.xmlStringToJsonObject = xmlStringToJsonObject;
exports.xmlFileToJsonObject = xmlFileToJsonObject;
exports.xmlStreamToJsonObject = xmlStreamToJsonObject;
exports.xmlDocumentToJsonObject = xmlDocumentToJsonObject;
exports.xmlStringToJsonFile = xmlStringToJsonFile;
exports.xmlFileToJsonFile = xmlFileToJsonFile;
exports.xmlStreamToJsonFile = xmlStreamToJsonFile;
exports.xmlDocumentToJsonFile = xmlDocumentToJsonFile;
exports.jsonObjectToXmlDocument = jsonObjectToXmlDocument;
exports.jsonStringToXmlDocument = jsonStringToXmlDocument;
exports.jsonFileToXmlDocument = jsonFileToXmlDocument;
exports.jsonStreamToXmlDocument = jsonStreamToXmlDocument;
exports.jsonObjectToXmlFile = jsonObjectToXmlFile;
exports.jsonStringToXmlFile = jsonStringToXmlFile;
exports.jsonFileToXmlFile = jsonFileToXmlFile;
exports.jsonStreamToXmlFile = jsonStreamToXmlFile;
const promises_1 = require("node:fs/promises");
const DOMBuilder_js_1 = require("../DOMBuilder.js");
const SAXParser_js_1 = require("../SAXParser.js");
const XMLDocument_js_1 = require("../XMLDocument.js");
const XMLElement_js_1 = require("../XMLElement.js");
const XMLAttribute_js_1 = require("../XMLAttribute.js");
const TextNode_js_1 = require("../TextNode.js");
const CData_js_1 = require("../CData.js");
const XMLComment_js_1 = require("../XMLComment.js");
const ProcessingInstruction_js_1 = require("../ProcessingInstruction.js");
const XMLDeclaration_js_1 = require("../XMLDeclaration.js");
const XMLDocumentType_js_1 = require("../XMLDocumentType.js");
const RESERVED_KEYS = new Set([
"_attributes",
"_text",
"_cdata",
"_comments",
"_processingInstructions",
"_content"
]);
function xmlStringToJsonObject(xml, options) {
const { mode = "simple", ...parseOptions } = options ?? {};
const document = parseXmlFromString(xml, parseOptions);
if (mode === "roundtrip") {
return xmlDocumentToJsonObject(document, { mode: "roundtrip" });
}
return xmlDocumentToJsonObject(document);
}
async function xmlFileToJsonObject(path, options) {
const mode = options?.mode ?? "simple";
const encoding = options?.encoding ?? "utf8";
const xmlText = await (0, promises_1.readFile)(path, { encoding });
if (mode === "roundtrip") {
return xmlStringToJsonObject(xmlText, { mode: "roundtrip" });
}
return xmlStringToJsonObject(xmlText);
}
async function xmlStreamToJsonObject(stream, options) {
const { mode = "simple", ...streamOptions } = options ?? {};
const document = await parseXmlFromStream(stream, streamOptions);
if (mode === "roundtrip") {
return xmlDocumentToJsonObject(document, { mode: "roundtrip" });
}
return xmlDocumentToJsonObject(document);
}
function xmlDocumentToJsonObject(document, options) {
const root = document.getRoot();
if (!root) {
throw new Error("XML document does not contain a root element");
}
const mode = options?.mode ?? "simple";
const includeOrderedContent = mode === "roundtrip";
const rootValue = elementToJsonValue(root, { includeOrderedContent });
if (mode !== "roundtrip") {
return rootValue;
}
const result = {
rootName: root.getName(),
root: rootValue
};
const declaration = document.getXmlDeclaration();
if (declaration) {
const declarationJson = {};
if (declaration.getVersion()) {
declarationJson.version = declaration.getVersion();
}
if (declaration.getEncoding()) {
declarationJson.encoding = declaration.getEncoding();
}
const standalone = declaration.getStandalone();
if (standalone !== undefined) {
declarationJson.standalone = standalone;
}
if (Object.keys(declarationJson).length > 0) {
result.declaration = declarationJson;
}
}
const docType = document.getDocumentType();
if (docType) {
const doctypeJson = {
name: docType.name
};
const publicId = docType.getPublicId();
if (publicId) {
doctypeJson.publicId = publicId;
}
const systemId = docType.getSystemId();
if (systemId) {
doctypeJson.systemId = systemId;
}
const internalSubset = docType.getInternalSubset();
if (internalSubset) {
doctypeJson.internalSubset = internalSubset;
}
result.doctype = doctypeJson;
}
const prolog = [];
const epilog = [];
let seenRoot = false;
let docTypeEncountered = false;
for (const node of document.contentIterator()) {
if (node instanceof XMLDeclaration_js_1.XMLDeclaration) {
continue;
}
if (node instanceof XMLDocumentType_js_1.XMLDocumentType) {
docTypeEncountered = true;
continue;
}
if (node instanceof XMLElement_js_1.XMLElement) {
if (!seenRoot) {
seenRoot = true;
}
continue;
}
if (node instanceof XMLComment_js_1.XMLComment) {
if (seenRoot) {
const entry = { type: "comment", value: node.getValue() };
epilog.push(entry);
}
else {
const entry = { type: "comment", value: node.getValue() };
if (docTypeEncountered) {
entry.afterDoctype = true;
}
prolog.push(entry);
}
continue;
}
if (node instanceof ProcessingInstruction_js_1.ProcessingInstruction) {
const data = node.getData();
const entryBase = {
type: "processingInstruction",
target: node.getTarget()
};
if (data && data.length > 0) {
entryBase.data = data;
}
if (seenRoot) {
epilog.push(entryBase);
}
else {
const entry = { ...entryBase };
if (docTypeEncountered) {
entry.afterDoctype = true;
}
prolog.push(entry);
}
continue;
}
if (node instanceof TextNode_js_1.TextNode) {
const entryBase = {
type: "text",
value: node.getValue()
};
if (seenRoot) {
epilog.push(entryBase);
}
else {
const entry = { ...entryBase };
if (docTypeEncountered) {
entry.afterDoctype = true;
}
prolog.push(entry);
}
}
}
if (prolog.length > 0) {
result.prolog = prolog;
}
if (epilog.length > 0) {
result.epilog = epilog;
}
return result;
}
async function xmlStringToJsonFile(xml, targetPath, options, indent = 2, encoding = "utf8") {
const payloadSource = options?.mode === "roundtrip"
? xmlStringToJsonObject(xml, options)
: xmlStringToJsonObject(xml, options);
const payload = JSON.stringify(payloadSource, null, indent);
await (0, promises_1.writeFile)(targetPath, payload, { encoding });
}
async function xmlFileToJsonFile(path, targetPath, xmlEncoding = "utf8", indent = 2, jsonEncoding = "utf8", options) {
const document = await parseXmlFromFile(path, xmlEncoding);
await xmlDocumentToJsonFile(document, targetPath, indent, jsonEncoding, options);
}
async function xmlStreamToJsonFile(stream, targetPath, options, indent = 2, encoding = "utf8") {
const { mode = "simple", ...streamOptions } = options ?? {};
const document = await parseXmlFromStream(stream, streamOptions);
if (mode === "roundtrip") {
await xmlDocumentToJsonFile(document, targetPath, indent, encoding, { mode: "roundtrip" });
}
else {
await xmlDocumentToJsonFile(document, targetPath, indent, encoding);
}
}
async function xmlDocumentToJsonFile(document, targetPath, indent = 2, encoding = "utf8", options) {
const payloadSource = options?.mode === "roundtrip"
? xmlDocumentToJsonObject(document, options)
: xmlDocumentToJsonObject(document, options);
const payload = JSON.stringify(payloadSource, null, indent);
await (0, promises_1.writeFile)(targetPath, payload, { encoding });
}
function jsonObjectToXmlDocument(json, rootElementName) {
if (isXmlJsonDocument(json)) {
return jsonDocumentToXmlDocument(json);
}
const resolvedRootName = rootElementName !== undefined
? rootElementName
: inferRootElementNameFromSimpleJson(json) ?? "json";
return simpleJsonObjectToXmlDocument(json, resolvedRootName);
}
function jsonStringToXmlDocument(jsonText, rootElementName = "json") {
const parsed = JSON.parse(jsonText);
return jsonObjectToXmlDocument(parsed, rootElementName);
}
async function jsonFileToXmlDocument(path, rootElementName = "json", encoding = "utf8") {
const jsonText = await (0, promises_1.readFile)(path, { encoding });
return jsonStringToXmlDocument(jsonText, rootElementName);
}
async function jsonStreamToXmlDocument(stream, rootElementName = "json", encoding = "utf8") {
const jsonText = await readStreamToString(stream, encoding);
return jsonStringToXmlDocument(jsonText, rootElementName);
}
async function jsonObjectToXmlFile(json, targetPath, rootElementName = "json", encoding = "utf8") {
const document = jsonObjectToXmlDocument(json, rootElementName);
await (0, promises_1.writeFile)(targetPath, document.toString(), { encoding });
}
async function jsonStringToXmlFile(jsonText, targetPath, rootElementName = "json", encoding = "utf8") {
const document = jsonStringToXmlDocument(jsonText, rootElementName);
await (0, promises_1.writeFile)(targetPath, document.toString(), { encoding });
}
async function jsonFileToXmlFile(path, targetPath, rootElementName = "json", jsonEncoding = "utf8", xmlEncoding = "utf8") {
const document = await jsonFileToXmlDocument(path, rootElementName, jsonEncoding);
await (0, promises_1.writeFile)(targetPath, document.toString(), { encoding: xmlEncoding });
}
async function jsonStreamToXmlFile(stream, targetPath, rootElementName = "json", jsonEncoding = "utf8", xmlEncoding = "utf8") {
const document = await jsonStreamToXmlDocument(stream, rootElementName, jsonEncoding);
await (0, promises_1.writeFile)(targetPath, document.toString(), { encoding: xmlEncoding });
}
function parseXmlFromString(xml, options) {
const builder = new DOMBuilder_js_1.DOMBuilder();
builder.initialize();
const parser = new SAXParser_js_1.SAXParser();
parser.setContentHandler(builder);
parser.parseString(xml, options);
const document = builder.getDocument();
if (!document) {
throw new Error("Unable to parse XML string");
}
return document;
}
async function parseXmlFromFile(path, encoding = "utf8") {
const xmlText = await (0, promises_1.readFile)(path, { encoding });
return parseXmlFromString(xmlText);
}
async function parseXmlFromStream(stream, options) {
const builder = new DOMBuilder_js_1.DOMBuilder();
builder.initialize();
const parser = new SAXParser_js_1.SAXParser();
parser.setContentHandler(builder);
await parser.parseStream(stream, options);
const document = builder.getDocument();
if (!document) {
throw new Error("Unable to parse XML stream");
}
return document;
}
function elementToJsonValue(element, config) {
const attributes = element.getAttributes();
const childGroups = new Map();
const textPieces = new Array();
const cdataPieces = new Array();
const commentPieces = new Array();
const processingInstructions = new Array();
const orderedContent = config.includeOrderedContent ? new Array() : undefined;
const childOccurrences = config.includeOrderedContent ? new Map() : undefined;
for (const node of element.getContent()) {
if (node instanceof XMLElement_js_1.XMLElement) {
const childName = node.getName();
const converted = elementToJsonValue(node, config);
let bucket = childGroups.get(childName);
if (!bucket) {
bucket = new Array();
childGroups.set(childName, bucket);
}
bucket.push(converted);
if (orderedContent && childOccurrences) {
const occurrence = childOccurrences.get(childName) ?? 0;
childOccurrences.set(childName, occurrence + 1);
orderedContent.push({ kind: "element", name: childName, occurrence });
}
continue;
}
if (node instanceof TextNode_js_1.TextNode) {
textPieces.push(node.getValue());
if (orderedContent) {
orderedContent.push({ kind: "text", value: node.getValue() });
}
continue;
}
if (node instanceof CData_js_1.CData) {
cdataPieces.push(node.getValue());
if (orderedContent) {
orderedContent.push({ kind: "cdata", value: node.getValue() });
}
continue;
}
if (node instanceof XMLComment_js_1.XMLComment) {
commentPieces.push(node.getValue());
if (orderedContent) {
orderedContent.push({ kind: "comment", value: node.getValue() });
}
continue;
}
if (node instanceof ProcessingInstruction_js_1.ProcessingInstruction) {
const entry = { target: node.getTarget() };
const data = node.getData();
if (data && data.length > 0) {
entry.data = data;
}
processingInstructions.push(entry);
if (orderedContent) {
orderedContent.push({ kind: "processingInstruction", target: node.getTarget(), data });
}
}
}
const hasChildElements = childGroups.size > 0;
const concatenatedText = textPieces.join("");
const hasSignificantText = textPieces.length > 0 && (!hasChildElements || textPieces.some((value) => value.trim().length > 0));
const hasAttributes = attributes.length > 0;
const hasCData = cdataPieces.length > 0;
const hasComments = commentPieces.length > 0;
const hasProcessing = processingInstructions.length > 0;
if (!hasAttributes && !hasChildElements && !hasCData && !hasComments && !hasProcessing) {
if (!hasSignificantText) {
return "";
}
return concatenatedText;
}
if (!hasAttributes && !hasComments && !hasProcessing && !hasCData && !hasSignificantText && childGroups.size === 1) {
const [childName, values] = [...childGroups.entries()][0];
if (childName === deriveArrayItemName(element.getName())) {
return values;
}
}
const result = {};
if (hasAttributes) {
const attributeMap = {};
for (const attribute of attributes) {
attributeMap[attribute.getName()] = attribute.getValue();
}
result._attributes = attributeMap;
}
if (hasSignificantText) {
result._text = concatenatedText;
}
if (hasCData) {
result._cdata = cdataPieces.length === 1 ? cdataPieces[0] : cdataPieces;
}
if (hasComments) {
result._comments = commentPieces.length === 1 ? commentPieces[0] : commentPieces;
}
if (hasProcessing) {
result._processingInstructions = processingInstructions;
}
for (const [childName, values] of childGroups.entries()) {
if (values.length === 1) {
result[childName] = values[0];
}
else {
result[childName] = values;
}
}
if (orderedContent && orderedContent.length > 0) {
const actualOrder = orderedContent.map((entry) => {
if (entry.kind === "element") {
return `element:${entry.name}:${entry.occurrence}`;
}
return entry.kind;
});
const fallbackOrder = new Array();
if (hasSignificantText) {
fallbackOrder.push("text");
}
if (hasCData) {
for (let index = 0; index < cdataPieces.length; index++) {
fallbackOrder.push("cdata");
}
}
if (hasComments) {
for (let index = 0; index < commentPieces.length; index++) {
fallbackOrder.push("comment");
}
}
if (hasProcessing) {
for (let index = 0; index < processingInstructions.length; index++) {
fallbackOrder.push("processingInstruction");
}
}
for (const [childName, values] of childGroups.entries()) {
for (let index = 0; index < values.length; index++) {
fallbackOrder.push(`element:${childName}:${index}`);
}
}
const needsOrderedContent = actualOrder.length !== fallbackOrder.length || actualOrder.some((value, index) => value !== fallbackOrder[index]);
if (needsOrderedContent) {
result._content = orderedContent;
}
}
if (!hasAttributes && !hasSignificantText && !hasCData && !hasComments && !hasProcessing && childGroups.size === 0) {
return "";
}
return result;
}
function jsonDocumentToXmlDocument(jsonDoc) {
const document = new XMLDocument_js_1.XMLDocument();
if (jsonDoc.declaration) {
const version = jsonDoc.declaration.version ?? "1.0";
const encoding = jsonDoc.declaration.encoding ?? "utf-8";
const declaration = new XMLDeclaration_js_1.XMLDeclaration(version, encoding, jsonDoc.declaration.standalone);
document.setXmlDeclaration(declaration);
}
const prologNodes = jsonDoc.prolog ?? [];
for (const node of prologNodes) {
if (!node.afterDoctype) {
appendMiscNode(document, node);
}
}
if (jsonDoc.doctype) {
const publicId = jsonDoc.doctype.publicId ?? "";
const systemId = jsonDoc.doctype.systemId ?? "";
const docType = new XMLDocumentType_js_1.XMLDocumentType(jsonDoc.doctype.name, publicId, systemId);
if (jsonDoc.doctype.internalSubset) {
docType.setInternalSubset(jsonDoc.doctype.internalSubset);
}
document.setDocumentType(docType);
}
for (const node of prologNodes) {
if (node.afterDoctype) {
appendMiscNode(document, node);
}
}
const root = jsonValueToElement(jsonDoc.rootName, jsonDoc.root, jsonDoc.rootName);
document.setRoot(root);
if (jsonDoc.epilog) {
for (const node of jsonDoc.epilog) {
appendMiscNode(document, node);
}
}
return document;
}
function appendMiscNode(document, node) {
if (node.type === "comment") {
document.addComment(new XMLComment_js_1.XMLComment(node.value));
}
else if (node.type === "processingInstruction") {
const data = node.data ?? "";
document.addProcessingInstruction(new ProcessingInstruction_js_1.ProcessingInstruction(node.target, data));
}
else {
document.addTextNode(new TextNode_js_1.TextNode(node.value));
}
}
function simpleJsonObjectToXmlDocument(value, rootElementName) {
const document = new XMLDocument_js_1.XMLDocument();
const root = jsonValueToElement(rootElementName, value, rootElementName);
document.setRoot(root);
return document;
}
function inferRootElementNameFromSimpleJson(value) {
if (value === null || value === undefined) {
return undefined;
}
if (Array.isArray(value)) {
return undefined;
}
if (typeof value !== "object") {
return undefined;
}
const objectValue = value;
const candidateKeys = Object.keys(objectValue).filter((key) => !RESERVED_KEYS.has(key));
if (candidateKeys.length !== 1) {
return undefined;
}
return candidateKeys[0];
}
function jsonValueToElement(name, value, contextName) {
const element = new XMLElement_js_1.XMLElement(name);
if (value === null || value === undefined) {
return element;
}
if (Array.isArray(value)) {
const itemName = deriveArrayItemName(contextName);
for (const item of value) {
const child = jsonValueToElement(itemName, item, itemName);
element.addElement(child);
}
if (value.length === 0) {
element.addString("");
}
return element;
}
if (typeof value === "string") {
if (value.length > 0) {
element.addString(value);
}
return element;
}
if (typeof value === "number" || typeof value === "boolean") {
element.addString(String(value));
return element;
}
const objectValue = value;
if (objectValue._attributes) {
for (const [attrName, attrValue] of Object.entries(objectValue._attributes)) {
if (attrValue === null || attrValue === undefined) {
continue;
}
element.setAttribute(new XMLAttribute_js_1.XMLAttribute(attrName, String(attrValue)));
}
}
if (objectValue._content && objectValue._content.length > 0) {
appendOrderedContent(element, objectValue, objectValue._content);
return element;
}
if (objectValue._text !== undefined && objectValue._text.length > 0) {
element.addString(objectValue._text);
}
if (objectValue._cdata !== undefined) {
const cdataValues = ensureArrayOfStrings(objectValue._cdata);
for (const cdata of cdataValues) {
element.addCData(new CData_js_1.CData(cdata));
}
}
if (objectValue._comments !== undefined) {
const commentValues = ensureArrayOfStrings(objectValue._comments);
for (const comment of commentValues) {
element.addComment(new XMLComment_js_1.XMLComment(comment));
}
}
if (objectValue._processingInstructions) {
for (const pi of objectValue._processingInstructions) {
const data = pi.data ?? "";
element.addProcessingInstruction(new ProcessingInstruction_js_1.ProcessingInstruction(pi.target, data));
}
}
for (const [childName, childValue] of Object.entries(objectValue)) {
if (RESERVED_KEYS.has(childName)) {
continue;
}
if (childValue === undefined) {
continue;
}
if (Array.isArray(childValue)) {
for (const entry of childValue) {
const childElement = jsonValueToElement(childName, entry, childName);
element.addElement(childElement);
}
}
else {
const childElement = jsonValueToElement(childName, childValue, childName);
element.addElement(childElement);
}
}
return element;
}
function appendOrderedContent(element, source, content) {
const childCache = new Map();
for (const entry of content) {
switch (entry.kind) {
case "text":
element.addTextNode(new TextNode_js_1.TextNode(entry.value));
break;
case "cdata":
element.addCData(new CData_js_1.CData(entry.value));
break;
case "comment":
element.addComment(new XMLComment_js_1.XMLComment(entry.value));
break;
case "processingInstruction": {
const data = entry.data ?? "";
element.addProcessingInstruction(new ProcessingInstruction_js_1.ProcessingInstruction(entry.target, data));
break;
}
case "element": {
const values = getChildValues(source, entry.name, childCache);
const jsonValue = values[entry.occurrence];
if (jsonValue === undefined) {
break;
}
const childElement = jsonValueToElement(entry.name, jsonValue, entry.name);
element.addElement(childElement);
break;
}
}
}
}
function getChildValues(source, childName, cache) {
let values = cache.get(childName);
if (!values) {
const rawValue = source[childName];
if (rawValue === undefined) {
values = [];
}
else if (Array.isArray(rawValue)) {
if (rawValue.length > 0 && typeof rawValue[0] === "object" && rawValue[0] !== null && "kind" in rawValue[0]) {
values = [];
}
else {
values = rawValue;
}
}
else {
values = [rawValue];
}
cache.set(childName, values);
}
return values;
}
function ensureArrayOfStrings(value) {
return Array.isArray(value) ? value : [value];
}
function deriveArrayItemName(parentName) {
if (parentName.length > 3 && parentName.endsWith("ies")) {
return parentName.substring(0, parentName.length - 3) + "y";
}
if (parentName.length > 1 && parentName.endsWith("s")) {
return parentName.substring(0, parentName.length - 1);
}
return "item";
}
function isXmlJsonDocument(value) {
if (!value || typeof value !== "object") {
return false;
}
const candidate = value;
return typeof candidate.rootName === "string" && candidate.root !== undefined;
}
async function readStreamToString(stream, encoding) {
return new Promise((resolve, reject) => {
const chunks = new Array();
stream.setEncoding(encoding);
stream.on("data", (chunk) => {
chunks.push(chunk);
});
stream.once("error", reject);
stream.once("end", () => {
resolve(chunks.join(""));
});
});
}
//# sourceMappingURL=JsonConversion.js.map