Text

Hyphenation

Words hyphenated by the rules of 80 languages and spellings, patterns built in: a word's syllables, soft hyphens put in text and HTML for narrow columns and e-mails, and text wrapped at a width with words broken where they may be.

Version 1.0.0 · 5 functions · 1 crate

#text#languages#email#writing

Functions

What use hyphenation.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use hyphenate_word, hyphenate_text from hyphenation (Quillo 0.6).

hyphenate_word(word: str, lang: str) → listor an error

A word's syllables where it may be broken in lang ("en-us", "en-gb", "de", "fr", "nl"...): ["hy", "phen", "ation"]. A word too short to break comes back whole.

hyphenate_text(text: str, lang: str, min_length: int) → stror an error

text with soft hyphens (U+00AD) in its words of min_length letters or more: a browser or e-mail client breaks a word there at a line's end and shows a hyphen, and nowhere else.

hyphenate_html(html: str, lang: str, min_length: int) → stror an error

HTML with &shy; in the words of its text (tags, attributes, entities, and <pre>, <code>, <script> and <style> left alone), words of min_length letters or more: for narrow columns and e-mails, where CSS hyphens: auto is not supported.

hyphenate_wrap(text: str, width: int, lang: str) → listor an error

text wrapped to lines of at most width characters, a word that does not fit broken at a hyphenation point with a - (only when it does not fit whole): for receipts, labels and terminals. Paragraph breaks are kept.

hyphenate_languages() → list

The language codes there are patterns for: ["af", "be", "bg", ..., "en-gb", "en-us", ...].

Get it

$ quillo install hyphenation               # hyphenation.qwer into this folder$ quillo install hyphenation --example     # and its example, hyphenation.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/hyphenation.qwer$ curl -O https://quillolang.org/plugins/files/hyphenation.qwe

Then, in your program, all of it or only what you call:

main.qwe
use hyphenation.qweruse hyphenate_word, hyphenate_text from hyphenation      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
hyphenation = { version = "=0.8.4", features = ["embed_all"] }

Example

hyphenation.qwe
// A newsletter in narrow columns, hyphenated for e-mail clients that ignore CSS, and a receipt printer's 24-character lines.////   quillo run hyphenation.qwe      then open http://localhost:8080/wordsuse "hyphenation.qwer"route GET /words {    {        english: hyphenate_word("hyphenation", "en-us")?,        british: hyphenate_word("characteristically", "en-gb")?,        german: hyphenate_word("Donaudampfschifffahrt", "de")?,        french: hyphenate_word("anticonstitutionnellement", "fr")?,        dutch: hyphenate_word("ziekenhuisopname", "nl")?,        shy: replace(hyphenate_text("Unbelievable accommodation", "en-us", 6)?, "\u{ad}", "|"),        html: hyphenate_html("<p>Extraordinary <code>configuration</code> &amp; documentation</p>", "en-us", 6)?,        receipt: hyphenate_wrap("International transportation surcharge applies", 16, "en-us")?,        languages: len(hyphenate_languages()),        unknown: hyphenate_word("x", "xx").error,    }}route GET /newsletter = <html><body style="width: 220px">    <p>{raw(hyphenate_html("Our extraordinarily comprehensive documentation explains international configuration.", "en-us", 7)?)}</p></body></html>
$ quillo run hyphenation.qwe$ curl 'localhost:8080/words'

Source

