texlang_stdlib/
job.rs

1use std::{
2    collections::HashMap,
3    ffi::OsString,
4    path::{Path, PathBuf},
5};
6use texlang::prelude as txl;
7use texlang::traits::*;
8use texlang::*;
9use texlang_common as common;
10
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct Component {
13    #[cfg_attr(feature = "serde", serde(skip))]
14    job_name: Option<PathBuf>,
15    default_job_name: PathBuf,
16    dump_format: i32,
17    dump_validate: i32,
18    #[cfg_attr(feature = "serde", serde(skip))]
19    num_dumps: usize,
20}
21
22impl Default for Component {
23    fn default() -> Self {
24        Self {
25            job_name: None,
26            default_job_name: "jobname".into(),
27            dump_format: 0,
28            dump_validate: 0,
29            num_dumps: 0,
30        }
31    }
32}
33
34impl Component {
35    pub fn job_name(&self) -> &Path {
36        match &self.job_name {
37            None => &self.default_job_name,
38            Some(job_name) => job_name,
39        }
40    }
41
42    // TODO: this should be called after the first \input.
43    // Probably should add a hook to the VM for this case.
44    // TODO: maybe this should be bundled with \input
45    /// Set the job name based on the provided file path.
46    pub fn set_job_name(&mut self, file_path: &Path) {
47        if let Some(file_stem) = file_path.file_stem() {
48            self.job_name = Some(file_stem.into())
49        }
50    }
51}
52
53/// Get the `\jobname` primitive.
54pub fn get_jobname<S: HasComponent<Component>>() -> command::BuiltIn<S> {
55    command::BuiltIn::new_expansion(|token, input| {
56        let job_name: String = input
57            .state()
58            .component()
59            .job_name()
60            .to_string_lossy()
61            .into();
62        input.push_string_tokens(token, &job_name);
63        Ok(())
64    })
65}
66
67/// Get the `\dump` primitive.
68#[cfg(feature = "serde")]
69pub fn get_dump<
70    S: HasComponent<Component>
71        + serde::Serialize
72        + serde::de::DeserializeOwned
73        + common::HasFileSystem,
74>() -> command::BuiltIn<S> {
75    command::BuiltIn::new_execution(dump_primitive_fn)
76}
77
78#[cfg(feature = "serde")]
79fn dump_primitive_fn<
80    S: HasComponent<Component>
81        + serde::Serialize
82        + serde::de::DeserializeOwned
83        + common::HasFileSystem,
84>(
85    _: token::Token,
86    input: &mut vm::ExecutionInput<S>,
87) -> txl::Result<()> {
88    let component = input.state().component();
89    let mut job_name: OsString = component.job_name().into();
90    let num_dumps = component.num_dumps;
91    if num_dumps > 0 {
92        job_name.push(format!["-{}", num_dumps + 1]);
93    }
94    let mut output_file: PathBuf = job_name.into();
95    output_file.set_extension(match component.dump_format {
96        1 => "fmt.json",
97        2 => "fmt.bincode",
98        _ => "fmt.mp",
99    });
100
101    // TODO: error handle all these serialization errors.
102    let serialized = match component.dump_format {
103        0 => rmp_serde::encode::to_vec(input.vm()).unwrap(),
104        1 => serde_json::to_vec_pretty(input.vm()).unwrap(),
105        2 => bincode::serde::encode_to_vec(input.vm(), bincode::config::standard()).unwrap(),
106        i => {
107            return Err(input.fatal_error(
108                error::SimpleFailedPreconditionError::new(format![
109                    r"\dumpFormat has invalid value {i}",
110                ])
111                .with_note(r"\dumpFormat must be either 0 (message pack), 1 (json) or 2 (bincode)"),
112            ))
113        }
114    };
115
116    if component.dump_validate != 0 {
117        let built_in_commands: HashMap<&str, command::BuiltIn<S>> = input
118            .vm()
119            .commands_map
120            .built_in_commands()
121            .iter()
122            .map(|(cs_name, command)| {
123                (
124                    input.vm().cs_name_interner().resolve(*cs_name).unwrap(),
125                    command.clone(),
126                )
127            })
128            .collect();
129        match component.dump_format {
130            0 => {
131                let mut deserializer = rmp_serde::decode::Deserializer::from_read_ref(&serialized);
132                // TODO: this shouldn't panic if the deserialization fails!
133                vm::VM::<S>::deserialize_with_built_in_commands(
134                    &mut deserializer,
135                    built_in_commands,
136                )
137                .unwrap();
138            }
139            1 => {
140                let mut deserializer = serde_json::Deserializer::from_slice(&serialized);
141                vm::VM::<S>::deserialize_with_built_in_commands(
142                    &mut deserializer,
143                    built_in_commands,
144                )
145                .unwrap();
146            }
147            2 => {
148                let deserialized: Box<vm::serde::DeserializedVM<S>> =
149                    bincode::serde::decode_from_slice(&serialized, bincode::config::standard())
150                        .unwrap()
151                        .0;
152                vm::serde::finish_deserialization(deserialized, built_in_commands);
153            }
154            _ => unreachable!(),
155        };
156    }
157
158    // TODO: error handle file write
159    input
160        .state()
161        .file_system()
162        .borrow_mut()
163        .write_bytes(&output_file, &serialized)
164        .unwrap();
165    input.state_mut().component_mut().num_dumps += 1;
166    Ok(())
167}
168
169pub fn get_dumpformat<S: HasComponent<Component>>() -> command::BuiltIn<S> {
170    variable::Command::new_singleton(
171        |state: &S, _| &state.component().dump_format,
172        |state: &mut S, _| &mut state.component_mut().dump_format,
173    )
174    .into()
175}
176
177pub fn get_dumpvalidate<S: HasComponent<Component>>() -> command::BuiltIn<S> {
178    variable::Command::new_singleton(
179        |state: &S, _| &state.component().dump_validate,
180        |state: &mut S, _| &mut state.component_mut().dump_validate,
181    )
182    .into()
183}