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