Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/domFacade/ConcreteNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/domFacade/DomFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand Down
20 changes: 20 additions & 0 deletions src/evaluationUtils/buildEvaluationContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
6 changes: 5 additions & 1 deletion src/expressions/axes/ChildAxis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
4 changes: 3 additions & 1 deletion src/expressions/axes/FollowingAxis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
) {
Expand Down
4 changes: 3 additions & 1 deletion src/expressions/axes/PrecedingAxis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
Expand Down
13 changes: 10 additions & 3 deletions src/expressions/functions/builtInFunctions_identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 [];
}
Expand Down Expand Up @@ -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.');
Expand Down Expand Up @@ -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.');
Expand Down
5 changes: 4 additions & 1 deletion src/expressions/functions/builtInFunctions_node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion src/expressions/path/AbsolutePathExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 5 additions & 1 deletion src/expressions/util/createChildGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ export default function createChildGenerator(
bucket: Bucket | null,
): IIterator<ChildNodePointer> {
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;
Expand Down
13 changes: 10 additions & 3 deletions src/expressions/util/createDescendantGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/jsCodegen/emitPathExpr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down
10 changes: 10 additions & 0 deletions test/specs/parsing/axes/FollowingSiblingAxis.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']],
Expand Down
10 changes: 10 additions & 0 deletions test/specs/parsing/axes/PrecedingSibling.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']],
Expand Down
72 changes: 72 additions & 0 deletions test/specs/parsing/evaluateXPath.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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,
Expand Down