1use crate::{Op, Values, Var};
65use std::fmt::Write;
66
67const INDENT: &str = " ";
69
70const OFFSET_WIDTH: usize = 8;
72
73#[derive(Clone, Debug)]
83pub struct Options {
84 pub byte_offsets: bool,
90 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#[derive(Default)]
138pub struct Formatter {
139 options: Options,
140 values: Values,
141 buffer: String,
143}
144
145impl Formatter {
146 pub fn new(options: Options) -> Self {
148 Self {
149 options,
150 values: Default::default(),
151 buffer: Default::default(),
152 }
153 }
154 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 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 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 pub fn options(&self) -> &Options {
209 &self.options
210 }
211
212 pub fn values(&self) -> &Values {
214 &self.values
215 }
216}
217
218pub 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 formatter.write_op(&mut out, &op, 0).unwrap();
233 }
234 out
235}
236
237impl std::fmt::Display for Op {
238 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
262fn write_op(out: &mut String, op: &Op, values: Option<&Values>) {
266 match op {
267 Op::Char { char, move_h } => {
268 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 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 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
396fn 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
405fn 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 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}