include/tools/XPath.hxx         |   72 ++++++++++++++
 svx/qa/unit/svdraw.cxx          |  128 +++++++++++++++----------
 tools/CppunitTest_tools_test.mk |   12 +-
 tools/Library_tl.mk             |    1 
 tools/qa/cppunit/test_xpath.cxx |   95 +++++++++++++++++++
 tools/source/xml/XPath.cxx      |  197 ++++++++++++++++++++++++++++++++++++++++
 6 files changed, 445 insertions(+), 60 deletions(-)

New commits:
commit 99354e5cb0abb025c0ee980676bf0e37822c8119
Author:     Tomaž Vajngerl <[email protected]>
AuthorDate: Sun Dec 14 12:07:31 2025 +0900
Commit:     Tomaž Vajngerl <[email protected]>
CommitDate: Mon Dec 22 09:50:21 2025 +0100

    introduce tools::XPath to be able to reuse the result after call
    
    In tests we many times rerun the XPath search to just check another
    XML attribute, or first check the size of child elements and then
    check each element's attributes in a separate call (re-running the
    XPath search). Re-running an XPath search all the time takes up
    quite some CPU resources and doing it a lot will accumulate, so the
    idea here is to have an object as a result on which you can reuse
    instead of re-running the search.
    
    In addition it is possible to combine the search path from the
    previous search, so it is possible to construct paths in a more
    concise way.
    
    Example:
    auto primitivePath = aXPath.create("/primitive2D");
    auto polyPath = aXPath.create(primitivePath, "/polypolygon");
    
    polyPath will use a combined path "/primitive2D/polypolygon"
    as the search path.
    
    Checking can be done by simply using CPPUNIT_ASSERT* functions
    with this, but we might extend UnoApiXmlTest with some checks
    for convenience because the assertXPath methods do have better
    reporting when the assert fails. Also integration into the
    UnoApiXmlTest class will need to be done, because namespaces need
    to be registered before tools::XPath can be used correctly. This
    change does not touch that yet.
    
    A couple of tests in SvdrawTest have been changed to use the new
    tools::XPath class - most notable testRectangleObject. These tests
    don't use any namespaces so it is easy to convert those.
    
    Change-Id: I29251d2e7f4c3ee5b76e46daf78c21d0ca067cb4
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/195618
    Tested-by: Jenkins
    Reviewed-by: Tomaž Vajngerl <[email protected]>

diff --git a/include/tools/XPath.hxx b/include/tools/XPath.hxx
new file mode 100644
index 000000000000..28b7d30ba1ef
--- /dev/null
+++ b/include/tools/XPath.hxx
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#pragma once
+
+#include <tools/toolsdllapi.h>
+#include <rtl/string.hxx>
+#include <rtl/ustring.hxx>
+
+#include <memory>
+#include <functional>
+
+#include <libxml/xmlwriter.h>
+#include <libxml/xpath.h>
+#include <libxml/xpathInternals.h>
+#include <libxml/parserInternals.h>
+
+namespace tools
+{
+class TOOLS_DLLPUBLIC XmlElement final
+{
+    xmlNodePtr mpXmlNode;
+
+public:
+    XmlElement(xmlNodePtr pXmlNode)
+        : mpXmlNode(pXmlNode)
+    {
+    }
+
+    std::string_view name();
+    OUString attribute(const char* pAttribute);
+    sal_Int32 countChildren();
+    std::unique_ptr<XmlElement> at(sal_Int32 nIndex);
+};
+
+class TOOLS_DLLPUBLIC XPathObject final
+{
+    xmlXPathObjectPtr mpXPathObject;
+    std::string_view maPath;
+
+public:
+    XPathObject(xmlXPathObjectPtr pXPathObject, std::string_view aString);
+    ~XPathObject();
+    std::string_view getPathString() { return maPath; }
+    sal_Int32 count();
+    OUString attribute(const char* pAttribute);
+    OUString content();
+    std::unique_ptr<XmlElement> at(sal_Int32 nIndex);
+};
+
+class TOOLS_DLLPUBLIC XPath final
+{
+    xmlDocPtr mpXmlDocPtr;
+    std::function<void(xmlXPathContextPtr)> mFuncRegisterNamespaces;
+
+public:
+    XPath(xmlDocPtr pDocPtr);
+    XPath(xmlDocPtr pDocPtr, std::function<void(xmlXPathContextPtr)> 
funcRegisterNamespaces);
+    std::unique_ptr<XPathObject> create(std::string_view aString);
+    std::unique_ptr<XPathObject> create(std::unique_ptr<XPathObject> const& 
pPathObject,
+                                        std::string_view aString);
+};
+
+} // end tools namespace
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/svx/qa/unit/svdraw.cxx b/svx/qa/unit/svdraw.cxx
index 16b1107b1842..8c50855e1924 100644
--- a/svx/qa/unit/svdraw.cxx
+++ b/svx/qa/unit/svdraw.cxx
@@ -46,6 +46,7 @@
 #include <sfx2/objsh.hxx>
 
 #include <sdr/contact/objectcontactofobjlistpainter.hxx>