hyphenation.qwer
//! Words hyphenated by the rules of 80 languages and spellings (TeX's patterns, built in): the syllables of a word, soft hyphens put in text and in HTML for narrow columns and e-mails, and text wrapped at a width with words broken where they may be.//! version: 1.0.0//!//! [dependencies]//! hyphenation = { version = "=0.8.4", features = ["embed_all"] }use hyphenation::{Hyphenator, Language, Load, Standard};use std::collections::HashMap;use std::sync::{Arc, Mutex, OnceLock};const SHY: char = '\u{AD}';fn language(code: &str) -> Result<Language, String> {    let c = code.trim().to_lowercase().replace('_', "-");    let alias = match c.as_str() {        "" | "en" => "en-us",        "de" | "de-de" | "de-at" => "de-1996",        "de-ch" => "de-ch-1901",        "el" => "el-monoton",        "nb" | "no" => "nb",        "sr" => "sr-cyrl",        "mn" => "mn-cyrl",        "pt-br" | "pt-pt" => "pt",        "es-mx" | "es-es" => "es",        "fr-fr" | "fr-ca" => "fr",        other => other,    };    Language::try_from_code(alias).ok_or_else(|| format!("`{}` is not a language with hyphenation patterns (en-us, en-gb, de, fr, es, nl...)", code))}fn dict(code: &str) -> Result<Arc<Standard>, String> {    static CACHE: OnceLock<Mutex<HashMap<Language, Arc<Standard>>>> = OnceLock::new();    let lang = language(code)?;    let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));    let mut c = cache.lock().map_err(|_| "the dictionaries are unavailable".to_string())?;    if let Some(d) = c.get(&lang) { return Ok(d.clone()) }    let d = Arc::new(Standard::from_embedded(lang).map_err(|e| format!("{}: {}", lang, e))?);    c.insert(lang, d.clone());    Ok(d)}fn parts(d: &Standard, word: &str) -> Vec<String> {    let w = d.hyphenate(word);    let mut out = Vec::new();    let mut last = 0;    for &b in &w.breaks {        if b > last && b < word.len() && word.is_char_boundary(b) { out.push(word[last..b].to_string()); last = b; }    }    out.push(word[last..].to_string());    out}// Each run of letters in `text` given to `f`, the rest copied.fn each_word(text: &str, mut f: impl FnMut(&str, &mut String)) -> String {    let mut out = String::with_capacity(text.len() + text.len() / 8);    let mut word = String::new();    for c in text.chars() {        if c.is_alphabetic() { word.push(c) } else {            if !word.is_empty() { f(&word, &mut out); word.clear(); }            out.push(c);        }    }    if !word.is_empty() { f(&word, &mut out) }    out}/// A word's syllables where it may be broken in `lang` (`"en-us"`, `"en-gb"`, `"de"`, `"fr"`, `"nl"`...): `["hy", "phen", "ation"]`. A word too short to break comes back whole.pub fn hyphenate_word(word: &str, lang: &str) -> Result<Vec<String>, String> {    let d = dict(lang)?;    Ok(parts(&d, word.trim()))}/// `text` with soft hyphens (U+00AD) in its words of `min_length` letters or more: a browser or e-mail client breaks a word there at a line's end and shows a hyphen, and nowhere else.pub fn hyphenate_text(text: &str, lang: &str, min_length: i64) -> Result<String, String> {    let d = dict(lang)?;    Ok(each_word(text, |w, out| {        if (w.chars().count() as i64) < min_length { out.push_str(w) } else { out.push_str(&parts(&d, w).join(&SHY.to_string())) }    }))}/// HTML with `&shy;` in the words of its text (tags, attributes, entities, and `<pre>`, `<code>`, `<script>` and `<style>` left alone), words of `min_length` letters or more: for narrow columns and e-mails, where CSS `hyphens: auto` is not supported.pub fn hyphenate_html(html: &str, lang: &str, min_length: i64) -> Result<String, String> {    let d = dict(lang)?;    let mut out = String::with_capacity(html.len() + html.len() / 6);    let mut rest = html;    let mut skip_until: Option<String> = None;    while !rest.is_empty() {        if rest.starts_with('<') {            let end = rest.find('>').map(|i| i + 1).unwrap_or(rest.len());            let tag = &rest[..end];            let name: String = tag.trim_start_matches('<').chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '/').collect::<String>().to_lowercase();            if let Some(stop) = &skip_until { if &name == stop { skip_until = None } }            else if ["pre", "code", "script", "style", "textarea"].contains(&name.as_str()) { skip_until = Some(format!("/{}", name)) }            out.push_str(tag);            rest = &rest[end..];            continue;        }        let end = rest.find('<').unwrap_or(rest.len());        let text = &rest[..end];        if skip_until.is_some() { out.push_str(text) } else {            // Entities stay whole: split around them.            let mut t = text;            while !t.is_empty() {                if let Some(a) = t.find('&') {                    let b = t[a..].find(';').map(|i| a + i + 1).filter(|&b| b - a <= 12).unwrap_or(a + 1);                    out.push_str(&each_word(&t[..a], |w, o| if (w.chars().count() as i64) < min_length { o.push_str(w) } else { o.push_str(&parts(&d, w).join("&shy;")) }));                    out.push_str(&t[a..b]);                    t = &t[b..];                } else {                    out.push_str(&each_word(t, |w, o| if (w.chars().count() as i64) < min_length { o.push_str(w) } else { o.push_str(&parts(&d, w).join("&shy;")) }));                    t = "";                }            }        }        rest = &rest[end..];    }    Ok(out)}/// `text` wrapped to lines of at most `width` characters, a word that does not fit broken at a hyphenation point with a `-` (only when it does not fit whole): for receipts, labels and terminals. Paragraph breaks are kept.pub fn hyphenate_wrap(text: &str, width: i64, lang: &str) -> Result<Vec<String>, String> {    if width < 4 { return Err("a width of 4 characters at least".into()) }    let d = dict(lang)?;    let w = width as usize;    let mut lines = Vec::new();    for para in text.split('\n') {        let mut line = String::new();        for word in para.split_whitespace() {            let len = |s: &str| s.chars().count();            let room = if line.is_empty() { w } else { w.saturating_sub(len(&line) + 1) };            if len(word) <= room {                if !line.is_empty() { line.push(' ') }                line.push_str(word);                continue;            }            // Break the word: as many syllables as fit with a hyphen, the rest on following lines.            let syl = parts(&d, word);            let mut rest: Vec<String> = syl;            loop {                let room = if line.is_empty() { w } else { w.saturating_sub(len(&line) + 1) };                let all: String = rest.concat();                if len(&all) <= room { if !line.is_empty() { line.push(' ') } line.push_str(&all); break }                let mut take = 0;                let mut acc = String::new();                while take < rest.len() - 1 && len(&acc) + len(&rest[take]) + 1 <= room { acc.push_str(&rest[take]); take += 1; }                if take == 0 {                    if line.is_empty() {                        // A syllable longer than the line: cut it.                        let first: String = all.chars().take(w - 1).collect();                        lines.push(format!("{}-", first));                        rest = vec![all.chars().skip(w - 1).collect()];                    } else {                        lines.push(std::mem::take(&mut line));                    }                    continue;                }                if !line.is_empty() { line.push(' ') }                line.push_str(&acc);                line.push('-');                lines.push(std::mem::take(&mut line));                rest = rest[take..].to_vec();            }        }        lines.push(line);    }    Ok(lines)}/// The language codes there are patterns for: `["af", "be", "bg", ..., "en-gb", "en-us", ...]`.pub fn hyphenate_languages() -> Vec<String> {    let mut v: Vec<String> = Vec::new();    for code in ["af", "sq", "hy", "as", "eu", "be", "bn", "bg", "ca", "zh-latn-pinyin", "cop", "hr", "cs", "da", "nl", "en-gb", "en-us", "eo", "et", "mul-ethi", "fi", "fi-x-school", "fr", "fur", "gl", "ka", "de-1901", "de-1996", "de-ch-1901", "grc", "el-monoton", "el-polyton", "gu", "hi", "hu", "is", "id", "ia", "ga", "it", "kn", "kmr", "la", "la-x-classic", "la-x-liturgic", "lv", "lt", "mk", "ml", "mr", "mn-cyrl", "nb", "nn", "oc", "or", "pi", "pa", "pms", "pl", "pt", "ro", "rm", "ru", "sa", "sr-cyrl", "sh-cyrl", "sh-latn", "cu", "sk", "sl", "es", "sv", "ta", "te", "th", "tr", "tk", "uk", "hsb", "cy"] {        if Language::try_from_code(code).is_some() { v.push(code.to_string()) }    }    v.sort();    v}
Sentences sentences.qwer · 1.0.0Text split into sentences as a reader would: "Dr.", "p.m.", initials, decimals, "e.g." and quoted questions handled, with abbreviations for seven languages; positions for highlighting, and previews of whole sentences that fit a length.textwritingseolanguages7 views · 9 downloadsChinese words chinese_segment.qwer · 1.0.0Chinese text split into words for search indexes and counts, with parts of speech, and written in pinyin with tones, tone numbers or initials for sorting and finding names.chinesesearchtextlanguages7 views · 7 downloadsHTML to text html_text.qwer · 1.0.0HTML as readable plain text for the text part of an email: headings, lists and tables laid out, links listed at the end, and one-line previews.emailhtmltextexport7 views · 7 downloadsJapanese kana kana.qwer · 1.0.0Japanese kana: romaji typed as hiragana or katakana, kana written in Hepburn romaji, hiragana and katakana each to the other, half-width katakana from bank and till systems made full-width, and which scripts a text uses.unicodelanguagesformstext6 views · 10 downloadsNumbers in words number_words.qwer · 1.0.0Numbers and amounts of money in words for cheques, invoices and contracts, in English, French and Ukrainian, with ordinals, years and Roman numerals both ways.invoicesmoneytextlanguages8 views · 7 downloadsStemming stemming.qwer · 1.0.0Words cut to their stems in 18 languages, so running, runs and run count as one word in a search or a tag cloud.searchtextlanguagestags7 views · 8 downloads
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On Hyphenation: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.