texlang_stdlib/
script.rs

1//! Support for running TeX scripts.
2//!
3//! This module enables using TeX as a scripting language.
4//! TeX files are processed using the usual TeX semantics, but instead
5//! of typesetting the result and outputting it to PDF (say), the output is returned as a list of tokens.
6//! These can be easily converted to a string using [texlang::token::write_tokens].
7
8use texlang::traits::*;
9use texlang::*;
10
11#[derive(Default)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub struct Component {
14    tokens: Vec<token::Token>,
15    #[cfg_attr(feature = "serde", serde(skip))]
16    writer: Option<token::Writer<Box<dyn std::io::Write>>>,
17    num_trailing_newlines: usize,
18    allow_undefined_command: bool,
19}
20
21impl Component {
22    fn push_token(&mut self, token: token::Token, interner: &token::CsNameInterner) {
23        self.tokens.push(token);
24        if let Some(writer) = &mut self.writer {
25            writer.write(interner, token).unwrap()
26        }
27    }
28}
29
30// TODO: this should just be an argument to the run function
31pub fn set_allow_undefined_command<S: HasComponent<Component>>(state: &mut S, value: bool) {
32    state.component_mut().allow_undefined_command = value;
33}
34
35/// Get the `\newline` command.
36///
37/// This adds a newline to the output.
38pub fn get_newline<S: HasComponent<Component>>() -> command::BuiltIn<S> {
39    command::BuiltIn::new_execution(newline_primitive_fn)
40}
41
42fn newline_primitive_fn<S: HasComponent<Component>>(
43    t: token::Token,
44    input: &mut vm::ExecutionInput<S>,
45) -> command::Result<()> {
46    let (state, interner) = input.state_mut_and_cs_name_interner();
47    let mut c = state.component_mut();
48    let newline_token = token::Token::new_space('\n', t.trace_key());
49    c.push_token(newline_token, interner);
50    c.num_trailing_newlines += 1;
51    Ok(())
52}
53
54/// Get the `\par` command.
55///
56/// The `\par` command adds two newlines to the output.
57/// Consecutive `\par` commands are treated as one.
58pub fn get_par<S: HasComponent<Component>>() -> command::BuiltIn<S> {
59    command::BuiltIn::new_execution(par_primitive_fn)
60}
61
62fn par_primitive_fn<S: HasComponent<Component>>(
63    t: token::Token,
64    input: &mut vm::ExecutionInput<S>,
65) -> command::Result<()> {
66    let (state, interner) = input.state_mut_and_cs_name_interner();
67    let mut c = state.component_mut();
68    let par_token = token::Token::new_space('\n', t.trace_key());
69    match c.num_trailing_newlines {
70        0 => {
71            c.push_token(par_token, interner);
72            c.push_token(par_token, interner);
73            c.num_trailing_newlines += 2;
74        }
75        1 => {
76            c.push_token(par_token, interner);
77            c.num_trailing_newlines += 1;
78        }
79        _ => {}
80    }
81    Ok(())
82}
83
84/// Run the Texlang interpreter for the provided VM and return the result as list of tokens.
85pub fn run<S: HasComponent<Component>>(
86    vm: &mut vm::VM<S>,
87) -> Result<Vec<token::Token>, Box<error::Error>> {
88    vm.run::<Handlers>()?;
89    let mut result = Vec::new();
90    std::mem::swap(&mut result, &mut vm.state.component_mut().tokens);
91    Ok(result)
92}
93
94/// Run the Texlang interpreter for the provided VM and write the tokens as strings to the provided writer.
95pub fn run_and_write<S: HasComponent<Component>>(
96    vm: &mut vm::VM<S>,
97    io_writer: Box<dyn std::io::Write>,
98) -> Result<(), Box<error::Error>> {
99    vm.state.component_mut().writer = Some(token::Writer::new(io_writer));
100    vm.run::<Handlers>()
101}
102
103struct Handlers;
104
105impl<S: HasComponent<Component>> vm::Handlers<S> for Handlers {
106    fn character_handler(
107        mut token: token::Token,
108        input: &mut vm::ExecutionInput<S>,
109    ) -> command::Result<()> {
110        let (state, interner) = input.state_mut_and_cs_name_interner();
111        let mut c = state.component_mut();
112        if let Some('\n') = token.char() {
113            token = token::Token::new_space(' ', token.trace_key());
114        }
115        c.push_token(token, interner);
116        c.num_trailing_newlines = 0;
117        Ok(())
118    }
119
120    fn undefined_command_handler(
121        token: token::Token,
122        input: &mut vm::ExecutionInput<S>,
123    ) -> command::Result<()> {
124        if input.state().component().allow_undefined_command {
125            Handlers::character_handler(token, input)
126        } else {
127            Err(error::UndefinedCommandError::new(input.vm(), token).into())
128        }
129    }
130
131    fn unexpanded_expansion_command(
132        token: token::Token,
133        input: &mut vm::ExecutionInput<S>,
134    ) -> command::Result<()> {
135        Handlers::character_handler(token, input)
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use std::collections::HashMap;
142
143    use super::*;
144    use crate::def;
145    use crate::testing::*;
146    use texlang::token;
147
148    fn initial_commands() -> HashMap<&'static str, command::BuiltIn<State>> {
149        HashMap::from([
150            ("par", get_par()),
151            ("def", def::get_def()),
152            ("newline", get_newline()),
153        ])
154    }
155
156    macro_rules! script_tests {
157        ( $( ($name: ident, $input: expr, $want: expr) ),* $(,)? ) => {
158            $(
159            #[test]
160            fn $name() {
161                let options = vec![TestOption::InitialCommands(initial_commands)];
162                let input = $input;
163                let options = ResolvedOptions::new(&options);
164                let mut vm = initialize_vm(&options);
165                let got_tokens = execute_source_code(&mut vm, input, &options);
166                let got = token::write_tokens(&got_tokens.unwrap(), vm.cs_name_interner());
167                let want = $want.to_string();
168
169                if got != want {
170                    println!("Output is different:");
171                    println!("------[got]-------");
172                    println!("{}", got);
173                    println!("------[want]------");
174                    println!("{}", want);
175                    println!("-----------------");
176                    panic!("write_tokens test failed");
177                }
178            }
179            )*
180        };
181    }
182
183    script_tests![
184        (char_newline_1, "H\nW", "H W"),
185        (newline_1, "H\\newline W", "H\nW"),
186        (newline_2, "H\\newline \\newline W", "H\n\nW"),
187        (newline_3, "H\\newline \\newline \\newline W", "H\n\n\nW"),
188        (par_1, "H\n\n\nW", "H\n\nW"),
189        (par_2, "H\n\n\n\n\nW", "H\n\nW"),
190    ];
191}