From 63c5eef072c7fbda015c35162387bbdf8441964d Mon Sep 17 00:00:00 2001 From: William Summers Date: Mon, 21 Apr 2025 08:44:24 -0500 Subject: [PATCH 1/5] added value trasnformer interface --- src/components/ValueTransformer.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/components/ValueTransformer.js diff --git a/src/components/ValueTransformer.js b/src/components/ValueTransformer.js new file mode 100644 index 0000000..0dbcd4c --- /dev/null +++ b/src/components/ValueTransformer.js @@ -0,0 +1,12 @@ +class ValueTransformer { + /** + * Process a value, transforming it if applicable + * @param {any} value - Value to potentially transform + * @param {Object} context - Context including direction and other information + * @returns {any} - Transformed value or original if not applicable + */ + process(value, context = {}) { + // Base implementation returns original value + return value; + } + } \ No newline at end of file From 4449c1a7dfb9f634ea8c88bbae8a4db9fb753eae Mon Sep 17 00:00:00 2001 From: William Summers Date: Mon, 21 Apr 2025 08:59:32 -0500 Subject: [PATCH 2/5] value transforms processing --- src/XMLJSONTransformer.js | 9 ++- src/components/ConfigurationManager.js | 8 +-- src/components/NodeProcessor.js | 96 +++----------------------- src/components/ValueTransformer.js | 12 ---- src/transformers/BooleanTransformer.js | 87 ++++++++++++++--------- src/transformers/ValueTransformer.js | 17 +++++ 6 files changed, 92 insertions(+), 137 deletions(-) delete mode 100644 src/components/ValueTransformer.js create mode 100644 src/transformers/ValueTransformer.js diff --git a/src/XMLJSONTransformer.js b/src/XMLJSONTransformer.js index 19fd071..772f525 100644 --- a/src/XMLJSONTransformer.js +++ b/src/XMLJSONTransformer.js @@ -26,15 +26,18 @@ export class XMLJSONTransformer { // Initialize configuration manager this.configManager = new ConfigurationManager(config); + // Expose configuration for backward compatibility + this.config = this.configManager.config; + + // Ensure transformers array exists + this.config.valueTransforms = Array.isArray(config.valueTransforms) ? config.valueTransforms : []; + // Initialize components this.nodeProcessor = new NodeProcessor(this.configManager, DOMEnvironment); this.xmlToJsonConverter = new XMLToJSONConverter(this.configManager, this.nodeProcessor, DOMEnvironment); this.jsonToXmlConverter = new JSONToXMLConverter(this.configManager, this.nodeProcessor, DOMEnvironment); this.pathNavigator = new PathNavigator(this.configManager); this.schemaGenerator = new SchemaGenerator(this.configManager); - - // Expose configuration for backward compatibility - this.config = this.configManager.config; } /** diff --git a/src/components/ConfigurationManager.js b/src/components/ConfigurationManager.js index dc8d228..73382de 100644 --- a/src/components/ConfigurationManager.js +++ b/src/components/ConfigurationManager.js @@ -43,12 +43,8 @@ class ConfigurationManager { preserveTextNodes: true, preserveWhitespace: false, - // Type conversion options - grokBoolean: false, - grokNumber: false, - - // value transform function - transformFunction: null, + // value transforms + transformers: [], // Output options outputOptions: { diff --git a/src/components/NodeProcessor.js b/src/components/NodeProcessor.js index 59f60ad..3965ce3 100644 --- a/src/components/NodeProcessor.js +++ b/src/components/NodeProcessor.js @@ -4,8 +4,7 @@ * Handles processing of different node types */ -import BooleanTransformer from '../transformers/BooleanTransformer.js'; -import NumberTransformer from '../transformers/NumberTransformer.js'; +import ValueTransformer from '../transformers/ValueTransformer.js'; class NodeProcessor { /** @@ -17,77 +16,20 @@ class NodeProcessor { this.configManager = configManager; this.domEnv = domEnv; this.config = configManager.config; - this.hasTransform = typeof this.config.transformFunction === "function"; - // Create transformers - this.booleanTransformer = new BooleanTransformer(); - this.numberTransformer = new NumberTransformer(); + // Get valueTransforms from config or use empty array (renamed from transformers) + this.valueTransforms = this.config.valueTransforms || []; } - /** - * Check if a node has mixed content (both text and element nodes) - * @param {Node} node - The DOM node to check - * @returns {boolean} - Whether the node has mixed content - */ - hasMixedContent(node) { - if ( - node.nodeType !== this.domEnv.nodeTypes.ELEMENT_NODE || - !node.hasChildNodes() - ) { - return false; - } - - let hasTextNode = false; - let hasElementNode = false; - - for (let i = 0; i < node.childNodes.length; i++) { - const childNode = node.childNodes[i]; - - if (childNode.nodeType === this.domEnv.nodeTypes.TEXT_NODE) { - // Skip pure whitespace nodes when checking for text content - if (childNode.textContent.trim() !== "") { - hasTextNode = true; - } - } else if (childNode.nodeType === this.domEnv.nodeTypes.ELEMENT_NODE) { - hasElementNode = true; - } - - if (hasTextNode && hasElementNode) return true; - } - - return hasTextNode && hasElementNode; - } - - /** - * Get the innerHTML of a node (with fallback for environments without innerHTML) - * @param {Node} node - The DOM node - * @returns {string} - The innerHTML of the node - */ - getInnerHTML(node) { - if (node.innerHTML !== undefined) { - return node.innerHTML; - } - - // Fallback implementation - const serializer = this.domEnv.createSerializer(); - let result = ""; - - for (let i = 0; i < node.childNodes.length; i++) { - result += serializer.serializeToString(node.childNodes[i]); - } - - return result; - } - /** * Create a context object for transform operations * @param {Node} node - DOM node * @param {string} nodeName - Node name * @param {string} direction - Transform direction - * @returns {Object|null} - Context object or null if no transform + * @returns {Object|null} - Context object or null if no valueTransforms */ createTransformContext(node, nodeName, direction) { - if (!this.hasTransform) return null; + if (!this.valueTransforms || this.valueTransforms.length === 0) return null; return { nodeName: nodeName, @@ -99,7 +41,7 @@ class NodeProcessor { } /** - * Transform a value using applicable transformers + * Transform a value using the transformer pipeline * @param {any} value - Value to transform * @param {Object} context - Transform context * @returns {any} - Transformed value @@ -111,32 +53,16 @@ class NodeProcessor { let result = value; - // Get direction (default to xml-to-json if not specified) - const direction = context && context.direction ? context.direction : 'xml-to-json'; - - // 1. Apply boolean transformer if enabled and applicable - if (this.config.grokBoolean && this.booleanTransformer.shouldApply(result, direction)) { - result = this.booleanTransformer.transform(result, direction); - } - - // 2. Apply number transformer if enabled and applicable - if (this.config.grokNumber && this.numberTransformer.shouldApply(result, direction)) { - result = this.numberTransformer.transform(result, direction); - } - - // 3. Apply custom transformer function if configured - if (typeof this.config.transformFunction === 'function') { - const transformed = this.config.transformFunction(result, context); - if (transformed !== undefined) { - result = transformed; - } + // Apply each transform in sequence (renamed from transformers) + for (const transform of this.valueTransforms) { + result = transform.process(result, context); } return result; } /** - * Apply transform function to a value + * Apply transform to a value (alias for transformValue) * @param {any} value - Value to transform * @param {Object} context - Transform context * @returns {any} - Transformed value @@ -157,7 +83,7 @@ class NodeProcessor { } /** - * Process a value based on configuration settings + * Process a value (alias for transformValue) * @param {string} value - Original value to process * @param {Object} context - Transform context * @returns {any} - Processed value diff --git a/src/components/ValueTransformer.js b/src/components/ValueTransformer.js deleted file mode 100644 index 0dbcd4c..0000000 --- a/src/components/ValueTransformer.js +++ /dev/null @@ -1,12 +0,0 @@ -class ValueTransformer { - /** - * Process a value, transforming it if applicable - * @param {any} value - Value to potentially transform - * @param {Object} context - Context including direction and other information - * @returns {any} - Transformed value or original if not applicable - */ - process(value, context = {}) { - // Base implementation returns original value - return value; - } - } \ No newline at end of file diff --git a/src/transformers/BooleanTransformer.js b/src/transformers/BooleanTransformer.js index 7d082e7..d5b9908 100644 --- a/src/transformers/BooleanTransformer.js +++ b/src/transformers/BooleanTransformer.js @@ -1,39 +1,64 @@ +import ValueTransformer from './ValueTransformer.js'; + /** - * Transforms values between string and boolean types + * Transforms string values to boolean types */ -class BooleanTransformer { - /** - * Check if transformer should be applied - * @param {any} value - Value to check - * @param {string} direction - Conversion direction ('xml-to-json' or 'json-to-xml') - * @returns {boolean} - Whether to apply transformation - */ - shouldApply(value, direction = 'xml-to-json') { - if (direction === 'xml-to-json') { - return typeof value === 'string' && - (value.toLowerCase() === 'true' || value.toLowerCase() === 'false'); - } else { - return typeof value === 'boolean'; - } - } +class BooleanTransformer extends ValueTransformer { + /** + * Creates a new BooleanTransformer + * @param {Object} options - Transformer options + * @param {string[]} options.trueValues - Values to consider as true + * @param {string[]} options.falseValues - Values to consider as false + */ + constructor(options = {}) { + super(); + + // Set default values if not provided + this.trueValues = options.trueValues || ['true']; + this.falseValues = options.falseValues || ['false']; + + // Precompute lowercase versions for case-insensitive comparison + this.trueValuesLower = this.trueValues.map(v => String(v).toLowerCase()); + this.falseValuesLower = this.falseValues.map(v => String(v).toLowerCase()); + } + + /** + * Process a value, transforming it if applicable + * @param {any} value - Value to potentially transform + * @param {Object} context - Context including direction and other information + * @returns {any} - Transformed value or original if not applicable + */ + process(value, context = {}) { + const direction = context.direction || 'xml-to-json'; - /** - * Transform value based on direction - * @param {any} value - Value to transform - * @param {string} direction - Conversion direction ('xml-to-json' or 'json-to-xml') - * @returns {boolean|string} - Transformed value - */ - transform(value, direction = 'xml-to-json') { - if (!this.shouldApply(value, direction)) return value; + if (direction === 'xml-to-json') { + // Only process strings in XML-to-JSON direction + if (typeof value !== 'string') return value; - if (direction === 'xml-to-json') { - // Convert string to boolean - return value.toLowerCase() === 'true'; - } else { - // Convert boolean to string - return String(value); + // Convert to lowercase for case-insensitive comparison + const valueLower = value.toLowerCase(); + + // Check against true values + if (this.trueValuesLower.includes(valueLower)) { + return true; + } + + // Check against false values + if (this.falseValuesLower.includes(valueLower)) { + return false; } + } + else if (direction === 'json-to-xml') { + // Only process booleans in JSON-to-XML direction + if (typeof value !== 'boolean') return value; + + // Convert to string representation + return String(value); } + + // If no transformation applies, return original value + return value; } +} - export default BooleanTransformer; +export default BooleanTransformer; \ No newline at end of file diff --git a/src/transformers/ValueTransformer.js b/src/transformers/ValueTransformer.js new file mode 100644 index 0000000..cb3c177 --- /dev/null +++ b/src/transformers/ValueTransformer.js @@ -0,0 +1,17 @@ +/** + * Abstract base class for value transformers + */ +class ValueTransformer { + /** + * Process a value, transforming it if applicable + * @param {any} value - Value to potentially transform + * @param {Object} context - Context including direction and other information + * @returns {any} - Transformed value or original if not applicable + */ + process(value, context = {}) { + // Base implementation returns original value + return value; + } +} + +export default ValueTransformer; \ No newline at end of file From e566b3e318481d10b25b4f378c6f15d025f787b8 Mon Sep 17 00:00:00 2001 From: William Summers Date: Mon, 21 Apr 2025 09:21:19 -0500 Subject: [PATCH 3/5] fix to prevent redef of xmlns namespace --- src/components/JSONToXMLConverter.js | 66 +++++++++++++++++++++------- src/components/NodeProcessor.js | 59 ++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 19 deletions(-) diff --git a/src/components/JSONToXMLConverter.js b/src/components/JSONToXMLConverter.js index fa1f20e..b27b107 100644 --- a/src/components/JSONToXMLConverter.js +++ b/src/components/JSONToXMLConverter.js @@ -87,6 +87,11 @@ class JSONToXMLConverter { const prefix = nodeObj[prefixKey]; if (uri && prefix && !declaredNamespaces.has(uri)) { + // Skip if this is an attempt to redefine the xmlns namespace + if (uri === "http://www.w3.org/2000/xmlns/" && prefix === "xmlns") { + return; // Skip this namespace declaration + } + // Add namespace declaration to root rootEl.setAttributeNS( "http://www.w3.org/2000/xmlns/", @@ -103,6 +108,14 @@ class JSONToXMLConverter { const attrPrefix = attrObj[prefixKey]; if (attrNs && attrPrefix && !declaredNamespaces.has(attrNs)) { + // Skip if this is an attempt to redefine the xmlns namespace + if ( + attrNs === "http://www.w3.org/2000/xmlns/" && + attrPrefix === "xmlns" + ) { + continue; // Skip this namespace declaration + } + // Add namespace declaration to root rootEl.setAttributeNS( "http://www.w3.org/2000/xmlns/", @@ -187,14 +200,12 @@ class JSONToXMLConverter { 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" - ); + const context = { + nodeName: element.nodeName, + nodeType: this.domEnv.nodeTypes.ELEMENT_NODE, + namespaceURI: nodeObj[nsKey] || "", + direction: "json-to-xml", + }; // Add attributes if (nodeObj[attrsKey]) { @@ -204,6 +215,14 @@ class JSONToXMLConverter { continue; } + // Skip attributes with the xmlns namespace + if ( + attrObj[nsKey] === "http://www.w3.org/2000/xmlns/" && + (attrObj[prefixKey] === "xmlns" || attrName === "xmlns") + ) { + continue; + } + // Get attribute value const attrVal = attrObj[valKey]; if (attrVal === undefined) { @@ -216,20 +235,31 @@ class JSONToXMLConverter { ? String(attrVal) : attrVal; + // Create attribute context + const attrContext = { + nodeName: attrName, + nodeType: this.domEnv.nodeTypes.ATTRIBUTE_NODE, + isAttribute: true, + namespaceURI: attrObj[nsKey] || "", + direction: "json-to-xml", + }; + // 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.transformValue( + strVal, + attrContext + ); // Handle namespaced attribute if (this.config.preserveNamespaces && attrObj[nsKey]) { const attrNs = attrObj[nsKey]; const attrPrefix = attrObj[prefixKey]; + // Skip if this is an attempt to redefine the xmlns namespace + if (attrNs === "http://www.w3.org/2000/xmlns/") { + continue; + } + if (attrPrefix) { try { element.setAttributeNS( @@ -259,8 +289,10 @@ class JSONToXMLConverter { : content; // Apply transform if configured - const transformedContent = - this.nodeProcessor.applyTransform(strContent, context) ?? strContent; + const transformedContent = this.nodeProcessor.transformValue( + strContent, + context + ); // Add content to element if (typeof transformedContent === "string") { diff --git a/src/components/NodeProcessor.js b/src/components/NodeProcessor.js index 3965ce3..0d36757 100644 --- a/src/components/NodeProcessor.js +++ b/src/components/NodeProcessor.js @@ -17,10 +17,65 @@ class NodeProcessor { this.domEnv = domEnv; this.config = configManager.config; - // Get valueTransforms from config or use empty array (renamed from transformers) + // Get valueTransforms from config or use empty array this.valueTransforms = this.config.valueTransforms || []; } + /** + * Check if a node has mixed content (both text and element nodes) + * @param {Node} node - The DOM node to check + * @returns {boolean} - Whether the node has mixed content + */ + hasMixedContent(node) { + if ( + node.nodeType !== this.domEnv.nodeTypes.ELEMENT_NODE || + !node.hasChildNodes() + ) { + return false; + } + + let hasTextNode = false; + let hasElementNode = false; + + for (let i = 0; i < node.childNodes.length; i++) { + const childNode = node.childNodes[i]; + + if (childNode.nodeType === this.domEnv.nodeTypes.TEXT_NODE) { + // Skip pure whitespace nodes when checking for text content + if (childNode.textContent.trim() !== "") { + hasTextNode = true; + } + } else if (childNode.nodeType === this.domEnv.nodeTypes.ELEMENT_NODE) { + hasElementNode = true; + } + + if (hasTextNode && hasElementNode) return true; + } + + return hasTextNode && hasElementNode; + } + + /** + * Get the innerHTML of a node (with fallback for environments without innerHTML) + * @param {Node} node - The DOM node + * @returns {string} - The innerHTML of the node + */ + getInnerHTML(node) { + if (node.innerHTML !== undefined) { + return node.innerHTML; + } + + // Fallback implementation + const serializer = this.domEnv.createSerializer(); + let result = ""; + + for (let i = 0; i < node.childNodes.length; i++) { + result += serializer.serializeToString(node.childNodes[i]); + } + + return result; + } + /** * Create a context object for transform operations * @param {Node} node - DOM node @@ -53,7 +108,7 @@ class NodeProcessor { let result = value; - // Apply each transform in sequence (renamed from transformers) + // Apply each transform in sequence for (const transform of this.valueTransforms) { result = transform.process(result, context); } From 62f6d231db157b7ad3f52a78bf1ccd9d5d0211db Mon Sep 17 00:00:00 2001 From: William Summers Date: Mon, 21 Apr 2025 09:31:07 -0500 Subject: [PATCH 4/5] refactor number transformer to new format fixed sci notation handling --- src/transformers/NumberTransformer.js | 91 +++++++++++++++------------ 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/src/transformers/NumberTransformer.js b/src/transformers/NumberTransformer.js index 8873263..acd7bbd 100644 --- a/src/transformers/NumberTransformer.js +++ b/src/transformers/NumberTransformer.js @@ -1,51 +1,60 @@ /** - * Transforms values between string and number types + * Transforms string values to number types */ -class NumberTransformer { - /** - * Check if transformer should be applied - * @param {any} value - Value to check - * @param {string} direction - Conversion direction ('xml-to-json' or 'json-to-xml') - * @returns {boolean} - Whether to apply transformation - */ - shouldApply(value, direction = 'xml-to-json') { - if (direction === 'xml-to-json') { - if (typeof value !== 'string' || value === '') return false; - - // Clean value (remove thousands separators) - const cleanValue = value.replace(/,(?=\d{3})/g, ''); - - // Check if it matches number patterns - return /^[-+]?[\d]+$/.test(cleanValue) || - /^[-+]?[\d]*\.[\d]+$/.test(cleanValue) || - /^[-+]?[\d]*\.?[\d]*[eE][-+]?[\d]+$/.test(cleanValue); - } else { - return typeof value === 'number'; - } - } +class NumberTransformer extends ValueTransformer { + /** + * Creates a NumberTransformer + * @param {Object} options - Configuration options + */ + constructor(options = {}) { + super(); + this.options = options; + + // Precompile regular expressions for better performance + this.integerPattern = /^[-+]?[\d]+$/; + this.floatPattern = /^[-+]?[\d]*\.[\d]+$/; + this.thousandsSeparatorPattern = /,(?=\d{3})/g; + } + + /** + * Process a value, transforming it if applicable + * @param {any} value - Value to potentially transform + * @param {Object} context - Context including direction and other information + * @returns {any} - Transformed value or original if not applicable + */ + process(value, context = {}) { + const direction = context.direction || 'xml-to-json'; - /** - * Transform value based on direction - * @param {any} value - Value to transform - * @param {string} direction - Conversion direction ('xml-to-json' or 'json-to-xml') - * @returns {number|string} - Transformed value - */ - transform(value, direction = 'xml-to-json') { - if (!this.shouldApply(value, direction)) return value; + if (direction === 'xml-to-json') { + // Only process strings in XML-to-JSON direction + if (typeof value !== 'string' || value === '') return value; - if (direction === 'xml-to-json') { - try { - // Convert string to number - const cleanValue = value.replace(/,(?=\d{3})/g, ''); + // Clean value (remove thousands separators) + const cleanValue = value.replace(this.thousandsSeparatorPattern, ''); + + try { + // Check if it's an integer or floating point number + if (this.integerPattern.test(cleanValue)) { + return parseInt(cleanValue, 10); + } else if (this.floatPattern.test(cleanValue)) { return parseFloat(cleanValue); - } catch (e) { - return value; } - } else { - // Convert number to string - return String(value); + } catch (e) { + // If parsing fails, return the original value + return value; } + } + else if (direction === 'json-to-xml') { + // Only process numbers in JSON-to-XML direction + if (typeof value !== 'number') return value; + + // Convert to string + return String(value); } + + // If no transformation applies, return original value + return value; } +} - export default NumberTransformer; +export default NumberTransformer; \ No newline at end of file From ef9749e0e40e2802d6227ff22529523fdcab7804 Mon Sep 17 00:00:00 2001 From: William Summers Date: Mon, 21 Apr 2025 09:31:51 -0500 Subject: [PATCH 5/5] demo page updates --- demo/demo.js | 379 +++++++++++++++++++++++++++++++++++++----------- demo/index.html | 104 +++++++++---- 2 files changed, 370 insertions(+), 113 deletions(-) diff --git a/demo/demo.js b/demo/demo.js index 170cddc..897b95e 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -5,18 +5,10 @@ import XMLJSONTransformer from '../dist/index.js'; import samples from './samples.js'; document.addEventListener('DOMContentLoaded', () => { - // Predefined transformer functions - const transformers = { - none: null, - uppercase: (val, context) => { - return typeof val === 'string' ? val.toUpperCase() : val; - }, - lowercase: (val, context) => { - return typeof val === 'string' ? val.toLowerCase() : val; - }, - custom: null // Will be set from textarea - }; - + // Keep track of added transformers + const transformers = []; + let transformerIdCounter = 0; + // Initialize the demo // Set default sample document.getElementById('xml-input').value = samples.library.xml; @@ -32,22 +24,151 @@ document.addEventListener('DOMContentLoaded', () => { configToggleBtn.textContent = isVisible ? 'Show Configuration Options' : 'Hide Configuration Options'; }); - // Transform function selector - const transformSelector = document.getElementById('transform-selector'); - const customTransformContainer = document.getElementById('custom-transform-container'); + // Transformer type selector logic + const transformerTypeSelector = document.getElementById('add-transformer-type'); + const booleanOptions = document.getElementById('boolean-options'); + const numberOptions = document.getElementById('number-options'); + const customOptions = document.getElementById('custom-options'); - transformSelector.addEventListener('change', (event) => { - const selectedTransform = event.target.value; - if (selectedTransform === 'custom') { - customTransformContainer.style.display = 'block'; - } else { - customTransformContainer.style.display = 'none'; + transformerTypeSelector.addEventListener('change', () => { + const selectedType = transformerTypeSelector.value; + // Hide all option panels + booleanOptions.style.display = 'none'; + numberOptions.style.display = 'none'; + customOptions.style.display = 'none'; + + // Show the selected option panel + if (selectedType === 'boolean') { + booleanOptions.style.display = 'block'; + } else if (selectedType === 'number') { + numberOptions.style.display = 'block'; + } else if (selectedType === 'custom') { + customOptions.style.display = 'block'; + } + }); + + // Add transformer button logic + document.getElementById('add-transformer-btn').addEventListener('click', () => { + const selectedType = transformerTypeSelector.value; + const transformerId = transformerIdCounter++; + let transformerName = ''; + let transformerConfig = null; + + if (selectedType === 'boolean') { + const trueValues = document.getElementById('boolean-true-values').value.split(',').map(v => v.trim()); + const falseValues = document.getElementById('boolean-false-values').value.split(',').map(v => v.trim()); + transformerName = 'Boolean Transformer'; + transformerConfig = { + type: 'boolean', + options: { + trueValues, + falseValues + } + }; + } else if (selectedType === 'number') { + transformerName = 'Number Transformer'; + transformerConfig = { + type: 'number' + }; + } else if (selectedType === 'custom') { + const customCode = document.getElementById('custom-function').value.trim(); + if (!customCode) { + showError('Please enter a custom transform function'); + return; + } + + transformerName = 'Custom Transformer'; + transformerConfig = { + type: 'custom', + code: customCode + }; } - // Update configuration - updateCurrentConfig(getConfig()); + // Add transformer to the list + transformers.push({ + id: transformerId, + name: transformerName, + config: transformerConfig + }); + + // Update the UI + updateTransformerList(); + updateCurrentConfig(); }); + // Function to update the transformer list in the UI + function updateTransformerList() { + const transformList = document.getElementById('transform-list'); + const noTransformsMessage = document.getElementById('no-transforms-message'); + + // Clear the list + while (transformList.firstChild) { + transformList.removeChild(transformList.firstChild); + } + + if (transformers.length === 0) { + // If no transformers, show the message + transformList.appendChild(noTransformsMessage); + return; + } + + // Add each transformer to the list + transformers.forEach((transformer, index) => { + const transformItem = document.createElement('div'); + transformItem.className = 'transform-item'; + + const nameSpan = document.createElement('span'); + nameSpan.textContent = `${index + 1}. ${transformer.name}`; + + const actionsDiv = document.createElement('div'); + actionsDiv.className = 'transform-actions'; + + const moveUpBtn = document.createElement('button'); + moveUpBtn.textContent = '↑'; + moveUpBtn.title = 'Move Up'; + moveUpBtn.disabled = index === 0; + moveUpBtn.addEventListener('click', () => { + if (index > 0) { + // Swap with the previous transformer + [transformers[index], transformers[index - 1]] = [transformers[index - 1], transformers[index]]; + updateTransformerList(); + updateCurrentConfig(); + } + }); + + const moveDownBtn = document.createElement('button'); + moveDownBtn.textContent = '↓'; + moveDownBtn.title = 'Move Down'; + moveDownBtn.disabled = index === transformers.length - 1; + moveDownBtn.addEventListener('click', () => { + if (index < transformers.length - 1) { + // Swap with the next transformer + [transformers[index], transformers[index + 1]] = [transformers[index + 1], transformers[index]]; + updateTransformerList(); + updateCurrentConfig(); + } + }); + + const removeBtn = document.createElement('button'); + removeBtn.textContent = '✕'; + removeBtn.title = 'Remove'; + removeBtn.addEventListener('click', () => { + transformers.splice(index, 1); + updateTransformerList(); + updateCurrentConfig(); + }); + + actionsDiv.appendChild(moveUpBtn); + actionsDiv.appendChild(moveDownBtn); + actionsDiv.appendChild(removeBtn); + + transformItem.appendChild(nameSpan); + transformItem.appendChild(actionsDiv); + + transformList.appendChild(transformItem); + }); + } + // Sample selector event listener document.getElementById('sample-selector').addEventListener('change', (event) => { const selectedSample = event.target.value; @@ -57,6 +178,109 @@ document.addEventListener('DOMContentLoaded', () => { } }); + // Create transformer instances for XMLJSONTransformer + function createTransformerInstances() { + return transformers.map(transformer => { + if (transformer.config.type === 'boolean') { + return new BooleanTransformer(transformer.config.options); + } else if (transformer.config.type === 'number') { + return new NumberTransformer(); + } else if (transformer.config.type === 'custom') { + return new CustomTransformer(transformer.config.code); + } + return null; + }).filter(t => t !== null); + } + + // Helper class for Boolean Transformer + class BooleanTransformer { + constructor(options = {}) { + this.trueValues = options.trueValues || ['true']; + this.falseValues = options.falseValues || ['false']; + this.trueValuesLower = this.trueValues.map(v => String(v).toLowerCase()); + this.falseValuesLower = this.falseValues.map(v => String(v).toLowerCase()); + } + + process(value, context = {}) { + const direction = context.direction || 'xml-to-json'; + + if (direction === 'xml-to-json') { + if (typeof value !== 'string') return value; + const valueLower = value.toLowerCase(); + + if (this.trueValuesLower.includes(valueLower)) { + return true; + } + + if (this.falseValuesLower.includes(valueLower)) { + return false; + } + } + else if (direction === 'json-to-xml') { + if (typeof value !== 'boolean') return value; + return String(value); + } + + return value; + } + } + + // Helper class for Number Transformer + class NumberTransformer { + process(value, context = {}) { + const direction = context.direction || 'xml-to-json'; + + if (direction === 'xml-to-json') { + if (typeof value !== 'string' || value === '') return value; + + // Clean value (remove thousands separators) + const cleanValue = value.replace(/,(?=\d{3})/g, ''); + + // Check if it matches number patterns + const isNumber = + /^[-+]?[\d]+$/.test(cleanValue) || + /^[-+]?[\d]*\.[\d]+$/.test(cleanValue) || + /^[-+]?[\d]*\.?[\d]*[eE][-+]?[\d]+$/.test(cleanValue); + + if (isNumber) { + try { + return parseFloat(cleanValue); + } catch (e) { + return value; + } + } + } + else if (direction === 'json-to-xml') { + if (typeof value === 'number') { + return String(value); + } + } + + return value; + } + } + + // Helper class for Custom Transformer + class CustomTransformer { + constructor(code) { + try { + this.transformFn = new Function('value', 'context', code); + } catch (error) { + console.error('Error creating custom transformer:', error); + this.transformFn = (value) => value; + } + } + + process(value, context = {}) { + try { + return this.transformFn(value, context); + } catch (error) { + console.error('Error in custom transformer:', error); + return value; + } + } + } + // XML to JSON conversion document.getElementById('xml-to-json').addEventListener('click', () => { try { @@ -108,22 +332,8 @@ document.addEventListener('DOMContentLoaded', () => { // Helper function to get configuration from UI function getConfig() { - const transformType = document.getElementById('transform-selector').value; - let transformFunction = transformers[transformType]; - - // Handle custom transform function - if (transformType === 'custom') { - const customCode = document.getElementById('transform-function').value.trim(); - if (customCode) { - try { - // Use Function constructor to create a function from the string - transformFunction = new Function('value', 'context', 'return ' + customCode); - } catch (error) { - showError('Error in custom transform function: ' + error.message); - transformFunction = null; - } - } - } + // Create value transforms + const valueTransforms = createTransformerInstances(); return { // Features to preserve @@ -134,14 +344,8 @@ document.addEventListener('DOMContentLoaded', () => { preserveTextNodes: document.getElementById('preserve-text-nodes').checked, preserveWhitespace: document.getElementById('preserve-whitespace').checked, - // Type conversion options - grokBoolean: document.getElementById('grok-boolean').checked, - grokNumber: document.getElementById('grok-number').checked, - - // Transform function - transformFunction: transformFunction, - - // Strip prefixes option removed from configuration + // Value transforms + valueTransforms: valueTransforms, // Output options outputOptions: { @@ -182,16 +386,16 @@ document.addEventListener('DOMContentLoaded', () => { } // Create a copy to prevent circular references when trying to stringify - const configCopy = JSON.parse(JSON.stringify(config)); + const configCopy = { ...config }; - // Add placeholder for transform function if it exists - if (config.transformFunction) { - const transformType = document.getElementById('transform-selector').value; - if (transformType === 'custom') { - configCopy.transformFunction = document.getElementById('transform-function').value; - } else { - configCopy.transformFunction = `[Function: ${transformType}]`; - } + // Handle value transforms display + if (config.valueTransforms && config.valueTransforms.length > 0) { + configCopy.valueTransforms = transformers.map(t => { + const { id, ...rest } = t; + return rest; + }); + } else { + configCopy.valueTransforms = []; } configElement.textContent = JSON.stringify(configCopy, null, 2); @@ -210,7 +414,10 @@ document.addEventListener('DOMContentLoaded', () => { }, 5000); } - // Function to load samples from external files + // Initialize transformer type selector to show first option + transformerTypeSelector.dispatchEvent(new Event('change')); + + // Function to load samples from external files (if available) async function loadExternalSamples() { try { const response = await fetch('samples/index.json'); @@ -235,43 +442,39 @@ document.addEventListener('DOMContentLoaded', () => { } // Update selection handler - selector.removeEventListener('change', selectorChangeHandler); - selector.addEventListener('change', selectorChangeHandler); + selector.addEventListener('change', async (event) => { + const selectedValue = event.target.value; + + // Handle built-in samples + if (selectedValue && !selectedValue.startsWith('external:') && samples[selectedValue]) { + document.getElementById('xml-input').value = samples[selectedValue].xml; + document.getElementById('json-output').value = ''; + return; + } + + // Handle external samples + if (selectedValue && selectedValue.startsWith('external:')) { + const sampleId = selectedValue.split(':')[1]; + try { + const sampleResponse = await fetch(`samples/${sampleId}.xml`); + if (!sampleResponse.ok) { + throw new Error(`Failed to load sample ${sampleId}`); + } + + const sampleXml = await sampleResponse.text(); + document.getElementById('xml-input').value = sampleXml; + document.getElementById('json-output').value = ''; + } catch (error) { + showError(`Error loading external sample: ${error.message}`); + } + } + }); } catch (error) { console.error('Error loading external samples:', error); // Silently fail - user can still use built-in samples } } - // Sample selector change handler (defined separately for removeEventListener) - async function selectorChangeHandler(event) { - const selectedValue = event.target.value; - - // Handle built-in samples - if (selectedValue && !selectedValue.startsWith('external:') && samples[selectedValue]) { - document.getElementById('xml-input').value = samples[selectedValue].xml; - document.getElementById('json-output').value = ''; - return; - } - - // Handle external samples - if (selectedValue && selectedValue.startsWith('external:')) { - const sampleId = selectedValue.split(':')[1]; - try { - const sampleResponse = await fetch(`samples/${sampleId}.xml`); - if (!sampleResponse.ok) { - throw new Error(`Failed to load sample ${sampleId}`); - } - - const sampleXml = await sampleResponse.text(); - document.getElementById('xml-input').value = sampleXml; - document.getElementById('json-output').value = ''; - } catch (error) { - showError(`Error loading external sample: ${error.message}`); - } - } - } - // Try to load external samples loadExternalSamples().catch(error => console.error('Error in loadExternalSamples:', error)); }); \ No newline at end of file diff --git a/demo/index.html b/demo/index.html index 1c0cd30..9e0bc1e 100644 --- a/demo/index.html +++ b/demo/index.html @@ -167,6 +167,39 @@ font-family: monospace; font-size: 14px; } + .transform-list { + border: 1px solid #ddd; + border-radius: 4px; + padding: 10px; + margin-bottom: 10px; + max-height: 200px; + overflow-y: auto; + } + .transform-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 5px; + border-bottom: 1px solid #eee; + } + .transform-item:last-child { + border-bottom: none; + } + .transform-actions { + display: flex; + gap: 5px; + } + .transform-actions button { + padding: 3px 8px; + font-size: 12px; + } + .transform-options { + margin-top: 10px; + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + background-color: #f9f9f9; + } @@ -235,39 +268,60 @@

Features to Preserve

- -
-

Type Conversion

-
-
- - -
-
- - +

Value Transformers

+

Add transformers that process values during conversion. Transformers are applied in the order they're added.

+ +
+ +
+ No transformers added yet
-
- -
-

Transform Function

+
-
- - + + +
+
+ +
-