diff --git a/src/encoding.rs b/src/encoding.rs new file mode 100644 index 0000000..d25da16 --- /dev/null +++ b/src/encoding.rs @@ -0,0 +1,70 @@ +use anyhow::{anyhow, Result}; +use codepage_437::CP437_WINGDINGS; + +// Serialize multi-line content with CR-terminated lines +fn encode_multiline(input: &str) -> Result> { + input + .chars() + .map(|c| { + if c == '\n' { + Ok('\r' as u8) + } else { + CP437_WINGDINGS + .encode(c) + .ok_or(anyhow!("Couldn't encode char: {}", c)) + } + }) + .collect() +} + +fn decode_multiline(input: &[u8]) -> String { + input + .iter() + .map(|&x| { + if x == '\r' as u8 { + '\n' + } else { + CP437_WINGDINGS.decode(x) + } + }) + .collect() +} + +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; + + use crate::encoding::{decode_multiline, encode_multiline}; + + fn serialize_code(input: &str) -> Vec { + encode_multiline(input).expect("Error in test") + } + + #[test] + fn roundtrip_multiline() { + let bytes: Vec = (0..=255).collect(); + assert_eq!(bytes, serialize_code(&decode_multiline(&bytes))) + } + + #[test] + fn newlines() { + assert_debug_snapshot!(serialize_code("ABC\nDEF"), @r###" + [ + 65, + 66, + 67, + 13, + 68, + 69, + 70, + ] + "###) + } + + #[test] + fn wingdings() { + assert_debug_snapshot!(decode_multiline(&(0..32).collect::>()), @r###""\0☺☻♥♦♣♠•◘○◙♂♀\n♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼""###); + assert_debug_snapshot!(decode_multiline(&(112..144).collect::>()), @r###""pqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅ""###); + assert_debug_snapshot!(decode_multiline(&(224..=255).collect::>()), @r###""αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u{a0}""###); + } +} diff --git a/src/main.rs b/src/main.rs index e702325..8f8061a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod encoding; mod preprocess; mod world; diff --git a/src/preprocess/eval.rs b/src/preprocess/eval.rs new file mode 100644 index 0000000..244008e --- /dev/null +++ b/src/preprocess/eval.rs @@ -0,0 +1,115 @@ +use std::{fs, path::Path}; + +use anyhow::{anyhow, bail, Result}; + +use super::{ + parse::{parse, Expr}, + scan::scan, +}; + +pub struct Context { + file_loader: Box, +} + +trait FileLoaderTrait { + fn load(&self, path: &Path) -> Result; +} + +struct FileLoader; +struct MockFileLoader { + content: String, +} + +impl FileLoaderTrait for FileLoader { + fn load(&self, path: &Path) -> Result { + fs::read_to_string(path).map_err(|e| anyhow!("Couldn't load file: {}", e)) + } +} + +impl FileLoaderTrait for MockFileLoader { + fn load(&self, _path: &Path) -> Result { + Ok(self.content.clone()) + } +} + +impl Context { + pub fn eval_program(&self, input: &str) -> Result { + let tokens = scan(input).0; + let exprs = parse(tokens)?; + let mut result: Vec = vec![]; + for expr in exprs { + match expr { + Expr::ZztOop(s) => result.push(s), + Expr::Macro(name, args) => match name.as_str() { + "include" => { + if args.len() != 1 { + bail!("wrong number of args for %include"); + } + let filename = if let Expr::String(s) = args[0].as_ref() { + s + } else { + bail!("%include filename must be a string") + }; + + let mut content = self.file_loader.load(Path::new(filename))?; + content = content.replace("\r\n", "\n"); + if content.ends_with("\n") { + content.pop(); + } + result.push(content) + } + _ => bail!("Unknown macro: {:?}", name), + }, + _ => { + bail!("Unexpected expr: {:?}", expr); + } + } + } + Ok(result.join("")) + } +} + +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; + + use super::*; + + fn make_context(data: String) -> Context { + Context { + file_loader: Box::new(MockFileLoader { content: data }), + } + } + + #[test] + fn include() { + let program = format!("foo\n%include \"bb.txt\"\nquux"); + let file = "bar\nbaz\n"; + assert_debug_snapshot!(make_context(file.into()).eval_program(&program), @r###" + Ok( + "foo\nbar\nbaz\nquux", + ) + "###); + } + + #[test] + fn include_windows() { + let program = format!("%include \"foo.txt\""); + let file = "foo\r\nbar"; + assert_debug_snapshot!(make_context(file.into()).eval_program(&program), @r###" + Ok( + "foo\nbar", + ) + "###); + } + + #[test] + fn unknown_macro() { + make_context("".into()) + .eval_program("%foo") + .expect_err("Expected error: unknown macro"); + assert_debug_snapshot!(make_context("".into()) + .eval_program("%foo") + .expect_err("Expected error: unknown macro"), @r###""Unknown macro: \"foo\"""###); + } +} diff --git a/src/preprocess/mod.rs b/src/preprocess/mod.rs index 9876455..d45ed7b 100644 --- a/src/preprocess/mod.rs +++ b/src/preprocess/mod.rs @@ -1,2 +1,3 @@ +pub mod eval; pub mod parse; pub mod scan; diff --git a/src/preprocess/parse.rs b/src/preprocess/parse.rs index cc6e1f4..89eaf62 100644 --- a/src/preprocess/parse.rs +++ b/src/preprocess/parse.rs @@ -76,7 +76,7 @@ impl Parser { args.push(Box::new(Expr::String(val))); } Token::Newline => { - self.advance(); + //self.advance(); break; } _ => return Err(anyhow!("Unexpected token {:?}", token)), @@ -163,7 +163,7 @@ mod tests { [], ), ZztOop( - "Baz.", + "\nBaz.", ), ] "###)