schema/libreoffice/OpenDocument-v1.4+libreoffice-schema.rng |   11 +++
 sw/source/core/doc/docredln.cxx                             |   22 +++++++
 sw/source/core/unocore/unoredline.cxx                       |   21 ++++++-
 xmloff/qa/unit/data/del-then-format.docx                    |binary
 xmloff/qa/unit/text.cxx                                     |   21 +++++++
 xmloff/source/text/XMLRedlineExport.cxx                     |   34 +++++++++---
 6 files changed, 97 insertions(+), 12 deletions(-)

New commits:
commit 80139ffdd67f53535eaf784cdc2324b5fae9969d
Author:     Miklos Vajna <vmik...@collabora.com>
AuthorDate: Tue May 20 08:25:44 2025 +0200
Commit:     Caolán McNamara <caolan.mcnam...@collabora.com>
CommitDate: Tue May 20 09:35:41 2025 +0200

    tdf#166319 sw interdependent redlines: handle ODF export of delete under 
format
    
    The bugdoc has <del>AA<format>BB</format>CC</del> in it, DOCX import
    works, but exporting to ODT turns the format-on-delete redline into a
    plain "format" redline.
    
    What happens is that various code only checks the m_eType of the first
    SwRedlineData of an SwRangeRedline, so format-on-delete is handled as
    format. But this is not correct: if the format-on-delete is to be
    accepted, then it's just a small detail that there was a format on top
    of it: the result should be text deleted from the document on accept,
    not accepting the the format changes and leaving the text in the
    document.
    
    Fix the problem by:
    1) Extending SwRangeRedline::Hide() and SwRangeRedline::Show(), so when
       the ODF export moves delete redlines to their own toplevel SwNode
       section, so it can refer to these ranges using a single SwStartNode
       index, this move happens for format-on-delete as well, not just deletes.
    2) Then can extend SwXRedlinePortion::GetPropertyValue()'s
       lcl_GetSuccessorProperties() to produce an XText for the deleted
       range of a format-on-delete redline.
    3) Finally consume this new XText in
       XMLRedlineExport::ExportChangeInfo().
    
    Note that this way a format redline can now have its own XText (in case
    it's on top of a delete), but we want to export the text only once, so
    restrict the content export for a redline to the 'delete' type.
    
    Change-Id: I91902a2ae4b464ace41717a196d19530be9d84d1
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/185548
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com>
    Reviewed-by: Caolán McNamara <caolan.mcnam...@collabora.com>

diff --git a/schema/libreoffice/OpenDocument-v1.4+libreoffice-schema.rng 
b/schema/libreoffice/OpenDocument-v1.4+libreoffice-schema.rng
index 6a8e3149c66e..10023519509e 100644
--- a/schema/libreoffice/OpenDocument-v1.4+libreoffice-schema.rng
+++ b/schema/libreoffice/OpenDocument-v1.4+libreoffice-schema.rng
@@ -4034,4 +4034,15 @@ 
xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.
       </rng:attribute>
       </rng:optional>
   </rng:define>
+
+  <!-- TODO(vmiklos) no proposal for insert/delete redline under another 
redline -->
+  <rng:define name="text-changed-region" combine="choice">
+    <rng:element name="text:changed-region">
+      <rng:ref name="text-changed-region-attr"/>
+      <rng:ref name="text-changed-region-content"/>
+      <rng:optional>
+        <rng:ref name="text-changed-region-content"/>
+      </rng:optional>
+    </rng:element>
+  </rng:define>
 </rng:grammar>
diff --git a/sw/source/core/doc/docredln.cxx b/sw/source/core/doc/docredln.cxx
index b48c9bbd44e8..ada8f45d34c1 100644
--- a/sw/source/core/doc/docredln.cxx
+++ b/sw/source/core/doc/docredln.cxx
@@ -1590,6 +1590,24 @@ void SwRangeRedline::CallDisplayFunc(size_t nMyPos)
         ShowOriginal(0, nMyPos);
 }
 
