feat: add RTL support in epub and txt readers (#1700)
Co-authored-by: Zach Nelson <zach@zdnelson.com>
This commit is contained in:
co-authored by
Zach Nelson
parent
cc2079a578
commit
f5bc554ae7
@@ -0,0 +1,234 @@
|
||||
#include "BidiUtils.h"
|
||||
|
||||
extern "C" {
|
||||
#include "minibidi.h"
|
||||
}
|
||||
|
||||
#undef when
|
||||
#undef otherwise
|
||||
|
||||
#include <Logging.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
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<const unsigned char*>(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<const unsigned char*>(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<const unsigned char*>(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<uint16_t>(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<std::string>& words, bool paragraphIsRtl,
|
||||
std::vector<uint16_t>& 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<const unsigned char*>(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<uint16_t>(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<uint16_t>(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<int>(nWords) - 1; i >= 0; i--) {
|
||||
visualOrder.push_back(static_cast<uint16_t>(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<uint16_t>(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<uint16_t>(i);
|
||||
}
|
||||
|
||||
if (firstNatural[w] == UINT16_MAX && isNaturalDirectionClass(bidi_class(line[i].wc))) {
|
||||
firstNatural[w] = static_cast<uint16_t>(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<uint16_t>(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
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace BidiUtils {
|
||||
|
||||
// Paragraph-level P2/P3: scan the first N strong chars per word to find base direction.
|
||||
inline constexpr int RTL_PARAGRAPH_PROBE_DEPTH = 5;
|
||||
|
||||
bool startsWithRtl(const char* utf8, int maxStrongChars = RTL_PARAGRAPH_PROBE_DEPTH);
|
||||
|
||||
int detectParagraphLevel(const char* utf8, int fallbackLevel = 0, int maxStrongChars = 64);
|
||||
|
||||
// paragraphLevel: -1 = auto-detect, 0 = LTR, 1 = RTL
|
||||
bool applyBidiVisual(const char* utf8, std::string& out, int paragraphLevel = -1);
|
||||
|
||||
bool computeVisualWordOrder(const std::vector<std::string>& words, bool paragraphIsRtl,
|
||||
std::vector<uint16_t>& visualOrder);
|
||||
|
||||
} // namespace BidiUtils
|
||||
@@ -0,0 +1,39 @@
|
||||
/* bidi_pairs.t — unified mirror + bracket table for CrossPoint.
|
||||
*
|
||||
* Replaces both mirroring.t and brackets.t. canonical.t is dropped
|
||||
* (fullwidth brackets are not used in Hebrew epub content).
|
||||
*
|
||||
* Each entry: {from, to, bracket_type}
|
||||
* bracket_type == BRACKo : `from` is an opening bracket
|
||||
* bracket_type == BRACKc : `from` is a closing bracket; `to` = opener
|
||||
* bracket_type == BRACKx : not a bracket pair — mirrored by rule L4 only
|
||||
*
|
||||
* mirror(c) → always returns `to` for any entry where c==from
|
||||
* bracket(c) → returns 0 for BRACKx; c for BRACKo; `to` (opener) for BRACKc
|
||||
*
|
||||
* Sorted ascending by `from` (binary search).
|
||||
*/
|
||||
|
||||
/* ASCII brackets — both bracket pairs AND L4 mirrors */
|
||||
{0x0028, 0x0029, BRACKo}, /* ( */
|
||||
{0x0029, 0x0028, BRACKc}, /* ) */
|
||||
{0x003C, 0x003E, BRACKo}, /* < */
|
||||
{0x003E, 0x003C, BRACKc}, /* > */
|
||||
{0x005B, 0x005D, BRACKo}, /* [ */
|
||||
{0x005D, 0x005B, BRACKc}, /* ] */
|
||||
{0x007B, 0x007D, BRACKo}, /* { */
|
||||
{0x007D, 0x007B, BRACKc}, /* } */
|
||||
|
||||
/* Angle quotation marks — L4 mirror only */
|
||||
{0x00AB, 0x00BB, BRACKx}, /* « → » */
|
||||
{0x00BB, 0x00AB, BRACKx}, /* » → « */
|
||||
|
||||
/* Curly quotes — L4 mirror only */
|
||||
{0x2018, 0x2019, BRACKx}, /* ' → ' */
|
||||
{0x2019, 0x2018, BRACKx}, /* ' → ' */
|
||||
{0x201C, 0x201D, BRACKx}, /* " → " */
|
||||
{0x201D, 0x201C, BRACKx}, /* " → " */
|
||||
|
||||
/* Single angle quotes */
|
||||
{0x2039, 0x203A, BRACKo}, /* ‹ → › */
|
||||
{0x203A, 0x2039, BRACKc}, /* › → ‹ */
|
||||
@@ -0,0 +1,115 @@
|
||||
/* bidiclasses.t — bidi class table for CrossPoint Hebrew/English epub.
|
||||
*
|
||||
* Coverage rationale:
|
||||
* Hebrew + English is the primary target. However, CrossPoint renders
|
||||
* Latin and Cyrillic scripts for many other languages, so these MUST be
|
||||
* classified as L (not fall through to ON) to avoid regression when they
|
||||
* appear adjacent to Hebrew runs.
|
||||
*
|
||||
* Scripts NOT in this table fall through to ON — correct per UAX#9 for
|
||||
* scripts CrossPoint's fonts don't support (CJK, Arabic, Devanagari, etc.)
|
||||
* ON is the right class for "unknown" — it behaves neutrally.
|
||||
*
|
||||
* Entries sorted ascending by first (binary search requirement).
|
||||
*/
|
||||
|
||||
/* ── ASCII C0 controls ────────────────────────────────────────────────── */
|
||||
{0x0000, 0x0008, BN},
|
||||
{0x0009, 0x0009, S},
|
||||
{0x000A, 0x000A, B},
|
||||
{0x000B, 0x000B, S},
|
||||
{0x000C, 0x000C, WS},
|
||||
{0x000D, 0x000D, B},
|
||||
{0x000E, 0x001B, BN},
|
||||
{0x001C, 0x001E, B},
|
||||
{0x001F, 0x001F, S},
|
||||
{0x0020, 0x0020, WS},
|
||||
|
||||
/* ── ASCII punctuation: number-adjacent classes ─────────────────────── */
|
||||
{0x0023, 0x0025, ET}, /* # $ % */
|
||||
{0x002B, 0x002B, ES}, /* + */
|
||||
{0x002C, 0x002C, CS}, /* , */
|
||||
{0x002D, 0x002D, ES}, /* - */
|
||||
{0x002E, 0x002F, CS}, /* . / */
|
||||
{0x0030, 0x0039, EN}, /* 0-9 */
|
||||
{0x003A, 0x003A, CS}, /* : */
|
||||
|
||||
/* ── Basic Latin letters ─────────────────────────────────────────────── */
|
||||
{0x0041, 0x005A, L}, /* A-Z */
|
||||
{0x0061, 0x007A, L}, /* a-z */
|
||||
|
||||
/* ── C1 / BN ────────────────────────────────────────────────────────── */
|
||||
{0x007F, 0x0084, BN},
|
||||
{0x0085, 0x0085, B},
|
||||
{0x0086, 0x009F, BN},
|
||||
|
||||
/* ── Latin-1 supplement ─────────────────────────────────────────────── */
|
||||
{0x00A0, 0x00A0, CS}, /* non-breaking space */
|
||||
{0x00A2, 0x00A5, ET}, /* ¢ £ ¤ ¥ */
|
||||
{0x00AA, 0x00AA, L},
|
||||
{0x00AD, 0x00AD, BN}, /* soft hyphen */
|
||||
{0x00B0, 0x00B1, ET}, /* ° ± */
|
||||
{0x00B2, 0x00B3, EN}, /* ² ³ */
|
||||
{0x00B5, 0x00B5, L},
|
||||
{0x00B9, 0x00B9, EN}, /* ¹ */
|
||||
{0x00BA, 0x00BA, L},
|
||||
{0x00C0, 0x00D6, L},
|
||||
{0x00D8, 0x00F6, L},
|
||||
{0x00F8, 0x02B8, L}, /* Latin Extended-A/B, IPA, Spacing Modifiers
|
||||
covers: Polish, Czech, Slovak, Turkish, etc. */
|
||||
|
||||
/* ── Combining Diacritical Marks (NSM) ──────────────────────────────── */
|
||||
/* Needed for decomposed Latin characters (some epubs use NFD/NFKD form) */
|
||||
{0x0300, 0x036F, NSM},
|
||||
|
||||
/* ── Cyrillic (L) ────────────────────────────────────────────────────── */
|
||||
/* Required: CrossPoint supports Russian, Ukrainian, Bulgarian, etc.
|
||||
Without these, Cyrillic chars fall to ON, breaking mixed Hebrew+Russian. */
|
||||
{0x0400, 0x04FF, L}, /* Cyrillic */
|
||||
{0x0500, 0x052F, L}, /* Cyrillic Supplement */
|
||||
|
||||
/* ── Hebrew vowel points / cantillation (NSM) ───────────────────────── */
|
||||
/* Do NOT remove: niqqud must be NSM or pointed Hebrew breaks after reorder */
|
||||
{0x0591, 0x05A1, NSM},
|
||||
{0x05A3, 0x05B9, NSM},
|
||||
{0x05BB, 0x05BD, NSM},
|
||||
{0x05BE, 0x05BE, R}, /* maqaf (Hebrew hyphen) */
|
||||
{0x05BF, 0x05BF, NSM},
|
||||
{0x05C0, 0x05C0, R}, /* paseq */
|
||||
{0x05C1, 0x05C2, NSM},
|
||||
{0x05C3, 0x05C3, R}, /* sof pasuq */
|
||||
{0x05C4, 0x05C4, NSM},
|
||||
|
||||
/* ── Hebrew letters ─────────────────────────────────────────────────── */
|
||||
{0x05D0, 0x05EA, R}, /* alef … tav */
|
||||
{0x05F0, 0x05F4, R}, /* alternative forms + geresh/gershayim */
|
||||
|
||||
/* ── Latin Extended Additional (L) ─────────────────────────────────── */
|
||||
/* Covers accented chars for Vietnamese, Welsh, Romanian, etc.
|
||||
Not currently rendered by CrossPoint fonts, but costs only 2 table rows. */
|
||||
{0x1E00, 0x1EFF, L},
|
||||
|
||||
/* ── Unicode directional format characters ─────────────────────────── */
|
||||
/* All must be present — UBA X-rules depend on them */
|
||||
{0x200B, 0x200D, BN}, /* ZWSP, ZWNJ, ZWJ */
|
||||
{0x200E, 0x200E, L}, /* LEFT-TO-RIGHT MARK */
|
||||
{0x200F, 0x200F, R}, /* RIGHT-TO-LEFT MARK */
|
||||
{0x2028, 0x2028, WS},
|
||||
{0x2029, 0x2029, B},
|
||||
{0x202A, 0x202A, LRE},
|
||||
{0x202B, 0x202B, RLE},
|
||||
{0x202C, 0x202C, PDF},
|
||||
{0x202D, 0x202D, LRO},
|
||||
{0x202E, 0x202E, RLO},
|
||||
{0x202F, 0x202F, WS}, /* narrow no-break space */
|
||||
{0x2060, 0x2063, BN},
|
||||
/* Unicode 6.3 isolate markers */
|
||||
{0x2066, 0x2066, LRI},
|
||||
{0x2067, 0x2067, RLI},
|
||||
{0x2068, 0x2068, FSI},
|
||||
{0x2069, 0x2069, PDI},
|
||||
{0x206A, 0x206F, BN},
|
||||
|
||||
|
||||
/* ── Byte Order Mark ────────────────────────────────────────────────── */
|
||||
{0xFEFF, 0xFEFF, BN},
|
||||
@@ -0,0 +1,565 @@
|
||||
/*
|
||||
* minibidi.c — Unicode Bidirectional Algorithm (UAX #9) for CrossPoint/ESP32C3
|
||||
*
|
||||
* Original author: Ahmad Khalifa (www.arabeyes.org, MIT licence)
|
||||
* Mintty changes: Thomas Wolff (rules N0, W7/L1/X9 fixes, isolates)
|
||||
*
|
||||
* UAX #9: https://www.unicode.org/reports/tr9/
|
||||
*/
|
||||
|
||||
#include "minibidi.h"
|
||||
|
||||
#define leastGreaterOdd(x) (((x) + 1) | 1)
|
||||
#define leastGreaterEven(x) (((x) + 2) & ~1)
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
* flip_runs / find_run (UAX#9 rule L2)
|
||||
* ═══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
static int find_run(uchar* levels, int start, int count, int tlevel) {
|
||||
for (int i = start; i < count; i++)
|
||||
if (tlevel <= levels[i]) return i;
|
||||
return count;
|
||||
}
|
||||
|
||||
static void flip_runs(bidi_char* from, uchar* levels, int tlevel, int count) {
|
||||
int i = 0, j = 0;
|
||||
while (i < count && j < count) {
|
||||
i = j = find_run(levels, i, count, tlevel);
|
||||
while (i < count && tlevel <= levels[i]) i++;
|
||||
for (int k = i - 1; k > j; k--, j++) {
|
||||
bidi_char tmp = from[k];
|
||||
from[k] = from[j];
|
||||
from[j] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
* bidi_class()
|
||||
* ═══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
uchar bidi_class(ucschar ch) {
|
||||
static const struct {
|
||||
ucschar first, last;
|
||||
uchar type;
|
||||
} lookup[] = {
|
||||
#include "bidiclasses.t"
|
||||
};
|
||||
|
||||
int i = -1, j = lengthof(lookup);
|
||||
while (j - i > 1) {
|
||||
int k = (i + j) / 2;
|
||||
if (ch < lookup[k].first)
|
||||
j = k;
|
||||
else if (ch > lookup[k].last)
|
||||
i = k;
|
||||
else
|
||||
return lookup[k].type;
|
||||
}
|
||||
return ON; /* correct UAX#9 fallback for unlisted characters */
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
* Character class predicates
|
||||
* ═══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
bool is_rtl_class(uchar bc) {
|
||||
const int mask = (1 << R) | (1 << AL) | (1 << RLE) | (1 << RLO) | (1 << RLI) | (1 << FSI);
|
||||
return (mask >> bc) & 1;
|
||||
}
|
||||
|
||||
static inline bool is_NI(uchar bc) {
|
||||
const int mask = (1 << B) | (1 << S) | (1 << WS) | (1 << ON) | (1 << FSI) | (1 << LRI) | (1 << RLI) | (1 << PDI);
|
||||
return (mask >> bc) & 1;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
* Unified bracket + mirror table (bidi_pairs.t)
|
||||
*
|
||||
* Replaces both brackets.t and mirroring.t. canonical.t is dropped.
|
||||
* ═══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
enum { BRACKx = 0, BRACKo = 1, BRACKc = 2 };
|
||||
|
||||
typedef struct {
|
||||
ucschar from, to;
|
||||
uchar bracket; /* BRACKo / BRACKc / BRACKx */
|
||||
} bidi_pair;
|
||||
|
||||
static const bidi_pair pairs[] = {
|
||||
#include "bidi_pairs.t"
|
||||
};
|
||||
|
||||
/* Binary search over the pairs table */
|
||||
static const bidi_pair* find_pair(ucschar c) {
|
||||
int i = -1, j = lengthof(pairs);
|
||||
while (j - i > 1) {
|
||||
int k = (i + j) / 2;
|
||||
if (c == pairs[k].from)
|
||||
return &pairs[k];
|
||||
else if (c < pairs[k].from)
|
||||
j = k;
|
||||
else
|
||||
i = k;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* bracket(c):
|
||||
* 0 → not a bracket
|
||||
* c → opening bracket
|
||||
* opener → closing bracket (returns the matching opener)
|
||||
*/
|
||||
static ucschar bracket(ucschar c) {
|
||||
const bidi_pair* p = find_pair(c);
|
||||
if (!p || p->bracket == BRACKx) return 0;
|
||||
return (p->bracket == BRACKo) ? c : p->to;
|
||||
}
|
||||
|
||||
/*
|
||||
* mirror(c): returns the mirrored form for rule L4,
|
||||
* or c unchanged if not in the table.
|
||||
*/
|
||||
ucschar mirror(ucschar c) {
|
||||
const bidi_pair* p = find_pair(c);
|
||||
return p ? p->to : c;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
* Directional Status Stack
|
||||
* (replaces GCC nested functions — ESP32C3 has no executable stack)
|
||||
* ═══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
typedef struct {
|
||||
uchar emb[BIDI_MAX_LINE + 1];
|
||||
uchar ovr[BIDI_MAX_LINE + 1];
|
||||
bool isol[BIDI_MAX_LINE + 1];
|
||||
int top;
|
||||
} DirStatusStack;
|
||||
|
||||
static inline void dss_init(DirStatusStack* s) { s->top = -1; }
|
||||
static inline int dss_count(const DirStatusStack* s) { return s->top + 1; }
|
||||
|
||||
static inline void dss_push(DirStatusStack* s, uchar emb, uchar ovr, bool isol) {
|
||||
if (s->top < BIDI_MAX_LINE) {
|
||||
++s->top;
|
||||
s->emb[s->top] = emb;
|
||||
s->ovr[s->top] = ovr;
|
||||
s->isol[s->top] = isol;
|
||||
}
|
||||
}
|
||||
|
||||
static inline void dss_pop(DirStatusStack* s, uchar* emb, uchar* ovr, bool* isol) {
|
||||
if (s->top >= 0) s->top--;
|
||||
if (s->top >= 0) {
|
||||
*emb = s->emb[s->top];
|
||||
*ovr = s->ovr[s->top];
|
||||
*isol = s->isol[s->top];
|
||||
} else {
|
||||
/* Stack underflow: return safe defaults (should not happen in valid input) */
|
||||
*emb = 0; /* LTR base level */
|
||||
*ovr = ON; /* No override */
|
||||
*isol = false; /* No isolate */
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
* do_bidi() — The main UAX#9 algorithm
|
||||
* ═══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
int do_bidi(bool autodir, int paragraphLevel, bidi_char* line, int count) {
|
||||
if (count > BIDI_MAX_LINE) count = BIDI_MAX_LINE;
|
||||
|
||||
uchar currentEmbedding, currentOverride;
|
||||
bool currentIsolate;
|
||||
int i, j;
|
||||
|
||||
/* Fixed-size working arrays — no VLAs, no heap */
|
||||
uchar types[BIDI_MAX_LINE];
|
||||
uchar levels[BIDI_MAX_LINE];
|
||||
bool skip[BIDI_MAX_LINE];
|
||||
|
||||
/* ── P2/P3: detect paragraph level ── */
|
||||
int isolateLevel = 0, resLevel = -1;
|
||||
bool hasRTL = false;
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
uchar type = bidi_class(line[i].wc);
|
||||
if (type == LRI || type == RLI || type == FSI) {
|
||||
hasRTL = true;
|
||||
isolateLevel++;
|
||||
} else if (type == PDI) {
|
||||
hasRTL = true;
|
||||
if (isolateLevel > 0) isolateLevel--;
|
||||
} else if (isolateLevel == 0) {
|
||||
if (type == R || type == AL) {
|
||||
hasRTL = true;
|
||||
if (resLevel < 0) resLevel = 1;
|
||||
break;
|
||||
} else if (type == RLE || type == LRE || type == RLO || type == LRO || type == PDF) {
|
||||
hasRTL = true;
|
||||
if (resLevel >= 0) break;
|
||||
} else if (type == L) {
|
||||
if (resLevel < 0) resLevel = 0;
|
||||
} else if (type == AN)
|
||||
hasRTL = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (autodir) {
|
||||
if (resLevel >= 0) paragraphLevel = resLevel;
|
||||
} else
|
||||
resLevel = paragraphLevel;
|
||||
|
||||
/* Fast path: pure LTR line with LTR paragraph — nothing to reorder */
|
||||
if (!hasRTL && !paragraphLevel) return 0;
|
||||
|
||||
/* ── X1–X8: compute embedding levels ── */
|
||||
currentEmbedding = (uchar)paragraphLevel;
|
||||
currentOverride = ON;
|
||||
currentIsolate = false;
|
||||
isolateLevel = 0;
|
||||
|
||||
DirStatusStack dss;
|
||||
dss_init(&dss);
|
||||
dss_push(&dss, currentEmbedding, currentOverride, currentIsolate);
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
uchar tempType = bidi_class(line[i].wc);
|
||||
levels[i] = currentEmbedding;
|
||||
|
||||
/* FSI: look-ahead to resolve direction */
|
||||
if (tempType == FSI) {
|
||||
int lvl = 0;
|
||||
tempType = LRI;
|
||||
for (int k = i + 1; k < count; k++) {
|
||||
uchar kt = bidi_class(line[k].wc);
|
||||
if (kt == FSI || kt == RLI || kt == LRI)
|
||||
lvl++;
|
||||
else if (kt == PDI) {
|
||||
if (lvl)
|
||||
lvl--;
|
||||
else
|
||||
break;
|
||||
} else if (kt == R || kt == AL) {
|
||||
tempType = RLI;
|
||||
break;
|
||||
} else if (kt == L)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (tempType) {
|
||||
when RLE : currentEmbedding = leastGreaterOdd(currentEmbedding);
|
||||
currentOverride = ON;
|
||||
currentIsolate = false;
|
||||
dss_push(&dss, currentEmbedding, currentOverride, currentIsolate);
|
||||
when LRE : currentEmbedding = leastGreaterEven(currentEmbedding);
|
||||
currentOverride = ON;
|
||||
currentIsolate = false;
|
||||
dss_push(&dss, currentEmbedding, currentOverride, currentIsolate);
|
||||
when RLO : currentEmbedding = leastGreaterOdd(currentEmbedding);
|
||||
currentOverride = R;
|
||||
currentIsolate = false;
|
||||
dss_push(&dss, currentEmbedding, currentOverride, currentIsolate);
|
||||
when LRO : currentEmbedding = leastGreaterEven(currentEmbedding);
|
||||
currentOverride = L;
|
||||
currentIsolate = false;
|
||||
dss_push(&dss, currentEmbedding, currentOverride, currentIsolate);
|
||||
when RLI : if (currentOverride != ON) tempType = currentOverride;
|
||||
currentEmbedding = leastGreaterOdd(currentEmbedding);
|
||||
isolateLevel++;
|
||||
currentOverride = ON;
|
||||
currentIsolate = true;
|
||||
dss_push(&dss, currentEmbedding, currentOverride, currentIsolate);
|
||||
when LRI : if (currentOverride != ON) tempType = currentOverride;
|
||||
currentEmbedding = leastGreaterEven(currentEmbedding);
|
||||
isolateLevel++;
|
||||
currentOverride = ON;
|
||||
currentIsolate = true;
|
||||
dss_push(&dss, currentEmbedding, currentOverride, currentIsolate);
|
||||
when PDF : if (!currentIsolate && dss_count(&dss) >= 2)
|
||||
dss_pop(&dss, ¤tEmbedding, ¤tOverride, ¤tIsolate);
|
||||
levels[i] = currentEmbedding;
|
||||
when PDI : if (isolateLevel > 0) {
|
||||
while (!currentIsolate && dss_count(&dss) > 0)
|
||||
dss_pop(&dss, ¤tEmbedding, ¤tOverride, ¤tIsolate);
|
||||
dss_pop(&dss, ¤tEmbedding, ¤tOverride, ¤tIsolate);
|
||||
isolateLevel--;
|
||||
}
|
||||
if (currentOverride != ON) tempType = currentOverride;
|
||||
levels[i] = currentEmbedding;
|
||||
when WS : case S:
|
||||
if (currentOverride != ON) tempType = currentOverride;
|
||||
otherwise : if (currentOverride != ON) tempType = currentOverride;
|
||||
}
|
||||
types[i] = tempType;
|
||||
}
|
||||
|
||||
/* ── X9: mask format chars as NSM (Wolff fix: NSM not BN) ── */
|
||||
for (i = 0; i < count; i++) {
|
||||
switch (types[i]) {
|
||||
when RLE : case LRE:
|
||||
case RLO:
|
||||
case LRO:
|
||||
case PDF:
|
||||
case BN:
|
||||
types[i] = NSM;
|
||||
skip[i] = true;
|
||||
otherwise:
|
||||
skip[i] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── W1: NSM inherits type of previous char (or sor) ── */
|
||||
if (types[0] == NSM) types[0] = (paragraphLevel & 1) ? R : L;
|
||||
for (i = 1; i < count; i++) {
|
||||
if (types[i] == NSM) {
|
||||
switch (types[i - 1]) {
|
||||
when LRI : case RLI:
|
||||
case FSI:
|
||||
case PDI:
|
||||
types[i] = ON;
|
||||
otherwise : types[i] = types[i - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── W2: EN after AL → AN ── */
|
||||
for (i = 0; i < count; i++) {
|
||||
if (types[i] == EN) {
|
||||
for (j = i - 1; j >= 0; j--) {
|
||||
uchar t = types[j];
|
||||
if (t == AL) {
|
||||
types[i] = AN;
|
||||
break;
|
||||
}
|
||||
if (t == R || t == L) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── W3: AL → R ── */
|
||||
for (i = 0; i < count; i++)
|
||||
if (types[i] == AL) types[i] = R;
|
||||
|
||||
/* ── W4: single ES/CS between same numerals → that numeral type ── */
|
||||
for (i = 1; i + 1 < count; i++) {
|
||||
if (types[i] == ES || types[i] == CS) {
|
||||
int prev = i - 1;
|
||||
while (prev >= 0 && skip[prev]) prev--;
|
||||
int next = i + 1;
|
||||
while (next < count && skip[next]) next++;
|
||||
if (prev >= 0 && next < count) {
|
||||
if (types[i] == ES && types[prev] == EN && types[next] == EN) types[i] = EN;
|
||||
if (types[i] == CS) {
|
||||
if (types[prev] == EN && types[next] == EN) types[i] = EN;
|
||||
if (types[prev] == AN && types[next] == AN) types[i] = AN;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── W5: ET adjacent to EN → EN (forward pass) ── */
|
||||
for (i = 0; i < count; i++) {
|
||||
if (skip[i] || types[i] != ET) continue;
|
||||
for (j = i; j < count; j++) {
|
||||
if (skip[j]) continue;
|
||||
if (types[j] == ET) continue;
|
||||
if (types[j] == EN) types[i] = EN;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* W5 backward pass */
|
||||
for (i = count - 1; i >= 0; i--) {
|
||||
if (skip[i] || types[i] != ET) continue;
|
||||
for (j = i; j >= 0; j--) {
|
||||
if (skip[j]) continue;
|
||||
if (types[j] == ET) continue;
|
||||
if (types[j] == EN) types[i] = EN;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── W6: remaining ES, ET, CS → ON ── */
|
||||
for (i = 0; i < count; i++)
|
||||
if (types[i] == ES || types[i] == ET || types[i] == CS) types[i] = ON;
|
||||
|
||||
/* ── W7: EN after last strong L (back to sor) → L ── */
|
||||
{
|
||||
uchar last_strong = (paragraphLevel & 1) ? R : L;
|
||||
for (i = 0; i < count; i++) {
|
||||
if (skip[i]) continue;
|
||||
if (types[i] == L || types[i] == R) last_strong = types[i];
|
||||
if (types[i] == EN && last_strong == L) types[i] = L;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── N0: bracket pair handling ── */
|
||||
{
|
||||
uchar e = (paragraphLevel & 1) ? R : L;
|
||||
uchar o = (e == L) ? R : L;
|
||||
#define BRACKET_STACK 63
|
||||
struct {
|
||||
ucschar opener;
|
||||
int pos;
|
||||
} openers[BRACKET_STACK];
|
||||
int opener_top = 0;
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
if (skip[i]) continue;
|
||||
ucschar bc = bracket(line[i].wc);
|
||||
if (!bc) continue;
|
||||
|
||||
if (bc == line[i].wc) {
|
||||
/* Opening bracket */
|
||||
if (opener_top < BRACKET_STACK) {
|
||||
openers[opener_top].opener = line[i].wc;
|
||||
openers[opener_top].pos = i;
|
||||
opener_top++;
|
||||
}
|
||||
} else {
|
||||
/* Closing bracket: find matching opener */
|
||||
int k;
|
||||
for (k = opener_top - 1; k >= 0; k--)
|
||||
if (openers[k].opener == bc) break;
|
||||
if (k < 0) continue;
|
||||
|
||||
int open_pos = openers[k].pos;
|
||||
opener_top = k;
|
||||
|
||||
bool found_e = false, found_o = false;
|
||||
for (int m = open_pos + 1; m < i; m++) {
|
||||
if (skip[m]) continue;
|
||||
uchar t = types[m];
|
||||
if (t == EN || t == AN) t = R;
|
||||
if (t == R || t == AL) {
|
||||
if (e == R)
|
||||
found_e = true;
|
||||
else
|
||||
found_o = true;
|
||||
} else if (t == L) {
|
||||
if (e == L)
|
||||
found_e = true;
|
||||
else
|
||||
found_o = true;
|
||||
}
|
||||
}
|
||||
|
||||
uchar dir;
|
||||
if (found_e) {
|
||||
dir = e;
|
||||
} else if (found_o) {
|
||||
uchar ctx = e;
|
||||
for (int m = open_pos - 1; m >= 0; m--) {
|
||||
if (skip[m]) continue;
|
||||
uchar t = types[m];
|
||||
if (t == EN || t == AN) t = R;
|
||||
if (t == R || t == AL) {
|
||||
ctx = R;
|
||||
break;
|
||||
} else if (t == L) {
|
||||
ctx = L;
|
||||
break;
|
||||
}
|
||||
}
|
||||
dir = (ctx == o) ? o : e;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
types[open_pos] = dir;
|
||||
types[i] = dir;
|
||||
for (int m = open_pos + 1; m < i; m++)
|
||||
if (is_NI(types[m])) types[m] = dir;
|
||||
}
|
||||
}
|
||||
#undef BRACKET_STACK
|
||||
}
|
||||
|
||||
/* ── N1: NI between same-direction strongs → that direction ── */
|
||||
for (i = 0; i < count; i++) {
|
||||
if (skip[i] || !is_NI(types[i])) continue;
|
||||
int end = i;
|
||||
while (end + 1 < count && (skip[end + 1] || is_NI(types[end + 1]))) end++;
|
||||
|
||||
uchar prev_strong = (paragraphLevel & 1) ? R : L;
|
||||
for (j = i - 1; j >= 0; j--) {
|
||||
if (skip[j]) continue;
|
||||
uchar t = types[j];
|
||||
if (t == EN || t == AN) t = R;
|
||||
if (t == R || t == L) {
|
||||
prev_strong = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
uchar next_strong = (paragraphLevel & 1) ? R : L;
|
||||
for (j = end + 1; j < count; j++) {
|
||||
if (skip[j]) continue;
|
||||
uchar t = types[j];
|
||||
if (t == EN || t == AN) t = R;
|
||||
if (t == R || t == L) {
|
||||
next_strong = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (prev_strong == next_strong)
|
||||
for (j = i; j <= end; j++) types[j] = prev_strong;
|
||||
i = end;
|
||||
}
|
||||
|
||||
/* ── N2: remaining NI → embedding direction ── */
|
||||
for (i = 0; i < count; i++)
|
||||
if (is_NI(types[i])) types[i] = (levels[i] & 1) ? R : L;
|
||||
|
||||
/* ── I1/I2: adjust levels ── */
|
||||
for (i = 0; i < count; i++) {
|
||||
if (skip[i]) continue;
|
||||
if ((levels[i] & 1) == 0) {
|
||||
if (types[i] == R)
|
||||
levels[i] += 1;
|
||||
else if (types[i] == AN || types[i] == EN)
|
||||
levels[i] += 2;
|
||||
} else {
|
||||
if (types[i] == L || types[i] == EN || types[i] == AN) levels[i] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── L1: reset trailing/segment whitespace to paragraph level ── */
|
||||
for (i = count - 1; i >= 0; i--) {
|
||||
if (skip[i]) continue;
|
||||
uchar t = types[i];
|
||||
if (t == WS || t == S || t == B)
|
||||
levels[i] = (uchar)paragraphLevel;
|
||||
else
|
||||
break;
|
||||
}
|
||||
for (i = 0; i < count; i++) {
|
||||
if (types[i] == S) {
|
||||
levels[i] = (uchar)paragraphLevel;
|
||||
for (j = i - 1; j >= 0; j--) {
|
||||
if (skip[j]) continue;
|
||||
if (types[j] == WS || types[j] == BN)
|
||||
levels[j] = (uchar)paragraphLevel;
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── L2: reverse from highest level down to lowest odd ── */
|
||||
uchar max_level = (uchar)paragraphLevel, min_odd = 255;
|
||||
for (i = 0; i < count; i++) {
|
||||
if (levels[i] > max_level) max_level = levels[i];
|
||||
if ((levels[i] & 1) && levels[i] < min_odd) min_odd = levels[i];
|
||||
}
|
||||
for (int level = max_level; level >= (int)min_odd; level--) flip_runs(line, levels, level, count);
|
||||
|
||||
/* ── L4: mirror characters in RTL runs ── */
|
||||
for (i = 0; i < count; i++)
|
||||
if (levels[i] & 1) line[i].wc = mirror(line[i].wc);
|
||||
|
||||
return paragraphLevel;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
#ifndef MINIBIDI_H
|
||||
#define MINIBIDI_H
|
||||
|
||||
/*
|
||||
* minibidi.h — standalone header for ESP32C3 BiDi calculations
|
||||
*
|
||||
* Derived from [mintty](https://github.com/mintty/mintty/) (Thomas Wolff, MIT licence).
|
||||
* Stripped of: Arabic shaping, box-drawing mirror, terminal dependencies,
|
||||
* GCC nested functions, VLAs, and non-Hebrew/English Unicode data.
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ── Basic types ─────────────────────────────────────────────────────── */
|
||||
typedef uint8_t uchar;
|
||||
typedef uint32_t ucschar; /* Unicode codepoint; BMP-only content fits uint16_t
|
||||
but uint32_t is safer and ESP32C3 is 32-bit anyway */
|
||||
|
||||
/* ── Convenience macros ──────────────────────────────────────────────── */
|
||||
#define lengthof(a) ((int)(sizeof(a) / sizeof(*(a))))
|
||||
|
||||
/* PuTTY/mintty switch-case style — kept for readability of algorithm */
|
||||
#define when \
|
||||
break; \
|
||||
case
|
||||
#define otherwise \
|
||||
break; \
|
||||
default
|
||||
|
||||
/* Maximum line length the algorithm will process.
|
||||
Adjust to your actual screen width. Stack cost = ~5×MAX bytes. */
|
||||
#define BIDI_MAX_LINE 128
|
||||
|
||||
/* ── bidi_char ───────────────────────────────────────────────────────── */
|
||||
/* origwc: the codepoint as it came from the epub text stream
|
||||
wc: working codepoint (may be replaced by mirrored form after do_bidi)
|
||||
index: original logical position, so the caller can reorder glyphs */
|
||||
typedef struct {
|
||||
ucschar origwc;
|
||||
ucschar wc;
|
||||
uint16_t index;
|
||||
} bidi_char;
|
||||
|
||||
/* ── Bidi character classes (UAX #9) ────────────────────────────────── */
|
||||
enum {
|
||||
L, /* Left-to-Right */
|
||||
LRE, /* Left-to-Right Embedding */
|
||||
LRO, /* Left-to-Right Override */
|
||||
R, /* Right-to-Left */
|
||||
AL, /* Right-to-Left Arabic */
|
||||
RLE, /* Right-to-Left Embedding */
|
||||
RLO, /* Right-to-Left Override */
|
||||
PDF, /* Pop Directional Format */
|
||||
EN, /* European Number */
|
||||
ES, /* European Number Separator */
|
||||
ET, /* European Number Terminator */
|
||||
AN, /* Arabic Number */
|
||||
CS, /* Common Number Separator */
|
||||
NSM, /* Non-Spacing Mark */
|
||||
BN, /* Boundary Neutral */
|
||||
B, /* Paragraph Separator */
|
||||
S, /* Segment Separator */
|
||||
WS, /* Whitespace */
|
||||
ON, /* Other Neutrals */
|
||||
/* Unicode 6.3 isolate types */
|
||||
LRI, /* Left-to-Right Isolate */
|
||||
RLI, /* Right-to-Left Isolate */
|
||||
FSI, /* First Strong Isolate */
|
||||
PDI, /* Pop Directional Isolate */
|
||||
};
|
||||
|
||||
/* ── Public API ──────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* bidi_class(ch)
|
||||
* Returns the UAX#9 bidi class of Unicode codepoint ch.
|
||||
* Unknown characters return ON (correct per spec).
|
||||
*/
|
||||
uchar bidi_class(ucschar ch);
|
||||
|
||||
/*
|
||||
* is_rtl_class(bc)
|
||||
* Returns true if bidi class bc can cause RTL reordering.
|
||||
* Use to fast-skip lines with no RTL content.
|
||||
*/
|
||||
bool is_rtl_class(uchar bc);
|
||||
|
||||
/*
|
||||
* mirror(ch)
|
||||
* Returns the mirrored form of Unicode codepoint ch for UAX#9 rule L4.
|
||||
* If no mirror exists, returns ch unchanged.
|
||||
*/
|
||||
ucschar mirror(ucschar ch);
|
||||
|
||||
/*
|
||||
* do_bidi(autodir, paragraphLevel, line, count)
|
||||
*
|
||||
* Applies UAX#9 Bidirectional Algorithm (rules P–L) to `line[0..count-1]`.
|
||||
* Reorders the array in-place; sets line[i].wc to the mirrored form where
|
||||
* required (rule L4). Returns the resolved paragraph level (0=LTR, 1=RTL),
|
||||
* or 0 if the line was left-to-right and no reordering was done.
|
||||
*
|
||||
* autodir: true → detect paragraph direction from content (P2/P3)
|
||||
* false → use paragraphLevel as-is
|
||||
* paragraphLevel: 0 = LTR, 1 = RTL. Ignored when autodir=true unless
|
||||
* the content has no strong type (used as fallback).
|
||||
*
|
||||
* count must be ≤ BIDI_MAX_LINE; lines longer than that are silently
|
||||
* truncated to BIDI_MAX_LINE before processing.
|
||||
*/
|
||||
int do_bidi(bool autodir, int paragraphLevel, bidi_char* line, int count);
|
||||
|
||||
#endif /* MINIBIDI_H */
|
||||
Reference in New Issue
Block a user