diff --git a/src/domFacade/ConcreteNode.ts b/src/domFacade/ConcreteNode.ts index 7ed111748..8b434820e 100644 --- a/src/domFacade/ConcreteNode.ts +++ b/src/domFacade/ConcreteNode.ts @@ -34,7 +34,7 @@ export type ConcreteDocumentFragmentNode = DocumentFragment & { export type ConcreteProcessingInstructionNode = ProcessingInstruction & { nodeType: NODE_TYPES.PROCESSING_INSTRUCTION_NODE; }; -export type ConcreteParentNode = ConcreteElementNode | ConcreteDocumentNode; +export type ConcreteParentNode = ConcreteElementNode | ConcreteDocumentNode | ConcreteDocumentFragmentNode; export type ConcreteChildNode = | ConcreteElementNode | ConcreteTextNode diff --git a/src/domFacade/DomFacade.ts b/src/domFacade/DomFacade.ts index 312fb1a4a..bad41a659 100644 --- a/src/domFacade/DomFacade.ts +++ b/src/domFacade/DomFacade.ts @@ -112,7 +112,7 @@ class DomFacade { ? parentNode.childNodes : this._domFacade['getChildNodes'](parentNode, bucket); - if (parentNode.nodeType === NODE_TYPES.DOCUMENT_NODE) { + if (parentNode.nodeType === NODE_TYPES.DOCUMENT_NODE || parentNode.nodeType === NODE_TYPES.DOCUMENT_FRAGMENT_NODE) { return childNodes.filter( (childNode) => childNode['nodeType'] !== NODE_TYPES.DOCUMENT_TYPE_NODE, ); diff --git a/src/evaluationUtils/buildEvaluationContext.ts b/src/evaluationUtils/buildEvaluationContext.ts index 19d63d12d..c17c02a3b 100644 --- a/src/evaluationUtils/buildEvaluationContext.ts +++ b/src/evaluationUtils/buildEvaluationContext.ts @@ -42,6 +42,26 @@ export function createDefaultNamespaceResolver(contextItem: any): (s: string) => if (!contextItem || typeof contextItem !== 'object' || !('lookupNamespaceURI' in contextItem)) { return (_prefix) => null; } + + // For DOCUMENT_FRAGMENT_NODE (e.g., ShadowRoot), use special handling + // ShadowRoot may have lookupNamespaceURI but it returns null for the default namespace + // Instead, we should use the host element or first element child for namespace resolution + if (contextItem.nodeType === 11) { // DOCUMENT_FRAGMENT_NODE + // For ShadowRoot, try to use the host element + if ('host' in contextItem && contextItem.host && 'lookupNamespaceURI' in contextItem.host) { + return (prefix) => contextItem.host['lookupNamespaceURI'](prefix || null); + } + + // Otherwise, try the first element child + const firstElementChild = contextItem.firstElementChild; + if (firstElementChild && 'lookupNamespaceURI' in firstElementChild) { + return (prefix) => firstElementChild['lookupNamespaceURI'](prefix || null); + } + + // Last resort: return null + return (_prefix) => null; + } + return (prefix) => contextItem['lookupNamespaceURI'](prefix || null); } diff --git a/src/expressions/axes/ChildAxis.ts b/src/expressions/axes/ChildAxis.ts index a97515a62..c7a2bd3e9 100644 --- a/src/expressions/axes/ChildAxis.ts +++ b/src/expressions/axes/ChildAxis.ts @@ -30,7 +30,11 @@ class ChildAxis extends Expression { const domFacade = executionParameters.domFacade; const contextNode = validateContextNode(dynamicContext.contextItem); const nodeType = domFacade.getNodeType(contextNode); - if (nodeType !== NODE_TYPES.ELEMENT_NODE && nodeType !== NODE_TYPES.DOCUMENT_NODE) { + if ( + nodeType !== NODE_TYPES.ELEMENT_NODE && + nodeType !== NODE_TYPES.DOCUMENT_NODE && + nodeType !== NODE_TYPES.DOCUMENT_FRAGMENT_NODE + ) { return sequenceFactory.empty(); } diff --git a/src/expressions/axes/FollowingAxis.ts b/src/expressions/axes/FollowingAxis.ts index f331d4fee..a7efc8de5 100644 --- a/src/expressions/axes/FollowingAxis.ts +++ b/src/expressions/axes/FollowingAxis.ts @@ -23,7 +23,9 @@ function createFollowingGenerator( for ( let ancestorNode = node as ParentNodePointer; - ancestorNode && domFacade.getNodeType(ancestorNode) !== NODE_TYPES.DOCUMENT_NODE; + ancestorNode && + domFacade.getNodeType(ancestorNode) !== NODE_TYPES.DOCUMENT_NODE && + domFacade.getNodeType(ancestorNode) !== NODE_TYPES.DOCUMENT_FRAGMENT_NODE; // Any parent can contain the node we want ancestorNode = domFacade.getParentNodePointer(ancestorNode as ChildNodePointer, null) ) { diff --git a/src/expressions/axes/PrecedingAxis.ts b/src/expressions/axes/PrecedingAxis.ts index edb71ecbf..3c6c2fa3a 100644 --- a/src/expressions/axes/PrecedingAxis.ts +++ b/src/expressions/axes/PrecedingAxis.ts @@ -23,7 +23,9 @@ function createPrecedingGenerator( for ( let ancestorNode = node; - ancestorNode && domFacade.getNodeType(ancestorNode) !== NODE_TYPES.DOCUMENT_NODE; + ancestorNode && + domFacade.getNodeType(ancestorNode) !== NODE_TYPES.DOCUMENT_NODE && + domFacade.getNodeType(ancestorNode) !== NODE_TYPES.DOCUMENT_FRAGMENT_NODE; // Any parent can contain the node we want. documents AND elements ancestorNode = domFacade.getParentNodePointer(ancestorNode, null) as ChildNodePointer ) { diff --git a/src/expressions/functions/builtInFunctions_identifiers.ts b/src/expressions/functions/builtInFunctions_identifiers.ts index d0cf94659..40d22900f 100644 --- a/src/expressions/functions/builtInFunctions_identifiers.ts +++ b/src/expressions/functions/builtInFunctions_identifiers.ts @@ -17,7 +17,8 @@ function findDescendants( ): Node[] { if ( node.node.nodeType !== NODE_TYPES.ELEMENT_NODE && - node.node.nodeType !== NODE_TYPES.DOCUMENT_NODE + node.node.nodeType !== NODE_TYPES.DOCUMENT_NODE && + node.node.nodeType !== NODE_TYPES.DOCUMENT_FRAGMENT_NODE ) { return []; } @@ -65,7 +66,10 @@ const fnId: FunctionDefinitionType = ( }, Object.create(null)); let documentNode = targetNodeValue.value; - while (domFacade.getNodeType(documentNode) !== NODE_TYPES.DOCUMENT_NODE) { + while ( + domFacade.getNodeType(documentNode) !== NODE_TYPES.DOCUMENT_NODE && + domFacade.getNodeType(documentNode) !== NODE_TYPES.DOCUMENT_FRAGMENT_NODE + ) { documentNode = domFacade.getParentNodePointer(documentNode); if (documentNode === null) { throw new Error('FODC0001: the root node of the target node is not a document node.'); @@ -118,7 +122,10 @@ const fnIdref: FunctionDefinitionType = ( }, Object.create(null)); let documentNode = targetNodeValue.value; - while (domFacade.getNodeType(documentNode) !== NODE_TYPES.DOCUMENT_NODE) { + while ( + domFacade.getNodeType(documentNode) !== NODE_TYPES.DOCUMENT_NODE && + domFacade.getNodeType(documentNode) !== NODE_TYPES.DOCUMENT_FRAGMENT_NODE + ) { documentNode = domFacade.getParentNodePointer(documentNode); if (documentNode === null) { throw new Error('FODC0001: the root node of the context node is not a document node.'); diff --git a/src/expressions/functions/builtInFunctions_node.ts b/src/expressions/functions/builtInFunctions_node.ts index 8f4be779e..dd7bb9947 100644 --- a/src/expressions/functions/builtInFunctions_node.ts +++ b/src/expressions/functions/builtInFunctions_node.ts @@ -310,7 +310,10 @@ const fnPath: FunctionDefinitionType = ( } } } - if (domFacade.getNodeType(ancestor) === NODE_TYPES.DOCUMENT_NODE) { + if ( + domFacade.getNodeType(ancestor) === NODE_TYPES.DOCUMENT_NODE || + domFacade.getNodeType(ancestor) === NODE_TYPES.DOCUMENT_FRAGMENT_NODE + ) { return sequenceFactory.create(createAtomicValue(result || '/', ValueType.XSSTRING)); } result = 'Q{http://www.w3.org/2005/xpath-functions}root()' + result; diff --git a/src/expressions/path/AbsolutePathExpression.ts b/src/expressions/path/AbsolutePathExpression.ts index 33ea38b0e..f8cbaa6e1 100644 --- a/src/expressions/path/AbsolutePathExpression.ts +++ b/src/expressions/path/AbsolutePathExpression.ts @@ -32,7 +32,10 @@ class AbsolutePathExpression extends Expression { const domFacade = executionParameters.domFacade; let documentNode = node; - while (domFacade.getNodeType(documentNode) !== NODE_TYPES.DOCUMENT_NODE) { + while ( + domFacade.getNodeType(documentNode) !== NODE_TYPES.DOCUMENT_NODE && + domFacade.getNodeType(documentNode) !== NODE_TYPES.DOCUMENT_FRAGMENT_NODE + ) { documentNode = domFacade.getParentNodePointer(documentNode); if (documentNode === null) { throw new Error( diff --git a/src/expressions/util/createChildGenerator.ts b/src/expressions/util/createChildGenerator.ts index ac743c6dd..1209634d5 100644 --- a/src/expressions/util/createChildGenerator.ts +++ b/src/expressions/util/createChildGenerator.ts @@ -10,7 +10,11 @@ export default function createChildGenerator( bucket: Bucket | null, ): IIterator { const nodeType = domFacade.getNodeType(pointer); - if (nodeType !== NODE_TYPES.ELEMENT_NODE && nodeType !== NODE_TYPES.DOCUMENT_NODE) { + if ( + nodeType !== NODE_TYPES.ELEMENT_NODE && + nodeType !== NODE_TYPES.DOCUMENT_NODE && + nodeType !== NODE_TYPES.DOCUMENT_FRAGMENT_NODE + ) { return { next: () => { return DONE_TOKEN; diff --git a/src/expressions/util/createDescendantGenerator.ts b/src/expressions/util/createDescendantGenerator.ts index 3a9b43eaf..5b1e0ec81 100644 --- a/src/expressions/util/createDescendantGenerator.ts +++ b/src/expressions/util/createDescendantGenerator.ts @@ -13,7 +13,11 @@ function findDeepestLastDescendant( bucket: Bucket | null, ): NodePointer { const nodeType = domFacade.getNodeType(pointer); - if (nodeType !== NODE_TYPES.ELEMENT_NODE && nodeType !== NODE_TYPES.DOCUMENT_NODE) { + if ( + nodeType !== NODE_TYPES.ELEMENT_NODE && + nodeType !== NODE_TYPES.DOCUMENT_NODE && + nodeType !== NODE_TYPES.DOCUMENT_FRAGMENT_NODE + ) { return pointer; } @@ -55,7 +59,9 @@ export default function createDescendantGenerator( const nodeType = domFacade.getNodeType(currentPointer); const previousSibling = - nodeType === NODE_TYPES.DOCUMENT_NODE || nodeType === NODE_TYPES.ATTRIBUTE_NODE + nodeType === NODE_TYPES.DOCUMENT_NODE || + nodeType === NODE_TYPES.DOCUMENT_FRAGMENT_NODE || + nodeType === NODE_TYPES.ATTRIBUTE_NODE ? null : domFacade.getPreviousSiblingPointer( currentPointer as ChildNodePointer, @@ -67,7 +73,8 @@ export default function createDescendantGenerator( } currentPointer = - nodeType === NODE_TYPES.DOCUMENT_NODE + nodeType === NODE_TYPES.DOCUMENT_NODE || + nodeType === NODE_TYPES.DOCUMENT_FRAGMENT_NODE ? null : domFacade.getParentNodePointer( currentPointer as ChildNodePointer, diff --git a/src/jsCodegen/emitPathExpr.ts b/src/jsCodegen/emitPathExpr.ts index 0f8f9d6e2..a7028e40f 100644 --- a/src/jsCodegen/emitPathExpr.ts +++ b/src/jsCodegen/emitPathExpr.ts @@ -278,7 +278,7 @@ function emitRootExpr( acceptAst( `(function () { let n = ${contextItemExpr.code}; - while (n.nodeType !== /*DOCUMENT_NODE*/${NODE_TYPES.DOCUMENT_NODE}) { + while (n.nodeType !== /*DOCUMENT_NODE*/${NODE_TYPES.DOCUMENT_NODE} && n.nodeType !== /*DOCUMENT_FRAGMENT_NODE*/${NODE_TYPES.DOCUMENT_FRAGMENT_NODE}) { n = domFacade.getParentNode(n); if (n === null) { throw new Error('XPDY0050: the root node of the context node is not a document node.'); diff --git a/test/specs/parsing/axes/FollowingSiblingAxis.tests.ts b/test/specs/parsing/axes/FollowingSiblingAxis.tests.ts index 5b34f5d74..a5855b6d9 100644 --- a/test/specs/parsing/axes/FollowingSiblingAxis.tests.ts +++ b/test/specs/parsing/axes/FollowingSiblingAxis.tests.ts @@ -38,6 +38,16 @@ describe('following-sibling', () => { ); }); + it('supports document fragments with multiple children', () => { + const fragment = documentNode.createDocumentFragment(); + const first = fragment.appendChild(documentNode.createElement('first')); + const second = fragment.appendChild(documentNode.createElement('second')); + + chai.assert.deepEqual(evaluateXPathToNodes('following-sibling::element()', first), [ + second, + ]); + }); + it('passes buckets for followingSibling', () => { jsonMlMapper.parse( ['parentElement', ['firstChildElement'], ['secondChildElement']], diff --git a/test/specs/parsing/axes/PrecedingSibling.tests.ts b/test/specs/parsing/axes/PrecedingSibling.tests.ts index 230f39e81..2b3adaf7a 100644 --- a/test/specs/parsing/axes/PrecedingSibling.tests.ts +++ b/test/specs/parsing/axes/PrecedingSibling.tests.ts @@ -38,6 +38,16 @@ describe('preceding-sibling', () => { ); }); + it('supports document fragments with multiple children', () => { + const fragment = documentNode.createDocumentFragment(); + const first = fragment.appendChild(documentNode.createElement('first')); + const second = fragment.appendChild(documentNode.createElement('second')); + + chai.assert.deepEqual(evaluateXPathToNodes('preceding-sibling::element()', second), [ + first, + ]); + }); + it('passes buckets for preceding-sibling', () => { jsonMlMapper.parse( ['parentElement', ['firstChildElement'], ['secondChildElement']], diff --git a/test/specs/parsing/evaluateXPath.tests.ts b/test/specs/parsing/evaluateXPath.tests.ts index 0d7856b36..b585a93cb 100644 --- a/test/specs/parsing/evaluateXPath.tests.ts +++ b/test/specs/parsing/evaluateXPath.tests.ts @@ -26,6 +26,67 @@ describe('evaluateXPath', () => { documentNode = new slimdom.Document(); }); + describe('document fragments as roots', () => { + it('supports descendant queries on document fragments', () => { + const fragment = documentNode.createDocumentFragment(); + const first = fragment.appendChild(documentNode.createElement('item')); + first.setAttribute('id', 'first'); + fragment.appendChild(documentNode.createElement('item')); + + chai.assert.deepEqual( + evaluateXPathToNodes('//item', fragment, domFacade), + [fragment.firstChild, fragment.lastChild], + ); + }); + + it('supports predicates on document fragments', () => { + const fragment = documentNode.createDocumentFragment(); + const first = fragment.appendChild(documentNode.createElement('item')); + first.setAttribute('id', 'first'); + fragment.appendChild(documentNode.createElement('item')); + + chai.assert.deepEqual( + evaluateXPathToNodes('//item[@id="first"]', fragment, domFacade), + [first], + ); + }); + + it('supports more complex expressions on document fragments', () => { + const fragment = documentNode.createDocumentFragment(); + const first = fragment.appendChild(documentNode.createElement('item')); + first.setAttribute('id', 'first'); + const second = fragment.appendChild(documentNode.createElement('item')); + second.setAttribute('id', 'second'); + + chai.assert.equal( + evaluateXPathToString('string-join(//item/@id, ",")', fragment, domFacade), + 'first,second', + ); + }); + + it('supports functions on document fragments', () => { + const fragment = documentNode.createDocumentFragment(); + const child = fragment.appendChild(documentNode.createElement('item')); + + chai.assert.isTrue( + evaluateXPathToBoolean('root(.) is .', fragment, domFacade), + 'root(.) should be the fragment', + ); + chai.assert.equal( + evaluateXPathToString('name(//item)', fragment, domFacade), + 'item', + ); + chai.assert.isTrue( + evaluateXPathToBoolean('has-children(.)', fragment, domFacade), + 'has-children() should work on fragments', + ); + chai.assert.equal( + evaluateXPathToFirstNode('root(.)', child, domFacade), + fragment, + ); + }); + }); + describe('ANY_TYPE', () => { it('Keeps booleans booleans', () => chai.assert.equal(evaluateXPath('true()', documentNode, domFacade), true)); @@ -303,6 +364,17 @@ describe('evaluateXPath', () => { documentNode, ])); + it('supports document fragments with multiple children', () => { + const fragment = documentNode.createDocumentFragment(); + const first = fragment.appendChild(documentNode.createElement('first')); + const second = fragment.appendChild(documentNode.createElement('second')); + + chai.assert.deepEqual( + evaluateXPathToNodes('following-sibling::element()', first, domFacade), + [second], + ); + }); + it('Returns all nodes', () => chai.assert.deepEqual(evaluateXPathToNodes('(., ., .)', documentNode, domFacade), [ documentNode,