sw/inc/IDocumentSettingAccess.hxx                |    1 
 sw/source/core/doc/DocumentSettingManager.cxx    |   10 ++++
 sw/source/core/inc/DocumentSettingManager.hxx    |    1 
 sw/source/core/inc/txtfrm.hxx                    |    7 +-
 sw/source/core/layout/frmtool.cxx                |    6 ++
 sw/source/core/text/inftxt.cxx                   |   24 ++++++----
 sw/source/core/text/inftxt.hxx                   |    6 ++
 sw/source/core/text/itratr.cxx                   |   40 ++++++++++++++++-
 sw/source/core/text/itratr.hxx                   |    2 
 sw/source/core/text/itrpaint.cxx                 |    1 
 sw/source/core/text/itrtxt.cxx                   |   18 +++++++
 sw/source/core/text/redlnitr.cxx                 |   54 ++++++++++++++++++-----
 sw/source/core/text/txtfrm.cxx                   |   21 ++++++--
 sw/source/filter/ww8/ww8par.cxx                  |    1 
 sw/source/uibase/uno/SwXDocumentSettings.cxx     |   18 +++++++
 sw/source/writerfilter/dmapper/SettingsTable.cxx |    2 
 16 files changed, 180 insertions(+), 32 deletions(-)

New commits:
commit 0849ddd0b1b3c384c5f3de8fe0bbb9df558fa786
Author:     Michael Stahl <[email protected]>
AuthorDate: Wed Oct 8 19:29:46 2025 +0200
Commit:     Michael Stahl <[email protected]>
CommitDate: Thu Oct 9 13:07:17 2025 +0200

    sw: text formatting: implement per-line paragraph properties like Word
    
    This doesn't make a whole lot of sense.
    
    Add compatibility setting "HiddenParagraphMarkPerLineProperties" for RTF
    and DOCX compatibilityMode < 15.
    
    Apparently what Word's "Compatibility Mode" is doing in case a paragraph
    mark has hidden formatting is that it merges the last line of the first
    paragraph and the first line of the second paragraph together, and
    applies the first paragraph's properties to the line (and the preceding
    lines); but for the second line of the second paragraph, it applies the
    second paragraph's properties.
    
    This is now implemented here; firstly, by adding a flag to the
    MergedPara's Extents so that the situation can be distinguished (if the
    paragraphs are joined by a delete redline, Word does something different
    of course).  Because it's possible that the hidden paragraph break is on
    a paragraph that doesn't have any extents, but a preceding paragraph has
    extents that are affected, this sometimes requires 0-length dummy
    extents.
    
    FindParaPropsNodeIgnoreHidden() sets the last paragraph as
    pParaPropsNode, which is used for all non-per-line properties.
    
    Note that it's somewhat likely that the various Update functions in
    txtfrm.cxx don't maintain the isHiddenParaMerge flag on extents
    correctly, so it may look different than Word when editing.
    
    Currently it affects these properties:
    
    * line spacing
    
    * tab stops
    
    These are now set per line by SwTextIter::Next() calling
    SwAttrIter::GetTextNodeForLinePropsWordCompat() and a factored out
    SwLineInfo::InitLineInfo().
    
    Evidently Word also does adjustment this way, but it's not implemented
    here.
    
    Change-Id: I216d9e2afdac9ab6f97c0ea822d4d501689df7a6
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/192077
    Tested-by: Jenkins
    Reviewed-by: Michael Stahl <[email protected]>

diff --git a/sw/inc/IDocumentSettingAccess.hxx 
b/sw/inc/IDocumentSettingAccess.hxx
index 79884c24b17b..8ade13a9ba52 100644
--- a/sw/inc/IDocumentSettingAccess.hxx
+++ b/sw/inc/IDocumentSettingAccess.hxx
@@ -101,6 +101,7 @@ enum class DocumentSettingId
     JUSTIFY_LINES_WITH_SHRINKING,
     APPLY_TEXT_ATTR_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH,
     APPLY_PARAGRAPH_MARK_FORMAT_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH,
+    HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES,
     DO_NOT_MIRROR_RTL_DRAW_OBJS,
     // COMPATIBILITY FLAGS END
     BROWSE_MODE,
diff --git a/sw/source/core/doc/DocumentSettingManager.cxx 
b/sw/source/core/doc/DocumentSettingManager.cxx
index e448e8c0fda2..9a689fa9d005 100644
--- a/sw/source/core/doc/DocumentSettingManager.cxx
+++ b/sw/source/core/doc/DocumentSettingManager.cxx
@@ -269,6 +269,8 @@ bool sw::DocumentSettingManager::get(/*[in]*/ 
DocumentSettingId id) const
             return mbApplyTextAttrToEmptyLineAtEndOfParagraph;
         case 
