tdf#63929 sw: Show bidi control characters as formatting marks

Updates Writer to visually indicate the following Unicode bidi control
characters when "Show Formatting Marks" is enabled:

- U+200E LEFT-TO-RIGHT MARK
- U+200F RIGHT-TO-LEFT MARK
- U+202A LEFT-TO-RIGHT EMBEDDING
- U+202B RIGHT-TO-LEFT EMBEDDING
- U+202C POP DIRECTIONAL FORMATTING
- U+202D LEFT-TO-RIGHT OVERRIDE
- U+202E RIGHT-TO-LEFT OVERRIDE

Change-Id: I1989eb793f8f89633c067a4bcbf0f1245dc0b9f0
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/203132
Reviewed-by: Jonathan Clark <jonathan@libreoffice.org>
Tested-by: Christian Lohmaier <lohmaier+LibreOffice@googlemail.com>
This commit is contained in:
Jonathan Clark
2026-04-01 19:08:28 +02:00
parent ce3486dbac
commit e09c5de0c6
6 changed files with 200 additions and 9 deletions
@@ -53,6 +53,10 @@ public:
void testHintAsian();
void testHintComplex();
void testHintMultipleRuns();
void testBidiMarkRemovesZeroLengthRun();
void testBidiMarkPartOfPreviousRun();
void testBidiEmbeddingPartOfPreviousRun();
void testBidiOverridePartOfPreviousRun();
CPPUNIT_TEST_SUITE(ScriptChangeScannerTest);
CPPUNIT_TEST(testEmpty);
@@ -85,6 +89,10 @@ public:
CPPUNIT_TEST(testHintAsian);
CPPUNIT_TEST(testHintComplex);
CPPUNIT_TEST(testHintMultipleRuns);
CPPUNIT_TEST(testBidiMarkRemovesZeroLengthRun);
CPPUNIT_TEST(testBidiMarkPartOfPreviousRun);
CPPUNIT_TEST(testBidiEmbeddingPartOfPreviousRun);
CPPUNIT_TEST(testBidiOverridePartOfPreviousRun);
CPPUNIT_TEST_SUITE_END();
};
@@ -659,14 +667,14 @@ void ScriptChangeScannerTest::testRtlRunOverrideCJKAsian()
CPPUNIT_ASSERT(!pDirScanner->AtEnd());
CPPUNIT_ASSERT_EQUAL(sal_uInt8(0), pDirScanner->Peek().m_nLevel);
CPPUNIT_ASSERT_EQUAL(sal_Int32(0), pDirScanner->Peek().m_nStartIndex);
CPPUNIT_ASSERT_EQUAL(sal_Int32(2), pDirScanner->Peek().m_nEndIndex);
CPPUNIT_ASSERT_EQUAL(sal_Int32(3), pDirScanner->Peek().m_nEndIndex);
CPPUNIT_ASSERT(!pDirScanner->Peek().m_bHasEmbeddedStrongLTR);
pDirScanner->Advance();
CPPUNIT_ASSERT(!pDirScanner->AtEnd());
CPPUNIT_ASSERT_EQUAL(sal_uInt8(1), pDirScanner->Peek().m_nLevel);
CPPUNIT_ASSERT_EQUAL(sal_Int32(2), pDirScanner->Peek().m_nStartIndex);
CPPUNIT_ASSERT_EQUAL(sal_Int32(3), pDirScanner->Peek().m_nStartIndex);
CPPUNIT_ASSERT_EQUAL(sal_Int32(6), pDirScanner->Peek().m_nEndIndex);
CPPUNIT_ASSERT(!pDirScanner->Peek().m_bHasEmbeddedStrongLTR);
@@ -1060,6 +1068,122 @@ void ScriptChangeScannerTest::testHintMultipleRuns()
CPPUNIT_ASSERT(pScanner->AtEnd());
}
void ScriptChangeScannerTest::testBidiMarkRemovesZeroLengthRun()
{
auto aText = u"Before\u200FAfter"_ustr;
ScriptHintProvider stHints;
auto pDirScanner = MakeDirectionChangeScanner(aText, 0);
CPPUNIT_ASSERT(!pDirScanner->AtEnd());
CPPUNIT_ASSERT_EQUAL(sal_uInt8(0), pDirScanner->Peek().m_nLevel);
CPPUNIT_ASSERT_EQUAL(sal_Int32(0), pDirScanner->Peek().m_nStartIndex);
CPPUNIT_ASSERT_EQUAL(sal_Int32(7), pDirScanner->Peek().m_nEndIndex);
CPPUNIT_ASSERT(!pDirScanner->Peek().m_bHasEmbeddedStrongLTR);
pDirScanner->Advance();
CPPUNIT_ASSERT(!pDirScanner->AtEnd());
CPPUNIT_ASSERT_EQUAL(sal_uInt8(0), pDirScanner->Peek().m_nLevel);
CPPUNIT_ASSERT_EQUAL(sal_Int32(7), pDirScanner->Peek().m_nStartIndex);
CPPUNIT_ASSERT_EQUAL(sal_Int32(12), pDirScanner->Peek().m_nEndIndex);
CPPUNIT_ASSERT(!pDirScanner->Peek().m_bHasEmbeddedStrongLTR);
pDirScanner->Advance();
CPPUNIT_ASSERT(pDirScanner->AtEnd());
}
void ScriptChangeScannerTest::testBidiMarkPartOfPreviousRun()
{
auto aText = u"Before..شز..\u200FAfter"_ustr;
ScriptHintProvider stHints;
auto pDirScanner = MakeDirectionChangeScanner(aText, 0);
CPPUNIT_ASSERT(!pDirScanner->AtEnd());
CPPUNIT_ASSERT_EQUAL(sal_uInt8(0), pDirScanner->Peek().m_nLevel);
CPPUNIT_ASSERT_EQUAL(sal_Int32(0), pDirScanner->Peek().m_nStartIndex);
CPPUNIT_ASSERT_EQUAL(sal_Int32(8), pDirScanner->Peek().m_nEndIndex);
CPPUNIT_ASSERT(!pDirScanner->Peek().m_bHasEmbeddedStrongLTR);
pDirScanner->Advance();
CPPUNIT_ASSERT(!pDirScanner->AtEnd());
CPPUNIT_ASSERT_EQUAL(sal_uInt8(1), pDirScanner->Peek().m_nLevel);
CPPUNIT_ASSERT_EQUAL(sal_Int32(8), pDirScanner->Peek().m_nStartIndex);
CPPUNIT_ASSERT_EQUAL(sal_Int32(13), pDirScanner->Peek().m_nEndIndex);
CPPUNIT_ASSERT(!pDirScanner->Peek().m_bHasEmbeddedStrongLTR);
pDirScanner->Advance();
CPPUNIT_ASSERT(!pDirScanner->AtEnd());
CPPUNIT_ASSERT_EQUAL(sal_uInt8(0), pDirScanner->Peek().m_nLevel);
CPPUNIT_ASSERT_EQUAL(sal_Int32(13), pDirScanner->Peek().m_nStartIndex);
CPPUNIT_ASSERT_EQUAL(sal_Int32(18), pDirScanner->Peek().m_nEndIndex);
CPPUNIT_ASSERT(!pDirScanner->Peek().m_bHasEmbeddedStrongLTR);
pDirScanner->Advance();
CPPUNIT_ASSERT(pDirScanner->AtEnd());
}
void ScriptChangeScannerTest::testBidiEmbeddingPartOfPreviousRun()
{
auto aText = u"Before\u202B..ش..\u202CAfter"_ustr;
ScriptHintProvider stHints;
auto pDirScanner = MakeDirectionChangeScanner(aText, 0);
CPPUNIT_ASSERT(!pDirScanner->AtEnd());
CPPUNIT_ASSERT_EQUAL(sal_uInt8(0), pDirScanner->Peek().m_nLevel);
CPPUNIT_ASSERT_EQUAL(sal_Int32(0), pDirScanner->Peek().m_nStartIndex);
CPPUNIT_ASSERT_EQUAL(sal_Int32(7), pDirScanner->Peek().m_nEndIndex);
CPPUNIT_ASSERT(!pDirScanner->Peek().m_bHasEmbeddedStrongLTR);
pDirScanner->Advance();
CPPUNIT_ASSERT(!pDirScanner->AtEnd());
CPPUNIT_ASSERT_EQUAL(sal_uInt8(1), pDirScanner->Peek().m_nLevel);
CPPUNIT_ASSERT_EQUAL(sal_Int32(7), pDirScanner->Peek().m_nStartIndex);
CPPUNIT_ASSERT_EQUAL(sal_Int32(12), pDirScanner->Peek().m_nEndIndex);
CPPUNIT_ASSERT(!pDirScanner->Peek().m_bHasEmbeddedStrongLTR);
pDirScanner->Advance();
CPPUNIT_ASSERT(!pDirScanner->AtEnd());
CPPUNIT_ASSERT_EQUAL(sal_uInt8(0), pDirScanner->Peek().m_nLevel);
CPPUNIT_ASSERT_EQUAL(sal_Int32(12), pDirScanner->Peek().m_nStartIndex);
CPPUNIT_ASSERT_EQUAL(sal_Int32(18), pDirScanner->Peek().m_nEndIndex);
CPPUNIT_ASSERT(!pDirScanner->Peek().m_bHasEmbeddedStrongLTR);
pDirScanner->Advance();
CPPUNIT_ASSERT(pDirScanner->AtEnd());
}
void ScriptChangeScannerTest::testBidiOverridePartOfPreviousRun()
{
auto aText = u"Before\u202EAfter"_ustr;
ScriptHintProvider stHints;
auto pDirScanner = MakeDirectionChangeScanner(aText, 0);
CPPUNIT_ASSERT(!pDirScanner->AtEnd());
CPPUNIT_ASSERT_EQUAL(sal_uInt8(0), pDirScanner->Peek().m_nLevel);
CPPUNIT_ASSERT_EQUAL(sal_Int32(0), pDirScanner->Peek().m_nStartIndex);
CPPUNIT_ASSERT_EQUAL(sal_Int32(7), pDirScanner->Peek().m_nEndIndex);
CPPUNIT_ASSERT(!pDirScanner->Peek().m_bHasEmbeddedStrongLTR);
pDirScanner->Advance();
CPPUNIT_ASSERT(!pDirScanner->AtEnd());
CPPUNIT_ASSERT_EQUAL(sal_uInt8(1), pDirScanner->Peek().m_nLevel);
CPPUNIT_ASSERT_EQUAL(sal_Int32(7), pDirScanner->Peek().m_nStartIndex);
CPPUNIT_ASSERT_EQUAL(sal_Int32(12), pDirScanner->Peek().m_nEndIndex);
CPPUNIT_ASSERT(!pDirScanner->Peek().m_bHasEmbeddedStrongLTR);
pDirScanner->Advance();
CPPUNIT_ASSERT(pDirScanner->AtEnd());
}
CPPUNIT_TEST_SUITE_REGISTRATION(ScriptChangeScannerTest);
}
@@ -61,6 +61,8 @@ i18nutil::ScriptHintType ScriptHintProvider::Peek() const { return m_eCurrHint;
namespace
{
constexpr sal_uInt32 CHAR_LRM = 0x200e;
constexpr sal_uInt32 CHAR_RLM = 0x200f;
constexpr sal_uInt32 CHAR_NNBSP = 0x202f;
class IcuDirectionChangeScanner : public DirectionChangeScanner
@@ -102,6 +104,42 @@ private:
bHasEmbeddedStrongLTR = RangeHasStrongLTR(m_nCurrIndex, nEndIndex);
}
// UBA treats certain bidi control characters as strongly-directional, but
// LO assumes portion-terminal control characters are always at the end of
// the terminated portion. Advance the end of the range to include them.
if (nEndIndex < m_rText.getLength())
{
auto nCh = m_rText[nEndIndex];
bool bAdvanceOne = (nCh == CHAR_LRM || nCh == CHAR_RLM);
switch (u_charDirection(nCh))
{
case U_LEFT_TO_RIGHT_EMBEDDING:
case U_RIGHT_TO_LEFT_EMBEDDING:
case U_LEFT_TO_RIGHT_OVERRIDE:
case U_RIGHT_TO_LEFT_OVERRIDE:
bAdvanceOne = true;
break;
default:
break;
}
if (bAdvanceOne)
{
// LRM/RLM can sometimes form a 1-character run. If this happens, the
// next run needs to be skipped entirely.
int32_t nControlEndIndex = 0;
UBiDiLevel nControlCurrLevel = 0;
ubidi_getLogicalRun(m_pBidi, nEndIndex, &nControlEndIndex, &nControlCurrLevel);
++nEndIndex;
if (nEndIndex == nControlEndIndex)
{
++m_nCurr;
}
}
}
m_stCurr = { m_nCurrIndex, nEndIndex, nCurrLevel, bHasEmbeddedStrongLTR };
m_nCurrIndex = nEndIndex;
+7 -2
View File
@@ -178,8 +178,13 @@ constexpr bool SW_ISPRINTABLE(sal_Unicode c) { return c >= ' ' && 127 != c; }
#define CHAR_HARDBLANK u'\x00A0'
#define CHAR_HARDHYPHEN u'\x2011'
#define CHAR_SOFTHYPHEN u'\x00AD'
#define CHAR_RLM u'\x200F'
#define CHAR_LRM u'\x200E'
#define CHAR_LRM u'\x200E' // LEFT-TO-RIGHT MARK
#define CHAR_RLM u'\x200F' // RIGHT-TO-LEFT MARK
#define CHAR_LRE u'\x202A' // LEFT-TO-RIGHT EMBEDDING
#define CHAR_RLE u'\x202B' // RIGHT-TO-LEFT EMBEDDING
#define CHAR_PDF u'\x202C' // POP DIRECTIONAL FORMATTING
#define CHAR_LRO u'\x202D' // LEFT-TO-RIGHT OVERRIDE
#define CHAR_RLO u'\x202E' // RIGHT-TO-LEFT OVERRIDE
#define CHAR_ZWSP u'\x200B'
#define CHAR_WJ u'\x2060'
#define CHAR_NNBSP u'\x202F' //NARROW NO-BREAK SPACE
+7
View File
@@ -2027,6 +2027,13 @@ TextFrameIndex SwTextFormatInfo::ScanPortionEnd(TextFrameIndex const nStart,
case CH_BREAK:
case CHAR_ZWSP :
case CHAR_WJ :
case CHAR_LRM:
case CHAR_RLM:
case CHAR_LRE:
case CHAR_RLE:
case CHAR_PDF:
case CHAR_LRO:
case CHAR_RLO:
SetHookChar( cPos );
return i;
+7
View File
@@ -1878,6 +1878,13 @@ SwLinePortion *SwTextFormatter::NewPortion(SwTextFormatInfo &rInf,
case CHAR_ZWSP: // zero width space
case CHAR_WJ : // word joiner
case CHAR_LRM: // left-to-right mark
case CHAR_RLM: // right-to-left mark
case CHAR_LRE: // left-to-right-embedding
case CHAR_RLE: // right-to-left embedding
case CHAR_PDF: // pop directional formatting
case CHAR_LRO: // left-to-right override
case CHAR_RLO: // right-to-left override
pPor = new SwControlCharPortion( cChar ); break;
case CH_TXTATR_BREAKWORD:
+15 -5
View File
@@ -626,11 +626,21 @@ bool SwControlCharPortion::DoPaint(SwTextPaintInfo const& rTextPaintInfo,
switch (mcChar)
{
case CHAR_ZWSP:
rOutString = "/"; break;
// case CHAR_LRM :
// rText = sal_Unicode(0x2514); break;
// case CHAR_RLM :
// rText = sal_Unicode(0x2518); break;
rOutString = u"/"_ustr;
break;
case CHAR_LRM:
case CHAR_LRE:
case CHAR_LRO:
rOutString = u"\u21B1"_ustr;
break;
case CHAR_RLM:
case CHAR_RLE:
case CHAR_RLO:
rOutString = u"\u21B0"_ustr;
break;
case CHAR_PDF:
rOutString = u"\u21A5"_ustr;
break;
default:
assert(false);
break;