Add macro evaluator with %include implementation
This commit is contained in:
@@ -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<Vec<u8>> {
|
||||
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<u8> {
|
||||
encode_multiline(input).expect("Error in test")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_multiline() {
|
||||
let bytes: Vec<u8> = (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::<Vec<_>>()), @r###""\0☺☻♥♦♣♠•◘○◙♂♀\n♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼""###);
|
||||
assert_debug_snapshot!(decode_multiline(&(112..144).collect::<Vec<_>>()), @r###""pqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅ""###);
|
||||
assert_debug_snapshot!(decode_multiline(&(224..=255).collect::<Vec<_>>()), @r###""αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u{a0}""###);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod encoding;
|
||||
mod preprocess;
|
||||
mod world;
|
||||
|
||||
|
||||
@@ -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<dyn FileLoaderTrait>,
|
||||
}
|
||||
|
||||
trait FileLoaderTrait {
|
||||
fn load(&self, path: &Path) -> Result<String>;
|
||||
}
|
||||
|
||||
struct FileLoader;
|
||||
struct MockFileLoader {
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl FileLoaderTrait for FileLoader {
|
||||
fn load(&self, path: &Path) -> Result<String> {
|
||||
fs::read_to_string(path).map_err(|e| anyhow!("Couldn't load file: {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
impl FileLoaderTrait for MockFileLoader {
|
||||
fn load(&self, _path: &Path) -> Result<String> {
|
||||
Ok(self.content.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn eval_program(&self, input: &str) -> Result<String> {
|
||||
let tokens = scan(input).0;
|
||||
let exprs = parse(tokens)?;
|
||||
let mut result: Vec<String> = 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\"""###);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod eval;
|
||||
pub mod parse;
|
||||
pub mod scan;
|
||||
|
||||
@@ -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.",
|
||||
),
|
||||
]
|
||||
"###)
|
||||
|
||||
Reference in New Issue
Block a user