tdf#145720 DOCX export: fix loss of tracked moving

of documents created in MSO to keep interoperability.

Export moved redlines as moveFrom/moveTo instead of
del/ins elements (also for newly created tracked moving).

Export "MoveBookmark" elements moveFromRangeStart,
moveFromRangeEnd, moveToRangeStart, moveToRangeEnd, which
imported from DOCX documents created in MSO. Without them,
moveFrom/moveTo elements were imported as plain deletion
or insertion in MSO.

Note: MoveBookmark elements were imported and exported as
collapsed plain bookmarks. Now keep their ranges, also store
the information of moveFrom/moveTo for correct export.
In the export filter, mandatory author and date of the tracking
information restored from RedlineData of the first redline
within the MoveBookmark, if it's possible.

Follow-up to commit f51fa7534421a195a58b4a737a2e836d8c25ba81
"tdf#145718 sw, DOCX import: complete tracked text moving".

Change-Id: I54242453a7f7d8f73ea074fc74e8e7bc86d07d01
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/126258
Tested-by: László Németh <nemeth@numbertext.org>
Reviewed-by: László Németh <nemeth@numbertext.org>
(cherry picked from commit 9e1e88ad5cf2dc0e9b188c60930445652a6c7519,
commit bbb09ebda08fec0702de0fb50dbe630acf73af2f and
commit 3efde47ec9ee091479c04129696f99dc934c3f64)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/126290
Tested-by: Jenkins
diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport11.cxx b/sw/qa/extras/ooxmlexport/ooxmlexport11.cxx
index 8cb43ff..5fae2da 100644
--- a/sw/qa/extras/ooxmlexport/ooxmlexport11.cxx
+++ b/sw/qa/extras/ooxmlexport/ooxmlexport11.cxx
@@ -885,9 +885,9 @@ DECLARE_OOXMLEXPORT_TEST(testTdf104797, "tdf104797.docx")
    // check moveFrom and moveTo
    CPPUNIT_ASSERT_EQUAL( OUString( "Will this sentence be duplicated?" ), getParagraph( 1 )->getString());
    CPPUNIT_ASSERT_EQUAL( OUString( "" ), getRun( getParagraph( 1 ), 1 )->getString());
    CPPUNIT_ASSERT(hasProperty(getRun(getParagraph(1), 2), "RedlineType"));
    CPPUNIT_ASSERT_EQUAL(OUString("Delete"),getProperty<OUString>(getRun(getParagraph(1), 2), "RedlineType"));
    CPPUNIT_ASSERT_EQUAL(true,getProperty<bool>(getRun(getParagraph(1), 2), "IsStart"));
    CPPUNIT_ASSERT(hasProperty(getRun(getParagraph(1), 3), "RedlineType"));
    CPPUNIT_ASSERT_EQUAL(OUString("Delete"),getProperty<OUString>(getRun(getParagraph(1), 3), "RedlineType"));
    CPPUNIT_ASSERT_EQUAL(true,getProperty<bool>(getRun(getParagraph(1), 3), "IsStart"));
    CPPUNIT_ASSERT_EQUAL( OUString( "This is a filler sentence. Will this sentence be duplicated ADDED STUFF?" ),
            getParagraph( 2 )->getString());
    CPPUNIT_ASSERT_EQUAL( OUString( "" ), getRun( getParagraph( 2 ), 1 )->getString());
@@ -897,23 +897,39 @@ DECLARE_OOXMLEXPORT_TEST(testTdf104797, "tdf104797.docx")
    CPPUNIT_ASSERT_EQUAL(OUString("Insert"),getProperty<OUString>(getRun(getParagraph(2), 3), "RedlineType"));
    CPPUNIT_ASSERT_EQUAL(true,getProperty<bool>(getRun(getParagraph(2), 3), "IsStart"));

    CPPUNIT_ASSERT_EQUAL( OUString( " " ), getRun( getParagraph( 2 ), 4 )->getString());
    CPPUNIT_ASSERT_EQUAL( OUString( "" ), getRun( getParagraph( 2 ), 5 )->getString());
    CPPUNIT_ASSERT(hasProperty(getRun(getParagraph(2), 6), "RedlineType"));
    CPPUNIT_ASSERT_EQUAL(OUString("Insert"),getProperty<OUString>(getRun(getParagraph(2), 6), "RedlineType"));
    CPPUNIT_ASSERT_EQUAL( OUString( "" ), getRun( getParagraph( 2 ), 7 )->getString());
    CPPUNIT_ASSERT(hasProperty(getRun(getParagraph(2), 7), "RedlineType"));
    CPPUNIT_ASSERT_EQUAL(true,getProperty<bool>(getRun(getParagraph(2), 7), "IsStart"));
    CPPUNIT_ASSERT_EQUAL( OUString( "Will this sentence be duplicated" ), getRun( getParagraph( 2 ), 8 )->getString());
    CPPUNIT_ASSERT_EQUAL( OUString( " ADDED STUFF" ), getRun( getParagraph( 2 ), 11 )->getString());
    CPPUNIT_ASSERT_EQUAL( OUString( "?" ), getRun( getParagraph( 2 ), 14 )->getString());
}

