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 that serves as a seat for a
66/// small alef (U+0670) or a combining hamza (U+0654 and U+0655) is not bare
67/// and is always kept, as the combination serves as a unit in Quranic
68/// orthography.
69///
70/// Returns the (possibly stripped) text along with the points, whose
71/// indices refer to it.
72///
73/// # Example
74///
75/// ```
76/// use kashida::{builtin_pattern_set, find_kashida_points};
77///
78/// let set = builtin_pattern_set("arabic-simple").unwrap();
79/// let (cleaned, points) = find_kashida_points("بيت", set, true);
80/// for point in points {
81///     // Insert a kashida after grapheme cluster `point.index`.
82///     println!("{} @ {}", point.priority, point.index);
83/// }
84/// ```
85pub fn find_kashida_points(
86    word: &str,
87    set: &PatternSet,
88    remove_existing_kashida: bool,
89) -> (String, Vec<KashidaPoint>) {
90    let cleaned = if remove_existing_kashida {
91        strip_bare_tatweel(word)
92    } else {
93        word.to_string()
94    };
95    let points = find_kashida_points_patterns(&cleaned, set);
96    (cleaned, points)
97}