Skip to main content

retex_engine/
misc.rs

1//! Smaller commands: extensions (`\write`, `\openout`, `\special`),
2//! LuaTeX's `normal` commands, hyphenation data, directions, random
3//! numbers, Lua calls and backend hooks.
4
5use std::rc::Rc;
6
7use crate::Engine;
8use crate::cmd::Cmd;
9use crate::input::TokKind;
10use crate::primitives_gen as pg;
11use crate::token::{Token, TokenList};
12
13/// `extension` modifiers.
14#[allow(missing_docs)]
15pub mod ext {
16    pub const OPEN: u32 = 0;
17    pub const WRITE: u32 = 1;
18    pub const CLOSE: u32 = 2;
19    pub const IMMEDIATE: u32 = 4;
20    pub const DEFERRED: u32 = 5;
21    pub const SPECIAL: u32 = 6;
22    pub const SAVE_BOX_RESOURCE: u32 = 7;
23    pub const USE_BOX_RESOURCE: u32 = 8;
24    pub const SAVE_IMAGE_RESOURCE: u32 = 9;
25    pub const USE_IMAGE_RESOURCE: u32 = 10;
26    pub const END_LOCAL_CONTROL: u32 = 11;
27    pub const DVI_EXTENSION: u32 = 12;
28    pub const PDF_EXTENSION: u32 = 13;
29}
30
31/// Whether `\write` stream `j` goes to the terminal (and log).
32fn j_is_terminal(j: i32, open: bool) -> bool {
33    j >= 0 && !((0..=127).contains(&j) && open)
34}
35
36impl Engine {
37    /// Records a LaTeX/package warning written to the terminal (default
38    /// output mode).
39    pub fn record_latex_warning(&mut self, text: &str) {
40        let Some((origin, message, line)) = crate::diag::parse_latex_warning(text) else {
41            return;
42        };
43        let mut d = crate::diag::Diagnostic::new(crate::diag::Severity::Warning, message);
44        d.origin = Some(origin);
45        if let Some((file, cur, _, _)) = self.source_location() {
46            d.file = Some(file);
47            d.line = Some(line.unwrap_or(cur));
48        }
49        if let Some(list) = self.diagnostics.as_mut() {
50            list.push(d);
51        }
52    }
53
54    /// Extension commands (`do_extension`).
55    pub fn do_extension(&mut self) {
56        match self.cur_chr {
57            ext::IMMEDIATE => {
58                self.get_x_token();
59                if self.cur_cmd == Cmd::Extension {
60                    match self.cur_chr {
61                        ext::OPEN => {
62                            self.scan_stream_and_open(true);
63                            return;
64                        }
65                        ext::WRITE => {
66                            let (stream, list) = self.scan_write();
67                            self.write_out(stream, &list);
68                            return;
69                        }
70                        ext::CLOSE => {
71                            self.scan_int();
72                            let n = self.cur_val;
73                            self.close_write_stream(n);
74                            return;
75                        }
76                        ext::PDF_EXTENSION => {
77                            self.pdf_extension(true);
78                            return;
79                        }
80                        ext::SAVE_IMAGE_RESOURCE => {
81                            self.save_image_resource(true);
82                            return;
83                        }
84                        ext::SAVE_BOX_RESOURCE => {
85                            self.save_box_resource(true);
86                            return;
87                        }
88                        _ => {}
89                    }
90                }
91                self.back_input();
92            }
93            ext::OPEN => self.scan_stream_and_open(false),
94            ext::WRITE => {
95                let (stream, list) = self.scan_write();
96                self.append_ext_whatsit(crate::nodes::Whatsit::Write { stream, data: list });
97            }
98            ext::CLOSE => {
99                self.scan_limited_stream();
100                let stream = self.cur_val as u32;
101                self.append_ext_whatsit(crate::nodes::Whatsit::Close { stream });
102            }
103            ext::SPECIAL => {
104                let toks = self.scan_toks_expanded();
105                let data =
106                    self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX));
107                self.append_ext_whatsit(crate::nodes::Whatsit::Special { data });
108            }
109            ext::PDF_EXTENSION => self.pdf_extension(false),
110            ext::SAVE_IMAGE_RESOURCE => self.save_image_resource(false),
111            ext::USE_IMAGE_RESOURCE => self.use_image_resource(),
112            ext::SAVE_BOX_RESOURCE => self.save_box_resource(false),
113            ext::USE_BOX_RESOURCE => self.use_box_resource(),
114            ext::END_LOCAL_CONTROL => {}
115            _ => {
116                let (c, r) = (self.cur_cmd, self.cur_chr);
117                let s = self.print_to_string(|e| e.print_cmd_chr(c, r));
118                self.not_implemented(&String::from_utf8_lossy(&s));
119            }
120        }
121    }
122
123    fn scan_stream_and_open(&mut self, immediate: bool) {
124        self.scan_limited_stream();
125        let n = self.cur_val;
126        self.scan_optional_equals();
127        let name = self.scan_file_name();
128        if immediate {
129            self.open_write_stream(n, &name);
130        } else {
131            self.append_ext_whatsit(crate::nodes::Whatsit::Open {
132                stream: n as u32,
133                name,
134            });
135        }
136    }
137
138    /// Scans a `\openout`/`\closeout` stream number (0–127).
139    fn scan_limited_stream(&mut self) {
140        self.scan_int();
141        let n = self.cur_val;
142        if !(0..=127).contains(&n) {
143            self.print_err("Bad number");
144            self.printer.print_str(" (");
145            self.printer.print_int(i64::from(n));
146            self.printer.print_char(')' as u32);
147            self.help(&[
148                "Since I expected to read a number between 0 and 127,",
149                "I changed this one to zero.",
150            ]);
151            self.int_error_plain();
152            self.cur_val = 0;
153        }
154    }
155
156    /// Completes an error without printing the value again.
157    fn int_error_plain(&mut self) {
158        self.error();
159    }
160
161    /// Appends a whatsit node to the current list.
162    pub fn append_ext_whatsit(&mut self, w: crate::nodes::Whatsit) {
163        let sub = whatsit_subtype(&w);
164        let p = self.new_node(
165            crate::nodes::id::WHATSIT,
166            sub,
167            crate::nodes::NodeData::Whatsit(Box::new(w)),
168        );
169        self.tail_append(p);
170    }
171
172    /// Opens `\write` stream `n` on file `name` (`.tex` added if no
173    /// extension).
174    pub fn open_write_stream(&mut self, n: i32, name: &[u8]) {
175        if !(0..=127).contains(&n) {
176            return;
177        }
178        let mut s = String::from_utf8_lossy(name).into_owned();
179        if std::path::Path::new(&s).extension().is_none() {
180            s.push_str(".tex");
181        }
182        let mut path = std::path::PathBuf::from(&s);
183        if let Some(dir) = &self.files.output_dir {
184            path = dir.join(path);
185        }
186        match std::fs::File::create(&path) {
187            Ok(f) => {
188                let w = crate::files::RecordingWriter::new(f, path.clone(), self.written_sink());
189                if self.write_paths.len() <= n as usize {
190                    self.write_paths.resize(n as usize + 1, None);
191                }
192                self.write_paths[n as usize] = Some(path.clone());
193                self.printer.open_write(n as u32, Box::new(w));
194                // LuaTeX logs \openout as a raw line in the log, on a line of
195                // its own; TeX's line positions are left unchanged.
196                if self.printer.log_opened() {
197                    let mut line = format!("\n\\openout{n} = ").into_bytes();
198                    line.extend_from_slice(s.as_bytes());
199                    line.push(b'\n');
200                    self.printer.log_raw(&line);
201                }
202            }
203            Err(_) => {
204                self.prompt_file_name(name, "output file name", ".tex");
205            }
206        }
207    }
208
209    /// Closes `\write` stream `n`.
210    pub fn close_write_stream(&mut self, n: i32) {
211        if (0..=127).contains(&n) {
212            self.printer.close_write(n as u32);
213        }
214    }
215
216    /// Scans `\write <n> {…}`; returns the stream and the unexpanded list.
217    fn scan_write(&mut self) -> (i32, TokenList) {
218        self.scan_int();
219        let n = self.cur_val;
220        let list = self.scan_general_text();
221        (n, list)
222    }
223
224    /// Expands and writes a `\write` text (`write_out`).
225    pub fn write_out(&mut self, stream: i32, list: &TokenList) {
226        // Expand the list as \edef does, with \endwrite as terminator.
227        let mut toks: Vec<Token> = Vec::with_capacity(list.len() + 2);
228        toks.push(Token::RIGHT_BRACE);
229        toks.push(Token::cs(self.cs.end_write));
230        let q: TokenList = Rc::from(toks);
231        self.ins_list(q);
232        self.begin_token_list(list.clone(), TokKind::WriteText);
233        let q: TokenList = Rc::from(vec![Token::LEFT_BRACE]);
234        self.ins_list(q);
235        let old_mode = self.mode;
236        self.mode = crate::maincontrol::Mode::None;
237        self.cur_cs = self.cs.end_write;
238        let save_def = self.def_ref_cs;
239        self.def_ref_cs = self.cs.end_write;
240        let expanded = self.scan_toks(false, true);
241        self.def_ref_cs = save_def;
242        self.get_token();
243        if self.cur_tok != Token::cs(self.cs.end_write) {
244            self.print_err("Unbalanced write command");
245            self.help(&[
246                "On this page there's a \\write with fewer real {'s than }'s.",
247                "I can't handle that very well; good luck.",
248            ]);
249            self.error();
250            loop {
251                self.get_token();
252                if self.cur_tok == Token::cs(self.cs.end_write) {
253                    break;
254                }
255            }
256        }
257        self.mode = old_mode;
258        let old = self.printer.selector;
259        if self.diagnostics.is_some()
260            && j_is_terminal(stream, self.printer.write_open(stream as u32))
261        {
262            let text = self
263                .print_to_string(|e| e.token_show_list(&expanded, 0, expanded.len(), 10_000_000));
264            self.record_latex_warning(&String::from_utf8_lossy(&text));
265        }
266        // Streams 0-127 are files; a negative stream writes to the log
267        // only, any other stream (or a closed one) to the terminal and log.
268        let j = stream;
269        let to_file = (0..=127).contains(&j) && self.printer.write_open(j as u32);
270        if to_file {
271            self.printer.selector = crate::print::Selector::File(j as u32);
272        } else {
273            if j < 0 && old == crate::print::Selector::TermAndLog {
274                self.printer.selector = crate::print::Selector::LogOnly;
275            }
276            self.printer.print_nl("");
277        }
278        self.token_show_list(&expanded, 0, expanded.len(), 10_000_000);
279        self.printer.print_ln();
280        self.printer.selector = old;
281    }
282
283    /// LuaTeX's `normal` commands (`\savecatcodetable`, …).
284    pub fn do_normal(&mut self) {
285        match self.cur_chr {
286            0 => {
287                // \savecatcodetable n
288                self.scan_int();
289                let n = self.cur_val as u32;
290                let table = self.eqtb.cur_cat_table();
291                let copy = self
292                    .eqtb
293                    .catcode_tables
294                    .get(&table)
295                    .cloned()
296                    .unwrap_or_else(|| crate::eqtb::CodeTable::new(crate::eqtb::initex_catcode));
297                self.eqtb.catcode_tables.insert(n, copy);
298            }
299            1 => {
300                // \initcatcodetable n
301                self.scan_int();
302                let n = self.cur_val as u32;
303                self.eqtb
304                    .catcode_tables
305                    .insert(n, crate::eqtb::CodeTable::new(crate::eqtb::initex_catcode));
306            }
307            2 => {
308                // \setrandomseed: \randomseed shows the seed's absolute
309                // value; the generator ignores its sign anyway.
310                self.scan_int();
311                self.random_seed = self.cur_val.wrapping_abs();
312                self.randoms.init(self.cur_val);
313            }
314            3 => self.append_ext_whatsit(crate::nodes::Whatsit::SavePos),
315            4 | 5 => {
316                // \latelua: a whatsit executed at shipout.
317                let data = self.scan_general_text();
318                self.append_ext_whatsit(crate::nodes::Whatsit::LateLua {
319                    data: crate::nodes::LateData::Tokens(data),
320                    name: Vec::new(),
321                });
322            }
323            6 => {
324                // \expandglyphsinfont <font> <stretch> <shrink> <step>
325                // (microtype's \pdffontexpand).
326                self.scan_font_ident();
327                let f = self.cur_val as u32;
328                self.scan_int();
329                let stretch = self.cur_val;
330                self.scan_int();
331                let shrink = self.cur_val;
332                self.scan_int();
333                let step = self.cur_val;
334                self.set_font_expansion(f, stretch, shrink, step);
335                if self.scan_keyword("autoexpand") {
336                    self.scan_optional_space();
337                    self.luatex_warning("font expansion", "autoexpand not supported");
338                }
339            }
340            _ => {
341                self.scan_font_ident();
342            }
343        }
344    }
345
346    /// Scans a direction specifier (`TLT`, `TRT`, `RTT`, `LTL`).
347    pub fn scan_direction_spec(&mut self) -> i32 {
348        for (i, name) in ["TLT", "TRT", "LTL", "RTT"].iter().enumerate() {
349            if self.scan_keyword(name) {
350                self.scan_optional_space();
351                return i as i32;
352            }
353        }
354        // LuaTeX also takes the value of another direction parameter
355        // (`\pagedir\bodydir`, as babel does), but no number.
356        self.get_x_token();
357        if self.cur_cmd == Cmd::AssignDir {
358            return self.dir_param(self.cur_chr);
359        }
360        self.back_input();
361        self.print_err("Bad direction");
362        self.help(&[]);
363        self.error();
364        0
365    }
366
367    /// Stores a direction parameter (`\textdir` etc.). Observed in
368    /// LuaTeX: the assignment is always local, even with `\global`.
369    pub fn assign_dir_param(&mut self, global: bool, p: u32, d: i32) {
370        let _ = global;
371        if (p as usize) < self.eqtb.dir_par.len() {
372            self.define(
373                false,
374                crate::eqtb::Loc::DirPar(p),
375                crate::eqtb::Val::Int(i64::from(d)),
376            );
377        }
378    }
379
380    /// Reads a direction parameter.
381    pub fn dir_param(&self, p: u32) -> i32 {
382        self.eqtb.dir_par.get(p as usize).map_or(0, |s| s.val)
383    }
384
385    /// Sets the direction of a box register.
386    pub fn set_box_dir(&mut self, _n: u32, _d: i32) {}
387
388    /// `\uniformdeviate n`: METAFONT's `unif_rand(n)`, between 0
389    /// (inclusive) and `n` (exclusive).
390    pub fn uniform_deviate(&mut self, n: i32) -> i32 {
391        self.randoms.unif_rand(n)
392    }
393
394    /// `\normaldeviate`: METAFONT's `norm_rand`, a scaled normal deviate.
395    pub fn normal_deviate(&mut self) -> i32 {
396        self.randoms.norm_rand()
397    }
398
399    /// LuaTeX's `\immediateassignment` / `\immediateassigned`.
400    pub fn immediate_assignment(&mut self, group: bool) {
401        if group {
402            let list = self.scan_general_text();
403            let _ = list;
404        } else {
405            self.get_x_token();
406            if self.cur_cmd > Cmd::MAX_NON_PREFIXED {
407                self.prefixed_command();
408            }
409        }
410    }
411
412    /// `\pdfvariable` and `\pdffeedback` in expansion contexts.
413    pub fn backend_expandable(&mut self) {
414        if self.cur_cmd == Cmd::Variable {
415            self.expand_backend_variable();
416        } else {
417            self.expand_backend_feedback();
418        }
419    }
420
421    /// The last saved box/image resource indices.
422    pub fn last_saved_resource(&self, m: u32) -> i32 {
423        use crate::scan::last_item as li;
424        match m {
425            li::LAST_SAVED_IMAGE_RESOURCE_INDEX => self.last_image_index,
426            li::LAST_SAVED_IMAGE_RESOURCE_PAGES => self.last_image_pages,
427            li::LAST_SAVED_BOX_RESOURCE_INDEX => self.last_box_index,
428            _ => 0,
429        }
430    }
431
432    /// `\luadef`: an expandable Lua call, or a non-expandable one when
433    /// `\protected`.
434    pub fn def_lua_call(&mut self, global: bool, protected: bool) {
435        self.get_r_token();
436        let p = self.cur_cs;
437        self.scan_optional_equals();
438        self.scan_int();
439        let cmd = if protected {
440            Cmd::LuaCall
441        } else {
442            Cmd::LuaExpandableCall
443        };
444        let m = crate::eqtb::Meaning::prim(cmd, self.cur_val as u32);
445        self.define(
446            global,
447            crate::eqtb::Loc::Cs(p),
448            crate::eqtb::Val::Meaning(m),
449        );
450    }
451
452    /// The Lua code for the token list of `\directlua` or `\latelua`:
453    /// the list as text, without control sequences named `par` (LuaTeX
454    /// drops them, so blank lines in Lua code are harmless).
455    pub fn lua_code(&mut self, toks: &[Token]) -> Vec<u8> {
456        let par = self.hash.lookup(b"par").map(Token::cs);
457        let kept: Vec<Token>;
458        let toks = if par.is_some_and(|p| toks.contains(&p)) {
459            kept = toks.iter().copied().filter(|t| Some(*t) != par).collect();
460            &kept[..]
461        } else {
462            toks
463        };
464        self.print_to_string(|e| e.show_token_list(toks, 0, toks.len(), i64::MAX))
465    }
466
467    /// Runs Lua code for `\directlua`.
468    pub fn run_lua(&mut self, code: &[u8]) {
469        let Some(host) = self.lua.clone() else {
470            self.print_err("retex: \\directlua needs the Lua interpreter");
471            self.help(&[]);
472            self.error();
473            return;
474        };
475        let t0 = std::time::Instant::now();
476        let r = host.run(self, "\\directlua", code);
477        crate::timing::LUA.with(|a| a.add(t0));
478        if crate::timing::enabled() && t0.elapsed().as_millis() >= 5 {
479            let head = String::from_utf8_lossy(&code[..code.len().min(70)]).replace('\n', " ");
480            crate::timing::mark(&format!(
481                "slow Lua chunk ({:.1} ms): {head}",
482                t0.elapsed().as_secs_f64() * 1e3
483            ));
484        }
485        if let Err(msg) = r {
486            // RETEX_DEBUG_LUA=1 shows the failing chunk as expanded.
487            if std::env::var_os("RETEX_DEBUG_LUA").is_some() {
488                eprintln!(
489                    "--- failing Lua chunk ---\n{}\n---",
490                    String::from_utf8_lossy(code)
491                );
492            }
493            self.lua_error(&msg);
494        }
495    }
496
497    /// Reports a Lua error the way LuaTeX does.
498    pub fn lua_error(&mut self, msg: &str) {
499        self.print_err("LuaTeX error ");
500        self.printer.print_str(msg);
501        self.help(&[
502            "The lua interpreter ran into a problem, so the",
503            "remainder of this lua chunk will be ignored.",
504        ]);
505        self.error();
506    }
507
508    /// Calls a registered Lua function (`\luafunction n`).
509    pub fn call_lua_function(&mut self, n: i32) {
510        // The function receives its slot number.
511        let code = format!("return lua.get_functions_table()[{n}]({n})");
512        self.run_lua(code.as_bytes());
513    }
514
515    /// Calls stored Lua bytecode (`\luabytecode n`).
516    pub fn call_lua_bytecode(&mut self, n: i32) {
517        let code = format!("return lua.bytecode[{n}]()");
518        self.run_lua(code.as_bytes());
519    }
520
521    /// Expandable Lua calls defined by `\luadef`.
522    pub fn lua_expandable_call(&mut self) {
523        let n = self.cur_chr as i32;
524        self.call_lua_function(n);
525    }
526
527    /// Finishes output files at the end of the run (PDF backend hook).
528    pub fn finish_output(&mut self) {
529        self.finish_backend();
530        if let Some(hook) = self.finish_hook.take() {
531            hook(self);
532        }
533    }
534
535    /// The sink where written file contents are recorded.
536    fn written_sink(&self) -> crate::files::WrittenSink {
537        self.written_sink.clone()
538    }
539
540    /// Stores a muglue `\Umath…` parameter.
541    pub fn store_math_param_glue(&mut self, g: crate::glue::Glue) -> i32 {
542        self.math_glues.push(g);
543        (self.math_glues.len() - 1) as i32
544    }
545
546    /// Retrieves a muglue `\Umath…` parameter.
547    pub fn math_param_glue(&self, v: i32) -> crate::glue::Glue {
548        self.math_glues
549            .get(v as usize)
550            .copied()
551            .unwrap_or(crate::glue::Glue::ZERO)
552    }
553}
554
555/// Avoids an unused import warning while `pg` is not used directly.
556#[allow(dead_code)]
557const _PG: u32 = pg::assign_int::COUNT;
558
559impl Engine {
560    /// Converts a dimension string like `"1.5pt"` to scaled points by
561    /// running TeX's dimension scanner on it (for the Lua API).
562    pub fn dimen_from_string(&mut self, s: &str) -> i64 {
563        let mut toks = crate::the::str_toks(s.as_bytes());
564        let stop = Token::cs(self.cs.frozen_relax);
565        toks.push(stop);
566        let depth = self.input.stack.len();
567        self.ins_list(Rc::from(toks));
568        self.scan_normal_dimen();
569        let v = self.cur_val;
570        // Discard whatever the scanner left, up to the stop marker.
571        while self.input.stack.len() > depth {
572            self.get_token();
573            if self.cur_tok == stop {
574                break;
575            }
576        }
577        i64::from(v)
578    }
579}
580
581/// The LuaTeX whatsit subtype of a whatsit payload.
582fn whatsit_subtype(w: &crate::nodes::Whatsit) -> u16 {
583    use crate::nodes::{Whatsit, whatsit_sub as ws};
584    match w {
585        Whatsit::Open { .. } => ws::OPEN,
586        Whatsit::Write { .. } => ws::WRITE,
587        Whatsit::Close { .. } => ws::CLOSE,
588        Whatsit::Special { .. } => ws::SPECIAL,
589        Whatsit::LateLua { .. } => ws::LATE_LUA,
590        Whatsit::PdfLiteral { late, .. } => {
591            if *late {
592                ws::PDF_LATE_LITERAL
593            } else {
594                ws::PDF_LITERAL
595            }
596        }
597        Whatsit::SavePos => ws::SAVE_POS,
598        Whatsit::PdfColorstack { .. } => ws::PDF_COLORSTACK,
599        Whatsit::PdfSetmatrix { .. } => ws::PDF_SETMATRIX,
600        Whatsit::PdfSave => ws::PDF_SAVE,
601        Whatsit::PdfRestore => ws::PDF_RESTORE,
602        Whatsit::PdfAnnot { .. } => ws::PDF_ANNOT,
603        Whatsit::PdfStartLink { .. } => ws::PDF_START_LINK,
604        Whatsit::PdfEndLink => ws::PDF_END_LINK,
605        Whatsit::PdfDest { .. } => ws::PDF_DEST,
606        Whatsit::PdfRefobj { .. } => ws::PDF_REFOBJ,
607        Whatsit::Other { .. } => ws::USER_DEFINED,
608    }
609}