DECLARE_OOXMLEXPORT_TEST(testTdf145720, "tdf104797.docx")
{
    // check moveFromRangeStart/End and moveToRangeStart/End (to keep tracked text moving)
    xmlDocUniquePtr pXmlDoc = parseExport("word/document.xml");
    if (mbExported)
    {
        // TODO fix export of moved text
        CPPUNIT_ASSERT_EQUAL( OUString( " Will this sentence be duplicated ADDED STUFF?" ), getRun( getParagraph( 2 ), 4 )->getString());
    }
    else
    {
        CPPUNIT_ASSERT_EQUAL( OUString( " " ), getRun( getParagraph( 2 ), 4 )->getString());
        CPPUNIT_ASSERT_EQUAL( OUString( "" ), getRun( getParagraph( 2 ), 5 )->getString());
        CPPUNIT_ASSERT(hasProperty(getRun(getParagraph(2), 5), "RedlineType"));
        CPPUNIT_ASSERT_EQUAL(OUString("Insert"),getProperty<OUString>(getRun(getParagraph(2), 5), "RedlineType"));
        CPPUNIT_ASSERT_EQUAL( OUString( "" ), getRun( getParagraph( 2 ), 6 )->getString());
        CPPUNIT_ASSERT(hasProperty(getRun(getParagraph(2), 6), "RedlineType"));
        CPPUNIT_ASSERT_EQUAL(true,getProperty<bool>(getRun(getParagraph(2), 6), "IsStart"));
        CPPUNIT_ASSERT_EQUAL( OUString( "Will this sentence be duplicated" ), getRun( getParagraph( 2 ), 7 )->getString());
        CPPUNIT_ASSERT_EQUAL( OUString( " ADDED STUFF" ), getRun( getParagraph( 2 ), 10 )->getString());
        CPPUNIT_ASSERT_EQUAL( OUString( "?" ), getRun( getParagraph( 2 ), 13 )->getString());
        // These were 0 (missing move*FromRange* elements)
        assertXPath(pXmlDoc, "/w:document/w:body/w:p[1]/w:moveFrom/w:moveFromRangeStart", 1);
        assertXPath(pXmlDoc, "/w:document/w:body/w:p[1]/w:moveFromRangeEnd", 1);
        assertXPath(pXmlDoc, "/w:document/w:body/w:p[2]/w:moveTo/w:moveToRangeStart", 1);
        assertXPath(pXmlDoc, "/w:document/w:body/w:p[2]/w:moveToRangeEnd", 1);

        // paired names
        assertXPath(pXmlDoc, "/w:document/w:body/w:p[1]/w:moveFrom/w:moveFromRangeStart", "name", "move471382752");
        assertXPath(pXmlDoc, "/w:document/w:body/w:p[2]/w:moveTo/w:moveToRangeStart", "name", "move471382752");

        // mandatory authors and dates
        assertXPath(pXmlDoc, "/w:document/w:body/w:p[1]/w:moveFrom/w:moveFromRangeStart", "author", u"Tekijä");
        assertXPath(pXmlDoc, "/w:document/w:body/w:p[2]/w:moveTo/w:moveToRangeStart", "author", u"Tekijä");
        assertXPath(pXmlDoc, "/w:document/w:body/w:p[1]/w:moveFrom/w:moveFromRangeStart", "date", "0-00-00T00:00:00Z");
        assertXPath(pXmlDoc, "/w:document/w:body/w:p[2]/w:moveTo/w:moveToRangeStart", "date", "0-00-00T00:00:00Z");
    }
}

@@ -1113,8 +1129,21 @@ CPPUNIT_TEST_FIXTURE(Test, testTdf132271)
    loadAndSave("tdf132271.docx");
    xmlDocUniquePtr pXmlDoc = parseExport("word/document.xml");
    // import change tracking in floating tables
    assertXPath(pXmlDoc, "//w:del", 2);
    assertXPath(pXmlDoc, "//w:ins", 2);
    if (!mbExported)
    {
        assertXPath(pXmlDoc, "//w:del", 2);
        assertXPath(pXmlDoc, "//w:ins", 2);
        assertXPath(pXmlDoc, "//w:moveFrom", 0);
        assertXPath(pXmlDoc, "//w:moveTo", 0);
    }
    else
    {
        assertXPath(pXmlDoc, "//w:del", 1);
        assertXPath(pXmlDoc, "//w:ins", 1);
        // tracked text moving recognized during the import
        assertXPath(pXmlDoc, "//w:moveFrom", 1);
        assertXPath(pXmlDoc, "//w:moveTo", 1);
    }
}

