905 lines
32 KiB
C
905 lines
32 KiB
C
/*
|
||
* 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+0621–U+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+0621–U+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+0621–U+064A ─────
|
||
* Source: UnicodeData.txt, Arabic Presentation Forms-A (U+FB50–U+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;
|
||
|
||
/* ── 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;
|
||
}
|