+namespace
+{
+RedlineType GetRedlineTypeIgnoringAdditonalFormat(const SwRangeRedline& 
rRedline)
+{
+    RedlineType eType = rRedline.GetType();
+
+    if (eType == RedlineType::Format && rRedline.GetStackCount() > 1
+        && rRedline.GetType(1) == RedlineType::Delete)
+    {
+        // Consider format-on-delete the same as simple delete, so the range 
gets moved to the
+        // "Deleted Change Tracking content" toplevel section from body 
content during file save.
+        eType = RedlineType::Delete;
+    }
+
+    return eType;
+}
+}
+
 void SwRangeRedline::Show(sal_uInt16 nLoop, size_t nMyPos, bool bForced)
 {
     SwDoc& rDoc = GetDoc();
@@ -1611,7 +1629,7 @@ void SwRangeRedline::Show(sal_uInt16 nLoop, size_t 
nMyPos, bool bForced)
     rDoc.getIDocumentRedlineAccess().SetRedlineFlags_intern(eOld | 
RedlineFlags::Ignore);
     ::sw::UndoGuard const undoGuard(rDoc.GetIDocumentUndoRedo());
 
-    switch( GetType() )
+    switch (GetRedlineTypeIgnoringAdditonalFormat(*this))
     {
     case RedlineType::Insert:           // Content has been inserted
         m_bIsVisible = true;
@@ -1651,7 +1669,7 @@ void SwRangeRedline::Hide(sal_uInt16 nLoop, size_t 
nMyPos, bool /*bForced*/)
     rDoc.getIDocumentRedlineAccess().SetRedlineFlags_intern(eOld | 
RedlineFlags::Ignore);
     ::sw::UndoGuard const undoGuard(rDoc.GetIDocumentUndoRedo());
 
-    switch( GetType() )
+    switch (GetRedlineTypeIgnoringAdditonalFormat(*this))
     {
     case RedlineType::Insert:           // Content has been inserted
         m_bIsVisible = true;
diff --git a/sw/source/core/unocore/unoredline.cxx 
b/sw/source/core/unocore/unoredline.cxx
index 414bebbc1056..76a95fdcffe7 100644
--- a/sw/source/core/unocore/unoredline.cxx
+++ b/sw/source/core/unocore/unoredline.cxx
@@ -176,6 +176,22 @@ static uno::Sequence<beans::PropertyValue> 
lcl_GetSuccessorProperties(const SwRa
     const SwRedlineData* pNext = rRedline.GetRedlineData().Next();
     if(pNext)
     {
+        uno::Reference<text::XText> xRedlineText;
+        if (pNext->GetType() == RedlineType::Delete)
+        {
+            // Something on delete: produce the XText for the underlying 
delete.
+            const SwNodeIndex* pNodeIdx = rRedline.GetContentIdx();
+            if (pNodeIdx
+                && (pNodeIdx->GetNode().EndOfSectionIndex() - 
pNodeIdx->GetNode().GetIndex())
+                       > SwNodeOffset(1))
+            {
+                // We have at least one node between the start and end node, 
create the
+                // SwXRedlineText.
+                SwDoc& rDoc = rRedline.GetDoc();
+                xRedlineText = new SwXRedlineText(&rDoc, *pNodeIdx);
+            }
+        }
+
         return
         {
             // GetAuthorString(n) walks the SwRedlineData* chain;
@@ -183,10 +199,11 @@ static uno::Sequence<beans::PropertyValue> 
lcl_GetSuccessorProperties(const SwRa
             comphelper::makePropertyValue(UNO_NAME_REDLINE_AUTHOR, 
rRedline.GetAuthorString(1)),
             comphelper::makePropertyValue(UNO_NAME_REDLINE_DATE_TIME, 
pNext->GetTimeStamp().GetUNODateTime()),
             comphelper::makePropertyValue(UNO_NAME_REDLINE_COMMENT, 
pNext->GetComment()),
-            comphelper::makePropertyValue(UNO_NAME_REDLINE_TYPE, 
SwRedlineTypeToOUString(pNext->GetType()))
+            comphelper::makePropertyValue(UNO_NAME_REDLINE_TYPE, 
SwRedlineTypeToOUString(pNext->GetType())),
+            comphelper::makePropertyValue(UNO_NAME_REDLINE_TEXT, xRedlineText)
         };
     }
-    return uno::Sequence<beans::PropertyValue>(4);
+    return uno::Sequence<beans::PropertyValue>(5);
 }
 
 uno::Any SwXRedlinePortion::getPropertyValue( const OUString& rPropertyName )
diff --git a/xmloff/qa/unit/data/del-then-format.docx 
b/xmloff/qa/unit/data/del-then-format.docx
new file mode 100644
index 000000000000..f458578f5568
Binary files /dev/null and b/xmloff/qa/unit/data/del-then-format.docx differ
diff --git a/xmloff/qa/unit/text.cxx b/xmloff/qa/unit/text.cxx
index 2ef9e2673360..ce11001b12cd 100644
--- a/xmloff/qa/unit/text.cxx
+++ b/xmloff/qa/unit/text.cxx
@@ -1286,6 +1286,27 @@ CPPUNIT_TEST_FIXTURE(XmloffStyleTest, testNestedSpans)
     CPPUNIT_ASSERT_EQUAL(awt::FontWeight::BOLD, fWeight);
 }
 
+CPPUNIT_TEST_FIXTURE(XmloffStyleTest, testDeleteThenFormatOdtExport)
+{
+    // Given a document that has a delete redline, and then a format redline 
on top of it:
+    loadFromFile(u"del-then-format.docx");
+
+    // When exporting that to DOCX:
+    save(u"writer8"_ustr);
+
+    // Then make sure <text:changed-region> not only has the 
<text:format-change> child, but also an
+    // <text:deletion> one, with text:
+    xmlDocUniquePtr pXmlDoc = parseExport(u"content.xml"_ustr);
+    // Without the accompanying fix in place, this test would have failed with:
+    // - Expected: 1
+    // - Actual  : 0
+    // - XPath '//text:tracked-changes/text:changed-region[2]/text:deletion' 
number of nodes is incorrect
+    // i.e. <text:insertion> was written for a delete-then-format.
+    assertXPath(pXmlDoc, 
"//text:tracked-changes/text:changed-region[2]/text:deletion");
+    // Also the delete's text was in the body text.
+    assertXPath(pXmlDoc, 
"//text:tracked-changes/text:changed-region[2]/text:deletion/text:p");
+}
+
 CPPUNIT_PLUGIN_IMPLEMENT();
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/xmloff/source/text/XMLRedlineExport.cxx 
b/xmloff/source/text/XMLRedlineExport.cxx
index 093040c3b15b..34e3ae5c5264 100644
--- a/xmloff/source/text/XMLRedlineExport.cxx
+++ b/xmloff/source/text/XMLRedlineExport.cxx
@@ -43,6 +43,7 @@
 #include <xmloff/xmlexp.hxx>
 #include <xmloff/xmluconv.hxx>
 #include <unotools/securityoptions.hxx>
+#include <comphelper/sequenceashashmap.hxx>
 
 
 using namespace ::com::sun::star;
@@ -361,7 +362,8 @@ void XMLRedlineExport::ExportChangedRegion(
         aAny = rPropSet->getPropertyValue(u"RedlineText"_ustr);
         Reference<XText> xText;
         aAny >>= xText;
-        if (xText.is())
+        // Avoid double export for format-on-delete: no write on format, only 
on delete.
+        if (xText.is() && sType == u"Delete")
         {
             rExport.GetTextParagraphExport()->exportText(xText);
             // default parameters: bProgress, bExportParagraph ???
@@ -380,15 +382,31 @@ void XMLRedlineExport::ExportChangedRegion(
     // process change info
     if (aSuccessorData.hasElements())
     {
-        // The only change that can be "undone" is an insertion -
-        // after all, you can't re-insert a deletion, but you can
-        // delete an insertion. This assumption is asserted in
-        // ExportChangeInfo(Sequence<PropertyValue>&).
+        // Look up the type of the change. In practice this is always insert 
or delete, other types
+        // can't have additional redlines on top of them.
+        OUString sType;
+        comphelper::SequenceAsHashMap aMap(aSuccessorData);
+        auto it = aMap.find("RedlineType");
+        if (it != aMap.end())
+        {
+            it->second >>= sType;
+        }
         SvXMLElementExport aSecondChangeElem(
-            rExport, XML_NAMESPACE_TEXT, XML_INSERTION,
+            rExport, XML_NAMESPACE_TEXT, ConvertTypeName(sType),
             true, true);
 
         ExportChangeInfo(aSuccessorData);
+        it = aMap.find("RedlineText");
+        if (it != aMap.end())
+        {
+            // Delete has its own content outside the body text: export it 
here.
+            uno::Reference<text::XText> xText;
+            it->second >>= xText;
+            if (xText.is())
+            {
+                rExport.GetTextParagraphExport()->exportText(xText);
+            }
+        }
     }
     // else: no hierarchical change
 }
@@ -529,10 +547,10 @@ void XMLRedlineExport::ExportChangeInfo(
         }
         else if( rVal.Name == "RedlineType" )
         {
-            // check if this is an insertion; cf. comment at calling location
+            // check if this has one of the expected types
             OUString sTmp;
             rVal.Value >>= sTmp;
-            DBG_ASSERT(sTmp == "Insert",
+            DBG_ASSERT(sTmp == "Insert" || sTmp == "Delete",
                        "hierarchical change must be insertion");
         }
         // else: unknown value -> ignore

Reply via email to