CPPUNIT_TEST_FIXTURE(Test, testTdf136667)
@@ -1122,8 +1151,21 @@ CPPUNIT_TEST_FIXTURE(Test, testTdf136667)
    loadAndSave("tdf136667.docx");
    xmlDocUniquePtr pXmlDoc = parseExport("word/document.xml");
    // import change tracking in floating tables
    assertXPath(pXmlDoc, "//w:del", 2);
    assertXPath(pXmlDoc, "//w:ins", 4);
    if (!mbExported)
    {
        assertXPath(pXmlDoc, "//w:del", 2);
        assertXPath(pXmlDoc, "//w:ins", 4);
        assertXPath(pXmlDoc, "//w:moveFrom", 0);
        assertXPath(pXmlDoc, "//w:moveTo", 0);
    }
    else
    {
        assertXPath(pXmlDoc, "//w:del", 1);
        assertXPath(pXmlDoc, "//w:ins", 3);
        // tracked text moving recognized during the import
        assertXPath(pXmlDoc, "//w:moveFrom", 1);
        assertXPath(pXmlDoc, "//w:moveTo", 1);
    }
}

CPPUNIT_TEST_FIXTURE(Test, testTdf136850)
diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport13.cxx b/sw/qa/extras/ooxmlexport/ooxmlexport13.cxx
index 86473c1..6600928 100644
--- a/sw/qa/extras/ooxmlexport/ooxmlexport13.cxx
+++ b/sw/qa/extras/ooxmlexport/ooxmlexport13.cxx
@@ -763,24 +763,13 @@ DECLARE_OOXMLEXPORT_TEST(testTdf123460, "tdf123460.docx")
    // check paragraph mark deletion at terminating moveFrom
    CPPUNIT_ASSERT_EQUAL(true,getParagraph( 2 )->getString().startsWith("Nunc"));
    CPPUNIT_ASSERT_EQUAL( OUString( "" ), getRun( getParagraph( 2 ), 1 )->getString());
    CPPUNIT_ASSERT(hasProperty(getRun(getParagraph(2), 1), "RedlineType"));
    CPPUNIT_ASSERT_EQUAL(OUString("Delete"),getProperty<OUString>(getRun(getParagraph(2), 1), "RedlineType"));
    CPPUNIT_ASSERT_EQUAL(true, getRun( getParagraph( 2 ), 2 )->getString().endsWith("tellus."));
    CPPUNIT_ASSERT_EQUAL( OUString( "" ), getRun( getParagraph( 2 ), 3 )->getString());
    if (mbExported)
    {
        // TODO fix export of moved text
        bool bCaught = false;
        try
        {
            getRun( getParagraph( 2 ), 4 );
        }
        catch (container::NoSuchElementException&)
        {
            bCaught = true;
        }
        CPPUNIT_ASSERT_EQUAL(true, bCaught);
    }
    CPPUNIT_ASSERT(hasProperty(getRun(getParagraph(2), 2), "RedlineType"));
    CPPUNIT_ASSERT_EQUAL(OUString("Delete"),getProperty<OUString>(getRun(getParagraph(2), 2), "RedlineType"));
    CPPUNIT_ASSERT_EQUAL(true, getRun( getParagraph( 2 ), 3 )->getString().endsWith("tellus."));
    // deleted paragraph mark at the end of the second paragraph
    CPPUNIT_ASSERT(hasProperty(getRun(getParagraph(2), 5), "RedlineType"));
    CPPUNIT_ASSERT_EQUAL(OUString("Delete"),getProperty<OUString>(getRun(getParagraph(2), 5), "RedlineType"));
    CPPUNIT_ASSERT_EQUAL( OUString( "" ), getRun( getParagraph( 2 ), 6 )->getString());
}

//tdf#125298: fix charlimit restrictions in bookmarknames and field references if they contain non-ascii characters
diff --git a/sw/source/filter/ww8/docxattributeoutput.cxx b/sw/source/filter/ww8/docxattributeoutput.cxx
index 51a8ac1..0aa0336 100644
--- a/sw/source/filter/ww8/docxattributeoutput.cxx
+++ b/sw/source/filter/ww8/docxattributeoutput.cxx
@@ -1720,7 +1720,7 @@ void DocxAttributeOutput::EndRun(const SwTextNode* pNode, sal_Int32 nPos, bool /
    // XML_r node should be surrounded with bookmark-begin and bookmark-end nodes if it has bookmarks.
    // The same is applied for permission ranges.
    // But due to unit test "testFdo85542" let's output bookmark-begin with bookmark-end.
    DoWriteBookmarksStart(m_rBookmarksStart);
    DoWriteBookmarksStart(m_rBookmarksStart, m_pMoveRedlineData);
    DoWriteBookmarksEnd(m_rBookmarksEnd);
    DoWritePermissionsStart();
    DoWriteAnnotationMarks();
@@ -1905,6 +1905,29 @@ void DocxAttributeOutput::DoWriteBookmarkTagEnd(sal_Int32 const nId)
        FSNS(XML_w, XML_id), OString::number(nId));
}

