1#![deny(missing_docs)]
2
3use std::{
10 error::Error,
11 fmt::{Debug, Display},
12};
13
14use ascii::{AsciiChar, AsciiStr, AsciiString};
15
16pub use ascii;
17
18const BRAILLE_ASCII_TO_UNICODE: [char; 96] = [
19 '⠀', '⠮', '⠐', '⠼', '⠫', '⠩', '⠯', '⠄', '⠷', '⠾', '⠡', '⠬', '⠠', '⠤', '⠨', '⠌', '⠴', '⠂', '⠆',
20 '⠒', '⠲', '⠢', '⠖', '⠶', '⠦', '⠔', '⠱', '⠰', '⠣', '⠿', '⠜', '⠹', '⠈', '⠁', '⠃', '⠉', '⠙', '⠑',
21 '⠋', '⠛', '⠓', '⠊', '⠚', '⠅', '⠇', '⠍', '⠝', '⠕', '⠏', '⠟', '⠗', '⠎', '⠞', '⠥', '⠧', '⠺', '⠭',
22 '⠽', '⠵', '⠪', '⠳', '⠻', '⠘', '⠸', '⠈', '⠁', '⠃', '⠉', '⠙', '⠑', '⠋', '⠛', '⠓', '⠊', '⠚', '⠅',
23 '⠇', '⠍', '⠝', '⠕', '⠏', '⠟', '⠗', '⠎', '⠞', '⠥', '⠧', '⠺', '⠭', '⠽', '⠵', '⠪', '⠳', '⠻', '⠘',
24 '⠸',
25];
26
27const BRAILLE_CELL_TO_BRAILLE_ASCII: &[u8; 64] =
28 b" A1B'K2L@CIF/MSP\"E3H9O6R^DJG>NTQ,*5<-U8V.%[$+X!&;:4\\0Z7(_?W]#Y)=";
29
30#[derive(Clone, Copy, PartialEq, Eq)]
36pub struct FromUnicodeError<T> {
37 index: usize,
38 source: T,
39}
40
41impl<T> Display for FromUnicodeError<T> {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 write!(
44 f,
45 "the char at index {} is not Braille or an ASCII control character",
46 self.index
47 )
48 }
49}
50
51impl<T> Debug for FromUnicodeError<T> {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 Display::fmt(self, f)
54 }
55}
56
57impl<T> Error for FromUnicodeError<T> {}
58
59#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
62pub struct BrailleAsciiString {
63 inner: AsciiString,
64}
65
66impl BrailleAsciiString {
67 pub fn new() -> Self {
69 Default::default()
70 }
71
72 pub fn from_ascii(s: AsciiString) -> Self {
74 BrailleAsciiString { inner: s }
75 }
76
77 pub fn from_unicode_braille<T: AsRef<str>>(t: T) -> Result<Self, FromUnicodeError<T>> {
82 let s = t.as_ref();
83 let mut bytes = Vec::with_capacity(s.chars().count());
84 for (i, c) in s.char_indices() {
85 if c <= ' ' {
86 bytes.push(c as u8);
87 } else if ('\u{2800}'..'\u{2840}').contains(&c) {
88 let j = (u32::from(c) - 0x2800) as u8;
89 bytes.push(BRAILLE_CELL_TO_BRAILLE_ASCII[usize::from(j)]);
90 } else {
91 return Err(FromUnicodeError {
92 index: i,
93 source: t,
94 });
95 }
96 }
97 Ok(BrailleAsciiString {
98 inner: unsafe { AsciiString::from_ascii_unchecked(bytes) },
99 })
100 }
101
102 pub fn as_ascii(&self) -> &AsciiStr {
104 &self.inner
105 }
106
107 pub fn as_ascii_mut(&mut self) -> &mut AsciiStr {
109 &mut self.inner
110 }
111
112 pub fn into_ascii(self) -> AsciiString {
114 self.inner
115 }
116
117 pub fn to_unicode_braille(&self) -> String {
119 self.inner
120 .chars()
121 .map(|ch| {
122 if ch < AsciiChar::Space {
123 ch.as_char()
124 } else {
125 let i = usize::from(ch.as_byte() - 0x20);
126 BRAILLE_ASCII_TO_UNICODE[i]
127 }
128 })
129 .collect()
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 const TOM_SAWYER_ASCII: &str = r#"
138 ,,! ,,ADV5TURES ,,( ,,TOM ,,SAWY]
139 ,,BY
140 ,,M>K ,,TWA9
141 7,SAMUEL ,LANGHORNE ,CLEM5S7
142 ;,P ;,R ;,E ;,F ,A ;,C ;,E
143 ,,MO/ (! ADV5TURES RECORD$ 9 ? BOOK
144RE,Y O3URR$2 "O OR TWO 7 EXP]I;ES ( MY
145[N1 ! RE/ ^? ( BOYS :O 7 S*OOLMATES (
146M9E4 ,HUCK ,F9N IS DRAWN F LIFE2 ,TOM
147,SAWY] AL1 B N F AN 9DIVIDUAL--HE IS A
148-B9A- TION (! "*I/ICS ( ?REE BOYS :OM ,I
149KNEW1 & "!=E 2L;GS 6! -POSITE ORD] (
150>*I- TECTURE4
151 ,! ODD SUP]/I;NS T\*$ ^U 7 ALL PREVA-
152L5T AM;G *N & SLAVES 9 ! ,WE/ AT ! P]IOD
153( ? /ORY--T IS 6SAY1 ?IRTY OR =TY YE>S
154AGO4
155 ,AL? MY BOOK IS 9T5D$ MA9LY =! EN-
156T]TA9;T ( BOYS & GIRLS1 ,I HOPE X W N 2
157%UNN$ 0M5 & WOM5 ON T A3.T1 = "P ( MY
158PLAN HAS BE5 6TRY 6PL1SANTLY REM9D
159ADULTS ( :AT !Y ONCE 7 !MVS1 &( H[ !Y
160FELT & ?"\ & TALK$1 & :AT QUE] 5T]PRISES
161!Y "S"TS 5GAG$ IN4
162 ,,! ,,AU?OR4
163 ,,H>T=D1 #AHGF4
164 ;,T ,O ;,M ;,S ,A ;,W ;,Y ;,E ;,R
165--------------------------------------#B
166"#;
167
168 const TOM_SAWYER_BRAILLE: &str = r#"
169⠀⠀⠠⠠⠮⠀⠠⠠⠁⠙⠧⠢⠞⠥⠗⠑⠎⠀⠠⠠⠷⠀⠠⠠⠞⠕⠍⠀⠠⠠⠎⠁⠺⠽⠻⠀
170⠀⠀⠠⠠⠃⠽⠀
171⠀⠀⠠⠠⠍⠜⠅⠀⠠⠠⠞⠺⠁⠔⠀
172⠀⠀⠶⠠⠎⠁⠍⠥⠑⠇⠀⠠⠇⠁⠝⠛⠓⠕⠗⠝⠑⠀⠠⠉⠇⠑⠍⠢⠎⠶⠀⠀⠀
173⠀⠀⠰⠠⠏⠀⠰⠠⠗⠀⠰⠠⠑⠀⠰⠠⠋⠀⠠⠁⠀⠰⠠⠉⠀⠰⠠⠑⠀
174⠀⠀⠠⠠⠍⠕⠌⠀⠷⠮⠀⠁⠙⠧⠢⠞⠥⠗⠑⠎⠀⠗⠑⠉⠕⠗⠙⠫⠀⠔⠀⠹⠀⠃⠕⠕⠅
175⠗⠑⠠⠽⠀⠕⠒⠥⠗⠗⠫⠆⠀⠐⠕⠀⠕⠗⠀⠞⠺⠕⠀⠶⠀⠑⠭⠏⠻⠊⠰⠑⠎⠀⠷⠀⠍⠽
176⠪⠝⠂⠀⠮⠀⠗⠑⠌⠀⠘⠹⠀⠷⠀⠃⠕⠽⠎⠀⠱⠕⠀⠶⠀⠎⠡⠕⠕⠇⠍⠁⠞⠑⠎⠀⠷
177⠍⠔⠑⠲⠀⠠⠓⠥⠉⠅⠀⠠⠋⠔⠝⠀⠊⠎⠀⠙⠗⠁⠺⠝⠀⠋⠀⠇⠊⠋⠑⠆⠀⠠⠞⠕⠍
178⠠⠎⠁⠺⠽⠻⠀⠁⠇⠂⠀⠃⠀⠝⠀⠋⠀⠁⠝⠀⠔⠙⠊⠧⠊⠙⠥⠁⠇⠤⠤⠓⠑⠀⠊⠎⠀⠁
179⠤⠃⠔⠁⠤⠀⠞⠊⠕⠝⠀⠷⠮⠀⠐⠡⠊⠌⠊⠉⠎⠀⠷⠀⠹⠗⠑⠑⠀⠃⠕⠽⠎⠀⠱⠕⠍⠀⠠⠊
180⠅⠝⠑⠺⠂⠀⠯⠀⠐⠮⠿⠑⠀⠆⠇⠰⠛⠎⠀⠖⠮⠀⠤⠏⠕⠎⠊⠞⠑⠀⠕⠗⠙⠻⠀⠷⠀⠀⠀
181⠜⠡⠊⠤⠀⠞⠑⠉⠞⠥⠗⠑⠲⠀⠀
182⠀⠀⠠⠮⠀⠕⠙⠙⠀⠎⠥⠏⠻⠌⠊⠰⠝⠎⠀⠞⠳⠡⠫⠀⠘⠥⠀⠶⠀⠁⠇⠇⠀⠏⠗⠑⠧⠁⠤
183⠇⠢⠞⠀⠁⠍⠰⠛⠀⠡⠝⠀⠯⠀⠎⠇⠁⠧⠑⠎⠀⠔⠀⠮⠀⠠⠺⠑⠌⠀⠁⠞⠀⠮⠀⠏⠻⠊⠕⠙
184⠷⠀⠹⠀⠌⠕⠗⠽⠤⠤⠞⠀⠊⠎⠀⠖⠎⠁⠽⠂⠀⠹⠊⠗⠞⠽⠀⠕⠗⠀⠿⠞⠽⠀⠽⠑⠜⠎
185⠁⠛⠕⠲⠀⠀⠀
186⠀⠀⠠⠁⠇⠹⠀⠍⠽⠀⠃⠕⠕⠅⠀⠊⠎⠀⠔⠞⠢⠙⠫⠀⠍⠁⠔⠇⠽⠀⠿⠮⠀⠑⠝⠤
187⠞⠻⠞⠁⠔⠰⠞⠀⠷⠀⠃⠕⠽⠎⠀⠯⠀⠛⠊⠗⠇⠎⠂⠀⠠⠊⠀⠓⠕⠏⠑⠀⠭⠀⠺⠀⠝⠀⠆
188⠩⠥⠝⠝⠫⠀⠴⠍⠢⠀⠯⠀⠺⠕⠍⠢⠀⠕⠝⠀⠞⠀⠁⠒⠨⠞⠂⠀⠿⠀⠐⠏⠀⠷⠀⠍⠽
189⠏⠇⠁⠝⠀⠓⠁⠎⠀⠃⠑⠢⠀⠖⠞⠗⠽⠀⠖⠏⠇⠂⠎⠁⠝⠞⠇⠽⠀⠗⠑⠍⠔⠙⠀
190⠁⠙⠥⠇⠞⠎⠀⠷⠀⠱⠁⠞⠀⠮⠽⠀⠕⠝⠉⠑⠀⠶⠀⠮⠍⠧⠎⠂⠀⠯⠷⠀⠓⠪⠀⠮⠽
191⠋⠑⠇⠞⠀⠯⠀⠹⠐⠳⠀⠯⠀⠞⠁⠇⠅⠫⠂⠀⠯⠀⠱⠁⠞⠀⠟⠥⠑⠻⠀⠢⠞⠻⠏⠗⠊⠎⠑⠎
192⠮⠽⠀⠐⠎⠐⠞⠎⠀⠢⠛⠁⠛⠫⠀⠊⠝⠲⠀⠀⠀⠀
193⠀⠀⠠⠠⠮⠀⠠⠠⠁⠥⠹⠕⠗⠲⠀⠀
194⠀⠀⠠⠠⠓⠜⠞⠿⠙⠂⠀⠼⠁⠓⠛⠋⠲⠀
195⠀⠀⠰⠠⠞⠀⠠⠕⠀⠰⠠⠍⠀⠰⠠⠎⠀⠠⠁⠀⠰⠠⠺⠀⠰⠠⠽⠀⠰⠠⠑⠀⠰⠠⠗⠀⠀⠀⠀
196⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠼⠃
197"#;
198
199 #[test]
200 fn test_to_unicode_braille() {
201 let ascii = AsciiString::from_ascii(
202 " A1B'K2L@CIF/MSP\"E3H9O6R^DJG>NTQ,*5<-U8V.%[$+X!&;:4\\0Z7(_?W]#Y)=",
203 )
204 .unwrap();
205 let val = BrailleAsciiString::from_ascii(ascii);
206 let expected = "⠀⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏⠐⠑⠒⠓⠔⠕⠖⠗⠘⠙⠚⠛⠜⠝⠞⠟⠠⠡⠢⠣⠤⠥⠦⠧⠨⠩⠪⠫⠬⠭⠮⠯⠰⠱⠲⠳⠴⠵⠶⠷⠸⠹⠺⠻⠼⠽⠾⠿";
207 assert_eq!(val.to_unicode_braille(), expected);
208
209 let ascii = AsciiString::from_ascii(TOM_SAWYER_ASCII).unwrap();
210 let tom_sawyer = BrailleAsciiString::from_ascii(ascii);
211
212 assert_eq!(tom_sawyer.to_unicode_braille(), TOM_SAWYER_BRAILLE);
213 }
214
215 #[test]
216 fn test_from_unicode_braille() {
217 let val = BrailleAsciiString::from_unicode_braille(
218 "⠀⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏⠐⠑⠒⠓⠔⠕⠖⠗⠘⠙⠚⠛⠜⠝⠞⠟⠠⠡⠢⠣⠤⠥⠦⠧⠨⠩⠪⠫⠬⠭⠮⠯⠰⠱⠲⠳⠴⠵⠶⠷⠸⠹⠺⠻⠼⠽⠾⠿",
219 )
220 .unwrap();
221 let expected = " A1B'K2L@CIF/MSP\"E3H9O6R^DJG>NTQ,*5<-U8V.%[$+X!&;:4\\0Z7(_?W]#Y)=";
222 assert_eq!(val.as_ascii().as_str(), expected);
223
224 let val = BrailleAsciiString::from_unicode_braille(TOM_SAWYER_BRAILLE).unwrap();
225 assert_eq!(val.as_ascii().as_str(), TOM_SAWYER_ASCII);
226 }
227}