mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-27 03:56:50 -06:00
When typing in Kurdish, Arabic or Hebrew in a new document or text shape, default paragraph direction (props.rtl: None) was resolving to LTR. In an LTR paragraph, visual reordering pinned newly typed characters and the caret to the left margin, resetting to the start of the line with every keystroke and jumping on space characters due to UAX #9 rule L1. - Add first_strong_is_rtl() in deckcraft-model following UAX #9 rules P2/P3. - Auto-resolve unassigned paragraph direction to RTL in shape_para layout when the paragraph starts with an RTL script, defaulting alignment to Align::Right. - Auto-promote paragraph to RTL (props.rtl = Some(true), props.align = Some(Align::Right)) when typing or inserting text starting with an RTL character. - Add regression tests in deckcraft-model, deckcraft-text, and deckcraft-engine.
82 lines
2.6 KiB
Rust
82 lines
2.6 KiB
Rust
//! Bidirectional text helpers (UAX #9).
|
|
//!
|
|
//! Follows Unicode Bidirectional Algorithm (UAX #9 rules P2 and P3) for paragraph
|
|
//! base direction auto-detection.
|
|
|
|
/// Does the first strong directional character in `text` belong to a right-to-left script?
|
|
///
|
|
/// Follows UAX #9 rules P2 and P3: characters with strong bidirectional class AL or R
|
|
/// (Arabic, Hebrew, Syriac, Thaana…) give `true`; strong L characters (Latin, Greek,
|
|
/// Cyrillic, CJK…) give `false`. Neutral, weak, and whitespace characters are skipped.
|
|
pub fn first_strong_is_rtl(text: &str) -> bool {
|
|
for c in text.chars() {
|
|
if c == '\u{200F}' {
|
|
// Right-to-Left Mark (RLM)
|
|
return true;
|
|
}
|
|
if c == '\u{200E}' {
|
|
// Left-to-Right Mark (LRM)
|
|
return false;
|
|
}
|
|
if c.is_alphabetic() {
|
|
return is_rtl_script(c);
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Helper checking if a char belongs to standard RTL Unicode blocks.
|
|
pub fn is_rtl_script(c: char) -> bool {
|
|
matches!(
|
|
c,
|
|
'\u{0590}'..='\u{05FF}' // Hebrew
|
|
| '\u{0600}'..='\u{06FF}' // Arabic
|
|
| '\u{0700}'..='\u{074F}' // Syriac
|
|
| '\u{0750}'..='\u{077F}' // Arabic Supplement
|
|
| '\u{0780}'..='\u{07BF}' // Thaana
|
|
| '\u{0870}'..='\u{089F}' // Arabic Extended-B
|
|
| '\u{08A0}'..='\u{08FF}' // Arabic Extended-A
|
|
| '\u{FB1D}'..='\u{FB4F}' // Hebrew Presentation Forms
|
|
| '\u{FB50}'..='\u{FDFF}' // Arabic Presentation Forms-A
|
|
| '\u{FE70}'..='\u{FEFE}' // Arabic Presentation Forms-B
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn detects_arabic_and_kurdish() {
|
|
assert!(first_strong_is_rtl("مرحبا"));
|
|
assert!(first_strong_is_rtl("من ناوم هێڤاره"));
|
|
assert!(first_strong_is_rtl("سلام"));
|
|
}
|
|
|
|
#[test]
|
|
fn detects_hebrew() {
|
|
assert!(first_strong_is_rtl("שלום"));
|
|
}
|
|
|
|
#[test]
|
|
fn detects_latin_as_not_rtl() {
|
|
assert!(!first_strong_is_rtl("Hello"));
|
|
assert!(!first_strong_is_rtl("DeckCraft"));
|
|
}
|
|
|
|
#[test]
|
|
fn skips_neutrals_numbers_and_whitespace() {
|
|
assert!(first_strong_is_rtl(" 123 مرحبا"));
|
|
assert!(first_strong_is_rtl("... (2024) سنة"));
|
|
assert!(!first_strong_is_rtl(" 123 Hello"));
|
|
assert!(!first_strong_is_rtl(""));
|
|
assert!(!first_strong_is_rtl(" 12345 "));
|
|
}
|
|
|
|
#[test]
|
|
fn honours_directional_marks() {
|
|
assert!(first_strong_is_rtl("\u{200F}Hello"));
|
|
assert!(!first_strong_is_rtl("\u{200E}مرحبا"));
|
|
}
|
|
}
|