@tripsnek/tmf
Version:
TypeScript Modeling Framework - A TypeScript port of the Eclipse Modeling Framework (EMF)
329 lines • 11.5 kB
JavaScript
;
/**
* A drop-in replacement for xml2js that converts XML to JavaScript objects
* with the same structure and behavior as xml2js.parseString()
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.XmlToJsParser = void 0;
exports.parseString = parseString;
exports.parseStringSync = parseStringSync;
// Node type constants (in case Node global is not available)
const NODE_TYPES = {
ELEMENT_NODE: 1,
TEXT_NODE: 3,
CDATA_SECTION_NODE: 4,
};
class XmlToJsParser {
options;
constructor(options = {}) {
this.options = {
explicitArray: options.explicitArray !== false,
mergeAttrs: options.mergeAttrs === true,
explicitRoot: options.explicitRoot !== false,
ignoreAttrs: options.ignoreAttrs === true,
trim: options.trim === true,
normalize: options.normalize === true,
...options,
};
}
/**
* Parse XML string to JavaScript object (async version)
*/
parseString(xml) {
return new Promise((resolve, reject) => {
try {
const result = this.parseStringSync(xml);
resolve(result);
}
catch (error) {
reject(error);
}
});
}
/**
* Parse XML string to JavaScript object (sync version)
*/
parseStringSync(xml) {
if (this.isDOMParserAvailable()) {
return this.parseInBrowser(xml);
}
else {
// For Node.js environment, we'll need to implement our own XML parser
// or use a lightweight alternative
return this.parseInNode(xml);
}
}
/**
* Check if DOMParser is available (browser environment)
*/
isDOMParserAvailable() {
return (typeof globalThis !== 'undefined' &&
typeof globalThis.DOMParser !== 'undefined');
}
/**
* Browser implementation using DOMParser
*/
parseInBrowser(xml) {
try {
const parser = new globalThis.DOMParser();
const xmlDoc = parser.parseFromString(xml, 'text/xml');
// Check for parser errors
const parserError = xmlDoc.querySelector('parsererror');
if (parserError) {
throw new Error(`XML Parse Error: ${parserError.textContent}`);
}
if (!xmlDoc.documentElement) {
throw new Error('Invalid XML: No document element found');
}
const result = this.domElementToJs(xmlDoc.documentElement, true); // true = isRootLevel
if (this.options.explicitRoot) {
return { [xmlDoc.documentElement.tagName]: result };
}
else {
return result;
}
}
catch (error) {
throw new Error(`XML parsing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Node.js implementation using simple regex-based parser
* This is a lightweight XML parser that handles most common cases
*/
parseInNode(xml) {
// Remove XML declaration and comments
xml = xml.replace(/<\?xml[^>]*\?>/g, '');
xml = xml.replace(/<!--[\s\S]*?-->/g, '');
xml = xml.trim();
const stack = [];
const result = {};
let current = result;
let isRootLevel = true;
// Simple regex to match XML tags and content
const tagRegex = /<\/?([^>\s]+)([^>]*)>/g;
let lastIndex = 0;
let match;
while ((match = tagRegex.exec(xml)) !== null) {
const [fullMatch, tagName, attributesStr] = match;
const isClosing = fullMatch.startsWith('</');
const isSelfClosing = fullMatch.endsWith('/>');
// Handle text content before this tag
if (match.index > lastIndex) {
const textContent = xml.substring(lastIndex, match.index);
if (textContent.trim()) {
this.addTextContent(current, textContent);
}
}
if (isClosing) {
// Closing tag - pop from stack
if (stack.length > 0) {
current = stack.pop();
if (stack.length === 0) {
isRootLevel = true;
}
}
}
else {
// Opening tag or self-closing tag
const element = {};
// Parse attributes
if (attributesStr) {
if (!this.options.ignoreAttrs && attributesStr.trim()) {
const attrs = this.parseAttributes(attributesStr);
if (Object.keys(attrs).length > 0) {
if (this.options.mergeAttrs) {
Object.assign(element, attrs);
}
else {
element.$ = attrs;
}
}
}
}
// Add element to current parent (don't force array for root level)
if (tagName)
this.addChildElement(current, tagName, element, isRootLevel);
// If not self-closing, push to stack for children
if (!isSelfClosing) {
stack.push(current);
current = element;
isRootLevel = false;
}
}
lastIndex = tagRegex.lastIndex;
}
// Handle any remaining text content
if (lastIndex < xml.length) {
const textContent = xml.substring(lastIndex);
if (textContent.trim()) {
this.addTextContent(current, textContent);
}
}
// Return the root element
const rootKeys = Object.keys(result);
if (rootKeys.length === 1 && this.options.explicitRoot) {
return result;
}
else if (rootKeys.length === 1) {
return result[rootKeys[0]];
}
else {
return result;
}
}
/**
* Convert DOM element to JavaScript object (for browser)
*/
domElementToJs(element, isRootLevel = false) {
const result = {};
// Handle attributes
if (!this.options.ignoreAttrs &&
element.attributes &&
element.attributes.length > 0) {
const attrs = {};
for (let i = 0; i < element.attributes.length; i++) {
const attr = element.attributes[i];
attrs[attr.name] = attr.value;
}
if (this.options.mergeAttrs) {
Object.assign(result, attrs);
}
else {
result.$ = attrs;
}
}
// Group child elements by tag name
const childElements = {};
const textParts = [];
// Process all child nodes
for (let i = 0; i < element.childNodes.length; i++) {
const child = element.childNodes[i];
if (child.nodeType === NODE_TYPES.ELEMENT_NODE) {
const childElement = child;
const tagName = childElement.tagName;
if (!childElements[tagName]) {
childElements[tagName] = [];
}
childElements[tagName].push(childElement);
}
else if (child.nodeType === NODE_TYPES.TEXT_NODE ||
child.nodeType === NODE_TYPES.CDATA_SECTION_NODE) {
const textContent = child.textContent || '';
if (textContent.trim()) {
textParts.push(textContent);
}
}
}
// Add child elements to result
for (const [tagName, elements] of Object.entries(childElements)) {
const childObjects = elements.map((el) => this.domElementToJs(el, false)); // children are not root level
// Use the helper method to maintain consistency with Node.js implementation
if (elements.length === 1) {
this.addChildElement(result, tagName, childObjects[0], false);
}
else {
// Multiple elements with same tag name always become arrays
result[tagName] = childObjects;
}
}
// Handle text content
if (textParts.length > 0 && Object.keys(childElements).length === 0) {
const textContent = textParts.join('');
const finalText = this.options.trim ? textContent.trim() : textContent;
if (this.options.normalize) {
result._ = finalText.replace(/\s+/g, ' ');
}
else {
result._ = finalText;
}
}
return result;
}
/**
* Parse attribute string into object
*/
parseAttributes(attributesStr) {
const attrs = {};
const attrRegex = /(\S+)=["']([^"']*)["']/g;
let match;
while ((match = attrRegex.exec(attributesStr)) !== null) {
const [, name, value] = match;
if (name)
attrs[name] = value;
}
return attrs;
}
/**
* Add text content to current element
*/
addTextContent(current, textContent) {
const trimmed = textContent.trim();
if (trimmed) {
const finalText = this.options.trim ? trimmed : textContent;
const normalizedText = this.options.normalize
? finalText.replace(/\s+/g, ' ')
: finalText;
if (current._) {
current._ += normalizedText;
}
else {
current._ = normalizedText;
}
}
}
/**
* Add child element to parent
*/
addChildElement(parent, tagName, element, isRootLevel = false) {
if (parent[tagName]) {
// Convert to array if not already
if (!Array.isArray(parent[tagName])) {
parent[tagName] = [parent[tagName]];
}
parent[tagName].push(element);
}
else {
// For root level elements, don't force array even if explicitArray is true
// xml2js only uses arrays for child elements, not the root element
if (this.options.explicitArray && !isRootLevel) {
parent[tagName] = [element];
}
else {
parent[tagName] = element;
}
}
}
}
exports.XmlToJsParser = XmlToJsParser;
// Default instance with xml2js-compatible defaults
const defaultParser = new XmlToJsParser({
explicitArray: true,
mergeAttrs: false,
explicitRoot: true,
ignoreAttrs: false,
trim: false,
normalize: false,
});
/**
* Drop-in replacement for xml2js.parseString()
*/
function parseString(xml, callback) {
if (callback) {
defaultParser
.parseString(xml)
.then((result) => callback(null, result))
.catch((err) => callback(err));
return Promise.resolve();
}
else {
return defaultParser.parseString(xml);
}
}
/**
* Synchronous version
*/
function parseStringSync(xml) {
return defaultParser.parseStringSync(xml);
}
//# sourceMappingURL=xml-to-js-parser.js.map