From b917da05cbd966d7887f33b26a31e248467f3459 Mon Sep 17 00:00:00 2001 From: William Summers Date: Sat, 19 Apr 2025 09:45:08 -0500 Subject: [PATCH 01/13] packaging refactor --- package.json | 4 +- rollup.config.js | 34 +- src/XMLJSONTransformer.js | 98 ++ src/components/ConfigurationManager.js | 152 +++ src/components/DomEnvironment.js | 58 + src/components/JSONToXMLConverter.js | 448 +++++++ src/components/NodeProcessor.js | 113 ++ src/components/PathNavigator.js | 241 ++++ src/components/SchemaGenerator.js | 212 ++++ src/index.js | 17 + src/types/index.d.ts | 18 + src/xml-json-transformer.js | 1123 ----------------- test/customMatchers.js | 55 - test/e2e/browser-compatibility.test.js | 0 test/fixtures/json-samples/mixed-content.json | 0 test/fixtures/json-samples/namespaces.json | 0 test/fixtures/json-samples/simple.json | 0 test/fixtures/json-samples/special-nodes.json | 0 test/fixtures/xml-samples/mixed-content.xml | 11 + test/fixtures/xml-samples/namespaces.xml | 14 + test/fixtures/xml-samples/simple.xml | 13 + test/fixtures/xml-samples/special-nodes.xml | 13 + test/helpers/customMatchers.js | 0 test/helpers/test-utils.js | 142 +++ test/integration/json-to-xml-workflow.test.js | 0 .../mixed-content-handling.test.js | 0 test/integration/xml-to-json-workflow.test.js | 375 ++++++ test/unit/ConfigurationManager.test.js | 143 +++ test/unit/DOMEnvironment.test.js | 0 test/unit/JSONToXMLConverter.test.js | 0 test/unit/NodeProcessor.test.js | 118 ++ test/unit/PathNavigator.test.js | 277 ++++ test/unit/SchemaGenerator.test.js | 0 test/unit/XMLJSONTransformer.test.js | 0 test/unit/XMLToJSONConverter.test.js | 0 test/unit/xml-json-getpath.test.js | 409 ------ test/unit/xml-json-transformFunction.test.js | 139 -- test/unit/xml-json-transformer.test.js | 452 ------- 38 files changed, 2481 insertions(+), 2198 deletions(-) create mode 100644 src/XMLJSONTransformer.js create mode 100644 src/components/ConfigurationManager.js create mode 100644 src/components/DomEnvironment.js create mode 100644 src/components/JSONToXMLConverter.js create mode 100644 src/components/NodeProcessor.js create mode 100644 src/components/PathNavigator.js create mode 100644 src/components/SchemaGenerator.js create mode 100644 src/index.js create mode 100644 src/types/index.d.ts delete mode 100644 src/xml-json-transformer.js delete mode 100644 test/customMatchers.js create mode 100644 test/e2e/browser-compatibility.test.js create mode 100644 test/fixtures/json-samples/mixed-content.json create mode 100644 test/fixtures/json-samples/namespaces.json create mode 100644 test/fixtures/json-samples/simple.json create mode 100644 test/fixtures/json-samples/special-nodes.json create mode 100644 test/fixtures/xml-samples/mixed-content.xml create mode 100644 test/fixtures/xml-samples/namespaces.xml create mode 100644 test/fixtures/xml-samples/simple.xml create mode 100644 test/fixtures/xml-samples/special-nodes.xml create mode 100644 test/helpers/customMatchers.js create mode 100644 test/helpers/test-utils.js create mode 100644 test/integration/json-to-xml-workflow.test.js create mode 100644 test/integration/mixed-content-handling.test.js create mode 100644 test/integration/xml-to-json-workflow.test.js create mode 100644 test/unit/ConfigurationManager.test.js create mode 100644 test/unit/DOMEnvironment.test.js create mode 100644 test/unit/JSONToXMLConverter.test.js create mode 100644 test/unit/NodeProcessor.test.js create mode 100644 test/unit/PathNavigator.test.js create mode 100644 test/unit/SchemaGenerator.test.js create mode 100644 test/unit/XMLJSONTransformer.test.js create mode 100644 test/unit/XMLToJSONConverter.test.js delete mode 100644 test/unit/xml-json-getpath.test.js delete mode 100644 test/unit/xml-json-transformFunction.test.js delete mode 100644 test/unit/xml-json-transformer.test.js diff --git a/package.json b/package.json index 6ac70ec..3012d75 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "name": "xmltojson", "version": "2.0.0", "type": "module", - "main": "./dist/xml-json-transformer.js", + "main": "./dist/index.js", "exports": { - ".": "./dist/xml-json-transformer.js" + ".": "./dist/index.js" }, "files": ["dist"], "scripts": { diff --git a/rollup.config.js b/rollup.config.js index e2fb3f6..ab3473c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -21,11 +21,11 @@ const banner = `/*! */`; export default [ - // Main ESM build (for both browser and Node.js) + // Main ESM build { - input: "src/xml-json-transformer.js", + input: "src/index.js", output: { - file: "./dist/xml-json-transformer.js", + file: "./dist/index.js", format: "es", banner, sourcemap: true, @@ -39,14 +39,14 @@ export default [ filesize(), ], }, - // Minified ESM build (for production use) + // Minified ESM version { - input: "src/xml-json-transformer.js", + input: "src/index.js", output: { - file: "./dist/xml-json-transformer.min.js", + file: "./dist/index.min.js", format: "es", banner, - sourcemap: false, // No source map for production + sourcemap: false, }, plugins: [ resolve(), @@ -54,16 +54,15 @@ export default [ babelHelpers: "bundled", exclude: "node_modules/**", }), - terser(), // Minification + terser(), filesize(), ], }, - // Browser-specific build (UMD for direct ]]> + + + + More CDATA]]> + + + \ No newline at end of file diff --git a/test/helpers/customMatchers.js b/test/helpers/customMatchers.js new file mode 100644 index 0000000..e69de29 diff --git a/test/helpers/test-utils.js b/test/helpers/test-utils.js new file mode 100644 index 0000000..42fd0eb --- /dev/null +++ b/test/helpers/test-utils.js @@ -0,0 +1,142 @@ +/** + * Test utilities for XMLJSONTransformer tests + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// Get directory name in ESM +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +/** + * Read a fixture file from the fixtures directory + * @param {string} filename - Name of the fixture file + * @param {string} type - Type of fixture ('xml-samples' or 'json-samples') + * @returns {string} - Content of the fixture file + */ +export const readFixture = (filename, type = 'xml-samples') => { + const fixturePath = path.join(__dirname, '../fixtures', type, filename); + return fs.readFileSync(fixturePath, 'utf8'); +}; + +/** + * Normalize XML string by removing whitespace and comments + * @param {string} xml - XML string to normalize + * @returns {string} - Normalized XML string + */ +export const normalizeXML = (xml) => { + return xml + .replace(//g, '') // Remove comments + .replace(/>\s+<') // Remove whitespace between tags + .replace(/\s+/g, ' ') // Collapse whitespace + .trim(); // Trim leading/trailing whitespace +}; + +/** + * Create a JSON object with the most common properties pre-filled + * @param {string} elementName - Name of the root element + * @param {string} value - Text content of the element + * @param {Object} options - Additional options + * @returns {Object} - JSON object with common properties + */ +export const createBaseJSON = (elementName, value = '', options = {}) => { + const { + attributes = {}, + children = [], + namespace = '', + comments = [], + cdata = [], + processing = [] + } = options; + + // Convert attributes from simple object to XMLJSONTransformer format + const formattedAttrs = {}; + for (const [key, val] of Object.entries(attributes)) { + formattedAttrs[key] = { + '@val': val, + '@ns': '' + }; + } + + // Create base object + const result = {}; + result[elementName] = { + '@ns': namespace, + '@val': value, + '@attrs': formattedAttrs, + '@children': children, + '@comments': comments, + '@cdata': cdata, + '@processing': processing + }; + + return result; +}; + +/** + * Create a simple XML string for testing + * @param {string} elementName - Name of the root element + * @param {string} value - Text content of the element + * @param {Object} options - Additional options + * @returns {string} - XML string + */ +export const createSimpleXML = (elementName, value = '', options = {}) => { + const { + attributes = {}, + children = [], + declaration = true + } = options; + + // Create attributes string + const attrs = Object.entries(attributes) + .map(([key, val]) => `${key}="${val}"`) + .join(' '); + + // Create children string + const childrenStr = children + .map(([name, val]) => `<${name}>${val}`) + .join(''); + + // Create XML + const xml = `<${elementName}${attrs ? ' ' + attrs : ''}>${value}${childrenStr}`; + + // Add declaration if requested + return declaration ? `\n${xml}` : xml; +}; + +/** + * Parse XML string into DOM node for testing + * @param {string} xmlString - XML string to parse + * @returns {Document} - DOM document + */ +export const parseXML = (xmlString) => { + const parser = new DOMParser(); + return parser.parseFromString(xmlString, 'text/xml'); +}; + +/** + * Generate a random attribute map for testing + * @param {number} count - Number of attributes to generate + * @returns {Object} - Map of attribute names to values + */ +export const generateRandomAttributes = (count = 3) => { + const attrs = {}; + for (let i = 0; i < count; i++) { + attrs[`attr${i}`] = `value${i}`; + } + return attrs; +}; + +/** + * Generate a random array of child elements for testing + * @param {number} count - Number of child elements to generate + * @returns {Array} - Array of child name/value pairs + */ +export const generateRandomChildren = (count = 3) => { + const children = []; + for (let i = 0; i < count; i++) { + children.push([`child${i}`, `value${i}`]); + } + return children; +}; \ No newline at end of file diff --git a/test/integration/json-to-xml-workflow.test.js b/test/integration/json-to-xml-workflow.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/integration/mixed-content-handling.test.js b/test/integration/mixed-content-handling.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/integration/xml-to-json-workflow.test.js b/test/integration/xml-to-json-workflow.test.js new file mode 100644 index 0000000..586535b --- /dev/null +++ b/test/integration/xml-to-json-workflow.test.js @@ -0,0 +1,375 @@ +/** + * Integration tests for JSON to XML workflow + * + * Tests the complete flow from JSON structure to XML conversion, + * including handling of various features and edge cases. + */ + +import XMLJSONTransformer from '../../src/XMLJSONTransformer.js'; + +describe('JSON to XML Workflow', () => { + let transformer; + + beforeEach(() => { + // Create a transformer with default settings + transformer = new XMLJSONTransformer(); + }); + + test('should transform simple JSON to XML', () => { + const json = { + "root": { + "@ns": "", + "@val": "", + "@attrs": { + "version": { + "@val": "1.0", + "@ns": "" + } + }, + "@children": [ + { + "element": { + "@ns": "", + "@val": "Simple text", + "@attrs": { + "id": { + "@val": "123", + "@ns": "" + } + } + } + } + ] + } + }; + + const xml = transformer.jsonToXML(json); + + // Check structure + expect(xml).toNormalizeContain(''); + expect(xml).toNormalizeContain(''); + expect(xml).toNormalizeContain('Simple text'); + + // Round-trip should produce equivalent JSON + const roundTrip = transformer.xmlToJSON(xml); + expect(roundTrip.root['@attrs'].version['@val']).toBe('1.0'); + expect(roundTrip.root['@children'][0].element['@val']).toBe('Simple text'); + }); + + test('should handle namespaces in JSON to XML conversion', () => { + const json = { + "Envelope": { + "@ns": "http://www.w3.org/2003/05/soap-envelope", + "@val": "", + "@attrs": {}, + "@children": [ + { + "Body": { + "@ns": "http://www.w3.org/2003/05/soap-envelope", + "@val": "", + "@children": [ + { + "GetStock": { + "@ns": "http://www.example.org/stock", + "@val": "", + "@children": [ + { + "StockName": { + "@ns": "http://www.example.org/stock", + "@val": "ACME" + } + } + ] + } + } + ] + } + } + ] + } + }; + + const xml = transformer.jsonToXML(json); + + // Check namespace declarations and prefixes + expect(xml).toNormalizeContain('xmlns="http://www.w3.org/2003/05/soap-envelope"'); + // The prefix may vary, but the namespace URI should be present + expect(xml).toNormalizeContain('http://www.example.org/stock'); + expect(xml).toNormalizeContain('ACME<'); + + // Test with prefixes preserved + const prefixTransformer = new XMLJSONTransformer({ + stripPrefixes: false + }); + + // Create JSON with explicit prefixes + const jsonWithPrefixes = { + "soap:Envelope": { + "@ns": "http://www.w3.org/2003/05/soap-envelope", + "@val": "", + "@attrs": {}, + "@children": [ + { + "soap:Body": { + "@ns": "http://www.w3.org/2003/05/soap-envelope", + "@children": [ + { + "m:GetStock": { + "@ns": "http://www.example.org/stock", + "@children": [ + { + "m:StockName": { + "@ns": "http://www.example.org/stock", + "@val": "ACME" + } + } + ] + } + } + ] + } + } + ] + } + }; + + const xmlWithPrefixes = prefixTransformer.jsonToXML(jsonWithPrefixes); + expect(xmlWithPrefixes).toNormalizeContain(' { + const json = { + "root": { + "@ns": "", + "@val": "", + "@attrs": {}, + "@comments": ["This is a comment", "Another comment"], + "@processing": ["xml-stylesheet type=\"text/css\" href=\"style.css\""], + "@children": [ + { + "element": { + "@ns": "", + "@val": "", + "@attrs": { + "id": { + "@val": "123", + "@ns": "" + } + }, + "@cdata": [""] + } + } + ] + } + }; + + const xml = transformer.jsonToXML(json); + + // Check special nodes + expect(xml).toNormalizeContain(''); + expect(xml).toNormalizeContain(''); + expect(xml).toNormalizeContain(''); + expect(xml).toNormalizeContain('alert(\'Hello\');]]>'); + + // Round-trip check + const roundTrip = transformer.xmlToJSON(xml); + expect(roundTrip.root['@comments']).toContainEqual('This is a comment'); + expect(roundTrip.root['@processing'][0]).toBe('xml-stylesheet type="text/css" href="style.css"'); + expect(roundTrip.root['@children'][0].element['@cdata'][0]).toBe(''); + }); + + test('should handle mixed content in JSON to XML conversion', () => { + const json = { + "paragraph": { + "@ns": "", + "@val": "This is mixed content with formatting.", + "@attrs": {} + } + }; + + const xml = transformer.jsonToXML(json); + + // Check mixed content + expect(xml).toNormalizeContain('This is mixed content with formatting.'); + + // Round-trip check + const roundTrip = transformer.xmlToJSON(xml); + expect(roundTrip.paragraph['@val']).toBe('This is mixed content with formatting.'); + }); + + test('should apply custom transform functions when converting from JSON to XML', () => { + // Create transformer with custom transform + const customTransformer = new XMLJSONTransformer({ + transformFunction: (value, context) => { + if (typeof value !== 'string') return value; + + if (context.direction === 'json-to-xml') { + // For XML output, wrap text in square brackets + return `[${value}]`; + } + return value; + } + }); + + const json = { + "root": { + "@val": "Root text", + "@attrs": { + "attr": { + "@val": "Attribute value" + } + }, + "@children": [ + { + "child": { + "@val": "Child text" + } + } + ] + } + }; + + const xml = customTransformer.jsonToXML(json); + + // Check transformed values + expect(xml).toNormalizeContain('[Root text]'); + expect(xml).toNormalizeContain('[Child text]'); + }); + + test('should handle empty and null values correctly', () => { + const json = { + "root": { + "@val": "", + "@attrs": { + "empty": { + "@val": "" + }, + "zero": { + "@val": "0" + } + }, + "@children": [ + { + "empty": { + "@val": "" + } + }, + { + "null": { + "@val": null + } + } + ] + } + }; + + const xml = transformer.jsonToXML(json); + + // Check empty and null values + expect(xml).toNormalizeContain(''); + expect(xml).toNormalizeContain(''); + expect(xml).toNormalizeContain(''); + + // Transformer with xsi:nil for null values + const nilTransformer = new XMLJSONTransformer({ + transformFunction: (value, context) => { + if (value === null) { + context.isNull = true; + return ''; + } + return value; + } + }); + + const xmlWithNil = nilTransformer.jsonToXML(json); + expect(xmlWithNil).toNormalizeContain('xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'); + expect(xmlWithNil).toNormalizeContain('xsi:nil="true"'); + }); + + test('should handle deeply nested structures', () => { + const json = { + "level1": { + "@val": "", + "@children": [ + { + "level2": { + "@val": "", + "@children": [ + { + "level3": { + "@val": "", + "@children": [ + { + "level4": { + "@val": "Deep content" + } + } + ] + } + } + ] + } + } + ] + } + }; + + const xml = transformer.jsonToXML(json); + + // Check nested structure + expect(xml).toNormalizeContain(''); + expect(xml).toNormalizeContain(''); + expect(xml).toNormalizeContain(''); + expect(xml).toNormalizeContain('Deep content'); + + // Proper nesting (using simplified checks) + const nestedPattern = /\s*\s*\s*Deep content<\/level4>\s*<\/level3>\s*<\/level2>\s*<\/level1>/; + expect(xml.replace(/\n/g, ' ')).toMatch(nestedPattern); + }); + + test('should pretty print XML output when configured', () => { + const json = { + "root": { + "@val": "", + "@children": [ + { + "child": { + "@val": "Value" + } + } + ] + } + }; + + // Default pretty printing + const prettyXml = transformer.jsonToXML(json); + expect(prettyXml).toContain('\n'); + expect(prettyXml).toMatch(/>\s+\s+ { + let defaultConfig; + + beforeEach(() => { + // Create a manager with default config to test against + const defaultManager = new ConfigurationManager(); + defaultConfig = defaultManager.config; + }); + + test('should use default configuration when no config is provided', () => { + const manager = new ConfigurationManager(); + + // Check some key default properties + expect(manager.config.preserveNamespaces).toBe(true); + expect(manager.config.stripPrefixes).toBe(true); + expect(manager.config.outputOptions.prettyPrint).toBe(true); + + // Check default property names + expect(manager.config.propNames.namespace).toBe('@ns'); + expect(manager.config.propNames.value).toBe('@val'); + expect(manager.config.propNames.children).toBe('@children'); + }); + + test('should merge user config with defaults', () => { + const userConfig = { + preserveNamespaces: false, + outputOptions: { + indent: 4 + } + }; + + const manager = new ConfigurationManager(userConfig); + + // Changed properties should reflect user values + expect(manager.config.preserveNamespaces).toBe(false); + expect(manager.config.outputOptions.indent).toBe(4); + + // Unchanged properties should keep defaults + expect(manager.config.stripPrefixes).toBe(true); + expect(manager.config.propNames.namespace).toBe('@ns'); + }); + + test('should create reverse mapping for property names', () => { + const manager = new ConfigurationManager(); + + // Check reverse mappings + expect(manager.propNamesReverse['@ns']).toBe('namespace'); + expect(manager.propNamesReverse['@val']).toBe('value'); + expect(manager.propNamesReverse['@attrs']).toBe('attributes'); + }); + + test('should use custom property names when provided', () => { + const userConfig = { + propNames: { + namespace: '_ns', + value: '_val', + attributes: '_attrs' + } + }; + + const manager = new ConfigurationManager(userConfig); + + // Check custom property names + expect(manager.config.propNames.namespace).toBe('_ns'); + expect(manager.config.propNames.value).toBe('_val'); + expect(manager.config.propNames.attributes).toBe('_attrs'); + + // Check reverse mappings + expect(manager.propNamesReverse['_ns']).toBe('namespace'); + expect(manager.propNamesReverse['_val']).toBe('value'); + expect(manager.propNamesReverse['_attrs']).toBe('attributes'); + }); + + test('should calculate XML indent string from config', () => { + // Test with numeric indent + const manager1 = new ConfigurationManager({ outputOptions: { indent: 3 } }); + expect(manager1.xmlIndent).toBe(' '); // 3 spaces + + // Test with 0 indent + const manager2 = new ConfigurationManager({ outputOptions: { indent: 0 } }); + expect(manager2.xmlIndent).toBe(''); // Empty string + + // Test with non-numeric indent (should use default of 2 spaces) + const manager3 = new ConfigurationManager({ outputOptions: { indent: '----' } }); + expect(manager3.xmlIndent).toBe(' '); // Default 2 spaces + }); + + test('should handle deep merge of nested properties', () => { + const userConfig = { + outputOptions: { + prettyPrint: false, + json: { + compact: false + } + } + }; + + const manager = new ConfigurationManager(userConfig); + + // Check that specified properties were changed + expect(manager.config.outputOptions.prettyPrint).toBe(false); + expect(manager.config.outputOptions.json.compact).toBe(false); + + // Check that unspecified properties were preserved + expect(manager.config.outputOptions.json.removeEmptyStrings).toBe(true); + expect(manager.config.outputOptions.indent).toBe(3); + }); + + test('shouldPreserve method should return correct values', () => { + // Default manager preserves most features + const defaultManager = new ConfigurationManager(); + expect(defaultManager.shouldPreserve('namespace')).toBe(true); + expect(defaultManager.shouldPreserve('comments')).toBe(true); + expect(defaultManager.shouldPreserve('whitespace')).toBe(false); + + // Custom manager with specific features disabled + const customManager = new ConfigurationManager({ + preserveNamespaces: false, + preserveComments: false + }); + expect(customManager.shouldPreserve('namespace')).toBe(false); + expect(customManager.shouldPreserve('comments')).toBe(false); + expect(customManager.shouldPreserve('cdata')).toBe(true); + }); + + test('getPropName and getFeatureFromProp should return correct values', () => { + const manager = new ConfigurationManager(); + + // Get property name from feature + expect(manager.getPropName('namespace')).toBe('@ns'); + expect(manager.getPropName('children')).toBe('@children'); + + // Get feature from property name + expect(manager.getFeatureFromProp('@ns')).toBe('namespace'); + expect(manager.getFeatureFromProp('@children')).toBe('children'); + }); +}); \ No newline at end of file diff --git a/test/unit/DOMEnvironment.test.js b/test/unit/DOMEnvironment.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/JSONToXMLConverter.test.js b/test/unit/JSONToXMLConverter.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/NodeProcessor.test.js b/test/unit/NodeProcessor.test.js new file mode 100644 index 0000000..3b1b1c1 --- /dev/null +++ b/test/unit/NodeProcessor.test.js @@ -0,0 +1,118 @@ +/** + * Unit tests for the NodeProcessor class + */ + +import ConfigurationManager from '../../src/ConfigurationManager.js'; +import NodeProcessor from '../../src/NodeProcessor.js'; +import DOMEnvironment from '../../src/DOMEnvironment.js'; + +describe('NodeProcessor', () => { + let configManager; + let nodeProcessor; + let parser; + + beforeEach(() => { + configManager = new ConfigurationManager(); + nodeProcessor = new NodeProcessor(configManager, DOMEnvironment); + parser = DOMEnvironment.createParser(); + }); + + // Helper to create a DOM node for testing + const createNode = (xmlString) => { + const doc = parser.parseFromString(xmlString, 'text/xml'); + return doc.documentElement; + }; + + test('should detect mixed content correctly', () => { + // Regular element with text + const simpleNode = createNode('Simple text'); + expect(nodeProcessor.hasMixedContent(simpleNode)).toBe(false); + + // Element with only child elements + const childElementNode = createNode('TextMore'); + expect(nodeProcessor.hasMixedContent(childElementNode)).toBe(false); + + // Element with mixed content + const mixedNode = createNode('Text emphasized and normal'); + expect(nodeProcessor.hasMixedContent(mixedNode)).toBe(true); + + // Element with whitespace but no actual mixed content + const whitespaceNode = createNode('\n Text\n More\n'); + expect(nodeProcessor.hasMixedContent(whitespaceNode)).toBe(false); + }); + + test('should handle HTML markup detection correctly', () => { + // Plain text + expect(nodeProcessor.containsHtmlMarkup('Just plain text')).toBe(false); + + // HTML markup + expect(nodeProcessor.containsHtmlMarkup('Emphasized text')).toBe(true); + expect(nodeProcessor.containsHtmlMarkup('
')).toBe(true); + + // XML-like but not HTML + expect(nodeProcessor.containsHtmlMarkup('2 < 5 and 10 > 3')).toBe(false); + + // Non-string input + expect(nodeProcessor.containsHtmlMarkup(123)).toBe(false); + expect(nodeProcessor.containsHtmlMarkup(null)).toBe(false); + }); + + test('should create transform context correctly', () => { + const node = createNode('Test'); + + // Default no transform case + expect(nodeProcessor.createTransformContext(node, 'root', 'xml-to-json')).toBeNull(); + + // With transform function + const configWithTransform = new ConfigurationManager({ + transformFunction: (val) => val.toUpperCase() + }); + const processorWithTransform = new NodeProcessor(configWithTransform, DOMEnvironment); + + const context = processorWithTransform.createTransformContext(node, 'root', 'xml-to-json'); + expect(context).toEqual({ + nodeName: 'root', + nodeType: DOMEnvironment.nodeTypes.ELEMENT_NODE, + namespaceURI: 'http://example.org', + attributes: node.attributes, + direction: 'xml-to-json' + }); + }); + + test('should apply transform function when configured', () => { + // Simple uppercase transform + const uppercaseConfig = new ConfigurationManager({ + transformFunction: (val) => typeof val === 'string' ? val.toUpperCase() : val + }); + const uppercaseProcessor = new NodeProcessor(uppercaseConfig, DOMEnvironment); + + // Test transform + const context = { direction: 'xml-to-json' }; + expect(uppercaseProcessor.applyTransform('test', context)).toBe('TEST'); + expect(uppercaseProcessor.applyTransform(null, context)).toBeNull(); + expect(uppercaseProcessor.applyTransform(123, context)).toBe(123); + + // Test transform that returns undefined + const undefinedConfig = new ConfigurationManager({ + transformFunction: () => undefined + }); + const undefinedProcessor = new NodeProcessor(undefinedConfig, DOMEnvironment); + + // Should return original value if transform returns undefined + expect(undefinedProcessor.applyTransform('test', context)).toBe('test'); + }); + + test('should get inner HTML correctly', () => { + // Simple node + const simpleNode = createNode('Text'); + expect(nodeProcessor.getInnerHTML(simpleNode)).toNormalizeEqual('Text'); + + // Mixed content node + const mixedNode = createNode('Text emphasized'); + expect(nodeProcessor.getInnerHTML(mixedNode)).toNormalizeEqual('Text emphasized'); + + // Node with CDATA + const cdataNode = createNode('Raw]]>'); + expect(nodeProcessor.getInnerHTML(cdataNode)).toNormalizeContain('Raw'); + }); +}); \ No newline at end of file diff --git a/test/unit/PathNavigator.test.js b/test/unit/PathNavigator.test.js new file mode 100644 index 0000000..58e4f6e --- /dev/null +++ b/test/unit/PathNavigator.test.js @@ -0,0 +1,277 @@ +/** + * Unit tests for the PathNavigator class + */ + +import ConfigurationManager from '../../src/ConfigurationManager.js'; +import PathNavigator from '../../src/PathNavigator.js'; + +describe('PathNavigator', () => { + let configManager; + let navigator; + let sampleJSON; + + beforeEach(() => { + // Create a default configuration + configManager = new ConfigurationManager(); + navigator = new PathNavigator(configManager); + + // Create a sample JSON structure similar to what would be produced by xmlToJSON + sampleJSON = { + "catalog": { + "@ns": "http://example.org/catalog", + "@val": "", + "@attrs": {}, + "@comments": ["This is a catalog of books"], + "@processing": [], + "@children": [ + { + "book": { + "@ns": "http://example.org/catalog", + "@val": "", + "@attrs": { + "id": { + "@val": "bk101", + "@ns": "" + }, + "category": { + "@val": "fiction", + "@ns": "" + } + }, + "@children": [ + { + "title": { + "@ns": "http://example.org/catalog", + "@val": "The Catcher in the Rye", + "@attrs": {}, + "@children": [] + } + }, + { + "author": { + "@ns": "http://example.org/catalog", + "@val": "J.D. Salinger", + "@attrs": {}, + "@children": [] + } + } + ] + } + }, + { + "book": { + "@ns": "http://example.org/catalog", + "@val": "", + "@attrs": { + "id": { + "@val": "bk102", + "@ns": "" + }, + "category": { + "@val": "fiction", + "@ns": "" + } + }, + "@children": [ + { + "title": { + "@ns": "http://example.org/catalog", + "@val": "To Kill a Mockingbird", + "@attrs": {}, + "@children": [] + } + }, + { + "author": { + "@ns": "http://example.org/catalog", + "@val": "Harper Lee", + "@attrs": {}, + "@children": [] + } + } + ] + } + } + ] + } + }; + }); + + describe('getPath method', () => { + test('should access root element properties directly', () => { + const namespace = navigator.getPath(sampleJSON, "catalog.@ns"); + expect(namespace).toBe("http://example.org/catalog"); + + const comments = navigator.getPath(sampleJSON, "catalog.@comments"); + expect(comments).toEqual(["This is a catalog of books"]); + }); + + test('should access child elements by path', () => { + // Access the first book's id attribute + const bookId = navigator.getPath(sampleJSON, "catalog.@children[0].book.@attrs.id.@val"); + expect(bookId).toBe("bk101"); + + // Access the second book's category attribute + const bookCategory = navigator.getPath(sampleJSON, "catalog.@children[1].book.@attrs.category.@val"); + expect(bookCategory).toBe("fiction"); + }); + + test('should access nested child elements', () => { + // Access the first book's title + const title = navigator.getPath(sampleJSON, "catalog.@children[0].book.@children[0].title.@val"); + expect(title).toBe("The Catcher in the Rye"); + + // Access the second book's author + const author = navigator.getPath(sampleJSON, "catalog.@children[1].book.@children[1].author.@val"); + expect(author).toBe("Harper Lee"); + }); + + test('should flatten results when accessing multiple elements', () => { + // Access all book titles + const titles = navigator.getPath(sampleJSON, "catalog.@children.book.@children.title.@val"); + expect(titles).toEqual(["The Catcher in the Rye", "To Kill a Mockingbird"]); + + // Access all book authors + const authors = navigator.getPath(sampleJSON, "catalog.@children.book.@children.author.@val"); + expect(authors).toEqual(["J.D. Salinger", "Harper Lee"]); + }); + + test('should return undefined for non-existent paths', () => { + const nonExistent = navigator.getPath(sampleJSON, "catalog.@children[0].book.@children[5].price.@val"); + expect(nonExistent).toBeUndefined(); + }); + + test('should use fallback value for non-existent paths when provided', () => { + const fallback = "Not Found"; + const nonExistent = navigator.getPath(sampleJSON, "catalog.@children[0].book.@children[5].price.@val", fallback); + expect(nonExistent).toBe(fallback); + }); + + test('should handle array indexing correctly', () => { + // Access specific array elements + const secondBook = navigator.getPath(sampleJSON, "catalog.@children[1].book"); + expect(secondBook["@attrs"].id["@val"]).toBe("bk102"); + + // Out of bounds index should return undefined + const outOfBounds = navigator.getPath(sampleJSON, "catalog.@children[5]"); + expect(outOfBounds).toBeUndefined(); + }); + + test('should handle null or undefined input gracefully', () => { + // Test with null object + const nullResult = navigator.getPath(null, "some.path"); + expect(nullResult).toBeUndefined(); + + // Test with undefined object + const undefinedResult = navigator.getPath(undefined, "some.path"); + expect(undefinedResult).toBeUndefined(); + + // Test with fallback values + const nullWithFallback = navigator.getPath(null, "some.path", "fallback"); + expect(nullWithFallback).toBe("fallback"); + }); + }); + + describe('getShortPath method', () => { + test('should handle shorthand paths for common access patterns', () => { + // Testing a shortcut path to get all book titles directly + const bookTitles = navigator.getShortPath(sampleJSON, "catalog.book.title.@val"); + expect(bookTitles).toEqual(["The Catcher in the Rye", "To Kill a Mockingbird"]); + + // Testing a shortcut path to get all author names directly + const authors = navigator.getShortPath(sampleJSON, "catalog.book.author.@val"); + expect(authors).toEqual(["J.D. Salinger", "Harper Lee"]); + }); + + test('should handle mixed shorthand and explicit paths', () => { + // Mix of shorthand and explicit path + const bookIds = navigator.getShortPath(sampleJSON, "catalog.book.@attrs.id.@val"); + expect(bookIds).toEqual(["bk101", "bk102"]); + }); + }); + + describe('findElements method', () => { + test('should find elements matching criteria', () => { + // Find books with category = fiction + const books = navigator.findElements(sampleJSON, { + 'name': 'book', + '@attrs.category.@val': 'fiction' + }); + + expect(books.length).toBe(2); + expect(books[0].book["@attrs"].id["@val"]).toBe("bk101"); + expect(books[1].book["@attrs"].id["@val"]).toBe("bk102"); + + // Find book with specific ID + const specificBook = navigator.findElements(sampleJSON, { + 'name': 'book', + '@attrs.id.@val': 'bk102' + }); + + expect(specificBook.length).toBe(1); + expect(specificBook[0].book["@attrs"].id["@val"]).toBe("bk102"); + }); + + test('should return empty array when no elements match', () => { + const nonExistent = navigator.findElements(sampleJSON, { + 'name': 'book', + '@attrs.category.@val': 'non-fiction' + }); + + expect(nonExistent).toEqual([]); + }); + }); + + describe('setPath method', () => { + test('should update a value at a specific path', () => { + // Update a title + const updated = navigator.setPath( + sampleJSON, + "catalog.@children[0].book.@children[0].title.@val", + "Updated Title" + ); + + // Check the updated value + expect(updated.catalog["@children"][0].book["@children"][0].title["@val"]).toBe("Updated Title"); + + // Original should not be modified + expect(sampleJSON.catalog["@children"][0].book["@children"][0].title["@val"]).toBe("The Catcher in the Rye"); + }); + + test('should create missing properties if needed', () => { + // Add a new property + const updated = navigator.setPath( + sampleJSON, + "catalog.@children[0].book.@children[0].title.@new_prop", + "New Value" + ); + + // Check the new property + expect(updated.catalog["@children"][0].book["@children"][0].title["@new_prop"]).toBe("New Value"); + + // Original should not have the new property + expect(sampleJSON.catalog["@children"][0].book["@children"][0].title["@new_prop"]).toBeUndefined(); + }); + + test('should handle array indices when setting values', () => { + // Add a new element to an array + const updated = navigator.setPath( + sampleJSON, + "catalog.@children[2]", + { + "magazine": { + "@val": "New Magazine", + "@attrs": {} + } + } + ); + + // Check the new element + expect(updated.catalog["@children"].length).toBe(3); + expect(updated.catalog["@children"][2].magazine["@val"]).toBe("New Magazine"); + + // Original should not be modified + expect(sampleJSON.catalog["@children"].length).toBe(2); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/SchemaGenerator.test.js b/test/unit/SchemaGenerator.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/XMLJSONTransformer.test.js b/test/unit/XMLJSONTransformer.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/XMLToJSONConverter.test.js b/test/unit/XMLToJSONConverter.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/xml-json-getpath.test.js b/test/unit/xml-json-getpath.test.js deleted file mode 100644 index 3c8985f..0000000 --- a/test/unit/xml-json-getpath.test.js +++ /dev/null @@ -1,409 +0,0 @@ -/** - * Unit tests for the getPath method of XMLJSONTransformer - */ - -import { XMLJSONTransformer } from '../../src/xml-json-transformer.js'; - -describe("XMLJSONTransformer.getPath", () => { - let transformer; - let sampleJSON; - - beforeEach(() => { - // Create a fresh transformer instance before each test - transformer = new XMLJSONTransformer(); - - // Create a sample JSON structure similar to what would be produced by xmlToJSON - sampleJSON = { - "catalog": { - "@ns": "http://example.org/catalog", - "@val": "", - "@attrs": {}, - "@cdata": [], - "@comments": ["This is a catalog of books"], - "@processing": [], - "@children": [ - { - "book": { - "@ns": "http://example.org/catalog", - "@val": "", - "@attrs": { - "id": { - "@val": "bk101", - "@ns": "" - }, - "category": { - "@val": "fiction", - "@ns": "" - } - }, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [ - { - "title": { - "@ns": "http://example.org/catalog", - "@val": "The Catcher in the Rye", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - }, - { - "author": { - "@ns": "http://example.org/catalog", - "@val": "J.D. Salinger", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - }, - { - "year": { - "@ns": "http://example.org/catalog", - "@val": "1951", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - } - ] - } - }, - { - "book": { - "@ns": "http://example.org/catalog", - "@val": "", - "@attrs": { - "id": { - "@val": "bk102", - "@ns": "" - }, - "category": { - "@val": "fiction", - "@ns": "" - } - }, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [ - { - "title": { - "@ns": "http://example.org/catalog", - "@val": "To Kill a Mockingbird", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - }, - { - "author": { - "@ns": "http://example.org/catalog", - "@val": "Harper Lee", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - }, - { - "year": { - "@ns": "http://example.org/catalog", - "@val": "1960", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - } - ] - } - } - ] - } - }; - }); - - test("should access root element properties directly", () => { - const namespace = transformer.getPath(sampleJSON, "catalog.@ns"); - expect(namespace).toBe("http://example.org/catalog"); - - const comments = transformer.getPath(sampleJSON, "catalog.@comments"); - expect(comments).toEqual(["This is a catalog of books"]); - }); - - test("should access child elements by path", () => { - // Access the first book's id attribute - const bookId = transformer.getPath(sampleJSON, "catalog.@children[0].book.@attrs.id.@val"); - expect(bookId).toBe("bk101"); - - // Access the second book's category attribute - const bookCategory = transformer.getPath(sampleJSON, "catalog.@children[1].book.@attrs.category.@val"); - expect(bookCategory).toBe("fiction"); - }); - - test("should access nested child elements", () => { - // Access the first book's title - const title = transformer.getPath(sampleJSON, "catalog.@children[0].book.@children[0].title.@val"); - expect(title).toBe("The Catcher in the Rye"); - - // Access the second book's author - const author = transformer.getPath(sampleJSON, "catalog.@children[1].book.@children[1].author.@val"); - expect(author).toBe("Harper Lee"); - }); - - test("should flatten results when accessing multiple elements", () => { - // Access all book titles (should be flattened automatically) - const titles = transformer.getPath(sampleJSON, "catalog.@children.book.@children.title.@val"); - expect(titles).toEqual(["The Catcher in the Rye", "To Kill a Mockingbird"]); - - // Access all book years - const years = transformer.getPath(sampleJSON, "catalog.@children.book.@children.year.@val"); - expect(years).toEqual(["1951", "1960"]); - }); - - test("should return undefined for non-existent paths", () => { - const nonExistent = transformer.getPath(sampleJSON, "catalog.@children[0].book.@children[5].price.@val"); - expect(nonExistent).toBeUndefined(); - }); - - test("should use fallback value for non-existent paths when provided", () => { - const fallback = "Not Found"; - const nonExistent = transformer.getPath(sampleJSON, "catalog.@children[0].book.@children[5].price.@val", fallback); - expect(nonExistent).toBe(fallback); - }); - - test("should handle array indexing correctly", () => { - // Access specific array elements - const secondBook = transformer.getPath(sampleJSON, "catalog.@children[1].book"); - expect(secondBook["@attrs"].id["@val"]).toBe("bk102"); - - // Out of bounds index should return undefined - const outOfBounds = transformer.getPath(sampleJSON, "catalog.@children[5]"); - expect(outOfBounds).toBeUndefined(); - }); - - test("should handle accessing specific elements by direct reference", () => { - // Direct reference to all books - const books = transformer.getPath(sampleJSON, "catalog.@children.book"); - expect(books.length).toBe(2); - expect(books[0]["@attrs"].id["@val"]).toBe("bk101"); - expect(books[1]["@attrs"].id["@val"]).toBe("bk102"); - }); - - test("should handle shortcut paths for common access patterns", () => { - // Testing a shortcut path to get all book titles directly - const bookTitles = transformer.getPath(sampleJSON, "catalog.book.title.@val"); - expect(bookTitles).toEqual(["The Catcher in the Rye", "To Kill a Mockingbird"]); - - // Testing a shortcut path to get all author names directly - const authors = transformer.getPath(sampleJSON, "catalog.book.author.@val"); - expect(authors).toEqual(["J.D. Salinger", "Harper Lee"]); - }); - - test("should handle custom configuration of property names", () => { - // Create a transformer with custom property names - const customTransformer = new XMLJSONTransformer({ - propNames: { - namespace: "_ns", - value: "_val", - attributes: "_attrs", - children: "_children" - } - }); - - // Create a sample with custom property names - const customJSON = { - "catalog": { - "_ns": "http://example.org/catalog", - "_val": "", - "_attrs": {}, - "_children": [ - { - "book": { - "_ns": "http://example.org/catalog", - "_val": "", - "_attrs": { - "id": { - "_val": "bk101", - "_ns": "" - } - }, - "_children": [ - { - "title": { - "_ns": "http://example.org/catalog", - "_val": "The Catcher in the Rye", - "_attrs": {}, - "_children": [] - } - } - ] - } - } - ] - } - }; - - // Access properties using custom property names - const bookTitle = customTransformer.getPath(customJSON, "catalog._children[0].book._children[0].title._val"); - expect(bookTitle).toBe("The Catcher in the Rye"); - }); - - test("should handle complex nested structures", () => { - // Create a more complex nested structure - const complexJSON = { - "library": { - "@ns": "", - "@val": "", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [ - { - "section": { - "@ns": "", - "@val": "", - "@attrs": { - "name": { - "@val": "fiction", - "@ns": "" - } - }, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [ - { - "shelf": { - "@ns": "", - "@val": "", - "@attrs": { - "id": { - "@val": "A1", - "@ns": "" - } - }, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [ - { - "book": { - "@ns": "", - "@val": "", - "@attrs": { - "id": { - "@val": "book1", - "@ns": "" - } - }, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [ - { - "title": { - "@ns": "", - "@val": "Nested Book Title", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - } - ] - } - } - ] - } - } - ] - } - } - ] - } - }; - - // Access deeply nested elements - const shelfId = transformer.getPath(complexJSON, "library.@children[0].section.@children[0].shelf.@attrs.id.@val"); - expect(shelfId).toBe("A1"); - - // Access book title using shortcut path - const bookTitle = transformer.getPath(complexJSON, "library.section.shelf.book.title.@val"); - expect(bookTitle).toEqual(["Nested Book Title"]); - }); - - test("should handle empty arrays and objects gracefully", () => { - const emptyJSON = { - "root": { - "@ns": "", - "@val": "", - "@attrs": {}, - "@cdata": [], - "@comments": [], - "@processing": [], - "@children": [] - } - }; - - // Access properties of an empty JSON structure - // Direct access to an empty array property should return that empty array - const children = transformer.getPath(emptyJSON, "root.@children"); - expect(children).toEqual([]); - - // Access non-existent child elements - // With the simplified implementation, this should return an empty array - const nonExistent = transformer.getPath(emptyJSON, "root.@children.element"); - expect(nonExistent).toEqual([]); - }); - - // Skip this test if the implementation has a 'this' binding issue - test.skip("should handle invalid paths gracefully - needs implementation fix", () => { - // Test with invalid path syntax - const invalidPath = transformer.getPath(sampleJSON, "catalog.$invalid..path"); - expect(invalidPath).toBeUndefined(); - - // Test with fallback value - const withFallback = transformer.getPath(sampleJSON, "catalog.$invalid..path", "fallback"); - expect(withFallback).toBe("fallback"); - }); - - // Alternative version with a valid but non-existent path - test("should handle non-existent paths gracefully", () => { - // Test with a valid syntax but non-existent path - const nonExistentPath = transformer.getPath(sampleJSON, "catalog.nonExistent.element"); - expect(nonExistentPath).toBeUndefined(); - - // Test with fallback value - const withFallback = transformer.getPath(sampleJSON, "catalog.nonExistent.element", "fallback"); - expect(withFallback).toBe("fallback"); - }); - - test("should handle null or undefined input gracefully", () => { - // Test with null object - const nullResult = transformer.getPath(null, "some.path"); - expect(nullResult).toBeUndefined(); - - // Test with undefined object - const undefinedResult = transformer.getPath(undefined, "some.path"); - expect(undefinedResult).toBeUndefined(); - - // Test with fallback values - const nullWithFallback = transformer.getPath(null, "some.path", "fallback"); - expect(nullWithFallback).toBe("fallback"); - }); -}); \ No newline at end of file diff --git a/test/unit/xml-json-transformFunction.test.js b/test/unit/xml-json-transformFunction.test.js deleted file mode 100644 index b1be909..0000000 --- a/test/unit/xml-json-transformFunction.test.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Simplified unit tests for the transform function feature - */ - -import { XMLJSONTransformer } from '../../src/xml-json-transformer.js'; - -describe("XMLJSONTransformer.transformFunction (Simple Case)", () => { - let transformer; - - // Simple transform function: - // - For XML to JSON: element values to uppercase, attribute values to lowercase - // - For JSON to XML: reverse operations - const simpleTransform = (value, context) => { - // Skip null/undefined values - if (value === null || value === undefined) return value; - - // Skip non-string values - if (typeof value !== 'string') return value; - - if (context.direction === 'xml-to-json') { - // XML to JSON transformation - if (context.isAttribute) { - // Attribute values to lowercase - return value.toLowerCase(); - } else { - // Element values to uppercase - return value.toUpperCase(); - } - } else if (context.direction === 'json-to-xml') { - // JSON to XML transformation - if (context.isAttribute) { - // Attribute values back to uppercase - return value.toUpperCase(); - } else { - // Element values back to lowercase - return value.toLowerCase(); - } - } - - // Default case - return value; - }; - - beforeEach(() => { - // Create a transformer with the simple transform function - transformer = new XMLJSONTransformer({ - transformFunction: simpleTransform - }); - }); - - test("should transform element values to uppercase and attribute values to lowercase", () => { - const xml = 'value'; - - const result = transformer.xmlToJSON(xml); - - // Element values should be uppercase - expect(result.root["@children"][0].child["@val"]).toBe("VALUE"); - - // Attribute values should be lowercase - expect(result.root["@attrs"].id["@val"]).toBe("id123"); - expect(result.root["@children"][0].child["@attrs"].name["@val"]).toBe("name"); - }); - - test("should reverse transformations when converting from JSON to XML", () => { - const json = { - "root": { - "@ns": "", - "@val": "ROOT VALUE", - "@attrs": { - "id": { - "@val": "id123", - "@ns": "" - } - }, - "@children": [ - { - "child": { - "@ns": "", - "@val": "CHILD VALUE", - "@attrs": { - "name": { - "@val": "name", - "@ns": "" - } - } - } - } - ] - } - }; - - const xml = transformer.jsonToXML(json); - - // Element values should be lowercase in XML - expect(xml).toNormalizeContain('>root value<'); - expect(xml).toNormalizeContain('>child value<'); - - // Attribute values should be uppercase in XML - expect(xml).toNormalizeContain('id="ID123"'); - expect(xml).toNormalizeContain('name="NAME"'); - }); - - test("should handle bidirectional transformation correctly", () => { - const originalXml = 'value'; - - // Convert XML to JSON - const json = transformer.xmlToJSON(originalXml); - - // Convert JSON back to XML - const newXml = transformer.jsonToXML(json); - - // Convert the new XML to JSON again to verify transformations - const newJson = transformer.xmlToJSON(newXml); - - // Check that values were transformed as expected - // Element values: value -> VALUE -> value -> VALUE - expect(newJson.root["@children"][0].child["@val"]).toNormalizeContain("VALUE"); - - // Attribute values: ID123 -> id123 -> ID123 -> id123 - expect(newJson.root["@attrs"].id["@val"]).toNormalizeContain("id123"); - expect(newJson.root["@children"][0].child["@attrs"].name["@val"]).toNormalizeContain("name"); - }); - - test("should handle mixed content", () => { - const xml = '

This is mixed content

'; - - const result = transformer.xmlToJSON(xml); - - // Mixed content is stored as a single value - // It should be transformed to uppercase - expect(result.p["@val"]).toNormalizeContain("THIS IS MIXED CONTENT"); - - // Convert back to XML - const newXml = transformer.jsonToXML(result); - - // Check the content is transformed to lowercase - expect(newXml).toNormalizeContain('>this is mixed content<'); - }); -}); \ No newline at end of file diff --git a/test/unit/xml-json-transformer.test.js b/test/unit/xml-json-transformer.test.js deleted file mode 100644 index 306d3c0..0000000 --- a/test/unit/xml-json-transformer.test.js +++ /dev/null @@ -1,452 +0,0 @@ -/** - * Unit tests for the XMLJSONTransformer class - * These tests can be run in a browser or Node.js environment with a test framework - * like Jest, Mocha, or Jasmine. - */ - -import { XMLJSONTransformer } from '../../src/xml-json-transformer.js'; - -describe("XMLJSONTransformer", () => { - let transformer; - - beforeEach(() => { - // Create a fresh transformer instance before each test - transformer = new XMLJSONTransformer(); - }); - - describe("Basic functionality", () => { - test("should transform a simple XML element to JSON", () => { - const xml = "Hello World"; - const expected = { - root: { - "@ns": "", - "@val": "Hello World", - }, - }; - - const result = transformer.xmlToJSON(xml); - expect(result).toEqual(expected); - }); - - test("should transform a simple JSON object to XML", () => { - const json = { - root: { - "@ns": "", - "@val": "Hello World", - }, - }; - const expected = "Hello World"; - - const result = transformer.jsonToXML(json); - // Remove whitespace for comparison - expect(result).toNormalizeEqual(expected); - }); - - test("should handle empty elements", () => { - const xml = ""; - const expected = { - empty: {}, - }; - - const result = transformer.xmlToJSON(xml); - expect(result).toEqual(expected); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip).toNormalizeEqual(""); - }); - - test("should return JSON as string when requested", () => { - const xml = "Hello World"; - - const result = transformer.xmlToJSON(xml, true); - expect(typeof result).toBe("string"); - expect(JSON.parse(result)).toEqual({ - root: { - "@ns": "", - "@val": "Hello World", - }, - }); - }); - - test("should format JSON with proper indentation", () => { - const json = { - root: { - "@val": "Hello World", - }, - }; - - const result = transformer.jsonToString(json); - console.log(result); - expect(result).toContain("\n"); - expect(result).toContain(" "); - }); - }); - - describe("Attributes", () => { - test("should handle elements with attributes", () => { - const xml = 'Product'; - const expected = { - item: { - "@ns": "", - "@val": "Product", - "@attrs": { - id: { - "@val": "123", - "@ns": "", - }, - category: { - "@val": "book", - "@ns": "", - }, - }, - }, - }; - - const result = transformer.xmlToJSON(xml); - expect(result).toNormalizeEqual(expected); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip).toNormalizeContain('id="123"'); - expect(roundTrip).toNormalizeContain('category="book"'); - }); - - test("should handle attributes with namespace", () => { - const xml = - 'Link'; - - const result = transformer.xmlToJSON(xml); - expect(result.item).toBeDefined(); - expect(result.item["@attrs"]).toBeDefined(); - - // With stripPrefixes=true (default), the attribute might be stored as "href" - // rather than "xlink:href" - const attrName = Object.keys(result.item["@attrs"]).find( - key => key === "xlink:href" || key === "href" - ); - expect(attrName).toBeDefined(); - - const attr = result.item["@attrs"][attrName]; - expect(attr).toBeDefined(); - expect(attr["@val"]).toBe("http://example.com"); - - // In some DOM implementations, the attribute might have a namespace - if (attr["@ns"]) { - expect(attr["@ns"]).toBe("http://www.w3.org/1999/xlink"); - } - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes('href="http://example.com"')).toBe(true); - }); - - test("should handle empty attributes", () => { - const xml = 'Empty value'; - - const result = transformer.xmlToJSON(xml); - // With compact mode and removeEmptyStrings, empty values might be undefined - // Check if the attribute exists but value might be undefined - expect(result.item["@attrs"].empty).toBeDefined(); - expect(result.item["@attrs"].zero["@val"]).toBe("0"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes('empty=""')).toBe(true); - expect(roundTrip.includes('zero="0"')).toBe(true); - }); - }); - - describe("Namespaces", () => { - test("should handle namespaces", () => { - const xml = - 'Content'; - - const result = transformer.xmlToJSON(xml); - // With stripPrefixes=true (default config), the prefix is removed - expect(result.root).toBeDefined(); - expect(result.root["@ns"]).toEqual("http://example.com/ns1"); - - const child = result.root["@children"][0].child; - expect(child).toBeDefined(); - expect(child["@ns"]).toEqual("http://example.com/ns1"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes('xmlns')).toBe(true); - expect(roundTrip.includes('http://example.com/ns1')).toBe(true); - }); - - test("should handle default namespaces", () => { - const xml = 'Content'; - - const result = transformer.xmlToJSON(xml); - expect(result.root["@ns"]).toEqual("http://example.com/default"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes('xmlns="http://example.com/default"')).toBe( - true - ); - }); - - test("should not include namespaces when configured", () => { - const nsTransformer = new XMLJSONTransformer({ - preserveNamespaces: false, - }); - - const xml = - 'Content'; - - const result = nsTransformer.xmlToJSON(xml); - // With stripPrefixes=true (default), we should have "root" not "x:root" - expect(result.root).toBeDefined(); - expect(result.root["@ns"]).toBeUndefined(); - - const child = result.root["@children"][0].child; - expect(child).toBeDefined(); - expect(child["@ns"]).toBeUndefined(); - - const roundTrip = nsTransformer.jsonToXML(result); - expect(roundTrip.includes('xmlns')).toBe(false); - }); - - test("should strip prefixes when configured", () => { - const prefixTransformer = new XMLJSONTransformer({ - stripPrefixes: true, - }); - - const xml = - 'Content'; - - const result = prefixTransformer.xmlToJSON(xml); - expect(result.root).toBeDefined(); - expect(result.root["@ns"]).toEqual("http://example.com/ns1"); - - const children = result.root["@children"]; - expect(children[0].child).toBeDefined(); - - const roundTrip = prefixTransformer.jsonToXML(result); - // The namespace should still be preserved even if prefixes are stripped - expect(roundTrip.includes("http://example.com/ns1")).toBe(true); - }); - - test("should both remove namespaces and strip prefixes when configured", () => { - const simpleTransformer = new XMLJSONTransformer({ - preserveNamespaces: false, - stripPrefixes: true, - }); - - const xml = - 'Content'; - - const result = simpleTransformer.xmlToJSON(xml); - expect(result.root).toBeDefined(); - expect(result.root["@ns"]).toBeUndefined(); - - const children = result.root["@children"]; - expect(children[0].child).toBeDefined(); - expect(children[0].child["@ns"]).toBeUndefined(); - - const roundTrip = simpleTransformer.jsonToXML(result); - expect(roundTrip.includes("xmlns")).toBe(false); - expect(roundTrip.includes("")).toBe(true); - expect(roundTrip.includes("")).toBe(true); - }); - - test("should handle multiple namespaces", () => { - const xml = ` - - A content - B content - - `; - - const result = transformer.xmlToJSON(xml); - - // With stripPrefixes=true (default), we should have "element" not "a:element" - expect(result.root["@children"]).toBeDefined(); - expect(result.root["@children"].length).toBe(2); - - const aElement = result.root["@children"][0].element; - const bElement = result.root["@children"][1].element; - - expect(aElement).toBeDefined(); - expect(bElement).toBeDefined(); - - expect(aElement["@ns"]).toBe("http://example.com/a"); - expect(bElement["@ns"]).toBe("http://example.com/b"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes('xmlns')).toBe(true); - expect(roundTrip.includes('http://example.com/a')).toBe(true); - expect(roundTrip.includes('http://example.com/b')).toBe(true); - }); - }); - - describe("Special node types", () => { - test("should handle CDATA sections", () => { - const xml = "Bold text]]>"; - - const result = transformer.xmlToJSON(xml); - expect(result.root["@cdata"]).toBeDefined(); - expect(result.root["@cdata"].length).toBe(1); - expect(result.root["@cdata"][0]).toBe("Bold text"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip).toNormalizeContain("Bold text]]>"); - }); - - test("should handle multiple CDATA sections", () => { - const xml = - ""; - - const result = transformer.xmlToJSON(xml); - expect(result.root["@cdata"]).toBeDefined(); - expect(result.root["@cdata"].length).toBe(2); - expect(result.root["@cdata"][0]).toBe("First section"); - expect(result.root["@cdata"][1]).toBe("Second section"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip).toNormalizeContain(""); - expect(roundTrip).toNormalizeContain(""); - }); - - test("should handle comments", () => { - const xml = ""; - - const result = transformer.xmlToJSON(xml); - expect(result.root["@comments"]).toBeDefined(); - expect(result.root["@comments"].length).toBe(1); - expect(result.root["@comments"][0]).toBe(" This is a comment "); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes("")).toBe(true); - }); - - test("should handle multiple comments", () => { - const xml = ""; - - const result = transformer.xmlToJSON(xml); - expect(result.root["@comments"]).toBeDefined(); - expect(result.root["@comments"].length).toBe(2); - expect(result.root["@comments"][0]).toBe(" First comment "); - expect(result.root["@comments"][1]).toBe(" Second comment "); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes("")).toBe(true); - expect(roundTrip.includes("")).toBe(true); - }); - - test("should handle processing instructions", () => { - const xml = ''; - - const result = transformer.xmlToJSON(xml); - expect(result.root["@processing"]).toBeDefined(); - expect(result.root["@processing"].length).toBe(1); - expect(result.root["@processing"][0]).toBe("custom-pi data"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes("")).toBe(true); - }); - - test("should handle multiple processing instructions", () => { - const xml = ""; - - const result = transformer.xmlToJSON(xml); - expect(result.root["@processing"]).toBeDefined(); - expect(result.root["@processing"].length).toBe(2); - expect(result.root["@processing"][0]).toBe("first-pi data1"); - expect(result.root["@processing"][1]).toBe("second-pi data2"); - - const roundTrip = transformer.jsonToXML(result); - expect(roundTrip.includes("")).toBe(true); - expect(roundTrip.includes("")).toBe(true); - }); - - test("should not preserve special nodes when configured", () => { - const noSpecialNodesTransformer = new XMLJSONTransformer({ - preserveComments: false, - preserveCDATA: false, - preserveProcessingInstr: false, - }); - - const xml = ` - - - - - Text content - - `; - - const result = noSpecialNodesTransformer.xmlToJSON(xml); - - // When preserving is disabled, these collections might not exist in compact mode - expect(result.root["@comments"]).toBeUndefined(); - expect(result.root["@cdata"]).toBeUndefined(); - expect(result.root["@processing"]).toBeUndefined(); - - expect(result.root["@val"]).toBeDefined(); - expect(result.root["@val"].trim()).toBe("Text content"); - - const roundTrip = noSpecialNodesTransformer.jsonToXML(result); - expect(roundTrip).not.toNormalizeContain("Comment"); - expect(roundTrip).not.toNormalizeContain("CDATA"); - expect(roundTrip).not.toNormalizeContain("pi-target"); - expect(roundTrip).toNormalizeContain("Text content"); - }); - }); - - describe("Mixed Content Handling", () => { - test("should handle simple mixed content in transformation", () => { - const xml = "This has mixed content."; - - const result = transformer.xmlToJSON(xml); - - // The value should contain the entire content including markup - expect(result.paragraph["@val"]).toBe( - "This has mixed content." - ); - - // Children array should not exist for mixed content in compact mode - expect(result.paragraph["@children"]).toBeUndefined(); - - // Transform back to XML - const roundTrip = transformer.jsonToXML(result); - - // Remove whitespace for comparison - const normalizedOriginal = xml.replace(/\s+/g, ""); - const normalizedRoundTrip = roundTrip.replace(/\s+/g, ""); - - expect(normalizedRoundTrip).toBe(normalizedOriginal); - }); - - test("should handle complex mixed content", () => { - const xml = ` -
-

This paragraph has emphasized text and strong text mixed with regular text.

-

Another paragraph with a link in the middle.

-
- `; - - const result = transformer.xmlToJSON(xml); - - // The

elements should be processed as mixed content - const paragraphs = result.article["@children"]; - expect(paragraphs.length).toBe(2); - - const firstP = paragraphs[0].p; - const secondP = paragraphs[1].p; - - // Check that the paragraphs contain the markup - expect(firstP["@val"]).toNormalizeContain("emphasized"); - expect(firstP["@val"]).toNormalizeContain("strong"); - expect(secondP["@val"]).toNormalizeContain( - 'a link' - ); - - // Transform back to XML - const roundTrip = transformer.jsonToXML(result); - - // Verify the transformed XML contains the same elements - expect(roundTrip).toNormalizeContain("emphasized"); - expect(roundTrip).toNormalizeContain("strong"); - expect(roundTrip).toNormalizeContain('a link'); - }); - }); -}); \ No newline at end of file From 849d52b36ac29d0055cbe64146020f766a2f83c5 Mon Sep 17 00:00:00 2001 From: William Summers Date: Sat, 19 Apr 2025 10:01:51 -0500 Subject: [PATCH 02/13] updated build paths and test paths --- demo/index.html | 2 +- jest.config.js | 2 +- rollup.config.js | 4 +- src/XMLJSONTransformer.js | 2 +- src/components/XMLToJSONConverter.js | 335 +++++++++++++++++++++++++ src/index.js | 2 +- test/unit/ConfigurationManager.test.js | 2 +- test/unit/NodeProcessor.test.js | 6 +- test/unit/PathNavigator.test.js | 4 +- 9 files changed, 347 insertions(+), 12 deletions(-) create mode 100644 src/components/XMLToJSONConverter.js diff --git a/demo/index.html b/demo/index.html index fe0b615..277b1c4 100644 --- a/demo/index.html +++ b/demo/index.html @@ -328,7 +328,7 @@

JSON

"] - } - } - ] - } - }; - - const xml = transformer.jsonToXML(json); - - // Check special nodes - expect(xml).toNormalizeContain(''); - expect(xml).toNormalizeContain(''); - expect(xml).toNormalizeContain(''); - expect(xml).toNormalizeContain('alert(\'Hello\');]]>'); - - // Round-trip check - const roundTrip = transformer.xmlToJSON(xml); - expect(roundTrip.root['@comments']).toContainEqual('This is a comment'); - expect(roundTrip.root['@processing'][0]).toBe('xml-stylesheet type="text/css" href="style.css"'); - expect(roundTrip.root['@children'][0].element['@cdata'][0]).toBe(''); - }); - - test('should handle mixed content in JSON to XML conversion', () => { - const json = { - "paragraph": { - "@ns": "", - "@val": "This is mixed content with formatting.", - "@attrs": {} - } - }; - - const xml = transformer.jsonToXML(json); - - // Check mixed content - expect(xml).toNormalizeContain('This is mixed content with formatting.'); - - // Round-trip check - const roundTrip = transformer.xmlToJSON(xml); - expect(roundTrip.paragraph['@val']).toBe('This is mixed content with formatting.'); - }); - - test('should apply custom transform functions when converting from JSON to XML', () => { - // Create transformer with custom transform - const customTransformer = new XMLJSONTransformer({ - transformFunction: (value, context) => { - if (typeof value !== 'string') return value; - - if (context.direction === 'json-to-xml') { - // For XML output, wrap text in square brackets - return `[${value}]`; - } - return value; - } - }); - - const json = { - "root": { - "@val": "Root text", - "@attrs": { - "attr": { - "@val": "Attribute value" - } - }, - "@children": [ - { - "child": { - "@val": "Child text" - } - } - ] - } - }; - - const xml = customTransformer.jsonToXML(json); - - // Check transformed values - expect(xml).toNormalizeContain('[Root text]'); - expect(xml).toNormalizeContain('[Child text]'); - }); - - test('should handle empty and null values correctly', () => { - const json = { - "root": { - "@val": "", - "@attrs": { - "empty": { - "@val": "" - }, - "zero": { - "@val": "0" - } - }, - "@children": [ - { - "empty": { - "@val": "" - } - }, - { - "null": { - "@val": null - } - } - ] - } - }; - - const xml = transformer.jsonToXML(json); - - // Check empty and null values - expect(xml).toNormalizeContain(''); - expect(xml).toNormalizeContain(''); - expect(xml).toNormalizeContain(''); - - // Transformer with xsi:nil for null values - const nilTransformer = new XMLJSONTransformer({ - transformFunction: (value, context) => { - if (value === null) { - context.isNull = true; - return ''; - } - return value; - } - }); - - const xmlWithNil = nilTransformer.jsonToXML(json); - expect(xmlWithNil).toNormalizeContain('xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'); - expect(xmlWithNil).toNormalizeContain('xsi:nil="true"'); - }); - - test('should handle deeply nested structures', () => { - const json = { - "level1": { - "@val": "", - "@children": [ - { - "level2": { - "@val": "", - "@children": [ - { - "level3": { - "@val": "", - "@children": [ - { - "level4": { - "@val": "Deep content" - } - } - ] - } - } - ] - } - } - ] - } - }; - - const xml = transformer.jsonToXML(json); - - // Check nested structure - expect(xml).toNormalizeContain(''); - expect(xml).toNormalizeContain(''); - expect(xml).toNormalizeContain(''); - expect(xml).toNormalizeContain('Deep content'); - - // Proper nesting (using simplified checks) - const nestedPattern = /\s*\s*\s*Deep content<\/level4>\s*<\/level3>\s*<\/level2>\s*<\/level1>/; - expect(xml.replace(/\n/g, ' ')).toMatch(nestedPattern); - }); - - test('should pretty print XML output when configured', () => { - const json = { - "root": { - "@val": "", - "@children": [ - { - "child": { - "@val": "Value" - } - } - ] - } - }; - - // Default pretty printing - const prettyXml = transformer.jsonToXML(json); - expect(prettyXml).toContain('\n'); - expect(prettyXml).toMatch(/>\s+\s+ { expect(nullWithFallback).toBe("fallback"); }); }); - - describe('getShortPath method', () => { - test('should handle shorthand paths for common access patterns', () => { - // Testing a shortcut path to get all book titles directly - const bookTitles = navigator.getShortPath(sampleJSON, "catalog.book.title.@val"); - expect(bookTitles).toEqual(["The Catcher in the Rye", "To Kill a Mockingbird"]); - - // Testing a shortcut path to get all author names directly - const authors = navigator.getShortPath(sampleJSON, "catalog.book.author.@val"); - expect(authors).toEqual(["J.D. Salinger", "Harper Lee"]); - }); - - test('should handle mixed shorthand and explicit paths', () => { - // Mix of shorthand and explicit path - const bookIds = navigator.getShortPath(sampleJSON, "catalog.book.@attrs.id.@val"); - expect(bookIds).toEqual(["bk101", "bk102"]); - }); - }); - - describe('findElements method', () => { - test('should find elements matching criteria', () => { - // Find books with category = fiction - const books = navigator.findElements(sampleJSON, { - 'name': 'book', - '@attrs.category.@val': 'fiction' - }); - - expect(books.length).toBe(2); - expect(books[0].book["@attrs"].id["@val"]).toBe("bk101"); - expect(books[1].book["@attrs"].id["@val"]).toBe("bk102"); - - // Find book with specific ID - const specificBook = navigator.findElements(sampleJSON, { - 'name': 'book', - '@attrs.id.@val': 'bk102' - }); - - expect(specificBook.length).toBe(1); - expect(specificBook[0].book["@attrs"].id["@val"]).toBe("bk102"); - }); - - test('should return empty array when no elements match', () => { - const nonExistent = navigator.findElements(sampleJSON, { - 'name': 'book', - '@attrs.category.@val': 'non-fiction' - }); - - expect(nonExistent).toEqual([]); - }); - }); - - describe('setPath method', () => { - test('should update a value at a specific path', () => { - // Update a title - const updated = navigator.setPath( - sampleJSON, - "catalog.@children[0].book.@children[0].title.@val", - "Updated Title" - ); - - // Check the updated value - expect(updated.catalog["@children"][0].book["@children"][0].title["@val"]).toBe("Updated Title"); - - // Original should not be modified - expect(sampleJSON.catalog["@children"][0].book["@children"][0].title["@val"]).toBe("The Catcher in the Rye"); - }); - - test('should create missing properties if needed', () => { - // Add a new property - const updated = navigator.setPath( - sampleJSON, - "catalog.@children[0].book.@children[0].title.@new_prop", - "New Value" - ); - - // Check the new property - expect(updated.catalog["@children"][0].book["@children"][0].title["@new_prop"]).toBe("New Value"); - - // Original should not have the new property - expect(sampleJSON.catalog["@children"][0].book["@children"][0].title["@new_prop"]).toBeUndefined(); - }); - - test('should handle array indices when setting values', () => { - // Add a new element to an array - const updated = navigator.setPath( - sampleJSON, - "catalog.@children[2]", - { - "magazine": { - "@val": "New Magazine", - "@attrs": {} - } - } - ); - - // Check the new element - expect(updated.catalog["@children"].length).toBe(3); - expect(updated.catalog["@children"][2].magazine["@val"]).toBe("New Magazine"); - - // Original should not be modified - expect(sampleJSON.catalog["@children"].length).toBe(2); - }); - }); }); \ No newline at end of file diff --git a/test/unit/SchemaGenerator.test.js b/test/unit/SchemaGenerator.test.js deleted file mode 100644 index e69de29..0000000 diff --git a/test/unit/XMLJSONTransformer.test.js b/test/unit/XMLJSONTransformer.test.js deleted file mode 100644 index e69de29..0000000 diff --git a/test/unit/XMLToJSONConverter.test.js b/test/unit/XMLToJSONConverter.test.js deleted file mode 100644 index e69de29..0000000 From 727a3e7ef52fd8278627659ef6ee1e38e16dd061 Mon Sep 17 00:00:00 2001 From: William Summers Date: Sat, 19 Apr 2025 10:54:22 -0500 Subject: [PATCH 05/13] fixed default import in demo page --- demo/index.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/index.html b/demo/index.html index 277b1c4..e8fbc5c 100644 --- a/demo/index.html +++ b/demo/index.html @@ -328,7 +328,8 @@

JSON

+ + + \ No newline at end of file diff --git a/src/components/XMLToJSONConverter.js b/src/components/XMLToJSONConverter.js index 88d5d27..7929f29 100644 --- a/src/components/XMLToJSONConverter.js +++ b/src/components/XMLToJSONConverter.js @@ -136,6 +136,8 @@ class XMLToJSONConverter { } /** + * processRegularContent + * * Process regular (non-mixed) content * @param {Object} nodeObj - Node object * @param {Node} node - DOM node @@ -147,6 +149,9 @@ class XMLToJSONConverter { // Get the raw value let value = node.nodeValue; + // Strip leading and trailing whitespace and newlines + value = value.trim(); + // Step 1: Apply transform function if exists value = this.nodeProcessor.applyTransform(value, context); @@ -164,6 +169,9 @@ class XMLToJSONConverter { // Simple text content case let value = node.textContent; + // Strip leading and trailing whitespace and newlines + value = value.trim(); + // Step 1: Apply transform function if exists value = this.nodeProcessor.applyTransform(value, context); @@ -263,7 +271,12 @@ class XMLToJSONConverter { ) { continue; } - textContent += childNode.textContent; + + // For text nodes, only append non-empty content + const nodeText = childNode.textContent; + if (nodeText.trim() !== "" || this.config.preserveWhitespace) { + textContent += nodeText; + } } break; @@ -298,11 +311,15 @@ class XMLToJSONConverter { this.config.preserveTextNodes && !nodeObj[this.config.propNames.value] ) { + // Trim the accumulated text content if not preserving whitespace + if (!this.config.preserveWhitespace) { + textContent = textContent.trim(); + } + // Apply transform if needed - nodeObj[this.config.propNames.value] = this.nodeProcessor.applyTransform( - textContent, - context - ); + textContent = this.nodeProcessor.applyTransform(textContent, context); + + nodeObj[this.config.propNames.value] = textContent; } // Add child nodes if present From df8fbe4b301f315d3e65cc1cd0c38cefebd8cef9 Mon Sep 17 00:00:00 2001 From: William Summers Date: Sat, 19 Apr 2025 17:23:56 -0500 Subject: [PATCH 08/13] improved namespace handling --- src/components/JSONToXMLConverter.js | 780 ++++++++++++++++++--------- src/components/NodeProcessor.js | 64 +-- 2 files changed, 519 insertions(+), 325 deletions(-) diff --git a/src/components/JSONToXMLConverter.js b/src/components/JSONToXMLConverter.js index 2a143e9..8652983 100644 --- a/src/components/JSONToXMLConverter.js +++ b/src/components/JSONToXMLConverter.js @@ -1,7 +1,7 @@ /** * JSONToXMLConverter * - * Handles converting JSON to XML + * Handles converting JSON to XML with clean namespace handling */ class JSONToXMLConverter { /** @@ -26,24 +26,42 @@ class JSONToXMLConverter { // Create a new XML document const doc = this.domEnv.createDocument(null, null, null); - // Process namespace prefixes if needed - const nsMap = this.manageNamespacePrefixes(jsonObj); - - // Process the root element - const rootElName = Object.keys(jsonObj).find((key) => !key.startsWith("@")); + // Get the root element name + const rootElName = Object.keys(jsonObj).find(key => !key.startsWith("@")); if (!rootElName) { throw new Error("Invalid JSON: No root element found"); } const rootJSON = jsonObj[rootElName]; - const rootEl = this.createElementFromJSON(doc, rootElName, rootJSON, nsMap); - doc.appendChild(rootEl); - // Serialize the XML document + // Only process namespaces if preserving them + if (this.config.preserveNamespaces) { + // Pre-scan to identify namespaces and assign prefixes + const nsMap = this.scanNamespaces(jsonObj); + + // Create root element with proper namespace + const rootEl = this.createElement(doc, rootElName, rootJSON, nsMap); + + // Declare all namespaces on the root element + this.declareNamespaces(rootEl, nsMap); + + // Process root element (add attributes, content, and children) + this.processElement(doc, rootEl, rootJSON, nsMap, true); + + // Add root to document + doc.appendChild(rootEl); + } else { + // Simple processing without preserving namespaces + const rootEl = this.createSimpleElement(doc, rootElName, rootJSON); + this.processSimpleElement(doc, rootEl, rootJSON); + doc.appendChild(rootEl); + } + + // Serialize to XML const serializer = this.domEnv.createSerializer(); let xmlString = serializer.serializeToString(doc); - // Add XML declaration if configured + // Add XML declaration if (this.config.outputOptions.xml.declaration) { xmlString = '\n' + xmlString; } @@ -57,313 +75,540 @@ class JSONToXMLConverter { } /** - * Manage namespace prefixes when converting JSON to XML - * @param {Object} jsonObj - JSON object - * @returns {Map} - Map of namespace URIs to prefixes + * Scan the JSON object to collect all namespaces and assign prefixes + * @param {Object} jsonObj - JSON object to scan + * @returns {Object} - Namespace mapping information */ - manageNamespacePrefixes(jsonObj) { - // Only process when preserving namespaces but stripping prefixes - if (!this.config.stripPrefixes || !this.config.preserveNamespaces) { - return null; - } - + scanNamespaces(jsonObj) { const nsKey = this.config.propNames.namespace; const childrenKey = this.config.propNames.children; const attrsKey = this.config.propNames.attributes; - - // Map to store namespace URI to prefix mappings - const nsMap = new Map(); - // Counter for generating unique prefixes - let prefixCounter = 0; - - // Simplified function to collect all unique namespaces - const collectNamespaces = (node) => { - // Check if this node has a namespace - if (node[nsKey] && node[nsKey] !== "") { - const nsURI = node[nsKey]; - - // Generate a prefix if we haven't seen this namespace - if (!nsMap.has(nsURI)) { - const prefix = `ns${++prefixCounter}`; - nsMap.set(nsURI, prefix); + + // Initialize namespace mapping + const nsMap = { + // URI to prefix mapping + uriToPrefix: new Map(), + // Prefix to URI mapping + prefixToUri: new Map(), + // Original prefixes from element and attribute names + originalPrefixes: new Map(), + // Track namespace URIs that should be reserved for their original prefixes + reservedPrefixes: new Set(), + // Next auto-generated prefix number + nextPrefixNum: 1 + }; + + // Helper to extract prefix from name + const extractPrefix = (name) => { + if (name.includes(':')) { + return name.split(':')[0]; + } + return null; + }; + + // Initial pass to collect all original prefixes + const collectOriginalPrefixes = (name, nodeObj) => { + // Check element namespace and prefix + const ns = nodeObj[nsKey]; + if (ns) { + const prefix = extractPrefix(name); + if (prefix) { + // Remember original prefix for this namespace + if (!nsMap.originalPrefixes.has(ns)) { + nsMap.originalPrefixes.set(ns, prefix); + nsMap.reservedPrefixes.add(prefix); + } } } - - // Process attributes - if (node[attrsKey]) { - for (const attrObj of Object.values(node[attrsKey])) { - if (attrObj[nsKey] && attrObj[nsKey] !== "") { - const nsURI = attrObj[nsKey]; - - if (!nsMap.has(nsURI)) { - const prefix = `ns${++prefixCounter}`; - nsMap.set(nsURI, prefix); + + // Check attribute namespaces and prefixes + if (nodeObj[attrsKey]) { + for (const [attrName, attrObj] of Object.entries(nodeObj[attrsKey])) { + // Skip xmlns attributes + if (attrName === 'xmlns' || attrName.startsWith('xmlns:')) { + continue; + } + + const attrNs = attrObj[nsKey]; + if (attrNs) { + const prefix = extractPrefix(attrName); + if (prefix) { + // Remember original prefix for this namespace + if (!nsMap.originalPrefixes.has(attrNs)) { + nsMap.originalPrefixes.set(attrNs, prefix); + nsMap.reservedPrefixes.add(prefix); + } } } } } - + // Process children recursively - if (Array.isArray(node[childrenKey])) { - for (const childObj of node[childrenKey]) { - for (const childData of Object.values(childObj)) { - if (typeof childData === "object" && childData !== null) { - collectNamespaces(childData); + if (Array.isArray(nodeObj[childrenKey])) { + for (const childObj of nodeObj[childrenKey]) { + for (const [childName, childData] of Object.entries(childObj)) { + if (!childName.startsWith('@')) { + collectOriginalPrefixes(childName, childData); } } } } }; - - // Start collection from the root node - const rootName = Object.keys(jsonObj)[0]; - collectNamespaces(jsonObj[rootName]); - - return nsMap; - } - - /** - * Create a DOM element from a JSON object - * @param {Document} doc - DOM document - * @param {string} elName - Element name - * @param {Object} jsonObj - JSON object - * @param {Map} nsMap - Namespace URI to prefix map - * @returns {Element} - DOM element - */ - createElementFromJSON(doc, elName, jsonObj, nsMap = null) { - const propNames = this.config.propNames; - const nsKey = propNames.namespace; - const valKey = propNames.value; - const attrsKey = propNames.attributes; - const cdataKey = propNames.cdata; - const commentsKey = propNames.comments; - const processingKey = propNames.processing; - const childrenKey = propNames.children; - - // Create the element (with namespace if provided and preserving namespaces) - let element; - const nsURI = jsonObj[nsKey] || ""; - - // Create context with direction (only if transform function exists) - const context = this.nodeProcessor.createTransformContext( - { nodeType: this.domEnv.nodeTypes.ELEMENT_NODE, namespaceURI: nsURI }, - elName, - "json-to-xml" - ); - - // Handle element creation with namespaces - element = this.createNamespacedElement(doc, elName, nsURI, nsMap); - - // Add attributes - if (jsonObj[attrsKey]) { - this.addAttributesToElement(element, jsonObj[attrsKey], context, nsMap); - } - - // Handle content - this.addContentToElement(element, jsonObj, valKey, context); - - // Only add special nodes and children if not already handling mixed content - const contentValue = jsonObj[valKey]; - if ( - !contentValue || - !this.nodeProcessor.containsHtmlMarkup(String(contentValue)) - ) { - // Add CDATA sections - if (this.config.preserveCDATA && Array.isArray(jsonObj[cdataKey])) { - for (const cdataText of jsonObj[cdataKey]) { - const cdataSection = doc.createCDATASection(cdataText); - element.appendChild(cdataSection); - } - } - - // Add comments - if (this.config.preserveComments && Array.isArray(jsonObj[commentsKey])) { - for (const commentText of jsonObj[commentsKey]) { - const comment = doc.createComment(commentText); - element.appendChild(comment); + + // Second pass to assign all prefixes + const assignPrefixes = (name, nodeObj) => { + // Process element namespace + const ns = nodeObj[nsKey]; + if (ns && !nsMap.uriToPrefix.has(ns)) { + // Use original prefix if available + if (nsMap.originalPrefixes.has(ns)) { + const prefix = nsMap.originalPrefixes.get(ns); + nsMap.uriToPrefix.set(ns, prefix); + nsMap.prefixToUri.set(prefix, ns); + } else { + // Generate new prefix + let newPrefix; + do { + newPrefix = `ns${nsMap.nextPrefixNum++}`; + } while (nsMap.reservedPrefixes.has(newPrefix)); + + nsMap.uriToPrefix.set(ns, newPrefix); + nsMap.prefixToUri.set(newPrefix, ns); } } - - // Add processing instructions - if ( - this.config.preserveProcessingInstr && - Array.isArray(jsonObj[processingKey]) - ) { - for (const piText of jsonObj[processingKey]) { - const [target, data] = piText.split(" ", 2); - const pi = doc.createProcessingInstruction(target, data || ""); - element.appendChild(pi); + + // Process attribute namespaces + if (nodeObj[attrsKey]) { + for (const [attrName, attrObj] of Object.entries(nodeObj[attrsKey])) { + // Skip xmlns attributes + if (attrName === 'xmlns' || attrName.startsWith('xmlns:')) { + if (attrName.startsWith('xmlns:')) { + // Extract declared prefix and namespace + const declaredPrefix = attrName.substring(6); + const declaredNs = attrObj[this.config.propNames.value]; + + if (declaredPrefix && declaredNs) { + nsMap.reservedPrefixes.add(declaredPrefix); + if (!nsMap.originalPrefixes.has(declaredNs)) { + nsMap.originalPrefixes.set(declaredNs, declaredPrefix); + } + } + } + continue; + } + + const attrNs = attrObj[nsKey]; + if (attrNs && !nsMap.uriToPrefix.has(attrNs)) { + // Use original prefix if available + if (nsMap.originalPrefixes.has(attrNs)) { + const prefix = nsMap.originalPrefixes.get(attrNs); + nsMap.uriToPrefix.set(attrNs, prefix); + nsMap.prefixToUri.set(prefix, attrNs); + } else { + // Generate new prefix + let newPrefix; + do { + newPrefix = `ns${nsMap.nextPrefixNum++}`; + } while (nsMap.reservedPrefixes.has(newPrefix)); + + nsMap.uriToPrefix.set(attrNs, newPrefix); + nsMap.prefixToUri.set(newPrefix, attrNs); + } + } } } - + // Process children recursively - if (Array.isArray(jsonObj[childrenKey])) { - for (const childObj of jsonObj[childrenKey]) { + if (Array.isArray(nodeObj[childrenKey])) { + for (const childObj of nodeObj[childrenKey]) { for (const [childName, childData] of Object.entries(childObj)) { - if (!childName.startsWith("@")) { - const childElement = this.createElementFromJSON( - doc, - childName, - childData, - nsMap - ); - element.appendChild(childElement); + if (!childName.startsWith('@')) { + assignPrefixes(childName, childData); } } } } - } + }; + + // Start with the root element + const rootName = Object.keys(jsonObj)[0]; + const rootObj = jsonObj[rootName]; + + // First collect original prefixes + collectOriginalPrefixes(rootName, rootObj); + + // Then assign prefixes to all namespaces + assignPrefixes(rootName, rootObj); + + return nsMap; + } - return element; + /** + * Declare all namespaces on the root element + * @param {Element} rootEl - Root element + * @param {Object} nsMap - Namespace mapping + */ + declareNamespaces(rootEl, nsMap) { + // Add all namespace declarations to root + for (const [uri, prefix] of nsMap.uriToPrefix.entries()) { + if (uri && prefix) { + rootEl.setAttributeNS('http://www.w3.org/2000/xmlns/', `xmlns:${prefix}`, uri); + } + } } /** - * Create a namespaced element + * Create an element with proper namespace * @param {Document} doc - DOM document - * @param {string} elName - Element name - * @param {string} nsURI - Namespace URI - * @param {Map} nsMap - Namespace URI to prefix map - * @returns {Element} - DOM element + * @param {string} name - Element name + * @param {Object} nodeObj - Element JSON object + * @param {Object} nsMap - Namespace mapping + * @returns {Element} - Created element */ - createNamespacedElement(doc, elName, nsURI, nsMap) { - let element; - - // Only use namespace prefixing when needed - if (this.config.preserveNamespaces && nsURI) { - if (this.config.stripPrefixes && nsMap && nsMap.has(nsURI)) { - // Use the generated prefix for this namespace - const prefix = nsMap.get(nsURI); - const qualifiedName = `${prefix}:${elName}`; - + createElement(doc, name, nodeObj, nsMap) { + const nsUri = nodeObj[this.config.propNames.namespace] || ''; + + // Handle element with namespace + if (nsUri) { + // Get local name (without prefix) + const localName = name.includes(':') ? name.split(':')[1] : name; + + // Get assigned prefix for this namespace + const prefix = nsMap.uriToPrefix.get(nsUri); + + if (prefix) { + // Create with namespace and assigned prefix try { - element = doc.createElementNS(nsURI, qualifiedName); - - // Always declare the namespace on the element using this prefix - element.setAttributeNS( - "http://www.w3.org/2000/xmlns/", - `xmlns:${prefix}`, - nsURI - ); - } catch (error) { - console.warn( - `Error creating element with namespace: ${error.message}` - ); - element = doc.createElement(elName); + return doc.createElementNS(nsUri, `${prefix}:${localName}`); + } catch (e) { + console.warn(`Error creating element with namespace: ${e.message}`); + return doc.createElement(localName); } } else { - // Standard namespace handling when not stripping prefixes - try { - element = doc.createElementNS(nsURI, elName); - - // Add default namespace declaration if needed - if (!elName.includes(":") && nsURI) { - element.setAttribute("xmlns", nsURI); - } - } catch (error) { - console.warn( - `Error creating element with namespace: ${error.message}` - ); - element = doc.createElement(elName); - } + // No prefix assigned (should not happen) + return doc.createElement(localName); } - } else { - // No namespace or not preserving namespaces - element = doc.createElement(elName); } + + // No namespace - create simple element + const localName = name.includes(':') ? name.split(':')[1] : name; + return doc.createElement(localName); + } - return element; + /** + * Create an element without namespace handling + * @param {Document} doc - DOM document + * @param {string} name - Element name + * @param {Object} nodeObj - Element JSON object + * @returns {Element} - Created element + */ + createSimpleElement(doc, name, nodeObj) { + // Strip prefix if configured + if (this.config.stripPrefixes && name.includes(':')) { + return doc.createElement(name.split(':')[1]); + } + return doc.createElement(name); } /** - * Add attributes to an element - * @param {Element} element - DOM element - * @param {Object} attributes - Attributes object - * @param {Object} context - Transform context - * @param {Map} nsMap - Namespace URI to prefix map + * Process an element with namespace support + * @param {Document} doc - DOM document + * @param {Element} element - Element to process + * @param {Object} nodeObj - Element JSON object + * @param {Object} nsMap - Namespace mapping + * @param {boolean} isRoot - Whether this is the root element */ - addAttributesToElement(element, attributes, context, nsMap) { - for (const [attrName, attrObj] of Object.entries(attributes)) { - // Apply transform to attribute value if needed - const originalAttrValue = attrObj[this.config.propNames.value]; - let attrValue = - this.nodeProcessor.applyTransform(originalAttrValue, { + processElement(doc, element, nodeObj, nsMap, isRoot = false) { + const nsKey = this.config.propNames.namespace; + const valKey = this.config.propNames.value; + const attrsKey = this.config.propNames.attributes; + const cdataKey = this.config.propNames.cdata; + const commentsKey = this.config.propNames.comments; + const processingKey = this.config.propNames.processing; + const childrenKey = this.config.propNames.children; + + // Create transform context + const context = this.nodeProcessor.createTransformContext( + { + nodeType: this.domEnv.nodeTypes.ELEMENT_NODE, + namespaceURI: nodeObj[nsKey] || "" + }, + element.nodeName, + "json-to-xml" + ); + + // Add attributes (skip xmlns attributes unless this is the root) + if (nodeObj[attrsKey]) { + // Process attributes + for (const [attrName, attrObj] of Object.entries(nodeObj[attrsKey])) { + // Skip xmlns attributes except at root + if ((attrName === 'xmlns' || attrName.startsWith('xmlns:')) && !isRoot) { + continue; + } + + // Get attribute value + const attrVal = attrObj[valKey]; + if (attrVal === undefined) { + continue; + } + + // Convert to string if needed + const strVal = typeof attrVal === 'boolean' || typeof attrVal === 'number' + ? String(attrVal) + : attrVal; + + // Apply transform if configured + const transformedVal = this.nodeProcessor.applyTransform(strVal, { ...context, nodeName: attrName, nodeType: this.domEnv.nodeTypes.ATTRIBUTE_NODE, - isAttribute: true, - }) ?? ""; - - // Convert boolean and number values to strings for XML attributes - if (typeof attrValue === "boolean" || typeof attrValue === "number") { - attrValue = String(attrValue); + isAttribute: true + }) ?? strVal; + + // Special handling for xmlns attributes on root + if (isRoot && (attrName === 'xmlns' || attrName.startsWith('xmlns:'))) { + element.setAttribute(attrName, transformedVal); + continue; + } + + // Process attribute namespace + const attrNs = attrObj[nsKey] || ''; + + if (attrNs) { + // Get assigned prefix + const prefix = nsMap.uriToPrefix.get(attrNs); + + if (prefix) { + // Get local name (without prefix) + const localName = attrName.includes(':') ? attrName.split(':')[1] : attrName; + + try { + // Set attribute with namespace and assigned prefix + element.setAttributeNS(attrNs, `${prefix}:${localName}`, transformedVal); + } catch (e) { + // Fallback to regular attribute + element.setAttribute(localName, transformedVal); + } + } else { + // No prefix assigned (should not happen) + element.setAttribute(attrName, transformedVal); + } + } else { + // Regular attribute without namespace + element.setAttribute(attrName, transformedVal); + } + } + } + + // Add content + const content = nodeObj[valKey]; + if (content !== undefined && content !== null) { + // Format content + const strContent = typeof content === 'boolean' || typeof content === 'number' + ? String(content) + : content; + + // Apply transform if configured + const transformedContent = this.nodeProcessor.applyTransform(strContent, context) ?? strContent; + + // Add content to element + if (typeof transformedContent === 'string') { + if (this.nodeProcessor.containsHtmlMarkup(transformedContent)) { + // Mixed content + if (typeof element.innerHTML !== 'undefined') { + element.innerHTML = transformedContent; + } else { + element.textContent = transformedContent; + } + } else { + // Simple text + element.textContent = transformedContent; + } + } + } + + // Skip child processing if this is mixed content + if (content && this.nodeProcessor.containsHtmlMarkup(String(content))) { + return; + } + + // Add CDATA sections if preserving them + if (this.config.preserveCDATA && Array.isArray(nodeObj[cdataKey])) { + for (const cdataText of nodeObj[cdataKey]) { + const cdataSection = doc.createCDATASection(cdataText); + element.appendChild(cdataSection); + } + } + + // Add comments if preserving them + if (this.config.preserveComments && Array.isArray(nodeObj[commentsKey])) { + for (const commentText of nodeObj[commentsKey]) { + const comment = doc.createComment(commentText); + element.appendChild(comment); + } + } + + // Add processing instructions if preserving them + if (this.config.preserveProcessingInstr && Array.isArray(nodeObj[processingKey])) { + for (const piText of nodeObj[processingKey]) { + const [target, data] = piText.split(' ', 2); + const pi = doc.createProcessingInstruction(target, data || ''); + element.appendChild(pi); + } + } + + // Process children + if (Array.isArray(nodeObj[childrenKey])) { + for (const childObj of nodeObj[childrenKey]) { + for (const [childName, childData] of Object.entries(childObj)) { + if (!childName.startsWith('@')) { + // Create child with namespace + const childEl = this.createElement(doc, childName, childData, nsMap); + + // Process child element + this.processElement(doc, childEl, childData, nsMap, false); + + // Add to parent + element.appendChild(childEl); + } + } } - - const attrNs = attrObj[this.config.propNames.namespace]; - - // Rest of the attribute handling code... - // (The existing code for handling namespaces and setting attributes) } } /** - * Add content to an element - * @param {Element} element - DOM element - * @param {Object} jsonObj - JSON object - * @param {string} valKey - Value property key - * @param {Object} context - Transform context + * Process an element without namespace support + * @param {Document} doc - DOM document + * @param {Element} element - Element to process + * @param {Object} nodeObj - Element JSON object */ - addContentToElement(element, jsonObj, node, valKey, context) { - // Check if content is mixed (contains HTML markup) - const originalValue = jsonObj[valKey]; - - // Apply transform function if exists - const transformedValue = this.nodeProcessor.applyTransform( - originalValue, - context + processSimpleElement(doc, element, nodeObj) { + const valKey = this.config.propNames.value; + const attrsKey = this.config.propNames.attributes; + const cdataKey = this.config.propNames.cdata; + const commentsKey = this.config.propNames.comments; + const processingKey = this.config.propNames.processing; + const childrenKey = this.config.propNames.children; + + // Create transform context + const context = this.nodeProcessor.createTransformContext( + { + nodeType: this.domEnv.nodeTypes.ELEMENT_NODE, + namespaceURI: "" + }, + element.nodeName, + "json-to-xml" ); - - // Handle special cases from transform function - if (context && context.isNull) { - // Add xsi:nil="true" attribute for null values - element.setAttributeNS( - "http://www.w3.org/2000/xmlns/", - "xmlns:xsi", - "http://www.w3.org/2001/XMLSchema-instance" - ); - element.setAttributeNS( - "http://www.w3.org/2001/XMLSchema-instance", - "xsi:nil", - "true" - ); - } - - // Use the transformed value if available, otherwise use original - let contentValue = - transformedValue !== undefined ? transformedValue : originalValue; - - // Convert boolean and number values to strings for XML - if (contentValue !== undefined) { - if ( - typeof contentValue === "boolean" || - typeof contentValue === "number" - ) { - contentValue = String(contentValue); + + // Add attributes (without namespace handling) + if (nodeObj[attrsKey]) { + for (const [attrName, attrObj] of Object.entries(nodeObj[attrsKey])) { + // Skip xmlns attributes + if (attrName === 'xmlns' || attrName.startsWith('xmlns:')) { + continue; + } + + // Get attribute value + const attrVal = attrObj[valKey]; + if (attrVal === undefined) { + continue; + } + + // Convert to string if needed + const strVal = typeof attrVal === 'boolean' || typeof attrVal === 'number' + ? String(attrVal) + : attrVal; + + // Apply transform if configured + const transformedVal = this.nodeProcessor.applyTransform(strVal, { + ...context, + nodeName: attrName, + nodeType: this.domEnv.nodeTypes.ATTRIBUTE_NODE, + isAttribute: true + }) ?? strVal; + + // Strip prefix if configured + const processedName = this.config.stripPrefixes && attrName.includes(':') + ? attrName.split(':')[1] + : attrName; + + // Add attribute + element.setAttribute(processedName, transformedVal); } - - if ( - typeof contentValue === "string" && - this.nodeProcessor.containsHtmlMarkup(contentValue) - ) { - // For mixed content, set innerHTML - if (typeof element.innerHTML !== "undefined") { - element.innerHTML = contentValue; + } + + // Add content + const content = nodeObj[valKey]; + if (content !== undefined && content !== null) { + // Format content + const strContent = typeof content === 'boolean' || typeof content === 'number' + ? String(content) + : content; + + // Apply transform if configured + const transformedContent = this.nodeProcessor.applyTransform(strContent, context) ?? strContent; + + // Add content to element + if (typeof transformedContent === 'string') { + if (this.nodeProcessor.containsHtmlMarkup(transformedContent)) { + // Mixed content + if (typeof element.innerHTML !== 'undefined') { + element.innerHTML = transformedContent; + } else { + element.textContent = transformedContent; + } } else { - // Fallback for environments without innerHTML - element.textContent = contentValue; + // Simple text + element.textContent = transformedContent; + } + } + } + + // Skip child processing if this is mixed content + if (content && this.nodeProcessor.containsHtmlMarkup(String(content))) { + return; + } + + // Add CDATA sections if preserving them + if (this.config.preserveCDATA && Array.isArray(nodeObj[cdataKey])) { + for (const cdataText of nodeObj[cdataKey]) { + const cdataSection = doc.createCDATASection(cdataText); + element.appendChild(cdataSection); + } + } + + // Add comments if preserving them + if (this.config.preserveComments && Array.isArray(nodeObj[commentsKey])) { + for (const commentText of nodeObj[commentsKey]) { + const comment = doc.createComment(commentText); + element.appendChild(comment); + } + } + + // Add processing instructions if preserving them + if (this.config.preserveProcessingInstr && Array.isArray(nodeObj[processingKey])) { + for (const piText of nodeObj[processingKey]) { + const [target, data] = piText.split(' ', 2); + const pi = doc.createProcessingInstruction(target, data || ''); + element.appendChild(pi); + } + } + + // Process children + if (Array.isArray(nodeObj[childrenKey])) { + for (const childObj of nodeObj[childrenKey]) { + for (const [childName, childData] of Object.entries(childObj)) { + if (!childName.startsWith('@')) { + // Create child without namespace + const childEl = this.createSimpleElement(doc, childName, childData); + + // Process child element + this.processSimpleElement(doc, childEl, childData); + + // Add to parent + element.appendChild(childEl); + } } - } else if (contentValue !== undefined) { - // For simple text content - element.textContent = String(contentValue); } } } @@ -374,7 +619,9 @@ class JSONToXMLConverter { * @returns {string} - Formatted XML string */ prettyPrintXML(xmlString) { - const PADDING = this.configManager.xmlIndent; + const PADDING = typeof this.config.outputOptions.indent === 'number' + ? ' '.repeat(this.config.outputOptions.indent) + : ' '; // Handle XML declaration separately if present let declaration = ""; @@ -402,10 +649,6 @@ class JSONToXMLConverter { const isClosingTag = /^<\/[^>]+>/.test(line); const isOpeningTag = /^<[^!?\/][^>]*[^\/]>$/.test(line); const isSelfClosingTag = /^<[^>]+\/>$/.test(line); - const isComment = /^$/.test(line); - const isCDATA = /^$/.test(line); - const isProcessingInstruction = /^<\?.*\?>$/.test(line); - const isTextNode = !line.startsWith("<") && !line.endsWith(">"); if (isClosingTag) { indentLevel = Math.max(indentLevel - 1, 0); @@ -417,7 +660,6 @@ class JSONToXMLConverter { if (isOpeningTag) { indentLevel++; } - // other types (self-closing, comments, etc.) do not affect indent level } // Prepend the XML declaration if it was present @@ -425,4 +667,4 @@ class JSONToXMLConverter { } } -export default JSONToXMLConverter; +export default JSONToXMLConverter; \ No newline at end of file diff --git a/src/components/NodeProcessor.js b/src/components/NodeProcessor.js index 9cc4f44..2bd8e87 100644 --- a/src/components/NodeProcessor.js +++ b/src/components/NodeProcessor.js @@ -109,70 +109,22 @@ class NodeProcessor { * @returns {boolean} - Whether string contains HTML markup */ containsHtmlMarkup(str) { - return typeof str === "string" && /<[a-z][\s\S]*>/i.test(str); - } - - /** - * Attempt to convert a string value to a boolean - * @param {string} value - String value to convert - * @returns {boolean|string} - Boolean value if conversion successful, original string otherwise - */ - grokBooleanValue(value) { - if (typeof value !== "string") return value; - - const normalized = value.trim().toLowerCase(); - if (normalized === "true") return true; - if (normalized === "false") return false; - - return value; - } - - /** - * Attempt to convert a string value to a number - * @param {string} value - String value to convert - * @returns {number|string} - Number value if conversion successful, original string otherwise - */ - grokNumberValue(value) { - if (typeof value !== "string") return value; - - // Remove thousands separators and normalize decimal points - const normalized = value.trim().replace(/,/g, ""); - - // Check if it's a valid number (integer, float, or scientific notation) - if (/^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/.test(normalized)) { - const number = parseFloat(normalized); - - // Only return the number if it's not NaN or Infinity - if (!isNaN(number) && isFinite(number)) { - // Return an integer if there's no decimal part - return Number.isInteger(number) ? number : number; - } - } - - return value; + if (typeof str !== "string") return false; + const hasMarkup = /<[a-z][\s\S]*>/i.test(str); + return hasMarkup; } /** * Process a value based on configuration settings * @param {string} value - Original value to process - * @returns {any} - Processed value (possibly converted to boolean or number) + * @returns {any} - Processed value */ processValue(value) { if (value === undefined || value === null) return value; - - let processed = value; - - // Apply type conversions if configured - if (this.config.grokBoolean) { - processed = this.grokBooleanValue(processed); - } - - if (this.config.grokNumber && typeof processed === "string") { - processed = this.grokNumberValue(processed); - } - - return processed; + + // Simply return the value without type conversions for simplicity + return value; } } -export default NodeProcessor; +export default NodeProcessor; \ No newline at end of file From 03fd75a5bf431922e54cdd6b9573491b5e09a52f Mon Sep 17 00:00:00 2001 From: William Summers Date: Sun, 20 Apr 2025 00:36:42 -0500 Subject: [PATCH 09/13] improved namespace handling --- src/components/ConfigurationManager.js | 296 +++++++-------- src/components/JSONToXMLConverter.js | 474 +++++-------------------- src/components/SchemaGenerator.js | 402 +++++++++++---------- src/components/XMLToJSONConverter.js | 36 +- 4 files changed, 473 insertions(+), 735 deletions(-) diff --git a/src/components/ConfigurationManager.js b/src/components/ConfigurationManager.js index d8a1ddf..fa62374 100644 --- a/src/components/ConfigurationManager.js +++ b/src/components/ConfigurationManager.js @@ -1,158 +1,162 @@ /** * ConfigurationManager - * + * * Responsible for handling and validating transformer configuration */ class ConfigurationManager { - /** - * Creates a new ConfigurationManager with default settings - * @param {Object} userConfig - User provided configuration - */ - constructor(userConfig = {}) { - // Default configuration - this.config = this.mergeWithDefaults(userConfig); - - // Create a reverse mapping for property names (for JSON to XML conversion) - this.propNamesReverse = Object.entries(this.config.propNames) - .reduce((acc, [key, value]) => { - acc[value] = key; - return acc; - }, {}); - - // Calculate XML indentation string - this.xmlIndent = typeof this.config.outputOptions.indent === "number" + /** + * Creates a new ConfigurationManager with default settings + * @param {Object} userConfig - User provided configuration + */ + constructor(userConfig = {}) { + // Default configuration + this.config = this.mergeWithDefaults(userConfig); + + // Create a reverse mapping for property names (for JSON to XML conversion) + this.propNamesReverse = Object.entries(this.config.propNames).reduce( + (acc, [key, value]) => { + acc[value] = key; + return acc; + }, + {} + ); + + // Calculate XML indentation string + this.xmlIndent = + typeof this.config.outputOptions.indent === "number" ? " ".repeat(this.config.outputOptions.indent) : " "; - } - - /** - * Merges user-provided configuration with default settings - * @param {Object} userConfig - The user-provided config - * @returns {Object} - Merged configuration - */ - mergeWithDefaults(userConfig) { - const defaultConfig = { - // Features to preserve during transformation - preserveNamespaces: true, - preserveComments: true, - preserveProcessingInstr: true, - preserveCDATA: true, - preserveTextNodes: true, - preserveWhitespace: false, - - // Type conversion options - grokBoolean: false, - grokNumber: false, - - // value transform function - transformFunction: null, - - // Element name handling - stripPrefixes: true, - - // Output options - outputOptions: { - prettyPrint: true, - indent: 3, - - // JSON-specific options - json: { - compact: true, - removeEmptyStrings: true, - }, - - // XML-specific options - xml: { - declaration: true, - }, + } + + /** + * Merges user-provided configuration with default settings + * @param {Object} userConfig - The user-provided config + * @returns {Object} - Merged configuration + */ + mergeWithDefaults(userConfig) { + const defaultConfig = { + // Features to preserve during transformation + preserveNamespaces: true, + preserveComments: true, + preserveProcessingInstr: true, + preserveCDATA: true, + preserveTextNodes: true, + preserveWhitespace: false, + + // Type conversion options + grokBoolean: false, + grokNumber: false, + + // value transform function + transformFunction: null, + + // Element name handling + stripPrefixes: true, + + // Output options + outputOptions: { + prettyPrint: true, + indent: 3, + + // JSON-specific options + json: { + compact: true, + removeEmptyStrings: true, }, - - // Property names in the JSON representation - propNames: { - namespace: "@ns", - value: "@val", - attributes: "@attrs", - cdata: "@cdata", - comments: "@comments", - processing: "@processing", - children: "@children", - } - }; - - // Deep merge with user config - return this.deepMerge(defaultConfig, userConfig); - } - - /** - * Deep merge two objects - * @param {Object} target - Target object - * @param {Object} source - Source object - * @returns {Object} - Merged object - */ - deepMerge(target, source) { - const output = { ...target }; - - if (this.isObject(target) && this.isObject(source)) { - Object.keys(source).forEach(key => { - if (this.isObject(source[key])) { - if (!(key in target)) { - Object.assign(output, { [key]: source[key] }); - } else { - output[key] = this.deepMerge(target[key], source[key]); - } - } else { + + // XML-specific options + xml: { + declaration: true, + }, + }, + + // Property names in the JSON representation + propNames: { + namespace: "@ns", + prefix: "@prefix", + attributes: "@attrs", + value: "@val", + cdata: "@cdata", + comments: "@comments", + processing: "@processing", + children: "@children", + }, + }; + + // Deep merge with user config + return this.deepMerge(defaultConfig, userConfig); + } + + /** + * Deep merge two objects + * @param {Object} target - Target object + * @param {Object} source - Source object + * @returns {Object} - Merged object + */ + deepMerge(target, source) { + const output = { ...target }; + + if (this.isObject(target) && this.isObject(source)) { + Object.keys(source).forEach((key) => { + if (this.isObject(source[key])) { + if (!(key in target)) { Object.assign(output, { [key]: source[key] }); + } else { + output[key] = this.deepMerge(target[key], source[key]); } - }); - } - - return output; - } - - /** - * Check if value is an object - * @param {any} item - Value to check - * @returns {boolean} - Whether value is an object - */ - isObject(item) { - return (item && typeof item === 'object' && !Array.isArray(item)); - } - - /** - * Check if a property should be preserved based on configuration - * @param {string} propType - Type of property - * @returns {boolean} - Whether the property should be preserved - */ - shouldPreserve(propType) { - const preserveMap = { - 'namespace': this.config.preserveNamespaces, - 'cdata': this.config.preserveCDATA, - 'comments': this.config.preserveComments, - 'processing': this.config.preserveProcessingInstr, - 'whitespace': this.config.preserveWhitespace, - 'textNodes': this.config.preserveTextNodes - }; - - return preserveMap[propType] || false; - } - - /** - * Get the config property name for a specific feature - * @param {string} feature - Feature name - * @returns {string} - Property name in JSON representation - */ - getPropName(feature) { - return this.config.propNames[feature]; - } - - /** - * Get the feature name from property name - * @param {string} propName - Property name in JSON - * @returns {string} - Feature name - */ - getFeatureFromProp(propName) { - return this.propNamesReverse[propName]; + } else { + Object.assign(output, { [key]: source[key] }); + } + }); } + + return output; + } + + /** + * Check if value is an object + * @param {any} item - Value to check + * @returns {boolean} - Whether value is an object + */ + isObject(item) { + return item && typeof item === "object" && !Array.isArray(item); + } + + /** + * Check if a property should be preserved based on configuration + * @param {string} propType - Type of property + * @returns {boolean} - Whether the property should be preserved + */ + shouldPreserve(propType) { + const preserveMap = { + namespace: this.config.preserveNamespaces, + cdata: this.config.preserveCDATA, + comments: this.config.preserveComments, + processing: this.config.preserveProcessingInstr, + whitespace: this.config.preserveWhitespace, + textNodes: this.config.preserveTextNodes, + }; + + return preserveMap[propType] || false; + } + + /** + * Get the config property name for a specific feature + * @param {string} feature - Feature name + * @returns {string} - Property name in JSON representation + */ + getPropName(feature) { + return this.config.propNames[feature]; + } + + /** + * Get the feature name from property name + * @param {string} propName - Property name in JSON + * @returns {string} - Feature name + */ + getFeatureFromProp(propName) { + return this.propNamesReverse[propName]; } - - export default ConfigurationManager; \ No newline at end of file +} + +export default ConfigurationManager; diff --git a/src/components/JSONToXMLConverter.js b/src/components/JSONToXMLConverter.js index 8652983..28f8ed8 100644 --- a/src/components/JSONToXMLConverter.js +++ b/src/components/JSONToXMLConverter.js @@ -1,7 +1,7 @@ /** * JSONToXMLConverter * - * Handles converting JSON to XML with clean namespace handling + * Handles converting JSON to XML with prefix-based namespace handling */ class JSONToXMLConverter { /** @@ -27,35 +27,29 @@ class JSONToXMLConverter { const doc = this.domEnv.createDocument(null, null, null); // Get the root element name - const rootElName = Object.keys(jsonObj).find(key => !key.startsWith("@")); + const rootElName = Object.keys(jsonObj).find((key) => !key.startsWith("@")); if (!rootElName) { throw new Error("Invalid JSON: No root element found"); } const rootJSON = jsonObj[rootElName]; - - // Only process namespaces if preserving them + + // Track the namespaces we've already declared + const declaredNamespaces = new Map(); + + // Create the root element + const rootEl = this.createElement(doc, rootElName, rootJSON); + + // Declare namespaces on the root element if preserving them if (this.config.preserveNamespaces) { - // Pre-scan to identify namespaces and assign prefixes - const nsMap = this.scanNamespaces(jsonObj); - - // Create root element with proper namespace - const rootEl = this.createElement(doc, rootElName, rootJSON, nsMap); - - // Declare all namespaces on the root element - this.declareNamespaces(rootEl, nsMap); - - // Process root element (add attributes, content, and children) - this.processElement(doc, rootEl, rootJSON, nsMap, true); - - // Add root to document - doc.appendChild(rootEl); - } else { - // Simple processing without preserving namespaces - const rootEl = this.createSimpleElement(doc, rootElName, rootJSON); - this.processSimpleElement(doc, rootEl, rootJSON); - doc.appendChild(rootEl); + this.collectAndDeclareNamespaces(rootEl, jsonObj, declaredNamespaces); } + + // Process the root element + this.processElement(doc, rootEl, rootJSON, declaredNamespaces); + + // Add root to document + doc.appendChild(rootEl); // Serialize to XML const serializer = this.domEnv.createSerializer(); @@ -75,251 +69,108 @@ class JSONToXMLConverter { } /** - * Scan the JSON object to collect all namespaces and assign prefixes - * @param {Object} jsonObj - JSON object to scan - * @returns {Object} - Namespace mapping information + * Collect and declare all namespaces on the root element + * @param {Element} rootEl - Root element + * @param {Object} jsonObj - JSON object + * @param {Map} declaredNamespaces - Map of declared namespaces */ - scanNamespaces(jsonObj) { + collectAndDeclareNamespaces(rootEl, jsonObj, declaredNamespaces) { const nsKey = this.config.propNames.namespace; + const prefixKey = this.config.propNames.prefix; const childrenKey = this.config.propNames.children; const attrsKey = this.config.propNames.attributes; - // Initialize namespace mapping - const nsMap = { - // URI to prefix mapping - uriToPrefix: new Map(), - // Prefix to URI mapping - prefixToUri: new Map(), - // Original prefixes from element and attribute names - originalPrefixes: new Map(), - // Track namespace URIs that should be reserved for their original prefixes - reservedPrefixes: new Set(), - // Next auto-generated prefix number - nextPrefixNum: 1 - }; - - // Helper to extract prefix from name - const extractPrefix = (name) => { - if (name.includes(':')) { - return name.split(':')[0]; - } - return null; - }; - - // Initial pass to collect all original prefixes - const collectOriginalPrefixes = (name, nodeObj) => { - // Check element namespace and prefix - const ns = nodeObj[nsKey]; - if (ns) { - const prefix = extractPrefix(name); - if (prefix) { - // Remember original prefix for this namespace - if (!nsMap.originalPrefixes.has(ns)) { - nsMap.originalPrefixes.set(ns, prefix); - nsMap.reservedPrefixes.add(prefix); - } - } - } - - // Check attribute namespaces and prefixes - if (nodeObj[attrsKey]) { - for (const [attrName, attrObj] of Object.entries(nodeObj[attrsKey])) { - // Skip xmlns attributes - if (attrName === 'xmlns' || attrName.startsWith('xmlns:')) { - continue; - } - - const attrNs = attrObj[nsKey]; - if (attrNs) { - const prefix = extractPrefix(attrName); - if (prefix) { - // Remember original prefix for this namespace - if (!nsMap.originalPrefixes.has(attrNs)) { - nsMap.originalPrefixes.set(attrNs, prefix); - nsMap.reservedPrefixes.add(prefix); - } - } - } - } - } + // Function to collect namespaces + const collectNS = (name, nodeObj) => { + // Get namespace and prefix + const uri = nodeObj[nsKey]; + const prefix = nodeObj[prefixKey]; - // Process children recursively - if (Array.isArray(nodeObj[childrenKey])) { - for (const childObj of nodeObj[childrenKey]) { - for (const [childName, childData] of Object.entries(childObj)) { - if (!childName.startsWith('@')) { - collectOriginalPrefixes(childName, childData); - } - } - } - } - }; - - // Second pass to assign all prefixes - const assignPrefixes = (name, nodeObj) => { - // Process element namespace - const ns = nodeObj[nsKey]; - if (ns && !nsMap.uriToPrefix.has(ns)) { - // Use original prefix if available - if (nsMap.originalPrefixes.has(ns)) { - const prefix = nsMap.originalPrefixes.get(ns); - nsMap.uriToPrefix.set(ns, prefix); - nsMap.prefixToUri.set(prefix, ns); - } else { - // Generate new prefix - let newPrefix; - do { - newPrefix = `ns${nsMap.nextPrefixNum++}`; - } while (nsMap.reservedPrefixes.has(newPrefix)); - - nsMap.uriToPrefix.set(ns, newPrefix); - nsMap.prefixToUri.set(newPrefix, ns); - } + if (uri && prefix && !declaredNamespaces.has(uri)) { + // Add namespace declaration to root + rootEl.setAttributeNS('http://www.w3.org/2000/xmlns/', `xmlns:${prefix}`, uri); + declaredNamespaces.set(uri, prefix); } - // Process attribute namespaces + // Process attributes if (nodeObj[attrsKey]) { - for (const [attrName, attrObj] of Object.entries(nodeObj[attrsKey])) { - // Skip xmlns attributes - if (attrName === 'xmlns' || attrName.startsWith('xmlns:')) { - if (attrName.startsWith('xmlns:')) { - // Extract declared prefix and namespace - const declaredPrefix = attrName.substring(6); - const declaredNs = attrObj[this.config.propNames.value]; - - if (declaredPrefix && declaredNs) { - nsMap.reservedPrefixes.add(declaredPrefix); - if (!nsMap.originalPrefixes.has(declaredNs)) { - nsMap.originalPrefixes.set(declaredNs, declaredPrefix); - } - } - } - continue; - } - + for (const [_, attrObj] of Object.entries(nodeObj[attrsKey])) { const attrNs = attrObj[nsKey]; - if (attrNs && !nsMap.uriToPrefix.has(attrNs)) { - // Use original prefix if available - if (nsMap.originalPrefixes.has(attrNs)) { - const prefix = nsMap.originalPrefixes.get(attrNs); - nsMap.uriToPrefix.set(attrNs, prefix); - nsMap.prefixToUri.set(prefix, attrNs); - } else { - // Generate new prefix - let newPrefix; - do { - newPrefix = `ns${nsMap.nextPrefixNum++}`; - } while (nsMap.reservedPrefixes.has(newPrefix)); - - nsMap.uriToPrefix.set(attrNs, newPrefix); - nsMap.prefixToUri.set(newPrefix, attrNs); - } + const attrPrefix = attrObj[prefixKey]; + + if (attrNs && attrPrefix && !declaredNamespaces.has(attrNs)) { + // Add namespace declaration to root + rootEl.setAttributeNS('http://www.w3.org/2000/xmlns/', `xmlns:${attrPrefix}`, attrNs); + declaredNamespaces.set(attrNs, attrPrefix); } } } - // Process children recursively + // Process children if (Array.isArray(nodeObj[childrenKey])) { for (const childObj of nodeObj[childrenKey]) { for (const [childName, childData] of Object.entries(childObj)) { if (!childName.startsWith('@')) { - assignPrefixes(childName, childData); + collectNS(childName, childData); } } } } }; - // Start with the root element + // Start collection from root const rootName = Object.keys(jsonObj)[0]; - const rootObj = jsonObj[rootName]; - - // First collect original prefixes - collectOriginalPrefixes(rootName, rootObj); - - // Then assign prefixes to all namespaces - assignPrefixes(rootName, rootObj); - - return nsMap; + collectNS(rootName, jsonObj[rootName]); } /** - * Declare all namespaces on the root element - * @param {Element} rootEl - Root element - * @param {Object} nsMap - Namespace mapping - */ - declareNamespaces(rootEl, nsMap) { - // Add all namespace declarations to root - for (const [uri, prefix] of nsMap.uriToPrefix.entries()) { - if (uri && prefix) { - rootEl.setAttributeNS('http://www.w3.org/2000/xmlns/', `xmlns:${prefix}`, uri); - } - } - } - - /** - * Create an element with proper namespace + * Create an element with namespace if needed * @param {Document} doc - DOM document * @param {string} name - Element name * @param {Object} nodeObj - Element JSON object - * @param {Object} nsMap - Namespace mapping * @returns {Element} - Created element */ - createElement(doc, name, nodeObj, nsMap) { - const nsUri = nodeObj[this.config.propNames.namespace] || ''; + createElement(doc, name, nodeObj) { + const nsKey = this.config.propNames.namespace; + const prefixKey = this.config.propNames.prefix; - // Handle element with namespace - if (nsUri) { - // Get local name (without prefix) - const localName = name.includes(':') ? name.split(':')[1] : name; - - // Get assigned prefix for this namespace - const prefix = nsMap.uriToPrefix.get(nsUri); - - if (prefix) { - // Create with namespace and assigned prefix - try { - return doc.createElementNS(nsUri, `${prefix}:${localName}`); - } catch (e) { - console.warn(`Error creating element with namespace: ${e.message}`); - return doc.createElement(localName); - } - } else { - // No prefix assigned (should not happen) - return doc.createElement(localName); + // Skip namespace handling if not preserving or no namespace + if (!this.config.preserveNamespaces || !nodeObj[nsKey]) { + return doc.createElement(name); + } + + const nsUri = nodeObj[nsKey]; + const prefix = nodeObj[prefixKey]; + + // Create element with namespace + if (prefix) { + try { + return doc.createElementNS(nsUri, `${prefix}:${name}`); + } catch (e) { + console.warn(`Error creating element with namespace: ${e.message}`); + return doc.createElement(name); } } - // No namespace - create simple element - const localName = name.includes(':') ? name.split(':')[1] : name; - return doc.createElement(localName); - } - - /** - * Create an element without namespace handling - * @param {Document} doc - DOM document - * @param {string} name - Element name - * @param {Object} nodeObj - Element JSON object - * @returns {Element} - Created element - */ - createSimpleElement(doc, name, nodeObj) { - // Strip prefix if configured - if (this.config.stripPrefixes && name.includes(':')) { - return doc.createElement(name.split(':')[1]); + // No prefix - create simple element with namespace + try { + return doc.createElementNS(nsUri, name); + } catch (e) { + console.warn(`Error creating element with namespace: ${e.message}`); + return doc.createElement(name); } - return doc.createElement(name); } /** - * Process an element with namespace support + * Process an element (attributes, content, children) * @param {Document} doc - DOM document * @param {Element} element - Element to process * @param {Object} nodeObj - Element JSON object - * @param {Object} nsMap - Namespace mapping - * @param {boolean} isRoot - Whether this is the root element + * @param {Map} declaredNamespaces - Map of declared namespaces */ - processElement(doc, element, nodeObj, nsMap, isRoot = false) { + processElement(doc, element, nodeObj, declaredNamespaces) { const nsKey = this.config.propNames.namespace; + const prefixKey = this.config.propNames.prefix; const valKey = this.config.propNames.value; const attrsKey = this.config.propNames.attributes; const cdataKey = this.config.propNames.cdata; @@ -337,12 +188,11 @@ class JSONToXMLConverter { "json-to-xml" ); - // Add attributes (skip xmlns attributes unless this is the root) + // Add attributes if (nodeObj[attrsKey]) { - // Process attributes for (const [attrName, attrObj] of Object.entries(nodeObj[attrsKey])) { - // Skip xmlns attributes except at root - if ((attrName === 'xmlns' || attrName.startsWith('xmlns:')) && !isRoot) { + // Skip xmlns attributes - already handled + if (attrName === 'xmlns' || attrName.startsWith('xmlns:')) { continue; } @@ -365,36 +215,21 @@ class JSONToXMLConverter { isAttribute: true }) ?? strVal; - // Special handling for xmlns attributes on root - if (isRoot && (attrName === 'xmlns' || attrName.startsWith('xmlns:'))) { - element.setAttribute(attrName, transformedVal); - continue; - } - - // Process attribute namespace - const attrNs = attrObj[nsKey] || ''; - - if (attrNs) { - // Get assigned prefix - const prefix = nsMap.uriToPrefix.get(attrNs); + // Handle namespaced attribute + if (this.config.preserveNamespaces && attrObj[nsKey]) { + const attrNs = attrObj[nsKey]; + const attrPrefix = attrObj[prefixKey]; - if (prefix) { - // Get local name (without prefix) - const localName = attrName.includes(':') ? attrName.split(':')[1] : attrName; - + if (attrPrefix) { try { - // Set attribute with namespace and assigned prefix - element.setAttributeNS(attrNs, `${prefix}:${localName}`, transformedVal); + element.setAttributeNS(attrNs, `${attrPrefix}:${attrName}`, transformedVal); } catch (e) { - // Fallback to regular attribute - element.setAttribute(localName, transformedVal); + element.setAttribute(attrName, transformedVal); } } else { - // No prefix assigned (should not happen) element.setAttribute(attrName, transformedVal); } } else { - // Regular attribute without namespace element.setAttribute(attrName, transformedVal); } } @@ -462,148 +297,11 @@ class JSONToXMLConverter { for (const childObj of nodeObj[childrenKey]) { for (const [childName, childData] of Object.entries(childObj)) { if (!childName.startsWith('@')) { - // Create child with namespace - const childEl = this.createElement(doc, childName, childData, nsMap); - - // Process child element - this.processElement(doc, childEl, childData, nsMap, false); - - // Add to parent - element.appendChild(childEl); - } - } - } - } - } - - /** - * Process an element without namespace support - * @param {Document} doc - DOM document - * @param {Element} element - Element to process - * @param {Object} nodeObj - Element JSON object - */ - processSimpleElement(doc, element, nodeObj) { - const valKey = this.config.propNames.value; - const attrsKey = this.config.propNames.attributes; - const cdataKey = this.config.propNames.cdata; - const commentsKey = this.config.propNames.comments; - const processingKey = this.config.propNames.processing; - const childrenKey = this.config.propNames.children; - - // Create transform context - const context = this.nodeProcessor.createTransformContext( - { - nodeType: this.domEnv.nodeTypes.ELEMENT_NODE, - namespaceURI: "" - }, - element.nodeName, - "json-to-xml" - ); - - // Add attributes (without namespace handling) - if (nodeObj[attrsKey]) { - for (const [attrName, attrObj] of Object.entries(nodeObj[attrsKey])) { - // Skip xmlns attributes - if (attrName === 'xmlns' || attrName.startsWith('xmlns:')) { - continue; - } - - // Get attribute value - const attrVal = attrObj[valKey]; - if (attrVal === undefined) { - continue; - } - - // Convert to string if needed - const strVal = typeof attrVal === 'boolean' || typeof attrVal === 'number' - ? String(attrVal) - : attrVal; - - // Apply transform if configured - const transformedVal = this.nodeProcessor.applyTransform(strVal, { - ...context, - nodeName: attrName, - nodeType: this.domEnv.nodeTypes.ATTRIBUTE_NODE, - isAttribute: true - }) ?? strVal; - - // Strip prefix if configured - const processedName = this.config.stripPrefixes && attrName.includes(':') - ? attrName.split(':')[1] - : attrName; - - // Add attribute - element.setAttribute(processedName, transformedVal); - } - } - - // Add content - const content = nodeObj[valKey]; - if (content !== undefined && content !== null) { - // Format content - const strContent = typeof content === 'boolean' || typeof content === 'number' - ? String(content) - : content; - - // Apply transform if configured - const transformedContent = this.nodeProcessor.applyTransform(strContent, context) ?? strContent; - - // Add content to element - if (typeof transformedContent === 'string') { - if (this.nodeProcessor.containsHtmlMarkup(transformedContent)) { - // Mixed content - if (typeof element.innerHTML !== 'undefined') { - element.innerHTML = transformedContent; - } else { - element.textContent = transformedContent; - } - } else { - // Simple text - element.textContent = transformedContent; - } - } - } - - // Skip child processing if this is mixed content - if (content && this.nodeProcessor.containsHtmlMarkup(String(content))) { - return; - } - - // Add CDATA sections if preserving them - if (this.config.preserveCDATA && Array.isArray(nodeObj[cdataKey])) { - for (const cdataText of nodeObj[cdataKey]) { - const cdataSection = doc.createCDATASection(cdataText); - element.appendChild(cdataSection); - } - } - - // Add comments if preserving them - if (this.config.preserveComments && Array.isArray(nodeObj[commentsKey])) { - for (const commentText of nodeObj[commentsKey]) { - const comment = doc.createComment(commentText); - element.appendChild(comment); - } - } - - // Add processing instructions if preserving them - if (this.config.preserveProcessingInstr && Array.isArray(nodeObj[processingKey])) { - for (const piText of nodeObj[processingKey]) { - const [target, data] = piText.split(' ', 2); - const pi = doc.createProcessingInstruction(target, data || ''); - element.appendChild(pi); - } - } - - // Process children - if (Array.isArray(nodeObj[childrenKey])) { - for (const childObj of nodeObj[childrenKey]) { - for (const [childName, childData] of Object.entries(childObj)) { - if (!childName.startsWith('@')) { - // Create child without namespace - const childEl = this.createSimpleElement(doc, childName, childData); + // Create child element + const childEl = this.createElement(doc, childName, childData); // Process child element - this.processSimpleElement(doc, childEl, childData); + this.processElement(doc, childEl, childData, declaredNamespaces); // Add to parent element.appendChild(childEl); diff --git a/src/components/SchemaGenerator.js b/src/components/SchemaGenerator.js index 50cf5ab..2336c02 100644 --- a/src/components/SchemaGenerator.js +++ b/src/components/SchemaGenerator.js @@ -1,212 +1,236 @@ /** * SchemaGenerator - * + * * Generates JSON schema for the XMLJSONTransformer */ class SchemaGenerator { - /** - * Creates a new SchemaGenerator - * @param {ConfigurationManager} configManager - Configuration manager - */ - constructor(configManager) { - this.configManager = configManager; - this.config = configManager.config; - } - - /** - * Generate a JSON schema that matches the current configuration - * @returns {Object} - JSON schema - */ - generateSchema() { - const propNames = this.config.propNames; - const compact = this.config.outputOptions?.json?.compact || false; - const removeEmptyStrings = this.config.outputOptions?.json?.removeEmptyStrings || false; - const preserveNamespaces = this.config.preserveNamespaces; - - // Determine which properties are required based on the configuration - const requiredProps = []; - - if (!compact) { - requiredProps.push( - propNames.attributes, - propNames.cdata, - propNames.comments, - propNames.processing, - propNames.children - ); - - if (!removeEmptyStrings) { - requiredProps.push(propNames.value); - - if (preserveNamespaces) { - requiredProps.push(propNames.namespace); - } + /** + * Creates a new SchemaGenerator + * @param {ConfigurationManager} configManager - Configuration manager + */ + constructor(configManager) { + this.configManager = configManager; + this.config = configManager.config; + } + + /** + * Generate a JSON schema that matches the current configuration + * @returns {Object} - JSON schema + */ + generateSchema() { + const propNames = this.config.propNames; + const compact = this.config.outputOptions?.json?.compact || false; + const removeEmptyStrings = + this.config.outputOptions?.json?.removeEmptyStrings || false; + const preserveNamespaces = this.config.preserveNamespaces; + + // Determine which properties are required based on the configuration + const requiredProps = []; + + if (!compact) { + requiredProps.push( + propNames.attributes, + propNames.cdata, + propNames.comments, + propNames.processing, + propNames.children + ); + + if (!removeEmptyStrings) { + requiredProps.push(propNames.value); + + if (preserveNamespaces) { + requiredProps.push(propNames.namespace); + // Note: prefix is not required as it may not be present for all elements } } - - // Create schema for element properties - const elementProperties = {}; - - // Add namespace property if preserving namespaces - if (preserveNamespaces) { - elementProperties[propNames.namespace] = { - description: "Namespace URI of the element", - type: "string", - }; - } - - // Add value property - elementProperties[propNames.value] = { - description: "Text content of the element or raw content for mixed content elements", + } + + // Create schema for element properties + const elementProperties = {}; + + // Add namespace property if preserving namespaces + if (preserveNamespaces) { + elementProperties[propNames.namespace] = { + description: "Namespace URI of the element", type: "string", }; - - // Add attributes property - elementProperties[propNames.attributes] = { - description: "Element attributes", - type: "object", - patternProperties: { - "^.*$": { - type: "object", - properties: {}, - }, - }, - }; - - // Add attribute properties based on configuration - const attrProperties = elementProperties[propNames.attributes].patternProperties["^.*$"].properties; - - attrProperties[propNames.value] = { - description: "Attribute value", + + // Add prefix property if preserving namespaces + elementProperties[propNames.prefix] = { + description: "Namespace prefix of the element", type: "string", }; - - if (preserveNamespaces) { - attrProperties[propNames.namespace] = { - description: "Namespace URI of the attribute", - type: "string", - }; - } - - // Set required properties for attributes - const requiredAttrProps = [propNames.value]; - if (preserveNamespaces) { - requiredAttrProps.push(propNames.namespace); - } - - elementProperties[propNames.attributes].patternProperties["^.*$"].required = requiredAttrProps; - - // Add CDATA property - elementProperties[propNames.cdata] = { - description: "CDATA sections within the element", - type: "array", - items: { - type: "string", - }, - }; - - // Add comments property - elementProperties[propNames.comments] = { - description: "Comments within the element", - type: "array", - items: { - type: "string", - }, - }; - - // Add processing instructions property - elementProperties[propNames.processing] = { - description: "Processing instructions within the element", - type: "array", - items: { - type: "string", - }, - }; - - // Create the recursive child elements schema - const childElementSchema = { - type: "object", - properties: {}, // Will be filled in by the self-reference below - required: [], - }; - - // Add children property - elementProperties[propNames.children] = { - description: "Child elements", - type: "array", - items: { + } + + // Add value property + elementProperties[propNames.value] = { + description: + "Text content of the element or raw content for mixed content elements", + type: "string", + }; + + // Add attributes property + elementProperties[propNames.attributes] = { + description: "Element attributes", + type: "object", + patternProperties: { + "^.*$": { type: "object", - additionalProperties: false, - patternProperties: { - "^[^@].*$": childElementSchema, - }, + properties: {}, }, + }, + }; + + // Add attribute properties based on configuration + const attrProperties = + elementProperties[propNames.attributes].patternProperties["^.*$"] + .properties; + + attrProperties[propNames.value] = { + description: "Attribute value", + type: "string", + }; + + if (preserveNamespaces) { + attrProperties[propNames.namespace] = { + description: "Namespace URI of the attribute", + type: "string", }; - - // Create the final schema object - const schema = { - $schema: "http://json-schema.org/draft-07/schema#", - title: "XMLJSONTransformer Schema", - description: "JSON Schema for XML representation in XMLJSONTransformer", + + // Add prefix property for attributes + attrProperties[propNames.prefix] = { + description: "Namespace prefix of the attribute", + type: "string", + }; + } + + // Set required properties for attributes + const requiredAttrProps = [propNames.value]; + if (preserveNamespaces) { + requiredAttrProps.push(propNames.namespace); + // Note: prefix is not required as it may not be present for all attributes + } + + elementProperties[propNames.attributes].patternProperties["^.*$"].required = + requiredAttrProps; + + // Rest of the method remains the same... + + // Add CDATA property + elementProperties[propNames.cdata] = { + description: "CDATA sections within the element", + type: "array", + items: { + type: "string", + }, + }; + + // Add comments property + elementProperties[propNames.comments] = { + description: "Comments within the element", + type: "array", + items: { + type: "string", + }, + }; + + // Add processing instructions property + elementProperties[propNames.processing] = { + description: "Processing instructions within the element", + type: "array", + items: { + type: "string", + }, + }; + + // Create the recursive child elements schema + const childElementSchema = { + type: "object", + properties: {}, // Will be filled in by the self-reference below + required: [], + }; + + // Add children property + elementProperties[propNames.children] = { + description: "Child elements", + type: "array", + items: { type: "object", additionalProperties: false, patternProperties: { - "^[^@].*$": { - description: "XML element name as property key", - type: "object", - properties: elementProperties, - required: requiredProps, + "^[^@].*$": childElementSchema, + }, + }, + }; + + // Create the final schema object + const schema = { + $schema: "http://json-schema.org/draft-07/schema#", + title: "XMLJSONTransformer Schema", + description: "JSON Schema for XML representation in XMLJSONTransformer", + type: "object", + additionalProperties: false, + patternProperties: { + "^[^@].*$": { + description: "XML element name as property key", + type: "object", + properties: elementProperties, + required: requiredProps, + }, + }, + allOf: [ + { + description: "Schema requires exactly one XML element as the root", + minProperties: 1, + maxProperties: 1, + }, + ], + }; + + return schema; + } + + /** + * Generate an example based on the schema + * @returns {Object} - Example JSON object + */ + generateExample() { + const propNames = this.config.propNames; + + // Simple example with common features + return { + root: { + [propNames.namespace]: "http://example.org/ns", + [propNames.prefix]: "ex", // Added prefix property + [propNames.value]: "Root content", + [propNames.attributes]: { + id: { + [propNames.value]: "root-1", + [propNames.namespace]: "", + }, + lang: { + [propNames.value]: "en", + [propNames.namespace]: "", + [propNames.prefix]: "xml", // Added prefix property to attribute }, }, - allOf: [ + [propNames.children]: [ { - description: "Schema requires exactly one XML element as the root", - minProperties: 1, - maxProperties: 1, - }, - ], - }; - - return schema; - } - - /** - * Generate an example based on the schema - * @returns {Object} - Example JSON object - */ - generateExample() { - const propNames = this.config.propNames; - - // Simple example with common features - return { - "root": { - [propNames.namespace]: "http://example.org/ns", - [propNames.value]: "Root content", - [propNames.attributes]: { - "id": { - [propNames.value]: "root-1", - [propNames.namespace]: "" + child: { + [propNames.namespace]: "http://example.org/ns", + [propNames.prefix]: "ex", // Added prefix property + [propNames.value]: "Child content", + [propNames.attributes]: {}, + [propNames.cdata]: ["Raw content"], + [propNames.comments]: ["Comment about the child"], + [propNames.children]: [], }, - "lang": { - [propNames.value]: "en", - [propNames.namespace]: "" - } }, - [propNames.children]: [ - { - "child": { - [propNames.namespace]: "http://example.org/ns", - [propNames.value]: "Child content", - [propNames.attributes]: {}, - [propNames.cdata]: ["Raw content"], - [propNames.comments]: ["Comment about the child"], - [propNames.children]: [] - } - } - ] - } - }; - } + ], + }, + }; } - - export default SchemaGenerator; \ No newline at end of file +} + +export default SchemaGenerator; diff --git a/src/components/XMLToJSONConverter.js b/src/components/XMLToJSONConverter.js index 7929f29..959e871 100644 --- a/src/components/XMLToJSONConverter.js +++ b/src/components/XMLToJSONConverter.js @@ -46,10 +46,13 @@ class XMLToJSONConverter { processNode(node) { // Get the node name (tag name for elements) let nodeName = node.nodeName; + let prefix = null; - // Strip namespace prefix if configured - if (this.config.stripPrefixes && nodeName.includes(":")) { - nodeName = nodeName.split(":").pop(); + // Extract prefix if present + if (nodeName.includes(":")) { + const parts = nodeName.split(":"); + prefix = parts[0]; + nodeName = parts[1]; // Just store the local name without prefix } // Create the base JSON object @@ -66,6 +69,11 @@ class XMLToJSONConverter { // Always add namespace when preserving namespaces is enabled if (this.config.preserveNamespaces) { nodeObj[this.config.propNames.namespace] = node.namespaceURI || ""; + + // Store prefix if present + if (prefix) { + nodeObj[this.config.propNames.prefix] = prefix; + } } // When using compact mode and node is empty, return an empty object for the element @@ -206,13 +214,17 @@ class XMLToJSONConverter { // Process attribute name (strip prefix if configured) let attrName = attr.name; - if (this.config.stripPrefixes && attrName.includes(":")) { - attrName = attrName.split(":").pop(); + let prefix = null; + + if (attrName.includes(":")) { + const parts = attrName.split(":"); + prefix = parts[0]; + attrName = parts[1]; // Just store local name } const attrObj = {}; - // Step 1: Apply transform to attribute value if needed + // Apply transform to attribute value if needed let attrValue = this.nodeProcessor.applyTransform(attr.value, { ...context, nodeName: attrName, @@ -220,11 +232,6 @@ class XMLToJSONConverter { isAttribute: true, }); - // Step 2: Apply type conversions if configured - if (this.config.grokBoolean || this.config.grokNumber) { - attrValue = this.nodeProcessor.processValue(attrValue); - } - // Only add value property if not empty or if we're not removing empty strings if ( attrValue !== "" || @@ -233,9 +240,14 @@ class XMLToJSONConverter { attrObj[this.config.propNames.value] = attrValue; } - // Always add namespace if preserving namespaces + // Add namespace if preserving namespaces if (this.config.preserveNamespaces) { attrObj[this.config.propNames.namespace] = attr.namespaceURI || ""; + + // Store prefix if present + if (prefix) { + attrObj[this.config.propNames.prefix] = prefix; + } } nodeObj[this.config.propNames.attributes][attrName] = attrObj; From cd28766692427572a9312624f2a7ce058c8e607b Mon Sep 17 00:00:00 2001 From: William Summers Date: Sun, 20 Apr 2025 01:31:30 -0500 Subject: [PATCH 10/13] pretty printing doesn't introduce extra newlines --- src/components/JSONToXMLConverter.js | 214 +++++++++++++++------------ 1 file changed, 120 insertions(+), 94 deletions(-) diff --git a/src/components/JSONToXMLConverter.js b/src/components/JSONToXMLConverter.js index 28f8ed8..fa1f20e 100644 --- a/src/components/JSONToXMLConverter.js +++ b/src/components/JSONToXMLConverter.js @@ -33,21 +33,21 @@ class JSONToXMLConverter { } const rootJSON = jsonObj[rootElName]; - + // Track the namespaces we've already declared const declaredNamespaces = new Map(); - + // Create the root element const rootEl = this.createElement(doc, rootElName, rootJSON); - + // Declare namespaces on the root element if preserving them if (this.config.preserveNamespaces) { this.collectAndDeclareNamespaces(rootEl, jsonObj, declaredNamespaces); } - + // Process the root element this.processElement(doc, rootEl, rootJSON, declaredNamespaces); - + // Add root to document doc.appendChild(rootEl); @@ -79,45 +79,53 @@ class JSONToXMLConverter { const prefixKey = this.config.propNames.prefix; const childrenKey = this.config.propNames.children; const attrsKey = this.config.propNames.attributes; - + // Function to collect namespaces const collectNS = (name, nodeObj) => { // Get namespace and prefix const uri = nodeObj[nsKey]; const prefix = nodeObj[prefixKey]; - + if (uri && prefix && !declaredNamespaces.has(uri)) { // Add namespace declaration to root - rootEl.setAttributeNS('http://www.w3.org/2000/xmlns/', `xmlns:${prefix}`, uri); + rootEl.setAttributeNS( + "http://www.w3.org/2000/xmlns/", + `xmlns:${prefix}`, + uri + ); declaredNamespaces.set(uri, prefix); } - + // Process attributes if (nodeObj[attrsKey]) { for (const [_, attrObj] of Object.entries(nodeObj[attrsKey])) { const attrNs = attrObj[nsKey]; const attrPrefix = attrObj[prefixKey]; - + if (attrNs && attrPrefix && !declaredNamespaces.has(attrNs)) { // Add namespace declaration to root - rootEl.setAttributeNS('http://www.w3.org/2000/xmlns/', `xmlns:${attrPrefix}`, attrNs); + rootEl.setAttributeNS( + "http://www.w3.org/2000/xmlns/", + `xmlns:${attrPrefix}`, + attrNs + ); declaredNamespaces.set(attrNs, attrPrefix); } } } - + // Process children if (Array.isArray(nodeObj[childrenKey])) { for (const childObj of nodeObj[childrenKey]) { for (const [childName, childData] of Object.entries(childObj)) { - if (!childName.startsWith('@')) { + if (!childName.startsWith("@")) { collectNS(childName, childData); } } } } }; - + // Start collection from root const rootName = Object.keys(jsonObj)[0]; collectNS(rootName, jsonObj[rootName]); @@ -133,15 +141,15 @@ class JSONToXMLConverter { createElement(doc, name, nodeObj) { const nsKey = this.config.propNames.namespace; const prefixKey = this.config.propNames.prefix; - + // Skip namespace handling if not preserving or no namespace if (!this.config.preserveNamespaces || !nodeObj[nsKey]) { return doc.createElement(name); } - + const nsUri = nodeObj[nsKey]; const prefix = nodeObj[prefixKey]; - + // Create element with namespace if (prefix) { try { @@ -151,7 +159,7 @@ class JSONToXMLConverter { return doc.createElement(name); } } - + // No prefix - create simple element with namespace try { return doc.createElementNS(nsUri, name); @@ -177,52 +185,58 @@ class JSONToXMLConverter { const commentsKey = this.config.propNames.comments; const processingKey = this.config.propNames.processing; const childrenKey = this.config.propNames.children; - + // Create transform context const context = this.nodeProcessor.createTransformContext( { nodeType: this.domEnv.nodeTypes.ELEMENT_NODE, - namespaceURI: nodeObj[nsKey] || "" + namespaceURI: nodeObj[nsKey] || "", }, element.nodeName, "json-to-xml" ); - + // Add attributes if (nodeObj[attrsKey]) { for (const [attrName, attrObj] of Object.entries(nodeObj[attrsKey])) { // Skip xmlns attributes - already handled - if (attrName === 'xmlns' || attrName.startsWith('xmlns:')) { + if (attrName === "xmlns" || attrName.startsWith("xmlns:")) { continue; } - + // Get attribute value const attrVal = attrObj[valKey]; if (attrVal === undefined) { continue; } - + // Convert to string if needed - const strVal = typeof attrVal === 'boolean' || typeof attrVal === 'number' - ? String(attrVal) - : attrVal; - + const strVal = + typeof attrVal === "boolean" || typeof attrVal === "number" + ? String(attrVal) + : attrVal; + // Apply transform if configured - const transformedVal = this.nodeProcessor.applyTransform(strVal, { - ...context, - nodeName: attrName, - nodeType: this.domEnv.nodeTypes.ATTRIBUTE_NODE, - isAttribute: true - }) ?? strVal; - + const transformedVal = + this.nodeProcessor.applyTransform(strVal, { + ...context, + nodeName: attrName, + nodeType: this.domEnv.nodeTypes.ATTRIBUTE_NODE, + isAttribute: true, + }) ?? strVal; + // Handle namespaced attribute if (this.config.preserveNamespaces && attrObj[nsKey]) { const attrNs = attrObj[nsKey]; const attrPrefix = attrObj[prefixKey]; - + if (attrPrefix) { try { - element.setAttributeNS(attrNs, `${attrPrefix}:${attrName}`, transformedVal); + element.setAttributeNS( + attrNs, + `${attrPrefix}:${attrName}`, + transformedVal + ); } catch (e) { element.setAttribute(attrName, transformedVal); } @@ -234,23 +248,25 @@ class JSONToXMLConverter { } } } - + // Add content const content = nodeObj[valKey]; if (content !== undefined && content !== null) { // Format content - const strContent = typeof content === 'boolean' || typeof content === 'number' - ? String(content) - : content; - + const strContent = + typeof content === "boolean" || typeof content === "number" + ? String(content) + : content; + // Apply transform if configured - const transformedContent = this.nodeProcessor.applyTransform(strContent, context) ?? strContent; - + const transformedContent = + this.nodeProcessor.applyTransform(strContent, context) ?? strContent; + // Add content to element - if (typeof transformedContent === 'string') { + if (typeof transformedContent === "string") { if (this.nodeProcessor.containsHtmlMarkup(transformedContent)) { // Mixed content - if (typeof element.innerHTML !== 'undefined') { + if (typeof element.innerHTML !== "undefined") { element.innerHTML = transformedContent; } else { element.textContent = transformedContent; @@ -261,12 +277,12 @@ class JSONToXMLConverter { } } } - + // Skip child processing if this is mixed content if (content && this.nodeProcessor.containsHtmlMarkup(String(content))) { return; } - + // Add CDATA sections if preserving them if (this.config.preserveCDATA && Array.isArray(nodeObj[cdataKey])) { for (const cdataText of nodeObj[cdataKey]) { @@ -274,7 +290,7 @@ class JSONToXMLConverter { element.appendChild(cdataSection); } } - + // Add comments if preserving them if (this.config.preserveComments && Array.isArray(nodeObj[commentsKey])) { for (const commentText of nodeObj[commentsKey]) { @@ -282,27 +298,30 @@ class JSONToXMLConverter { element.appendChild(comment); } } - + // Add processing instructions if preserving them - if (this.config.preserveProcessingInstr && Array.isArray(nodeObj[processingKey])) { + if ( + this.config.preserveProcessingInstr && + Array.isArray(nodeObj[processingKey]) + ) { for (const piText of nodeObj[processingKey]) { - const [target, data] = piText.split(' ', 2); - const pi = doc.createProcessingInstruction(target, data || ''); + const [target, data] = piText.split(" ", 2); + const pi = doc.createProcessingInstruction(target, data || ""); element.appendChild(pi); } } - + // Process children if (Array.isArray(nodeObj[childrenKey])) { for (const childObj of nodeObj[childrenKey]) { for (const [childName, childData] of Object.entries(childObj)) { - if (!childName.startsWith('@')) { + if (!childName.startsWith("@")) { // Create child element const childEl = this.createElement(doc, childName, childData); - + // Process child element this.processElement(doc, childEl, childData, declaredNamespaces); - + // Add to parent element.appendChild(childEl); } @@ -317,52 +336,59 @@ class JSONToXMLConverter { * @returns {string} - Formatted XML string */ prettyPrintXML(xmlString) { - const PADDING = typeof this.config.outputOptions.indent === 'number' - ? ' '.repeat(this.config.outputOptions.indent) - : ' '; - - // Handle XML declaration separately if present - let declaration = ""; - if (xmlString.startsWith("") + 2; - declaration = xmlString.substring(0, endIndex) + "\n"; - xmlString = xmlString.substring(endIndex); - } + const INDENT = + typeof this.config.outputOptions.indent === "number" + ? " ".repeat(this.config.outputOptions.indent) + : " "; + let formatted = ""; + let indent = 0; - // Normalize spacing between tags and content - const tokens = xmlString - .replace(/>\s*<") // collapse inter-tag whitespace - .replace(//g, ">\n") // newline after each tag - .split("\n") // split into lines - .map((line) => line.trim()) - .filter((line) => line.length > 0); // remove empty lines + // Remove newlines and extra whitespace between tags + xmlString = xmlString.replace(/>\s+<").trim(); - let indentLevel = 0; - const result = []; + // Split into tags and text + const tokens = xmlString + .split(/(<[^>]+>)/) + .filter((token) => token.trim() !== ""); for (let i = 0; i < tokens.length; i++) { - const line = tokens[i]; + const token = tokens[i]; - const isClosingTag = /^<\/[^>]+>/.test(line); - const isOpeningTag = /^<[^!?\/][^>]*[^\/]>$/.test(line); - const isSelfClosingTag = /^<[^>]+\/>$/.test(line); + if (token.startsWith("]*\/>$/)) { + // Self-closing tag + formatted += INDENT.repeat(indent) + token + "\n"; + } else if (token.startsWith("<")) { + // Opening tag + // If next token is text and the one after that is a closing tag for this tag, keep inline + const next = tokens[i + 1]; + const nextNext = tokens[i + 2]; - if (isClosingTag) { - indentLevel = Math.max(indentLevel - 1, 0); - } - - const indent = PADDING.repeat(indentLevel); - result.push(indent + line); - - if (isOpeningTag) { - indentLevel++; + if ( + next && + !next.startsWith("<") && + nextNext === ` Date: Sun, 20 Apr 2025 01:34:53 -0500 Subject: [PATCH 11/13] stripPrefixes is no longer an option. its just the behavior --- src/components/ConfigurationManager.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/ConfigurationManager.js b/src/components/ConfigurationManager.js index fa62374..dc8d228 100644 --- a/src/components/ConfigurationManager.js +++ b/src/components/ConfigurationManager.js @@ -50,9 +50,6 @@ class ConfigurationManager { // value transform function transformFunction: null, - // Element name handling - stripPrefixes: true, - // Output options outputOptions: { prettyPrint: true, From 08a76e5befb9cd9a55ddfda3a4cf0c4d9267b612 Mon Sep 17 00:00:00 2001 From: William Summers Date: Sun, 20 Apr 2025 01:43:37 -0500 Subject: [PATCH 12/13] demo page updates --- demo/demo.js | 133 +---------------------------------------------- demo/index.html | 6 ++- demo/samples.js | 134 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 132 deletions(-) create mode 100644 demo/samples.js diff --git a/demo/demo.js b/demo/demo.js index 9b72f64..ebae669 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -2,139 +2,9 @@ * XMLJSONTransformer Browser Demo */ import XMLJSONTransformer from '../dist/index.js'; +import samples from './samples.js'; document.addEventListener('DOMContentLoaded', () => { - // Sample definitions - const samples = { - library: { - name: "Library Catalog", - description: "XML example with namespaces, comments, CDATA and processing instructions", - xml: ` - - - - JavaScript: The Good Parts - Douglas Crockford - 2008 - Programming - - - - Clean Code - Robert C. Martin - 2008 - Programming - - - -` - }, - soap: { - name: "SOAP Message", - description: "XML example with multiple namespaces and nested elements", - xml: ` - - - - 1234 - - - - - IBM - - -` - }, - mixed: { - name: "Mixed Content", - description: "XML with mixed content (text and elements mixed)", - xml: `
-

This paragraph has emphasized text and strong text mixed with regular text.

-

Another paragraph with a link in the middle.

-
    -
  • Item with emphasized text
  • -
  • Plain item
  • -
-
` - }, - rss: { - name: "RSS Feed", - description: "Example RSS 2.0 feed", - xml: ` - - - Example RSS Feed - https://example.com - This is an example RSS feed - en-us - Mon, 01 Jul 2023 12:00:00 GMT - - First Article - https://example.com/first-article - bold text]]> - Mon, 01 Jul 2023 10:00:00 GMT - https://example.com/first-article - - - Second Article - https://example.com/second-article - a link]]> - Mon, 01 Jul 2023 11:00:00 GMT - https://example.com/second-article - - -` - }, - svg: { - name: "SVG Graphic", - description: "Scalable Vector Graphics example", - xml: ` - - - - - SVG Text -` - }, - atom: { - name: "Atom Feed", - description: "Example Atom 1.0 feed", - xml: ` - - Example Atom Feed - - 2023-07-01T12:00:00Z - - John Doe - john@example.com - - urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6 - - - First Entry - - urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a - 2023-07-01T10:00:00Z - Summary of the first entry - This is the content of the first entry.

]]>
-
- - - Second Entry - - urn:uuid:1225c695-cfb8-4ebb-bbbb-80da344efa6a - 2023-07-01T11:00:00Z - Summary of the second entry - This is the content of the second entry.

]]>
-
-
` - } - }; - // Predefined transformer functions const transformers = { none: null, @@ -294,6 +164,7 @@ document.addEventListener('DOMContentLoaded', () => { // Property names propNames: { namespace: document.getElementById('namespace-prop').value, + prefix: document.getElementById('prefix-prop').value, value: document.getElementById('value-prop').value, attributes: document.getElementById('attributes-prop').value, cdata: document.getElementById('cdata-prop').value, diff --git a/demo/index.html b/demo/index.html index 19b751a..038dd66 100644 --- a/demo/index.html +++ b/demo/index.html @@ -292,7 +292,7 @@

Output Options

- +
@@ -328,6 +328,10 @@

Property Names

+
+ + +
diff --git a/demo/samples.js b/demo/samples.js new file mode 100644 index 0000000..42a7254 --- /dev/null +++ b/demo/samples.js @@ -0,0 +1,134 @@ +/** + * XML samples for XMLJSONTransformer demo + */ +export const samples = { + library: { + name: "Library Catalog", + description: "XML example with namespaces, comments, CDATA and processing instructions", + xml: ` + + + + JavaScript: The Good Parts + Douglas Crockford + 2008 + Programming + + + + Clean Code + Robert C. Martin + 2008 + Programming + + + + ` + }, + soap: { + name: "SOAP Message", + description: "XML example with multiple namespaces and nested elements", + xml: ` + + + + 1234 + + + + + IBM + + + ` + }, + mixed: { + name: "Mixed Content", + description: "XML with mixed content (text and elements mixed)", + xml: `
+