void DocxAttributeOutput::DoWriteMoveRangeTagStart(const OString & bookmarkName,
    bool bFrom, const SwRedlineData* pRedlineData)
{
    const OUString &rAuthor( SW_MOD()->GetRedlineAuthor( pRedlineData->GetAuthor() ) );
    OString aDate( DateTimeToOString( pRedlineData->GetTimeStamp() ) );

    m_pSerializer->singleElementNS(XML_w, bFrom
                ? XML_moveFromRangeStart
                : XML_moveToRangeStart,
        FSNS(XML_w, XML_id), OString::number(m_nNextBookmarkId),
        FSNS(XML_w, XML_author ), OUStringToOString(rAuthor, RTL_TEXTENCODING_UTF8),
        FSNS(XML_w, XML_date ), aDate,
        FSNS(XML_w, XML_name), bookmarkName);
}

void DocxAttributeOutput::DoWriteMoveRangeTagEnd(sal_Int32 const nId, bool bFrom)
{
    m_pSerializer->singleElementNS(XML_w, bFrom
            ? XML_moveFromRangeEnd
            : XML_moveToRangeEnd,
        FSNS(XML_w, XML_id), OString::number(nId));
}

void DocxAttributeOutput::DoWriteBookmarkStartIfExist(sal_Int32 nRunPos)
{
    auto aRange = m_aBookmarksOfParagraphStart.equal_range(nRunPos);
@@ -1934,15 +1957,29 @@ void DocxAttributeOutput::DoWriteBookmarkEndIfExist(sal_Int32 nRunPos)
}

