Skip to main content

justify/
justify.rs

1use icu_segmenter::{
2    options::{LineBreakOptions, WordBreakInvariantOptions},
3    GraphemeClusterSegmenter, LineSegmenter, WordSegmenter,
4};
5use kashida::{builtin_pattern_set, find_kashida_points, PatternSet};
6
7const KASHIDA: &str = "\u{0640}";
8const WIDTH: usize = 32;
9
10const MAX_KASHIDA: usize = 2;
11
12const TEXT: &str = concat!(
13    "قال أفلاطون: «الخط عقال العقل». وقال إقليدس ",
14    "الإغريقي: «الخط هندسة روحانية وإن ظهرت بآلة ",
15    "جسمانية». وقال أبو دلف رحالة القرن العاشر ",
16    "الميلادي: «الخط رياض العلوم». وقال النظام المعتزلي: ",
17    "«الخط أصيل في الروح وإن ظهر بحواس البدن»."
18);
19
20fn justify(line: &str, set: &PatternSet) -> String {
21    // Split line into words, and find the highest-priority kashida point in
22    // each word.
23    let mut words = Vec::new();
24    let mut prev = 0;
25    for end in WordSegmenter::new_auto(WordBreakInvariantOptions::default()).segment_str(line) {
26        let (cleaned, points) = find_kashida_points(&line[prev..end], set, true);
27        prev = end;
28        words.push((
29            cleaned,
30            points.into_iter().max_by_key(|point| point.priority),
31        ));
32    }
33
34    // Insert kashidas until the line is filled, starting with highest-priority
35    // points across the line.
36    let mut room = WIDTH - line.chars().count();
37    for priority in (0..=9).rev() {
38        for (word, point) in &mut words {
39            // No more space left to fill.
40            if room == 0 {
41                break;
42            }
43            if let Some(point) = point.filter(|point| point.priority == priority) {
44                // The kashida goes after the grapheme cluster at point.index.
45                let mut boundaries = GraphemeClusterSegmenter::new().segment_str(word);
46                let index = boundaries.nth(point.index as usize + 1).unwrap();
47                // Do not insert more than MAX_KASHIDA at each point.
48                let count = MAX_KASHIDA.min(room);
49                word.insert_str(index, &KASHIDA.repeat(count));
50                room -= count;
51            }
52        }
53    }
54    words.into_iter().map(|(word, _)| word).collect()
55}
56
57fn main() {
58    // Break the text into lines.
59    let (mut lines, mut start, mut prev) = (Vec::new(), 0, 0);
60    for brk in LineSegmenter::new_auto(LineBreakOptions::default()).segment_str(TEXT) {
61        if TEXT[start..brk].trim_end().chars().count() > WIDTH {
62            lines.push(TEXT[start..prev].trim_end());
63            start = prev;
64        }
65        prev = brk;
66    }
67    lines.push(TEXT[start..].trim_end());
68
69    // Print the unjustified lines.
70    println!("unjustified");
71    for line in &lines {
72        println!("{line}");
73    }
74
75    // Print justified lines, the last line is unjustified.
76    let pattern_set = builtin_pattern_set("arabic-naskh").unwrap();
77    println!("\njustified");
78    let last = lines.pop().unwrap();
79    for line in lines {
80        println!("{}", justify(line, pattern_set));
81    }
82    println!("{last}");
83}