From 953da6df6fad0cb194a9e4f0c90c32aff4648b52 Mon Sep 17 00:00:00 2001 From: Leonel Sanches da Silva <53848829+leonelsanchesdasilva@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:44:42 -0800 Subject: [PATCH 1/5] Reapplying #150 due to a Copilot limitation. --- src/xpath/lib | 2 +- src/xpath/xpath.ts | 22 ++-- tests/json-to-xml.test.tsx | 195 +++++++++++---------------------- tests/xml/xml-to-json.test.tsx | 30 ++++- 4 files changed, 105 insertions(+), 144 deletions(-) diff --git a/src/xpath/lib b/src/xpath/lib index baf6329..cb4c0d4 160000 --- a/src/xpath/lib +++ b/src/xpath/lib @@ -1 +1 @@ -Subproject commit baf632939603e298c579655d00959e29fc66ca13 +Subproject commit cb4c0d466a72b6206df23602408290500bc886e1 diff --git a/src/xpath/xpath.ts b/src/xpath/xpath.ts index 040cb80..97575de 100644 --- a/src/xpath/xpath.ts +++ b/src/xpath/xpath.ts @@ -3,7 +3,6 @@ // while maintaining backward compatibility with the existing XSLT API. import { XNode } from '../dom'; -import { DOM_DOCUMENT_NODE, DOM_TEXT_NODE, DOM_ELEMENT_NODE } from '../constants'; import { XPathLexer } from './lib/src/lexer'; import { XPathParser } from './lib/src/parser'; import { XPathExpression, XPathLocationPath, XPathUnionExpression } from './lib/src/expressions'; @@ -299,8 +298,8 @@ class NodeConverter { // xml-to-json() function - XSLT 3.0 specific functions['xml-to-json'] = (nodes: any) => { // Check XSLT version - only supported in 3.0 - if (exprContext.xsltVersion === '1.0') { - throw new Error('xml-to-json() is not supported in XSLT 1.0. Use version="3.0" in your stylesheet.'); + if (exprContext.xsltVersion !== '3.0') { + throw new Error('xml-to-json() is only supported in XSLT 3.0. Use version="3.0" in your stylesheet.'); } // Handle node set or single node @@ -316,14 +315,14 @@ class NodeConverter { // json-to-xml() function - XSLT 3.0 specific functions['json-to-xml'] = (jsonText: any) => { // Check XSLT version - only supported in 3.0 - if (exprContext.xsltVersion === '1.0') { - throw new Error('json-to-xml() is not supported in XSLT 1.0. Use version="3.0" in your stylesheet.'); + if (exprContext.xsltVersion !== '3.0') { + throw new Error('json-to-xml() is only supported in XSLT 3.0. Use version="3.0" in your stylesheet.'); } // Handle node set or single value const jsonStr = Array.isArray(jsonText) ? jsonText[0] : jsonText; if (!jsonStr) { - return []; + return null; } // Convert JSON string to XML document node using xpath lib converter @@ -331,7 +330,7 @@ class NodeConverter { const xpathNode = converter.convert(String(jsonStr)); if (!xpathNode) { - return []; + return null; } // Get owner document from context @@ -343,7 +342,7 @@ class NodeConverter { const convertedNode = this.convertXPathNodeToXNode(xpathNode, ownerDoc); // Return as array for consistency with xpath processor - return convertedNode ? [convertedNode] : []; + return convertedNode ? [convertedNode] : null; }; return functions; @@ -358,6 +357,9 @@ class NodeConverter { return null; } + const { XNode: XNodeClass } = require('../dom'); + const { DOM_DOCUMENT_NODE, DOM_TEXT_NODE, DOM_ELEMENT_NODE } = require('../constants'); + let node: XNode; if (xpathNode.nodeType === DOM_DOCUMENT_NODE) { @@ -371,7 +373,7 @@ class NodeConverter { } else if (xpathNode.nodeType === DOM_TEXT_NODE) { // Create a text node const textContent = xpathNode.textContent || ''; - node = new XNode( + node = new XNodeClass( DOM_TEXT_NODE, '#text', textContent, @@ -379,7 +381,7 @@ class NodeConverter { ); } else { // Element node (DOM_ELEMENT_NODE) - node = new XNode( + node = new XNodeClass( DOM_ELEMENT_NODE, xpathNode.nodeName || 'element', '', diff --git a/tests/json-to-xml.test.tsx b/tests/json-to-xml.test.tsx index aea3dd9..dc645d9 100644 --- a/tests/json-to-xml.test.tsx +++ b/tests/json-to-xml.test.tsx @@ -3,23 +3,6 @@ import assert from 'assert'; import { Xslt } from '../src/xslt'; import { XmlParser } from '../src/dom'; -import { DOM_TEXT_NODE } from '../src/constants'; - -// Helper function to get text content from an element -// XNode doesn't have textContent property, so we need to get it from child text nodes -function getTextContent(element: any): string { - if (!element) return ''; - if (element.nodeType === DOM_TEXT_NODE) { // Text node - return element.nodeValue || ''; - } - if (element.childNodes && element.childNodes.length > 0) { - return element.childNodes - .filter((node: any) => node.nodeType === DOM_TEXT_NODE) // Only text nodes - .map((node: any) => node.nodeValue || '') - .join(''); - } - return ''; -} describe('json-to-xml', () => { it('json-to-xml() should throw error in XSLT 1.0', async () => { @@ -40,7 +23,30 @@ describe('json-to-xml', () => { await assert.rejects( async () => await xsltClass.xsltProcess(xml, xslt), { - message: /json-to-xml\(\) is not supported in XSLT 1\.0/ + message: /json-to-xml\(\) is only supported in XSLT 3\.0/ + } + ); + }); + + it('json-to-xml() should throw error in XSLT 2.0', async () => { + const xmlString = ``; + + const xsltString = ` + + + + + `; + + const xsltClass = new Xslt(); + const xmlParser = new XmlParser(); + const xml = xmlParser.xmlParse(xmlString); + const xslt = xmlParser.xmlParse(xsltString); + + await assert.rejects( + async () => await xsltClass.xsltProcess(xml, xslt), + { + message: /json-to-xml\(\) is only supported in XSLT 3\.0/ } ); }); @@ -63,11 +69,8 @@ describe('json-to-xml', () => { const xslt = xmlParser.xmlParse(xsltString); const outXmlString = await xsltClass.xsltProcess(xml, xslt); - // Parse output and verify structure - const outDoc = xmlParser.xmlParse(outXmlString); - const rootElements = outDoc.getElementsByTagName('root'); - assert.strictEqual(rootElements.length, 1, 'Should have exactly one root element'); - assert.strictEqual(getTextContent(rootElements[0]), 'hello', 'Root element should contain text "hello"'); + // Should contain a root element with text content "hello" + assert(outXmlString.includes('hello')); }); it('json-to-xml() should convert simple number JSON', async () => { @@ -88,11 +91,8 @@ describe('json-to-xml', () => { const xslt = xmlParser.xmlParse(xsltString); const outXmlString = await xsltClass.xsltProcess(xml, xslt); - // Parse output and verify structure - const outDoc = xmlParser.xmlParse(outXmlString); - const rootElements = outDoc.getElementsByTagName('root'); - assert.strictEqual(rootElements.length, 1, 'Should have exactly one root element'); - assert.strictEqual(getTextContent(rootElements[0]), '42', 'Root element should contain text "42"'); + // Should contain the number 42 + assert(outXmlString.includes('42')); }); it('json-to-xml() should convert boolean true', async () => { @@ -113,11 +113,8 @@ describe('json-to-xml', () => { const xslt = xmlParser.xmlParse(xsltString); const outXmlString = await xsltClass.xsltProcess(xml, xslt); - // Parse output and verify structure - const outDoc = xmlParser.xmlParse(outXmlString); - const rootElements = outDoc.getElementsByTagName('root'); - assert.strictEqual(rootElements.length, 1, 'Should have exactly one root element'); - assert.strictEqual(getTextContent(rootElements[0]), 'true', 'Root element should contain text "true"'); + // Should contain "true" + assert(outXmlString.includes('true')); }); it('json-to-xml() should convert boolean false', async () => { @@ -138,11 +135,8 @@ describe('json-to-xml', () => { const xslt = xmlParser.xmlParse(xsltString); const outXmlString = await xsltClass.xsltProcess(xml, xslt); - // Parse output and verify structure - const outDoc = xmlParser.xmlParse(outXmlString); - const rootElements = outDoc.getElementsByTagName('root'); - assert.strictEqual(rootElements.length, 1, 'Should have exactly one root element'); - assert.strictEqual(getTextContent(rootElements[0]), 'false', 'Root element should contain text "false"'); + // Should contain "false" + assert(outXmlString.includes('false')); }); it('json-to-xml() should convert null JSON', async () => { @@ -163,11 +157,8 @@ describe('json-to-xml', () => { const xslt = xmlParser.xmlParse(xsltString); const outXmlString = await xsltClass.xsltProcess(xml, xslt); - // Parse output and verify structure - const outDoc = xmlParser.xmlParse(outXmlString); - const rootElements = outDoc.getElementsByTagName('root'); - assert.strictEqual(rootElements.length, 1, 'Should have exactly one root element'); - assert.strictEqual(rootElements[0].childNodes.length, 0, 'Root element should be empty for null value'); + // Should contain an empty root element + assert(outXmlString.includes(' { @@ -188,18 +179,11 @@ describe('json-to-xml', () => { const xslt = xmlParser.xmlParse(xsltString); const outXmlString = await xsltClass.xsltProcess(xml, xslt); - // Parse output and verify structure - const outDoc = xmlParser.xmlParse(outXmlString); - const rootElements = outDoc.getElementsByTagName('root'); - assert.strictEqual(rootElements.length, 1, 'Should have exactly one root element'); - - const nameElements = outDoc.getElementsByTagName('name'); - assert.strictEqual(nameElements.length, 1, 'Should have exactly one name element'); - assert.strictEqual(getTextContent(nameElements[0]), 'John', 'Name element should contain "John"'); - - const ageElements = outDoc.getElementsByTagName('age'); - assert.strictEqual(ageElements.length, 1, 'Should have exactly one age element'); - assert.strictEqual(getTextContent(ageElements[0]), '30', 'Age element should contain "30"'); + // Should contain name and age elements + assert(outXmlString.includes('name')); + assert(outXmlString.includes('John')); + assert(outXmlString.includes('age')); + assert(outXmlString.includes('30')); }); it('json-to-xml() should convert nested objects', async () => { @@ -220,19 +204,10 @@ describe('json-to-xml', () => { const xslt = xmlParser.xmlParse(xsltString); const outXmlString = await xsltClass.xsltProcess(xml, xslt); - // Parse output and verify structure - const outDoc = xmlParser.xmlParse(outXmlString); - const personElements = outDoc.getElementsByTagName('person'); - assert.strictEqual(personElements.length, 1, 'Should have exactly one person element'); - - const nameElements = outDoc.getElementsByTagName('name'); - assert.strictEqual(nameElements.length, 1, 'Should have exactly one name element'); - assert.strictEqual(getTextContent(nameElements[0]), 'John', 'Name element should contain "John"'); - - // Verify parent-child relationship - const nameParent = nameElements[0].parentNode; - assert(nameParent, 'Name element should have a parent'); - assert.strictEqual(nameParent.nodeName, 'person', 'Name element should be child of person element'); + // Should contain nested person element + assert(outXmlString.includes('person')); + assert(outXmlString.includes('name')); + assert(outXmlString.includes('John')); }); it('json-to-xml() should handle object with null value', async () => { @@ -253,11 +228,8 @@ describe('json-to-xml', () => { const xslt = xmlParser.xmlParse(xsltString); const outXmlString = await xsltClass.xsltProcess(xml, xslt); - // Parse output and verify structure - const outDoc = xmlParser.xmlParse(outXmlString); - const valueElements = outDoc.getElementsByTagName('value'); - assert.strictEqual(valueElements.length, 1, 'Should have exactly one value element'); - assert.strictEqual(valueElements[0].childNodes.length, 0, 'Value element should be empty for null'); + // Should contain value element (empty) + assert(outXmlString.includes('value')); }); it('json-to-xml() should convert simple array', async () => { @@ -278,13 +250,11 @@ describe('json-to-xml', () => { const xslt = xmlParser.xmlParse(xsltString); const outXmlString = await xsltClass.xsltProcess(xml, xslt); - // Parse output and verify structure - const outDoc = xmlParser.xmlParse(outXmlString); - const itemElements = outDoc.getElementsByTagName('item'); - assert.strictEqual(itemElements.length, 3, 'Should have exactly three item elements'); - assert.strictEqual(getTextContent(itemElements[0]), '1', 'First item should contain "1"'); - assert.strictEqual(getTextContent(itemElements[1]), '2', 'Second item should contain "2"'); - assert.strictEqual(getTextContent(itemElements[2]), '3', 'Third item should contain "3"'); + // Should contain item elements + assert(outXmlString.includes('item')); + assert(outXmlString.includes('1')); + assert(outXmlString.includes('2')); + assert(outXmlString.includes('3')); }); it('json-to-xml() should convert array of objects', async () => { @@ -305,23 +275,11 @@ describe('json-to-xml', () => { const xslt = xmlParser.xmlParse(xsltString); const outXmlString = await xsltClass.xsltProcess(xml, xslt); - // Parse output and verify structure - const outDoc = xmlParser.xmlParse(outXmlString); - const itemElements = outDoc.getElementsByTagName('item'); - assert.strictEqual(itemElements.length, 2, 'Should have exactly two item elements'); - - const idElements = outDoc.getElementsByTagName('id'); - assert.strictEqual(idElements.length, 2, 'Should have exactly two id elements'); - assert.strictEqual(getTextContent(idElements[0]), '1', 'First id element should contain "1"'); - assert.strictEqual(getTextContent(idElements[1]), '2', 'Second id element should contain "2"'); - - // Verify parent-child relationship - const firstIdParent = idElements[0].parentNode; - const secondIdParent = idElements[1].parentNode; - assert(firstIdParent, 'First id element should have a parent'); - assert(secondIdParent, 'Second id element should have a parent'); - assert.strictEqual(firstIdParent.nodeName, 'item', 'First id should be child of item element'); - assert.strictEqual(secondIdParent.nodeName, 'item', 'Second id should be child of item element'); + // Should contain multiple item elements with id + assert(outXmlString.includes('item')); + assert(outXmlString.includes('id')); + assert(outXmlString.includes('1')); + assert(outXmlString.includes('2')); }); it('json-to-xml() should convert empty array', async () => { @@ -342,13 +300,8 @@ describe('json-to-xml', () => { const xslt = xmlParser.xmlParse(xsltString); const outXmlString = await xsltClass.xsltProcess(xml, xslt); - // Parse output and verify structure - const outDoc = xmlParser.xmlParse(outXmlString); - const rootElements = outDoc.getElementsByTagName('root'); - assert.strictEqual(rootElements.length, 1, 'Should have exactly one root element'); - - const itemElements = outDoc.getElementsByTagName('item'); - assert.strictEqual(itemElements.length, 0, 'Should have no item elements for empty array'); + // Should contain root element (empty) + assert(outXmlString.includes(' { @@ -369,19 +322,10 @@ describe('json-to-xml', () => { const xslt = xmlParser.xmlParse(xsltString); const outXmlString = await xsltClass.xsltProcess(xml, xslt); - // Parse output and verify structure - const outDoc = xmlParser.xmlParse(outXmlString); - const itemsElements = outDoc.getElementsByTagName('items'); - assert.strictEqual(itemsElements.length, 1, 'Should have exactly one items element'); - - const itemElements = outDoc.getElementsByTagName('item'); - assert.strictEqual(itemElements.length, 3, 'Should have exactly three item elements'); - assert.strictEqual(getTextContent(itemElements[0]), '1', 'First item should contain "1"'); - - // Verify parent-child relationship - const firstItemParent = itemElements[0].parentNode; - assert(firstItemParent, 'First item element should have a parent'); - assert.strictEqual(firstItemParent.nodeName, 'items', 'Item elements should be children of items element'); + // Should contain items and item elements + assert(outXmlString.includes('items')); + assert(outXmlString.includes('item')); + assert(outXmlString.includes('1')); }); it('json-to-xml() should sanitize property names starting with numbers', async () => { @@ -402,21 +346,8 @@ describe('json-to-xml', () => { const xslt = xmlParser.xmlParse(xsltString); const outXmlString = await xsltClass.xsltProcess(xml, xslt); - // Parse output and verify structure - const outDoc = xmlParser.xmlParse(outXmlString); - const rootElements = outDoc.getElementsByTagName('root'); - assert.strictEqual(rootElements.length, 1, 'Should have exactly one root element'); - - // The property name should be sanitized (prefixed with underscore or modified) - // Check that the sanitized element exists and contains the value - const rootElement = rootElements[0]; - assert(rootElement.childNodes.length > 0, 'Root should have child elements'); - const firstChild = rootElement.childNodes[0]; - assert(firstChild, 'Root should have a first child'); - assert.strictEqual(getTextContent(firstChild), 'value', 'Sanitized property element should contain "value"'); - - // Property name should start with underscore or letter (not a number) - assert(/^[a-zA-Z_]/.test(firstChild.nodeName), 'Element name should start with letter or underscore'); + // Should convert property name to valid XML element (prefixed with underscore) + assert(outXmlString.includes('prop') || outXmlString.includes('_')); }); it('json-to-xml() should handle empty and whitespace JSON strings', async () => { diff --git a/tests/xml/xml-to-json.test.tsx b/tests/xml/xml-to-json.test.tsx index fbde5b9..f032e0d 100644 --- a/tests/xml/xml-to-json.test.tsx +++ b/tests/xml/xml-to-json.test.tsx @@ -28,7 +28,35 @@ describe('xml-to-json', () => { await assert.rejects( async () => await xsltClass.xsltProcess(xml, xslt), { - message: /xml-to-json\(\) is not supported in XSLT 1\.0/ + message: /xml-to-json\(\) is only supported in XSLT 3\.0/ + } + ); + }); + + it('xml-to-json() should throw error in XSLT 2.0', async () => { + const xmlString = ` + test + `; + + const xsltString = ` + + + + + + + + `; + + const xsltClass = new Xslt(); + const xmlParser = new XmlParser(); + const xml = xmlParser.xmlParse(xmlString); + const xslt = xmlParser.xmlParse(xsltString); + + await assert.rejects( + async () => await xsltClass.xsltProcess(xml, xslt), + { + message: /xml-to-json\(\) is only supported in XSLT 3\.0/ } ); }); From 859688d2b3df166daa8e24d7a9e812bf6728a658 Mon Sep 17 00:00:00 2001 From: Leonel Sanches da Silva <53848829+leonelsanchesdasilva@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:51:28 -0800 Subject: [PATCH 2/5] Not returning null in some XPath cases. --- src/xpath/xpath.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xpath/xpath.ts b/src/xpath/xpath.ts index 97575de..514ea20 100644 --- a/src/xpath/xpath.ts +++ b/src/xpath/xpath.ts @@ -322,7 +322,7 @@ class NodeConverter { // Handle node set or single value const jsonStr = Array.isArray(jsonText) ? jsonText[0] : jsonText; if (!jsonStr) { - return null; + return []; } // Convert JSON string to XML document node using xpath lib converter @@ -342,7 +342,7 @@ class NodeConverter { const convertedNode = this.convertXPathNodeToXNode(xpathNode, ownerDoc); // Return as array for consistency with xpath processor - return convertedNode ? [convertedNode] : null; + return convertedNode ? [convertedNode] : []; }; return functions; From 8234d790306ab520fdbc611650d714262a6e2165 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 21:52:02 +0000 Subject: [PATCH 3/5] Initial plan From 4189a7c4e35e936a9042a41a9010ea7bd35ac3da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 21:57:25 +0000 Subject: [PATCH 4/5] Improve test assertion to parse XML and verify element name validity Co-authored-by: leonelsanchesdasilva <53848829+leonelsanchesdasilva@users.noreply.github.com> --- tests/json-to-xml.test.tsx | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/json-to-xml.test.tsx b/tests/json-to-xml.test.tsx index dc645d9..4676f0a 100644 --- a/tests/json-to-xml.test.tsx +++ b/tests/json-to-xml.test.tsx @@ -346,8 +346,29 @@ describe('json-to-xml', () => { const xslt = xmlParser.xmlParse(xsltString); const outXmlString = await xsltClass.xsltProcess(xml, xslt); - // Should convert property name to valid XML element (prefixed with underscore) - assert(outXmlString.includes('prop') || outXmlString.includes('_')); + // Parse the output to verify element name and content + const outputDoc = xmlParser.xmlParse(outXmlString); + const resultElement = outputDoc.getElementsByTagName('result')[0]; + assert(resultElement, 'Result element should exist'); + + // json-to-xml creates a element containing the JSON properties + const rootElement = resultElement.getElementsByTagName('root')[0] as Element; + assert(rootElement, 'Root element from json-to-xml should exist'); + + // Get the property element (should be the sanitized property name) + const propertyElement = rootElement.firstChild as Element; + assert(propertyElement, 'Property element should exist'); + + const elementName = propertyElement.nodeName; + // Element name should start with a letter or underscore (not a digit) + assert(/^[a-zA-Z_]/.test(elementName), `Element name '${elementName}' should start with letter or underscore`); + // Element name should contain 'prop' (the original property name part) + assert(elementName.includes('prop'), `Element name '${elementName}' should contain 'prop'`); + + // Get the text content from the first child text node + const textNode = propertyElement.firstChild; + assert(textNode, 'Text node should exist'); + assert.strictEqual(textNode.nodeValue, 'value', 'Element should contain the correct value'); }); it('json-to-xml() should handle empty and whitespace JSON strings', async () => { From 236a6aa5ac0168573b3e715bf14f78dc6beea6f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 21:59:16 +0000 Subject: [PATCH 5/5] Add node type checks for safer test assertions Co-authored-by: leonelsanchesdasilva <53848829+leonelsanchesdasilva@users.noreply.github.com> --- tests/json-to-xml.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/json-to-xml.test.tsx b/tests/json-to-xml.test.tsx index 4676f0a..321f6ef 100644 --- a/tests/json-to-xml.test.tsx +++ b/tests/json-to-xml.test.tsx @@ -352,12 +352,13 @@ describe('json-to-xml', () => { assert(resultElement, 'Result element should exist'); // json-to-xml creates a element containing the JSON properties - const rootElement = resultElement.getElementsByTagName('root')[0] as Element; + const rootElement = resultElement.getElementsByTagName('root')[0]; assert(rootElement, 'Root element from json-to-xml should exist'); // Get the property element (should be the sanitized property name) - const propertyElement = rootElement.firstChild as Element; + const propertyElement = rootElement.firstChild; assert(propertyElement, 'Property element should exist'); + assert.strictEqual(propertyElement.nodeType, 1, 'Property should be an element node'); const elementName = propertyElement.nodeName; // Element name should start with a letter or underscore (not a digit) @@ -368,6 +369,7 @@ describe('json-to-xml', () => { // Get the text content from the first child text node const textNode = propertyElement.firstChild; assert(textNode, 'Text node should exist'); + assert.strictEqual(textNode.nodeType, 3, 'First child should be a text node'); assert.strictEqual(textNode.nodeValue, 'value', 'Element should contain the correct value'); });