This paragraph has emphasized text and strong text mixed with regular text.

+

Another paragraph with a link in the middle.

+
    +
  • Item with emphasized text
  • +
  • Plain item
  • +
+
` + }, + rss: { + name: "RSS Feed", + description: "Example RSS 2.0 feed", + xml: ` + + + Example RSS Feed + https://example.com + This is an example RSS feed + en-us + Mon, 01 Jul 2023 12:00:00 GMT + + First Article + https://example.com/first-article + bold text]]> + Mon, 01 Jul 2023 10:00:00 GMT + https://example.com/first-article + + + Second Article + https://example.com/second-article + a link]]> + Mon, 01 Jul 2023 11:00:00 GMT + https://example.com/second-article + + + ` + }, + svg: { + name: "SVG Graphic", + description: "Scalable Vector Graphics example", + xml: ` + + + + + SVG Text + ` + }, + atom: { + name: "Atom Feed", + description: "Example Atom 1.0 feed", + xml: ` + + Example Atom Feed + + 2023-07-01T12:00:00Z + + John Doe + john@example.com + + urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6 + + + First Entry + + urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a + 2023-07-01T10:00:00Z + Summary of the first entry + This is the content of the first entry.

]]>
+
+ + + Second Entry + + urn:uuid:1225c695-cfb8-4ebb-bbbb-80da344efa6a + 2023-07-01T11:00:00Z + Summary of the second entry + This is the content of the second entry.

]]>
+
+
` + } + }; + + export default samples; \ No newline at end of file From e66ea2e6151a0db2255a189e1e00af385ed706ea Mon Sep 17 00:00:00 2001 From: William Summers Date: Sun, 20 Apr 2025 01:47:40 -0500 Subject: [PATCH 13/13] removed prefix option from demo options --- demo/demo.js | 3 +-- demo/index.html | 10 +--------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/demo/demo.js b/demo/demo.js index ebae669..170cddc 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -141,8 +141,7 @@ document.addEventListener('DOMContentLoaded', () => { // Transform function transformFunction: transformFunction, - // Element name handling - stripPrefixes: document.getElementById('strip-prefixes').checked, + // Strip prefixes option removed from configuration // Output options outputOptions: { diff --git a/demo/index.html b/demo/index.html index 038dd66..1c0cd30 100644 --- a/demo/index.html +++ b/demo/index.html @@ -235,15 +235,7 @@

Features to Preserve

-
-

Element Name Handling

-
-
- - -
-
-
+

Type Conversion