Skip to main content

retex_engine/
print.rs

1//! Low-level output: terminal, log file, strings and `\write` streams.
2//!
3//! This follows TeX's printing model (*TeX: The Program* §54–§71): a
4//! `selector` chooses the destinations. Terminal and log lines are broken
5//! after `max_print_line` characters. The current `\newlinechar` starts a
6//! new line.
7//!
8//! Character representation, as LuaTeX prints it to the terminal and log:
9//! characters below 32 appear in `^^` notation (`^^A`), except tab (9),
10//! line feed (10) and carriage return (13), which are output raw, like 127.
11//! Characters ≥ 128 are output as UTF-8. Line lengths count bytes, but a
12//! multi-byte character is never split across lines. `\write` files and
13//! strings receive raw UTF-8.
14
15use std::io::Write;
16
17use crate::utf8x;
18
19/// Where printed text goes (TeX's `selector`).
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum Selector {
22    /// Discard output.
23    NoPrint,
24    /// Terminal only.
25    TermOnly,
26    /// Log file only.
27    LogOnly,
28    /// Terminal and log file.
29    TermAndLog,
30    /// The pseudo-printing buffer used by `show_context`.
31    Pseudo,
32    /// Append to the string under construction.
33    NewString,
34    /// A `\write` stream.
35    File(u32),
36}
37
38impl Selector {
39    /// TeX's `selector<pseudo` test: real output destinations where the
40    /// new-line character starts a new line.
41    pub fn is_output(self) -> bool {
42        matches!(
43            self,
44            Selector::NoPrint
45                | Selector::TermOnly
46                | Selector::LogOnly
47                | Selector::TermAndLog
48                | Selector::File(_)
49        )
50    }
51}
52
53/// The printer state.
54pub struct Printer {
55    /// Current destination.
56    pub selector: Selector,
57    /// Characters on the current terminal line.
58    pub term_offset: usize,
59    /// Whether the last terminal output was an automatic line break at
60    /// `max_print_line`.
61    pub term_just_wrapped: bool,
62    /// Characters on the current log line.
63    pub file_offset: usize,
64    /// Maximum line length for terminal and log.
65    pub max_print_line: usize,
66    /// Extra characters allowed on a log line. LuaTeX lets text printed
67    /// from Lua (`texio`) and its own warnings run one character past
68    /// `max_print_line` in the log, but not on the terminal.
69    pub log_slack: usize,
70    /// Default output mode: nothing is written to the terminal, but line
71    /// positions are tracked as if it were, so the log is unchanged.
72    pub term_quiet: bool,
73    /// While the terminal is quiet: the text of its current line (a
74    /// prompt such as `Enter file name: `), shown when the terminal is read.
75    pub quiet_line: Vec<u8>,
76    /// While `Some`, the text printed to the terminal or log is also
77    /// collected here (used to turn error messages into diagnostics).
78    pub capture: Option<Vec<u8>>,
79    /// Width of error context lines.
80    pub error_line: usize,
81    /// Width of the first half of an error context line.
82    pub half_error_line: usize,
83    /// Characters printed so far in the current pseudo-print or other
84    /// counted operation (TeX's `tally`).
85    pub tally: usize,
86    /// Buffer for pseudo printing: one entry per counted position, a byte
87    /// or the bytes of an invalid input byte shown with its successors.
88    pub trick_buf: Vec<Vec<u8>>,
89    /// Limit of pseudo-printed characters to keep.
90    pub trick_count: usize,
91    /// The tally at the start of the second line of pseudo printing.
92    pub first_count: usize,
93    /// The string under construction (`NewString`).
94    pub cur_string: Vec<u8>,
95    /// The current `\newlinechar` (−1 for none).
96    pub new_line_char: i32,
97    /// The terminal.
98    term: Box<dyn Write>,
99    /// The log file, once opened.
100    log: Option<Box<dyn Write>>,
101    /// `\write` streams that are open, by stream number.
102    write_files: Vec<Option<Box<dyn Write>>>,
103}
104
105impl std::fmt::Debug for Printer {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        f.debug_struct("Printer")
108            .field("selector", &self.selector)
109            .field("term_offset", &self.term_offset)
110            .field("file_offset", &self.file_offset)
111            .finish_non_exhaustive()
112    }
113}
114
115impl Printer {
116    /// A printer writing to `term`.
117    pub fn new(term: Box<dyn Write>) -> Self {
118        Printer {
119            selector: Selector::TermOnly,
120            term_offset: 0,
121            term_just_wrapped: false,
122            file_offset: 0,
123            max_print_line: 79,
124            log_slack: 0,
125            term_quiet: false,
126            quiet_line: Vec::new(),
127            capture: None,
128            error_line: 79,
129            half_error_line: 50,
130            tally: 0,
131            trick_buf: Vec::new(),
132            trick_count: 0,
133            first_count: 0,
134            cur_string: Vec::new(),
135            new_line_char: -1,
136            term,
137            log: None,
138            write_files: Vec::new(),
139        }
140    }
141
142    /// Installs the log file.
143    pub fn set_log(&mut self, log: Box<dyn Write>) {
144        self.log = Some(log);
145    }
146
147    /// Whether a log file is open.
148    pub fn log_opened(&self) -> bool {
149        self.log.is_some()
150    }
151
152    /// Flushes and closes the log file.
153    pub fn close_log(&mut self) {
154        if let Some(mut log) = self.log.take() {
155            let _ = log.flush();
156        }
157    }
158
159    /// Writes raw bytes to the log without bookkeeping (for banners).
160    pub fn log_raw(&mut self, bytes: &[u8]) {
161        if let Some(log) = self.log.as_mut() {
162            let _ = log.write_all(bytes);
163        }
164    }
165
166    /// Writes raw bytes to the terminal without bookkeeping (nothing while
167    /// the terminal is quiet).
168    pub fn term_raw(&mut self, bytes: &[u8]) {
169        if !self.term_quiet {
170            let _ = self.term.write_all(bytes);
171        }
172    }
173
174    /// Flushes the terminal (TeX's `update_terminal`).
175    pub fn update_terminal(&mut self) {
176        let _ = self.term.flush();
177    }
178
179    /// Opens `\write` stream `n` on `w`.
180    pub fn open_write(&mut self, n: u32, w: Box<dyn Write>) {
181        let n = n as usize;
182        if self.write_files.len() <= n {
183            self.write_files.resize_with(n + 1, || None);
184        }
185        self.write_files[n] = Some(w);
186    }
187
188    /// The `\write` streams that are open.
189    pub fn open_write_streams(&self) -> Vec<u32> {
190        (0..self.write_files.len() as u32)
191            .filter(|&n| self.write_files[n as usize].is_some())
192            .collect()
193    }
194
195    /// Closes `\write` stream `n`.
196    pub fn close_write(&mut self, n: u32) {
197        if let Some(slot) = self.write_files.get_mut(n as usize)
198            && let Some(mut w) = slot.take()
199        {
200            let _ = w.flush();
201        }
202    }
203
204    /// Whether `\write` stream `n` is open.
205    pub fn write_open(&self, n: u32) -> bool {
206        matches!(self.write_files.get(n as usize), Some(Some(_)))
207    }
208
209    fn wterm(&mut self, b: &[u8]) {
210        if !self.term_quiet {
211            let _ = self.term.write_all(b);
212        } else {
213            match b.iter().rposition(|&c| c == b'\n') {
214                Some(i) => self.quiet_line = b[i + 1..].to_vec(),
215                None => self.quiet_line.extend_from_slice(b),
216            }
217        }
218    }
219
220    fn wlog(&mut self, b: &[u8]) {
221        if let Some(log) = self.log.as_mut() {
222            let _ = log.write_all(b);
223        }
224    }
225
226    /// Ends the current line (TeX's `print_ln`).
227    pub fn print_ln(&mut self) {
228        if let Some(c) = self.capture.as_mut()
229            && matches!(
230                self.selector,
231                Selector::TermAndLog | Selector::TermOnly | Selector::LogOnly
232            )
233        {
234            c.push(b'\n');
235        }
236        match self.selector {
237            Selector::TermAndLog => {
238                self.wterm(b"\n");
239                self.wlog(b"\n");
240                self.term_offset = 0;
241                self.file_offset = 0;
242            }
243            Selector::LogOnly => {
244                self.wlog(b"\n");
245                self.file_offset = 0;
246            }
247            Selector::TermOnly => {
248                self.wterm(b"\n");
249                self.term_offset = 0;
250            }
251            Selector::NoPrint | Selector::Pseudo | Selector::NewString => {}
252            Selector::File(n) => {
253                if let Some(Some(w)) = self.write_files.get_mut(n as usize) {
254                    let _ = w.write_all(b"\n");
255                }
256            }
257        }
258    }
259
260    /// Outputs one printable unit (one ASCII character or the bytes of one
261    /// multi-byte character) to terminal and/or log, with line breaking.
262    fn put_unit(&mut self, bytes: &[u8], term: bool, log: bool) {
263        let n = bytes.len();
264        if term {
265            if n > 1 && self.term_offset + n > self.max_print_line {
266                self.wterm(b"\n");
267                self.term_offset = 0;
268            }
269            self.wterm(bytes);
270            self.term_offset += n;
271            self.term_just_wrapped = false;
272            if self.term_offset >= self.max_print_line {
273                self.wterm(b"\n");
274                self.term_offset = 0;
275                self.term_just_wrapped = true;
276            }
277        }
278        if log {
279            let max = self.max_print_line + self.log_slack;
280            if n > 1 && self.file_offset + n > max {
281                self.wlog(b"\n");
282                self.file_offset = 0;
283            }
284            self.wlog(bytes);
285            let before = self.file_offset;
286            self.file_offset += n;
287            // TeX tests for equality: a line that already ran past the
288            // limit (see `log_slack`) is not broken by later characters.
289            if self.file_offset >= max && before < max {
290                self.wlog(b"\n");
291                self.file_offset = 0;
292            }
293        }
294    }
295
296    /// Prints character `c`, handling the new-line character and the
297    /// printable representation for terminal and log.
298    pub fn print_char(&mut self, c: u32) {
299        // Beyond Unicode: the character code of `\alignmark` and
300        // `\aligntab` (0x110001), which a parameter text written with
301        // `\alignmark` shows (`\def\a\alignmark1{}` gives `macro:^^A1->`,
302        // observed in LuaTeX 1.22 and 1.24).
303        let c = if c > 0x10_FFFF { c - 0x11_0000 } else { c };
304        if c as i32 == self.new_line_char && self.selector.is_output() {
305            self.print_ln();
306            return;
307        }
308        match self.selector {
309            Selector::TermAndLog | Selector::TermOnly | Selector::LogOnly => {
310                if let Some(cap) = self.capture.as_mut() {
311                    utf8x::push(cap, c);
312                }
313                let (term, log) = match self.selector {
314                    Selector::TermAndLog => (true, true),
315                    Selector::TermOnly => (true, false),
316                    _ => (false, true),
317                };
318                if c < 32 && c != 9 && c != 10 && c != 13 {
319                    for b in [b'^', b'^', (c + 64) as u8] {
320                        self.put_unit(&[b], term, log);
321                    }
322                } else if c < 128 {
323                    self.put_unit(&[c as u8], term, log);
324                } else {
325                    let e = utf8x::encode(c);
326                    self.put_unit(&e, term, log);
327                }
328            }
329            Selector::NoPrint => {}
330            Selector::Pseudo => {
331                // A control character stays a single byte: it is shown in
332                // `^^` notation when the buffer is printed, but counts as
333                // one column (observed).
334                let e = utf8x::encode(c);
335                for b in e {
336                    self.pseudo_byte(b);
337                }
338                return;
339            }
340            Selector::NewString => utf8x::push(&mut self.cur_string, c),
341            Selector::File(n) => {
342                let e = utf8x::encode(c);
343                if let Some(Some(w)) = self.write_files.get_mut(n as usize) {
344                    let _ = w.write_all(&e);
345                }
346            }
347        }
348        self.tally += 1;
349    }
350
351    /// Prints byte `b` as it is (a byte of invalid UTF-8 input).
352    pub fn print_raw_byte(&mut self, b: u8) {
353        let (term, log) = match self.selector {
354            Selector::TermAndLog => (true, true),
355            Selector::TermOnly => (true, false),
356            Selector::LogOnly => (false, true),
357            _ => (false, false),
358        };
359        if term || log {
360            if term {
361                self.put_unit(&[b], true, false);
362            }
363            if log {
364                if b >= 0xC0 {
365                    // Observed: in the log, a byte that starts a sequence
366                    // takes no column.
367                    self.wlog(&[b]);
368                } else {
369                    self.put_unit(&[b], false, true);
370                }
371            }
372            return;
373        }
374        match self.selector {
375            Selector::Pseudo => self.pseudo_unit(&[b]),
376            Selector::NewString => self.cur_string.push(b),
377            _ => {}
378        }
379    }
380
381    /// Pseudo-prints `bytes` (possibly none) as they are, counted as one
382    /// position: how LuaTeX shows an invalid byte of an input line.
383    pub fn pseudo_unit(&mut self, bytes: &[u8]) {
384        if self.tally < self.trick_count {
385            let i = self.tally % self.error_line;
386            if self.trick_buf.len() <= i {
387                self.trick_buf.resize(i + 1, vec![b' ']);
388            }
389            self.trick_buf[i] = bytes.to_vec();
390        }
391        self.tally += 1;
392    }
393
394    fn pseudo_byte(&mut self, b: u8) {
395        self.pseudo_unit(&[b]);
396    }
397
398    /// Prints an ASCII string (e.g. a fixed message).
399    pub fn print_str(&mut self, s: &str) {
400        // LuaTeX prints C strings with `tprint`, which lets log lines run
401        // one character past `max_print_line` (as for Lua output).
402        let slack = self.log_slack;
403        self.log_slack = 1;
404        for c in s.chars() {
405            self.print_char(c as u32);
406        }
407        self.log_slack = slack;
408    }
409
410    /// Prints a string returned by a Lua callback (`glyph_info`): the log
411    /// gets the bytes as they are, the terminal shows control characters
412    /// in `^^` notation.
413    pub fn print_callback_bytes(&mut self, s: &[u8]) {
414        let (term, log) = match self.selector {
415            Selector::TermAndLog => (true, true),
416            Selector::TermOnly => (true, false),
417            Selector::LogOnly => (false, true),
418            _ => {
419                self.print_bytes(s);
420                return;
421            }
422        };
423        // Observed: like other text from Lua, its log lines may run one
424        // character past `max_print_line` (overfull box messages with
425        // luaotfload's `glyph_info`).
426        let slack = std::mem::replace(&mut self.log_slack, 1);
427        let mut i = 0;
428        while i < s.len() {
429            let (c, n) = utf8x::decode(s, i);
430            let unit = &s[i..i + n];
431            if log {
432                self.put_unit(unit, false, true);
433            }
434            if term {
435                if c < 32 {
436                    for b in [b'^', b'^', (c + 64) as u8] {
437                        self.put_unit(&[b], true, false);
438                    }
439                } else {
440                    self.put_unit(unit, true, false);
441                }
442            }
443            self.tally += 1;
444            i += n;
445        }
446        self.log_slack = slack;
447    }
448
449    /// Prints digits and other characters TeX prints one by one with
450    /// `print_char` (no log slack).
451    pub fn print_plain(&mut self, s: &str) {
452        for c in s.chars() {
453            self.print_char(c as u32);
454        }
455    }
456
457    /// Prints a byte string in extended UTF-8 (e.g. a control-sequence
458    /// name or file name).
459    pub fn print_bytes(&mut self, s: &[u8]) {
460        let mut i = 0;
461        while i < s.len() {
462            let (c, n) = utf8x::decode(s, i);
463            self.print_char(c);
464            i += n;
465        }
466    }
467
468    /// Prints a byte string as text coming from Lua, with LuaTeX's log line
469    /// length for it (see [`log_slack`](Self::log_slack)).
470    pub fn print_lua_bytes(&mut self, s: &[u8]) {
471        self.log_slack = 1;
472        self.print_bytes(s);
473        self.log_slack = 0;
474    }
475
476    /// A LuaTeX warning, `warning  (category): message`, on a line of its
477    /// own.
478    pub fn luatex_warning(&mut self, category: &str, msg: &str) {
479        self.print_ln();
480        self.print_lua_bytes(format!("warning  ({category}): {msg}").as_bytes());
481        self.print_ln();
482    }
483
484    /// Starts a new line unless at the beginning of one (TeX's `print_nl`).
485    pub fn print_nl(&mut self, s: &str) {
486        self.begin_line();
487        self.print_str(s);
488    }
489
490    /// The test-and-newline part of `print_nl`: ends the current terminal or
491    /// log line if it is not empty.
492    pub fn begin_line(&mut self) {
493        let term = matches!(self.selector, Selector::TermOnly | Selector::TermAndLog);
494        let log = matches!(self.selector, Selector::LogOnly | Selector::TermAndLog);
495        if (term && self.term_offset > 0) || (log && self.file_offset > 0) {
496            self.print_ln();
497        }
498    }
499
500    /// Prints a decimal integer (TeX's `print_int`).
501    pub fn print_int(&mut self, n: i64) {
502        self.print_plain(&n.to_string());
503    }
504
505    /// Prints a scaled value (TeX's `print_scaled`).
506    pub fn print_scaled(&mut self, s: i32) {
507        let mut out = String::new();
508        retex_arith::print_scaled(&mut out, s);
509        // LuaTeX prints the integer part with `print_int` and the
510        // fraction as a C string (which may run into the log slack).
511        match out.find('.') {
512            Some(i) => {
513                self.print_plain(&out[..i]);
514                self.print_str(&out[i..]);
515            }
516            None => self.print_plain(&out),
517        }
518    }
519
520    /// Prints `"` followed by the uppercase hexadecimal value (TeX's
521    /// `print_hex`).
522    pub fn print_hex(&mut self, n: i64) {
523        self.print_char('"' as u32);
524        self.print_plain(&format!("{n:X}"));
525    }
526
527    /// Prints a positive integer in lowercase roman numerals (TeX's
528    /// `print_roman_int`).
529    pub fn print_roman_int(&mut self, n: i64) {
530        self.print_plain(&roman(n));
531    }
532
533    /// Takes the string built by `NewString` printing.
534    pub fn take_string(&mut self) -> Vec<u8> {
535        std::mem::take(&mut self.cur_string)
536    }
537
538    /// Flushes all outputs.
539    pub fn flush(&mut self) {
540        let _ = self.term.flush();
541        if let Some(log) = self.log.as_mut() {
542            let _ = log.flush();
543        }
544        for w in self.write_files.iter_mut().flatten() {
545            let _ = w.flush();
546        }
547    }
548}
549
550/// Lowercase roman numerals as TeX's `print_roman_int` produces them;
551/// nothing for values ≤ 0.
552pub fn roman(mut n: i64) -> String {
553    // TeX's algorithm, driven by the string "m2d5c2l5x2v5i".
554    let table = b"m2d5c2l5x2v5i";
555    let mut out = String::new();
556    if n <= 0 {
557        return out;
558    }
559    let mut j = 0usize;
560    let mut v: i64 = 1000;
561    loop {
562        while n >= v {
563            out.push(table[j] as char);
564            n -= v;
565        }
566        if n <= 0 {
567            return out;
568        }
569        let mut k = j + 2;
570        let mut u = v / i64::from(table[k - 1] - b'0');
571        if table[k - 1] == b'2' {
572            k += 2;
573            u /= i64::from(table[k - 1] - b'0');
574        }
575        if n + u >= v {
576            out.push(table[k] as char);
577            n += u;
578        } else {
579            j += 2;
580            v /= i64::from(table[j - 1] - b'0');
581        }
582    }
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588    use std::cell::RefCell;
589    use std::rc::Rc;
590
591    /// A `Write` that appends to a shared buffer.
592    #[derive(Clone, Default)]
593    struct Shared(Rc<RefCell<Vec<u8>>>);
594    impl Write for Shared {
595        fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
596            self.0.borrow_mut().extend_from_slice(b);
597            Ok(b.len())
598        }
599        fn flush(&mut self) -> std::io::Result<()> {
600            Ok(())
601        }
602    }
603
604    #[test]
605    fn romans() {
606        assert_eq!(roman(1984), "mcmlxxxiv");
607        assert_eq!(roman(4), "iv");
608        assert_eq!(roman(9), "ix");
609        assert_eq!(roman(0), "");
610        assert_eq!(roman(3999), "mmmcmxcix");
611    }
612
613    #[test]
614    fn control_chars_and_utf8() {
615        let buf = Shared::default();
616        let mut p = Printer::new(Box::new(buf.clone()));
617        for c in [1, 9, 0xE9, 127] {
618            p.print_char(c);
619        }
620        assert_eq!(&*buf.0.borrow(), "^^A\té\u{7f}".as_bytes());
621    }
622
623    #[test]
624    fn wraps_without_splitting_utf8() {
625        let buf = Shared::default();
626        let mut p = Printer::new(Box::new(buf.clone()));
627        for _ in 0..40 {
628            p.print_char(0xE9);
629        }
630        let s = String::from_utf8(buf.0.borrow().clone()).unwrap();
631        let lines: Vec<&str> = s.split('\n').collect();
632        assert_eq!(lines[0].chars().count(), 39);
633        assert_eq!(lines[1].chars().count(), 1);
634    }
635}