Coverage Report

Created: 2026-10-14 09:33

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libreoffice/i18nutil/source/utility/scriptchangescanner.cxx
Line
Count
Source
1
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
2
/*
3
 * This file is part of the LibreOffice project.
4
 *
5
 * This Source Code Form is subject to the terms of the Mozilla Public
6
 * License, v. 2.0. If a copy of the MPL was not distributed with this
7
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8
 */
9
10
#include <i18nutil/scriptchangescanner.hxx>
11
#include <i18nutil/unicode.hxx>
12
#include <i18nutil/scriptclass.hxx>
13
#include <unicode/uchar.h>
14
#include <unicode/ubidi.h>
15
#include <sal/log.hxx>
16
#include <com/sun/star/i18n/ScriptType.hpp>
17
#include <com/sun/star/i18n/CharType.hpp>
18
#include <com/sun/star/i18n/UnicodeType.hpp>
19
20
namespace css = ::com::sun::star;
21
22
namespace i18nutil
23
{
24
0
void ScriptHintProvider::SetParagraphLevelHint(ScriptHintType eType) { m_eParaHint = eType; }
25
26
void ScriptHintProvider::AddHint(ScriptHintType eType, sal_Int32 nStartIndex, sal_Int32 nEndIndex)
27
20
{
28
20
    m_aHints.emplace_back(eType, nStartIndex, nEndIndex);
29
20
}
30
31
void ScriptHintProvider::Start()
32
5.56M
{
33
5.56M
    m_pCurrIt = m_aHints.begin();
34
5.56M
    m_nCurrIdx = 0;
35
36
5.56M
    m_eCurrHint = m_eParaHint;
37
5.56M
    if (m_pCurrIt != m_aHints.end() && m_pCurrIt->m_nStartIndex == 0 && m_pCurrIt->m_nEndIndex > 0)
38
20
    {
39
20
        m_eCurrHint = m_pCurrIt->m_eType;
40
20
    }
41
5.56M
}
42
43
void ScriptHintProvider::AdvanceTo(sal_Int32 nNextIdx)
44
514M
{
45
514M
    m_nCurrIdx = nNextIdx;
46
47
514M
    while (m_pCurrIt != m_aHints.end() && m_nCurrIdx >= m_pCurrIt->m_nEndIndex)
48
0
    {
49
0
        ++m_pCurrIt;
50
0
    }
51
52
514M
    m_eCurrHint = m_eParaHint;
53
514M
    if (m_pCurrIt != m_aHints.end() && m_nCurrIdx >= m_pCurrIt->m_nStartIndex
54
1.41k
        && m_nCurrIdx < m_pCurrIt->m_nEndIndex)
55
1.41k
    {
56
1.41k
        m_eCurrHint = m_pCurrIt->m_eType;
57
1.41k
    }
58
514M
}
59
60
514M
i18nutil::ScriptHintType ScriptHintProvider::Peek() const { return m_eCurrHint; }
61
62
namespace
63
{
64
constexpr sal_uInt32 CHAR_LRM = 0x200e;
65
constexpr sal_uInt32 CHAR_RLM = 0x200f;
66
constexpr sal_uInt32 CHAR_NNBSP = 0x202f;
67
68
class IcuDirectionChangeScanner : public DirectionChangeScanner
69
{
70
private:
71
    const OUString& m_rText;
72
    UBiDi* m_pBidi;
73
    DirectionChange m_stCurr;
74
    UBiDiLevel m_nInitialDirection;
75
    int32_t m_nCurrIndex = 0;
76
    int m_nCount = 0;
77
    int m_nCurr = 0;
78
    bool m_bAtEnd = false;
79
80
    bool RangeHasStrongLTR(sal_Int32 nStart, sal_Int32 nEnd)
81
886k
    {
82
2.55M
        for (sal_Int32 nCharIdx = nStart; nCharIdx < nEnd; ++nCharIdx)
83
2.28M
        {
84
2.28M
            auto nCharDir = u_charDirection(m_rText[nCharIdx]);
85
2.28M
            if (nCharDir == U_LEFT_TO_RIGHT || nCharDir == U_LEFT_TO_RIGHT_EMBEDDING
86
1.67M
                || nCharDir == U_LEFT_TO_RIGHT_OVERRIDE)
87
615k
            {
88
615k
                return true;
89
615k
            }
90
2.28M
        }
91
92
271k
        return false;
93
886k
    }
94
95
    void PopulateCurr()
96
11.1M
    {
97
11.1M
        int32_t nEndIndex = 0;
98
11.1M
        UBiDiLevel nCurrLevel = 0;
99
11.1M
        ubidi_getLogicalRun(m_pBidi, m_nCurrIndex, &nEndIndex, &nCurrLevel);
100
101
11.1M
        bool bHasEmbeddedStrongLTR = false;
102
11.1M
        if ((nCurrLevel % 2) == UBIDI_LTR && nCurrLevel > UBIDI_RTL)
103
886k
        {
104
886k
            bHasEmbeddedStrongLTR = RangeHasStrongLTR(m_nCurrIndex, nEndIndex);
105
886k
        }
106
107
        // UBA treats certain bidi control characters as strongly-directional, but
108
        // LO assumes portion-terminal control characters are always at the end of
109
        // the terminated portion. Advance the end of the range to include them.
110
11.1M
        if (nEndIndex < m_rText.getLength())
111
5.75M
        {
112
5.75M
            auto nCh = m_rText[nEndIndex];
113
5.75M
            bool bAdvanceOne = (nCh == CHAR_LRM || nCh == CHAR_RLM);
114
5.75M
            switch (u_charDirection(nCh))
115
5.75M
            {
116
66.2k
                case U_LEFT_TO_RIGHT_EMBEDDING:
117
68.9k
                case U_RIGHT_TO_LEFT_EMBEDDING:
118
75.6k
                case U_LEFT_TO_RIGHT_OVERRIDE:
119
122k
                case U_RIGHT_TO_LEFT_OVERRIDE:
120
122k
                    bAdvanceOne = true;
121
122k
                    break;
122
123
5.62M
                default:
124
5.62M
                    break;
125
5.75M
            }
126
127
5.75M
            if (bAdvanceOne)
128
209k
            {
129
                // LRM/RLM can sometimes form a 1-character run. If this happens, the
130
                // next run needs to be skipped entirely.
131
209k
                int32_t nControlEndIndex = 0;
132
209k
                UBiDiLevel nControlCurrLevel = 0;
133
209k
                ubidi_getLogicalRun(m_pBidi, nEndIndex, &nControlEndIndex, &nControlCurrLevel);
134
135
209k
                ++nEndIndex;
136
209k
                if (nEndIndex == nControlEndIndex)
137
38.9k
                {
138
38.9k
                    ++m_nCurr;
139
38.9k
                }
140
209k
            }
141
5.75M
        }
142
143
11.1M
        m_stCurr = { m_nCurrIndex, nEndIndex, nCurrLevel, bHasEmbeddedStrongLTR };
144
145
11.1M
        m_nCurrIndex = nEndIndex;
146
11.1M
        ++m_nCurr;
147
148
11.1M
        m_bAtEnd = false;
149
11.1M
    }
150
151
public:
152
    IcuDirectionChangeScanner(const OUString& rText, UBiDiLevel nInitialDirection)
153
5.56M
        : m_rText(rText)
154
5.56M
        , m_nInitialDirection(nInitialDirection)
155
5.56M
    {
156
5.56M
        UErrorCode nError = U_ZERO_ERROR;
157
5.56M
        m_pBidi = ubidi_openSized(rText.getLength(), 0, &nError);
158
5.56M
        nError = U_ZERO_ERROR;
159
160
5.56M
        ubidi_setPara(m_pBidi, reinterpret_cast<const UChar*>(rText.getStr()), rText.getLength(),
161
5.56M
                      nInitialDirection, nullptr, &nError);
162
5.56M
        nError = U_ZERO_ERROR;
163
164
5.56M
        m_nCount = ubidi_countRuns(m_pBidi, &nError);
165
5.56M
        Reset();
166
5.56M
    }
167
168
5.56M
    ~IcuDirectionChangeScanner() override { ubidi_close(m_pBidi); }
169
170
    void Reset() override
171
5.75M
    {
172
5.75M
        m_nCurrIndex = 0;
173
5.75M
        m_nCurr = 0;
174
5.75M
        m_stCurr = { /*start*/ 0, /*end*/ 0, /*level*/ m_nInitialDirection,
175
5.75M
                     /*has embedded strong LTR*/ false };
176
5.75M
        m_bAtEnd = true;
177
178
5.75M
        if (m_nCurr < m_nCount)
179
5.65M
        {
180
5.65M
            PopulateCurr();
181
5.65M
        }
182
5.75M
    }
183
184
111M
    bool AtEnd() const override { return m_bAtEnd; }
185
186
    void Advance() override
187
5.62M
    {
188
5.62M
        if (m_nCurr >= m_nCount)
189
141k
        {
190
141k
            m_bAtEnd = true;
191
141k
            return;
192
141k
        }
193
194
5.48M
        PopulateCurr();
195
5.48M
    }
196
197
111M
    DirectionChange Peek() const override { return m_stCurr; }
198
199
    UBiDiLevel GetLevelAt(sal_Int32 nIndex) const override
200
514M
    {
201
514M
        return ubidi_getLevelAt(m_pBidi, nIndex);
202
514M
    }
203
};
204
205
class GreedyScriptChangeScanner : public ScriptChangeScanner
206
{
207
private:
208
    ScriptChange m_stCurr;
209
    DirectionChangeScanner* m_pDirScanner;
210
    ScriptHintProvider* m_pHints;
211
    const OUString& m_rText;
212
    sal_Int32 m_nIndex = 0;
213
    sal_Int32 m_nNextStart = 0;
214
    sal_Int16 m_nPrevScript = css::i18n::ScriptType::WEAK;
215
    bool m_bAtEnd = false;
216
217
    void AdvanceOnce()
218
45.3M
    {
219
45.3M
        m_stCurr
220
45.3M
            = ScriptChange{ /*start*/ m_nNextStart, /*end*/ m_nNextStart, /*type*/ m_nPrevScript };
221
222
45.3M
        if (m_nNextStart >= m_rText.getLength())
223
5.61M
        {
224
5.61M
            m_bAtEnd = true;
225
5.61M
            return;
226
5.61M
        }
227
228
39.7M
        auto nRunStart = m_nNextStart;
229
39.7M
        m_nNextStart = m_nIndex;
230
231
39.7M
        auto nScript = m_nPrevScript;
232
233
519M
        while (m_nIndex < m_rText.getLength())
234
514M
        {
235
514M
            auto nPrevIndex = m_nIndex;
236
514M
            auto nBidiLevel = m_pDirScanner->GetLevelAt(m_nIndex);
237
238
514M
            bool bCharIsRtl = (nBidiLevel % 2 == UBIDI_RTL);
239
514M
            bool bCharIsRtlOrEmbedded = (nBidiLevel > UBIDI_LTR);
240
514M
            bool bRunHasStrongEmbeddedLTR = false;
241
242
519M
            while (bCharIsRtlOrEmbedded && !m_pDirScanner->AtEnd())
243
110M
            {
244
110M
                const auto stDirRun = m_pDirScanner->Peek();
245
110M
                if (m_nIndex >= stDirRun.m_nStartIndex && m_nIndex < stDirRun.m_nEndIndex)
246
105M
                {
247
105M
                    bRunHasStrongEmbeddedLTR = stDirRun.m_bHasEmbeddedStrongLTR;
248
105M
                    break;
249
105M
                }
250
251
5.26M
                m_pDirScanner->Advance();
252
5.26M
            }
253
254
514M
            m_pHints->AdvanceTo(m_nIndex);
255
514M
            auto eCurrHint = m_pHints->Peek();
256
257
514M
            auto nChar = m_rText.iterateCodePoints(&m_nIndex);
258
514M
            nScript = GetScriptClass(nChar);
259
260
            // tdf#166011 Apply style:script-type rules per ODF standard:
261
514M
            if (eCurrHint == ScriptHintType::Ignore)
262
0
            {
263
                // The generic/latin font attributes are always used if the hint is ignore.
264
0
                nScript = css::i18n::ScriptType::LATIN;
265
0
            }
266
514M
            else if (nScript == css::i18n::ScriptType::WEAK)
267
46.2M
            {
268
46.2M
                switch (eCurrHint)
269
46.2M
                {
270
0
                    case ScriptHintType::Latin:
271
0
                        nScript = css::i18n::ScriptType::LATIN;
272
0
                        break;
273
274
27
                    case ScriptHintType::Asian:
275
27
                        nScript = css::i18n::ScriptType::ASIAN;
276
27
                        break;
277
278
0
                    case ScriptHintType::Complex:
279
0
                        nScript = css::i18n::ScriptType::COMPLEX;
280
0
                        break;
281
282
46.2M
                    default:
283
46.2M
                        break;
284
46.2M
                }
285
46.2M
            }
286
287
            // #i16354# Change script type for RTL text to CTL:
288
            // 1. All text in RTL runs will use the CTL font
289
            // #i89825# change the script type also to CTL (hennerdrewes)
290
            // 2. Text in embedded LTR runs that does not have any strong LTR characters (numbers!)
291
            // tdf#163660 Asian-script characters inside RTL runs should still use Asian font
292
514M
            if ((eCurrHint == ScriptHintType::Automatic || eCurrHint == ScriptHintType::Complex)
293
514M
                && (bCharIsRtl || (bCharIsRtlOrEmbedded && !bRunHasStrongEmbeddedLTR)))
294
38.2M
            {
295
38.2M
                if (nScript != css::i18n::ScriptType::ASIAN)
296
36.3M
                {
297
36.3M
                    nScript = css::i18n::ScriptType::COMPLEX;
298
36.3M
                }
299
38.2M
            }
300
476M
            else if (nScript == css::i18n::ScriptType::WEAK)
301
43.2M
            {
302
43.2M
                nScript = m_nPrevScript;
303
43.2M
            }
304
305
514M
            if (nScript != m_nPrevScript)
306
34.2M
            {
307
34.2M
                m_nNextStart = nPrevIndex;
308
34.2M
                break;
309
34.2M
            }
310
311
480M
            m_nNextStart = m_nIndex;
312
480M
        }
313
314
39.7M
        if (m_nNextStart > 0)
315
39.7M
        {
316
            // special case for dotted circle since it can be used with complex
317
            // before a mark, so we want it associated with the mark's script
318
            // tdf#112594: another special case for NNBSP followed by a Mongolian
319
            // character, since NNBSP has special uses in Mongolian (tdf#112594)
320
39.7M
            auto nPrevPos = m_nNextStart;
321
39.7M
            auto nPrevChar = m_rText.iterateCodePoints(&nPrevPos, -1);
322
39.7M
            if (m_nNextStart < m_rText.getLength()
323
34.2M
                && css::i18n::ScriptType::WEAK == GetScriptClass(nPrevChar))
324
4.83M
            {
325
4.83M
                auto nChar = m_rText.iterateCodePoints(&m_nNextStart, 0);
326
4.83M
                auto nType = unicode::getUnicodeType(nChar);
327
4.83M
                if (nType == css::i18n::UnicodeType::NON_SPACING_MARK
328
4.62M
                    || nType == css::i18n::UnicodeType::ENCLOSING_MARK
329
4.62M
                    || nType == css::i18n::UnicodeType::COMBINING_SPACING_MARK
330
4.59M
                    || (nPrevChar == CHAR_NNBSP
331
398
                        && u_getIntPropertyValue(nChar, UCHAR_SCRIPT) == USCRIPT_MONGOLIAN))
332
239k
                {
333
239k
                    m_nNextStart = nPrevPos;
334
239k
                }
335
4.83M
            }
336
39.7M
        }
337
338
39.7M
        m_stCurr = ScriptChange{ nRunStart, m_nNextStart, m_nPrevScript };
339
39.7M
        m_nPrevScript = nScript;
340
39.7M
    }
341
342
public:
343
    GreedyScriptChangeScanner(const OUString& rText, sal_Int16 nDefaultScriptType,
344
                              DirectionChangeScanner* pDirScanner, ScriptHintProvider* pHints)
345
5.56M
        : m_pDirScanner(pDirScanner)
346
5.56M
        , m_pHints(pHints)
347
5.56M
        , m_rText(rText)
348
5.56M
    {
349
        // tdf#66791: For compatibility with other programs, the Asian script is
350
        // applied to any weak-script quote characters if the enclosing paragraph
351
        // contains Chinese- or Japanese-script characters.
352
        // In the original Writer algorithm, the application language is used for
353
        // all leading weak characters (#94331#). This implementation deviates by
354
        // instead using the first-seen non-weak script.
355
5.56M
        sal_Int32 nCjBase = 0;
356
520M
        while (nCjBase < m_rText.getLength())
357
514M
        {
358
514M
            auto nChar = m_rText.iterateCodePoints(&nCjBase);
359
514M
            auto nScript = GetScriptClass(nChar);
360
514M
            if (m_nPrevScript == css::i18n::ScriptType::WEAK)
361
6.18M
            {
362
6.18M
                m_nPrevScript = nScript;
363
6.18M
            }
364
514M
        }
365
366
        // Fall back to the application language for leading weak characters if a
367
        // better candidate was not found.
368
5.56M
        if (m_nPrevScript == css::i18n::ScriptType::WEAK)
369
212k
        {
370
212k
            m_nPrevScript = nDefaultScriptType;
371
212k
        }
372
373
5.56M
        m_pHints->Start();
374
5.56M
        Advance();
375
5.56M
    }
376
377
90.2M
    bool AtEnd() const override { return m_bAtEnd; }
378
379
    void Advance() override
380
45.3M
    {
381
45.3M
        do
382
45.3M
        {
383
45.3M
            AdvanceOnce();
384
45.3M
        } while (!AtEnd() && (m_stCurr.m_nStartIndex == m_stCurr.m_nEndIndex));
385
45.3M
    }
386
387
39.9M
    ScriptChange Peek() const override { return m_stCurr; }
388
};
389
}
390
}
391
392
std::unique_ptr<i18nutil::DirectionChangeScanner>
393
i18nutil::MakeDirectionChangeScanner(const OUString& rText, sal_uInt8 nInitialDirection)
394
5.56M
{
395
5.56M
    return std::make_unique<IcuDirectionChangeScanner>(rText, nInitialDirection);
396
5.56M
}
397
398
std::unique_ptr<i18nutil::ScriptChangeScanner>
399
i18nutil::MakeScriptChangeScanner(const OUString& rText, sal_Int16 nDefaultScriptType,
400
                                  DirectionChangeScanner& rDirScanner, ScriptHintProvider& rHints)
401
5.56M
{
402
5.56M
    return std::make_unique<GreedyScriptChangeScanner>(rText, nDefaultScriptType, &rDirScanner,
403
5.56M
                                                       &rHints);
404
5.56M
}
405
406
/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */