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