]> git.kaiwu.me - njs.git/commitdiff
XML: add namespace-aware node accessors
authorDmitry Volyntsev <xeioex@nginx.com>
Fri, 25 Sep 2026 01:34:22 +0000 (18:34 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Tue, 29 Sep 2026 20:48:28 +0000 (13:48 -0700)
The existing $attr$name and $tag$name accessors compare local names
only, so x:ID and ID, or x:Issuer and Issuer, are indistinguishable.

The new XMLNode methods getAttributeNS(namespaceURI, localName) and
getChildNS(namespaceURI, localName) match both the namespace URI and
the local name.

external/njs_xml_module.c
external/qjs_xml_module.c
test/ts/test.ts
test/xml/xml.t.mjs
ts/njs_modules/xml.d.ts

index e745a5d6d6349e64dbd900582dab993490e177b0..fce06d907c4d937731b6f8d0e91494c9c4afeceb 100644 (file)
@@ -57,6 +57,12 @@ static njs_int_t njs_xml_attr_ext_prop_handler(njs_vm_t *vm,
     njs_value_t *unused, njs_value_t *retval);
 static njs_int_t njs_xml_node_ext_add_child(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
+static njs_int_t njs_xml_node_ext_get_attribute_ns(njs_vm_t *vm,
+    njs_value_t *args, njs_uint_t nargs, njs_index_t unused,
+    njs_value_t *retval);
+static njs_int_t njs_xml_node_ext_get_child_ns(njs_vm_t *vm,
+    njs_value_t *args, njs_uint_t nargs, njs_index_t unused,
+    njs_value_t *retval);
 static njs_int_t njs_xml_node_ext_attrs(njs_vm_t *vm, njs_object_prop_t *prop,
     uint32_t unused, njs_value_t *value, njs_value_t *setval,
     njs_value_t *retval);
@@ -106,6 +112,10 @@ static njs_int_t njs_xml_node_tags_handler(njs_vm_t *vm, njs_value_t *value,
 static xmlNode *njs_xml_external_node(njs_vm_t *vm, njs_value_t *value);
 static const u_char *njs_xml_string_to_c_string(njs_vm_t *vm, njs_str_t *str,
     u_char *dst, size_t size);
+static xmlNode *njs_xml_ns_args(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_str_t *uri, njs_str_t *local);
+static njs_bool_t njs_xml_ns_match(const xmlChar *name, xmlNs *ns,
+    njs_str_t *uri, njs_str_t *local);
 static njs_bool_t njs_xml_node_is_live(xmlNode *node);
 static xmlNode *njs_xml_list_detach(xmlNode *parent, njs_str_t *name);
 static njs_int_t njs_xml_list_retire(njs_vm_t *vm, xmlNode *parent,
@@ -259,6 +269,28 @@ static njs_external_t  njs_ext_xml_node[] = {
         }
     },
 
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("getAttributeNS"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_xml_node_ext_get_attribute_ns,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("getChildNS"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_xml_node_ext_get_child_ns,
+        }
+    },
+
     {
         .flags = NJS_EXTERN_PROPERTY,
         .name.string = njs_str("$attrs"),
@@ -760,6 +792,71 @@ njs_xml_node_ext_add_child(njs_vm_t *vm, njs_value_t *args,
 }
 
 
+static njs_int_t
+njs_xml_node_ext_get_attribute_ns(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval)
+{
+    xmlAttr    *attr;
+    xmlChar    *text;
+    xmlNode    *current;
+    njs_int_t  ret;
+    njs_str_t  uri, local;
+
+    current = njs_xml_ns_args(vm, args, nargs, &uri, &local);
+    if (njs_slow_path(current == NULL)) {
+        return NJS_ERROR;
+    }
+
+    for (attr = current->properties; attr != NULL; attr = attr->next) {
+        if (!njs_xml_ns_match(attr->name, attr->ns, &uri, &local)) {
+            continue;
+        }
+
+        text = xmlNodeGetContent((xmlNode *) attr);
+        if (njs_slow_path(text == NULL)) {
+            njs_vm_internal_error(vm, "xmlNodeGetContent() failed");
+            return NJS_ERROR;
+        }
+
+        ret = njs_vm_value_string_create(vm, retval, text, njs_strlen(text));
+        xmlFree(text);
+
+        return ret;
+    }
+
+    njs_value_undefined_set(retval);
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_xml_node_ext_get_child_ns(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval)
+{
+    xmlNode    *current, *node;
+    njs_str_t  uri, local;
+
+    current = njs_xml_ns_args(vm, args, nargs, &uri, &local);
+    if (njs_slow_path(current == NULL)) {
+        return NJS_ERROR;
+    }
+
+    for (node = current->children; node != NULL; node = node->next) {
+        if (node->type == XML_ELEMENT_NODE
+            && njs_xml_ns_match(node->name, node->ns, &uri, &local))
+        {
+            return njs_vm_external_create(vm, retval, njs_xml_node_proto_id,
+                                          node, 0);
+        }
+    }
+
+    njs_value_undefined_set(retval);
+
+    return NJS_OK;
+}
+
+
 static njs_int_t
 njs_xml_node_ext_attrs(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused,
     njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
@@ -1435,6 +1532,71 @@ njs_xml_string_to_c_string(njs_vm_t *vm, njs_str_t *str, u_char *dst,
 }
 
 
+static xmlNode *
+njs_xml_ns_args(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_str_t *uri, njs_str_t *local)
+{
+    xmlNode      *current;
+    njs_value_t  *value;
+
+    current = njs_vm_external(vm, njs_xml_node_proto_id,
+                              njs_argument(args, 0));
+    if (njs_slow_path(current == NULL)) {
+        njs_vm_type_error(vm, "\"this\" is not a XMLNode object");
+        return NULL;
+    }
+
+    value = njs_arg(args, nargs, 1);
+
+    if (njs_value_is_null(value)) {
+        uri->length = 0;
+        uri->start = NULL;
+
+    } else if (njs_value_is_string(value)) {
+        njs_value_string_get(vm, value, uri);
+
+    } else {
+        njs_vm_type_error(vm, "namespace URI is not a string or null");
+        return NULL;
+    }
+
+    value = njs_arg(args, nargs, 2);
+
+    if (njs_slow_path(!njs_value_is_string(value))) {
+        njs_vm_type_error(vm, "local name is not a string");
+        return NULL;
+    }
+
+    njs_value_string_get(vm, value, local);
+
+    return current;
+}
+
+
+/*
+ * An empty or null URI matches only names without a namespace, as in
+ * DOM getAttributeNS().
+ */
+
+static njs_bool_t
+njs_xml_ns_match(const xmlChar *name, xmlNs *ns, njs_str_t *uri,
+    njs_str_t *local)
+{
+    if (local->length != njs_strlen(name)
+        || njs_strncmp(local->start, name, local->length) != 0)
+    {
+        return 0;
+    }
+
+    if (ns == NULL || ns->href == NULL) {
+        return uri->length == 0;
+    }
+
+    return uri->length == njs_strlen(ns->href)
+           && njs_strncmp(uri->start, ns->href, uri->length) == 0;
+}
+
+
 static njs_bool_t
 njs_xml_node_is_live(xmlNode *node)
 {
index c0f795c7d68d97250b6cfe7c3b079ba7e4835bd9..9afb44de103ad96b90b8e64d0c973024525f7126 100644 (file)
@@ -87,6 +87,10 @@ static int qjs_xml_node_property_modify(JSContext *cx, JSValueConst obj,
     JSAtom atom, JSValueConst value, int delete);
 static JSValue qjs_xml_node_add_child(JSContext *cx, JSValueConst this_val,
     int argc, JSValueConst *argv);
+static JSValue qjs_xml_node_get_attribute_ns(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue qjs_xml_node_get_child_ns(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
 static JSValue qjs_xml_node_remove_children(JSContext *cx,
     JSValueConst this_val, int argc, JSValueConst *argv);
 static JSValue qjs_xml_node_set_attribute(JSContext *cx, JSValueConst this_val,
@@ -119,6 +123,12 @@ static qjs_xml_nset_t *qjs_xml_nset_create(JSContext *cx, xmlDoc *doc,
 static qjs_xml_nset_t *qjs_xml_nset_add(qjs_xml_nset_t *nset,
     qjs_xml_nset_t *add);
 static void qjs_xml_nset_free(JSContext *cx, qjs_xml_nset_t *nset);
+static qjs_xml_node_t *qjs_xml_ns_args(JSContext *cx, JSValueConst this_val,
+    JSValueConst *argv, njs_str_t *uri, njs_str_t *local);
+static void qjs_xml_ns_args_free(JSContext *cx, njs_str_t *uri,
+    njs_str_t *local);
+static int qjs_xml_ns_match(const xmlChar *name, xmlNs *ns, njs_str_t *uri,
+    njs_str_t *local);
 static int qjs_xml_node_is_live(xmlNode *node);
 static xmlNode *qjs_xml_list_detach(xmlNode *parent, njs_str_t *name);
 static int qjs_xml_list_retire(JSContext *cx, qjs_xml_doc_t *doc,
@@ -148,6 +158,8 @@ static const JSCFunctionListEntry qjs_xml_doc_proto[] = {
 static const JSCFunctionListEntry qjs_xml_node_proto[] = {
     JS_PROP_STRING_DEF("[Symbol.toStringTag]", "XMLNode", JS_PROP_CONFIGURABLE),
     JS_CFUNC_DEF("addChild", 1, qjs_xml_node_add_child),
+    JS_CFUNC_DEF("getAttributeNS", 2, qjs_xml_node_get_attribute_ns),
+    JS_CFUNC_DEF("getChildNS", 2, qjs_xml_node_get_child_ns),
     JS_CFUNC_DEF("removeChildren", 1, qjs_xml_node_remove_children),
     JS_CFUNC_DEF("setAttribute", 2, qjs_xml_node_set_attribute),
     JS_CFUNC_DEF("removeAttribute", 1, qjs_xml_node_remove_attribute),
@@ -1431,6 +1443,76 @@ qjs_xml_node_property_modify(JSContext *cx, JSValueConst obj, JSAtom atom,
 }
 
 
+static JSValue
+qjs_xml_node_get_attribute_ns(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    xmlAttr         *attr;
+    xmlChar         *text;
+    JSValue         value;
+    njs_str_t       uri, local;
+    qjs_xml_node_t  *current;
+
+    current = qjs_xml_ns_args(cx, this_val, argv, &uri, &local);
+    if (current == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    value = JS_UNDEFINED;
+
+    for (attr = current->node->properties; attr != NULL; attr = attr->next) {
+        if (!qjs_xml_ns_match(attr->name, attr->ns, &uri, &local)) {
+            continue;
+        }
+
+        text = xmlNodeGetContent((xmlNode *) attr);
+        if (text == NULL) {
+            value = JS_ThrowInternalError(cx, "xmlNodeGetContent() failed");
+            break;
+        }
+
+        value = JS_NewString(cx, (char *) text);
+        xmlFree(text);
+        break;
+    }
+
+    qjs_xml_ns_args_free(cx, &uri, &local);
+
+    return value;
+}
+
+
+static JSValue
+qjs_xml_node_get_child_ns(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    JSValue         value;
+    xmlNode         *node;
+    njs_str_t       uri, local;
+    qjs_xml_node_t  *current;
+
+    current = qjs_xml_ns_args(cx, this_val, argv, &uri, &local);
+    if (current == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    value = JS_UNDEFINED;
+
+    for (node = current->node->children; node != NULL; node = node->next) {
+        if (node->type == XML_ELEMENT_NODE
+            && qjs_xml_ns_match(node->name, node->ns, &uri, &local))
+        {
+            value = qjs_xml_node_make(cx, current->doc, node);
+            break;
+        }
+    }
+
+    qjs_xml_ns_args_free(cx, &uri, &local);
+
+    return value;
+}
+
+
 static JSValue
 qjs_xml_node_add_child(JSContext *cx, JSValueConst this_val,
     int argc, JSValueConst *argv)
@@ -2042,6 +2124,91 @@ qjs_xml_nset_free(JSContext *cx, qjs_xml_nset_t *nset)
 }
 
 
+static qjs_xml_node_t *
+qjs_xml_ns_args(JSContext *cx, JSValueConst this_val, JSValueConst *argv,
+    njs_str_t *uri, njs_str_t *local)
+{
+    size_t          length;
+    qjs_xml_node_t  *current;
+
+    current = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_XML_NODE);
+    if (current == NULL) {
+        JS_ThrowTypeError(cx, "\"this\" is not a XMLNode object");
+        return NULL;
+    }
+
+    if (!JS_IsNull(argv[0]) && !JS_IsString(argv[0])) {
+        JS_ThrowTypeError(cx, "namespace URI is not a string or null");
+        return NULL;
+    }
+
+    if (!JS_IsString(argv[1])) {
+        JS_ThrowTypeError(cx, "local name is not a string");
+        return NULL;
+    }
+
+    uri->start = NULL;
+    uri->length = 0;
+
+    if (JS_IsString(argv[0])) {
+        uri->start = (u_char *) JS_ToCStringLen(cx, &length, argv[0]);
+        if (uri->start == NULL) {
+            return NULL;
+        }
+
+        uri->length = length;
+    }
+
+    local->start = (u_char *) JS_ToCStringLen(cx, &length, argv[1]);
+    if (local->start == NULL) {
+        if (uri->start != NULL) {
+            JS_FreeCString(cx, (char *) uri->start);
+        }
+
+        return NULL;
+    }
+
+    local->length = length;
+
+    return current;
+}
+
+
+static void
+qjs_xml_ns_args_free(JSContext *cx, njs_str_t *uri, njs_str_t *local)
+{
+    if (uri->start != NULL) {
+        JS_FreeCString(cx, (char *) uri->start);
+    }
+
+    JS_FreeCString(cx, (char *) local->start);
+}
+
+
+/*
+ * An empty or null URI matches only names without a namespace, as in
+ * DOM getAttributeNS().
+ */
+
+static int
+qjs_xml_ns_match(const xmlChar *name, xmlNs *ns, njs_str_t *uri,
+    njs_str_t *local)
+{
+    if (local->length != njs_strlen(name)
+        || njs_strncmp(local->start, name, local->length) != 0)
+    {
+        return 0;
+    }
+
+    if (ns == NULL || ns->href == NULL) {
+        return uri->length == 0;
+    }
+
+    return uri->length == njs_strlen(ns->href)
+           && njs_strncmp(uri->start, ns->href, uri->length) == 0;
+}
+
+
 static int
 qjs_xml_node_is_live(xmlNode *node)
 {
index 1b52a904d766057f347c6676ed3106b4b70c596e..5a0e3c5b571396326b8980a2dc27ee46262bf430 100644 (file)
@@ -179,6 +179,11 @@ function xml_module(str: string) {
 
     node?.$tag$xxx?.$tag$yyy?.$attr$zzz;
 
+    let child: XMLNode | undefined = node.getChildNS('urn:x', 'xxx');
+    child = node.getChildNS(null, 'xxx');
+    let attr: string | undefined = node.getAttributeNS('urn:x', 'yyy');
+    attr = node.getAttributeNS(null, 'yyy');
+
     let buf:Buffer = xml.exclusiveC14n(node);
     buf = xml.exclusiveC14n(doc, node.$tag$xxx, false);
     buf = xml.exclusiveC14n(node, null, true, "aa bb");
index aff28956c8e0d3259e3d9c780e9f7c9bf0aa031a..6c129b880538288ad1ca17dbf451c766a6fa99a1 100644 (file)
@@ -52,6 +52,49 @@ let parse_tsuite = {
         { doc: `<n0:pdu xmlns:n0=\"http://a\"></n0:pdu>`,
           get: (doc) => doc.pdu.$ns,
           expected: 'http://a' },
+        { doc: `<r xmlns:x="urn:other" x:ID="wrong" ID="correct">`
+               + `<x:Issuer>foreign</x:Issuer><Issuer>trusted</Issuer></r>`,
+          get: (doc) => [
+              doc.r.getAttributeNS('', 'ID'),
+              doc.r.getAttributeNS(null, 'ID'),
+              doc.r.getAttributeNS('urn:other', 'ID'),
+              doc.r.getAttributeNS('urn:missing', 'ID'),
+              doc.r.getChildNS('', 'Issuer').$text,
+              doc.r.getChildNS(null, 'Issuer').$text,
+              doc.r.getChildNS('urn:other', 'Issuer').$text,
+              doc.r.getChildNS('urn:missing', 'Issuer')].join(','),
+          expected: 'correct,correct,wrong,,trusted,trusted,foreign,' },
+        { doc: `<r xmlns="urn:d" ID="plain"><c/></r>`,
+          get: (doc) => [
+              doc.r.getAttributeNS(null, 'ID'),
+              doc.r.getAttributeNS('urn:d', 'ID'),
+              doc.r.getChildNS(null, 'c'),
+              doc.r.getChildNS('urn:d', 'c').$name].join(','),
+          expected: 'plain,,,c' },
+        { doc: `<r xml:lang="en"/>`,
+          get: (doc) => doc.r.getAttributeNS(
+                            'http://www.w3.org/XML/1998/namespace', 'lang'),
+          expected: 'en' },
+        { doc: `<r><a><b/></a></r>`,
+          get: (doc) => doc.r.getChildNS(null, 'b'),
+          expected: undefined },
+        { doc: `<r><!--c--><?p?>text<c/></r>`,
+          get: (doc) => doc.r.getChildNS(null, 'c').$name,
+          expected: 'c' },
+        { doc: `<!DOCTYPE r [<!ENTITY e "complete">]>`
+               + `<r xmlns:x="urn:other" x:ID="pre&e;post"/>`,
+          get: (doc) => doc.r.getAttributeNS('urn:other', 'ID'),
+          expected: 'precompletepost' },
+        { get: (doc) => doc.note.getChildNS(undefined, 'to'),
+          exception: 'TypeError: namespace URI is not a string or null' },
+        { get: (doc) => doc.note.getAttributeNS(1, 'a'),
+          exception: 'TypeError: namespace URI is not a string or null' },
+        { get: (doc) => doc.note.getChildNS(null),
+          exception: 'TypeError: local name is not a string' },
+        { get: (doc) => doc.note.to.getAttributeNS(null, null),
+          exception: 'TypeError: local name is not a string' },
+        { get: (doc) => doc.note.getChildNS.call({}, null, 'to'),
+          exception: 'TypeError: "this" is not a XMLNode object' },
         { doc: `<root><foo>FOO</foo><foo>BAR</foo></root>`,
           get: (doc) => doc.root.$tags$foo[0].$text,
           expected: 'FOO' },
index fa97af4745f206f32ad04005a3fd638e2e2a7e7c..4cd7431e110f554d3d1285b0df9503664a9e5909 100644 (file)
@@ -15,6 +15,29 @@ declare module "xml" {
     }
 
     export interface XMLNode {
+        /**
+         * Returns the first direct child element with this namespace URI
+         * and local name, or undefined if there is none.
+         * @param namespaceURI - namespace URI; null or an empty string
+         * matches an element without a namespace.
+         * @param localName - local name of the element.
+         * @since 1.0.2.
+         */
+        getChildNS(namespaceURI: string | null, localName: string):
+            XMLNode | undefined;
+
+        /**
+         * Returns the value of an attribute present in the document with
+         * this namespace URI and local name, or undefined if there is none.
+         * @param namespaceURI - namespace URI; null or an empty string
+         * matches an attribute without a namespace.  An unprefixed
+         * attribute never belongs to the default namespace.
+         * @param localName - local name of the attribute.
+         * @since 1.0.2.
+         */
+        getAttributeNS(namespaceURI: string | null, localName: string):
+            string | undefined;
+
         /**
          * Adds a recursive copy of a namespace-free child node.
          * @param node - XMLNode to be added.