#include "BidiUtils.h" extern "C" { #include "minibidi.h" } #undef when #undef otherwise #include #include #include namespace { bool isNaturalDirectionClass(const uchar cls) { switch (cls) { case L: case R: case AL: case EN: case AN: return true; default: return false; } } } // namespace namespace BidiUtils { bool startsWithRtl(const char* utf8, int maxStrongChars) { if (!utf8 || maxStrongChars <= 0) return false; auto* p = reinterpret_cast(utf8); int checked = 0; while (*p) { const uint32_t cp = utf8NextCodepoint(&p); if (!cp || cp == REPLACEMENT_GLYPH) break; const uchar cls = bidi_class(cp); if (cls == R || cls == AL) return true; if (cls == L) return false; checked++; if (checked >= maxStrongChars) break; } return false; } int detectParagraphLevel(const char* utf8, const int fallbackLevel, const int maxStrongChars) { if (!utf8 || maxStrongChars <= 0) return fallbackLevel & 1; auto* p = reinterpret_cast(utf8); int checked = 0; while (*p) { const uint32_t cp = utf8NextCodepoint(&p); if (!cp || cp == REPLACEMENT_GLYPH) break; const uchar cls = bidi_class(cp); if (cls == R || cls == AL) return 1; if (cls == L) return 0; checked++; if (checked >= maxStrongChars) break; } return fallbackLevel & 1; } bool applyBidiVisual(const char* utf8, std::string& out, int paragraphLevel) { if (!utf8 || !*utf8) return false; static bidi_char line[BIDI_MAX_LINE]; int count = 0; auto* p = reinterpret_cast(utf8); while (*p) { if (count >= BIDI_MAX_LINE) { LOG_DBG("BIDI", "applyBidiVisual: input exceeds BIDI_MAX_LINE (%d chars), returning unprocessed", BIDI_MAX_LINE); return false; } const uint32_t cp = utf8NextCodepoint(&p); if (!cp || cp == REPLACEMENT_GLYPH) break; line[count].origwc = line[count].wc = cp; line[count].index = static_cast(count); count++; } if (!count) return false; const bool autodir = (paragraphLevel < 0); const int level = autodir ? 0 : (paragraphLevel & 1); do_bidi(autodir, level, line, count); out.clear(); out.reserve(std::strlen(utf8)); for (int i = 0; i < count; i++) { utf8AppendCodepoint(line[i].wc, out); } return true; } bool computeVisualWordOrder(const std::vector& words, bool paragraphIsRtl, std::vector& visualOrder) { visualOrder.clear(); const size_t nWords = words.size(); if (nWords <= 1 || nWords > BIDI_MAX_LINE) return false; static bidi_char line[BIDI_MAX_LINE]; int count = 0; bool truncated = false; for (size_t w = 0; w < nWords && !truncated; w++) { auto* p = reinterpret_cast(words[w].c_str()); while (*p) { if (count >= BIDI_MAX_LINE) { truncated = true; break; } const uint32_t cp = utf8NextCodepoint(&p); if (!cp || cp == REPLACEMENT_GLYPH) break; line[count].origwc = line[count].wc = cp; line[count].index = static_cast(w); count++; } if (!truncated && w + 1 < nWords) { if (count >= BIDI_MAX_LINE) { truncated = true; break; } line[count].origwc = line[count].wc = ' '; line[count].index = static_cast(nWords); count++; } } if (truncated || count == 0) return false; // Fast-path for homogeneous lines: skip UAX#9 if there's no mixing. bool hasL = false, hasR = false; for (int i = 0; i < count; i++) { uchar bc = bidi_class(line[i].wc); if (bc == L || bc == EN || bc == AN) hasL = true; else if (bc == R || bc == AL) hasR = true; } // Purely LTR line in RTL paragraph: identity order, but we might still need to reorder // if some characters are mirrored or neutral resolution differs. // Actually, UAX#9 rule L1/L2 says purely LTR in RTL para stays as is (identity). // Purely RTL line: just reverse the words. if (!hasL && hasR && paragraphIsRtl) { visualOrder.reserve(nWords); for (int i = static_cast(nWords) - 1; i >= 0; i--) { visualOrder.push_back(static_cast(i)); } return true; } if (!hasR) { if (!paragraphIsRtl) { // Pure LTR in LTR paragraph: nothing to do. return false; } // Pure LTR in RTL paragraph: no word reordering, but must use the // willReorder (left-to-right) positioning path, not the RTL right-to-left path. visualOrder.reserve(nWords); for (size_t i = 0; i < nWords; i++) { visualOrder.push_back(static_cast(i)); } return true; } do_bidi(/*autodir=*/false, paragraphIsRtl ? 1 : 0, line, count); uint16_t firstAny[BIDI_MAX_LINE]; uint16_t firstNatural[BIDI_MAX_LINE]; for (size_t w = 0; w < nWords; w++) { firstAny[w] = UINT16_MAX; firstNatural[w] = UINT16_MAX; } for (int i = 0; i < count; i++) { const uint16_t w = line[i].index; if (w >= nWords) continue; if (firstAny[w] == UINT16_MAX) { firstAny[w] = static_cast(i); } if (firstNatural[w] == UINT16_MAX && isNaturalDirectionClass(bidi_class(line[i].wc))) { firstNatural[w] = static_cast(i); } } visualOrder.reserve(nWords); for (int i = 0; i < count; i++) { const uint16_t w = line[i].index; if (w >= nWords) continue; const uint16_t anchor = firstNatural[w] != UINT16_MAX ? firstNatural[w] : firstAny[w]; if (anchor == UINT16_MAX) { visualOrder.clear(); return false; } if (anchor == static_cast(i)) { visualOrder.push_back(w); } } if (visualOrder.size() != nWords) { visualOrder.clear(); return false; } // Check if the order is exactly the same as the original input bool needsReorder = false; for (size_t i = 0; i < nWords; i++) { if (visualOrder[i] != i) { needsReorder = true; break; } } if (!needsReorder) { visualOrder.clear(); return false; } return true; } } // namespace BidiUtils