/// Write the start bookmarks
void DocxAttributeOutput::DoWriteBookmarksStart(std::vector<OUString>& rStarts)
void DocxAttributeOutput::DoWriteBookmarksStart(std::vector<OUString>& rStarts, const SwRedlineData* pRedlineData)
{
    for (const OUString & bookmarkName : rStarts)
    {
        // Output the bookmark
        DoWriteBookmarkTagStart(bookmarkName);
        // Output the bookmark (including MoveBookmark of the tracked moving)
        bool bMove = false;
        bool bFrom = false;
        OString sBookmarkName = OUStringToOString(
                BookmarkToWord(bookmarkName, &bMove, &bFrom), RTL_TEXTENCODING_UTF8);
        if ( bMove )
        {
            // TODO: redline data of MoveBookmark is restored from the first redline of the bookmark
            // range. But a later deletion within a tracked moving is still imported as plain
            // deletion, so check IsMoved() and skip the export of the tracked moving to avoid
            // export with bad author or date
            if ( pRedlineData && pRedlineData->IsMoved() )
                DoWriteMoveRangeTagStart(sBookmarkName, bFrom, pRedlineData);
        }
        else
            DoWriteBookmarkTagStart(bookmarkName);

        m_rOpenedBookmarksIds[bookmarkName] = m_nNextBookmarkId;
        m_sLastOpenedBookmark = OUStringToOString(BookmarkToWord(bookmarkName), RTL_TEXTENCODING_UTF8);
        m_sLastOpenedBookmark = sBookmarkName;
        m_nNextBookmarkId++;
    }
    rStarts.clear();
@@ -1955,10 +1992,17 @@ void DocxAttributeOutput::DoWriteBookmarksEnd(std::vector<OUString>& rEnds)
    {
        // Get the id of the bookmark
        auto pPos = m_rOpenedBookmarksIds.find(bookmarkName);

        if (pPos != m_rOpenedBookmarksIds.end())
        {
            // Output the bookmark
            DoWriteBookmarkTagEnd(pPos->second);
            bool bMove = false;
            bool bFrom = false;
            BookmarkToWord(bookmarkName, &bMove, &bFrom);
            // Output the bookmark (including MoveBookmark of the tracked moving)
            if ( bMove )
                DoWriteMoveRangeTagEnd(pPos->second, bFrom);
            else
                DoWriteBookmarkTagEnd(pPos->second);

            m_rOpenedBookmarksIds.erase(bookmarkName);
        }
@@ -3109,10 +3153,13 @@ void DocxAttributeOutput::RunText( const OUString& rText, rtl_TextEncoding /*eCh
    const sal_Unicode *pBegin = rText.getStr();
    const sal_Unicode *pEnd = pBegin + rText.getLength();

    // the text run is usually XML_t, with the exception of the deleted text
    // the text run is usually XML_t, with the exception of the deleted (and not moved) text
    sal_Int32 nTextToken = XML_t;
    if ( m_pRedlineData && m_pRedlineData->GetType() == RedlineType::Delete )
    if ( m_pRedlineData && !m_pRedlineData->IsMoved() &&
            m_pRedlineData->GetType() == RedlineType::Delete )
    {
        nTextToken = XML_delText;
    }

    sal_Unicode prevUnicode = *pBegin;

@@ -3503,17 +3550,18 @@ void DocxAttributeOutput::StartRedline( const SwRedlineData * pRedlineData )
            ? DateTime(Date( 1, 1, 1970 )) // Epoch time
            : pRedlineData->GetTimeStamp() ) );

    bool bMoved = pRedlineData->IsMoved();
    switch ( pRedlineData->GetType() )
    {
        case RedlineType::Insert:
            m_pSerializer->startElementNS( XML_w, XML_ins,
            m_pSerializer->startElementNS( XML_w, bMoved ? XML_moveTo : XML_ins,
                    FSNS( XML_w, XML_id ), aId,
                    FSNS( XML_w, XML_author ), aAuthor,
                    FSNS( XML_w, XML_date ), aDate );
            break;

        case RedlineType::Delete:
            m_pSerializer->startElementNS( XML_w, XML_del,
            m_pSerializer->startElementNS( XML_w, bMoved ? XML_moveFrom : XML_del,
                    FSNS( XML_w, XML_id ), aId,
                    FSNS( XML_w, XML_author ), aAuthor,
                    FSNS( XML_w, XML_date ), aDate );
@@ -3532,14 +3580,15 @@ void DocxAttributeOutput::EndRedline( const SwRedlineData * pRedlineData )
    if ( !pRedlineData || m_bWritingField )
        return;

    bool bMoved = pRedlineData->IsMoved();
    switch ( pRedlineData->GetType() )
    {
        case RedlineType::Insert:
            m_pSerializer->endElementNS( XML_w, XML_ins );
            m_pSerializer->endElementNS( XML_w, bMoved ? XML_moveTo : XML_ins );
            break;

        case RedlineType::Delete:
            m_pSerializer->endElementNS( XML_w, XML_del );
            m_pSerializer->endElementNS( XML_w, bMoved ? XML_moveFrom : XML_del );
            break;

        case RedlineType::Format:
@@ -8493,7 +8542,7 @@ void DocxAttributeOutput::WriteFormData_Impl( const ::sw::mark::IFieldmark& rFie
        m_Fields.begin()->pFieldmark = &rFieldmark;
}

void DocxAttributeOutput::WriteBookmarks_Impl( std::vector< OUString >& rStarts, std::vector< OUString >& rEnds )
void DocxAttributeOutput::WriteBookmarks_Impl( std::vector< OUString >& rStarts, std::vector< OUString >& rEnds, const SwRedlineData* pRedlineData )
{
    for ( const OUString & name : rStarts )
    {
@@ -8505,6 +8554,7 @@ void DocxAttributeOutput::WriteBookmarks_Impl( std::vector< OUString >& rStarts,
        else
        {
            m_rBookmarksStart.push_back(name);
            m_pMoveRedlineData = const_cast<SwRedlineData*>(pRedlineData);
        }
    }
    rStarts.clear();
@@ -10134,6 +10184,7 @@ DocxAttributeOutput::DocxAttributeOutput( DocxExport &rExport, const FSHelperPtr
      m_nNextBookmarkId( 0 ),
      m_nNextAnnotationMarkId( 0 ),
      m_nEmbedFlyLevel(0),
      m_pMoveRedlineData(nullptr),
      m_pCurrentFrame( nullptr ),
      m_bParagraphOpened( false ),
      m_bParagraphFrameOpen( false ),
diff --git a/sw/source/filter/ww8/docxattributeoutput.hxx b/sw/source/filter/ww8/docxattributeoutput.hxx
index 3897073..f053326 100644
--- a/sw/source/filter/ww8/docxattributeoutput.hxx
+++ b/sw/source/filter/ww8/docxattributeoutput.hxx
@@ -28,6 +28,7 @@
#include <IMark.hxx>
#include "docxexport.hxx"
#include <wrtswtbl.hxx>
#include <redline.hxx>

#include <editeng/boxitem.hxx>
#include <sax/fshelper.hxx>
@@ -393,7 +394,7 @@ public:
            OUString const* pBookmarkName = nullptr);
    void WriteFormData_Impl( const ::sw::mark::IFieldmark& rFieldmark );

    void WriteBookmarks_Impl( std::vector< OUString >& rStarts, std::vector< OUString >& rEnds );
    void WriteBookmarks_Impl( std::vector< OUString >& rStarts, std::vector< OUString >& rEnds, const SwRedlineData* pRedlineData = nullptr );
    void WriteFinalBookmarks_Impl( std::vector< OUString >& rStarts, std::vector< OUString >& rEnds );
    void WriteAnnotationMarks_Impl( std::vector< OUString >& rStarts, std::vector< OUString >& rEnds );
    void PushRelIdCache();
@@ -729,7 +730,10 @@ private:

    void DoWriteBookmarkTagStart(const OUString & bookmarkName);
    void DoWriteBookmarkTagEnd(sal_Int32 nId);
    void DoWriteBookmarksStart(std::vector<OUString>& rStarts);
    void DoWriteMoveRangeTagStart(const OString & bookmarkName,
            bool bFrom, const SwRedlineData* pRedlineData);
    void DoWriteMoveRangeTagEnd(sal_Int32 nId, bool bFrom);
    void DoWriteBookmarksStart(std::vector<OUString>& rStarts, const SwRedlineData* pRedlineData = nullptr);
    void DoWriteBookmarksEnd(std::vector<OUString>& rEnds);
    void DoWriteBookmarkStartIfExist(sal_Int32 nRunPos);
    void DoWriteBookmarkEndIfExist(sal_Int32 nRunPos);
@@ -829,6 +833,7 @@ private:
    /// Bookmarks to output
    std::vector<OUString> m_rBookmarksStart;
    std::vector<OUString> m_rBookmarksEnd;
    SwRedlineData* m_pMoveRedlineData;

    /// Bookmarks to output at the end
    std::vector<OUString> m_rFinalBookmarksStart;
diff --git a/sw/source/filter/ww8/docxexport.cxx b/sw/source/filter/ww8/docxexport.cxx
index 3d5fc3f..e2fc8f5 100644
--- a/sw/source/filter/ww8/docxexport.cxx
+++ b/sw/source/filter/ww8/docxexport.cxx
@@ -66,6 +66,7 @@
#include <ftninfo.hxx>
#include <pagedesc.hxx>
#include <poolfmt.hxx>
#include <redline.hxx>
#include <swdbdata.hxx>

#include <editeng/unoprnms.hxx>
@@ -145,7 +146,7 @@ bool DocxExport::CollapseScriptsforWordOk( sal_uInt16 nScript, sal_uInt16 nWhich
    return true;
}

void DocxExport::AppendBookmarks( const SwTextNode& rNode, sal_Int32 nCurrentPos, sal_Int32 nLen )
void DocxExport::AppendBookmarks( const SwTextNode& rNode, sal_Int32 nCurrentPos, sal_Int32 nLen, const SwRedlineData* pRedlineData )
{
    std::vector< OUString > aStarts;
    std::vector< OUString > aEnds;
@@ -172,7 +173,7 @@ void DocxExport::AppendBookmarks( const SwTextNode& rNode, sal_Int32 nCurrentPos
    if ( nCurrentPos == nEnd )
        m_pAttrOutput->WriteFinalBookmarks_Impl( aStarts, aEnds );
    else
        m_pAttrOutput->WriteBookmarks_Impl( aStarts, aEnds );
        m_pAttrOutput->WriteBookmarks_Impl( aStarts, aEnds, pRedlineData );
}

void DocxExport::AppendBookmark( const OUString& rName )
diff --git a/sw/source/filter/ww8/docxexport.hxx b/sw/source/filter/ww8/docxexport.hxx
index 91ca7c8..cdd18c9 100644
--- a/sw/source/filter/ww8/docxexport.hxx
+++ b/sw/source/filter/ww8/docxexport.hxx
@@ -145,7 +145,7 @@ public:
    /// Guess the script (asian/western).
    virtual bool CollapseScriptsforWordOk( sal_uInt16 nScript, sal_uInt16 nWhich ) override;

    virtual void AppendBookmarks( const SwTextNode& rNode, sal_Int32 nCurrentPos, sal_Int32 nLen ) override;
    virtual void AppendBookmarks( const SwTextNode& rNode, sal_Int32 nCurrentPos, sal_Int32 nLen, const SwRedlineData* pRedlineData = nullptr ) override;

    virtual void AppendBookmark( const OUString& rName ) override;

diff --git a/sw/source/filter/ww8/rtfexport.cxx b/sw/source/filter/ww8/rtfexport.cxx
index 2be381a..68fd898 100644
--- a/sw/source/filter/ww8/rtfexport.cxx
+++ b/sw/source/filter/ww8/rtfexport.cxx
@@ -118,7 +118,8 @@ bool RtfExport::CollapseScriptsforWordOk(sal_uInt16 nScript, sal_uInt16 nWhich)
    return true;
}

void RtfExport::AppendBookmarks(const SwTextNode& rNode, sal_Int32 nCurrentPos, sal_Int32 nLen)
void RtfExport::AppendBookmarks(const SwTextNode& rNode, sal_Int32 nCurrentPos, sal_Int32 nLen,
                                const SwRedlineData* /*pRedlineData*/)
{
    std::vector<OUString> aStarts;
    std::vector<OUString> aEnds;
diff --git a/sw/source/filter/ww8/rtfexport.hxx b/sw/source/filter/ww8/rtfexport.hxx
index 48b3379..36a735e 100644
--- a/sw/source/filter/ww8/rtfexport.hxx
+++ b/sw/source/filter/ww8/rtfexport.hxx
@@ -67,7 +67,8 @@ public:
    /// Guess the script (asian/western).
    bool CollapseScriptsforWordOk(sal_uInt16 nScript, sal_uInt16 nWhich) override;

    void AppendBookmarks(const SwTextNode& rNode, sal_Int32 nCurrentPos, sal_Int32 nLen) override;
    void AppendBookmarks(const SwTextNode& rNode, sal_Int32 nCurrentPos, sal_Int32 nLen,
                         const SwRedlineData* pSwRedlineData = nullptr) override;

    void AppendBookmark(const OUString& rName) override;

diff --git a/sw/source/filter/ww8/wrtw8nds.cxx b/sw/source/filter/ww8/wrtw8nds.cxx
index f33a527..4f3ca7f 100644
--- a/sw/source/filter/ww8/wrtw8nds.cxx
+++ b/sw/source/filter/ww8/wrtw8nds.cxx
@@ -1208,10 +1208,28 @@ bool WW8AttributeOutput::EndURL(bool const)
    return true;
}

OUString BookmarkToWord(const OUString &rBookmark)
OUString BookmarkToWord(const OUString &rBookmark, bool* pIsMove, bool* pIsFrom)
{
    sal_Int32 nTrim = 0; // position to remove "__RefMoveRange" from bookmark names
    if ( pIsMove )
    {
        static constexpr OUStringLiteral MoveFrom_Bookmark_NamePrefix = u"__RefMoveFrom__";
        static constexpr OUStringLiteral MoveTo_Bookmark_NamePrefix = u"__RefMoveTo__";
        if ( rBookmark.startsWith(MoveFrom_Bookmark_NamePrefix) )
        {
            *pIsMove = true;
            *pIsFrom = true;
            nTrim = MoveFrom_Bookmark_NamePrefix.getLength();
        }
        else if ( rBookmark.startsWith(MoveTo_Bookmark_NamePrefix) )
        {
            *pIsMove = true;
            *pIsFrom = false;
            nTrim = MoveTo_Bookmark_NamePrefix.getLength();
        }
    }
    OUString sRet(INetURLObject::encode(
        rBookmark.replace(' ', '_'), // Spaces are prohibited in bookmark name
        rBookmark.copy(nTrim).replace(' ', '_'), // Spaces are prohibited in bookmark name
        INetURLObject::PART_REL_SEGMENT_EXTRA,
        INetURLObject::EncodeMechanism::All, RTL_TEXTENCODING_ASCII_US));
    // Unicode letters are allowed
@@ -2418,7 +2436,7 @@ void MSWordExportBase::OutputTextNode( SwTextNode& rNode )
            AttrOutput().SetAnchorIsLinkedToNode( bPostponeWritingText && (FLY_POSTPONED != nStateOfFlyFrame) );
            // Append bookmarks in this range after flys, exclusive of final
            // position of this range
            AppendBookmarks( rNode, nCurrentPos, nNextAttr - nCurrentPos );
            AppendBookmarks( rNode, nCurrentPos, nNextAttr - nCurrentPos, pRedlineData );
            // Sadly only possible for main or glossary document parts: ECMA-376 Part 1 sect. 11.3.2
            if ( m_nTextTyp == TXT_MAINTEXT )
                AppendAnnotationMarks(aAttrIter, nCurrentPos, nNextAttr - nCurrentPos);
diff --git a/sw/source/filter/ww8/wrtww8.cxx b/sw/source/filter/ww8/wrtww8.cxx
index 2c2f40d..d179858 100644
--- a/sw/source/filter/ww8/wrtww8.cxx
+++ b/sw/source/filter/ww8/wrtww8.cxx
@@ -1415,7 +1415,7 @@ WW8_CP WW8_WrPct::Fc2Cp( sal_uLong nFc ) const
    return nFc + m_Pcts.back()->GetStartCp();
}

void WW8Export::AppendBookmarks( const SwTextNode& rNd, sal_Int32 nCurrentPos, sal_Int32 nLen )
void WW8Export::AppendBookmarks( const SwTextNode& rNd, sal_Int32 nCurrentPos, sal_Int32 nLen, const SwRedlineData* /*pRedlineData*/ )
{
    std::vector< const ::sw::mark::IMark* > aArr;
    sal_uInt16 nContent;
diff --git a/sw/source/filter/ww8/wrtww8.hxx b/sw/source/filter/ww8/wrtww8.hxx
index 1051c8b..72e5a8d 100644
--- a/sw/source/filter/ww8/wrtww8.hxx
+++ b/sw/source/filter/ww8/wrtww8.hxx
@@ -704,7 +704,7 @@ public:
    /// has two
    virtual bool CollapseScriptsforWordOk( sal_uInt16 nScript, sal_uInt16 nWhich ) = 0;

    virtual void AppendBookmarks( const SwTextNode& rNd, sal_Int32 nCurrentPos, sal_Int32 nLen ) = 0;
    virtual void AppendBookmarks( const SwTextNode& rNd, sal_Int32 nCurrentPos, sal_Int32 nLen, const SwRedlineData* pSwRedline = nullptr ) = 0;

    virtual void AppendBookmark( const OUString& rName ) = 0;

@@ -1074,7 +1074,7 @@ public:
                             const tools::SvRef<SotStorage>& xObjStg, OUString const& rStorageName,
                             SwOLENode* pOLENd);

    virtual void AppendBookmarks( const SwTextNode& rNd, sal_Int32 nCurrentPos, sal_Int32 nLen ) override;
    virtual void AppendBookmarks( const SwTextNode& rNd, sal_Int32 nCurrentPos, sal_Int32 nLen, const SwRedlineData* pRedlineData = nullptr ) override;
    virtual void AppendBookmark( const OUString& rName ) override;
    void AppendBookmarkEndWithCorrection( const OUString& rName );

@@ -1633,7 +1633,7 @@ public:
sal_Int16 GetWordFirstLineOffset(const SwNumFormat &rFormat);
// A bit of a bag on the side for now
OUString FieldString(ww::eField eIndex);
OUString BookmarkToWord(const OUString &rBookmark);
OUString BookmarkToWord(const OUString &rBookmark, bool* pIsMove = nullptr, bool* pIsFrom = nullptr);

class WW8SHDLong
{
diff --git a/writerfilter/source/dmapper/DomainMapper.cxx b/writerfilter/source/dmapper/DomainMapper.cxx
index b247e01..2958f00 100644
--- a/writerfilter/source/dmapper/DomainMapper.cxx
+++ b/writerfilter/source/dmapper/DomainMapper.cxx
@@ -2500,10 +2500,12 @@ void DomainMapper::sprmWithProps( Sprm& rSprm, const PropertyMapPtr& rContext )
    }
    break;
    case NS_ooxml::LN_EG_RangeMarkupElements_moveFromRangeStart:
        m_pImpl->SetMoveBookmark(/*bIsFrom=*/true);
        if (m_pImpl->hasTableManager())
            m_pImpl->getTableManager().setMoved( getPropertyName(PROP_TABLE_ROW_DELETE) );
    break;
    case NS_ooxml::LN_EG_RangeMarkupElements_moveToRangeStart:
        m_pImpl->SetMoveBookmark(/*bIsFrom=*/false);
        if (m_pImpl->hasTableManager())
            m_pImpl->getTableManager().setMoved( getPropertyName(PROP_TABLE_ROW_INSERT) );
    break;
diff --git a/writerfilter/source/dmapper/DomainMapper_Impl.cxx b/writerfilter/source/dmapper/DomainMapper_Impl.cxx
index 97cb351..befa25a 100644
--- a/writerfilter/source/dmapper/DomainMapper_Impl.cxx
+++ b/writerfilter/source/dmapper/DomainMapper_Impl.cxx
@@ -7051,10 +7051,14 @@ void DomainMapper_Impl::SetBookmarkName( const OUString& rBookmarkName )
            }
        }

        aBookmarkIter->second.m_sBookmarkName = rBookmarkName;
        aBookmarkIter->second.m_sBookmarkName = m_sCurrentBkmkPrefix + rBookmarkName;
        m_sCurrentBkmkPrefix.clear();
    }
    else
    {
        m_sCurrentBkmkName = rBookmarkName;
        m_sCurrentBkmkPrefix.clear();
    }
}

