Skip to main content

kashida/
lib.rs

1//! Arabic kashida (tatweel) insertion-point finding.
2//!
3//! Given text plus a compiled pattern set, produce the points where a
4//! kashida may be inserted, each with a priority (0–9, higher = stronger).
5
6#![forbid(unsafe_code)]
7#![warn(missing_docs)]
8#![warn(unreachable_pub)]
9
10mod builtin;
11mod error;
12mod grapheme;
13mod pattern;
14mod rasm;
15mod resolve;
16
17#[cfg(test)]
18mod tests;
19
20pub use builtin::{builtin_pattern_set, builtin_pattern_set_names, is_builtin_pattern_set};
21pub use error::{CompileError, CompileErrorKind};
22pub use pattern::{compile_pattern_text, PatternSet};
23
24use grapheme::{is_bare_tatweel_at, joined_runs, split_graphemes, KASHIDA};
25use resolve::resolve_run;
26
27/// A point where a kashida may be inserted.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub struct KashidaPoint {
30    /// The grapheme cluster index the kashida goes after.
31    pub index: u32,
32    /// The kashida point priority, from 0–9, higher priority means a more
33    /// preferable insertion point.
34    pub priority: u8,
35}
36
37/// Kashida insertion points for `word` from the pattern set alone.
38pub fn find_kashida_points_patterns(word: &str, set: &PatternSet) -> Vec<KashidaPoint> {
39    let graphemes = split_graphemes(word);
40    let mut out = Vec::new();
41    for run in joined_runs(&graphemes) {
42        out.extend(resolve_run(&graphemes, &run, set));
43    }
44    out
45}
46
47fn strip_bare_tatweel(word: &str) -> String {
48    if !word.contains(KASHIDA) {
49        return word.to_string();
50    }
51    let chars: Vec<char> = word.chars().collect();
52    let mut out = String::with_capacity(word.len());
53    for k in 0..chars.len() {
54        if is_bare_tatweel_at(&chars, k) {
55            continue;
56        }
57        out.push(chars[k]);
58    }
59    out
60}
61
62/// Kashida insertion points for `word` under the given pattern set.
63///
64/// Any **bare** kashida already in the text is stripped first, unless
65/// `remove_existing_kashida` is `false`. A kashida carrying a mark serves as
66/// a seat for it, so it is always kept.
67///
68/// Returns the (possibly stripped) text along with the points, whose
69/// indices refer to it.
70///
71/// # Example
72///
73/// ```
74/// use kashida::{builtin_pattern_set, find_kashida_points};
75///
76/// let set = builtin_pattern_set("arabic-simple").unwrap();
77/// let (cleaned, points) = find_kashida_points("بيت", set, true);
78/// for point in points {
79///     // Insert a kashida after grapheme cluster `point.index`.
80///     println!("{} @ {}", point.priority, point.index);
81/// }
82/// ```
83pub fn find_kashida_points(
84    word: &str,
85    set: &PatternSet,
86    remove_existing_kashida: bool,
87) -> (String, Vec<KashidaPoint>) {
88    let cleaned = if remove_existing_kashida {
89        strip_bare_tatweel(word)
90    } else {
91        word.to_string()
92    };
93    let points = find_kashida_points_patterns(&cleaned, set);
94    (cleaned, points)
95}