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}