Braille
Unicode braille from English text: grade 1 letter by letter, and Unified English Braille grade 2 with its wordsigns, groupsigns, positional rules and shortforms; read back cell by cell where the cells allow it; dot numbers and the Braille ASCII of `.brf` files both ways.
Version 1.0.0 · 7 functions · 1 crate
Functions
What use braille.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use braille_encode, braille_decode from braille (Quillo 0.6).
braille_encode(text: str, grade: int) → stror an errorText as Unicode braille. grade 1 spells every letter; 2 is Unified English Braille's contractions by spelling rules alone: a word needing a pronunciation rule, like hothouse or shepherd, gets the usual sign, as no dictionary is kept.
braille_decode(cells: str, grade: int) → anyor an errorBraille back to text: { text, uncertain }. Grade 2 reads each cell by its place in the word, so ⠆ is be first, bb inside and ; last. A cell it cannot place is U+FFFD and listed in uncertain; a capital at the start of a line and hyphenated words survive, but spacing marks and line breaks of a printed page do not.
braille_dots(cells: str) → listEach cell as dot numbers: ⠓ is 125, a blank cell 0.
braille_from_dots(dots: list) → stror an errorDot numbers to cells: ["125", "15"] is ⠓⠑, for six or eight dots; 0 is a blank cell.
braille_to_ascii(cells: str) → stror an errorCells as North American Braille ASCII, the text of a .brf file for embossers. Eight-dot cells have no form there.
braille_from_ascii(text: str) → stror an errorNorth American Braille ASCII as cells; small letters count as capitals, as embossers take them.
braille_signs() → anyThe grade 2 signs by kind, for a learner's chart: wordsigns, shortforms and groupsigns, each { word, cells, dots }.
Get it
$ quillo install braille # braille.qwer into this folder$ quillo install braille --example # and its example, braille.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/braille.qwer$ curl -O https://quillolang.org/plugins/files/braille.qweThen, in your program, all of it or only what you call:
use braille.qweruse braille_encode, braille_decode from braille // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
serde_json = { version = "1", features = ["preserve_order"] }Example
// Unicode braille: an English text in grade 1 and in Unified English Braille grade 2, and back,// the dots of each cell, and the Braille ASCII an embosser takes.//// quillo run braille.qwe// then open http://localhost:8080/encode?text=The+child+knows+the+way, /round, /dots and /refuseduse "braille.qwer"route GET /encode ? { text: str = "Hello world" } = { text: text, grade1: braille_encode(text, 1)?, grade2: braille_encode(text, 2)?,}route GET /samples = { wordsigns: braille_encode("the child can do it for you", 2)?, groups: braille_encode("mother found something together", 2)?, middle: braille_encode("bread rubber effect", 2)?, first: braille_encode("being content", 2)?, capitals: braille_encode("The NASA McGill", 2)?, numbers: braille_encode("Room 12b costs 3.50", 2)?, letter: braille_encode("plan b", 2)?, quote: braille_encode("\"Why?\" she said.", 2)?,}route GET /round { let text = "The father and the child were in the house, but it was not enough." let cells = braille_encode(text, 2)? { cells: cells, back: braille_decode(cells, 2)?.text, grade1: braille_decode(braille_encode("Room 42 is OK.", 1)?, 1)?.text, guess: braille_decode("⠎⠁⠸", 2)?, }}route GET /dots = { dots: braille_dots("⠓⠑"), cells: braille_from_dots(["125", "15", "0", "1234"])?, ascii: braille_to_ascii(braille_encode("Hello there", 2)?)?, from_ascii: braille_from_ascii(",HELLO")?,}route GET /refused = { grade: braille_encode("x", 3).error, character: braille_encode("€5", 1).error, cell: braille_decode("abc", 1).error, dots: braille_from_dots(["129"]).error,}route GET /signs = { shortforms: len(braille_signs().shortforms), wordsigns: len(braille_signs().wordsigns), first: first(braille_signs().shortforms) }$ quillo run braille.qwe$ curl 'localhost:8080/encode?text=Hello+world'$ curl 'localhost:8080/samples'$ curl 'localhost:8080/round'$ curl 'localhost:8080/dots'$ curl 'localhost:8080/refused'$ curl 'localhost:8080/signs'Source
//! Unicode braille: text to grade 1 and to Unified English Braille grade 2 with its wordsigns, groupsigns and shortforms, back to text where the cells allow it, and dot numbers and North American Braille ASCII both ways.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Value};const LETTERS: [char; 26] = ['⠁', '⠃', '⠉', '⠙', '⠑', '⠋', '⠛', '⠓', '⠊', '⠚', '⠅', '⠇', '⠍', '⠝', '⠕', '⠏', '⠟', '⠗', '⠎', '⠞', '⠥', '⠧', '⠺', '⠭', '⠽', '⠵'];const NUMBER: char = '⠼';const CAPITAL: char = '⠠';const LETTER: char = '⠰';// The 64 cells in order of their dot bits, as North American Braille ASCII writes them.const BRF: &[u8; 64] = b" A1B'K2L@CIF/MSP\"E3H9O6R^DJG>NTQ,*5<-U8V.%[$+X!&;:4\\0Z7(_?W]#Y)=";// A letter alone that stands for a word: b is "but", so the letter b alone needs the letter sign.const WORDSIGNS: &[(&str, &str)] = &[ ("but", "⠃"), ("can", "⠉"), ("do", "⠙"), ("every", "⠑"), ("from", "⠋"), ("go", "⠛"), ("have", "⠓"), ("just", "⠚"), ("knowledge", "⠅"), ("like", "⠇"), ("more", "⠍"), ("not", "⠝"), ("people", "⠏"), ("quite", "⠟"), ("rather", "⠗"), ("so", "⠎"), ("that", "⠞"), ("us", "⠥"), ("very", "⠧"), ("will", "⠺"), ("it", "⠭"), ("you", "⠽"), ("as", "⠵"), ("child", "⠡"), ("shall", "⠩"), ("this", "⠹"), ("which", "⠱"), ("out", "⠳"), ("still", "⠌"), ("be", "⠆"), ("enough", "⠢"), ("were", "⠶"), ("his", "⠦"), ("in", "⠔"), ("was", "⠴"),];const SHORTFORMS: &[(&str, &str)] = &[ ("about", "⠁⠃"), ("above", "⠁⠃⠧"), ("according", "⠁⠉"), ("across", "⠁⠉⠗"), ("after", "⠁⠋"), ("afternoon", "⠁⠋⠝"), ("afterward", "⠁⠋⠺"), ("again", "⠁⠛"), ("against", "⠁⠛⠌"), ("almost", "⠁⠇⠍"), ("already", "⠁⠇⠗"), ("also", "⠁⠇"), ("although", "⠁⠇⠹"), ("altogether", "⠁⠇⠞"), ("always", "⠁⠇⠺"), ("because", "⠆⠉"), ("before", "⠆⠋"), ("behind", "⠆⠓"), ("below", "⠆⠇"), ("beneath", "⠆⠝"), ("beside", "⠆⠎"), ("between", "⠆⠞"), ("beyond", "⠆⠽"), ("blind", "⠃⠇"), ("braille", "⠃⠗⠇"), ("children", "⠡⠝"), ("conceive", "⠒⠉⠧"), ("conceiving", "⠒⠉⠧⠛"), ("could", "⠉⠙"), ("deceive", "⠙⠉⠧"), ("deceiving", "⠙⠉⠧⠛"), ("declare", "⠙⠉⠇"), ("declaring", "⠙⠉⠇⠛"), ("either", "⠑⠊"), ("first", "⠋⠌"), ("friend", "⠋⠗"), ("good", "⠛⠙"), ("great", "⠛⠗⠞"), ("herself", "⠓⠻⠋"), ("him", "⠓⠍"), ("himself", "⠓⠍⠋"), ("immediate", "⠊⠍⠍"), ("its", "⠭⠎"), ("itself", "⠭⠋"), ("letter", "⠇⠗"), ("little", "⠇⠇"), ("much", "⠍⠡"), ("must", "⠍⠌"), ("myself", "⠍⠽⠋"), ("necessary", "⠝⠑⠉"), ("neither", "⠝⠑⠊"), ("oneself", "⠐⠕⠋"), ("ourselves", "⠳⠗⠧⠎"), ("paid", "⠏⠙"), ("perceive", "⠏⠻⠉⠧"), ("perceiving", "⠏⠻⠉⠧⠛"), ("perhaps", "⠏⠻⠓"), ("quick", "⠟⠅"), ("receive", "⠗⠉⠧"), ("receiving", "⠗⠉⠧⠛"), ("rejoice", "⠗⠚⠉"), ("rejoicing", "⠗⠚⠉⠛"), ("said", "⠎⠙"), ("should", "⠩⠙"), ("such", "⠎⠡"), ("themselves", "⠮⠍⠧⠎"), ("thyself", "⠹⠽⠋"), ("today", "⠞⠙"), ("together", "⠞⠛⠗"), ("tomorrow", "⠞⠍"), ("tonight", "⠞⠝"), ("would", "⠺⠙"), ("your", "⠽⠗"), ("yourself", "⠽⠗⠋"), ("yourselves", "⠽⠗⠧⠎"),];#[derive(Clone, Copy, PartialEq)]enum At { Any, NotFirst, Middle, First,}// Inside a word, longest first at each place.const GROUPS: &[(&str, &str, At)] = &[ ("character", "⠐⠡", At::Any), ("through", "⠐⠹", At::Any), ("question", "⠐⠟", At::Any), ("mother", "⠐⠍", At::Any), ("father", "⠐⠋", At::Any), ("cannot", "⠸⠉", At::Any), ("spirit", "⠸⠎", At::Any), ("their", "⠸⠮", At::Any), ("there", "⠐⠮", At::Any), ("where", "⠐⠱", At::Any), ("these", "⠘⠮", At::Any), ("those", "⠘⠹", At::Any), ("whose", "⠘⠱", At::Any), ("world", "⠸⠺", At::Any), ("under", "⠐⠥", At::Any), ("young", "⠐⠽", At::Any), ("ought", "⠐⠳", At::Any), ("right", "⠐⠗", At::Any), ("upon", "⠘⠥", At::Any), ("word", "⠘⠺", At::Any), ("many", "⠸⠍", At::Any), ("lord", "⠐⠇", At::Any), ("name", "⠐⠝", At::Any), ("know", "⠐⠅", At::Any), ("here", "⠐⠓", At::Any), ("ever", "⠐⠑", At::Any), ("part", "⠐⠏", At::Any), ("some", "⠐⠎", At::Any), ("time", "⠐⠞", At::Any), ("work", "⠐⠺", At::Any), ("had", "⠸⠓", At::Any), ("day", "⠐⠙", At::Any), ("one", "⠐⠕", At::Any), ("ound", "⠨⠙", At::NotFirst), ("ance", "⠨⠑", At::NotFirst), ("sion", "⠨⠝", At::NotFirst), ("less", "⠨⠎", At::NotFirst), ("ount", "⠨⠞", At::NotFirst), ("tion", "⠰⠝", At::NotFirst), ("ness", "⠰⠎", At::NotFirst), ("ment", "⠰⠞", At::NotFirst), ("ong", "⠰⠛", At::NotFirst), ("ful", "⠰⠇", At::NotFirst), ("ity", "⠰⠽", At::NotFirst), ("and", "⠯", At::Any), ("for", "⠿", At::Any), ("the", "⠮", At::Any), ("with", "⠾", At::Any), ("of", "⠷", At::Any), ("ing", "⠬", At::NotFirst), ("con", "⠒", At::First), ("dis", "⠲", At::First), ("be", "⠆", At::First), ("ch", "⠡", At::Any), ("gh", "⠣", At::Any), ("sh", "⠩", At::Any), ("th", "⠹", At::Any), ("wh", "⠱", At::Any), ("ed", "⠫", At::Any), ("er", "⠻", At::Any), ("ou", "⠳", At::Any), ("ow", "⠪", At::Any), ("st", "⠌", At::Any), ("ar", "⠜", At::Any), ("en", "⠢", At::Any), ("in", "⠔", At::Any), ("ea", "⠂", At::Middle), ("bb", "⠆", At::Middle), ("cc", "⠒", At::Middle), ("ff", "⠖", At::Middle), ("gg", "⠶", At::Middle),];// Signs only in a whole word, when it stands alone.const STRONG_WORDS: &[(&str, &str)] = &[("and", "⠯"), ("for", "⠿"), ("of", "⠷"), ("the", "⠮"), ("with", "⠾")];fn punct(c: char) -> Option<&'static str> { Some(match c { ',' => "⠂", ';' => "⠆", ':' => "⠒", '.' => "⠲", '!' => "⠖", '?' => "⠦", '\'' | '’' => "⠄", '-' => "⠤", '/' => "⠸⠌", '(' => "⠐⠣", ')' => "⠐⠜", '“' => "⠘⠦", '”' => "⠘⠴", '–' | '—' => "⠠⠤", '&' => "⠈⠯", '*' => "⠐⠔", '%' => "⠨⠴", '$' => "⠈⠎", '@' => "⠈⠁", '+' => "⠐⠖", '=' => "⠐⠶", '_' => "⠨⠤", '[' => "⠨⠣", ']' => "⠨⠜", '#' => "⠸⠹", '{' => "⠸⠣", '}' => "⠸⠜", '<' => "⠈⠣", '>' => "⠈⠜", _ => return None, })}fn letter(c: char) -> Option<char> { let l = c.to_ascii_lowercase(); if l.is_ascii_lowercase() { Some(LETTERS[l as usize - 97]) } else { None }}// One unbroken run of letters, its capitals marked, contracted or not.fn write_run(run: &[char], alone: bool, grade: i64, out: &mut String) { let n = run.len(); let all_caps = n >= 2 && run.iter().all(|c| c.is_ascii_uppercase()); if all_caps { out.push(CAPITAL); out.push(CAPITAL); } let low: String = run.iter().map(|c| c.to_ascii_lowercase()).collect(); let first_only = !all_caps && run[0].is_ascii_uppercase() && run[1..].iter().all(|c| c.is_ascii_lowercase()); if grade == 2 && alone && (all_caps || first_only || run.iter().all(|c| c.is_ascii_lowercase())) { let whole = SHORTFORMS.iter().chain(WORDSIGNS.iter()).chain(STRONG_WORDS.iter()).find(|e| e.0 == low); if let Some(e) = whole { if first_only { out.push(CAPITAL); } out.push_str(e.1); return; } } // A lone letter that is also a word in grade 2 needs the letter sign to be a letter. if grade == 2 && alone && n == 1 && WORDSIGNS.iter().any(|e| e.1.chars().next() == letter(run[0]) && e.1.chars().count() == 1) { if run[0].is_ascii_uppercase() { out.push(CAPITAL); } out.push(LETTER); out.push(letter(run[0]).unwrap()); return; } let mut i = 0; while i < n { if !all_caps && run[i].is_ascii_uppercase() { out.push(CAPITAL); } let mut used = 0; if grade == 2 { for (word, cells, at) in GROUPS { let len = word.len(); if i + len > n || &low[i..i + len] != *word { continue; } let inside_case = all_caps || run[i + 1..i + len].iter().all(|c| c.is_ascii_lowercase()); let place = match at { At::Any => true, At::NotFirst => i > 0, At::Middle => i > 0 && i + len < n, At::First => i == 0 && n > len, }; if inside_case && place { out.push_str(cells); used = len; break; } } } if used == 0 { out.push(letter(run[i]).unwrap()); used = 1; } i += used; }}fn to_braille(text: &str, grade: i64) -> Result<String, String> { if grade != 1 && grade != 2 { return Err(format!("grade is 1 or 2, not {}", grade)); } let cs: Vec<char> = text.chars().collect(); let mut out = String::new(); let mut i = 0; let mut open_quote = true; let mut after_number = false; while i < cs.len() { let c = cs[i]; if c == '\n' || c == '\r' { out.push(c); i += 1; after_number = false; continue; } if c.is_whitespace() { out.push('⠀'); i += 1; after_number = false; continue; } if c.is_ascii_digit() { out.push(NUMBER); while i < cs.len() { let d = cs[i]; if d.is_ascii_digit() { out.push(if d == '0' { LETTERS[9] } else { LETTERS[d as usize - 49] }); } else if (d == '.' || d == ',') && cs.get(i + 1).map(|x| x.is_ascii_digit()).unwrap_or(false) { out.push(if d == '.' { '⠲' } else { '⠂' }); } else { break; } i += 1; } after_number = true; continue; } if c.is_ascii_alphabetic() { let start = i; while i < cs.len() && cs[i].is_ascii_alphabetic() { i += 1; } let run = &cs[start..i]; // Letters a to j after a number would read as digits. if after_number && run[0].to_ascii_lowercase() <= 'j' { out.push(LETTER); } let before = if start > 0 { cs[start - 1] } else { ' ' }; let after = cs.get(i).copied().unwrap_or(' '); let alone = !before.is_ascii_digit() && !after.is_ascii_digit() && !matches!(before, '\'' | '’') && !matches!(after, '\'' | '’') && !after_number; write_run(run, alone, grade, &mut out); after_number = false; continue; } if c == '"' { out.push(if open_quote { '⠦' } else { '⠴' }); open_quote = !open_quote; i += 1; after_number = false; continue; } match punct(c) { Some(p) => out.push_str(p), None => { let pos = text.chars().take(i).count() + 1; return Err(format!("`{}` (character {}) has no braille form here: English letters, digits and common punctuation only", c, pos)); } } after_number = false; i += 1; } Ok(out)}/// Text as Unicode braille. `grade` 1 spells every letter; 2 is Unified English Braille's contractions by spelling rules alone: a word needing a pronunciation rule, like `hothouse` or `shepherd`, gets the usual sign, as no dictionary is kept.pub fn braille_encode(text: &str, grade: i64) -> Result<String, String> { to_braille(text, grade)}fn from_table(cells: &str, t: &[(&'static str, &'static str)]) -> Option<&'static str> { t.iter().find(|e| e.1 == cells).map(|e| e.0)}fn lower_letter(c: char) -> Option<char> { LETTERS.iter().position(|l| *l == c).map(|i| (b'a' + i as u8) as char)}const SIGNED: &[(&str, &str)] = &[ (",", "⠂"), (";", "⠆"), (":", "⠒"), (".", "⠲"), ("!", "⠖"), ("?", "⠦"), ("'", "⠄"), ("-", "⠤"),];const TWO_CELL: &[(&str, &str)] = &[ ("/", "⠸⠌"), ("(", "⠐⠣"), (")", "⠐⠜"), ("“", "⠘⠦"), ("”", "⠘⠴"), ("—", "⠠⠤"), ("&", "⠈⠯"), ("*", "⠐⠔"), ("%", "⠨⠴"), ("$", "⠈⠎"), ("@", "⠈⠁"), ("+", "⠐⠖"), ("=", "⠐⠶"), ("_", "⠨⠤"), ("[", "⠨⠣"), ("]", "⠨⠜"), ("#", "⠸⠹"), ("{", "⠸⠣"), ("}", "⠸⠜"), ("<", "⠈⠣"), (">", "⠈⠜"),];fn is_end_mark(c: char) -> bool { matches!(c, '⠂' | '⠆' | '⠒' | '⠲' | '⠖' | '⠦' | '⠴' | '⠄' | '⠜' | '⠐' | '⠌')}// One group of cells between spaces. Which sign a cell is depends on where it stands.fn read_word(w: &[char], grade: i64, out: &mut String, uncertain: &mut Vec<String>) { // A word of one sign comes first: `⠴` alone is `was`, though a closing quote after letters. if grade == 2 { let caps = w.iter().take_while(|c| **c == CAPITAL).count().min(2); let whole: String = w[caps..].iter().collect(); if let Some(word) = from_table(&whole, SHORTFORMS).or_else(|| from_table(&whole, WORDSIGNS)).or_else(|| from_table(&whole, STRONG_WORDS)) { out.push_str(&match caps { 0 => word.to_string(), 1 => word[..1].to_uppercase() + &word[1..], _ => word.to_uppercase(), }); return; } } let mut end = w.len(); while end > 0 && is_end_mark(w[end - 1]) { // ⠌ alone is `still`, and at the end of a word is `st`, so it stays with the letters. if matches!(w[end - 1], '⠌' | '⠜') { break; } end -= 1; } let mut start = 0; let mut quote_open = false; if grade == 2 && w.first() == Some(&'⠦') && end > 1 { quote_open = true; start = 1; } let mut cap = 0; while start + cap < end && w[start + cap] == CAPITAL && cap < 2 { cap += 1; } if quote_open { out.push('"'); } let body_start = start + cap; let body: String = w[body_start..end].iter().collect(); let apply = |word: &str, cap: usize, first: bool| -> String { if cap >= 2 { word.to_uppercase() } else if cap == 1 && first { let mut c = word.chars(); c.next().map(|f| f.to_uppercase().collect::<String>() + c.as_str()).unwrap_or_default() } else { word.to_string() } }; let bare = w.len() == end || !body.is_empty(); if grade == 2 && bare { if let Some(word) = from_table(&body, SHORTFORMS).or_else(|| from_table(&body, WORDSIGNS)).or_else(|| from_table(&body, STRONG_WORDS)) { out.push_str(&apply(word, cap, true)); for c in &w[end..] { push_mark(*c, out); } return; } } let mut i = body_start; let mut word = String::new(); let mut caps_next = cap == 1; let all_caps = cap >= 2; let mut number = false; while i < w.len() { let c = w[i]; let first = i == body_start; let last = i + 1 == end; if i >= end { flush(&mut word, all_caps, out); push_mark(c, out); i += 1; continue; } if c == NUMBER { number = true; i += 1; continue; } if number { if let Some(p) = LETTERS[..10].iter().position(|l| *l == c) { word.push(if p == 9 { '0' } else { (b'1' + p as u8) as char }); i += 1; continue; } if (c == '⠲' || c == '⠂') && w.get(i + 1).map(|x| LETTERS[..10].contains(x)).unwrap_or(false) { word.push(if c == '⠲' { '.' } else { ',' }); i += 1; continue; } number = false; } if c == CAPITAL && w.get(i + 1) != Some(&'⠤') { caps_next = true; i += 1; continue; } if c == LETTER && w.get(i + 1).map(|x| lower_letter(*x).is_some()).unwrap_or(false) { let l = lower_letter(w[i + 1]).unwrap(); word.push(if caps_next { l.to_ascii_uppercase() } else { l }); caps_next = false; i += 2; continue; } if i + 1 < w.len() { let two: String = w[i..i + 2].iter().collect(); if grade == 2 { if let Some(g) = GROUPS.iter().find(|g| g.1 == two) { word.push_str(&apply(g.0, if caps_next { 1 } else { 0 }, true)); caps_next = false; i += 2; continue; } } if let Some(p) = from_table(&two, TWO_CELL) { word.push_str(p); i += 2; continue; } } let cell = c.to_string(); let mut found: Option<String> = None; if let Some(l) = lower_letter(c) { found = Some(l.to_string()); } else if grade == 2 { let pick = GROUPS.iter().find(|g| { g.1 == cell && match g.2 { At::First => first, At::Middle => !first && !last, At::NotFirst => !first, At::Any => true, } }); found = pick.map(|g| g.0.to_string()); } match found { Some(text) => { word.push_str(&apply(&text, if caps_next { 1 } else { 0 }, true)); caps_next = false; } None => match from_table(&cell, SIGNED) { Some(p) => word.push_str(p), None => { uncertain.push(cell); word.push('\u{FFFD}'); } }, } i += 1; } flush(&mut word, all_caps, out);}fn flush(word: &mut String, all_caps: bool, out: &mut String) { out.push_str(&if all_caps { word.to_uppercase() } else { word.clone() }); word.clear();}fn push_mark(c: char, out: &mut String) { let cell = c.to_string(); if c == '⠴' { out.push('"'); } else if let Some(p) = from_table(&cell, SIGNED) { out.push_str(p); }}/// Braille back to text: { text, uncertain }. Grade 2 reads each cell by its place in the word, so `⠆` is `be` first, `bb` inside and `;` last. A cell it cannot place is U+FFFD and listed in `uncertain`; a capital at the start of a line and hyphenated words survive, but spacing marks and line breaks of a printed page do not.pub fn braille_decode(cells: &str, grade: i64) -> Result<Value, String> { if grade != 1 && grade != 2 { return Err(format!("grade is 1 or 2, not {}", grade)); } let mut out = String::new(); let mut uncertain = vec![]; for (li, line) in cells.split('\n').enumerate() { if li > 0 { out.push('\n'); } for (wi, word) in line.split(|c| c == '⠀' || c == ' ').enumerate() { if wi > 0 { out.push(' '); } let w: Vec<char> = word.chars().collect(); if let Some(bad) = w.iter().find(|c| !('\u{2800}'..='\u{28FF}').contains(*c)) { return Err(format!("`{}` is not a braille cell", bad)); } read_word(&w, grade, &mut out, &mut uncertain); } } Ok(json!({ "text": out, "uncertain": uncertain }))}/// Each cell as dot numbers: `⠓` is `125`, a blank cell `0`.pub fn braille_dots(cells: &str) -> Vec<String> { cells .chars() .filter(|c| ('\u{2800}'..='\u{28FF}').contains(c)) .map(|c| { let v = c as u32 - 0x2800; let s: String = (0..8).filter(|b| v >> b & 1 == 1).map(|b| char::from_digit(b + 1, 10).unwrap()).collect(); if s.is_empty() { "0".to_string() } else { s } }) .collect()}/// Dot numbers to cells: `["125", "15"]` is `⠓⠑`, for six or eight dots; `0` is a blank cell.pub fn braille_from_dots(dots: Vec<String>) -> Result<String, String> { let mut out = String::new(); for d in &dots { let mut v = 0u32; for c in d.chars() { match c.to_digit(10) { Some(n @ 1..=8) => { if v >> (n - 1) & 1 == 1 { return Err(format!("dot {} twice in `{}`", n, d)); } v |= 1 << (n - 1) } Some(0) if d.len() == 1 => {} _ => return Err(format!("`{}` is not a cell's dots: digits 1 to 8", d)), } } out.push(char::from_u32(0x2800 + v).unwrap()); } Ok(out)}/// Cells as North American Braille ASCII, the text of a `.brf` file for embossers. Eight-dot cells have no form there.pub fn braille_to_ascii(cells: &str) -> Result<String, String> { let mut out = String::new(); for c in cells.chars() { match c { '\n' | '\r' => out.push(c), '\u{2800}'..='\u{283F}' => out.push(BRF[c as usize - 0x2800] as char), _ => return Err(format!("`{}` has no Braille ASCII form", c)), } } Ok(out)}/// North American Braille ASCII as cells; small letters count as capitals, as embossers take them.pub fn braille_from_ascii(text: &str) -> Result<String, String> { let mut out = String::new(); for c in text.chars() { if c == '\n' || c == '\r' { out.push(c); continue; } let up = c.to_ascii_uppercase() as u32; let at = if up < 128 { BRF.iter().position(|b| *b as u32 == up) } else { None }; out.push(char::from_u32(0x2800 + at.ok_or(format!("`{}` is not Braille ASCII", c))? as u32).unwrap()); } Ok(out)}/// The grade 2 signs by kind, for a learner's chart: wordsigns, shortforms and groupsigns, each { word, cells, dots }.pub fn braille_signs() -> Value { let row = |w: &str, c: &str| json!({ "word": w, "cells": c, "dots": braille_dots(c) }); json!({ "wordsigns": WORDSIGNS.iter().chain(STRONG_WORDS.iter()).map(|e| row(e.0, e.1)).collect::<Vec<_>>(), "shortforms": SHORTFORMS.iter().map(|e| row(e.0, e.1)).collect::<Vec<_>>(), "groupsigns": GROUPS.iter().map(|e| row(e.0, e.1)).collect::<Vec<_>>(), })}Related
emoji.qwer · 1.0.0Emoji for chat and comments: :shortcodes: turned into emoji and back, emoji found with their names, stripped from subjects, and searched by name for a picker.7 views · 8 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 downloadsRight-to-left text bidi.qwer · 1.0.0Right-to-left text done right: a text's direction, mixed Arabic or Hebrew text put in display order for printers, terminals and images, user text isolated so it cannot flip a page, and Trojan Source direction controls found and removed.5 views · 7 downloadsSlugs slug.qwer · 1.0.0Slugs for addresses from titles in any script, unique ones for a blog, and text transliterated to ASCII.7 views · 8 downloadsUnicode unicode.qwer · 1.0.0Unicode text made comparable and measured: NFC and NFKC forms, accents removed, characters counted as people see them, safe truncation and terminal width.5 views · 8 downloadsThread 0 posts
On Braille: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.