Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
379 changes: 291 additions & 88 deletions demo/demo.js

Large diffs are not rendered by default.

104 changes: 79 additions & 25 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
</style>
</head>
<body>
Expand Down Expand Up @@ -235,39 +268,60 @@ <h4>Features to Preserve</h4>
</div>
</div>

<!-- Removed Element Name Handling section with stripPrefixes option -->

<div class="config-section">
<h4>Type Conversion</h4>
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" id="grok-boolean">
<label for="grok-boolean">Convert Boolean Values</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="grok-number">
<label for="grok-number">Convert Number Values</label>
<h4>Value Transformers</h4>
<p>Add transformers that process values during conversion. Transformers are applied in the order they're added.</p>

<div class="transform-list" id="transform-list">
<!-- Transforms will be added here dynamically -->
<div class="transform-item" id="no-transforms-message">
No transformers added yet
</div>
</div>
</div>

<div class="config-section">
<h4>Transform Function</h4>

<div class="input-group">
<div class="input-item" style="width: 100%;">
<label for="transform-selector">Predefined Transform:</label>
<select id="transform-selector">
<option value="none">None</option>
<option value="uppercase">UPPERCASE</option>
<option value="lowercase">lowercase</option>
<option value="custom">Custom</option>
<div class="input-item">
<label for="add-transformer-type">Add Transformer:</label>
<select id="add-transformer-type">
<option value="boolean">Boolean Transformer</option>
<option value="number">Number Transformer</option>
<option value="custom">Custom Transformer</option>
</select>
</div>
<div class="input-item" style="align-self: flex-end;">
<button id="add-transformer-btn">Add Transformer</button>
</div>
</div>
<div id="custom-transform-container" style="display: none; margin-top: 10px;">
<textarea id="transform-function" class="transform-function" placeholder="// Custom transform function

<div id="boolean-options" class="transform-options" style="display: none;">
<h5>Boolean Transformer Options</h5>
<div class="input-group">
<div class="input-item">
<label for="boolean-true-values">True Values (comma-separated):</label>
<input type="text" id="boolean-true-values" value="true,yes,1">
</div>
<div class="input-item">
<label for="boolean-false-values">False Values (comma-separated):</label>
<input type="text" id="boolean-false-values" value="false,no,0">
</div>
</div>
</div>

<div id="number-options" class="transform-options" style="display: none;">
<h5>Number Transformer Options</h5>
<p>The number transformer will automatically detect and convert numeric values.</p>
</div>

<div id="custom-options" class="transform-options" style="display: none;">
<h5>Custom Transformer</h5>
<p>Define a custom transformer function:</p>
<textarea id="custom-function" class="transform-function" placeholder="// Custom transform function
// Example: (value, context) => {
// return typeof value === 'string' ? value.toUpperCase() : value;
// // context contains: direction, nodeName, nodeType, etc.
// if (context.direction === 'xml-to-json') {
// return typeof value === 'string' ? value.toUpperCase() : value;
// }
// return value;
// }"></textarea>
</div>
</div>
Expand Down
9 changes: 6 additions & 3 deletions src/XMLJSONTransformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
8 changes: 2 additions & 6 deletions src/components/ConfigurationManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
66 changes: 49 additions & 17 deletions src/components/JSONToXMLConverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/",
Expand All @@ -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/",
Expand Down Expand Up @@ -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]) {
Expand All @@ -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) {
Expand All @@ -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(
Expand Down Expand Up @@ -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") {
Expand Down
41 changes: 11 additions & 30 deletions src/components/NodeProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -17,11 +16,9 @@ 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
this.valueTransforms = this.config.valueTransforms || [];
}

/**
Expand Down Expand Up @@ -84,10 +81,10 @@ class NodeProcessor {
* @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,
Expand All @@ -99,7 +96,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
Expand All @@ -111,32 +108,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
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
Expand All @@ -157,7 +138,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
Expand Down
Loading
Loading