+#include <tools/XPath.hxx>
 
 using namespace ::com::sun::star;
 
@@ -329,45 +330,56 @@ CPPUNIT_TEST_FIXTURE(SvdrawTest, testRectangleObject)
     svx::ExtendedPrimitive2dXmlDump aDumper;
     xmlDocUniquePtr pXmlDoc = aDumper.dumpAndParse(xPrimitiveSequence);
 
-    assertXPath(pXmlDoc, "/primitive2D", 1);
-
-    OString aBasePath("/primitive2D/sdrrectangle/group/polypolygoncolor"_ostr);
-    assertXPath(pXmlDoc, aBasePath, "color", u"#729fcf");
-
-    assertXPath(pXmlDoc, aBasePath + "/polypolygon", "height",
-                u"99"); // weird Rectangle is created with size 100
-    assertXPath(pXmlDoc, aBasePath + "/polypolygon", "width", u"99");
-    assertXPath(pXmlDoc, aBasePath + "/polypolygon", "minx", u"0");
-    assertXPath(pXmlDoc, aBasePath + "/polypolygon", "miny", u"0");
-    assertXPath(pXmlDoc, aBasePath + "/polypolygon", "maxx", u"99");
-    assertXPath(pXmlDoc, aBasePath + "/polypolygon", "maxy", u"99");
-
-    aBasePath = 
"/primitive2D/sdrrectangle/group/polypolygoncolor/polypolygon/polygon"_ostr;
-
-    assertXPath(pXmlDoc, aBasePath + "/point", 5);
-    assertXPath(pXmlDoc, aBasePath + "/point[1]", "x", u"49.5"); // hmm, 
weird, why?
-    assertXPath(pXmlDoc, aBasePath + "/point[1]", "y", u"99");
-    assertXPath(pXmlDoc, aBasePath + "/point[2]", "x", u"0");
-    assertXPath(pXmlDoc, aBasePath + "/point[2]", "y", u"99");
-    assertXPath(pXmlDoc, aBasePath + "/point[3]", "x", u"0");
-    assertXPath(pXmlDoc, aBasePath + "/point[3]", "y", u"0");
-    assertXPath(pXmlDoc, aBasePath + "/point[4]", "x", u"99");
-    assertXPath(pXmlDoc, aBasePath + "/point[4]", "y", u"0");
-    assertXPath(pXmlDoc, aBasePath + "/point[5]", "x", u"99");
-    assertXPath(pXmlDoc, aBasePath + "/point[5]", "y", u"99");
-
-    aBasePath = "/primitive2D/sdrrectangle/group/polygonstroke"_ostr;
-    assertXPath(pXmlDoc, aBasePath, 1);
-
-    assertXPath(pXmlDoc, aBasePath + "/line", "color", u"#3465a4");
-    assertXPath(pXmlDoc, aBasePath + "/line", "width", u"0");
-    assertXPath(pXmlDoc, aBasePath + "/line", "linejoin", u"Round");
-    assertXPath(pXmlDoc, aBasePath + "/line", "linecap", u"BUTT");
-
-    assertXPathContent(pXmlDoc, aBasePath + "/polygon", u"49.5,99 0,99 0,0 
99,0 99,99");
+    tools::XPath aPath(pXmlDoc.get());
+    CPPUNIT_ASSERT(aPath.create("/primitive2D"));
+
+    auto aPolyPoly = 
aPath.create("/primitive2D/sdrrectangle/group/polypolygoncolor");
+    CPPUNIT_ASSERT(aPolyPoly);
+    CPPUNIT_ASSERT_EQUAL(u"#729fcf"_ustr, aPolyPoly->attribute("color"));
+
+    auto aPolyPolyPath = aPath.create(aPolyPoly, "/polypolygon");
+    CPPUNIT_ASSERT(aPolyPolyPath);
+    CPPUNIT_ASSERT_EQUAL(1, aPolyPolyPath->count());
+    auto aPolyPolyElement = aPolyPolyPath->at(0);
+    CPPUNIT_ASSERT_EQUAL(u"99"_ustr, aPolyPolyElement->attribute(
+                                         "height")); // weird Rectangle is 
created with size 100
+    CPPUNIT_ASSERT_EQUAL(u"99"_ustr, aPolyPolyElement->attribute("width"));
+    CPPUNIT_ASSERT_EQUAL(u"0"_ustr, aPolyPolyElement->attribute("minx"));
+    CPPUNIT_ASSERT_EQUAL(u"0"_ustr, aPolyPolyElement->attribute("miny"));
+    CPPUNIT_ASSERT_EQUAL(u"99"_ustr, aPolyPolyElement->attribute("maxx"));
+    CPPUNIT_ASSERT_EQUAL(u"99"_ustr, aPolyPolyElement->attribute("maxy"));
+
+    auto aPolyPath
+        = 
aPath.create("/primitive2D/sdrrectangle/group/polypolygoncolor/polypolygon/polygon");
+    CPPUNIT_ASSERT(aPolyPath);
+    auto aPoints = aPath.create(aPolyPath, "/point");
+    CPPUNIT_ASSERT_EQUAL(5, aPoints->count());
+    CPPUNIT_ASSERT_EQUAL(u"49.5"_ustr, aPoints->at(0)->attribute("x"));
+    CPPUNIT_ASSERT_EQUAL(u"99"_ustr, aPoints->at(0)->attribute("y"));
+    CPPUNIT_ASSERT_EQUAL(u"0"_ustr, aPoints->at(1)->attribute("x"));
+    CPPUNIT_ASSERT_EQUAL(u"99"_ustr, aPoints->at(1)->attribute("y"));
+    CPPUNIT_ASSERT_EQUAL(u"0"_ustr, aPoints->at(2)->attribute("x"));
+    CPPUNIT_ASSERT_EQUAL(u"0"_ustr, aPoints->at(2)->attribute("y"));
+    CPPUNIT_ASSERT_EQUAL(u"99"_ustr, aPoints->at(3)->attribute("x"));
+    CPPUNIT_ASSERT_EQUAL(u"0"_ustr, aPoints->at(3)->attribute("y"));
+    CPPUNIT_ASSERT_EQUAL(u"99"_ustr, aPoints->at(4)->attribute("x"));
+    CPPUNIT_ASSERT_EQUAL(u"99"_ustr, aPoints->at(4)->attribute("y"));
+
+    auto aStrokePath = 
aPath.create("/primitive2D/sdrrectangle/group/polygonstroke");
+    CPPUNIT_ASSERT(aStrokePath);
+    CPPUNIT_ASSERT_EQUAL(1, aStrokePath->count());
+
+    auto aLinePath = aPath.create(aStrokePath, "/line");
+    CPPUNIT_ASSERT_EQUAL(u"#3465a4"_ustr, aLinePath->attribute("color"));
+    CPPUNIT_ASSERT_EQUAL(u"0"_ustr, aLinePath->attribute("width"));
+    CPPUNIT_ASSERT_EQUAL(u"Round"_ustr, aLinePath->attribute("linejoin"));
+    CPPUNIT_ASSERT_EQUAL(u"BUTT"_ustr, aLinePath->attribute("linecap"));
+
+    auto aLineContentPath = aPath.create(aStrokePath, "/polygon");
+    CPPUNIT_ASSERT_EQUAL(u"49.5,99 0,99 0,0 99,0 99,99"_ustr, 
aLineContentPath->content());
 
     // If solid line, then there is no line stroke information
-    assertXPath(pXmlDoc, aBasePath + "/stroke", 0);
+    CPPUNIT_ASSERT_EQUAL(0, aPath.create(aStrokePath, "/stroke")->count());
 
     pPage->RemoveObject(0);
 }