// This method was used as-is for DomainMapper_Impl::startOrEndPermissionRange() implementation.
@@ -7101,7 +7105,10 @@ void DomainMapper_Impl::StartOrEndBookmark( const OUString& rId )
                // then  move the bookmark-End to the earlier paragraph
                if (IsOutsideAParagraph())
                {
                    // keep bookmark range
                    uno::Reference< text::XTextRange > xStart = xCursor->getStart();
                    xCursor->goLeft( 1, false );
                    xCursor->gotoRange(xStart, true );
                }
                uno::Reference< container::XNamed > xBkmNamed( xBookmark, uno::UNO_QUERY_THROW );
                SAL_WARN_IF(aBookmarkIter->second.m_sBookmarkName.isEmpty(), "writerfilter.dmapper", "anonymous bookmark");
@@ -7143,6 +7150,16 @@ void DomainMapper_Impl::StartOrEndBookmark( const OUString& rId )
    }
}

void DomainMapper_Impl::SetMoveBookmark( bool bIsFrom )
{
    static constexpr OUStringLiteral MoveFrom_Bookmark_NamePrefix = u"__RefMoveFrom__";
    static constexpr OUStringLiteral MoveTo_Bookmark_NamePrefix = u"__RefMoveTo__";
    if ( bIsFrom )
        m_sCurrentBkmkPrefix = MoveFrom_Bookmark_NamePrefix;
    else
        m_sCurrentBkmkPrefix = MoveTo_Bookmark_NamePrefix;
}

