///|
/// Unicode Bidirectional Algorithm (UAX #9)
///
/// This module implements the Unicode Bidirectional Algorithm for
/// determining the display order of text containing both left-to-right
/// and right-to-left characters.
///|
/// Error types for Bidi processing
pub(all) suberror BidiError {
InvalidIsolate(String) // Mismatched isolate initiator/PDI
StackOverflow // Embedding level stack overflow (max 125)
}
///|
/// Direction of text
pub(all) enum Direction {
LTR // Left-to-Right (base level 0)
RTL // Right-to-Left (base level 1)
} derive(Eq)
///|
pub impl Show for Direction with fn output(self, logger) {
match self {
LTR => logger.write_string("LTR")
RTL => logger.write_string("RTL")
}
}
///|
/// Result of bidi processing - a paragraph with resolved levels
pub(all) struct BidiParagraph {
/// Original characters
chars : Array[Char]
/// Original Bidi classes (before resolution)
original_classes : Array[BidiClass]
/// Resolved Bidi classes (after W and N rules)
resolved_classes : Array[BidiClass]
/// Resolved embedding level per character (0-125)
levels : Array[Int]
/// Paragraph embedding level (0 for LTR, 1 for RTL)
base_level : Int
}
///|
/// Maximum embedding level allowed by UAX #9
let max_depth : Int = 125
///|
/// Look up Bidi_Class for a character
pub fn bidi_class(c : Char) -> BidiClass {
lookup_bidi_class(c)
}
///|
/// Detect the base direction of text (P2-P3)
/// Returns the direction of the first strong character,
/// or LTR if no strong character is found
pub fn detect_direction(text : String) -> Direction {
let level = determine_paragraph_level(text, default_level=0)
if level == 0 {
LTR
} else {
RTL
}
}
///|
/// Check if text requires bidi processing
/// Returns false for pure LTR text (optimization)
pub fn requires_bidi(text : String) -> Bool {
for c in text {
if is_strong_rtl(c) || is_explicit_formatting(c) {
return true
}
}
false
}
///|
/// Process text according to the Unicode Bidirectional Algorithm
/// If `direction` is omitted, infer it from the first strong character.
/// Returns a BidiParagraph with resolved embedding levels for each character
pub fn process(text : String, direction? : Direction) -> BidiParagraph {
let base_level = if direction is Some(direction) {
direction.level()
} else {
determine_paragraph_level(text, default_level=0)
}
process_at_base_level(text, base_level)
}
///|
/// Process text with explicit base direction
/// Use when the paragraph direction is known externally
#deprecated("Use process(text, direction=direction) instead")
pub fn process_with_direction(
text : String,
direction : Direction,
) -> BidiParagraph {
process(text, direction~)
}
///|
/// Process text with explicit base level
#deprecated("Use process(text, direction=LTR or RTL) instead")
pub fn process_with_base_level(
text : String,
base_level : Int,
) -> BidiParagraph {
process_at_base_level(text, base_level)
}
///|
fn process_at_base_level(text : String, base_level : Int) -> BidiParagraph {
// Convert to char array and get original classes
let chars : Array[Char] = []
for c in text {
chars.push(c)
}
let original_classes : Array[BidiClass] = chars.map(lookup_bidi_class)
// Make a copy for resolution
let resolved_classes = original_classes.copy()
// Allocate levels array
let levels : Array[Int] = Array::make(chars.length(), base_level)
// X1-X10: Process explicit formatting characters
let isolate_pairs = process_explicit(resolved_classes, levels, base_level)
// Align NSM levels with the previous non-X9 character.
adjust_nsm_levels(original_classes, levels)
// Compute isolating run sequences and process each
let sequences = compute_isolating_run_sequences(
resolved_classes, levels, original_classes, base_level, isolate_pairs,
)
for seq in sequences {
// W1-W7: Resolve weak types
resolve_weak_types(resolved_classes, levels, seq)
// N0: Resolve bracket pairs
resolve_bracket_pairs(chars, resolved_classes, levels, seq)
// Adjust NSM after bracket resolution (N0)
resolve_nsm_after_brackets(resolved_classes, original_classes, seq)
// N1-N2: Resolve neutral types
resolve_neutral_types(resolved_classes, levels, seq)
}
// I1-I2: Apply implicit levels
apply_implicit_levels(resolved_classes, levels)
// L1: Reset whitespace and separator levels
reset_whitespace_levels(original_classes, levels, base_level)
{ chars, original_classes, resolved_classes, levels, base_level }
}
///|
/// Reorder text for display according to resolved levels
/// Returns array of character indices in visual order
/// X9-removed characters (LRE, RLE, LRO, RLO, PDF, BN) are excluded
pub fn reorder(para : BidiParagraph) -> Array[Int] {
// X9: remove explicit formatting and BN before reordering.
let indices : Array[Int] = []
for i = 0; i < para.levels.length(); i = i + 1 {
let bc = para.original_classes[i]
if !bc.is_x9_removed() {
indices.push(i)
}
}
apply_reordering(indices, para.levels, para.base_level)
}
///|
/// Reorder text and return the visually-ordered string
pub fn reorder_string(para : BidiParagraph) -> String {
let order = reorder(para)
let sb = StringBuilder::new()
for i in order {
let c = para.chars[i]
let level = para.levels[i]
// Apply mirroring for RTL levels (odd levels)
if level % 2 == 1 {
sb.write_char(@bidi_data.bidi_mirroring_glyph(c))
} else {
sb.write_char(c)
}
}
sb.to_string()
}
///|
/// Get the mirrored glyph for a character if in RTL context
pub fn get_mirrored(c : Char, level : Int) -> Char {
if level % 2 == 1 {
@bidi_data.bidi_mirroring_glyph(c)
} else {
c
}
}
///|
/// Get embedding level for direction
pub fn Direction::level(self : Direction) -> Int {
match self {
LTR => 0
RTL => 1
}
}
///|
/// Get direction from embedding level
pub fn direction_from_level(level : Int) -> Direction {
if level % 2 == 0 {
LTR
} else {
RTL
}
}