@@ -414,14 +426,19 @@ CPPUNIT_TEST_FIXTURE(SvdrawTest, testFontWorks)
     SdrPage* pSdrPage = pDrawPage->GetSdrPage();
     xmlDocUniquePtr pXmlDoc = lcl_dumpAndParseFirstObjectWithAssert(pSdrPage);
 
-    assertXPath(pXmlDoc, "/primitive2D", 1);
+    tools::XPath aXPath(pXmlDoc.get());
+    CPPUNIT_ASSERT_EQUAL(1, aXPath.create("/primitive2D")->count());
+    CPPUNIT_ASSERT_EQUAL(u"Perspective"_ustr,
+                         
aXPath.create("//scene")->attribute("projectionMode"));
+
+    auto aFillPath = aXPath.create("//scene/extrude3D[1]/fill");
+    CPPUNIT_ASSERT_EQUAL(u"#ff0000"_ustr, aFillPath->attribute("color"));
+
+    auto aMaterialPath = 
aXPath.create("//scene/extrude3D[1]/object3Dattributes/material");
+    CPPUNIT_ASSERT_EQUAL(u"#ff0000"_ustr, aMaterialPath->attribute("color"));
 
-    assertXPath(pXmlDoc, "//scene", "projectionMode", u"Perspective");
-    assertXPath(pXmlDoc, "//scene/extrude3D[1]/fill", "color", u"#ff0000");
-    assertXPath(pXmlDoc, "//scene/extrude3D[1]/object3Dattributes/material", 
"color", u"#ff0000");
     // ODF default 50% is represented by Specular Intensity = 2^5. The 
