mod decode;
use std::fmt::Write as _;
use serde::Serialize;
use crate::Result;
use crate::formats::narc::Narc;
use crate::io::ByteSource;
pub struct Text<'a, A> {
archive: A,
narc: &'a Narc,
}
impl<'a, A: ByteSource> Text<'a, A> {
pub fn new(archive: A, narc: &'a Narc) -> Self {
Self { archive, narc }
}
pub fn len(&self) -> usize {
self.narc.len()
}
pub fn is_empty(&self) -> bool {
self.narc.is_empty()
}
pub fn bank(&self, bank: usize) -> Result<Vec<Message>> {
decode::bank(&self.narc.member(&self.archive, bank)?)
}
pub fn strings(&self, bank: usize) -> Result<Vec<String>> {
Ok(self.bank(bank)?.iter().map(Message::plain).collect())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
pub struct Message {
pub tokens: Vec<Token>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Token {
Text(String),
LineBreak,
ScrollLine,
ClearBox,
Command {
id: u16,
args: Vec<u16>,
},
Symbol(u16),
Unknown(u16),
}
const SYMBOLS: [(u16, &str, &str); 4] = [
(0x246D, "♂", "MALE"),
(0x246E, "♀", "FEMALE"),
(0x2486, "PK", "PK"),
(0x2487, "MN", "MN"),
];
fn symbol(code: u16) -> Option<(&'static str, &'static str)> {
SYMBOLS
.iter()
.find(|(known, _, _)| *known == code)
.map(|&(_, text, name)| (text, name))
}
impl Message {
pub fn plain(&self) -> String {
let mut out = String::new();
for token in &self.tokens {
match token {
Token::Text(text) => out.push_str(text),
Token::LineBreak | Token::ScrollLine | Token::ClearBox => out.push('\n'),
Token::Symbol(code) => out.push_str(symbol(*code).map_or("", |(text, _)| text)),
Token::Command { .. } | Token::Unknown(_) => {}
}
}
out
}
pub fn markup(&self) -> String {
let mut out = String::new();
for token in &self.tokens {
match token {
Token::Text(text) => {
for ch in text.chars() {
if matches!(ch, '\\' | '{') {
out.push('\\');
}
out.push(ch);
}
}
Token::LineBreak => out.push('\n'),
Token::ScrollLine => out.push('\r'),
Token::ClearBox => out.push('\x0C'),
Token::Command { id, args } => write_command(&mut out, *id, args),
Token::Symbol(code) => {
_ = match symbol(*code) {
Some((_, name)) => write!(out, "{{{name}}}"),
None => write!(out, "{{SYMBOL {code:#06X}}}"),
}
}
Token::Unknown(code) => _ = write!(out, "{{UNK_CHAR {code:#06X}}}"),
}
}
out
}
}
fn write_command(out: &mut String, id: u16, args: &[u16]) {
let [group, kind] = id.to_be_bytes();
let (name, kind) = match (id, group) {
(0xFF00, _) => ("COLOR".to_owned(), None),
(_, 0x01) => ("STRVAR".to_owned(), Some(kind.into())),
(_, 0x02) => ("NUMVAR".to_owned(), Some(kind.into())),
_ => (format!("CMD_{id:04X}"), None),
};
out.push('{');
out.push_str(&name);
for (index, value) in kind.into_iter().chain(args.iter().copied()).enumerate() {
out.push_str(if index == 0 { " " } else { ", " });
_ = write!(out, "{value}");
}
out.push('}');
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn markup_is_lossless_and_plain_is_text_only() {
let message = Message {
tokens: vec![
Token::Text("Hi {there}".into()),
Token::LineBreak,
Token::Command {
id: 0x0101,
args: vec![0],
},
Token::Command {
id: 0xFF00,
args: vec![2],
},
Token::Command {
id: 0xBD01,
args: vec![],
},
Token::Symbol(0x246D),
Token::Symbol(0x2462),
Token::ScrollLine,
Token::ClearBox,
Token::Unknown(0xD800),
],
};
assert_eq!(
message.markup(),
"Hi \\{there}\n{STRVAR 1, 0}{COLOR 2}{CMD_BD01}{MALE}{SYMBOL 0x2462}\r\x0C{UNK_CHAR 0xD800}"
);
assert_eq!(message.plain(), "Hi {there}\n♂\n\n");
}
}