Text

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

#braille#accessibility#unicode#embosser

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 error

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.

braille_decode(cells: str, grade: int) → anyor an error

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.

braille_dots(cells: str) → list

Each cell as dot numbers: ⠓ is 125, a blank cell 0.

braille_from_dots(dots: list) → stror an error

Dot numbers to cells: ["125", "15"] is ⠓⠑, for six or eight dots; 0 is a blank cell.

braille_to_ascii(cells: str) → stror an error

Cells 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 error

North American Braille ASCII as cells; small letters count as capitals, as embossers take them.

braille_signs() → any

The 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.qwe

Or with curl:

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

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

main.qwe
use braille.qweruse braille_encode, braille_decode from braille      // Quillo 0.6: only these

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

[dependencies]
serde_json = { version = "1", features = ["preserve_order"] }

Example

braille.qwe
// 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

braille.qwer
//! 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<_>>(),    })}
Thread
36Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.