sw/qa/uibase/shells/data/nearby-changes.fodt |   56 ++++++++
 sw/qa/uibase/shells/shells.cxx               |   93 ++++++++++++++
 sw/source/uibase/uno/loktxdoc.cxx            |  179 ++++++++++++++++++++-------
 3 files changed, 288 insertions(+), 40 deletions(-)

New commits:
commit ba0dc60a2ac54887c9cca9b7cb2a0755a7ccce54
Author:     Mike Kaganski <mike.kagan...@collabora.com>
AuthorDate: Tue Jul 1 12:26:14 2025 +0500
Commit:     Mike Kaganski <mike.kagan...@collabora.com>
CommitDate: Tue Jul 1 14:12:00 2025 +0200

    LOK Extract API: normalize textBefore / textAfter in redline extraction
    
    We return textBefore / textAfter context for each redline; but for
    it to make sense, it needs to reflect the text in the state when the
    redline was created.
    
    This change makes sure that for each processed redline, the document
    is set to the visual state present at the moment current for this
    redline: all previous changes are shown as if accepted; all future
    changes are not shown as if rejected.
    
    Change-Id: Ie5b9a971c2c98642b49212fbcd57df46a8daaccc
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/187221
    Tested-by: Jenkins
    Reviewed-by: Mike Kaganski <mike.kagan...@collabora.com>

diff --git a/sw/qa/uibase/shells/data/nearby-changes.fodt 
b/sw/qa/uibase/shells/data/nearby-changes.fodt
new file mode 100644
index 000000000000..6996965c63eb
--- /dev/null
+++ b/sw/qa/uibase/shells/data/nearby-changes.fodt
@@ -0,0 +1,56 @@
+<?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="ct1" text:id="ct1">
+     <text:deletion>
+      <office:change-info>
+       <dc:creator>Mike</dc:creator>
+       <dc:date>2025-07-01T12:00:00</dc:date>
+      </office:change-info>
+     </text:deletion>
+    </text:changed-region>
+    <text:changed-region xml:id="ct2" text:id="ct2">
+     <text:insertion>
+      <office:change-info>
+       <dc:creator>Mike</dc:creator>
+       <dc:date>2025-07-01T12:30:00</dc:date>
+      </office:change-info>
+     </text:insertion>
+    </text:changed-region>
+    <text:changed-region xml:id="ct3" text:id="ct3">
+     <text:format-change>
+      <office:change-info>
+       <dc:creator>Mike</dc:creator>
+       <dc:date>2025-07-01T13:00:00</dc:date>
+      </office:change-info>
+     </text:format-change>
+    </text:changed-region>
+    <text:changed-region xml:id="ct4" text:id="ct4">
+     <text:deletion>
+      <office:change-info>
+       <dc:creator>Mike</dc:creator>
+       <dc:date>2025-07-01T13:30:00</dc:date>
+      </office:change-info>
+     </text:deletion>
+    </text:changed-region>
+    <text:changed-region xml:id="ct5" text:id="ct5">
+     <text:insertion>
+      <office:change-info>
+       <dc:creator>Mike</dc:creator>
+       <dc:date>2025-07-01T14:00:00</dc:date>
+      </office:change-info>
+     </text:insertion>
+    </text:changed-region>
+   </text:tracked-changes>
+   <text:p>a <text:change-start text:change-id="ct1"/>del1<text:change-end 
text:change-id="ct1"/> b <text:change-start 
text:change-id="ct2"/>ins2<text:change-end text:change-id="ct2"/> c 
<text:change-start text:change-id="ct3"/><text:span 
text:style-name="T1">fmt3</text:span><text:change-end text:change-id="ct3"/> d 
<text:change-start text:change-id="ct4"/>del4<text:change-end 
text:change-id="ct4"/> e <text:change-start 
text:change-id="ct5"/>ins5<text:change-end text:change-id="ct5"/> f</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 22da30260421..117928917de9 100644
--- a/sw/qa/uibase/shells/shells.cxx
+++ b/sw/qa/uibase/shells/shells.cxx
@@ -1024,6 +1024,99 @@ CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, 
testDocumentStructureExtractRedlines)
     CPPUNIT_ASSERT(bool(it == docStructure.end()));
 }
 
+CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, 
testDocumentStructureExtractRedlines_textBeforeAfter)
+{
+    // The document here has a series of insert/delete/format changes having 
different times.
+    // This test checks, that for each extracted data, we produce textBefore / 
textAfter in proper
+    // form: all newer changes are pretended to never had happened / as if 
rejected; other changes
+    // are shown as if accepted. This produces the correct context for each 
current change.
+    createSwDoc("nearby-changes.fodt");
+
+    boost::property_tree::ptree tree;
+
+    // extract using the context length enough to cover the whole document text
+    tools::JsonWriter aJsonWriter;
+    std::string_view 
aCommand(".uno:ExtractDocumentStructure?filter=trackchanges,contextLen:100");
+    getSwTextDoc()->getCommandValues(aJsonWriter, aCommand);
+
+    std::stringstream 
aStream(std::string(aJsonWriter.finishAndGetAsOString()));
+    boost::property_tree::read_json(aStream, tree);
+
+    boost::property_tree::ptree docStructure = tree.get_child("DocStructure");
+    CPPUNIT_ASSERT_EQUAL(size_t(5), docStructure.size());
+    auto it = docStructure.begin();
+
+    // The text before covers older changes. It includes text that was 
inserted earlier; it
+    // excludes text that was deleted earlier.
+    // The text after covers newer changes. It includes text that was deleted 
later; it
+    // excludes text that was added later.
+    // The formatting changes don't affect text.
+    // The whole text looks like "a del1 b ins2 c fmt3 d del4 e ins5 f"
+    // where <delN>, <insN>, <fmtN> are successive 
deletions/insertions/formattings.
+
+    {
+        CPPUNIT_ASSERT(it != docStructure.end());
+        const auto & [ name, change ] = *it;
+        CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.0"s, name);
+        CPPUNIT_ASSERT_EQUAL("Delete"s, change.get<std::string>("type"));
+        CPPUNIT_ASSERT_EQUAL("2025-07-01T12:00:00"s, 
change.get<std::string>("dateTime"));
+        CPPUNIT_ASSERT_EQUAL("del1"s, change.get<std::string>("textChanged"));
+        CPPUNIT_ASSERT_EQUAL("a "s, change.get<std::string>("textBefore"));
+        CPPUNIT_ASSERT_EQUAL(" b  c fmt3 d del4 e  f"s, 
change.get<std::string>("textAfter"));
+        ++it;
+    }
+
+    {
+        CPPUNIT_ASSERT(it != docStructure.end());
+        const auto & [ name, change ] = *it;
+        CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.1"s, name);
+        CPPUNIT_ASSERT_EQUAL("Insert"s, change.get<std::string>("type"));
+        CPPUNIT_ASSERT_EQUAL("2025-07-01T12:30:00"s, 
change.get<std::string>("dateTime"));
+        CPPUNIT_ASSERT_EQUAL("ins2"s, change.get<std::string>("textChanged"));
+        CPPUNIT_ASSERT_EQUAL("a  b "s, change.get<std::string>("textBefore"));
+        CPPUNIT_ASSERT_EQUAL(" c fmt3 d del4 e  f"s, 
change.get<std::string>("textAfter"));
+        ++it;
+    }
+
+    {
+        CPPUNIT_ASSERT(it != docStructure.end());
+        const auto & [ name, change ] = *it;
+        CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.2"s, name);
+        CPPUNIT_ASSERT_EQUAL("Format"s, change.get<std::string>("type"));
+        CPPUNIT_ASSERT_EQUAL("2025-07-01T13:00:00"s, 
change.get<std::string>("dateTime"));
+        CPPUNIT_ASSERT_EQUAL("fmt3"s, change.get<std::string>("textChanged"));
+        CPPUNIT_ASSERT_EQUAL("a  b ins2 c "s, 
change.get<std::string>("textBefore"));
+        CPPUNIT_ASSERT_EQUAL(" d del4 e  f"s, 
change.get<std::string>("textAfter"));
+        ++it;
+    }
+
+    {
+        CPPUNIT_ASSERT(it != docStructure.end());
+        const auto & [ name, change ] = *it;
+        CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.3"s, name);
+        CPPUNIT_ASSERT_EQUAL("Delete"s, change.get<std::string>("type"));
+        CPPUNIT_ASSERT_EQUAL("2025-07-01T13:30:00"s, 
change.get<std::string>("dateTime"));
+        CPPUNIT_ASSERT_EQUAL("del4"s, change.get<std::string>("textChanged"));
+        CPPUNIT_ASSERT_EQUAL("a  b ins2 c fmt3 d "s, 
change.get<std::string>("textBefore"));
+        CPPUNIT_ASSERT_EQUAL(" e  f"s, change.get<std::string>("textAfter"));
+        ++it;
+    }
+
+    {
+        CPPUNIT_ASSERT(it != docStructure.end());
+        const auto & [ name, change ] = *it;
+        CPPUNIT_ASSERT_EQUAL("TrackChanges.ByIndex.4"s, name);
+        CPPUNIT_ASSERT_EQUAL("Insert"s, change.get<std::string>("type"));
+        CPPUNIT_ASSERT_EQUAL("2025-07-01T14:00:00"s, 
change.get<std::string>("dateTime"));
+        CPPUNIT_ASSERT_EQUAL("ins5"s, change.get<std::string>("textChanged"));
+        CPPUNIT_ASSERT_EQUAL("a  b ins2 c fmt3 d  e "s, 
change.get<std::string>("textBefore"));
+        CPPUNIT_ASSERT_EQUAL(" f"s, change.get<std::string>("textAfter"));
+        ++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 e638a8142c60..7701e874b9f8 100644
--- a/sw/source/uibase/uno/loktxdoc.cxx
+++ b/sw/source/uibase/uno/loktxdoc.cxx
@@ -33,12 +33,15 @@
 #include <sfx2/lokhelper.hxx>
 
 #include <IDocumentMarkAccess.hxx>
+#include <IDocumentRedlineAccess.hxx>
 #include <doc.hxx>
 #include <docsh.hxx>
 #include <fmtrfmrk.hxx>
 #include <wrtsh.hxx>
 #include <txtrfmrk.hxx>
 #include <ndtxt.hxx>
+#include <redline.hxx>
+#include <unoredline.hxx>
 #include <unoredlines.hxx>
 
 #include <unoport.hxx>
@@ -69,7 +72,8 @@ namespace
 class PropertyExtractor
 {
 public:
-    PropertyExtractor(uno::Reference<beans::XPropertySet>& xProperties, 
tools::JsonWriter& rWriter)
+    PropertyExtractor(const uno::Reference<beans::XPropertySet>& xProperties,
+                      tools::JsonWriter& rWriter)
         : m_xProperties(xProperties)
         , m_rWriter(rWriter)
     {
@@ -91,7 +95,7 @@ public:
     }
 
 private:
-    uno::Reference<beans::XPropertySet>& m_xProperties;
+    uno::Reference<beans::XPropertySet> m_xProperties;
     tools::JsonWriter& m_rWriter;
 };
 
@@ -854,8 +858,97 @@ void GetDocStructureDocProps(tools::JsonWriter& 
rJsonWriter, const SwDocShell* p
     }
 }
 
+// This class temporarily hides / shows redlines in the document, based on 
timestamp: when a redline
+// is newer than the date, it is "hidden" (the text looks as if that redline 
were rejected); and
+// otherwise the redline is "shown" (the text looks as if the redline is 
accepted). This allows to
+// obtain textBefore / textAfter context attributes for a given redline as it 
was when the redline
+// was created. The state of redlines is restored to original in dtor.
+class HideNewerShowOlder
+{
+public:
+    HideNewerShowOlder(DateTime limit, const SwRedlineTable& rTable)
+        : m_rTable(rTable)
+        , m_aRedlineShowStateRestore(collectRestoreData(m_rTable))
+    {
+        for (auto pRedline : m_rTable)
+        {
+            const auto& data = pRedline->GetRedlineData();
+            if (data.GetType() != RedlineType::Insert && data.GetType() != 
RedlineType::Delete)
+                continue;
+            bool hide;
+            if (limit < data.GetTimeStamp())
+                hide = data.GetType() == RedlineType::Insert;
+            else // not later
+                hide = data.GetType() == RedlineType::Delete;
+
+            if (hide)
+                Hide(pRedline, m_rTable);
+            else
+                Show(pRedline, m_rTable);
+        }
+    }
+    ~HideNewerShowOlder()
+    {
+        // I assume, that only the redlines explicitly handled in ctor would 
change their visible
+        // state; so here, only Insert / Delete redlines will be handled.
+        for (auto[pRedline, visible] : m_aRedlineShowStateRestore)
+        {
+            if (visible)
+                Show(pRedline, m_rTable);
+            else
+                Hide(pRedline, m_rTable);
+        }
+    }
+
+private:
+    static std::unordered_map<SwRangeRedline*, bool>
+    collectRestoreData(const SwRedlineTable& rTable)
+    {
+        std::unordered_map<SwRangeRedline*, bool> aRedlineShowStateRestore;
+        for (auto pRedline : rTable)
+            aRedlineShowStateRestore[pRedline] = pRedline->IsVisible();
+        return aRedlineShowStateRestore;
+    }
+    static void Show(SwRangeRedline* pRedline, const SwRedlineTable& rTable)
+    {
+        if (pRedline->IsVisible())
+            return;
+        switch (pRedline->GetType())
+        {
+            case RedlineType::Insert:
+            case RedlineType::Delete:
+                pRedline->Show(0, rTable.GetPos(pRedline), true);
+                pRedline->Show(1, rTable.GetPos(pRedline), true);
+                break;
+            default:
+                assert(!"Trying to show a redline that is not expected to 
change visibility here");
+        }
+    }
+    static void Hide(SwRangeRedline* pRedline, const SwRedlineTable& rTable)
+    {
+        if (!pRedline->IsVisible())
+            return;
+        switch (pRedline->GetType())
+        {
+            case RedlineType::Insert:
+                pRedline->ShowOriginal(0, rTable.GetPos(pRedline));
+                pRedline->ShowOriginal(1, rTable.GetPos(pRedline));
+                break;
+            case RedlineType::Delete:
+                pRedline->Hide(0, rTable.GetPos(pRedline));
+                pRedline->Hide(1, rTable.GetPos(pRedline));
+                break;
+            default:
+                assert(!"Trying to hide a redline that is not expected to 
change visibility here");
+        }
+    }
+
+    const SwRedlineTable& m_rTable;
+    std::unordered_map<SwRangeRedline*, bool> m_aRedlineShowStateRestore;
+};
+
 /// Implements getCommandValues(".uno:ExtractDocumentStructures") for redlines
-void GetDocStructureTrackChanges(tools::JsonWriter& rJsonWriter, const 
SwDocShell* pDocShell,
+void GetDocStructureTrackChanges(tools::JsonWriter& rJsonWriter, SwDocShell* 
pDocShell,
                                  std::u16string_view filterArguments)
 {
     // filter arguments are separated from the filter name by comma, and are 
name:value pairs
@@ -878,16 +971,17 @@ void GetDocStructureTrackChanges(tools::JsonWriter& 
rJsonWriter, const SwDocShel
         // else unknown filter argument (maybe from a newer API?) - ignore
     }
 
-    auto xRedlinesEnum = 
pDocShell->GetBaseModel()->getRedlines()->createEnumeration();
-    for (sal_Int32 i = 0; xRedlinesEnum->hasMoreElements(); ++i)
+    SwDoc& rDoc = *pDocShell->GetDoc();
+    const SwRedlineTable& rTable = 
rDoc.getIDocumentRedlineAccess().GetRedlineTable();
+
+    for (size_t i = 0; i < rTable.size(); ++i)
     {
-        auto xRedlineProperties = 
xRedlinesEnum->nextElement().query<beans::XPropertySet>();
-        assert(xRedlineProperties);
+        rtl::Reference<SwXRedline> pSwXRedline(new SwXRedline(*rTable[i]));
 
         auto TrackChangesNode
             = rJsonWriter.startNode(Concat2View("TrackChanges.ByIndex." + 
OString::number(i)));
 
-        PropertyExtractor extractor{ xRedlineProperties, rJsonWriter };
+        PropertyExtractor extractor{ pSwXRedline, rJsonWriter };
 
         extractor.extract<OUString>(UNO_NAME_REDLINE_TYPE, "type");
         extractor.extract<css::util::DateTime>(UNO_NAME_REDLINE_DATE_TIME, 
"dateTime");
@@ -895,39 +989,44 @@ void GetDocStructureTrackChanges(tools::JsonWriter& 
rJsonWriter, const SwDocShel
         extractor.extract<OUString>(UNO_NAME_REDLINE_DESCRIPTION, 
"description");
         extractor.extract<OUString>(UNO_NAME_REDLINE_COMMENT, "comment");
 
-        auto xStart = 
xRedlineProperties->getPropertyValue(UNO_NAME_REDLINE_START)
-                          .query<css::text::XTextRange>();
-        auto xEnd = xRedlineProperties->getPropertyValue(UNO_NAME_REDLINE_END)
-                        .query<css::text::XTextRange>();
-        if (xStart)
-        {
-            auto xCursor = xStart->getText()->createTextCursorByRange(xStart);
-            xCursor->goLeft(nContextLen, /*bExpand*/ true);
-            rJsonWriter.put("textBefore", xCursor->getString());
-        }
-        if (xEnd)
-        {
-            auto xCursor = xEnd->getText()->createTextCursorByRange(xEnd);
-            xCursor->goRight(nContextLen, /*bExpand*/ true);
-            rJsonWriter.put("textAfter", xCursor->getString());
-        }
-        OUString changeText;
-        if (xStart && xEnd)
-        {
-            // Read the added / formatted text from the main XText
-            auto xCursor = xStart->getText()->createTextCursorByRange(xStart);
-            xCursor->gotoRange(xEnd, /*bExpand*/ true);
-            changeText = xCursor->getString();
-        }
-        if (changeText.isEmpty())
         {
-            // It is unlikely that we get here: the change text will be 
obtained above,
-            // even for deletion change
-            if (auto xRedlineText = 
xRedlineProperties->getPropertyValue(UNO_NAME_REDLINE_TEXT)
-                                        .query<css::text::XText>())
-                changeText = xRedlineText->getString();
+            // Set the text into a state according to current redline's 
timestamp: all older changes
+            // are shows as if accepted, all newer are shown as if rejected.
+            HideNewerShowOlder 
prepare(pSwXRedline->GetRedline()->GetTimeStamp(), rTable);
+            auto xStart = pSwXRedline->getPropertyValue(UNO_NAME_REDLINE_START)
+                              .query<css::text::XTextRange>();
+            auto xEnd = pSwXRedline->getPropertyValue(UNO_NAME_REDLINE_END)
+                            .query<css::text::XTextRange>();
+            if (xStart)
+            {
+                auto xCursor = 
xStart->getText()->createTextCursorByRange(xStart);
+                xCursor->goLeft(nContextLen, /*bExpand*/ true);
+                rJsonWriter.put("textBefore", xCursor->getString());
+            }
+            if (xEnd)
+            {
+                auto xCursor = xEnd->getText()->createTextCursorByRange(xEnd);
+                xCursor->goRight(nContextLen, /*bExpand*/ true);
+                rJsonWriter.put("textAfter", xCursor->getString());
+            }
+            OUString changeText;
+            if (xStart && xEnd)
+            {
+                // Read the added / formatted text from the main XText
+                auto xCursor = 
xStart->getText()->createTextCursorByRange(xStart);
+                xCursor->gotoRange(xEnd, /*bExpand*/ true);
+                changeText = xCursor->getString();
+            }
+            if (changeText.isEmpty())
+            {
+                // It is unlikely that we get here: the change text will be 
obtained above,
+                // even for deletion change
+                if (auto xRedlineText = 
pSwXRedline->getPropertyValue(UNO_NAME_REDLINE_TEXT)
+                                            .query<css::text::XText>())
+                    changeText = xRedlineText->getString();
+            }
+            rJsonWriter.put("textChanged", changeText); // write 
unconditionally
         }
-        rJsonWriter.put("textChanged", changeText); // write unconditionally
         // 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>
@@ -941,7 +1040,7 @@ void GetDocStructureTrackChanges(tools::JsonWriter& 
rJsonWriter, const SwDocShel
 /// Parameters:
 ///
 /// - filter: To filter what document structure types to extract
-void GetDocStructure(tools::JsonWriter& rJsonWriter, const SwDocShell* 
pDocShell,
+void GetDocStructure(tools::JsonWriter& rJsonWriter, SwDocShell* pDocShell,
                      const std::map<OUString, OUString>& rArguments)
 {
     auto commentsNode = rJsonWriter.startNode("DocStructure");

Reply via email to