DocumentSettingId::APPLY_PARAGRAPH_MARK_FORMAT_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH:
             return mbApplyParagraphMarkFormatToEmptyLineAtEndOfParagraph;
+        case DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES:
+            return mbHiddenParagraphMarkPerLineProperties;
         case DocumentSettingId::DO_NOT_BREAK_WRAPPED_TABLES:
             return mbDoNotBreakWrappedTables;
         case DocumentSettingId::ALLOW_TEXT_AFTER_FLOATING_TABLE_BREAK:
@@ -488,6 +490,9 @@ void sw::DocumentSettingManager::set(/*[in]*/ 
DocumentSettingId id, /*[in]*/ boo
             mbApplyParagraphMarkFormatToEmptyLineAtEndOfParagraph = value;
             break;
 
+        case DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES:
+            mbHiddenParagraphMarkPerLineProperties = value;
+            break;
 
         case DocumentSettingId::DO_NOT_MIRROR_RTL_DRAW_OBJS:
             mbDoNotMirrorRtlDrawObjs = value;
@@ -1200,6 +1205,11 @@ void 
sw::DocumentSettingManager::dumpAsXml(xmlTextWriterPtr pWriter) const
                                 
BAD_CAST(OString::boolean(mbApplyParagraphMarkFormatToEmptyLineAtEndOfParagraph).getStr()));
     (void)xmlTextWriterEndElement(pWriter);
 
+    (void)xmlTextWriterStartElement(pWriter, 
BAD_CAST("mbHiddenParagraphMarkPerLineProperties"));
+    (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("value"),
+                                
BAD_CAST(OString::boolean(mbHiddenParagraphMarkPerLineProperties).getStr()));
+    (void)xmlTextWriterEndElement(pWriter);
+
     (void)xmlTextWriterStartElement(pWriter, 
BAD_CAST("mbDoNotMirrorRtlDrawObjs"));
     (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("value"),
                                 
BAD_CAST(OString::boolean(mbDoNotMirrorRtlDrawObjs).getStr()));
diff --git a/sw/source/core/inc/DocumentSettingManager.hxx 
b/sw/source/core/inc/DocumentSettingManager.hxx
index 54d916350bdc..f9be48ea8c3a 100644
--- a/sw/source/core/inc/DocumentSettingManager.hxx
+++ b/sw/source/core/inc/DocumentSettingManager.hxx
@@ -180,6 +180,7 @@ class DocumentSettingManager final :
     bool mbJustifyLinesWithShrinking = false;
     bool mbApplyTextAttrToEmptyLineAtEndOfParagraph = false; // this was a 
mistake
     bool mbApplyParagraphMarkFormatToEmptyLineAtEndOfParagraph = false;
+    bool mbHiddenParagraphMarkPerLineProperties = false;
     bool mbIgnoreHiddenCharsForLineCalculation = true;
     bool mbDoNotMirrorRtlDrawObjs = false;
     // If this is on as_char flys wrapping will be handled the same like in 
Word
diff --git a/sw/source/core/inc/txtfrm.hxx b/sw/source/core/inc/txtfrm.hxx
index 8a51b206101b..b68ec8d00c1a 100644
--- a/sw/source/core/inc/txtfrm.hxx
+++ b/sw/source/core/inc/txtfrm.hxx
@@ -92,11 +92,12 @@ struct Extent
     SwTextNode * /*const logically, but need assignment for std::vector*/ 
pNode;
     sal_Int32 nStart;
     sal_Int32 nEnd;
-    Extent(SwTextNode *const p, sal_Int32 const s, sal_Int32 const e)
-        : pNode(p), nStart(s), nEnd(e)
+    bool isHiddenParaMerge; //< for Word Compatibility Mode
+    Extent(SwTextNode *const p, sal_Int32 const s, sal_Int32 const e, bool 
const b)
+        : pNode(p), nStart(s), nEnd(e), isHiddenParaMerge(b)
     {
         assert(pNode);
-        assert(nStart != nEnd);
+        assert(nStart != nEnd || isHiddenParaMerge);
     }
 };
 
