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 `Some`, the text printed to the terminal or log is also
74    /// collected here (used to turn error messages into diagnostics).
75    pub capture: Option<Vec<u8>>,
76    /// Width of error context lines.
77    pub error_line: usize,
78    /// Width of the first half of an error context line.
79    pub half_error_line: usize,
80    /// Characters printed so far in the current pseudo-print or other
81    /// counted operation (TeX's `tally`).
82    pub tally: usize,
83    /// Buffer for pseudo printing (bytes).
84    pub trick_buf: Vec<u8>,
85    /// Limit of pseudo-printed characters to keep.
86    pub trick_count: usize,
87    /// The tally at the start of the second line of pseudo printing.
88    pub first_count: usize,
89    /// The string under construction (`NewString`).
90    pub cur_string: Vec<u8>,
91    /// The current `\newlinechar` (−1 for none).
92    pub new_line_char: i32,
93    /// The terminal.
94    term: Box<dyn Write>,
95    /// The log file, once opened.
96    log: Option<Box<dyn Write>>,
97    /// `\write` streams that are open, by stream number.
98    write_files: Vec<Option<Box<dyn Write>>>,
99}
100
101impl std::fmt::Debug for Printer {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        f.debug_struct("Printer")
104            .field("selector", &self.selector)
105            .field("term_offset", &self.term_offset)
106            .field("file_offset", &self.file_offset)
107            .finish_non_exhaustive()
108    }
109}
110
111impl Printer {
112    /// A printer writing to `term`.
113    pub fn new(term: Box<dyn Write>) -> Self {
114        Printer {
115            selector: Selector::TermOnly,
116            term_offset: 0,
117            term_just_wrapped: false,
118            file_offset: 0,
119            max_print_line: 79,
120            log_slack: 0,
121            term_quiet: false,
122            capture: None,
123            error_line: 79,
124            half_error_line: 50,
125            tally: 0,
126            trick_buf: Vec::new(),
127            trick_count: 0,
128            first_count: 0,
129            cur_string: Vec::new(),
130            new_line_char: -1,
131            term,
132            log: None,
133            write_files: Vec::new(),
134        }
135    }
136
137    /// Installs the log file.
138    pub fn set_log(&mut self, log: Box<dyn Write>) {
139        self.log = Some(log);
140    }
141
142    /// Whether a log file is open.
143    pub fn log_opened(&self) -> bool {
144        self.log.is_some()
145    }
146
147    /// Flushes and closes the log file.
148    pub fn close_log(&mut self) {
149        if let Some(mut log) = self.log.take() {
150            let _ = log.flush();
151        }
152    }
153
154    /// Writes raw bytes to the log without bookkeeping (for banners).
155    pub fn log_raw(&mut self, bytes: &[u8]) {
156        if let Some(log) = self.log.as_mut() {
157            let _ = log.write_all(bytes);
158        }
159    }
160
161    /// Writes raw bytes to the terminal without bookkeeping.
162    pub fn term_raw(&mut self, bytes: &[u8]) {
163        let _ = self.term.write_all(bytes);
164    }
165
166    /// Flushes the terminal (TeX's `update_terminal`).
167    pub fn update_terminal(&mut self) {
168        let _ = self.term.flush();
169    }
170
171    /// Opens `\write` stream `n` on `w`.
172    pub fn open_write(&mut self, n: u32, w: Box<dyn Write>) {
173        let n = n as usize;
174        if self.write_files.len() <= n {
175            self.write_files.resize_with(n + 1, || None);
176        }
177        self.write_files[n] = Some(w);
178    }
179
180    /// The `\write` streams that are open.
181    pub fn open_write_streams(&self) -> Vec<u32> {
182        (0..self.write_files.len() as u32)
183            .filter(|&n| self.write_files[n as usize].is_some())
184            .collect()
185    }
186
187    /// Closes `\write` stream `n`.
188    pub fn close_write(&mut self, n: u32) {
189        if let Some(slot) = self.write_files.get_mut(n as usize)
190            && let Some(mut w) = slot.take()
191        {
192            let _ = w.flush();
193        }
194    }
195
196    /// Whether `\write` stream `n` is open.
197    pub fn write_open(&self, n: u32) -> bool {
198        matches!(self.write_files.get(n as usize), Some(Some(_)))
199    }
200
201    fn wterm(&mut self, b: &[u8]) {
202        if !self.term_quiet {
203            let _ = self.term.write_all(b);
204        }
205    }
206
207    fn wlog(&mut self, b: &[u8]) {
208        if let Some(log) = self.log.as_mut() {
209            let _ = log.write_all(b);
210        }
211    }
212
213    /// Ends the current line (TeX's `print_ln`).
214    pub fn print_ln(&mut self) {
215        if let Some(c) = self.capture.as_mut()
216            && matches!(
217                self.selector,
218                Selector::TermAndLog | Selector::TermOnly | Selector::LogOnly
219            )
220        {
221            c.push(b'\n');
222        }
223        match self.selector {
224            Selector::TermAndLog => {
225                self.wterm(b"\n");
226                self.wlog(b"\n");
227                self.term_offset = 0;
228                self.file_offset = 0;
229            }
230            Selector::LogOnly => {
231                self.wlog(b"\n");
232                self.file_offset = 0;
233            }
234            Selector::TermOnly => {
235                self.wterm(b"\n");
236                self.term_offset = 0;
237            }
238            Selector::NoPrint | Selector::Pseudo | Selector::NewString => {}
239            Selector::File(n) => {
240                if let Some(Some(w)) = self.write_files.get_mut(n as usize) {
241                    let _ = w.write_all(b"\n");
242                }
243            }
244        }
245    }
246
247    /// Outputs one printable unit (one ASCII character or the bytes of one
248    /// multi-byte character) to terminal and/or log, with line breaking.
249    fn put_unit(&mut self, bytes: &[u8], term: bool, log: bool) {
250        let n = bytes.len();
251        if term {
252            if n > 1 && self.term_offset + n > self.max_print_line {
253                self.wterm(b"\n");
254                self.term_offset = 0;
255            }
256            self.wterm(bytes);
257            self.term_offset += n;
258            self.term_just_wrapped = false;
259            if self.term_offset >= self.max_print_line {
260                self.wterm(b"\n");
261                self.term_offset = 0;
262                self.term_just_wrapped = true;
263            }
264        }
265        if log {
266            let max = self.max_print_line + self.log_slack;
267            if n > 1 && self.file_offset + n > max {
268                self.wlog(b"\n");
269                self.file_offset = 0;
270            }
271            self.wlog(bytes);
272            let before = self.file_offset;
273            self.file_offset += n;
274            // TeX tests for equality: a line that already ran past the
275            // limit (see `log_slack`) is not broken by later characters.
276            if self.file_offset >= max && before < max {
277                self.wlog(b"\n");
278                self.file_offset = 0;
279            }
280        }
281    }
282
283    /// Prints character `c`, handling the new-line character and the
284    /// printable representation for terminal and log.
285    pub fn print_char(&mut self, c: u32) {
286        if c as i32 == self.new_line_char && self.selector.is_output() {
287            self.print_ln();
288            return;
289        }
290        match self.selector {
291            Selector::TermAndLog | Selector::TermOnly | Selector::LogOnly => {
292                if let Some(cap) = self.capture.as_mut() {
293                    utf8x::push(cap, c);
294                }
295                let (term, log) = match self.selector {
296                    Selector::TermAndLog => (true, true),
297                    Selector::TermOnly => (true, false),
298                    _ => (false, true),
299                };
300                if c < 32 && c != 9 && c != 10 && c != 13 {
301                    for b in [b'^', b'^', (c + 64) as u8] {
302                        self.put_unit(&[b], term, log);
303                    }
304                } else if c < 128 {
305                    self.put_unit(&[c as u8], term, log);
306                } else {
307                    let e = utf8x::encode(c);
308                    self.put_unit(&e, term, log);
309                }
310            }
311            Selector::NoPrint => {}
312            Selector::Pseudo => {
313                if c < 32 && c != 9 && c != 10 && c != 13 {
314                    for b in [b'^', b'^', (c + 64) as u8] {
315                        self.pseudo_byte(b);
316                    }
317                    return;
318                }
319                let e = utf8x::encode(c);
320                for b in e {
321                    self.pseudo_byte(b);
322                }
323                return;
324            }
325            Selector::NewString => utf8x::push(&mut self.cur_string, c),
326            Selector::File(n) => {
327                let e = utf8x::encode(c);
328                if let Some(Some(w)) = self.write_files.get_mut(n as usize) {
329                    let _ = w.write_all(&e);
330                }
331            }
332        }
333        self.tally += 1;
334    }
335
336    fn pseudo_byte(&mut self, b: u8) {
337        if self.tally < self.trick_count {
338            let i = self.tally % self.error_line;
339            if self.trick_buf.len() <= i {
340                self.trick_buf.resize(i + 1, b' ');
341            }
342            self.trick_buf[i] = b;
343        }
344        self.tally += 1;
345    }
346
347    /// Prints an ASCII string (e.g. a fixed message).
348    pub fn print_str(&mut self, s: &str) {
349        // LuaTeX prints C strings with `tprint`, which lets log lines run
350        // one character past `max_print_line` (as for Lua output).
351        let slack = self.log_slack;
352        self.log_slack = 1;
353        for c in s.chars() {
354            self.print_char(c as u32);
355        }
356        self.log_slack = slack;
357    }
358
359    /// Prints a string returned by a Lua callback (`glyph_info`): the log
360    /// gets the bytes as they are, the terminal shows control characters
361    /// in `^^` notation.
362    pub fn print_callback_bytes(&mut self, s: &[u8]) {
363        let (term, log) = match self.selector {
364            Selector::TermAndLog => (true, true),
365            Selector::TermOnly => (true, false),
366            Selector::LogOnly => (false, true),
367            _ => {
368                self.print_bytes(s);
369                return;
370            }
371        };
372        let mut i = 0;
373        while i < s.len() {
374            let (c, n) = utf8x::decode(s, i);
375            let unit = &s[i..i + n];
376            if log {
377                self.put_unit(unit, false, true);
378            }
379            if term {
380                if c < 32 {
381                    for b in [b'^', b'^', (c + 64) as u8] {
382                        self.put_unit(&[b], true, false);
383                    }
384                } else {
385                    self.put_unit(unit, true, false);
386                }
387            }
388            self.tally += 1;
389            i += n;
390        }
391    }
392
393    /// Prints digits and other characters TeX prints one by one with
394    /// `print_char` (no log slack).
395    pub fn print_plain(&mut self, s: &str) {
396        for c in s.chars() {
397            self.print_char(c as u32);
398        }
399    }
400
401    /// Prints a byte string in extended UTF-8 (e.g. a control-sequence
402    /// name or file name).
403    pub fn print_bytes(&mut self, s: &[u8]) {
404        let mut i = 0;
405        while i < s.len() {
406            let (c, n) = utf8x::decode(s, i);
407            self.print_char(c);
408            i += n;
409        }
410    }
411
412    /// Prints a byte string as text coming from Lua, with LuaTeX's log line
413    /// length for it (see [`log_slack`](Self::log_slack)).
414    pub fn print_lua_bytes(&mut self, s: &[u8]) {
415        self.log_slack = 1;
416        self.print_bytes(s);
417        self.log_slack = 0;
418    }
419
420    /// A LuaTeX warning, `warning  (category): message`, on a line of its
421    /// own.
422    pub fn luatex_warning(&mut self, category: &str, msg: &str) {
423        self.print_ln();
424        self.print_lua_bytes(format!("warning  ({category}): {msg}").as_bytes());
425        self.print_ln();
426    }
427
428    /// Starts a new line unless at the beginning of one (TeX's `print_nl`).
429    pub fn print_nl(&mut self, s: &str) {
430        self.begin_line();
431        self.print_str(s);
432    }
433
434    /// The test-and-newline part of `print_nl`: ends the current terminal or
435    /// log line if it is not empty.
436    pub fn begin_line(&mut self) {
437        let term = matches!(self.selector, Selector::TermOnly | Selector::TermAndLog);
438        let log = matches!(self.selector, Selector::LogOnly | Selector::TermAndLog);
439        if (term && self.term_offset > 0) || (log && self.file_offset > 0) {
440            self.print_ln();
441        }
442    }
443
444    /// Prints a decimal integer (TeX's `print_int`).
445    pub fn print_int(&mut self, n: i64) {
446        self.print_plain(&n.to_string());
447    }
448
449    /// Prints a scaled value (TeX's `print_scaled`).
450    pub fn print_scaled(&mut self, s: i32) {
451        let mut out = String::new();
452        retex_arith::print_scaled(&mut out, s);
453        // LuaTeX prints the integer part with `print_int` and the
454        // fraction as a C string (which may run into the log slack).
455        match out.find('.') {
456            Some(i) => {
457                self.print_plain(&out[..i]);
458                self.print_str(&out[i..]);
459            }
460            None => self.print_plain(&out),
461        }
462    }
463
464    /// Prints `"` followed by the uppercase hexadecimal value (TeX's
465    /// `print_hex`).
466    pub fn print_hex(&mut self, n: i64) {
467        self.print_char('"' as u32);
468        self.print_plain(&format!("{n:X}"));
469    }
470
471    /// Prints a positive integer in lowercase roman numerals (TeX's
472    /// `print_roman_int`).
473    pub fn print_roman_int(&mut self, n: i64) {
474        self.print_plain(&roman(n));
475    }
476
477    /// Takes the string built by `NewString` printing.
478    pub fn take_string(&mut self) -> Vec<u8> {
479        std::mem::take(&mut self.cur_string)
480    }
481
482    /// Flushes all outputs.
483    pub fn flush(&mut self) {
484        let _ = self.term.flush();
485        if let Some(log) = self.log.as_mut() {
486            let _ = log.flush();
487        }
488        for w in self.write_files.iter_mut().flatten() {
489            let _ = w.flush();
490        }
491    }
492}
493
494/// Lowercase roman numerals as TeX's `print_roman_int` produces them;
495/// nothing for values ≤ 0.
496pub fn roman(mut n: i64) -> String {
497    // TeX's algorithm, driven by the string "m2d5c2l5x2v5i".
498    let table = b"m2d5c2l5x2v5i";
499    let mut out = String::new();
500    if n <= 0 {
501        return out;
502    }
503    let mut j = 0usize;
504    let mut v: i64 = 1000;
505    loop {
506        while n >= v {
507            out.push(table[j] as char);
508            n -= v;
509        }
510        if n <= 0 {
511            return out;
512        }
513        let mut k = j + 2;
514        let mut u = v / i64::from(table[k - 1] - b'0');
515        if table[k - 1] == b'2' {
516            k += 2;
517            u /= i64::from(table[k - 1] - b'0');
518        }
519        if n + u >= v {
520            out.push(table[k] as char);
521            n += u;
522        } else {
523            j += 2;
524            v /= i64::from(table[j - 1] - b'0');
525        }
526    }
527}
528
529#[cfg(test)]
530mod tests {
531    use super::*;
532    use std::cell::RefCell;
533    use std::rc::Rc;
534
535    /// A `Write` that appends to a shared buffer.
536    #[derive(Clone, Default)]
537    struct Shared(Rc<RefCell<Vec<u8>>>);
538    impl Write for Shared {
539        fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
540            self.0.borrow_mut().extend_from_slice(b);
541            Ok(b.len())
542        }
543        fn flush(&mut self) -> std::io::Result<()> {
544            Ok(())
545        }
546    }
547
548    #[test]
549    fn romans() {
550        assert_eq!(roman(1984), "mcmlxxxiv");
551        assert_eq!(roman(4), "iv");
552        assert_eq!(roman(9), "ix");
553        assert_eq!(roman(0), "");
554        assert_eq!(roman(3999), "mmmcmxcix");
555    }
556
557    #[test]
558    fn control_chars_and_utf8() {
559        let buf = Shared::default();
560        let mut p = Printer::new(Box::new(buf.clone()));
561        for c in [1, 9, 0xE9, 127] {
562            p.print_char(c);
563        }
564        assert_eq!(&*buf.0.borrow(), "^^A\té\u{7f}".as_bytes());
565    }
566
567    #[test]
568    fn wraps_without_splitting_utf8() {
569        let buf = Shared::default();
570        let mut p = Printer::new(Box::new(buf.clone()));
571        for _ in 0..40 {
572            p.print_char(0xE9);
573        }
574        let s = String::from_utf8(buf.0.borrow().clone()).unwrap();
575        let lines: Vec<&str> = s.split('\n').collect();
576        assert_eq!(lines[0].chars().count(), 39);
577        assert_eq!(lines[1].chars().count(), 1);
578    }
579}