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}