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                   |    7 ++
 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                 |   56 ++++++++++++++++++-----
 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, 183 insertions(+), 32 deletions(-)

New commits:
commit d3ebd16bd284d9aa7ac988cd5d7437eaff0f6ed0
Author:     Michael Stahl <[email protected]>
AuthorDate: Wed Oct 8 19:29:46 2025 +0200
Commit:     Thorsten Behrens <[email protected]>
CommitDate: Mon Feb 16 05:06:48 2026 +0100

    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.
    
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/192077
    Tested-by: Jenkins
    Reviewed-by: Michael Stahl <[email protected]>
    (cherry picked from commit 0849ddd0b1b3c384c5f3de8fe0bbb9df558fa786)
    
    sw: fix too early reset of m_oParagraphBreak
    Thanks to Mike Kaganski for finding this
    (regression from commit 0849ddd0b1b3c384c5f3de8fe0bbb9df558fa786)
    
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/192105
    Reviewed-by: Michael Stahl <[email protected]>
    Tested-by: Jenkins
    (cherry picked from commit 2ae71afbef8e767a313584949268b62e5e695279)
    
    Conflicts:
            sw/source/core/text/inftxt.hxx
            sw/source/core/text/redlnitr.cxx
            sw/source/writerfilter/dmapper/SettingsTable.cxx
    
    Change-Id: I216d9e2afdac9ab6f97c0ea822d4d501689df7a6
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/198689
    Tested-by: Jenkins CollaboraOffice <[email protected]>
    Reviewed-by: Thorsten Behrens <[email protected]>

diff --git a/sw/inc/IDocumentSettingAccess.hxx 
b/sw/inc/IDocumentSettingAccess.hxx
index 1314f60479bc..28d5c4d8cb90 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 5106cb78ddfc..1ed663d9cc02 100644
--- a/sw/source/core/doc/DocumentSettingManager.cxx
+++ b/sw/source/core/doc/DocumentSettingManager.cxx
@@ -264,6 +264,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:
@@ -477,6 +479,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;
@@ -1177,6 +1182,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 92dfe6f834e8..c5e92d45f468 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 c1db19420eeb..8b368707080c 100644
--- a/sw/source/core/inc/txtfrm.hxx
+++ b/sw/source/core/inc/txtfrm.hxx
@@ -93,11 +93,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 eca2e8e1829c..1696b82b4bf5 100644
--- a/sw/source/core/layout/frmtool.cxx
+++ b/sw/source/core/layout/frmtool.cxx
@@ -1138,7 +1138,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 e701e2746a77..d1d0a1872f3d 100644
--- a/sw/source/core/text/inftxt.cxx
+++ b/sw/source/core/text/inftxt.cxx
@@ -113,11 +113,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;
 
@@ -138,7 +138,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++ )
@@ -152,7 +152,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();
 }
@@ -533,6 +539,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 )
@@ -546,6 +553,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 )
@@ -798,8 +806,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 70e17059adda..15aed5a00c6a 100644
--- a/sw/source/core/text/inftxt.hxx
+++ b/sw/source/core/text/inftxt.hxx
@@ -68,8 +68,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();
@@ -366,9 +367,12 @@ 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
+
     bool m_bOmitPaint = false;
     bool m_bInsertColorPaint = false;
     bool m_bDeleteColorPaint = false;
+
     void DrawText_(const OUString &rText, const SwLinePortion &rPor,
                    const TextFrameIndex nIdx, const TextFrameIndex nLen,
                    const bool bKern, const bool bWrong = false,
@@ -485,6 +489,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; }
     void SetOmitPaint(bool bOmitPaint) { m_bOmitPaint = bOmitPaint; }
     void SetInsertColorPaint(bool bInsertColorPaint) { m_bInsertColorPaint = 
bInsertColorPaint; }
     void SetDeleteColorPaint(bool bDeleteColorPaint) { m_bDeleteColorPaint = 
bDeleteColorPaint; }
diff --git a/sw/source/core/text/itratr.cxx b/sw/source/core/text/itratr.cxx
index aa3fd8353b0d..7e9a7b1fdf70 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 2e2b01d68492..52cd4f7befef 100644
--- a/sw/source/core/text/itratr.hxx
+++ b/sw/source/core/text/itratr.hxx
@@ -114,6 +114,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 711605d8cb60..75ec544960ef 100644
--- a/sw/source/core/text/itrpaint.cxx
+++ b/sw/source/core/text/itrpaint.cxx
@@ -72,6 +72,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 8c4bac91d563..645fff7d7227 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 949ea51dafdf..e0b3a9949204 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>
@@ -86,6 +87,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(SwTextNode & rTextNode,
             bool const isHideRedlines, sw::FieldmarkMode const eMode,
@@ -212,6 +217,7 @@ public:
             m_pStartPos = pRed->Start();
             m_pEndPos = pRed->End();
             ++m_RedlineIndex;
+            m_oParagraphBreak.reset();
             return true;
         }
         else if (m_oNextFieldmarkHide)
@@ -219,6 +225,7 @@ public:
             assert(!pNextRedlineHide || *m_oNextFieldmarkHide <= 
*pNextRedlineHide);
             m_pStartPos = &*m_oNextFieldmarkHide;
             m_pEndPos = &*m_Fieldmark.second;
+            m_oParagraphBreak.reset();
             return true;
         }
         else
@@ -299,6 +306,7 @@ public:
 
             m_pStartPos = nullptr;
             m_pEndPos = nullptr;
+            m_oParagraphBreak.reset();
             return false;
         }
     }
@@ -326,23 +334,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);
@@ -377,11 +398,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);
@@ -498,7 +530,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
@@ -507,7 +539,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 a96b465d9c7f..c3cde216cb7a 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 acae502032b9..c541a769024d 100644
--- a/sw/source/filter/ww8/ww8par.cxx
+++ b/sw/source/filter/ww8/ww8par.cxx
@@ -2017,6 +2017,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 f4add8c2644e..24891150009c 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,
@@ -276,6 +277,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 },
@@ -1118,6 +1120,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;
@@ -1767,6 +1779,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 4f309109a31f..77bfcf96f6ca 100644
--- a/sw/source/writerfilter/dmapper/SettingsTable.cxx
+++ b/sw/source/writerfilter/dmapper/SettingsTable.cxx
@@ -664,6 +664,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"HiddenParagraphMarkPerLineProperties"_ustr,
+                                uno::Any(true));
     }
 
     // Show changes value

Reply via email to