Skip to main content

Crate kashida

Crate kashida 

Source
Expand description

A library for finding kashida (tatweel) insertion points and priorities, driven by a small pattern language.

Given a text and a compiled pattern set, the crate returns the possible kashida insertion points and their priorities.

§Example

Here is an example that breaks a text into lines and justifies them by inserting kashida. For simplicity, the example assumes a monospaced font where every character has the same width.

  1. Break text into lines, greedily
  2. For each line, insert the highest priority kashidas first across the line:
    1. Only the highest kashida point in any given word
    2. Up to MAX_KASHIDA kashidas at each point
  3. Repeat with the next priority, until the line is filled, or there are no more kashida points to fill.

The example is also runnable with cargo run --example justify:

use icu_segmenter::{
    options::{LineBreakOptions, WordBreakInvariantOptions},
    GraphemeClusterSegmenter, LineSegmenter, WordSegmenter,
};
use kashida::{builtin_pattern_set, find_kashida_points, PatternSet};

const KASHIDA: &str = "\u{0640}";
const WIDTH: usize = 32;

const MAX_KASHIDA: usize = 2;

const TEXT: &str = concat!(
    "قال أفلاطون: «الخط عقال العقل». وقال إقليدس ",
    "الإغريقي: «الخط هندسة روحانية وإن ظهرت بآلة ",
    "جسمانية». وقال أبو دلف رحالة القرن العاشر ",
    "الميلادي: «الخط رياض العلوم». وقال النظام المعتزلي: ",
    "«الخط أصيل في الروح وإن ظهر بحواس البدن»."
);

fn justify(line: &str, set: &PatternSet) -> String {
    // Split line into words, and find the highest-priority kashida point in
    // each word.
    let mut words = Vec::new();
    let mut prev = 0;
    for end in WordSegmenter::new_auto(WordBreakInvariantOptions::default()).segment_str(line) {
        let (cleaned, points) = find_kashida_points(&line[prev..end], set, true);
        prev = end;
        words.push((
            cleaned,
            points.into_iter().max_by_key(|point| point.priority),
        ));
    }

    // Insert kashidas until the line is filled, starting with highest-priority
    // points across the line.
    let mut room = WIDTH - line.chars().count();
    for priority in (0..=9).rev() {
        for (word, point) in &mut words {
            // No more space left to fill.
            if room == 0 {
                break;
            }
            if let Some(point) = point.filter(|point| point.priority == priority) {
                // The kashida goes after the grapheme cluster at point.index.
                let mut boundaries = GraphemeClusterSegmenter::new().segment_str(word);
                let index = boundaries.nth(point.index as usize + 1).unwrap();
                // Do not insert more than MAX_KASHIDA at each point.
                let count = MAX_KASHIDA.min(room);
                word.insert_str(index, &KASHIDA.repeat(count));
                room -= count;
            }
        }
    }
    words.into_iter().map(|(word, _)| word).collect()
}

fn main() {
    // Break the text into lines.
    let (mut lines, mut start, mut prev) = (Vec::new(), 0, 0);
    for brk in LineSegmenter::new_auto(LineBreakOptions::default()).segment_str(TEXT) {
        if TEXT[start..brk].trim_end().chars().count() > WIDTH {
            lines.push(TEXT[start..prev].trim_end());
            start = prev;
        }
        prev = brk;
    }
    lines.push(TEXT[start..].trim_end());

    // Print the unjustified lines.
    println!("unjustified");
    for line in &lines {
        println!("{line}");
    }

    // Print justified lines, the last line is unjustified.
    let pattern_set = builtin_pattern_set("arabic-naskh").unwrap();
    println!("\njustified");
    let last = lines.pop().unwrap();
    for line in lines {
        println!("{}", justify(line, pattern_set));
    }
    println!("{last}");
}

Which prints:

unjustified
قال أفلاطون: «الخط عقال العقل».
وقال إقليدس الإغريقي: «الخط
هندسة روحانية وإن ظهرت بآلة
جسمانية». وقال أبو دلف رحالة
القرن العاشر الميلادي: «الخط
رياض العلوم». وقال النظام
المعتزلي: «الخط أصيل في الروح
وإن ظهر بحواس البدن».

justified
قال أفلاطون: «الخـط عقال العقل».
وقــال إقليـدس الإغريقي: «الخــط
هندســة روحانيــة وإن ظهرت بـآلة
جسمانيــة». وقــال أبو دلف رحالة
القرن العاشــر الميلادي: «الخــط
ريــاض العـلوم». وقــال النــظام
المعتزلي: «الخــط أصـيل في الروح
وإن ظهر بحواس البدن».

Structs§

CompileError
An error while compiling pattern text.
KashidaPoint
A point where a kashida may be inserted.
PatternSet
A compiled set of kashida insertion patterns.

Enums§

CompileErrorKind
What went wrong on a pattern line.

Functions§

builtin_pattern_set
The built-in pattern set of that name, compiled on first use, or None if there is none. builtin_pattern_set_names lists and describes them.
builtin_pattern_set_names
The names of the built-in pattern sets:
compile_pattern_text
Compiles pattern text into a PatternSet.
find_kashida_points
Kashida insertion points for text under the given pattern set.
find_kashida_points_patterns
Kashida insertion points for text from the pattern set alone.
is_builtin_pattern_set
Whether name refers to a built-in pattern set, without compiling it, unlike builtin_pattern_set.