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 written to an IO writer.
6
7use std::cell::RefCell;
8use std::ops::{Deref, DerefMut};
9use std::rc::Rc;
10use texlang::prelude as txl;
11use texlang::traits::*;
12use texlang::*;
13
14pub struct Component {
15    io_writer: Rc<RefCell<dyn std::io::Write>>,
16    writer: token::Writer,
17}
18
19impl Default for Component {
20    fn default() -> Self {
21        Self {
22            io_writer: Rc::new(RefCell::new(std::io::sink())),
23            writer: Default::default(),
24        }
25    }
26}
27
28#[cfg(feature = "serde")]
29impl serde::Serialize for Component {
30    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
31    where
32        S: serde::Serializer,
33    {
34        ().serialize(serializer)
35    }
36}
37
38#[cfg(feature = "serde")]
39impl<'de> serde::Deserialize<'de> for Component {
40    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41    where
42        D: serde::Deserializer<'de>,
43    {
44        <()>::deserialize(deserializer)?;
45        Ok(Default::default())
46    }
47}
48
49impl Component {
50    fn write_token<S: HasComponent<Self>>(input: &mut vm::ExecutionInput<S>, token: token::Token) {
51        let vm::Parts {
52            state,
53            cs_name_interner,
54            ..
55        } = input.vm_parts();
56        let c = state.component_mut();
57        c.writer
58            .write(
59                c.io_writer.borrow_mut().deref_mut(),
60                cs_name_interner,
61                token.value(),
62            )
63            .unwrap()
64    }
65}
66
67/// Get the `\newline` command.
68///
69/// This adds a newline to the output.
70pub fn get_newline<S: HasComponent<Component>>() -> command::BuiltIn<S> {
71    command::BuiltIn::new_execution(newline_primitive_fn)
72}
73
74fn newline_primitive_fn<S: HasComponent<Component>>(
75    _: token::Token,
76    input: &mut vm::ExecutionInput<S>,
77) -> txl::Result<()> {
78    input.state_mut().component_mut().writer.add_newline();
79    Ok(())
80}
81
82/// Get the `\par` command.
83///
84/// The `\par` command adds two newlines to the output.
85/// Consecutive `\par` commands are treated as one.
86pub fn get_par<S: HasComponent<Component>>() -> command::BuiltIn<S> {
87    command::BuiltIn::new_execution(par_primitive_fn)
88}
89
90fn par_primitive_fn<S: HasComponent<Component>>(
91    _: token::Token,
92    input: &mut vm::ExecutionInput<S>,
93) -> txl::Result<()> {
94    input.state_mut().component_mut().writer.start_paragraph();
95    Ok(())
96}
97
98/// Run the Texlang interpreter for the provided VM and return the result as a string.
99pub fn run_to_string<S: HasComponent<Component>>(
100    vm: &mut vm::VM<S>,
101) -> Result<String, Box<error::TracedTexError>> {
102    let buffer = Rc::new(RefCell::new(Vec::<u8>::new()));
103    vm.state.component_mut().io_writer = buffer.clone();
104    run(vm)?;
105    let result: String = std::str::from_utf8(buffer.borrow().deref()).unwrap().into();
106    Ok(result)
107}
108
109/// Run the Texlang interpreter for the provided VM and return the result as list of tokens.
110pub fn run<S: HasComponent<Component>>(
111    vm: &mut vm::VM<S>,
112) -> Result<(), Box<error::TracedTexError>> {
113    vm.run::<Handlers>()
114}
115
116// Set the IO writer that the script component writes to.
117pub fn set_io_writer<S: HasComponent<Component>, I: std::io::Write + 'static>(
118    vm: &mut vm::VM<S>,
119    writer: I,
120) {
121    vm.state.component_mut().io_writer = Rc::new(RefCell::new(writer))
122}
123
124struct Handlers;
125
126impl<S: HasComponent<Component>> vm::Handlers<S> for Handlers {
127    fn character_handler(
128        input: &mut vm::ExecutionInput<S>,
129        token: token::Token,
130        _: char,
131    ) -> txl::Result<()> {
132        // TODO: it's really not great that the character is ignored.
133        Component::write_token(input, token);
134        Ok(())
135    }
136
137    fn unexpanded_expansion_command(
138        input: &mut vm::ExecutionInput<S>,
139        token: token::Token,
140    ) -> txl::Result<()> {
141        Component::write_token(input, token);
142        Ok(())
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use std::collections::HashMap;
150
151    #[derive(Default)]
152    struct State {
153        script: Component,
154    }
155
156    impl vm::TexlangState for State {}
157
158    implement_has_component![State { script: Component }];
159
160    fn run_script_test(input: &str, want: &str) {
161        let built_ins = HashMap::from([("par", get_par()), ("newline", get_newline())]);
162        let mut vm = vm::VM::<State>::new_with_built_in_commands(built_ins);
163        vm.push_source("testing.tex", input).unwrap();
164        let got = run_to_string(&mut vm).unwrap();
165        let want = want.to_string();
166
167        if got != want {
168            println!("Output is different:");
169            println!("------[got]-------");
170            println!("{}", got);
171            println!("------[want]------");
172            println!("{}", want);
173            println!("-----------------");
174            panic!("run_script test failed");
175        }
176    }
177
178    macro_rules! script_tests {
179        ( $( ($name: ident, $input: expr, $want: expr) ),* $(,)? ) => {
180            $(
181            #[test]
182            fn $name() {
183              run_script_test($input, $want);
184            }
185            )*
186        };
187    }
188
189    script_tests![
190        (char_newline_1, "H\nW", "H W"),
191        (newline_1, "H\\newline W", "H\nW"),
192        (newline_2, "H\\newline \\newline W", "H\n\nW"),
193        (newline_3, "H\\newline \\newline \\newline W", "H\n\n\nW"),
194        (par_1, "H\n\n\nW", "H\n\nW"),
195        (par_2, "H\n\n\n\n\nW", "H\n\nW"),
196    ];
197}