relationship is not linear.
-    assertXPath(pXmlDoc, "//scene/extrude3D[1]/object3Dattributes/material", 
"specularIntensity",
-                u"32");
+    CPPUNIT_ASSERT_EQUAL(u"32"_ustr, 
aMaterialPath->attribute("specularIntensity"));
 }
 
 CPPUNIT_TEST_FIXTURE(SvdrawTest, testTdf148000_EOLinCurvedText)
@@ -437,16 +454,18 @@ CPPUNIT_TEST_FIXTURE(SvdrawTest, 
testTdf148000_EOLinCurvedText)
         SdrPage* pSdrPage = getFirstDrawPageWithAssert();
 
         xmlDocUniquePtr pXmlDoc = 
lcl_dumpAndParseFirstObjectWithAssert(pSdrPage);
+        tools::XPath aXPath(pXmlDoc.get());
 
         // this is a group shape, hence 2 nested objectinfo
-        OString aBasePath = 
"/primitive2D/objectinfo[4]/objectinfo/unhandled/group/unhandled/group/"
-                            "polypolygoncolor/polypolygon/"_ostr;
+        auto aBasePath = 
aXPath.create("/primitive2D/objectinfo[4]/objectinfo/unhandled/group/"
+                                       
"unhandled/group/polypolygoncolor/polypolygon");
 
         // The text is: "O" + eop + "O" + eol + "O"
         // It should be displayed as 3 line of text. (1 "O" letter in every 
line)
-        sal_Int32 nY1 = getXPath(pXmlDoc, aBasePath + "polygon[1]/point[1]", 
"y").toInt32();
-        sal_Int32 nY2 = getXPath(pXmlDoc, aBasePath + "polygon[3]/point[1]", 
"y").toInt32();
-        sal_Int32 nY3 = getXPath(pXmlDoc, aBasePath + "polygon[5]/point[1]", 
"y").toInt32();
+
+        sal_Int32 nY1 = aXPath.create(aBasePath, 
"/polygon[1]/point[1]")->attribute("y").toInt32();
+        sal_Int32 nY2 = aXPath.create(aBasePath, 
"/polygon[3]/point[1]")->attribute("y").toInt32();
+        sal_Int32 nY3 = aXPath.create(aBasePath, 
"/polygon[5]/point[1]")->attribute("y").toInt32();
 
         sal_Int32 nDiff21 = nY2 - nY1;
         sal_Int32 nDiff32 = nY3 - nY2;