diff --git a/sw/source/core/layout/frmtool.cxx 
b/sw/source/core/layout/frmtool.cxx
index 4183cd482587..227092ae2f55 100644
--- a/sw/source/core/layout/frmtool.cxx
+++ b/sw/source/core/layout/frmtool.cxx
@@ -1142,7 +1142,11 @@ static bool IsShown(SwNodeOffset const nIndex,
         }
         for (auto iter = *pIter; iter != *pEnd; ++iter)
         {
-            assert(iter->nStart != iter->nEnd); // TODO possible?
+            if (iter->nStart == iter->nEnd)
+            {
+                assert(iter->isHiddenParaMerge);
+                continue;
+            }
             assert(iter->pNode->GetIndex() == nIndex);
             if (rAnch.GetAnchorContentOffset() < iter->nStart)
             {
diff --git a/sw/source/core/text/inftxt.cxx b/sw/source/core/text/inftxt.cxx
index 9e7d36ed5fde..567ab6ab8425 100644
--- a/sw/source/core/text/inftxt.cxx
+++ b/sw/source/core/text/inftxt.cxx
@@ -114,11 +114,11 @@ SwLineInfo::~SwLineInfo()
 {
 }
 
-void SwLineInfo::CtorInitLineInfo( const SwAttrSet& rAttrSet,
-                                   const SwTextNode& rTextNode )
+void SwLineInfo::InitLineInfo(SwTextNode const& rTextNodeForLineProps)
 {
-    m_oRuler.emplace( rAttrSet.GetTabStops() );
-    if ( rTextNode.GetListTabStopPosition( m_nListTabStopPosition ) )
+    SwAttrSet const& 
rAttrSetForLineProps{rTextNodeForLineProps.GetSwAttrSet()};
+    m_oRuler.emplace(rAttrSetForLineProps.GetTabStops());
+    if (rTextNodeForLineProps.GetListTabStopPosition(m_nListTabStopPosition))
     {
         m_bListTabStopIncluded = true;
 
@@ -139,7 +139,7 @@ void SwLineInfo::CtorInitLineInfo( const SwAttrSet& 
rAttrSet,
         }
     }
 
-    if ( 
!rTextNode.getIDocumentSettingAccess()->get(DocumentSettingId::TABS_RELATIVE_TO_INDENT)
 )
+    if 
(!rTextNodeForLineProps.getIDocumentSettingAccess()->get(DocumentSettingId::TABS_RELATIVE_TO_INDENT))
     {
         // remove default tab stop at position 0
         for ( sal_uInt16 i = 0; i < m_oRuler->Count(); i++ )
@@ -153,7 +153,13 @@ void SwLineInfo::CtorInitLineInfo( const SwAttrSet& 
rAttrSet,
         }
     }
 
-    m_pSpace = &rAttrSet.GetLineSpacing();
+    m_pSpace = &rAttrSetForLineProps.GetLineSpacing();
+}
+
+void SwLineInfo::CtorInitLineInfo(const SwAttrSet& rAttrSet,
+                                  const SwTextNode& rTextNodeForLineProps)
+{
+    InitLineInfo(rTextNodeForLineProps);
     m_nVertAlign = rAttrSet.GetParaVertAlign().GetValue();
     m_nDefTabStop = std::numeric_limits<SwTwips>::max();
 }
@@ -530,6 +536,7 @@ SwTextPaintInfo::SwTextPaintInfo( const SwTextPaintInfo 
&rInf, const OUString* p
       m_aPos( rInf.GetPos() ),
       m_aPaintRect( rInf.GetPaintRect() ),
       m_nSpaceIdx( rInf.GetSpaceIdx() )
+    , m_pLineInfo(rInf.m_pLineInfo)
 { }
 
 SwTextPaintInfo::SwTextPaintInfo( const SwTextPaintInfo &rInf )
@@ -543,6 +550,7 @@ SwTextPaintInfo::SwTextPaintInfo( const SwTextPaintInfo 
&rInf )
       m_aPos( rInf.GetPos() ),
       m_aPaintRect( rInf.GetPaintRect() ),
       m_nSpaceIdx( rInf.GetSpaceIdx() )
+    , m_pLineInfo(rInf.m_pLineInfo)
 { }
 
 SwTextPaintInfo::SwTextPaintInfo( SwTextFrame *pFrame, const SwRect &rPaint )
@@ -796,8 +804,8 @@ void SwTextPaintInfo::CalcRect( const SwLinePortion& rPor,
                                SwRect* pRect, SwRect* pIntersect,
                                const bool bInsideBox ) const
 {
-    const SwAttrSet& rAttrSet = 
GetTextFrame()->GetTextNodeForParaProps()->GetSwAttrSet();
-    const SvxLineSpacingItem& rSpace = rAttrSet.GetLineSpacing();
+    assert(m_pLineInfo);
+    SvxLineSpacingItem const& rSpace{*m_pLineInfo->GetLineSpacing()};
     tools::Long nPropLineSpace = rSpace.GetPropLineSpace();
 
     SwTwips nHeight = rPor.Height();
diff --git a/sw/source/core/text/inftxt.hxx b/sw/source/core/text/inftxt.hxx
index afac3f32ef8d..fb6b2435df52 100644
--- a/sw/source/core/text/inftxt.hxx
+++ b/sw/source/core/text/inftxt.hxx
@@ -66,8 +66,9 @@ class SwLineInfo
     bool m_bListTabStopIncluded;
     tools::Long m_nListTabStopPosition;
 
+    void InitLineInfo(SwTextNode const& rTextNodeForLineProps);
     void CtorInitLineInfo( const SwAttrSet& rAttrSet,
-                           const SwTextNode& rTextNode );
+                           const SwTextNode& rTextNodeForLineProps);
 
     SW_DLLPUBLIC SwLineInfo();
     SW_DLLPUBLIC ~SwLineInfo();
@@ -365,6 +366,8 @@ class SwTextPaintInfo : public SwTextSizeInfo
     SwRect      m_aPaintRect; // Original paint rect (from Layout paint)
 
     sal_uInt16 m_nSpaceIdx;
+    SwLineInfo const* m_pLineInfo{nullptr}; // hack: need this to get line 
props
+
     void DrawText_(const OUString &rText, const SwLinePortion &rPor,
                    const TextFrameIndex nIdx, const TextFrameIndex nLen,
                    const bool bKern, const bool bWrong = false,
@@ -481,6 +484,7 @@ public:
 
     void SetSmartTags(sw::WrongListIterator *const pNew) { m_pSmartTags = 
pNew; }
     sw::WrongListIterator* GetSmartTags() const { return m_pSmartTags; }
+    void SetLineInfo(SwLineInfo const*const pLineInfo) { m_pLineInfo = 
pLineInfo; }
 };
 
 class SwTextFormatInfo : public SwTextPaintInfo
diff --git a/sw/source/core/text/itratr.cxx b/sw/source/core/text/itratr.cxx
index 4fb47e279bba..a25152138f9e 100644
--- a/sw/source/core/text/itratr.cxx
+++ b/sw/source/core/text/itratr.cxx
@@ -340,13 +340,14 @@ SwAttrIter::SeekNewPos(TextFrameIndex const nNewPos, bool 
*const o_pIsToEnd)
     bool isToEnd{false};
     if (m_pMergedPara)
     {
-        if (m_pMergedPara->extents.empty())
+        if (m_pMergedPara->mergedText.isEmpty())
         {
             isToEnd = true;
             assert(m_pMergedPara->pLastNode == newPos.first);
         }
         else
         {
+            assert(!m_pMergedPara->extents.empty());
             auto const& rLast{m_pMergedPara->extents.back()};
             isToEnd = rLast.pNode == newPos.first && rLast.nEnd == 
newPos.second;
             // for text formatting: use *last* node if all text is hidden
@@ -955,6 +956,43 @@ TextFrameIndex SwAttrIter::GetNextLayoutBreakAttr() const
     return TextFrameIndex{ nNext };
 }
 
+SwTextNode const&
+SwAttrIter::GetTextNodeForLinePropsWordCompat(TextFrameIndex const nStart)
+{
+    if (m_pMergedPara)
+    {
+        // skip any hidden to find the first non-hidden character on the line
+        TextFrameIndex nHiddenStart{COMPLETE_STRING};
+        TextFrameIndex nHiddenEnd{0};
+        m_pScriptInfo->GetBoundsOfHiddenRange(nStart, nHiddenStart, 
nHiddenEnd);
+        sal_Int32 nIndex(::std::max(nStart, nHiddenEnd));
+        // now, find the hidden paragraph break that follows the first
+        // non-hidden character on the line
+        for (auto it{m_pMergedPara->extents.begin()}; it != 
m_pMergedPara->extents.end(); ++it)
+        {
+            if (nIndex < (it->nEnd - it->nStart))
+            {
+                nIndex = 0;
+            }
+            if (nIndex == 0)
+            {
+                if (it->isHiddenParaMerge)
+                {
+                    return *it->pNode;
+                }
+            }
+            else
+            {
+                nIndex = nIndex - (it->nEnd - it->nStart);
+            }
+        }
+        // no hidden paragraph break => use default
+        assert(nIndex == 0 && "view index out of bounds");
+        return *m_pMergedPara->pParaPropsNode;
+    }
+    return *m_pTextNode;
+}
+
 namespace {
 
 class SwMinMaxArgs
diff --git a/sw/source/core/text/itratr.hxx b/sw/source/core/text/itratr.hxx
index d721ca46c826..9bf688e496e0 100644
--- a/sw/source/core/text/itratr.hxx
+++ b/sw/source/core/text/itratr.hxx
@@ -113,6 +113,8 @@ public:
     void SetPropFont( const sal_uInt8 nNew ) { m_nPropFont = nNew; }
 
     SwAttrHandler& GetAttrHandler() { return m_aAttrHandler; }
+
+    SwTextNode const& GetTextNodeForLinePropsWordCompat(TextFrameIndex nStart);
 };
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/core/text/itrpaint.cxx b/sw/source/core/text/itrpaint.cxx
index d9d5ec7f3ac2..293bca3a9747 100644
--- a/sw/source/core/text/itrpaint.cxx
+++ b/sw/source/core/text/itrpaint.cxx
@@ -70,6 +70,7 @@ void SwTextPainter::CtorInitTextPainter( SwTextFrame 
*pNewFrame, SwTextPaintInfo
     SwFont *pMyFnt = GetFnt();
     GetInfo().SetFont( pMyFnt );
     m_bPaintDrop = false;
+    GetInfo().SetLineInfo(&GetLineInfo());
 }
 
 SwLinePortion *SwTextPainter::CalcPaintOfst(const SwRect &rPaint, bool& 
rbSkippedNumPortions)
diff --git a/sw/source/core/text/itrtxt.cxx b/sw/source/core/text/itrtxt.cxx
index 1d1eed3e0837..84df842f3af5 100644
--- a/sw/source/core/text/itrtxt.cxx
+++ b/sw/source/core/text/itrtxt.cxx
@@ -24,6 +24,8 @@
 #include <editeng/paravertalignitem.hxx>
 
 #include "pormulti.hxx"
+#include <IDocumentSettingAccess.hxx>
+#include <rootfrm.hxx>
 #include <pagefrm.hxx>
 #include <tgrditem.hxx>
 #include "porfld.hxx"
@@ -42,10 +44,17 @@ void SwTextIter::CtorInitTextIter( SwTextFrame *pNewFrame, 
SwTextInfo *pNewInf )
 
     m_pFrame = pNewFrame;
     m_pInf = pNewInf;
-    m_aLineInf.CtorInitLineInfo( pNode->GetSwAttrSet(), *pNode );
+
     m_nFrameStart = m_pFrame->getFrameArea().Pos().Y() + 
m_pFrame->getFramePrintArea().Pos().Y();
     SwTextIter::Init();
 
+    SwTextNode const& rTextNodeForLineProps{
+        
(pNode->getIDocumentSettingAccess()->get(DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES)
+        && m_pFrame->getRootFrame()->GetParagraphBreakMode() == 
::sw::ParagraphBreakMode::Hidden)
+            ? GetTextNodeForLinePropsWordCompat(m_nStart)
+            : *pNode};
+    m_aLineInf.CtorInitLineInfo(pNode->GetSwAttrSet(), rTextNodeForLineProps);
+
     // Order is important: only execute FillRegister if GetValue!=0
     m_bRegisterOn = pNode->GetSwAttrSet().GetRegister().GetValue()
         && m_pFrame->FillRegister( m_nRegStart, m_nRegDiff );
@@ -116,6 +125,13 @@ const SwLineLayout *SwTextIter::Next()
         if( m_pCurr->GetLen() || ( m_nLineNr>1 && !m_pCurr->IsDummy() ) )
             ++m_nLineNr;
         m_pCurr = m_pCurr->GetNext();
+        if (m_pFrame->GetDoc().getIDocumentSettingAccess().get(
+                DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES)
+            && m_pFrame->getRootFrame()->GetParagraphBreakMode() == 
::sw::ParagraphBreakMode::Hidden)
+        {
+            SwTextNode const& 
rNode{GetTextNodeForLinePropsWordCompat(m_nStart)};
+            m_aLineInf.InitLineInfo(rNode);
+        }
         return m_pCurr;
     }
     else
diff --git a/sw/source/core/text/redlnitr.cxx b/sw/source/core/text/redlnitr.cxx
index 87564e2a3fbb..99710e5b20d7 100644
--- a/sw/source/core/text/redlnitr.cxx
+++ b/sw/source/core/text/redlnitr.cxx
@@ -36,6 +36,7 @@
 #include <IDocumentRedlineAccess.hxx>
 #include <IDocumentLayoutAccess.hxx>
 #include <IDocumentMarkAccess.hxx>
+#include <IDocumentSettingAccess.hxx>
 #include <IMark.hxx>
 #include <bookmark.hxx>
 #include <rootfrm.hxx>
@@ -83,6 +84,10 @@ private:
 public:
     SwPosition const* GetStartPos() const { return m_pStartPos; }
     SwPosition const* GetEndPos() const { return m_pEndPos; }
+    bool IsHiddenParagraphBreak() const
+    {   // only if it is merged *by* hidden break (it could be deleted at the 
same time)
+        return m_oParagraphBreak.has_value();
+    }
 
     HideIterator(const SwTextNode & rTextNode,
             bool const isHideRedlines, sw::FieldmarkMode const eMode,
@@ -106,6 +111,7 @@ public:
     // Note: caller is responsible for checking for immediately adjacent hides
     bool Next()
     {
+        m_oParagraphBreak.reset();
         SwPosition const* pNextRedlineHide(nullptr);
         assert(m_pEndPos);
         if (m_isHideRedlines)
@@ -293,23 +299,36 @@ void FindParaPropsNodeIgnoreHidden(sw::MergedPara & 
rMerged,
         pScriptInfo->GetBoundsOfHiddenRange(TextFrameIndex{0}, nHiddenStart, 
nHiddenEnd);
         if (TextFrameIndex{0} == nHiddenStart)
         {
-            if (nHiddenEnd == TextFrameIndex{rMerged.mergedText.getLength()})
+            // Word compatibilityMode < 15 changes properties per line, so 
just set the last node here
+            if (rMerged.pLastNode->getIDocumentSettingAccess()->get(
+                    
DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES))
             {
                 rMerged.pParaPropsNode = 
const_cast<SwTextNode*>(rMerged.pLastNode);
             }
-            else
-            {   // this requires MapViewToModel to never return a position at
-                // the end of a node (when all its text is hidden)
-                rMerged.pParaPropsNode = sw::MapViewToModel(rMerged, 
nHiddenEnd).first;
+            else // Word compatibilityMode 15 works differently!
+            {    // (and this is just an approximation of what it does)
+                if (nHiddenEnd == 
TextFrameIndex{rMerged.mergedText.getLength()})
+                {
+                    rMerged.pParaPropsNode = 
const_cast<SwTextNode*>(rMerged.pLastNode);
+                }
+                else
+                {   // this requires MapViewToModel to never return a position 
at
+                    // the end of a node (when all its text is hidden)
+                    rMerged.pParaPropsNode = sw::MapViewToModel(rMerged, 
nHiddenEnd).first;
+                }
             }
             return;
         }
     }
-    if (!rMerged.extents.empty())
+    for (auto const& it : rMerged.extents)
     {   // para props from first node that isn't empty (OOo/LO compat)
-        rMerged.pParaPropsNode = rMerged.extents.begin()->pNode;
+        if (it.nStart != it.nEnd) // filter isHiddenParaMerge dummy extents
+        {
+            rMerged.pParaPropsNode = it.pNode;
+            return;
+        }
+        else assert(it.isHiddenParaMerge);
     }
-    else
     {   // if every node is empty, the last one wins (Word compat)
         // (OOo/LO historically used first one)
         rMerged.pParaPropsNode = const_cast<SwTextNode*>(rMerged.pLastNode);
@@ -344,11 +363,22 @@ CheckParaRedlineMerge(SwTextFrame & rFrame, SwTextNode & 
rTextNode,
         assert(pNode != &rTextNode || &pStart->GetNode() == &rTextNode); // 
detect calls with wrong start node
         if (pStart->GetContentIndex() != nLastEnd) // not 0 so we eliminate 
adjacent deletes
         {
-            extents.emplace_back(pNode, nLastEnd, pStart->GetContentIndex());
+            extents.emplace_back(pNode, nLastEnd, pStart->GetContentIndex(), 
false);
             mergedText.append(pNode->GetText().subView(nLastEnd, 
pStart->GetContentIndex() - nLastEnd));
         }
         if (&pEnd->GetNode() != pNode)
         {
+            if (iter.IsHiddenParagraphBreak())
+            {
+                if (!extents.empty() && extents.back().pNode == pNode)
+                {
+                    extents.back().isHiddenParaMerge = true;
+                }
+                else
+                {   // dummy extent - must have "true" on one that has pNode!
+                    extents.emplace_back(pNode, pNode->Len(), pNode->Len(), 
true);
+                }
+            }
             if (pNode == &rTextNode)
             {
                 pNode->SetRedlineMergeFlag(SwNode::Merge::First);
@@ -465,7 +495,7 @@ CheckParaRedlineMerge(SwTextFrame & rFrame, SwTextNode & 
rTextNode,
     }
     if (nLastEnd != pNode->Len())
     {
-        extents.emplace_back(pNode, nLastEnd, pNode->Len());
+        extents.emplace_back(pNode, nLastEnd, pNode->Len(), false);
         mergedText.append(pNode->GetText().subView(nLastEnd, pNode->Len() - 
nLastEnd));
     }
     if (extents.empty()) // there was no text anywhere
@@ -474,7 +504,9 @@ CheckParaRedlineMerge(SwTextFrame & rFrame, SwTextNode & 
rTextNode,
     }
     else
     {
-        assert(!mergedText.isEmpty());
+        assert(!mergedText.isEmpty()
+            || ::std::all_of(extents.begin(), extents.end(),
+                    [](auto const& it){ return it.isHiddenParaMerge; }));
     }
     auto pRet{std::make_unique<sw::MergedPara>(rFrame, std::move(extents),
                 mergedText.makeStringAndClear(), &rTextNode, nodes.back())};
diff --git a/sw/source/core/text/txtfrm.cxx b/sw/source/core/text/txtfrm.cxx
index b097a5ee5ea0..30b49c04cdb2 100644
--- a/sw/source/core/text/txtfrm.cxx
+++ b/sw/source/core/text/txtfrm.cxx
@@ -839,6 +839,7 @@ void SwTextFrame::dumpAsXml(xmlTextWriterPtr writer) const
             (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( 
"txtNodeIndex" ), "%" SAL_PRIdINT32, sal_Int32(e.pNode->GetIndex()) );
             (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "start" 
), "%" SAL_PRIdINT32, e.nStart );
             (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "end" 
), "%" SAL_PRIdINT32, e.nEnd );
+            (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "isHPM" 
), "%d", e.isHiddenParaMerge ? 1 : 0 );
             (void)xmlTextWriterEndElement( writer );
         }
         (void)xmlTextWriterEndElement( writer );
@@ -1126,7 +1127,7 @@ static TextFrameIndex 
UpdateMergedParaForInsert(MergedPara & rMerged,
 //    assert((bFoundNode || rMerged.extents.empty()) && "text node not found - 
why is it sending hints to us");
     if (!bInserted)
     {   // must be in a gap
-        rMerged.extents.emplace(itInsert, const_cast<SwTextNode*>(&rNode), 
nIndex, nIndex + nLen);
+        rMerged.extents.emplace(itInsert, const_cast<SwTextNode*>(&rNode), 
nIndex, nIndex + nLen, false);
         text.insert(nTFIndex, rNode.GetText().subView(nIndex, nLen));
         nInserted = nLen;
         // called from SwRangeRedline::InvalidateRange()
@@ -1235,7 +1236,7 @@ TextFrameIndex UpdateMergedParaForDelete(MergedPara & 
rMerged,
                                 sal_Int32 const nOldEnd(it->nEnd);
                                 it->nEnd = nIndex;
                                 it = rMerged.extents.emplace(it+1,
-                                    it->pNode, nIndex + nDeleteHere, nOldEnd);
+                                    it->pNode, nIndex + nDeleteHere, nOldEnd, 
it->isHiddenParaMerge);
                             }
                             assert(nDeleteHere == nToDelete);
                         }
@@ -1301,7 +1302,7 @@ MapViewToModel(MergedPara const& rMerged, TextFrameIndex 
const i_nIndex)
         nIndex = nIndex - (pExtent->nEnd - pExtent->nStart);
     }
     assert(nIndex == 0 && "view index out of bounds");
-    return pExtent
+    return (pExtent && pExtent->nStart != pExtent->nEnd) // skip 
isHiddenParaMerge dummys
         ? std::make_pair(pExtent->pNode, pExtent->nEnd) //1-past-the-end index
         : std::make_pair(const_cast<SwTextNode*>(rMerged.pLastNode), 
rMerged.pLastNode->Len());
 }
@@ -1443,9 +1444,17 @@ SwTextNode const* SwTextFrame::GetTextNodeForFirstText() 
const
 {
     sw::MergedPara const*const pMerged(GetMergedPara());
     if (pMerged)
-        return pMerged->extents.empty()
-            ? pMerged->pFirstNode
-            : pMerged->extents.front().pNode;
+    {
+        for (auto const& it : pMerged->extents)
+        {
+            if (it.nStart != it.nEnd) // skip isHiddenParaMerge dummy extents
+            {
+                return it.pNode;
+            }
+            else assert(it.isHiddenParaMerge);
+        }
+        return pMerged->pFirstNode;
+    }
     else
         return static_cast<SwTextNode const*>(SwFrame::GetDep());
 }
diff --git a/sw/source/filter/ww8/ww8par.cxx b/sw/source/filter/ww8/ww8par.cxx
index 83a90939f3ca..3492f8662343 100644
--- a/sw/source/filter/ww8/ww8par.cxx
+++ b/sw/source/filter/ww8/ww8par.cxx
@@ -1971,6 +1971,7 @@ void SwWW8ImplReader::ImportDop()
     
m_rDoc.getIDocumentSettingAccess().set(DocumentSettingId::CONTINUOUS_ENDNOTES, 
true);
     // rely on default for HYPHENATE_URLS=false
     
m_rDoc.getIDocumentSettingAccess().set(DocumentSettingId::APPLY_PARAGRAPH_MARK_FORMAT_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH,
 true);
+    
m_rDoc.getIDocumentSettingAccess().set(DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES,
 true);
     // rely on default for IGNORE_HIDDEN_CHARS_FOR_LINE_CALCULATION=true
 
     IDocumentSettingAccess& rIDSA = m_rDoc.getIDocumentSettingAccess();
diff --git a/sw/source/uibase/uno/SwXDocumentSettings.cxx 
b/sw/source/uibase/uno/SwXDocumentSettings.cxx
index 9bb0e6940b2e..192c142a81b9 100644
--- a/sw/source/uibase/uno/SwXDocumentSettings.cxx
+++ b/sw/source/uibase/uno/SwXDocumentSettings.cxx
@@ -162,6 +162,7 @@ enum SwDocumentSettingsPropertyHandles
     HANDLE_USE_VARIABLE_WIDTH_NBSP,
     HANDLE_APPLY_TEXT_ATTR_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH,
     HANDLE_APPLY_PARAGRAPH_MARK_FORMAT_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH,
+    HANDLE_HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES,
     HANDLE_DO_NOT_MIRROR_RTL_DRAW_OBJS,
     HANDLE_PAINT_HELL_OVER_HEADER_FOOTER,
     HANDLE_MIN_ROW_HEIGHT_INCL_BORDER,
@@ -279,6 +280,7 @@ static rtl::Reference<MasterPropertySetInfo> 
lcl_createSettingsInfo()
         { u"UseVariableWidthNBSP"_ustr, HANDLE_USE_VARIABLE_WIDTH_NBSP, 
cppu::UnoType<bool>::get(), 0 },
         { u"ApplyTextAttrToEmptyLineAtEndOfParagraph"_ustr, 
HANDLE_APPLY_TEXT_ATTR_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH, 
cppu::UnoType<bool>::get(), 0 },
         { u"ApplyParagraphMarkFormatToEmptyLineAtEndOfParagraph"_ustr, 
HANDLE_APPLY_PARAGRAPH_MARK_FORMAT_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH, 
cppu::UnoType<bool>::get(), 0 },
+        { u"HiddenParagraphMarkPerLineProperties"_ustr, 
HANDLE_HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES, cppu::UnoType<bool>::get(), 0 
},
         { u"DoNotMirrorRtlDrawObjs"_ustr, HANDLE_DO_NOT_MIRROR_RTL_DRAW_OBJS, 
cppu::UnoType<bool>::get(), 0 },
         { u"PaintHellOverHeaderFooter"_ustr, 
HANDLE_PAINT_HELL_OVER_HEADER_FOOTER, cppu::UnoType<bool>::get(), 0 },
         { u"MinRowHeightInclBorder"_ustr, HANDLE_MIN_ROW_HEIGHT_INCL_BORDER, 
cppu::UnoType<bool>::get(), 0 },
@@ -1124,6 +1126,16 @@ void SwXDocumentSettings::_setSingleValue( const 
comphelper::PropertyInfo & rInf
             }
         }
         break;
+        case HANDLE_HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES:
+        {
+            bool bTmp;
+            if (rValue >>= bTmp)
+            {
+                mpDoc->getIDocumentSettingAccess().set(
+                    
DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES, bTmp);
+            }
+        }
+        break;
         case HANDLE_DO_NOT_MIRROR_RTL_DRAW_OBJS:
         {
             bool bTmp;
@@ -1794,6 +1806,12 @@ void SwXDocumentSettings::_getSingleValue( const 
comphelper::PropertyInfo & rInf
                 
DocumentSettingId::APPLY_PARAGRAPH_MARK_FORMAT_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH);
         }
         break;
+        case HANDLE_HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES:
+        {
+            rValue <<= mpDoc->getIDocumentSettingAccess().get(
+                DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES);
+        }
+        break;
         case HANDLE_DO_NOT_MIRROR_RTL_DRAW_OBJS:
         {
             rValue <<= mpDoc->getIDocumentSettingAccess().get(
diff --git a/sw/source/writerfilter/dmapper/SettingsTable.cxx 
b/sw/source/writerfilter/dmapper/SettingsTable.cxx
index e4b10814efa0..64f8eba21b13 100644
--- a/sw/source/writerfilter/dmapper/SettingsTable.cxx
+++ b/sw/source/writerfilter/dmapper/SettingsTable.cxx
@@ -636,6 +636,8 @@ void 
SettingsTable::ApplyProperties(rtl::Reference<SwXTextDocument> const& xDoc)
         
xDocumentSettings->setPropertyValue(u"MsWordCompMinLineHeightByFly"_ustr, 
uno::Any(true));
         xDocumentSettings->setPropertyValue(u"TabOverMargin"_ustr, 
uno::Any(true));
         xDocumentSettings->setPropertyValue(u"AddFrameOffsets"_ustr, 
uno::Any(true)); // tdf#138782
+        
xDocumentSettings->setPropertyValue(u"HiddenParagraphMarkPerLineProperties"_ustr,
+                                uno::Any(true));
     }
 
     // Show changes value

Reply via email to