Skip to main content

kashida/
lib.rs

1//! A library for finding _kashida_ (_tatweel_) insertion points and
2//! priorities, driven by a small pattern language.
3//!
4//! Given a text and a compiled pattern set, the crate returns the possible
5//! _kashida_ insertion points and their priorities.
6//!
7//! # Example
8//!
9//! Here is an example that breaks a text into lines and justifies them by
10//! inserting _kashida_. For simplicity, the example assumes a monospaced
11//! font where every character has the same width.
12//!
13//! 1. Break text into lines, greedily
14//! 2. For each line, insert the highest priority _kashidas_ first across the
15//!    line:
16//!    1. Only the highest _kashida_ point in any given word
17//!    2. Up to `MAX_KASHIDA` _kashidas_ at each point
18//! 4. Repeat with the next priority, until the line is filled, or there are no
19//!    more _kashida_ points to fill.
20//!
21//! The example is also runnable with
22//! `cargo run --example justify`:
23//!
24//! ```
25#![doc = include_str!("../examples/justify.rs")]
26//! ```
27//!
28//! Which prints:
29//!
30//! <pre dir="rtl">
31//! unjustified
32//! قال أفلاطون: «الخط عقال العقل».
33//! وقال إقليدس الإغريقي: «الخط
34//! هندسة روحانية وإن ظهرت بآلة
35//! جسمانية». وقال أبو دلف رحالة
36//! القرن العاشر الميلادي: «الخط
37//! رياض العلوم». وقال النظام
38//! المعتزلي: «الخط أصيل في الروح
39//! وإن ظهر بحواس البدن».
40//!
41//! justified
42//! قال أفلاطون: «الخـط عقال العقل».
43//! وقــال إقليـدس الإغريقي: «الخــط
44//! هندســة روحانيــة وإن ظهرت بـآلة
45//! جسمانيــة». وقــال أبو دلف رحالة
46//! القرن العاشــر الميلادي: «الخــط
47//! ريــاض العـلوم». وقــال النــظام
48//! المعتزلي: «الخــط أصـيل في الروح
49//! وإن ظهر بحواس البدن».
50//! </pre>
51
52#![forbid(unsafe_code)]
53#![warn(missing_docs)]
54#![warn(unreachable_pub)]
55
56mod builtin;
57mod error;
58mod grapheme;
59mod pattern;
60mod rasm;
61mod resolve;
62
63#[cfg(test)]
64mod tests;
65
66pub use builtin::{builtin_pattern_set, builtin_pattern_set_names, is_builtin_pattern_set};
67pub use error::{CompileError, CompileErrorKind};
68pub use pattern::{compile_pattern_text, PatternSet};
69
70use grapheme::{is_bare_tatweel_at, joined_runs, split_graphemes, KASHIDA};
71use resolve::resolve_run;
72
73/// A point where a kashida may be inserted.
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub struct KashidaPoint {
76    /// The grapheme cluster index the kashida goes after.
77    pub index: u32,
78    /// The kashida point priority, from 0–9, higher priority means a more
79    /// preferable insertion point.
80    pub priority: u8,
81}
82
83/// Kashida insertion points for `text` from the pattern set alone.
84pub fn find_kashida_points_patterns(text: &str, set: &PatternSet) -> Vec<KashidaPoint> {
85    let graphemes = split_graphemes(text);
86    let mut out = Vec::new();
87    for run in joined_runs(&graphemes) {
88        out.extend(resolve_run(&graphemes, &run, set));
89    }
90    out
91}
92
93fn strip_bare_tatweel(text: &str) -> String {
94    if !text.contains(KASHIDA) {
95        return text.to_string();
96    }
97    let chars: Vec<char> = text.chars().collect();
98    let mut out = String::with_capacity(text.len());
99    for k in 0..chars.len() {
100        if is_bare_tatweel_at(&chars, k) {
101            continue;
102        }
103        out.push(chars[k]);
104    }
105    out
106}
107
108/// Kashida insertion points for `text` under the given pattern set.
109///
110/// Any **bare** kashida already in the text is stripped first, unless
111/// `remove_existing_kashida` is `false`. A kashida carrying a mark serves as
112/// a seat for it, so it is always kept.
113///
114/// Returns the (possibly stripped) text along with the points, whose
115/// indices refer to it.
116///
117/// # Example
118///
119/// ```
120/// use kashida::{builtin_pattern_set, find_kashida_points};
121///
122/// let set = builtin_pattern_set("arabic-simple").unwrap();
123/// let (cleaned, points) = find_kashida_points("بيت", set, true);
124/// for point in points {
125///     // Insert a kashida after grapheme cluster `point.index`.
126///     println!("{} @ {}", point.priority, point.index);
127/// }
128/// ```
129pub fn find_kashida_points(
130    text: &str,
131    set: &PatternSet,
132    remove_existing_kashida: bool,
133) -> (String, Vec<KashidaPoint>) {
134    let cleaned = if remove_existing_kashida {
135        strip_bare_tatweel(text)
136    } else {
137        text.to_string()
138    };
139    let points = find_kashida_points_patterns(&cleaned, set);
140    (cleaned, points)
141}