From 7bdf94b22e6aba664dd238b92aceae2f96fe14d0 Mon Sep 17 00:00:00 2001
From: Arseny Kapoulkine
Date: Sun, 14 Jun 2026 22:06:06 -0700
Subject: [PATCH 1/3] Add xml_node::ensure_attribute and xml_node::ensure_child
These are equivalent to a named lookup (attribute/child) followed by
appending the attribute/child if an existing one is not found. The
implementation is trivial but this is fairly commonplace in code that
modifies an existing DOM.
---
src/pugixml.cpp | 32 ++++++++++++++++++++++++++++++++
src/pugixml.hpp | 12 ++++++++++++
2 files changed, 44 insertions(+)
diff --git a/src/pugixml.cpp b/src/pugixml.cpp
index a38b5a7ef..8c286d25e 100644
--- a/src/pugixml.cpp
+++ b/src/pugixml.cpp
@@ -6256,6 +6256,22 @@ namespace pugi
}
#endif
+ PUGI_IMPL_FN xml_attribute xml_node::ensure_attribute(const char_t* name_)
+ {
+ xml_attribute result = attribute(name_);
+
+ return result ? result : append_attribute(name_);
+ }
+
+#ifdef PUGIXML_HAS_STRING_VIEW
+ PUGI_IMPL_FN xml_attribute xml_node::ensure_attribute(string_view_t name_)
+ {
+ xml_attribute result = attribute(name_);
+
+ return result ? result : append_attribute(name_);
+ }
+#endif
+
PUGI_IMPL_FN xml_attribute xml_node::append_copy(const xml_attribute& proto)
{
if (!proto) return xml_attribute();
@@ -6470,6 +6486,22 @@ namespace pugi
}
#endif
+ PUGI_IMPL_FN xml_node xml_node::ensure_child(const char_t* name_)
+ {
+ xml_node result = child(name_);
+
+ return result ? result : append_child(name_);
+ }
+
+#ifdef PUGIXML_HAS_STRING_VIEW
+ PUGI_IMPL_FN xml_node xml_node::ensure_child(string_view_t name_)
+ {
+ xml_node result = child(name_);
+
+ return result ? result : append_child(name_);
+ }
+#endif
+
PUGI_IMPL_FN xml_node xml_node::append_copy(const xml_node& proto)
{
xml_node_type type_ = proto.type();
diff --git a/src/pugixml.hpp b/src/pugixml.hpp
index 8ebd32878..a6eca725d 100644
--- a/src/pugixml.hpp
+++ b/src/pugixml.hpp
@@ -625,6 +625,12 @@ namespace pugi
xml_attribute insert_attribute_before(string_view_t name, const xml_attribute& attr);
#endif
+ // Get attribute with specified name, adding one if it does not exist. Returns the existing or added attribute, or empty attribute on errors.
+ xml_attribute ensure_attribute(const char_t* name);
+ #ifdef PUGIXML_HAS_STRING_VIEW
+ xml_attribute ensure_attribute(string_view_t name);
+ #endif
+
// Add a copy of the specified attribute. Returns added attribute, or empty attribute on errors.
xml_attribute append_copy(const xml_attribute& proto);
xml_attribute prepend_copy(const xml_attribute& proto);
@@ -649,6 +655,12 @@ namespace pugi
xml_node insert_child_before(string_view_t name, const xml_node& node);
#endif
+ // Get child with specified name, adding one if it does not exist. Returns the existing or added node, or empty node on errors.
+ xml_node ensure_child(const char_t* name);
+ #ifdef PUGIXML_HAS_STRING_VIEW
+ xml_node ensure_child(string_view_t name);
+ #endif
+
// Add a copy of the specified node as a child. Returns added node, or empty node on errors.
xml_node append_copy(const xml_node& proto);
xml_node prepend_copy(const xml_node& proto);
From 09721beb808a134481849f6b59d33c4d4d517027 Mon Sep 17 00:00:00 2001
From: Arseny Kapoulkine
Date: Sun, 14 Jun 2026 22:08:00 -0700
Subject: [PATCH 2/3] tests: Add tests for xml_node::ensure_child/attribute
These include both raw string and string view variants.
---
tests/test_dom_modify.cpp | 59 +++++++++++++++++++++++++++++++++++++++
1 file changed, 59 insertions(+)
diff --git a/tests/test_dom_modify.cpp b/tests/test_dom_modify.cpp
index 2b96d9d7b..063b139af 100644
--- a/tests/test_dom_modify.cpp
+++ b/tests/test_dom_modify.cpp
@@ -379,6 +379,37 @@ TEST_XML(dom_node_append_attribute, "")
CHECK_NODE(doc, STR(""));
}
+TEST_XML(dom_node_ensure_attribute, "")
+{
+ CHECK(xml_node().ensure_attribute(STR("a")) == xml_attribute());
+ CHECK(doc.ensure_attribute(STR("a")) == xml_attribute());
+
+ xml_node node = doc.child(STR("node"));
+
+ // existing attribute is returned as is
+ xml_attribute a1 = node.ensure_attribute(STR("a1"));
+ CHECK(a1 && a1 == node.attribute(STR("a1")));
+ CHECK_STRING(a1.value(), STR("v1"));
+
+ // missing attribute is appended
+ xml_attribute a2 = node.ensure_attribute(STR("a2"));
+ CHECK(a2 && a2 != a1);
+ a2 = STR("v2");
+
+ // ensuring an existing attribute does not add a duplicate
+ CHECK(node.ensure_attribute(STR("a2")) == a2);
+
+#ifdef PUGIXML_HAS_STRING_VIEW
+ xml_attribute a3 = node.child(STR("child")).ensure_attribute(string_view_t(STR("a3")));
+#else
+ xml_attribute a3 = node.child(STR("child")).ensure_attribute(STR("a3"));
+#endif
+ CHECK(a3 && a3 != a1 && a3 != a2);
+ a3 = STR("v3");
+
+ CHECK_NODE(doc, STR(""));
+}
+
TEST_XML(dom_node_insert_attribute_after, "")
{
CHECK(xml_node().insert_attribute_after(STR("a"), xml_attribute()) == xml_attribute());
@@ -838,6 +869,34 @@ TEST_XML(dom_node_append_child_name, "foo")
CHECK_NODE(doc, STR("foo"));
}
+TEST_XML(dom_node_ensure_child, "foo")
+{
+ CHECK(xml_node().ensure_child(STR("")) == xml_node());
+ CHECK(doc.child(STR("node")).first_child().ensure_child(STR("n")) == xml_node());
+
+ xml_node node = doc.child(STR("node"));
+
+ // existing child is returned as is
+ xml_node child = node.ensure_child(STR("child"));
+ CHECK(child && child == node.child(STR("child")));
+
+ // missing child is appended
+ xml_node n1 = node.ensure_child(STR("n1"));
+ CHECK(n1 && n1 != child);
+
+ // ensuring an existing child does not add a duplicate
+ CHECK(node.ensure_child(STR("child")) == child);
+
+#ifdef PUGIXML_HAS_STRING_VIEW
+ xml_node n2 = doc.ensure_child(string_view_t(STR("n2")));
+#else
+ xml_node n2 = doc.ensure_child(STR("n2"));
+#endif
+ CHECK(n2 && n2 != child && n2 != n1);
+
+ CHECK_NODE(doc, STR("foo"));
+}
+
TEST_XML(dom_node_insert_child_after_name, "foo")
{
CHECK(xml_node().insert_child_after(STR(""), xml_node()) == xml_node());
From c97161520096a41f7bef17db47c174a95646ce3f Mon Sep 17 00:00:00 2001
From: Arseny Kapoulkine
Date: Sun, 14 Jun 2026 22:10:27 -0700
Subject: [PATCH 3/3] docs: Add documentation for ensure_attribute/node
Also add these to the changelog for 1.16 as it's right around the
corner and the changelog has already been written.
---
docs/manual.adoc | 17 ++++++++++++++---
docs/manual.html | 23 ++++++++++++++++++-----
2 files changed, 32 insertions(+), 8 deletions(-)
diff --git a/docs/manual.adoc b/docs/manual.adoc
index 4484978e8..4776595c9 100644
--- a/docs/manual.adoc
+++ b/docs/manual.adoc
@@ -1377,7 +1377,7 @@ include::samples/modify_base.cpp[tags=attr]
[[modify.add]]
=== Adding nodes/attributes
-[[xml_node::prepend_attribute]][[xml_node::append_attribute]][[xml_node::insert_attribute_after]][[xml_node::insert_attribute_before]][[xml_node::prepend_child]][[xml_node::append_child]][[xml_node::insert_child_after]][[xml_node::insert_child_before]]
+[[xml_node::prepend_attribute]][[xml_node::append_attribute]][[xml_node::insert_attribute_after]][[xml_node::insert_attribute_before]][[xml_node::ensure_attribute]][[xml_node::prepend_child]][[xml_node::append_child]][[xml_node::insert_child_after]][[xml_node::insert_child_before]][[xml_node::ensure_child]]
Nodes and attributes do not exist without a document tree, so you can't create them without adding them to some document. A node or attribute can be created at the end of node/attribute list or before/after some other node:
[source]
@@ -1404,9 +1404,14 @@ xml_node xml_node::insert_child_after(const char_t* name, const xml_node& node);
xml_node xml_node::insert_child_after(string_view_t name, const xml_node& node);
xml_node xml_node::insert_child_before(const char_t* name, const xml_node& node);
xml_node xml_node::insert_child_before(string_view_t name, const xml_node& node);
+
+xml_attribute xml_node::ensure_attribute(const char_t* name);
+xml_attribute xml_node::ensure_attribute(string_view_t name);
+xml_node xml_node::ensure_child(const char_t* name);
+xml_node xml_node::ensure_child(string_view_t name);
----
-`append_attribute` and `append_child` create a new node/attribute at the end of the corresponding list of the node the method is called on; `prepend_attribute` and `prepend_child` create a new node/attribute at the beginning of the list; `insert_attribute_after`, `insert_attribute_before`, `insert_child_after` and `insert_child_before` add the node/attribute before or after the specified node/attribute.
+`append_attribute` and `append_child` create a new node/attribute at the end of the corresponding list of the node the method is called on; `prepend_attribute` and `prepend_child` create a new node/attribute at the beginning of the list; `insert_attribute_after`, `insert_attribute_before`, `insert_child_after` and `insert_child_before` add the node/attribute before or after the specified node/attribute. `ensure_attribute` and `ensure_child` return the existing attribute/child with the specified name, appending a new one only if no such attribute/child exists; this makes it convenient to write code like `node.ensure_attribute("id") = 123;`.
Attribute functions create an attribute with the specified name; you can specify the empty name and change the name later if you want to. Node functions with the `type` argument create the node with the specified type; since node type can't be changed, you have to know the desired type beforehand. Also note that not all types can be added as children; see below for clarification. Node functions with the `name` argument create the element node (<>) with the specified name.
@@ -1422,7 +1427,7 @@ All functions return the handle to the created object on success, and null handl
Even if the operation fails, the document remains in consistent state, but the requested node/attribute is not added.
-CAUTION: `attribute()` and `child()` functions do not add attributes or nodes to the tree, so code like `node.attribute("id") = 123;` will not do anything if `node` does not have an attribute with name `"id"`. Make sure you're operating with existing attributes/nodes by adding them if necessary.
+CAUTION: `attribute()` and `child()` functions do not add attributes or nodes to the tree, so code like `node.attribute("id") = 123;` will not do anything if `node` does not have an attribute with name `"id"`. Make sure you're operating with existing attributes/nodes by adding them if necessary, or use `ensure_attribute`/`ensure_child`.
This is an example of adding new attributes/nodes to the document (link:samples/modify_add.cpp[]):
@@ -2208,6 +2213,7 @@ Anniversary release (pugixml turns 20 this year!). Changes:
* Improvements:
. `PUGIXML_CHARCONV_FLOAT` option can be enabled to switch floating point conversions to ``; this requires C{plus}{plus}17, makes the conversions locale-independent and can improve performance
+ . Add `xml_node::ensure_child` and `xml_node::ensure_attribute` that return the child/attribute with the specified name, adding one if it does not exist
. Improve performance of searching for nodes and attributes by name
. Loading a document from an empty buffer no longer performs memory allocations
@@ -3079,6 +3085,11 @@ const unsigned int +++parse_wnorm_attribute
xml_node +++insert_child_before+++(const char_t* name, const xml_node& node);
xml_node +++insert_child_before+++(string_view_t name, const xml_node& node);
+ xml_attribute +++ensure_attribute+++(const char_t* name);
+ xml_attribute +++ensure_attribute+++(string_view_t name);
+ xml_node +++ensure_child+++(const char_t* name);
+ xml_node +++ensure_child+++(string_view_t name);
+
xml_attribute +++append_copy+++(const xml_attribute& proto);
xml_attribute +++prepend_copy+++(const xml_attribute& proto);
xml_attribute +++insert_copy_after+++(const xml_attribute& proto, const xml_attribute& attr);
diff --git a/docs/manual.html b/docs/manual.html
index fd4c112c0..db0198dbb 100644
--- a/docs/manual.html
+++ b/docs/manual.html
@@ -2807,7 +2807,7 @@
+Add xml_node::ensure_child and xml_node::ensure_attribute that return the child/attribute with the specified name, adding one if it does not exist
+
+
Improve performance of searching for nodes and attributes by name
@@ -6128,6 +6136,11 @@ xml_node insert_child_before(const char_t* name, const xml_node& node);
xml_node insert_child_before(string_view_t name, const xml_node& node);
+ xml_attribute ensure_attribute(const char_t* name);
+ xml_attribute ensure_attribute(string_view_t name);
+ xml_node ensure_child(const char_t* name);
+ xml_node ensure_child(string_view_t name);
+
xml_attribute append_copy(const xml_attribute& proto);
xml_attribute prepend_copy(const xml_attribute& proto);
xml_attribute insert_copy_after(const xml_attribute& proto, const xml_attribute& attr);
@@ -6375,7 +6388,7 @@