Files
Crosspoint/lib/MiniBidi/minibidi.c
T

905 lines
32 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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;
}
/* ═══════════════════════════════════════════════════════════════════════
* Arabic contextual shaping — do_shape()
*
* Ported from mintty src/minibidi.c (https://github.com/mintty/mintty),
* original author Ahmad Khalifa (www.arabeyes.org), maintained by
* Thomas Wolff. MIT licence.
*
* CrossPoint deviations from the upstream code, each marked inline:
* 1. Joining-context lookups skip NSM marks (harakat). mintty stores
* combining marks out-of-line in terminal cells, so they never appear
* in its bidi_char array; in CrossPoint they are real array entries
* and must be transparent for joining (Unicode joining type T).
* 2. The Alef absorbed into a Lam-Alef ligature is overwritten with
* LIGATURE_PLACEHOLDER instead of a space: a terminal must keep the
* cell, a proportional-text renderer must drop the character.
* 3. The STYPE/SISOLATED macros became functions backed by a second
* lookup table covering Perso-Arabic letters outside mintty's native
* U+0621U+064A range (Farsi پ چ ژ گ, Urdu ٹ ڈ ڑ ں ہ ے, plus Sindhi/
* Pashto/Kurdish letters). Joining types are sourced from Unicode
* ArabicShaping.txt and presentation forms from UnicodeData.txt
* (Arabic Presentation Forms-A), both Unicode 17.0.0. Letters with a
* joining type but no presentation-form codepoints keep their base
* codepoint (neighbours still shape correctly around them).
* U+200C/U+200D also get their ArabicShaping.txt types (U and C) so
* that in-stream ZWJ/ZWNJ — which mintty never has in its array —
* affect adjacency the same way the joiners flags do.
* ═══════════════════════════════════════════════════════════════════════ */
/* Shaping Types (mintty) */
enum {
SL, /* Left-Joining, doesn't exist in U+0600 - U+06FF */
SR, /* Right-Joining, i.e. has Isolated, Final */
SD, /* Dual-Joining, i.e. has Isolated, Final, Initial, Medial */
SU, /* Non-Joining */
SC /* Join-Causing, like U+0640 (TATWEEL) */
};
typedef struct {
uchar type;
uchar form_b; /* isolated form = 0xFE00 + form_b (Presentation Forms-B) */
} shape_node;
/* Kept near the actual table, for verification. (mintty) */
enum { SHAPE_FIRST = 0x621, SHAPE_LAST = 0x64A };
/* mintty's shapetypes[] — verbatim */
static const shape_node shapetypes[] = {
/* index, Typ, Iso, Ligature Index */
/* 621 */ {SU, 0x80},
/* 622 */ {SR, 0x81},
/* 623 */ {SR, 0x83},
/* 624 */ {SR, 0x85},
/* 625 */ {SR, 0x87},
/* 626 */ {SD, 0x89},
/* 627 */ {SR, 0x8D},
/* 628 */ {SD, 0x8F},
/* 629 */ {SR, 0x93},
/* 62A */ {SD, 0x95},
/* 62B */ {SD, 0x99},
/* 62C */ {SD, 0x9D},
/* 62D */ {SD, 0xA1},
/* 62E */ {SD, 0xA5},
/* 62F */ {SR, 0xA9},
/* 630 */ {SR, 0xAB},
/* 631 */ {SR, 0xAD},
/* 632 */ {SR, 0xAF},
/* 633 */ {SD, 0xB1},
/* 634 */ {SD, 0xB5},
/* 635 */ {SD, 0xB9},
/* 636 */ {SD, 0xBD},
/* 637 */ {SD, 0xC1},
/* 638 */ {SD, 0xC5},
/* 639 */ {SD, 0xC9},
/* 63A */ {SD, 0xCD},
/* 63B */ {SU, 0x0},
/* 63C */ {SU, 0x0},
/* 63D */ {SU, 0x0},
/* 63E */ {SU, 0x0},
/* 63F */ {SU, 0x0},
/* 640 */ {SC, 0x0},
/* 641 */ {SD, 0xD1},
/* 642 */ {SD, 0xD5},
/* 643 */ {SD, 0xD9},
/* 644 */ {SD, 0xDD},
/* 645 */ {SD, 0xE1},
/* 646 */ {SD, 0xE5},
/* 647 */ {SD, 0xE9},
/* 648 */ {SR, 0xED},
/* 649 */ {SR, 0xEF},
/* 64A */ {SD, 0xF1}};
/* ── CrossPoint extension: joining types outside U+0621U+064A ──────────
* Source: Unicode ArabicShaping.txt (joining type column). Ranges of
* letters sharing one type are collapsed. Sorted ascending (binary
* search). Anything not listed here or in shapetypes[] is SU. */
static const struct {
ucschar first, last;
uchar type;
} xjointypes[] = {
{0x0620, 0x0620, SD}, /* kashmiri yeh */
{0x066E, 0x066F, SD}, /* dotless beh/qaf */
{0x0671, 0x0673, SR}, /* alef wasla + wavy-hamza alefs */
{0x0675, 0x0677, SR}, /* high-hamza alef/waw */
{0x0678, 0x0687, SD}, /* high-hamza yeh, beh group (ٹ ٺ ٻ … پ), hah group (چ ڇ) */
{0x0688, 0x0699, SR}, /* dal group (ڈ ڊ ڌ …), reh group (ڑ ړ ژ …) */
{0x069A, 0x06BF, SD}, /* seen/sad/feh/qaf/kaf/gaf/lam/noon groups (ک گ ں ھ …) */
{0x06C0, 0x06C0, SR}, /* heh with yeh above */
{0x06C1, 0x06C2, SD}, /* heh goal (ہ) */
{0x06C3, 0x06CB, SR}, /* teh marbuta goal, waw group (ۆ ۇ ۋ …) */
{0x06CC, 0x06CC, SD}, /* farsi yeh (ی) */
{0x06CD, 0x06CD, SR}, /* yeh with tail */
{0x06CE, 0x06CE, SD}, /* farsi yeh with V (Kurdish ێ) */
{0x06CF, 0x06CF, SR}, /* waw with dot above */
{0x06D0, 0x06D1, SD}, /* e (Pashto ې), yeh three dots below */
{0x06D2, 0x06D3, SR}, /* yeh barree (ے ۓ) */
{0x06D5, 0x06D5, SR}, /* ae (Kurdish ە) */
{0x06EE, 0x06EF, SR}, /* dal/reh with inverted V */
{0x06FA, 0x06FC, SD}, /* sheen/dad/ghain with dot below */
{0x06FF, 0x06FF, SD}, /* heh with inverted V */
{0x200C, 0x200C, SU}, /* ZWNJ — joining type U per ArabicShaping.txt */
{0x200D, 0x200D, SC}, /* ZWJ — joining type C per ArabicShaping.txt */
};
/* ── CrossPoint extension: presentation forms outside U+0621U+064A ─────
* Source: UnicodeData.txt, Arabic Presentation Forms-A (U+FB50U+FBFF).
* forms = number of consecutive presentation forms allocated for the
* letter, always in the order isolated, final, initial, medial (matching
* the SHAPE_* offsets below): 2 = isolated+final, 4 = all.
* Letters absent here have no presentation forms and keep their base
* codepoint. Sorted ascending (binary search). */
static const struct {
ucschar cp;
ucschar isolated;
uchar forms;
} xshapeforms[] = {
{0x0671, 0xFB50, 2}, /* alef wasla */
{0x0679, 0xFB66, 4}, /* tteh (Urdu ٹ) */
{0x067A, 0xFB5E, 4}, /* tteheh */
{0x067B, 0xFB52, 4}, /* beeh (Sindhi ٻ) */
{0x067E, 0xFB56, 4}, /* peh (Farsi پ) */
{0x067F, 0xFB62, 4}, /* teheh */
{0x0680, 0xFB5A, 4}, /* beheh (Sindhi ڀ) */
{0x0683, 0xFB76, 4}, /* nyeh (Sindhi ڃ) */
{0x0684, 0xFB72, 4}, /* dyeh (Sindhi ڄ) */
{0x0686, 0xFB7A, 4}, /* tcheh (Farsi چ) */
{0x0687, 0xFB7E, 4}, /* tcheheh (Sindhi ڇ) */
{0x0688, 0xFB88, 2}, /* ddal (Urdu ڈ) */
{0x068C, 0xFB84, 2}, /* dahal (Sindhi ڌ) */
{0x068D, 0xFB82, 2}, /* ddahal (Sindhi ڍ) */
{0x068E, 0xFB86, 2}, /* dul (Sindhi ڎ) */
{0x0691, 0xFB8C, 2}, /* rreh (Urdu ڑ) */
{0x0698, 0xFB8A, 2}, /* jeh (Farsi ژ) */
{0x06A4, 0xFB6A, 4}, /* veh (Kurdish ڤ) */
{0x06A6, 0xFB6E, 4}, /* peheh (Sindhi ڦ) */
{0x06A9, 0xFB8E, 4}, /* keheh (Farsi/Urdu ک) */
{0x06AD, 0xFBD3, 4}, /* ng */
{0x06AF, 0xFB92, 4}, /* gaf (Farsi/Urdu گ) */
{0x06B1, 0xFB9A, 4}, /* ngoeh (Sindhi ڱ) */
{0x06B3, 0xFB96, 4}, /* gueh (Sindhi ڳ) */
{0x06BA, 0xFB9E, 2}, /* noon ghunna (Urdu ں) — dual-joining but only
isolated+final forms exist; initial/medial
contexts keep the base codepoint */
{0x06BB, 0xFBA0, 4}, /* rnoon (Sindhi ڻ) */
{0x06BE, 0xFBAA, 4}, /* heh doachashmee (Urdu ھ) */
{0x06C0, 0xFBA4, 2}, /* heh with yeh above */
{0x06C1, 0xFBA6, 4}, /* heh goal (Urdu ہ) */
{0x06C5, 0xFBE0, 2}, /* kirghiz oe */
{0x06C6, 0xFBD9, 2}, /* oe (Kurdish ۆ) */
{0x06C7, 0xFBD7, 2}, /* u (ۇ) */
{0x06C8, 0xFBDB, 2}, /* yu */
{0x06C9, 0xFBE2, 2}, /* kirghiz yu */
{0x06CB, 0xFBDE, 2}, /* ve */
{0x06CC, 0xFBFC, 4}, /* farsi yeh (Farsi/Urdu ی) */
{0x06D0, 0xFBE4, 4}, /* e (Pashto ې) */
{0x06D2, 0xFBAE, 2}, /* yeh barree (Urdu ے) */
{0x06D3, 0xFBB0, 2}, /* yeh barree with hamza above (ۓ) */
};
/* Contextual form offsets from the isolated form — identical ordering in
Presentation Forms-A and -B, matching mintty's SFINAL/SINITIAL/SMEDIAL
(+1/+2/+3) macros. */
enum { SHAPE_ISOLATED = 0, SHAPE_FINAL = 1, SHAPE_INITIAL = 2, SHAPE_MEDIAL = 3 };
/* STYPE equivalent (mintty macro → function to add the extended table) */
static uchar stype(ucschar c) {
if (c >= SHAPE_FIRST && c <= SHAPE_LAST) return shapetypes[c - SHAPE_FIRST].type;
int i = -1, j = lengthof(xjointypes);
while (j - i > 1) {
int k = (i + j) / 2;
if (c < xjointypes[k].first)
j = k;
else if (c > xjointypes[k].last)
i = k;
else
return xjointypes[k].type;
}
return SU;
}
/* SISOLATED/SFINAL/SINITIAL/SMEDIAL equivalent.
form is one of the SHAPE_* offsets; returns c unchanged when the letter
has no presentation form allocated for that context. */
static ucschar shape_form(ucschar c, uchar form) {
if (c >= SHAPE_FIRST && c <= SHAPE_LAST) {
const uchar form_b = shapetypes[c - SHAPE_FIRST].form_b;
return form_b ? 0xFE00 + form_b + form : c;
}
int i = -1, j = lengthof(xshapeforms);
while (j - i > 1) {
int k = (i + j) / 2;
if (c < xshapeforms[k].cp)
j = k;
else if (c > xshapeforms[k].cp)
i = k;
else
return form < xshapeforms[k].forms ? xshapeforms[k].isolated + form : c;
}
return c;
}
/* CrossPoint deviation 1: NSM marks (harakat) sit between letters in our
array but are transparent for joining (Unicode joining type T). These
helpers find the effective joining neighbour. */
static int next_non_nsm(const bidi_char* line, int i, int count) {
int j = i + 1;
while (j < count && bidi_class(line[j].wc) == NSM) j++;
return j;
}
static int prev_non_nsm(const bidi_char* line, int i) {
int j = i - 1;
while (j >= 0 && bidi_class(line[j].wc) == NSM) j--;
return j;
}
/* The Main shaping function (mintty, structure preserved).
*
* line: visual-order buffer — must have been passed through do_bidi() first
* to: output buffer for the shaped data
* count: number of characters in line
*/
int do_shape(bidi_char* line, bidi_char* to, int count) {
for (int i = 0; i < count; i++) {
to[i] = line[i];
int tempShape = stype(line[i].wc);
switch (tempShape) {
case SR: { /* Right-Joining, i.e. has Isolated, Final */
const int nx = next_non_nsm(line, i, count); /* deviation 1: was i + 1 */
tempShape = (nx < count) ? stype(line[nx].wc) : SU;
if (tempShape == SL || tempShape == SD || tempShape == SC)
to[i].wc = shape_form(line[i].wc, SHAPE_FINAL);
else
to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED);
break;
}
case SD: { /* Dual-Joining, i.e. has Isolated, Final, Initial, Medial */
const int nx = next_non_nsm(line, i, count); /* deviation 1: was i + 1 */
const int pv = prev_non_nsm(line, i); /* deviation 1: was i - 1 */
/* Make Ligatures */
tempShape = (nx < count) ? stype(line[nx].wc) : SU;
if (line[i].wc == 0x644) { /* Lam: the Alef variant is at pv (visually left) */
int ligFlag = 0;
switch (pv >= 0 ? line[pv].wc : 0) {
case 0x622: /* Alef with Madda above → لآ */
ligFlag = 1;
to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEF6 : 0xFEF5;
break;
case 0x623: /* Alef with Hamza above → لأ */
ligFlag = 1;
to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEF8 : 0xFEF7;
break;
case 0x625: /* Alef with Hamza below → لإ */
ligFlag = 1;
to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEFA : 0xFEF9;
break;
case 0x627: /* Alef → لا */
ligFlag = 1;
to[i].wc = (tempShape == SL || tempShape == SD || tempShape == SC) ? 0xFEFC : 0xFEFB;
break;
}
if (ligFlag) {
to[pv].wc = LIGATURE_PLACEHOLDER; /* deviation 2: mintty writes 0x20 */
break;
}
}
/* Arabic joining formatters: adapt forms (mintty) */
const uchar joiners = line[i].joiners & 0xF;
const uchar prevjoiners = line[i].joiners >> 4;
if (prevjoiners == ZWNJ) {
/* backward join blocked; initial only if the visually-right
(logically next) neighbour joins, else isolated */
tempShape = (pv >= 0) ? stype(line[pv].wc) : SU;
if (tempShape == SR || tempShape == SD || tempShape == SC)
to[i].wc = shape_form(line[i].wc, SHAPE_INITIAL);
else
to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED);
} else if (prevjoiners == (ZWJ | ZWNJ)) {
to[i].wc = shape_form(line[i].wc, SHAPE_MEDIAL);
} else if (prevjoiners == ZWJ) {
to[i].wc = shape_form(line[i].wc, SHAPE_FINAL);
} else if (joiners & ZWNJ) {
/* forward join blocked; final only if the visually-left
(logically previous) neighbour joins, else isolated —
tempShape still holds stype(nx) from above */
if (tempShape == SL || tempShape == SD || tempShape == SC)
to[i].wc = shape_form(line[i].wc, SHAPE_FINAL);
else
to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED);
} else if (tempShape == SL || tempShape == SD || tempShape == SC) {
/* visually-right neighbour joins → final, or medial if the
visually-left neighbour joins too */
tempShape = (pv >= 0) ? stype(line[pv].wc) : SU;
if (tempShape == SR || tempShape == SD || tempShape == SC)
to[i].wc = shape_form(line[i].wc, SHAPE_MEDIAL);
else
to[i].wc = shape_form(line[i].wc, SHAPE_FINAL);
} else {
/* visually-right neighbour doesn't join → isolated, or initial
if the visually-left neighbour joins */
tempShape = (pv >= 0) ? stype(line[pv].wc) : SU;
if (tempShape == SR || tempShape == SD || tempShape == SC)
to[i].wc = shape_form(line[i].wc, SHAPE_INITIAL);
else
to[i].wc = shape_form(line[i].wc, SHAPE_ISOLATED);
}
break;
}
default:
/* SU, SL, SC and non-Arabic characters pass through unchanged */
break;
}
}
return 1;
}
/* ═══════════════════════════════════════════════════════════════════════
* 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;
/* ── X1X8: 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, &currentEmbedding, &currentOverride, &currentIsolate);
levels[i] = currentEmbedding;
when PDI : if (isolateLevel > 0) {
while (!currentIsolate && dss_count(&dss) > 0)
dss_pop(&dss, &currentEmbedding, &currentOverride, &currentIsolate);
dss_pop(&dss, &currentEmbedding, &currentOverride, &currentIsolate);
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;
}