NamespaceBindings.java
package org.jsoup.internal;
import org.jsoup.nodes.Attribute;
import org.jsoup.nodes.Attributes;
import org.jspecify.annotations.Nullable;
import java.util.ArrayDeque;
import java.util.HashMap;
/** Tracks namespace prefix bindings across nested element scopes. Jsoup internal; API subject to change. */
public final class NamespaceBindings {
private static final String XmlnsKey = "xmlns";
private static final String XmlnsPrefix = "xmlns:";
private static final NamespaceChange ScopeMarker = new NamespaceChange("", null);
private final HashMap<String, String> bindings = new HashMap<>();
private final ArrayDeque<NamespaceChange> changes = new ArrayDeque<>();
/** Creates an empty binding set. */
public NamespaceBindings() {}
/** Clears all bindings and scopes for reuse. */
public void clear() {
bindings.clear();
changes.clear();
}
/** Opens a namespace scope. */
public void pushScope() {
changes.push(ScopeMarker);
}
/** Closes the current scope and restores its parent bindings. */
public void popScope() {
NamespaceChange change;
while ((change = changes.pop()) != ScopeMarker)
change.restore(bindings);
}
/** Binds a prefix in the current scope, or at the root if no scope is open. */
public void put(String prefix, String namespace) {
String previousValue = bindings.get(prefix);
if (namespace.equals(previousValue)) return;
bindings.put(prefix, namespace);
if (!changes.isEmpty())
changes.push(new NamespaceChange(prefix, previousValue));
}
/** Returns the namespace URI bound to a prefix, or null if unbound. */
public @Nullable String get(String prefix) {
return bindings.get(prefix);
}
/** Applies namespace declarations from an attribute set. */
public void applyDeclarations(Attributes attributes) {
for (Attribute attribute : attributes) {
@Nullable String prefix = declarationPrefix(attribute.getKey());
if (prefix != null)
put(prefix, attribute.getValue());
}
}
/** Tests whether a key is an {@code xmlns} declaration. */
public static boolean isDeclaration(String key) {
return declarationPrefix(key) != null;
}
/** Returns the prefix declared by an {@code xmlns} key, or null if it is not a declaration. */
public static @Nullable String declarationPrefix(String key) {
if (key.equals(XmlnsKey)) return "";
if (key.startsWith(XmlnsPrefix)) return key.substring(XmlnsPrefix.length());
return null;
}
/** Returns the number of active bindings. */
int bindingCount() {
return bindings.size();
}
/** Returns the number of recorded scope markers and binding changes. */
int changeCount() {
return changes.size();
}
/** A previous binding recorded for scope restoration. */
private static final class NamespaceChange {
private final String prefix;
private final @Nullable String previousValue;
/** Records a prefix's previous binding. */
private NamespaceChange(String prefix, @Nullable String previousValue) {
this.prefix = prefix;
this.previousValue = previousValue;
}
/** Restores the previous binding. */
private void restore(HashMap<String, String> bindings) {
if (previousValue == null) bindings.remove(prefix);
else bindings.put(prefix, previousValue);
}
}
}