void DomainMapper_Impl::setPermissionRangeEd(const OUString& user)
{
    PermMap_t::iterator aPremIter = m_aPermMap.find(m_sCurrentPermId);
diff --git a/writerfilter/source/dmapper/DomainMapper_Impl.hxx b/writerfilter/source/dmapper/DomainMapper_Impl.hxx
index 7d58fe6..fb89fb7 100644
--- a/writerfilter/source/dmapper/DomainMapper_Impl.hxx
+++ b/writerfilter/source/dmapper/DomainMapper_Impl.hxx
@@ -488,6 +488,7 @@ private:
    BookmarkMap_t                                                                   m_aBookmarkMap;
    OUString                                                                        m_sCurrentBkmkId;
    OUString                                                                        m_sCurrentBkmkName;
    OUString                                                                        m_sCurrentBkmkPrefix;

    PermMap_t                                                                       m_aPermMap;
    sal_Int32                                                                       m_sCurrentPermId;
@@ -913,6 +914,8 @@ public:
    void SetBookmarkName( const OUString& rBookmarkName );
    void StartOrEndBookmark( const OUString& rId );

    void SetMoveBookmark( bool IsFrom );

    void setPermissionRangeEd(const OUString& user);
    void setPermissionRangeEdGrp(const OUString& group);
    void startOrEndPermissionRange(sal_Int32 permissinId);