inScope = context.getInScopeNamespaces();
+ if ((inherited != null && !inherited.isEmpty()) || (inScope != null && !inScope.isEmpty())) {
+ ancestorNS = new HashMap<>();
+ if (inherited != null) { ancestorNS.putAll(inherited); }
+ if (inScope != null) { ancestorNS.putAll(inScope); }
+ }
+ }
+
// create the output
final MemTreeBuilder builder = context.getDocumentBuilder();
final DocumentBuilderReceiver receiver = new DocumentBuilderReceiver(this, builder);
@@ -130,7 +167,25 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc
}
try {
receiver.setCheckNS(false);
- next.copyTo(context.getBroker(), receiver);
+ // When copy-namespaces no-inherit is active, pre-existing element nodes
+ // (i.e. nodes from variable references, not direct constructors) must have
+ // namespace undeclarations injected so that ancestor-traversal in
+ // in-scope-prefixes() is neutralized for inherited namespace bindings.
+ if (noInherit && ancestorNS != null
+ && next.getType() == Type.ELEMENT
+ && next instanceof NodeImpl) {
+ final NodeImpl nodeImpl = (NodeImpl) next;
+ final boolean isPreExisting = (innerBuilder == null)
+ || (nodeImpl.getOwnerDocument() != innerBuilder.getDocument());
+ if (isPreExisting) {
+ next.copyTo(context.getBroker(),
+ new NoInheritCopyReceiver(this, builder, ancestorNS));
+ } else {
+ next.copyTo(context.getBroker(), receiver);
+ }
+ } else {
+ next.copyTo(context.getBroker(), receiver);
+ }
receiver.setCheckNS(true);
} catch (DOMException e) {
if (e.code == DOMException.NAMESPACE_ERR) {
@@ -194,4 +249,104 @@ public Expression simplify() {
public boolean evalNextExpressionOnEmptyContextSequence() {
return true;
}
+
+ /**
+ * A {@link DocumentBuilderReceiver} that injects namespace undeclarations onto the
+ * root element of a copied pre-existing node when {@code declare copy-namespaces no-inherit}
+ * is active. The undeclarations neutralize ancestor namespace bindings so that
+ * {@code fn:in-scope-prefixes()} traversing the ancestor chain returns the correct result.
+ *
+ * Only prefixes present in {@code ancestorNS} but absent from the root element's own
+ * namespace declarations are undeclared (i.e. recorded as {@code xmlns:prefix=""}).
+ */
+ private static final class NoInheritCopyReceiver extends DocumentBuilderReceiver {
+
+ private final Map ancestorNS;
+ /** True once the root element's startElement event has been seen. */
+ private boolean rootSeen = false;
+ /** True once undeclarations have been flushed (happens before first non-namespace event). */
+ private boolean undeclsFlushed = false;
+ /** Prefixes that the root element itself declares (element prefix + xmlns:* nodes). */
+ private final Set rootOwnPrefixes = new HashSet<>();
+
+ NoInheritCopyReceiver(final Expression expr, final MemTreeBuilder builder,
+ final Map ancestorNS) {
+ super(expr, builder);
+ this.ancestorNS = ancestorNS;
+ }
+
+ @Override
+ public void startElement(final QName qname, final AttrList attribs) {
+ if (!rootSeen) {
+ rootSeen = true;
+ // Track the element's own namespace prefix so we don't undeclare it.
+ final String prefix = qname.getPrefix();
+ if (prefix != null && !prefix.isEmpty()) {
+ rootOwnPrefixes.add(prefix);
+ }
+ // Note: namespace declaration nodes for this element come via addNamespaceNode
+ // after startElement; they are collected in rootOwnPrefixes there.
+ } else {
+ maybeFlushUndeclarations();
+ }
+ super.startElement(qname, attribs);
+ }
+
+ @Override
+ public void addNamespaceNode(final QName qname) throws SAXException {
+ if (rootSeen && !undeclsFlushed) {
+ // Collect the prefix that this namespace node declares on the root element.
+ rootOwnPrefixes.add(qname.getLocalPart());
+ }
+ super.addNamespaceNode(qname);
+ }
+
+ @Override
+ public void characters(final CharSequence seq) throws SAXException {
+ maybeFlushUndeclarations();
+ super.characters(seq);
+ }
+
+ @Override
+ public void characters(final char[] ch, final int start, final int len) throws SAXException {
+ maybeFlushUndeclarations();
+ super.characters(ch, start, len);
+ }
+
+ @Override
+ public void endElement(final String ns, final String local, final String qname) throws SAXException {
+ maybeFlushUndeclarations();
+ super.endElement(ns, local, qname);
+ }
+
+ @Override
+ public void endElement(final QName qname) throws SAXException {
+ maybeFlushUndeclarations();
+ super.endElement(qname);
+ }
+
+ /**
+ * Emit namespace undeclarations for every ancestor namespace binding whose prefix is
+ * not already declared by the root element itself. Called before the first non-namespace
+ * event on the root element (child, text, endElement) to ensure the nodes attach to the
+ * root element node number in the MemTree.
+ */
+ private void maybeFlushUndeclarations() {
+ if (rootSeen && !undeclsFlushed) {
+ undeclsFlushed = true;
+ for (final Map.Entry entry : ancestorNS.entrySet()) {
+ final String prefix = entry.getKey();
+ if (!rootOwnPrefixes.contains(prefix)) {
+ try {
+ super.addNamespaceNode(
+ new QName(prefix, XMLConstants.NULL_NS_URI, XMLConstants.XMLNS_ATTRIBUTE));
+ } catch (final SAXException e) {
+ // Silently skip — undeclaration is best-effort; worst case
+ // in-scope-prefixes() returns a superset which is handled by cleanup.
+ }
+ }
+ }
+ }
+ }
+ }
}
diff --git a/exist-core/src/main/java/org/exist/xquery/ModuleContext.java b/exist-core/src/main/java/org/exist/xquery/ModuleContext.java
index fd63f4f0b6c..da028882b1e 100644
--- a/exist-core/src/main/java/org/exist/xquery/ModuleContext.java
+++ b/exist-core/src/main/java/org/exist/xquery/ModuleContext.java
@@ -357,6 +357,11 @@ public MemTreeBuilder getDocumentBuilder(final boolean explicitCreation) {
return parentContext.getDocumentBuilder(explicitCreation);
}
+ @Override
+ public MemTreeBuilder getCurrentDocumentBuilder() {
+ return parentContext.getCurrentDocumentBuilder();
+ }
+
@Override
public void pushDocumentContext() {
parentContext.pushDocumentContext();
@@ -523,6 +528,11 @@ public String getInheritedNamespace(final String prefix) {
return parentContext.getInheritedNamespace(prefix);
}
+ @Override
+ public Map getAllInheritedNamespaces() {
+ return parentContext.getAllInheritedNamespaces();
+ }
+
@Override
public String getInheritedPrefix(final String uri) {
return parentContext.getInheritedPrefix(uri);
diff --git a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java
index b3721c34179..1b15f7702a9 100644
--- a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java
+++ b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java
@@ -969,6 +969,18 @@ public String getInheritedNamespace(final String prefix) {
return inheritedInScopeNamespaces == null ? null : inheritedInScopeNamespaces.get(prefix);
}
+ public Map getAllInheritedNamespaces() {
+ return inheritedInScopeNamespaces;
+ }
+
+ public Map getInScopeNamespaces() {
+ return inScopeNamespaces;
+ }
+
+ public MemTreeBuilder getCurrentDocumentBuilder() {
+ return documentBuilder;
+ }
+
@Override
public String getInheritedPrefix(final String uri) {
return inheritedInScopePrefixes == null ? null : inheritedInScopePrefixes.get(uri);
diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInScopePrefixes.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInScopePrefixes.java
index a0a248dface..b8d46a82ff8 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInScopePrefixes.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInScopePrefixes.java
@@ -154,25 +154,20 @@ public static Map collectPrefixes(XQueryContext context, NodeVal
Node node = nodeValue.getNode();
if (context.preserveNamespaces()) {
//Horrible hacks to work-around bad in-scope NS : we reconstruct a NS context !
- if (context.inheritNamespaces()) {
- //Grab ancestors' NS
- final Deque stack = new ArrayDeque<>();
- do {
- if (node.getNodeType() == Node.ELEMENT_NODE) {
- stack.add((Element) node);
- }
- node = node.getParentNode();
- } while (node != null && node.getNodeType() == Node.ELEMENT_NODE);
-
- while (!stack.isEmpty()) {
- collectNamespacePrefixes(stack.pop(), prefixes);
- }
-
- } else {
- //Grab self's NS
+ // Always traverse ancestors for in-memory nodes.
+ // When copy-namespaces no-inherit is active, pre-existing nodes (variable copies)
+ // carry explicit namespace undeclarations (xmlns:prefix="") so that ancestor
+ // namespace entries are neutralized by the cleanup pass below.
+ final Deque stack = new ArrayDeque<>();
+ do {
if (node.getNodeType() == Node.ELEMENT_NODE) {
- collectNamespacePrefixes((Element) node, prefixes);
+ stack.add((Element) node);
}
+ node = node.getParentNode();
+ } while (node != null && node.getNodeType() == Node.ELEMENT_NODE);
+
+ while (!stack.isEmpty()) {
+ collectNamespacePrefixes(stack.pop(), prefixes);
}
} else {
if (context.inheritNamespaces()) {
@@ -192,17 +187,16 @@ public static Map collectPrefixes(XQueryContext context, NodeVal
}
}
- //clean up
- String key = null;
- String value = null;
- for (final Entry entry : prefixes.entrySet()) {
- key = entry.getKey();
- value = entry.getValue();
-
- if ((key == null || key.isEmpty()) && (value == null || value.isEmpty())) {
- prefixes.remove(key);
+ // clean up: remove namespace undeclarations (entries with empty URI).
+ // With copy-namespaces no-inherit, pre-existing nodes carry explicit undeclarations
+ // (xmlns:prefix="") that neutralize ancestor namespace bindings; those must not appear
+ // in the in-scope-prefixes() result.
+ final Iterator> cleanupIt = prefixes.entrySet().iterator();
+ while (cleanupIt.hasNext()) {
+ final Entry entry = cleanupIt.next();
+ if (entry.getValue() == null || entry.getValue().isEmpty()) {
+ cleanupIt.remove();
}
-
}
return prefixes;