sw/qa/extras/layout/data/nbsp-moved-to-new-line.fodt | 28 +++++++++++++ sw/qa/extras/layout/layout6.cxx | 39 +++++++++++++++++++ sw/source/core/text/guess.cxx | 3 - sw/source/core/text/porexp.cxx | 4 - sw/source/core/text/porexp.hxx | 2 5 files changed, 72 insertions(+), 4 deletions(-)
New commits: commit e3c068cb190e6cf04a49f9984f984d5ef72f9d9a Author: Mike Kaganski <[email protected]> AuthorDate: Sat Feb 7 14:57:33 2026 +0500 Commit: Mike Kaganski <[email protected]> CommitDate: Sat Feb 7 12:15:09 2026 +0100 tdf#167946: reimplement the fix for tdf#120677 Commit 4bb28ad217ea9d6511b6921dcd3d28328edcb4d6 (tdf#120677: restore treatment of blanks in SwTextGuess::Guess, 2018-11-12) made NBSPs behave like ordinary spaces for line breaking purposes. But that is wrong; NBSPs are explicitly defined in UAX #14 to behave differently. It turns out, that the problem in tdf#120677 was caused by specifics of handling of SwBlankPortion, which was defined as descendant of SwExpandPortion (since initial import). SwExpandPortion::Format uses special logic to change the passed SwTextFormatInfo using SwTextSlot, to allow formatting text that is different from what was initially passed to SwTextFormatInfo. This logic isn't needed for blanks, and happens to confuse the normal line-breaking algorithm to return zero for blank portions with NBSP. SwExpandPortion::Paint method, also previously used by SwBlankPortion, doesn't seem to include any logic specifically useful for blanks as well, and respective methods of SwTextPortion can be used instead. This change reverts the functional change of the previous fix for tdf#120677 (the unit test is kept), and makes SwBlankPortion inherit from SwTextPortion, which fixes both tdf#120677 and tdf#167946. Change-Id: I9109069093a7f58a997a93723f81137d1a6bd7f8 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/198882 Reviewed-by: Mike Kaganski <[email protected]> Tested-by: Jenkins diff --git a/sw/qa/extras/layout/data/nbsp-moved-to-new-line.fodt b/sw/qa/extras/layout/data/nbsp-moved-to-new-line.fodt new file mode 100644 index 000000000000..63ad3180eca5 --- /dev/null +++ b/sw/qa/extras/layout/data/nbsp-moved-to-new-line.fodt @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" office:version="1.4" office:mimetype="application/vnd.oasis.opendocument.text"> + <office:font-face-decls> + <style:font-face style:name="Liberation Serif" svg:font-family="'Liberation Serif'" style:font-family-generic="roman" style:font-pitch="variable"/> + </office:font-face-decls> + <office:styles> + <style:default-style style:family="paragraph"> + <style:paragraph-properties style:text-autospace="ideograph-alpha" style:punctuation-wrap="hanging" style:line-break="strict" style:writing-mode="page"/> + <style:text-properties style:font-name="Liberation Serif" fo:font-size="12pt" fo:hyphenate="false"/> + </style:default-style> + <style:style style:name="Standard" style:family="paragraph" style:class="text"/> + </office:styles> + <office:automatic-styles> + <style:page-layout style:name="pm1"> + <style:page-layout-properties fo:page-width="210mm" fo:page-height="297mm" style:print-orientation="portrait" fo:margin-top="20mm" fo:margin-bottom="20mm" fo:margin-left="20mm" fo:margin-right="20mm" style:writing-mode="lr-tb"/> + </style:page-layout> + </office:automatic-styles> + <office:master-styles> + <style:master-page style:name="Standard" style:page-layout-name="pm1"/> + </office:master-styles> + <office:body> + <office:text> + <!-- The first space after "elit." is a normal space; all the following up to comma are NBSPs --> + <text:p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. ,</text:p> + </office:text> + </office:body> +</office:document> \ No newline at end of file diff --git a/sw/qa/extras/layout/layout6.cxx b/sw/qa/extras/layout/layout6.cxx index 4f88d7392943..859515ff48f3 100644 --- a/sw/qa/extras/layout/layout6.cxx +++ b/sw/qa/extras/layout/layout6.cxx @@ -2113,6 +2113,45 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter6, testTdf170630) assertXPath(pXmlDoc, "//page[1]/body/txt[3]/anchored/fly", 2); } +CPPUNIT_TEST_FIXTURE(SwLayoutWriter6, testTdf167946) +{ + // Given a document with a sequence of non-breaking spaces after a text and a space, which + // sequence doesn't fit the rest of the line. Before the fix, only the comma was moved to the + // second line, and all NBSPs were elided as a hole portion at the end of the first line: + createSwDoc("nbsp-moved-to-new-line.fodt"); + + xmlDocUniquePtr pXmlDoc = parseLayoutDump(); + CPPUNIT_ASSERT(pXmlDoc); + + // One page: + assertXPath(pXmlDoc, "//page", 1); + + // One paragraph: + assertXPath(pXmlDoc, "//txt", 1); + + // Two lines in the paragraph: + assertXPath(pXmlDoc, "//txt/SwParaPortion/SwLineLayout", 2); + assertXPath(pXmlDoc, "//SwLineLayout", 2); + + // First line consists of a text portion with, and a hole portion for the following space: + assertXPath(pXmlDoc, "//SwLineLayout[1]/child::*[1]", "type", u"PortionType::Text"); + assertXPath(pXmlDoc, "//SwLineLayout[1]/child::*[1]", "portion", + u"Lorem ipsum dolor sit amet, consectetur adipiscing elit."); + assertXPath(pXmlDoc, "//SwLineLayout[1]/child::*[2]", "type", u"PortionType::Hole"); + assertXPath(pXmlDoc, "//SwLineLayout[1]/child::*[2]", "portion", u" "); + + // Second line has blank portions for the leading NBSPs, followed by a text portion with comma: + assertXPathChildren(pXmlDoc, "//SwLineLayout[2]", 81); + for (int i = 1; i <= 80; ++i) + { + OString aXPath = "//SwLineLayout[2]/child::*[" + OString::number(i) + "]"; + assertXPath(pXmlDoc, aXPath, "type", u"PortionType::Blank"); + assertXPath(pXmlDoc, aXPath, "portion", u"\xA0"); + } + assertXPath(pXmlDoc, "//SwLineLayout[2]/child::*[81]", "type", u"PortionType::Text"); + assertXPath(pXmlDoc, "//SwLineLayout[2]/child::*[81]", "portion", u","); +} + } // end of anonymous namespace CPPUNIT_PLUGIN_IMPLEMENT(); diff --git a/sw/source/core/text/guess.cxx b/sw/source/core/text/guess.cxx index 7f38f06c578f..3480d54d0ea6 100644 --- a/sw/source/core/text/guess.cxx +++ b/sw/source/core/text/guess.cxx @@ -43,7 +43,8 @@ using namespace ::com::sun::star::linguistic2; namespace{ -bool IsBlank(sal_Unicode ch) { return ch == CH_BLANK || ch == CH_FULL_BLANK || ch == CH_NB_SPACE || ch == CH_SIX_PER_EM; } +// UAX #14: spaces from SP and BA classes (elided in the end of a line) +bool IsBlank(sal_Unicode ch) { return ch == CH_BLANK || ch == CH_FULL_BLANK || ch == CH_SIX_PER_EM; } // Used when spaces should not be counted in layout // Returns adjusted cut position diff --git a/sw/source/core/text/porexp.cxx b/sw/source/core/text/porexp.cxx index afdf71da21c0..a9bb3add21b0 100644 --- a/sw/source/core/text/porexp.cxx +++ b/sw/source/core/text/porexp.cxx @@ -195,7 +195,7 @@ void SwBlankPortion::FormatEOL( SwTextFormatInfo &rInf ) */ bool SwBlankPortion::Format( SwTextFormatInfo &rInf ) { - const bool bFull = rInf.IsUnderflow() || SwExpandPortion::Format( rInf ); + const bool bFull = rInf.IsUnderflow() || SwTextPortion::Format(rInf); if( bFull && MayUnderflow( rInf, rInf.GetIdx(), rInf.IsUnderflow() ) ) { Truncate(); @@ -211,7 +211,7 @@ void SwBlankPortion::Paint( const SwTextPaintInfo &rInf ) const // Draw field shade (can be disabled individually) if (!m_bMulti) // No gray background for multiportion brackets rInf.DrawViewOpt(*this, PortionType::Blank); - SwExpandPortion::Paint(rInf); + SwTextPortion::Paint(rInf); if (rInf.GetOpt().IsViewMetaChars() && rInf.GetOpt().IsHardBlank()) { diff --git a/sw/source/core/text/porexp.hxx b/sw/source/core/text/porexp.hxx index baf45c7cf2dd..22b1347f78c3 100644 --- a/sw/source/core/text/porexp.hxx +++ b/sw/source/core/text/porexp.hxx @@ -39,7 +39,7 @@ public: }; /// Non-breaking space or non-breaking hyphen. -class SwBlankPortion : public SwExpandPortion +class SwBlankPortion final : public SwTextPortion { sal_Unicode m_cChar; bool m_bMulti; // For multiportion brackets
