sw/qa/uibase/shells/data/three-changes.fodt |   40 +++++++++++++
 sw/qa/uibase/shells/shells.cxx              |   84 ++++++++++++++++++++++++++++
 sw/source/uibase/uno/loktxdoc.cxx           |   77 +++++++++++++++++++++++++
 3 files changed, 201 insertions(+)

New commits:
commit f136069168d321d5ce3c0725ca85435ee7d377ac
Author:     Mike Kaganski <mike.kagan...@collabora.com>
AuthorDate: Mon Jun 16 22:03:43 2025 +0500
Commit:     Miklos Vajna <vmik...@collabora.com>
CommitDate: Tue Jun 17 13:35:02 2025 +0200

    LOK Extract API: introduce redline data extraction
    
    Filter name is "trackchanges". Returned are "TrackChanges.ByIndex.N"
    numbered JSON nodes with these fields:
    
    * "type": string ("Delete", "Insert", "Format")
    * "datetime": string (ISO datetime stamp)
    * "author": string
    * "description": string
    * "comment": string
    * "text_before": string (up to 200 characters)
    * "text_after": string (up to 200 characters)
    
    Change-Id: I467bb9989d2eb461c74f50b53e039c1b2260e74c
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/186579
    Reviewed-by: Miklos Vajna <vmik...@collabora.com>
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com>

diff --git a/sw/qa/uibase/shells/data/three-changes.fodt 
b/sw/qa/uibase/shells/data/three-changes.fodt
new file mode 100644
index 000000000000..0c7072b244a0
--- /dev/null
+++ b/sw/qa/uibase/shells/data/three-changes.fodt
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<office:document 
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" 
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" 
xmlns:dc="http://purl.org/dc/elements/1.1/"; 
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" 
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" 
office:version="1.4" office:mimetype="application/vnd.oasis.opendocument.text">
+ <office:automatic-styles>
+  <style:style style:name="T1" style:family="text">
+   <style:text-properties fo:font-weight="bold"/>
+  </style:style>
+ </office:automatic-styles>
+ <office:body>
+  <office:text>
+   <text:tracked-changes text:track-changes="false">
+    <text:changed-region xml:id="ct2412392131792" text:id="ct2412392131792">
+     <text:deletion>
+      <office:change-info>
+       <dc:creator>Mike</dc:creator>
+       <dc:date>2025-06-16T14:08:27</dc:date>
+      </office:change-info>
+     </text:deletion>
+    </text:changed-region>
+    <text:changed-region xml:id="ct2412428113776" text:id="ct2412428113776">
+     <text:format-change>
+      <office:change-info>
+       <dc:creator>Mike</dc:creator>
+       <dc:date>2025-06-17T12:41:00</dc:date>
+      </office:change-info>
+     </text:format-change>
+    </text:changed-region>
+    <text:changed-region xml:id="ct2412428117232" text:id="ct2412428117232">
+     <text:insertion>
+      <office:change-info>
+       <dc:creator>Mike</dc:creator>
+       <dc:date>2025-06-17T12:41:19</dc:date>
+      </office:change-info>
+     </text:insertion>
+    </text:changed-region>
+   </text:tracked-changes>
+   <text:p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum 
consequat mi quis pretium semper. Proin luctus orci ac neque venenatis, quis 
commodo dolor posuere. Curabitur dignissim sapien quis cursus egestas. 
<text:change-start text:change-id="ct2412392131792"/>Donec<text:change-end 
text:change-id="ct2412392131792"/> blandit auctor arcu, nec <text:change-start 
text:change-id="ct2412428113776"/><text:span 
text:style-name="T1">pellentesque</text:span><text:change-end 
text:change-id="ct2412428113776"/> eros molestie eget. In consectetur aliquam 
hendrerit. Sed cursus mauris vitae ligula pellentesque, non pellentesque urna 
aliquet. Fusce placerat mauris enim, nec rutrum purus semper vel. Praesent 
tincidunt neque eu pellentesque pharetra. Fusce pellentesque est 
orci.<text:change-start text:change-id="ct2412428117232"/> Sapienti 
sat.<text:change-end text:change-id="ct2412428117232"/></text:p>
+  </office:text>
+ </office:body>
+</office:document>
\ No newline at end of file
diff --git a/sw/qa/uibase/shells/shells.cxx b/sw/qa/uibase/shells/shells.cxx
index 999a1aa54109..39571984abf9 100644
--- a/sw/qa/uibase/shells/shells.cxx
+++ b/sw/qa/uibase/shells/shells.cxx
@@ -47,6 +47,10 @@
 #include <unotxdoc.hxx>
 #include <tools/json_writer.hxx>
 
