texlang_stdlib/
io.rs

1//! Primitives for input and output of files
2//!
3
4use std::path;
5use texlang::parse::FileLocation;
6use texlang::traits::*;
7use texlang::*;
8
9/// Get the `\input` expansion primitive.
10pub fn get_input<S: TexlangState>() -> command::BuiltIn<S> {
11    command::BuiltIn::new_expansion(input_fn)
12}
13
14fn input_fn<S: TexlangState>(
15    input_token: token::Token,
16    input: &mut vm::ExpansionInput<S>,
17) -> Result<Vec<token::Token>, Box<command::Error>> {
18    let file_location = FileLocation::parse(input)?;
19    let (file_path, source_code) = read_file(input_token, input.vm(), file_location, ".tex")?;
20    input.push_source(input_token, file_path.into(), source_code)?;
21    Ok(Vec::new())
22}
23
24fn read_file<S>(
25    t: token::Token,
26    vm: &vm::VM<S>,
27    file_location: parse::FileLocation,
28    default_extension: &str,
29) -> command::Result<(String, String)> {
30    let raw_file_path = format![
31        "{}{}",
32        &file_location.path,
33        match &file_location.extension {
34            None => default_extension,
35            Some(ext) => ext,
36        }
37    ];
38    let file_path = match path::Path::new(&raw_file_path).is_absolute() {
39        true => path::Path::new(&raw_file_path).to_path_buf(),
40        false => match file_location.area {
41            None => match &vm.working_directory {
42                None => {
43                    panic!("TODO: handle this error");
44                }
45                Some(working_directory) => working_directory.join(&raw_file_path),
46            },
47            Some(_area) => {
48                panic!("TODO: handle this error");
49            }
50        },
51    };
52
53    match vm.file_system.read_to_string(&file_path) {
54        Ok(s) => Ok((raw_file_path, s)),
55        Err(_err) => Err(error::SimpleTokenError::new(
56            vm,
57            t,
58            format!("could not read from {:?}", &file_path),
59        )
60        .into()), // .add_note(format!("underlying filesystem error: {err}"))
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::testing::*;
68    use std::collections::HashMap;
69
70    fn initial_commands() -> HashMap<&'static str, command::BuiltIn<State>> {
71        HashMap::from([("input", get_input())])
72    }
73
74    fn custom_vm_initialization(vm: &mut vm::VM<State>) {
75        let cwd = vm.working_directory.clone().unwrap();
76
77        let mut file_system: InMemoryFileSystem = Default::default();
78
79        let mut path_1 = cwd.to_path_buf();
80        path_1.push("file1.tex");
81        file_system.add_file(path_1, "content1\n");
82
83        let mut path_2 = cwd.to_path_buf();
84        path_2.push("file2.tex");
85        file_system.add_file(path_2, "content2%\n");
86
87        let mut path_3 = cwd.to_path_buf();
88        path_3.push("file3.tex");
89        file_system.add_file(path_3, r"\input nested/file4");
90
91        let mut path_4 = cwd.to_path_buf();
92        path_4.push("nested");
93        path_4.push("file4.tex");
94        file_system.add_file(path_4, "content4");
95
96        vm.file_system = Box::new(file_system);
97    }
98
99    test_suite!(
100        options(
101            TestOption::InitialCommands(initial_commands),
102            TestOption::CustomVMInitialization(custom_vm_initialization),
103        ),
104        expansion_equality_tests(
105            (basic_case, r"\input file1 hello", "content1 hello"),
106            (input_together, r"\input file2 hello", r"content2hello"),
107            (basic_case_with_ext, r"\input file1.tex", r"content1 "),
108            (nested, r"\input file3", r"content4"),
109        ),
110    );
111}