@@ -480,16 +499,17 @@ CPPUNIT_TEST_FIXTURE(SvdrawTest, 
testTdf148000_CurvedTextWidth)
         SdrPage* pSdrPage = getFirstDrawPageWithAssert();
 
         xmlDocUniquePtr pXmlDoc = 
lcl_dumpAndParseFirstObjectWithAssert(pSdrPage);
+        tools::XPath aXPath(pXmlDoc.get());
 
-        OString aBasePath = 
"/primitive2D/objectinfo[4]/objectinfo/unhandled/group/unhandled/group/"
-                            "polypolygoncolor/polypolygon/"_ostr;
+        auto aBasePath = 
aXPath.create("/primitive2D/objectinfo[4]/objectinfo/unhandled/group/"
+                                       
"unhandled/group/polypolygoncolor/polypolygon");
 
         // The text is: 7 line od "OOOOOOO"
         // Take the x coord of the 4 "O" on the corners
-        sal_Int32 nX1 = getXPath(pXmlDoc, aBasePath + "polygon[1]/point[1]", 
"x").toInt32();
-        sal_Int32 nX2 = getXPath(pXmlDoc, aBasePath + "polygon[13]/point[1]", 
"x").toInt32();
-        sal_Int32 nX3 = getXPath(pXmlDoc, aBasePath + "polygon[85]/point[1]", 
"x").toInt32();
-        sal_Int32 nX4 = getXPath(pXmlDoc, aBasePath + "polygon[97]/point[1]", 
"x").toInt32();
+        sal_Int32 nX1 = aXPath.create(aBasePath, 
"/polygon[1]/point[1]")->attribute("x").toInt32();
+        sal_Int32 nX2 = aXPath.create(aBasePath, 
"/polygon[13]/point[1]")->attribute("x").toInt32();
+        sal_Int32 nX3 = aXPath.create(aBasePath, 
"/polygon[85]/point[1]")->attribute("x").toInt32();
+        sal_Int32 nX4 = aXPath.create(aBasePath, 
"/polygon[97]/point[1]")->attribute("x").toInt32();
 
         if (i < 2)
         {
diff --git a/tools/CppunitTest_tools_test.mk b/tools/CppunitTest_tools_test.mk
index c2087bcde594..f5422b78e74f 100644
--- a/tools/CppunitTest_tools_test.mk
+++ b/tools/CppunitTest_tools_test.mk
@@ -11,8 +11,6 @@
 
 $(eval $(call gb_CppunitTest_CppunitTest,tools_test))
 
-$(eval $(call gb_CppunitTest_use_external,tools_test,boost_headers))
-
 $(eval $(call gb_CppunitTest_add_exception_objects,tools_test, \
     tools/qa/cppunit/test_bigint \
     tools/qa/cppunit/test_date \
@@ -31,6 +29,7 @@ $(eval $(call 
gb_CppunitTest_add_exception_objects,tools_test, \
     tools/qa/cppunit/test_100mm2twips \
     tools/qa/cppunit/test_xmlwalker \
     tools/qa/cppunit/test_xmlwriter \
+    tools/qa/cppunit/test_xpath \
     tools/qa/cppunit/test_GenericTypeSerializer \
     tools/qa/cppunit/test_guid \
     tools/qa/cppunit/test_cpuid \
@@ -53,6 +52,11 @@ $(eval $(call 
gb_CppunitTest_add_exception_objects,tools_test,\
     tools/qa/cppunit/test_cpu_runtime_detection_SSSE3_check, 
$(CXXFLAGS_INTRINSICS_SSSE3) \
 ))
 
+$(eval $(call gb_CppunitTest_use_externals,tools_test, \
+    boost_headers \
+    libxml2 \
+))
+
 $(eval $(call gb_CppunitTest_use_sdk_api,tools_test))
 
 $(eval $(call gb_CppunitTest_use_libraries,tools_test, \
@@ -75,8 +79,4 @@ $(eval $(call gb_CppunitTest_set_include,tools_test,\
     -I$(SRCDIR)/tools/inc \
 ))
 
-$(eval $(call gb_Library_use_externals,tools_test,\
-       libxml2 \
-))
-
 # vim: set noet sw=4 ts=4:
diff --git a/tools/Library_tl.mk b/tools/Library_tl.mk
index b82c065bdeef..5c3a7e453137 100644
--- a/tools/Library_tl.mk
+++ b/tools/Library_tl.mk
@@ -83,6 +83,7 @@ $(eval $(call gb_Library_add_exception_objects,tl,\
     tools/source/zcodec/zcodec \
     tools/source/xml/XmlWriter \
     tools/source/xml/XmlWalker \
+    tools/source/xml/XPath \
 ))
 
 ifneq ($(SYSTEM_LIBFIXMATH),TRUE)
diff --git a/tools/qa/cppunit/test_xpath.cxx b/tools/qa/cppunit/test_xpath.cxx
new file mode 100644
index 000000000000..3cbc29c90ff8
--- /dev/null
+++ b/tools/qa/cppunit/test_xpath.cxx
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <cppunit/extensions/HelperMacros.h>
+#include <test/bootstrapfixture.hxx>
+#include <rtl/ustring.hxx>
+#include <tools/stream.hxx>
+#include <tools/XPath.hxx>
+
+namespace
+{
+class XPathTest : public test::BootstrapFixture
+{
+    OUString maBasePath;
+
+public:
+    XPathTest()
+        : BootstrapFixture(true, false)
+    {
+    }
+
+    virtual void setUp() override { maBasePath = 
m_directories.getURLFromSrc(u"/tools/qa/data/"); }
+
+    void testXPath()
+    {
+        OUString aXmlFilePath = maBasePath + "test.xml";
+        SvFileStream aFileStream(aXmlFilePath, StreamMode::READ);
+        std::size_t nSize = aFileStream.remainingSize();
+        std::unique_ptr<sal_uInt8[]> pBuffer(new sal_uInt8[nSize + 1]);
+        pBuffer[nSize] = 0;
+        aFileStream.ReadBytes(pBuffer.get(), nSize);
+        auto pCharBuffer = reinterpret_cast<xmlChar*>(pBuffer.get());
+        xmlDocPtr pXmlDoc
+            = xmlReadDoc(pCharBuffer, nullptr, nullptr, XML_PARSE_NODICT | 
XML_PARSE_HUGE);
+
+        tools::XPath aXPath(pXmlDoc);
+        auto aNonExistentPath = aXPath.create("/nonexistent");
+        CPPUNIT_ASSERT(aNonExistentPath);
+        CPPUNIT_ASSERT_EQUAL(0, aNonExistentPath->count());
+
+        auto aRootResult = aXPath.create("/root");
+        CPPUNIT_ASSERT(aRootResult);
+        CPPUNIT_ASSERT_EQUAL(1, aRootResult->count());
+        CPPUNIT_ASSERT_EQUAL(u"Hello World"_ustr, 
aRootResult->attribute("root-attr"));
+        {
+            auto aRootElement = aRootResult->at(0);
+            CPPUNIT_ASSERT_EQUAL(std::string_view("root"), 
aRootElement->name());
+            CPPUNIT_ASSERT_EQUAL(4, aRootElement->countChildren());
+            CPPUNIT_ASSERT_EQUAL(std::string_view("child"), 
aRootElement->at(0)->name());
+            CPPUNIT_ASSERT_EQUAL(std::string_view("child"), 
aRootElement->at(1)->name());
+            CPPUNIT_ASSERT_EQUAL(std::string_view("child"), 
aRootElement->at(2)->name());
+            CPPUNIT_ASSERT_EQUAL(std::string_view("with-namespace"), 
aRootElement->at(3)->name());
+        }
+
+        auto aChildResult = aXPath.create(aRootResult, "/child");
+        CPPUNIT_ASSERT(aChildResult);
+        CPPUNIT_ASSERT_EQUAL(3, aChildResult->count());
+
+        auto aChildElement = aChildResult->at(0);
+        CPPUNIT_ASSERT_EQUAL(std::string_view("child"), aChildElement->name());
+        CPPUNIT_ASSERT_EQUAL(u"1"_ustr, aChildElement->attribute("number"));
+        CPPUNIT_ASSERT_EQUAL(u"123"_ustr, 
aChildElement->attribute("attribute"));
+
+        auto aGrandChildElement = aChildElement->at(0);
+        CPPUNIT_ASSERT(aGrandChildElement);
+        CPPUNIT_ASSERT_EQUAL(std::string_view("grandchild"), 
aGrandChildElement->name());
+
+        CPPUNIT_ASSERT_EQUAL(u"2"_ustr, 
aChildResult->at(1)->attribute("number"));
+        CPPUNIT_ASSERT_EQUAL(u"3"_ustr, 
aChildResult->at(2)->attribute("number"));
+
+        auto aGrandChildResult = aXPath.create(aRootResult, 
"/child[1]/grandchild");
+        CPPUNIT_ASSERT(aGrandChildResult);
+        CPPUNIT_ASSERT_EQUAL(1, aGrandChildResult->count());
+        CPPUNIT_ASSERT_EQUAL(u"Content"_ustr, aGrandChildResult->content());
+        CPPUNIT_ASSERT_EQUAL(u"ABC"_ustr, 
aGrandChildResult->attribute("attribute1"));
+        CPPUNIT_ASSERT_EQUAL(u"CDE"_ustr, 
aGrandChildResult->attribute("attribute2"));
+
+        xmlFreeDoc(pXmlDoc);
+    }
+
+    CPPUNIT_TEST_SUITE(XPathTest);
+    CPPUNIT_TEST(testXPath);
+    CPPUNIT_TEST_SUITE_END();
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(XPathTest);
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/tools/source/xml/XPath.cxx b/tools/source/xml/XPath.cxx
new file mode 100644
index 000000000000..06400cfbae0e
--- /dev/null
+++ b/tools/source/xml/XPath.cxx
@@ -0,0 +1,197 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <tools/XPath.hxx>
+
+namespace tools
+{
+namespace
+{
+OUString convert(xmlChar const* sXmlString)
+{
+    OUString sString;
+    rtl_convertStringToUString(&sString.pData, reinterpret_cast<char 
const*>(sXmlString),
+                               xmlStrlen(sXmlString), RTL_TEXTENCODING_UTF8,
+                               RTL_TEXTTOUNICODE_FLAGS_UNDEFINED_ERROR
+                                   | RTL_TEXTTOUNICODE_FLAGS_MBUNDEFINED_ERROR
+                                   | RTL_TEXTTOUNICODE_FLAGS_INVALID_ERROR);
+    return sString;
+}
+}
+
+std::string_view XmlElement::name() { return reinterpret_cast<const 
char*>(mpXmlNode->name); }
+
+OUString XmlElement::attribute(const char* pAttribute)
+{
+    OUString sString;
+
+    xmlChar* pProperty = xmlGetProp(mpXmlNode, BAD_CAST(pAttribute));
+    if (pProperty)
+    {
+        sString = convert(pProperty);
+        xmlFree(pProperty);
+    }
+    return sString;
+}
+
+sal_Int32 XmlElement::countChildren() { return 
sal_Int32(xmlChildElementCount(mpXmlNode)); }
+
+std::unique_ptr<XmlElement> XmlElement::at(sal_Int32 nIndex)
+{
+    sal_Int32 nCurrent = 0;
+    xmlNodePtr pCurrent = xmlFirstElementChild(mpXmlNode);
+
+    while (pCurrent != nullptr && nCurrent < nIndex)
+    {
+        pCurrent = xmlNextElementSibling(pCurrent);
+        nCurrent++;
+    }
+
+    if (pCurrent)
+        return std::make_unique<XmlElement>(pCurrent);
+    return {};
+}
+
+XPath::XPath(xmlDocPtr pDocPtr)
+    : XPath(pDocPtr, [](xmlXPathContextPtr) {})
+{
+}
+
+XPath::XPath(xmlDocPtr pDocPtr, std::function<void(xmlXPathContextPtr)> 
funcRegisterNamespaces)
+    : mpXmlDocPtr(pDocPtr)
+    , mFuncRegisterNamespaces(funcRegisterNamespaces)
+{
+}
+
+std::unique_ptr<XPathObject> XPath::create(std::string_view aString)
+{
+    OString aCopy(aString);
+    xmlXPathContextPtr pXPathContext = xmlXPathNewContext(mpXmlDocPtr);
+    mFuncRegisterNamespaces(pXPathContext);
+    xmlXPathObjectPtr pXPathObject
+        = xmlXPathEvalExpression(BAD_CAST(aCopy.getStr()), pXPathContext);
+    xmlXPathFreeContext(pXPathContext);
+
+    if (!pXPathObject)
+        return {};
+
+    if (!pXPathObject->nodesetval)
+    {
+        xmlXPathFreeObject(pXPathObject);
+        return {};
+    }
+
+    return std::make_unique<XPathObject>(pXPathObject, aString);
+}
+
+std::unique_ptr<XPathObject> XPath::create(std::unique_ptr<XPathObject> const& 
pPathObject,
+                                           std::string_view aString)
+{
+    return create(Concat2View(pPathObject->getPathString() + 
OString::Concat(aString)));
+}
+
+XPathObject::XPathObject(xmlXPathObjectPtr pXPathObject, std::string_view 
aString)
+    : mpXPathObject(pXPathObject)
+    , maPath(aString)
+{
+}
+
+XPathObject::~XPathObject()
+{
+    if (mpXPathObject)
+        xmlXPathFreeObject(mpXPathObject);
+}
+
+sal_Int32 XPathObject::count()
+{
+    if (!mpXPathObject)
+        return 0;
+
+    xmlNodeSetPtr pXmlNodes = mpXPathObject->nodesetval;
+    if (!pXmlNodes)
+        return 0;
+
+    return xmlXPathNodeSetGetLength(pXmlNodes);
+}
+
+OUString XPathObject::attribute(const char* pAttribute)
+{
+    auto pXmlElement = at(0);
+    if (!pXmlElement)
+        return OUString();
+
+    return pXmlElement->attribute(pAttribute);
+}
+
+std::unique_ptr<XmlElement> XPathObject::at(sal_Int32 nIndex)
+{
+    if (!mpXPathObject)
+        return {};
+
+    if (nIndex >= count())
+        return {};
+
+    xmlNodeSetPtr pXmlNodes = mpXPathObject->nodesetval;
+    if (!pXmlNodes)
+        return {};
+
+    xmlNodePtr pXmlNode = pXmlNodes->nodeTab[nIndex];
+    return std::make_unique<XmlElement>(pXmlNode);
+}
+
+OUString XPathObject::content()
+{
+    OUString sError;
+    if (!mpXPathObject)
+        return sError;
+
+    switch (mpXPathObject->type)
+    {
+        case XPATH_NODESET:
+        {
+            xmlNodeSetPtr pXmlNodes = mpXPathObject->nodesetval;
+            if (count() != 1)
+                return sError;
+
+            xmlNodePtr pXmlNode = pXmlNodes->nodeTab[0];
+            return convert(xmlNodeGetContent(pXmlNode));
+        }
+        case XPATH_BOOLEAN:
+        {
+            auto bBoolVal = mpXPathObject->boolval;
+            return bBoolVal ? u"true"_ustr : u"false"_ustr;
+        }
+        case XPATH_NUMBER:
+        {
+            auto floatVal = mpXPathObject->floatval;
+            return OUString::number(floatVal);
+        }
+        case XPATH_STRING:
+        {
+            return convert(mpXPathObject->stringval);
+        }
+
+        case XPATH_UNDEFINED:
+            // Undefined XPath type
+#if LIBXML_VERSION < 21000 || defined(LIBXML_XPTR_LOCS_ENABLED)
+        case XPATH_POINT:
+        case XPATH_RANGE:
+        case XPATH_LOCATIONSET:
+#endif
+        case XPATH_USERS:
+        case XPATH_XSLT_TREE:
+            // Unsupported XPath type
+            break;
+    }
+    return sError;
+}
+
+} // end tools namespace
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */

Reply via email to