+#include <boost/property_tree/json_parser.hpp>
+
+using namespace std::string_literals;
+
 /// Covers sw/source/uibase/shells/ fixes.
 class SwUibaseShellsTest : public SwModelTestBase
 {
@@ -884,6 +888,86 @@ CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, 
testDocumentStructureDocProperties)
     CPPUNIT_ASSERT_EQUAL(aExpectedStr, aJsonWriter.finishAndGetAsOString());
 }
 
+CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, testDocumentStructureExtractRedlines)
+{
+    createSwDoc("three-changes.fodt");
+
+    // extract
+    tools::JsonWriter aJsonWriter;
+    std::string_view 
aCommand(".uno:ExtractDocumentStructure?filter=trackchanges");
+    getSwTextDoc()->getCommandValues(aJsonWriter, aCommand);
+
+    boost::property_tree::ptree tree;
+    std::stringstream 
aStream(std::string(aJsonWriter.finishAndGetAsOString()));
+    boost::property_tree::read_json(aStream, tree);
+
+    CPPUNIT_ASSERT_EQUAL(size_t(1), tree.size());
+    boost::property_tree::ptree docStructure = tree.get_child("DocStructure");
+    CPPUNIT_ASSERT_EQUAL(size_t(3), docStructure.size());
+    auto it = docStructure.begin();
+
+    {
+        // First change
+        CPPUNIT_ASSERT(it != docStructure.end());
+        const auto & [ name, change ] = *it;
+        CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.0"s, name);
+        CPPUNIT_ASSERT_EQUAL(size_t(7), change.size());
+        CPPUNIT_ASSERT_EQUAL("Delete"s, change.get<std::string>("type"));
+        CPPUNIT_ASSERT_EQUAL("2025-06-16T14:08:27"s, 
change.get<std::string>("datetime"));
+        CPPUNIT_ASSERT_EQUAL("Mike"s, change.get<std::string>("author"));
+        CPPUNIT_ASSERT_EQUAL("Delete “Donec”"s, 
change.get<std::string>("description"));
+        CPPUNIT_ASSERT_EQUAL(""s, change.get<std::string>("comment"));
+        auto text_before = change.get<std::string>("text-before");
+        CPPUNIT_ASSERT_EQUAL(size_t(200), text_before.size());
+        CPPUNIT_ASSERT(text_before.ends_with(" egestas. "));
+        auto text_after = change.get<std::string>("text-after");
+        CPPUNIT_ASSERT_EQUAL(size_t(200), text_after.size());
+        CPPUNIT_ASSERT(text_after.starts_with(" blandit "));
+        ++it;
+    }
+
+    {
+        // Second change
+        CPPUNIT_ASSERT(it != docStructure.end());
+        const auto & [ name, change ] = *it;
+        CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.1"s, name);
+        CPPUNIT_ASSERT_EQUAL(size_t(7), change.size());
+        CPPUNIT_ASSERT_EQUAL("Format"s, change.get<std::string>("type"));
+        CPPUNIT_ASSERT_EQUAL("2025-06-17T12:41:00"s, 
change.get<std::string>("datetime"));
+        CPPUNIT_ASSERT_EQUAL("Mike"s, change.get<std::string>("author"));
+        CPPUNIT_ASSERT_EQUAL("Attributes changed"s, 
change.get<std::string>("description"));
+        CPPUNIT_ASSERT_EQUAL(""s, change.get<std::string>("comment"));
+        auto text_before = change.get<std::string>("text-before");
+        CPPUNIT_ASSERT_EQUAL(size_t(200), text_before.size());
+        CPPUNIT_ASSERT(text_before.ends_with(" arcu, nec "));
+        auto text_after = change.get<std::string>("text-after");
+        CPPUNIT_ASSERT_EQUAL(size_t(200), text_after.size());
+        CPPUNIT_ASSERT(text_after.starts_with(" eros "));
+        ++it;
+    }
+
+    {
+        // Third change
+        CPPUNIT_ASSERT(it != docStructure.end());
+        const auto & [ name, change ] = *it;
+        CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.2"s, name);
+        CPPUNIT_ASSERT_EQUAL(size_t(7), change.size());
+        CPPUNIT_ASSERT_EQUAL("Insert"s, change.get<std::string>("type"));
+        CPPUNIT_ASSERT_EQUAL("2025-06-17T12:41:19"s, 
change.get<std::string>("datetime"));
+        CPPUNIT_ASSERT_EQUAL("Mike"s, change.get<std::string>("author"));
+        CPPUNIT_ASSERT_EQUAL("Insert “ Sapienti sat.”"s, 
change.get<std::string>("description"));
+        CPPUNIT_ASSERT_EQUAL(""s, change.get<std::string>("comment"));
+        auto text_before = change.get<std::string>("text-before");
+        CPPUNIT_ASSERT_EQUAL(size_t(200), text_before.size());
+        CPPUNIT_ASSERT(text_before.ends_with(" est orci."));
+        auto text_after = change.get<std::string>("text-after");
+        CPPUNIT_ASSERT(text_after.empty());
+        ++it;
+    }
+
+    CPPUNIT_ASSERT(bool(it == docStructure.end()));
+}
+
 CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, testUpdateRefmarks)
 {
     // Given a document with two refmarks, one is not interesting the other is 
a citation:
diff --git a/sw/source/uibase/uno/loktxdoc.cxx 
b/sw/source/uibase/uno/loktxdoc.cxx
index 2bd36c3dbc58..ca3e5ab49b44 100644
--- a/sw/source/uibase/uno/loktxdoc.cxx
+++ b/sw/source/uibase/uno/loktxdoc.cxx
@@ -39,6 +39,7 @@
 #include <wrtsh.hxx>
 #include <txtrfmrk.hxx>
 #include <ndtxt.hxx>
+#include <unoredlines.hxx>
 
 #include <unoport.hxx>
 #include <unoprnms.hxx>
@@ -63,6 +64,37 @@ using namespace ::com::sun::star;
 
 namespace
 {
+// A helper class to make it easier to put UNO property values to JSON with a 
given name.
+// Removes noise from code.
+class PropertyExtractor
+{
+public:
+    PropertyExtractor(uno::Reference<beans::XPropertySet>& xProperties, 
tools::JsonWriter& rWriter)
+        : m_xProperties(xProperties)
+        , m_rWriter(rWriter)
+    {
+    }
+
+    template <typename T> void extract(const OUString& unoName, 
std::string_view jsonName)
+    {
+        if (T val; m_xProperties->getPropertyValue(unoName) >>= val)
+        {
+            if constexpr (std::is_same_v<T, util::DateTime>)
+            {
+                OUStringBuffer buf(32);
+                sax::Converter::convertDateTime(buf, val, nullptr, true);
+                m_rWriter.put(jsonName, buf.makeStringAndClear());
+            }
+            else
+                m_rWriter.put(jsonName, val);
+        }
+    }
+
+private:
+    uno::Reference<beans::XPropertySet>& m_xProperties;
+    tools::JsonWriter& m_rWriter;
+};
+
 /// Implements getCommandValues(".uno:TextFormFields").
 ///
 /// Parameters:
@@ -822,6 +854,48 @@ void GetDocStructureDocProps(tools::JsonWriter& 
rJsonWriter, const SwDocShell* p
     }
 }
 
+/// Implements getCommandValues(".uno:ExtractDocumentStructures") for redlines
+void GetDocStructureTrackChanges(tools::JsonWriter& rJsonWriter, const 
SwDocShell* pDocShell)
+{
+    auto xRedlinesEnum = 
pDocShell->GetBaseModel()->getRedlines()->createEnumeration();
+    for (sal_Int32 i = 0; xRedlinesEnum->hasMoreElements(); ++i)
+    {
+        auto xRedlineProperties = 
xRedlinesEnum->nextElement().query<beans::XPropertySet>();
+        assert(xRedlineProperties);
+
+        auto TrackChangesNode
+            = rJsonWriter.startNode(Concat2View("TrackChanges.ByIndex." + 
OString::number(i)));
+
+        PropertyExtractor extractor{ xRedlineProperties, rJsonWriter };
+
+        extractor.extract<OUString>(UNO_NAME_REDLINE_TYPE, "type");
+        extractor.extract<css::util::DateTime>(UNO_NAME_REDLINE_DATE_TIME, 
"datetime");
+        extractor.extract<OUString>(UNO_NAME_REDLINE_AUTHOR, "author");
+        extractor.extract<OUString>(UNO_NAME_REDLINE_DESCRIPTION, 
"description");
+        extractor.extract<OUString>(UNO_NAME_REDLINE_COMMENT, "comment");
+        if (auto xStart = 
xRedlineProperties->getPropertyValue(UNO_NAME_REDLINE_START)
+                              .query<css::text::XTextRange>())
+        {
+            auto xCursor = xStart->getText()->createTextCursorByRange(xStart);
+            xCursor->goLeft(200, /*bExpand*/ true);
+            rJsonWriter.put("text-before", xCursor->getString());
+        }
+        if (auto xEnd = 
xRedlineProperties->getPropertyValue(UNO_NAME_REDLINE_END)
+                            .query<css::text::XTextRange>())
+        {
+            auto xCursor = xEnd->getText()->createTextCursorByRange(xEnd);
+            xCursor->goRight(200, /*bExpand*/ true);
+            rJsonWriter.put("text-after", xCursor->getString());
+        }
+        // UNO_NAME_REDLINE_IDENTIFIER: OUString (the value of a pointer, not 
persistent)
+        // UNO_NAME_REDLINE_MOVED_ID: sal_uInt32; 0 == not moved, 1 == moved, 
but don't have its pair, 2+ == unique ID
+        // UNO_NAME_REDLINE_SUCCESSOR_DATA: uno::Sequence<beans::PropertyValue>
+        // UNO_NAME_IS_IN_HEADER_FOOTER: bool
+        // UNO_NAME_MERGE_LAST_PARA: bool
+        // UNO_NAME_REDLINE_TEXT: uno::Reference<text::XText>
+    }
+}
+
 /// Implements getCommandValues(".uno:ExtractDocumentStructures").
 ///
 /// Parameters:
@@ -844,6 +918,9 @@ void GetDocStructure(tools::JsonWriter& rJsonWriter, const 
SwDocShell* pDocShell
 
     if (filter.isEmpty() || filter == "docprops")
         GetDocStructureDocProps(rJsonWriter, pDocShell);
+
+    if (filter.isEmpty() || filter == "trackchanges")
+        GetDocStructureTrackChanges(rJsonWriter, pDocShell);
 }
 
 /// Implements getCommandValues(".uno:Sections").

Reply via email to