use textcode::{Iso6937, decode as decode_iso6937, encode as encode_iso6937};
const REPLACEMENT: char = char::REPLACEMENT_CHARACTER;
const ENCODE_FALLBACK: u8 = b'?';
const DIACRITICS: [(u8, char, char); 14] = [
(0xC1, '\u{0300}', '`'),
(0xC2, '\u{0301}', '´'),
(0xC3, '\u{0302}', '^'),
(0xC4, '\u{0303}', '~'),
(0xC5, '\u{0304}', '¯'),
(0xC6, '\u{0306}', '˘'),
(0xC7, '\u{0307}', '˙'),
(0xC8, '\u{0308}', '¨'),
(0xCA, '\u{030A}', '˚'),
(0xCB, '\u{0327}', '¸'),
(0xCC, '\u{0332}', '_'),
(0xCD, '\u{030B}', '˝'),
(0xCE, '\u{0328}', '˛'),
(0xCF, '\u{030C}', 'ˇ'),
];
fn diacritic(code: u8) -> Option<(char, char)> {
DIACRITICS
.iter()
.find(|(c, _, _)| *c == code)
.map(|&(_, mark, spacing)| (mark, spacing))
}
pub(crate) fn decode(data: &[u8]) -> String {
let mut text = String::with_capacity(data.len());
let mut i = 0;
while i < data.len() {
i += decode_next(&data[i..], &mut text);
}
text
}
fn decode_next(data: &[u8], text: &mut String) -> usize {
let code = data[0];
let Some((mark, spacing)) = diacritic(code) else {
text.push(decode_single(code));
return 1;
};
match data.get(1) {
Some(&letter) if letter.is_ascii_alphabetic() => {
push_accented_letter(text, code, letter, mark);
2
}
Some(b' ') => {
text.push(spacing);
2
}
_ => {
text.push(spacing);
1
}
}
}
fn decode_single(code: u8) -> char {
match code {
0x24 => '¤',
0xA4 => '$',
0x7F | 0xA6 | 0xA8 | 0xC0 | 0xC9 => REPLACEMENT,
_ => decode_iso6937::<Iso6937>([code])
.chars()
.next()
.filter(|&c| c != '\0')
.unwrap_or(REPLACEMENT),
}
}
fn push_accented_letter(text: &mut String, code: u8, letter: u8, mark: char) {
let composed = decode_iso6937::<Iso6937>([code, letter]);
let mut chars = composed.chars();
match (chars.next(), chars.next()) {
(Some(c), None) if c != '\0' && c != REPLACEMENT => text.push(c),
_ => {
text.push(char::from(letter));
text.push(mark);
}
}
}
pub(crate) fn encode(text: &str) -> Vec<u8> {
let mut data = Vec::with_capacity(text.len());
for c in text.chars() {
match c {
'$' => data.push(0xA4),
'¤' => data.push(0x24),
'€' => data.push(ENCODE_FALLBACK),
_ => data.extend(encode_iso6937::<Iso6937>(c.encode_utf8(&mut [0; 4]))),
}
}
data
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_letters_digits_and_punctuation() {
assert_eq!("Hello, World! 123 #@", decode(b"Hello, World! 123 #@"));
}
#[test]
fn currency_signs() {
assert_eq!("¤", decode(&[0x24]));
assert_eq!("$500,000", decode(b"\xA4500,000"));
assert_eq!("£¥", decode(&[0xA3, 0xA5]));
}
#[test]
fn unassigned_codes() {
for code in [0x7F, 0xA6, 0xA8, 0xC0, 0xC9, 0xD8, 0xE5] {
assert_eq!("\u{FFFD}", decode(&[code]), "code {code:02X}");
}
}
#[test]
fn column_a_to_f_symbols() {
assert_eq!("¡«←↑→↓", decode(&[0xA1, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF]));
assert_eq!("\u{2015}®©™♪", decode(&[0xD0, 0xD2, 0xD3, 0xD4, 0xD5]));
assert_eq!(
"\u{2126}ÆØŒßþ\u{AD}",
decode(&[0xE0, 0xE1, 0xE9, 0xEA, 0xFB, 0xFC, 0xFF])
);
}
#[test]
fn accented_letters() {
assert_eq!("Ä", decode(&[0xC8, b'A']));
assert_eq!("ê", decode(&[0xC3, b'e']));
assert_eq!(
"é à ç č ő ą",
decode(b"\xC2e \xC1a \xCBc \xCFc \xCDo \xCEa")
);
}
#[test]
fn letter_without_precomposed_form() {
assert_eq!("x\u{0308}", decode(&[0xC8, b'x']));
assert_eq!("a\u{0332}", decode(&[0xCC, b'a']));
}
#[test]
fn diacritic_followed_by_space_is_stand_alone() {
assert_eq!("´A", decode(&[0xC2, b' ', b'A']));
}
#[test]
fn diacritic_without_letter_is_stand_alone() {
assert_eq!("^", decode(&[0xC3]));
assert_eq!("^)", decode(&[0xC3, b')']));
assert_eq!("^^", decode(&[0xC3, 0xC3]));
assert_eq!("¨1", decode(&[0xC8, b'1']));
}
#[test]
fn encode_currency_signs() {
assert_eq!(vec![0xA4, b'5'], encode("$5"));
assert_eq!(vec![0x24], encode("¤"));
assert_eq!(vec![b'?'], encode("€"));
}
#[test]
fn encode_accented_letters() {
assert_eq!(vec![0xC2, b'e', 0xC8, b'A'], encode("éÄ"));
}
#[test]
fn encode_decode_roundtrip() {
let text = "Är det $5 eller 5 ¤? Ça coûte ¼ £, señor!";
assert_eq!(text, decode(&encode(text)));
}
}