dvi/
display.rs

1//! Human readable formatting of DVI operations.
2//!
3//! The format is designed to be easy to read and, sometime in the future,
4//! e parseable back into the same list of [`Op`] values.
5//! Each operation is printed like a function call whose name is the
6//! snake case version of the operation, and whose arguments are the
7//! payload of the operation:
8//!
9//! ```
10//! # use dvi::{Op, Var};
11//! assert_eq!(dvi::display::format_ops([Op::SetVar(Var::W, 300)]), "set_var(w, 300)\n");
12//! ```
13//!
14//! Operations with no payload are printed without parentheses:
15//!
16//! ```
17//! # use dvi::Op;
18//! assert_eq!(dvi::display::format_ops([Op::Push, Op::Pop]), "push\npop\n");
19//! ```
20//!
21//! Operations with a large payload place each argument on its own line:
22//!
23//! ```
24//! # use dvi::Op;
25//! assert_eq!(
26//!     dvi::display::format_ops([Op::Preamble{
27//!         dvi_format: 2,
28//!         unit_numerator: 25400000,
29//!         unit_denominator: 473628672,
30//!         magnification: 1000,
31//!         comment: "Texcraft".into(),
32//!     }]),
33//!     r#"preamble(
34//!   dvi_format: 2,
35//!   unit_numerator: 25400000,
36//!   unit_denominator: 473628672,
37//!   magnification: 1000,
38//!   comment: "Texcraft",
39//! )
40//! "#,
41//! );
42//! ```
43//!
44//! Finally, [`Op::Push`] and [`Op::Pop`] operations introduce indentation,
45//! and some operations are annotated with a comment describing runtime
46//! state that is not visible in the operation itself:
47//!
48//! ```
49//! # use dvi::{Op, Var};
50//! assert_eq!(
51//!     dvi::display::format_ops([
52//!         Op::SetVar(Var::W, 300),
53//!         Op::Push,
54//!         Op::Move(Var::W),
55//!         Op::Pop,
56//!     ]),
57//!     "set_var(w, 300)\npush\n  move(w)  // 300\npop\n",
58//! );
59//! ```
60//!
61//! Indentation can be disabled, and byte offsets can be printed, using
62//! the [`Options`] type.
63
64use crate::{Op, Values, Var};
65use std::fmt::Write;
66
67/// String used for one level of indentation.
68const INDENT: &str = "  ";
69
70/// Width of the byte offset column.
71const OFFSET_WIDTH: usize = 8;
72
73/// Options for the [`Formatter`].
74///
75/// The default options print no byte offsets and apply indentation:
76///
77/// ```
78/// let options: dvi::display::Options = Default::default();
79/// assert_eq!(options.byte_offsets, false);
80/// assert_eq!(options.indentation, true);
81/// ```
82#[derive(Clone, Debug)]
83pub struct Options {
84    /// If true, every line is prefixed with a column containing the offset
85    /// in the DVI data of the operation on that line.
86    ///
87    /// Only the first line of a multi-line operation displays the offset;
88    /// subsequent lines are padded so that the operations stay aligned.
89    pub byte_offsets: bool,
90    /// If true, [`Op::Push`] and [`Op::Pop`] operations introduce and remove
91    /// indentation.
92    ///
93    /// Indentation makes the structure of a DVI file much easier to see, but
94    /// it is worth disabling when diffing the output for two DVI files whose
95    /// push and pop operations are paired up differently. In that case
96    /// indentation makes the diff much larger than the true difference
97    /// between the files.
98    pub indentation: bool,
99}
100
101impl Default for Options {
102    fn default() -> Self {
103        Self {
104            byte_offsets: false,
105            indentation: true,
106        }
107    }
108}
109
110/// Formatter for lists of DVI operations.
111///
112/// Most of the format depends only on the operation being printed, and for
113/// that the [`std::fmt::Display`] implementation on [`Op`] suffices.
114/// This type exists because two parts of the format depend on the operations
115/// that came before:
116///
117/// - [`Op::Push`] and [`Op::Pop`] operations introduce and remove indentation.
118///
119/// - Some operations are annotated with a comment giving runtime state.
120///   For example [`Op::Move`] is annotated with the current value of the
121///   variable being moved by, as that value does not appear in the operation.
122///
123/// The formatter tracks this state using the [`Values`] data structure:
124///
125/// ```
126/// let ops = vec![
127///     dvi::Op::SetVar(dvi::Var::Y, 300),
128///     dvi::Op::Move(dvi::Var::Y),
129/// ];
130/// let mut formatter: dvi::display::Formatter = Default::default();
131/// let mut out = String::new();
132/// for op in &ops {
133///     formatter.write_op(&mut out, op, 0).unwrap();
134/// }
135/// assert_eq!(out, "set_var(y, 300)\nmove(y)  // 300\n");
136/// ```
137#[derive(Default)]
138pub struct Formatter {
139    options: Options,
140    values: Values,
141    /// Scratch buffer, reused across operations.
142    buffer: String,
143}
144
145impl Formatter {
146    /// Create a new formatter with the provided options.
147    pub fn new(options: Options) -> Self {
148        Self {
149            options,
150            values: Default::default(),
151            buffer: Default::default(),
152        }
153    }
154    /// Write a single operation, followed by a newline.
155    ///
156    /// Operations must be provided in the order they appear in the DVI data,
157    /// otherwise the indentation and the comments will be wrong.
158    /// The byte offset is the offset of the operation in the DVI data;
159    /// it is only printed if the [`Options::byte_offsets`] option is set.
160    pub fn write_op<W: Write>(
161        &mut self,
162        w: &mut W,
163        op: &Op,
164        byte_offset: usize,
165    ) -> std::fmt::Result {
166        // The indentation is the stack depth, except that a pop is printed
167        // at the depth of the push it matches so that the two line up.
168        let depth = self.values.stack_depth();
169        let (indent, stack_underflow) = match op {
170            Op::Pop => match depth.checked_sub(1) {
171                Some(indent) => (indent, false),
172                // A pop that is not matched by a push is invalid DVI data,
173                // but this is exactly the kind of file this formatter is
174                // used to debug, so print it with a marker instead of
175                // panicking.
176                None => (0, true),
177            },
178            _ => (depth, false),
179        };
180
181        self.buffer.clear();
182        write_op(&mut self.buffer, op, Some(&self.values));
183        if stack_underflow {
184            self.buffer.push_str("  // stack underflow");
185        }
186
187        let indent = if self.options.indentation { indent } else { 0 };
188        for (i, line) in self.buffer.lines().enumerate() {
189            if self.options.byte_offsets {
190                if i == 0 {
191                    write!(w, "{byte_offset:>OFFSET_WIDTH$}  ")?;
192                } else {
193                    write!(w, "{:>OFFSET_WIDTH$}  ", "")?;
194                }
195            }
196            for _ in 0..indent {
197                w.write_str(INDENT)?;
198            }
199            w.write_str(line)?;
200            w.write_char('\n')?;
201        }
202
203        self.values.update(op);
204        Ok(())
205    }
206
207    /// Get the options the formatter was created with.
208    pub fn options(&self) -> &Options {
209        &self.options
210    }
211
212    /// Get the values the formatter has accumulated so far.
213    pub fn values(&self) -> &Values {
214        &self.values
215    }
216}
217
218/// Format a list of operations as a string, using the default [`Options`].
219///
220/// Because the byte offsets of the operations are not known, this function
221/// cannot be used with the [`Options::byte_offsets`] option.
222///
223/// ```
224/// let ops = vec![dvi::Op::Right(300), dvi::Op::Down(-4)];
225/// assert_eq!(dvi::display::format_ops(ops), "right(300)\ndown(-4)\n");
226/// ```
227pub fn format_ops<I: IntoIterator<Item = Op>>(ops: I) -> String {
228    let mut formatter: Formatter = Default::default();
229    let mut out = String::new();
230    for op in ops {
231        // Writing to a string cannot fail.
232        formatter.write_op(&mut out, &op, 0).unwrap();
233    }
234    out
235}
236
237impl std::fmt::Display for Op {
238    /// Display a single operation.
239    ///
240    /// Operations whose format depends on the operations that came before
241    /// are displayed without that context: no indentation is applied, and
242    /// comments describing runtime state are omitted.
243    /// Use a [`Formatter`] to display a list of operations.
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        let mut out = String::new();
246        write_op(&mut out, self, None);
247        f.write_str(&out)
248    }
249}
250
251impl std::fmt::Display for Var {
252    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253        f.write_str(match self {
254            Var::W => "w",
255            Var::X => "x",
256            Var::Y => "y",
257            Var::Z => "z",
258        })
259    }
260}
261
262/// Write the operation to the provided string, without indentation.
263///
264/// If values are provided, comments describing runtime state are included.
265fn write_op(out: &mut String, op: &Op, values: Option<&Values>) {
266    match op {
267        Op::Char { char, move_h } => {
268            // The DVI format has separate set and put commands, and printing
269            // them separately avoids a positional boolean argument.
270            out.push_str(if *move_h { "char(" } else { "put_char(" });
271            write_char(out, *char);
272            out.push(')');
273        }
274        Op::Rule {
275            height,
276            width,
277            move_h,
278        } => {
279            let name = if *move_h { "rule" } else { "put_rule" };
280            write!(out, "{name}(height: {height}, width: {width})").unwrap();
281        }
282        // TODO: consider collapsing runs of no_op operations, which TeX emits
283        // as padding. This would make the output much shorter for some files,
284        // at the cost of the output no longer round-tripping.
285        Op::NoOp => out.push_str("no_op"),
286        Op::BeginPage {
287            parameters,
288            previous_begin_page,
289        } => {
290            let parameters: Vec<String> = parameters.iter().map(i32::to_string).collect();
291            write_multiline(
292                out,
293                "begin_page",
294                &[
295                    ("parameters", format!("[{}]", parameters.join(", "))),
296                    ("previous_begin_page", previous_begin_page.to_string()),
297                ],
298            );
299        }
300        Op::EndPage => out.push_str("end_page"),
301        Op::Push => out.push_str("push"),
302        Op::Pop => out.push_str("pop"),
303        Op::Right(d) => write!(out, "right({d})").unwrap(),
304        Op::Move(var) => {
305            write!(out, "move({var})").unwrap();
306            if let Some(values) = values {
307                // The value moved by does not appear in the operation, so
308                // print it as a comment.
309                write!(out, "  // {}", values.var(*var)).unwrap();
310            }
311        }
312        Op::SetVar(var, i) => write!(out, "set_var({var}, {i})").unwrap(),
313        Op::Down(d) => write!(out, "down({d})").unwrap(),
314        Op::EnableFont(f) => write!(out, "enable_font({f})").unwrap(),
315        Op::Extension(b) => match std::str::from_utf8(b) {
316            Ok(s) => write!(out, "extension({s:?})").unwrap(),
317            Err(_) => {
318                out.push_str("extension(bytes: \"");
319                for byte in b {
320                    write!(out, "{byte:02x}").unwrap();
321                }
322                out.push_str("\")");
323            }
324        },
325        Op::DefineFont {
326            number,
327            checksum,
328            at_size,
329            design_size,
330            area,
331            name,
332        } => write_multiline(
333            out,
334            "define_font",
335            &[
336                ("number", number.to_string()),
337                ("checksum", checksum.to_string()),
338                ("at_size", at_size.to_string()),
339                ("design_size", design_size.to_string()),
340                ("area", format!("{area:?}")),
341                ("name", format!("{name:?}")),
342            ],
343        ),
344        Op::Preamble {
345            dvi_format,
346            unit_numerator,
347            unit_denominator,
348            magnification,
349            comment,
350        } => write_multiline(
351            out,
352            "preamble",
353            &[
354                ("dvi_format", dvi_format.to_string()),
355                ("unit_numerator", unit_numerator.to_string()),
356                ("unit_denominator", unit_denominator.to_string()),
357                ("magnification", magnification.to_string()),
358                ("comment", format!("{comment:?}")),
359            ],
360        ),
361        Op::BeginPostamble {
362            final_begin_page,
363            unit_numerator,
364            unit_denominator,
365            magnification,
366            largest_height,
367            largest_width,
368            max_stack_depth,
369            num_pages,
370        } => write_multiline(
371            out,
372            "begin_postamble",
373            &[
374                ("final_begin_page", final_begin_page.to_string()),
375                ("unit_numerator", unit_numerator.to_string()),
376                ("unit_denominator", unit_denominator.to_string()),
377                ("magnification", magnification.to_string()),
378                ("largest_height", largest_height.to_string()),
379                ("largest_width", largest_width.to_string()),
380                ("max_stack_depth", max_stack_depth.to_string()),
381                ("num_pages", num_pages.to_string()),
382            ],
383        ),
384        Op::EndPostamble {
385            postamble,
386            dvi_format,
387            num_223_bytes,
388        } => write!(
389            out,
390            "end_postamble(postamble: {postamble}, dvi_format: {dvi_format}, num_223_bytes: {num_223_bytes})"
391        )
392        .unwrap(),
393    }
394}
395
396/// Write an operation with each argument on its own line.
397fn write_multiline(out: &mut String, name: &str, args: &[(&str, String)]) {
398    write!(out, "{name}(").unwrap();
399    for (key, value) in args {
400        write!(out, "\n{INDENT}{key}: {value},").unwrap();
401    }
402    out.push_str("\n)");
403}
404
405/// Write the argument of a char or put_char operation.
406///
407/// Printable ASCII characters are written as character literals because
408/// this makes the output of the formatter much easier to read.
409/// All other characters are written as numbers.
410fn write_char(out: &mut String, char: u32) {
411    match char::from_u32(char) {
412        Some(c) if c.is_ascii_graphic() || c == ' ' => match c {
413            '\'' => out.push_str(r"'\''"),
414            '\\' => out.push_str(r"'\\'"),
415            _ => {
416                out.push('\'');
417                out.push(c);
418                out.push('\'');
419            }
420        },
421        _ => write!(out, "{char}").unwrap(),
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428
429    macro_rules! format_tests {
430        ( $( ($name: ident, $ops: expr, $want: expr), )+ ) => {
431            $(
432            #[test]
433            fn $name() {
434                let ops: Vec<Op> = $ops.into();
435                let got = format_ops(ops);
436                let want: &str = $want;
437                assert_eq!(got, want);
438            }
439            )+
440        };
441    }
442
443    format_tests!(
444        (
445            no_payload,
446            [Op::NoOp, Op::EndPage],
447            "no_op\nend_page\n"
448        ),
449        (
450            char_printable,
451            [Op::Char{char: 'A' as u32, move_h: true}],
452            "char('A')\n"
453        ),
454        (
455            char_space,
456            [Op::Char{char: ' ' as u32, move_h: true}],
457            "char(' ')\n"
458        ),
459        (
460            char_quote,
461            [Op::Char{char: '\'' as u32, move_h: true}],
462            "char('\\'')\n"
463        ),
464        (
465            char_backslash,
466            [Op::Char{char: '\\' as u32, move_h: true}],
467            "char('\\\\')\n"
468        ),
469        (
470            char_not_printable,
471            [Op::Char{char: 0, move_h: true}],
472            "char(0)\n"
473        ),
474        (
475            char_large,
476            [Op::Char{char: 1000, move_h: true}],
477            "char(1000)\n"
478        ),
479        (
480            put_char,
481            [Op::Char{char: 'A' as u32, move_h: false}],
482            "put_char('A')\n"
483        ),
484        (
485            rule,
486            [Op::Rule{height: 1, width: 2, move_h: true}],
487            "rule(height: 1, width: 2)\n"
488        ),
489        (
490            put_rule,
491            [Op::Rule{height: 1, width: 2, move_h: false}],
492            "put_rule(height: 1, width: 2)\n"
493        ),
494        (
495            extension_string,
496            [Op::Extension("papersize=8.5in,11in".into())],
497            "extension(\"papersize=8.5in,11in\")\n"
498        ),
499        (
500            extension_bytes,
501            [Op::Extension(vec![0xff, 0x01])],
502            "extension(bytes: \"ff01\")\n"
503        ),
504        (
505            push_and_pop_indent,
506            [Op::Push, Op::Push, Op::NoOp, Op::Pop, Op::Pop, Op::NoOp],
507            "push\n  push\n    no_op\n  pop\npop\nno_op\n"
508        ),
509        (
510            unmatched_pop,
511            [Op::Pop],
512            "pop  // stack underflow\n"
513        ),
514        (
515            too_many_pops,
516            [Op::Push, Op::Pop, Op::Pop, Op::NoOp],
517            "push\npop\npop  // stack underflow\nno_op\n"
518        ),
519        (
520            unmatched_push,
521            [Op::Push, Op::Push, Op::NoOp],
522            "push\n  push\n    no_op\n"
523        ),
524        (
525            begin_page_resets_indentation,
526            [
527                Op::Push,
528                Op::Push,
529                Op::BeginPage{parameters: [0; 10], previous_begin_page: -1},
530                Op::NoOp,
531            ],
532            "push\n  push\n    begin_page(\n      parameters: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],\n      previous_begin_page: -1,\n    )\nno_op\n"
533        ),
534        (
535            move_comment,
536            [Op::SetVar(Var::W, 300), Op::Move(Var::W)],
537            "set_var(w, 300)\nmove(w)  // 300\n"
538        ),
539        (
540            move_comment_is_zero_by_default,
541            [Op::Move(Var::Z)],
542            "move(z)  // 0\n"
543        ),
544        (
545            move_comment_after_pop,
546            [
547                Op::SetVar(Var::W, 100),
548                Op::Push,
549                Op::SetVar(Var::W, 200),
550                Op::Move(Var::W),
551                Op::Pop,
552                Op::Move(Var::W),
553            ],
554            "set_var(w, 100)\npush\n  set_var(w, 200)\n  move(w)  // 200\npop\nmove(w)  // 100\n"
555        ),
556        (
557            move_comment_after_unmatched_pop,
558            [
559                Op::SetVar(Var::W, 100),
560                Op::Pop,
561                Op::Move(Var::W),
562            ],
563            "set_var(w, 100)\npop  // stack underflow\nmove(w)  // 100\n"
564        ),
565        (
566            move_comment_after_begin_page,
567            [
568                Op::SetVar(Var::W, 100),
569                Op::BeginPage{parameters: [0; 10], previous_begin_page: -1},
570                Op::Move(Var::W),
571            ],
572            "set_var(w, 100)\nbegin_page(\n  parameters: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],\n  previous_begin_page: -1,\n)\nmove(w)  // 0\n"
573        ),
574    );
575
576    #[test]
577    fn byte_offsets() {
578        let ops = [
579            Op::Push,
580            Op::DefineFont {
581                number: 0,
582                checksum: 1,
583                at_size: 2,
584                design_size: 3,
585                area: "".into(),
586                name: "cmr10".into(),
587            },
588            Op::Pop,
589        ];
590        let mut formatter = Formatter::new(Options {
591            byte_offsets: true,
592            ..Default::default()
593        });
594        let mut got = String::new();
595        for (i, op) in ops.iter().enumerate() {
596            formatter.write_op(&mut got, op, i * 10).unwrap();
597        }
598        let want = r#"       0  push
599      10    define_font(
600              number: 0,
601              checksum: 1,
602              at_size: 2,
603              design_size: 3,
604              area: "",
605              name: "cmr10",
606            )
607      20  pop
608"#;
609        assert_eq!(got, want);
610    }
611
612    #[test]
613    fn indentation_disabled() {
614        let ops = vec![
615            Op::Push,
616            Op::Push,
617            Op::NoOp,
618            Op::Pop,
619            Op::Pop,
620            Op::Pop,
621            Op::NoOp,
622        ];
623        let mut formatter = Formatter::new(Options {
624            indentation: false,
625            ..Default::default()
626        });
627        let mut got = String::new();
628        for op in &ops {
629            formatter.write_op(&mut got, op, 0).unwrap();
630        }
631        // Disabling indentation does not disable the stack underflow marker,
632        // which does not depend on indentation.
633        let want = "push\npush\nno_op\npop\npop\npop  // stack underflow\nno_op\n";
634        assert_eq!(got, want);
635    }
636
637    #[test]
638    fn indentation_disabled_with_byte_offsets() {
639        let ops = [Op::Push, Op::NoOp];
640        let mut formatter = Formatter::new(Options {
641            byte_offsets: true,
642            indentation: false,
643        });
644        let mut got = String::new();
645        for (i, op) in ops.iter().enumerate() {
646            formatter.write_op(&mut got, op, i).unwrap();
647        }
648        assert_eq!(got, "       0  push\n       1  no_op\n");
649    }
650
651    #[test]
652    fn display_single_op_omits_comment() {
653        assert_eq!(format!("{}", Op::Move(Var::W)), "move(w)");
654    }
655}