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        if c as i32 == self.new_line_char && self.selector.is_output() {
300            self.print_ln();
301            return;
302        }
303        match self.selector {
304            Selector::TermAndLog | Selector::TermOnly | Selector::LogOnly => {
305                if let Some(cap) = self.capture.as_mut() {
306                    utf8x::push(cap, c);
307                }
308                let (term, log) = match self.selector {
309                    Selector::TermAndLog => (true, true),
310                    Selector::TermOnly => (true, false),
311                    _ => (false, true),
312                };
313                if c < 32 && c != 9 && c != 10 && c != 13 {
314                    for b in [b'^', b'^', (c + 64) as u8] {
315                        self.put_unit(&[b], term, log);
316                    }
317                } else if c < 128 {
318                    self.put_unit(&[c as u8], term, log);
319                } else {
320                    let e = utf8x::encode(c);
321                    self.put_unit(&e, term, log);
322                }
323            }
324            Selector::NoPrint => {}
325            Selector::Pseudo => {
326                // A control character stays a single byte: it is shown in
327                // `^^` notation when the buffer is printed, but counts as
328                // one column (observed).
329                let e = utf8x::encode(c);
330                for b in e {
331                    self.pseudo_byte(b);
332                }
333                return;
334            }
335            Selector::NewString => utf8x::push(&mut self.cur_string, c),
336            Selector::File(n) => {
337                let e = utf8x::encode(c);
338                if let Some(Some(w)) = self.write_files.get_mut(n as usize) {
339                    let _ = w.write_all(&e);
340                }
341            }
342        }
343        self.tally += 1;
344    }
345
346    /// Prints byte `b` as it is (a byte of invalid UTF-8 input).
347    pub fn print_raw_byte(&mut self, b: u8) {
348        let (term, log) = match self.selector {
349            Selector::TermAndLog => (true, true),
350            Selector::TermOnly => (true, false),
351            Selector::LogOnly => (false, true),
352            _ => (false, false),
353        };
354        if term || log {
355            if term {
356                self.put_unit(&[b], true, false);
357            }
358            if log {
359                if b >= 0xC0 {
360                    // Observed: in the log, a byte that starts a sequence
361                    // takes no column.
362                    self.wlog(&[b]);
363                } else {
364                    self.put_unit(&[b], false, true);
365                }
366            }
367            return;
368        }
369        match self.selector {
370            Selector::Pseudo => self.pseudo_unit(&[b]),
371            Selector::NewString => self.cur_string.push(b),
372            _ => {}
373        }
374    }
375
376    /// Pseudo-prints `bytes` (possibly none) as they are, counted as one
377    /// position: how LuaTeX shows an invalid byte of an input line.
378    pub fn pseudo_unit(&mut self, bytes: &[u8]) {
379        if self.tally < self.trick_count {
380            let i = self.tally % self.error_line;
381            if self.trick_buf.len() <= i {
382                self.trick_buf.resize(i + 1, vec![b' ']);
383            }
384            self.trick_buf[i] = bytes.to_vec();
385        }
386        self.tally += 1;
387    }
388
389    fn pseudo_byte(&mut self, b: u8) {
390        self.pseudo_unit(&[b]);
391    }
392
393    /// Prints an ASCII string (e.g. a fixed message).
394    pub fn print_str(&mut self, s: &str) {
395        // LuaTeX prints C strings with `tprint`, which lets log lines run
396        // one character past `max_print_line` (as for Lua output).
397        let slack = self.log_slack;
398        self.log_slack = 1;
399        for c in s.chars() {
400            self.print_char(c as u32);
401        }
402        self.log_slack = slack;
403    }
404
405    /// Prints a string returned by a Lua callback (`glyph_info`): the log
406    /// gets the bytes as they are, the terminal shows control characters
407    /// in `^^` notation.
408    pub fn print_callback_bytes(&mut self, s: &[u8]) {
409        let (term, log) = match self.selector {
410            Selector::TermAndLog => (true, true),
411            Selector::TermOnly => (true, false),
412            Selector::LogOnly => (false, true),
413            _ => {
414                self.print_bytes(s);
415                return;
416            }
417        };
418        let mut i = 0;
419        while i < s.len() {
420            let (c, n) = utf8x::decode(s, i);
421            let unit = &s[i..i + n];
422            if log {
423                self.put_unit(unit, false, true);
424            }
425            if term {
426                if c < 32 {
427                    for b in [b'^', b'^', (c + 64) as u8] {
428                        self.put_unit(&[b], true, false);
429                    }
430                } else {
431                    self.put_unit(unit, true, false);
432                }
433            }
434            self.tally += 1;
435            i += n;
436        }
437    }
438
439    /// Prints digits and other characters TeX prints one by one with
440    /// `print_char` (no log slack).
441    pub fn print_plain(&mut self, s: &str) {
442        for c in s.chars() {
443            self.print_char(c as u32);
444        }
445    }
446
447    /// Prints a byte string in extended UTF-8 (e.g. a control-sequence
448    /// name or file name).
449    pub fn print_bytes(&mut self, s: &[u8]) {
450        let mut i = 0;
451        while i < s.len() {
452            let (c, n) = utf8x::decode(s, i);
453            self.print_char(c);
454            i += n;
455        }
456    }
457
458    /// Prints a byte string as text coming from Lua, with LuaTeX's log line
459    /// length for it (see [`log_slack`](Self::log_slack)).
460    pub fn print_lua_bytes(&mut self, s: &[u8]) {
461        self.log_slack = 1;
462        self.print_bytes(s);
463        self.log_slack = 0;
464    }
465
466    /// A LuaTeX warning, `warning  (category): message`, on a line of its
467    /// own.
468    pub fn luatex_warning(&mut self, category: &str, msg: &str) {
469        self.print_ln();
470        self.print_lua_bytes(format!("warning  ({category}): {msg}").as_bytes());
471        self.print_ln();
472    }
473
474    /// Starts a new line unless at the beginning of one (TeX's `print_nl`).
475    pub fn print_nl(&mut self, s: &str) {
476        self.begin_line();
477        self.print_str(s);
478    }
479
480    /// The test-and-newline part of `print_nl`: ends the current terminal or
481    /// log line if it is not empty.
482    pub fn begin_line(&mut self) {
483        let term = matches!(self.selector, Selector::TermOnly | Selector::TermAndLog);
484        let log = matches!(self.selector, Selector::LogOnly | Selector::TermAndLog);
485        if (term && self.term_offset > 0) || (log && self.file_offset > 0) {
486            self.print_ln();
487        }
488    }
489
490    /// Prints a decimal integer (TeX's `print_int`).
491    pub fn print_int(&mut self, n: i64) {
492        self.print_plain(&n.to_string());
493    }
494
495    /// Prints a scaled value (TeX's `print_scaled`).
496    pub fn print_scaled(&mut self, s: i32) {
497        let mut out = String::new();
498        retex_arith::print_scaled(&mut out, s);
499        // LuaTeX prints the integer part with `print_int` and the
500        // fraction as a C string (which may run into the log slack).
501        match out.find('.') {
502            Some(i) => {
503                self.print_plain(&out[..i]);
504                self.print_str(&out[i..]);
505            }
506            None => self.print_plain(&out),
507        }
508    }
509
510    /// Prints `"` followed by the uppercase hexadecimal value (TeX's
511    /// `print_hex`).
512    pub fn print_hex(&mut self, n: i64) {
513        self.print_char('"' as u32);
514        self.print_plain(&format!("{n:X}"));
515    }
516
517    /// Prints a positive integer in lowercase roman numerals (TeX's
518    /// `print_roman_int`).
519    pub fn print_roman_int(&mut self, n: i64) {
520        self.print_plain(&roman(n));
521    }
522
523    /// Takes the string built by `NewString` printing.
524    pub fn take_string(&mut self) -> Vec<u8> {
525        std::mem::take(&mut self.cur_string)
526    }
527
528    /// Flushes all outputs.
529    pub fn flush(&mut self) {
530        let _ = self.term.flush();
531        if let Some(log) = self.log.as_mut() {
532            let _ = log.flush();
533        }
534        for w in self.write_files.iter_mut().flatten() {
535            let _ = w.flush();
536        }
537    }
538}
539
540/// Lowercase roman numerals as TeX's `print_roman_int` produces them;
541/// nothing for values ≤ 0.
542pub fn roman(mut n: i64) -> String {
543    // TeX's algorithm, driven by the string "m2d5c2l5x2v5i".
544    let table = b"m2d5c2l5x2v5i";
545    let mut out = String::new();
546    if n <= 0 {
547        return out;
548    }
549    let mut j = 0usize;
550    let mut v: i64 = 1000;
551    loop {
552        while n >= v {
553            out.push(table[j] as char);
554            n -= v;
555        }
556        if n <= 0 {
557            return out;
558        }
559        let mut k = j + 2;
560        let mut u = v / i64::from(table[k - 1] - b'0');
561        if table[k - 1] == b'2' {
562            k += 2;
563            u /= i64::from(table[k - 1] - b'0');
564        }
565        if n + u >= v {
566            out.push(table[k] as char);
567            n += u;
568        } else {
569            j += 2;
570            v /= i64::from(table[j - 1] - b'0');
571        }
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578    use std::cell::RefCell;
579    use std::rc::Rc;
580
581    /// A `Write` that appends to a shared buffer.
582    #[derive(Clone, Default)]
583    struct Shared(Rc<RefCell<Vec<u8>>>);
584    impl Write for Shared {
585        fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
586            self.0.borrow_mut().extend_from_slice(b);
587            Ok(b.len())
588        }
589        fn flush(&mut self) -> std::io::Result<()> {
590            Ok(())
591        }
592    }
593
594    #[test]
595    fn romans() {
596        assert_eq!(roman(1984), "mcmlxxxiv");
597        assert_eq!(roman(4), "iv");
598        assert_eq!(roman(9), "ix");
599        assert_eq!(roman(0), "");
600        assert_eq!(roman(3999), "mmmcmxcix");
601    }
602
603    #[test]
604    fn control_chars_and_utf8() {
605        let buf = Shared::default();
606        let mut p = Printer::new(Box::new(buf.clone()));
607        for c in [1, 9, 0xE9, 127] {
608            p.print_char(c);
609        }
610        assert_eq!(&*buf.0.borrow(), "^^A\té\u{7f}".as_bytes());
611    }
612
613    #[test]
614    fn wraps_without_splitting_utf8() {
615        let buf = Shared::default();
616        let mut p = Printer::new(Box::new(buf.clone()));
617        for _ in 0..40 {
618            p.print_char(0xE9);
619        }
620        let s = String::from_utf8(buf.0.borrow().clone()).unwrap();
621        let lines: Vec<&str> = s.split('\n').collect();
622        assert_eq!(lines[0].chars().count(), 39);
623        assert_eq!(lines[1].chars().count(), 1);
624    }
625}