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
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 errorA 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 errortext 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 errorHTML with ­ 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 errortext 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() → listThe 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.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/hyphenation.qwer$ curl -O https://quillolang.org/plugins/files/hyphenation.qweThen, in your program, all of it or only what you call:
use hyphenation.qweruse hyphenate_word, hyphenate_text from hyphenation // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
hyphenation = { version = "=0.8.4", features = ["embed_all"] }Example
// 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> & 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
//! 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 `­` 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("­")) })); 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("­")) })); 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}Related
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.7 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.7 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.7 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.6 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.8 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.7 views · 8 downloadsThread 0 posts
On Hyphenation: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.