sw/qa/extras/ooxmlexport/data/tdf163894.docx |binary sw/qa/extras/ooxmlexport/data/tdf163894_hidden.docx |binary sw/qa/extras/ooxmlexport/ooxmlexport14.cxx | 60 ++++++ sw/source/core/fields/reffld.cxx | 173 ++++++++++++++++++-- 4 files changed, 217 insertions(+), 16 deletions(-)
New commits: commit da88bd54eba4bb3c5a588c211d0233a8eef77ffb Author: László Németh <[email protected]> AuthorDate: Tue Feb 10 10:27:52 2026 +0100 Commit: László Németh <[email protected]> CommitDate: Wed Feb 18 13:52:35 2026 +0100 tdf#163894 sw DOCX: (p)refer char style-ref on the actual page When a paragraph starts before the page, choose the referred text on the actual page for the header/ footer references. If there is no such text, choose the nearest one on the previous pages, in the case of the backward search, too, according to the interoperability. If there is no other text, choose the nearest referred text on the next pages. Follow-up to commit fce6492756e4657008ed1ce734a08929aa19c264 "tdf#163894 sw DOCX: add character styles to style-ref window", commit 955f0f9b5e7f1d4ba42eb314478cc6924b4b63a7 "tdf#163894 sw DOCX: fix style-ref with character style", commit d4fdafa103bfea94a279d7069ddc50ba92f67d01 "tdf#160402 writerfilter,sw: STYLEREF field can refer to character style" and commit 32c588dd1164aa2fc4c8120ddb74bd510cc082f9 "tdf#86790: Add support for a word-style styleref". Change-Id: I2e9dc239202b281989f3d1b1a182e55127f2c419 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/199591 Reviewed-by: László Németh <[email protected]> Tested-by: Jenkins diff --git a/sw/qa/extras/ooxmlexport/data/tdf163894.docx b/sw/qa/extras/ooxmlexport/data/tdf163894.docx new file mode 100644 index 000000000000..99b654efcf25 Binary files /dev/null and b/sw/qa/extras/ooxmlexport/data/tdf163894.docx differ diff --git a/sw/qa/extras/ooxmlexport/data/tdf163894_hidden.docx b/sw/qa/extras/ooxmlexport/data/tdf163894_hidden.docx new file mode 100644 index 000000000000..1c75658afcfb Binary files /dev/null and b/sw/qa/extras/ooxmlexport/data/tdf163894_hidden.docx differ diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport14.cxx b/sw/qa/extras/ooxmlexport/ooxmlexport14.cxx index 8e1730693e81..12a3930ef9c2 100644 --- a/sw/qa/extras/ooxmlexport/ooxmlexport14.cxx +++ b/sw/qa/extras/ooxmlexport/ooxmlexport14.cxx @@ -792,6 +792,66 @@ CPPUNIT_TEST_FIXTURE(Test, testTdf155707) assertXPath(pXmlDoc, "/w:document/w:body/w:p[17]/w:pPr/w:jc", "val", u"highKashida"); } +CPPUNIT_TEST_FIXTURE(Test, testTdf163894) +{ + createSwDoc("tdf163894.docx"); + save(TestFilter::DOCX); + + xmlDocUniquePtr pLayout = parseLayoutDump(); + assertXPath(pLayout, "/root/page[1]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[1]", + "expand", u"handbooks"); + assertXPath(pLayout, "/root/page[1]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[2]", + "expand", u"infuriating"); + assertXPath(pLayout, "/root/page[2]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[1]", + "expand", u"infuriating"); + + // backward search reaches the whole content of the first node on the page, + // i.e. which content was typeset on the previous pages, like here + assertXPath(pLayout, "/root/page[2]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[2]", + "expand", u"infuriating"); + + assertXPath(pLayout, "/root/page[3]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[1]", + "expand", u"initializes"); + assertXPath(pLayout, "/root/page[3]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[2]", + "expand", u"misfitting"); + assertXPath(pLayout, "/root/page[4]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[1]", + "expand", u"misrepresenting"); + assertXPath(pLayout, "/root/page[4]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[2]", + "expand", u"modicum"); +} + +CPPUNIT_TEST_FIXTURE(Test, testTdf163894_hidden) +{ + createSwDoc("tdf163894_hidden.docx"); + save(TestFilter::DOCX); + + xmlDocUniquePtr pLayout = parseLayoutDump(); + assertXPath(pLayout, "/root/page[1]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[1]", + "expand", u"handbooks"); + assertXPath(pLayout, "/root/page[1]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[2]", + "expand", u"infuriating"); + + // there is a single paragraph on the page: choose the nearest reference from the previous pages, + // not the nearest one on the following pages + assertXPath(pLayout, "/root/page[2]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[1]", + "expand", u"infuriating"); + assertXPath(pLayout, "/root/page[2]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[2]", + "expand", u"infuriating"); + + // hiding the field content by a hidden space formatted with the referred character style, + // separated by an also hidden space without the referred characters style from the other + // marching text content + assertXPath(pLayout, "/root/page[3]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[1]", + "expand", u" "); + assertXPath(pLayout, "/root/page[3]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[2]", + "expand", u" "); + + assertXPath(pLayout, "/root/page[4]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[1]", + "expand", u"mitosis"); + assertXPath(pLayout, "/root/page[4]/header/txt[1]/SwParaPortion/SwLineLayout/SwFieldPortion[2]", + "expand", u"modicum"); +} + CPPUNIT_TEST_FIXTURE(Test, testTdf161643) { createSwDoc("fdo76163.docx"); diff --git a/sw/source/core/fields/reffld.cxx b/sw/source/core/fields/reffld.cxx index 480f283cf840..f05c8edd93c3 100644 --- a/sw/source/core/fields/reffld.cxx +++ b/sw/source/core/fields/reffld.cxx @@ -1294,7 +1294,10 @@ namespace SwTextNode* SearchForStyleAnchor(const SwTextNode* pSelf, SwNode* pCurrent, std::u16string_view rStyleName, sal_Int32 *const pStart, sal_Int32 *const pEnd, - bool bCaseSensitive = true) + TextFrameIndex nStartOffset = TextFrameIndex(0), + TextFrameIndex nEndOffset = TextFrameIndex(0), + bool bCaseSensitive = true, + bool bSearchBackward = false) { if (pCurrent == pSelf) return nullptr; @@ -1316,8 +1319,67 @@ namespace return pTextNode; } + bool bHasEndOffset = true; + if ( nEndOffset == TextFrameIndex(0) ) + { + // search referred character formatting backward + // to get the last one in the node + nEndOffset = TextFrameIndex(pTextNode->GetText().getLength()); + bHasEndOffset = false; + } + bool bHasHint = false; - if (auto const pHints = pTextNode->GetpSwpHints()) + auto const pHints = pTextNode->GetpSwpHints(); + // first search backward + if ( pHints && bSearchBackward ) + { + for (size_t i = pHints->Count(); i > 0; --i) + { + auto const*const pHint(pHints->Get(i - 1)); + if (pHint->Which() == RES_TXTATR_CHARFMT) + { + // not on the actual page yet + if ( TextFrameIndex(pHint->GetStart()) > nEndOffset ) + { + continue; + } + + if (bCaseSensitive + ? pHint->GetCharFormat().GetCharFormat()->HasName(rStyleName) + : pHint->GetCharFormat().GetCharFormat()->GetName().toString().equalsIgnoreAsciiCase(rStyleName)) + { + // if the recent hint (started before the previous one) is not adjacent, + // return with the first start stored in pStart, i.e. with the last + // continuous page text in the paragraph, which is formatted + // with the requested character style + if ( bHasHint && ( !pEnd || *pHint->End() != *pStart ) ) + { + if ( TextFrameIndex(*pStart) < nEndOffset ) + { + // found a reference on the page + return pTextNode; + } + else + // the previous was a reference after the page: + // start a new one + bHasHint = false; + } + + if ( !bHasHint ) + { + if (pEnd) + *pEnd = *pHint->End(); + bHasHint = true; + } + + *pStart = pHint->GetStart(); + } + } + } + if ( bHasHint && TextFrameIndex(*pStart) < nEndOffset && TextFrameIndex(*pStart) >= nStartOffset ) + return pTextNode; + } + else if (pHints) { for (size_t i = 0, nCnt = pHints->Count(); i < nCnt; ++i) { @@ -1334,10 +1396,26 @@ namespace bHasHint = true; } // if the next hint is not adjacent, return with the last end stored in pEnd, - // i.e. with the first continuous text in the paragraph, which is formatted + // i.e. with the first continuous page text in the paragraph, which is formatted // with the requested character style else if ( !pEnd || *pEnd != pHint->GetStart() ) - return pTextNode; + { + // return with the previous matching hint, if + // 1) the previous matching hint was already on the page OR + // 2) there is only a single paragraph on the page and + // the recent hint is there after the page + if ( pEnd && ( TextFrameIndex(*pEnd) > nStartOffset || + ( bHasEndOffset && + TextFrameIndex(pHint->GetStart()) > nEndOffset ) ) ) + { + return pTextNode; + } + else + { + // start new matching + *pStart = pHint->GetStart(); + } + } if (pEnd) { @@ -1356,24 +1434,52 @@ namespace SwTextNode* SearchForStyleAnchor(const SwTextNode* pSelf, const SwNodes& rNodes, SwNodeOffset nNodeStart, SwNodeOffset nNodeEnd, bool bSearchBackward, std::u16string_view rStyleName, sal_Int32 *const pStart, sal_Int32 *const pEnd, + TextFrameIndex nPageStartOffset = TextFrameIndex(0), + TextFrameIndex nPageEndOffset = TextFrameIndex(0), bool bCaseSensitive = true) { if (!bSearchBackward) { - for (SwNodeOffset nCurrent = nNodeStart; nCurrent <= nNodeEnd; ++nCurrent) + SwNodeOffset nCurrent = nNodeStart; + // first node can have an offset for character styles, check that first + if ( nPageStartOffset != TextFrameIndex(0) ) + { + SwNode* pCurrent = rNodes[nCurrent]; + SwTextNode* pFound = SearchForStyleAnchor(pSelf, pCurrent, rStyleName, pStart, pEnd, nPageStartOffset, + nNodeStart == nNodeEnd ? nPageEndOffset : TextFrameIndex(0), bCaseSensitive); + + if (pFound) + return pFound; + } + + for (; nCurrent <= nNodeEnd; ++nCurrent) { SwNode* pCurrent = rNodes[nCurrent]; - SwTextNode* pFound = SearchForStyleAnchor(pSelf, pCurrent, rStyleName, pStart, pEnd, bCaseSensitive); + SwTextNode* pFound = SearchForStyleAnchor(pSelf, pCurrent, rStyleName, pStart, pEnd, TextFrameIndex(0), TextFrameIndex(0), bCaseSensitive); if (pFound) return pFound; } } else { - for (SwNodeOffset nCurrent = nNodeEnd; nCurrent >= nNodeStart; --nCurrent) + SwNodeOffset nCurrent = nNodeEnd; + + // first end node can have an offset for character styles, check that first + if ( nPageEndOffset != TextFrameIndex(0) ) + { + SwNode* pCurrent = rNodes[nCurrent]; + SwTextNode* pFound = SearchForStyleAnchor(pSelf, pCurrent, rStyleName, pStart, pEnd, nPageStartOffset, nPageEndOffset, bCaseSensitive, bSearchBackward); + if (pFound) + return pFound; + // continue with the last but one paragraph on the page + if ( nCurrent >= nNodeStart ) + --nCurrent; + } + + for (; nCurrent >= nNodeStart; --nCurrent) { SwNode* pCurrent = rNodes[nCurrent]; - SwTextNode* pFound = SearchForStyleAnchor(pSelf, pCurrent, rStyleName, pStart, pEnd, bCaseSensitive); + SwTextNode* pFound = SearchForStyleAnchor(pSelf, pCurrent, rStyleName, pStart, pEnd, TextFrameIndex(0), TextFrameIndex(0), bCaseSensitive, bSearchBackward); if (pFound) return pFound; } @@ -1605,6 +1711,8 @@ SwTextNode* SwGetRefFieldType::FindAnchorRefStyleMarginal(SwDoc* pDoc, const SwNode* pPageStart(nullptr); const SwNode* pPageEnd(nullptr); + TextFrameIndex nPageStartOffset(0); + TextFrameIndex nPageEndOffset(0); if (pPageFrame) { @@ -1617,6 +1725,10 @@ SwTextNode* SwGetRefFieldType::FindAnchorRefStyleMarginal(SwDoc* pDoc, { pPageStart = static_cast<const SwTextFrame*>(pPageStartFrame) ->GetTextNodeFirst(); + + // style-refs referring character style need offset data + nPageStartOffset = static_cast<const SwTextFrame*>(pPageStartFrame) + ->GetOffset(); } else { @@ -1631,6 +1743,35 @@ SwTextNode* SwGetRefFieldType::FindAnchorRefStyleMarginal(SwDoc* pDoc, { pPageEnd = static_cast<const SwTextFrame*>(pPageEndFrame) ->GetTextNodeFirst(); + + // style-refs referring character style need offset data + + // set page end offset to the last character of the paragraph + auto pPage = static_cast<const SwTextFrame*>(pPageEndFrame); + nPageEndOffset = TextFrameIndex(pPage->GetText().getLength()); + + // adjust the end offset, if the paragraph continues on the next page + const SwPageFrame* pNextPage = + static_cast<const SwPageFrame*>(pPageFrame->GetNext()); + if ( pNextPage ) + { + // the get the page end offset, when the paragraph is continued on the + // next page, check the first text frame of the next page + const SwContentFrame* pNextPageStart = + pNextPage->FindFirstBodyContent(); + if ( pNextPageStart && pNextPageStart->IsTextFrame() ) + { + auto pNextPageTextNode = static_cast<const SwTextFrame*>(pNextPageStart) + ->GetTextNodeFirst(); + if ( pNextPageTextNode == pPageEnd ) + { + nPageEndOffset = static_cast<const SwTextFrame*>(pNextPageStart) + ->GetOffset(); + // set end offset to the end of the first page + nPageEndOffset--; + } + } + } } else { @@ -1649,34 +1790,34 @@ SwTextNode* SwGetRefFieldType::FindAnchorRefStyleMarginal(SwDoc* pDoc, SwNodeOffset nPageEnd = pPageEnd->GetIndex(); const SwNodes& nodes = pDoc->GetNodes(); - pTextNd = SearchForStyleAnchor(pSelf, nodes, nPageStart, nPageEnd, bFlagFromBottom, styleName, pStart, pEnd); + pTextNd = SearchForStyleAnchor(pSelf, nodes, nPageStart, nPageEnd, bFlagFromBottom, styleName, pStart, pEnd, nPageStartOffset, nPageEndOffset); if (pTextNd) return pTextNd; // 2. Search up from the top of the page - pTextNd = SearchForStyleAnchor(pSelf, nodes, SwNodeOffset(0), nPageStart - 1, /*bBackwards*/true, styleName, pStart, pEnd); + pTextNd = SearchForStyleAnchor(pSelf, nodes, SwNodeOffset(0), nPageStart - 1, /*bBackwards*/true, styleName, pStart, pEnd, nPageStartOffset, nPageEndOffset); if (pTextNd) return pTextNd; // 3. Search down from the bottom of the page - pTextNd = SearchForStyleAnchor(pSelf, nodes, nPageEnd + 1, nodes.Count() - 1, /*bBackwards*/false, styleName, pStart, pEnd); + pTextNd = SearchForStyleAnchor(pSelf, nodes, nPageEnd + 1, nodes.Count() - 1, /*bBackwards*/false, styleName, pStart, pEnd, nPageStartOffset, nPageEndOffset); if (pTextNd) return pTextNd; // Word has case insensitive styles. LO has case sensitive styles. If we didn't find // it yet, maybe we could with a case insensitive search. Let's do that - pTextNd = SearchForStyleAnchor(pSelf, nodes, nPageStart, nPageEnd, bFlagFromBottom, styleName, pStart, pEnd, + pTextNd = SearchForStyleAnchor(pSelf, nodes, nPageStart, nPageEnd, bFlagFromBottom, styleName, pStart, pEnd, nPageStartOffset, nPageEndOffset, false /* bCaseSensitive */); if (pTextNd) return pTextNd; - pTextNd = SearchForStyleAnchor(pSelf, nodes, SwNodeOffset(0), nPageStart - 1, /*bBackwards*/true, styleName, pStart, pEnd, + pTextNd = SearchForStyleAnchor(pSelf, nodes, SwNodeOffset(0), nPageStart - 1, /*bBackwards*/true, styleName, pStart, pEnd, nPageStartOffset, nPageEndOffset, false /* bCaseSensitive */); if (pTextNd) return pTextNd; - pTextNd = SearchForStyleAnchor(pSelf, nodes, nPageEnd + 1, nodes.Count() - 1, /*bBackwards*/false, styleName, pStart, pEnd, + pTextNd = SearchForStyleAnchor(pSelf, nodes, nPageEnd + 1, nodes.Count() - 1, /*bBackwards*/false, styleName, pStart, pEnd, nPageStartOffset, nPageEndOffset, false /* bCaseSensitive */); return pTextNd; } @@ -1720,12 +1861,12 @@ SwTextNode* SwGetRefFieldType::FindAnchorRefStyleOther(SwDoc* pDoc, // Again, we need to remember that Word styles are not case sensitive pTextNd = SearchForStyleAnchor(pSelf, nodes, SwNodeOffset(0), nReference, /*bBackwards*/true, styleName, pStart, pEnd, - false /* bCaseSensitive */); + TextFrameIndex(0), TextFrameIndex(0), false /* bCaseSensitive */); if (pTextNd) return pTextNd; pTextNd = SearchForStyleAnchor(pSelf, nodes, nReference + 1, nodes.Count() - 1, /*bBackwards*/false, styleName, pStart, pEnd, - false /* bCaseSensitive */); + TextFrameIndex(0), TextFrameIndex(0), false /* bCaseSensitive */); return pTextNd; }
