1use crate::ds;
17use std::{collections::HashMap, path::PathBuf};
18
19pub trait TexEngine {
21 fn run(&self, tex_source_code: &str, auxiliary_files: &HashMap<PathBuf, Vec<u8>>) -> String;
23}
24
25#[derive(Clone, Debug)]
27pub struct BinaryNotFound {
28 pub binary_name: String,
30}
31
32impl std::fmt::Display for BinaryNotFound {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 write!(
35 f,
36 "binary `{}` not found (`which {}` failed)",
37 &self.binary_name, &self.binary_name
38 )
39 }
40}
41
42impl std::error::Error for BinaryNotFound {}
43
44pub fn new_tex_engine_binary(binary_name: String) -> Result<Box<dyn TexEngine>, BinaryNotFound> {
46 #[allow(clippy::expect_fun_call)]
47 if std::process::Command::new("which")
48 .arg(&binary_name)
49 .stdout(std::process::Stdio::null())
50 .spawn()
51 .expect(&format!["`which {binary_name}` command failed to start"])
52 .wait()
53 .expect(&format!["failed to run `which {binary_name}`"])
54 .success()
55 {
56 Ok(Box::new(TexEngineBinary(binary_name)))
57 } else {
58 Err(BinaryNotFound { binary_name })
59 }
60}
61
62struct TexEngineBinary(String);
63
64impl TexEngine for TexEngineBinary {
65 fn run(&self, tex_source_code: &str, auxiliary_files: &HashMap<PathBuf, Vec<u8>>) -> String {
66 let dir = std::env::temp_dir();
67
68 for (file_name, content) in auxiliary_files {
69 let mut path = dir.clone();
70 path.push(file_name);
71 eprintln!("writing to {}", path.as_os_str().to_string_lossy());
72 std::fs::write(&path, content).unwrap_or_else(|_| {
73 panic![
74 "Unable to write auxiliary file {}",
75 file_name.to_string_lossy()
76 ]
77 });
78 }
79
80 let mut input_path = dir.clone();
81 input_path.push("tex-input");
82 input_path.set_extension("tex");
83 eprintln!("writing to {}", input_path.as_os_str().to_string_lossy());
84 std::fs::write(&input_path, tex_source_code).expect("Unable to write file");
85
86 let output = std::process::Command::new(&self.0)
87 .current_dir(&dir)
88 .arg(&input_path)
89 .output()
90 .expect("failed to run tex command");
91 eprintln!("{}", String::from_utf8(output.stderr).unwrap());
92
93 let stdout = String::from_utf8(output.stdout).expect("stdout output of TeX is utf-8");
94 if !output.status.success() {
95 eprintln!("Warning: TeX command seems to have failed. Consult the logs in the log file (replace .tex input file with .log)");
96 }
97 stdout
98 }
99}
100
101pub fn build_horizontal_lists(
109 tex_engine: &dyn TexEngine,
110 auxiliary_files: &HashMap<PathBuf, Vec<u8>>,
111 preamble: &str,
112 contents: &mut dyn Iterator<Item = &String>,
113) -> (HashMap<String, u32>, Vec<ds::HList>) {
114 let macro_calls: Vec<String> = contents.map(|s| format!(r#"\printBox{{{s}}}"#)).collect();
115 let tex_source_code = CONVERT_TEXT_TEMPLATE
116 .replace("<preamble>", preamble)
117 .replace("<box_template>", r"\hbox{#1}")
118 .replace("<print_calls>", ¯o_calls.join("\n\n"));
119 let output = tex_engine.run(&tex_source_code, auxiliary_files);
120 let segments = extract_texcraft_segments(&output);
121 let mut fonts: HashMap<String, u32> = Default::default();
122 let hlists = segments
123 .map(|s| parse_hlist(&mut TexOutputIter::new(s), &mut fonts))
124 .collect();
125 (fonts, hlists)
126}
127
128pub fn build_vertical_lists(
137 tex_engine: &dyn TexEngine,
138 auxiliary_files: &HashMap<PathBuf, Vec<u8>>,
139 preamble: &str,
140 contents: &mut dyn Iterator<Item = &String>,
141) -> (HashMap<String, u32>, Vec<ds::VList>) {
142 let macro_calls: Vec<String> = contents.map(|s| format!(r#"\printBox{{{s}}}"#)).collect();
143 let tex_source_code = CONVERT_TEXT_TEMPLATE
144 .replace("<preamble>", preamble)
145 .replace("<box_template>", r"\vbox{\noindent \hsize=41pt #1}")
146 .replace("<print_calls>", ¯o_calls.join("\n\n"));
147 let output = tex_engine.run(&tex_source_code, auxiliary_files);
148 let segments = extract_texcraft_segments(&output);
149 let mut fonts: HashMap<String, u32> = Default::default();
150 let vlists = segments
151 .map(|s| parse_vlist(&mut TexOutputIter::new(s), &mut fonts))
152 .collect();
153 (fonts, vlists)
154}
155
156struct TexOutputIter<'tex> {
157 s: &'tex str,
158 depth: usize,
159}
160
161impl<'tex> Iterator for TexOutputIter<'tex> {
162 type Item = &'tex str;
163
164 fn next(&mut self) -> Option<Self::Item> {
165 let (line, n) = self.peek_impl()?;
166 self.s = &self.s[n..];
167 Some(line)
168 }
169}
170
171impl<'tex> TexOutputIter<'tex> {
172 fn new(mut s: &'tex str) -> Self {
173 loop {
174 let line = s
175 .split_inclusive('\n')
176 .next()
177 .expect("still searching for start of output");
178 s = &s[line.len()..];
179 if !line.starts_with(r"> \box0=") {
180 continue;
181 }
182 return Self { s, depth: 0 };
183 }
184 }
185 fn inner(&self) -> Self {
186 Self {
187 s: self.s,
188 depth: self.depth + 1,
189 }
190 }
191 fn peek(&mut self) -> Option<&'tex str> {
192 let (line, _) = self.peek_impl()?;
193 Some(line)
194 }
195 fn peek_impl(&mut self) -> Option<(&'tex str, usize)> {
196 loop {
197 let line = self.s.split_inclusive('\n').next()?;
198 let n = line.len();
199 let line = line.trim_end();
200 if line.is_empty() {
201 self.s = "";
202 return None;
203 }
204 let line_depth = line.chars().take_while(|&c| c == '.').count();
205 use std::cmp::Ordering::*;
206 match line_depth.cmp(&self.depth) {
207 Less => {
208 self.s = "";
209 return None;
210 }
211 Equal => {
212 return Some((&line[line_depth..], n));
213 }
214 Greater => {
215 self.s = &self.s[n..];
217 }
218 }
219 }
220 }
221}
222
223fn extract_texcraft_segments(mut s: &str) -> impl Iterator<Item = &str> {
227 std::iter::from_fn(move || {
228 loop {
230 let line = s.split_inclusive('\n').next()?;
231 s = &s[line.len()..];
232 if line.starts_with("Texcraft: begin") {
233 break;
234 }
235 }
236 let next = s;
237 let mut next_len = 0_usize;
238 loop {
240 let line = s.split_inclusive('\n').next()?;
241 s = &s[line.len()..];
242 next_len += line.len();
243 if line.starts_with("Texcraft: end") {
244 return Some(&next[0..next_len]);
245 }
246 }
247 })
248}
249
250fn parse_hlist(iter: &mut TexOutputIter, fonts: &mut HashMap<String, u32>) -> ds::HList {
254 let mut hlist = {
255 let line = iter.next().expect("hlist must be non-empty");
256 let s = line
257 .strip_prefix(r"\hbox(")
258 .expect("first line must be a hlist spec");
259 let i = s
260 .find('+')
261 .expect("hbox dimension spec has a + between height and depth");
262 let height = parse_scaled(&s[..i]);
263 let s = &s[i + 1..];
264 let i = s
265 .find(")x")
266 .expect("hbox dimension spec has a )x between depth and width");
267 let depth = parse_scaled(&s[..i]);
268 let rest = &s[i + 2..];
269 let (width_str, glue_set_str) = if let Some(j) = rest.find(", glue set ") {
270 (&rest[..j], Some(&rest[j + 11..]))
271 } else {
272 (rest, None)
273 };
274 let width = parse_scaled(width_str);
275 let (glue_ratio, glue_sign, glue_order) = glue_set_str.map(parse_glue_set).unwrap_or((
276 ds::GlueRatio(0.0),
277 ds::GlueSign::Normal,
278 core::GlueOrder::Normal,
279 ));
280 ds::HList {
281 height,
282 width,
283 depth,
284 glue_ratio,
285 glue_sign,
286 glue_order,
287 ..Default::default()
288 }
289 };
290 let mut iter = iter.inner();
291 while let Some(line) = iter.peek() {
292 let (keyword, tail) = keyword_and_tail(line);
293 let mut consume_line = true;
294 let elem: ds::Horizontal = match keyword {
295 "glue" => ds::Glue {
296 kind: ds::GlueKind::Normal,
297 value: parse_glue_value(tail),
298 }
299 .into(),
300 "kern" => {
301 let mut words = tail.split_ascii_whitespace();
302 let width = parse_scaled(words.next().expect("kern has 1 word"));
303 ds::Kern {
304 kind: ds::KernKind::Normal,
305 width,
306 }
307 .into()
308 }
309 "penalty" => {
310 let value: i32 = tail.trim().parse().expect("penalty value is i32");
311 ds::Penalty(value).into()
312 }
313 "rule" => {
314 let i = tail.find('x').expect("rule has a width");
315 ds::Rule {
316 width: parse_scaled(&tail[i + 1..]),
317 ..Default::default()
318 }
319 .into()
320 }
321 "discretionary" => {
322 _ = tail;
324 ds::Discretionary {
325 pre_break: vec![],
326 post_break: vec![],
327 replace_count: 1,
328 }
329 .into()
330 }
331 "hbox" => {
332 consume_line = false;
333 parse_hlist(&mut iter, fonts).into()
334 }
335 "vbox" => {
336 consume_line = false;
337 parse_vlist(&mut iter, fonts).into()
338 }
339 font_name => {
340 use std::collections::hash_map::Entry;
341 let num_fonts: u32 = fonts.len().try_into().expect("no more than 2^32 fonts");
342 let font = match fonts.entry(font_name.to_string()) {
343 Entry::Occupied(occupied_entry) => *occupied_entry.get(),
344 Entry::Vacant(vacant_entry) => {
345 vacant_entry.insert(num_fonts);
346 num_fonts
347 }
348 };
349 let mut words = tail.split_ascii_whitespace();
350 let char =
351 parse_char(words.next().unwrap_or_else(|| {
352 panic!("expected char after font command \\{font_name}")
353 }));
354 if words.next() == Some("(ligature") {
355 let og_chars = words.next().expect("lig has 4 words");
356 let og_chars = og_chars.strip_suffix(")").expect("lig ends with ')'");
357 ds::Ligature {
358 included_left_boundary: false,
359 included_right_boundary: false,
360 char,
361 font,
362 original_chars: og_chars.into(),
363 }
364 .into()
365 } else {
366 ds::Char { char, font }.into()
367 }
368 }
369 };
370 hlist.list.push(elem);
371 if consume_line {
372 iter.next();
373 }
374 }
375 hlist
376}
377
378fn parse_vlist(iter: &mut TexOutputIter, fonts: &mut HashMap<String, u32>) -> ds::VList {
380 let mut vlist = {
381 let line = iter.next().expect("vlist must be non-empty");
382 let s = line
383 .strip_prefix(r"\vbox(")
384 .expect("first line must be a vlist spec");
385 let i = s
386 .find('+')
387 .expect("vbox dimension spec has a + between height and depth");
388 let height = parse_scaled(&s[..i]);
389 let s = &s[i + 1..];
390 let i = s
391 .find(")x")
392 .expect("vbox dimension spec has a )x between depth and width");
393 let depth = parse_scaled(&s[..i]);
394 let width = parse_scaled(&s[i + 2..]);
395 ds::VList {
396 height,
397 width,
398 depth,
399 shift_amount: core::Scaled::ZERO,
400 list: vec![],
401 glue_ratio: ds::GlueRatio(0.0),
402 glue_sign: ds::GlueSign::Normal,
403 glue_order: core::GlueOrder::Normal,
404 }
405 };
406 let mut iter = iter.inner();
407 while let Some(line) = iter.peek() {
408 let (keyword, tail) = keyword_and_tail(line);
409 let mut consume_line = true;
410 let elem: ds::Vertical = match keyword {
411 "hbox" => {
412 consume_line = false;
413 parse_hlist(&mut iter, fonts).into()
414 }
415 "penalty" => {
416 let value: i32 = tail.trim().parse().expect("penalty value is i32");
417 ds::Penalty(value).into()
418 }
419 "glue" => ds::Vertical::Glue(ds::Glue {
420 kind: ds::GlueKind::Normal,
421 value: parse_glue_value(tail),
422 }),
423 _ => unimplemented!("vlist keyword {keyword} is not implemented"),
424 };
425 vlist.list.push(elem);
426 if consume_line {
427 iter.next();
428 }
429 }
430 vlist
431}
432
433fn keyword_and_tail(s: &str) -> (&str, &str) {
434 let mut c = s.chars();
435 assert_eq!(c.next(), Some('\\'), "line expected to begin with \\");
436 let mut keyword_len = 0_usize;
437 for next in c {
438 if next.is_alphabetic() {
439 keyword_len += next.len_utf8();
440 } else {
441 break;
442 }
443 }
444 (&s[1..1 + keyword_len], s[1 + keyword_len..].trim())
445}
446
447fn parse_glue_value(spec: &str) -> core::Glue {
452 let spec = if spec.starts_with('(') {
453 let close = spec.find(')').expect("named glue has ')'");
454 spec[close + 1..].trim_start()
455 } else {
456 spec.trim_start()
457 };
458 let mut words = spec.split_ascii_whitespace();
459 let width = parse_scaled(words.next().expect("glue has a width"));
460 let (stretch, stretch_order) = if words.next() == Some("plus") {
461 parse_glue_amount(words.next().expect("glue has stretch after plus"))
462 } else {
463 (core::Scaled::ZERO, core::GlueOrder::Normal)
464 };
465 let (shrink, shrink_order) = if words.next() == Some("minus") {
466 parse_glue_amount(words.next().expect("glue has shrink after minus"))
467 } else {
468 (core::Scaled::ZERO, core::GlueOrder::Normal)
469 };
470 core::Glue {
471 width,
472 stretch,
473 stretch_order,
474 shrink,
475 shrink_order,
476 }
477}
478
479fn parse_glue_amount(s: &str) -> (core::Scaled, core::GlueOrder) {
481 if let Some(s) = s.strip_suffix("filll") {
482 (parse_scaled(s), core::GlueOrder::Filll)
483 } else if let Some(s) = s.strip_suffix("fill") {
484 (parse_scaled(s), core::GlueOrder::Fill)
485 } else if let Some(s) = s.strip_suffix("fil") {
486 (parse_scaled(s), core::GlueOrder::Fil)
487 } else {
488 (parse_scaled(s), core::GlueOrder::Normal)
489 }
490}
491
492fn parse_glue_set(s: &str) -> (ds::GlueRatio, ds::GlueSign, core::GlueOrder) {
494 let (sign, s) = if let Some(s) = s.strip_prefix("- ") {
495 (ds::GlueSign::Shrinking, s)
496 } else {
497 (ds::GlueSign::Stretching, s)
498 };
499 let (s, order) = if let Some(s) = s.strip_suffix("filll") {
500 (s, core::GlueOrder::Filll)
501 } else if let Some(s) = s.strip_suffix("fill") {
502 (s, core::GlueOrder::Fill)
503 } else if let Some(s) = s.strip_suffix("fil") {
504 (s, core::GlueOrder::Fil)
505 } else {
506 (s, core::GlueOrder::Normal)
507 };
508 let ratio: f32 = s.parse().expect("glue set ratio is a float");
509 (ds::GlueRatio(ratio), sign, order)
510}
511
512fn parse_char(s: &str) -> char {
513 let mut cs = s.chars();
514 match cs.next().expect("char has one character") {
515 '^' => {
516 assert_eq!(cs.next(), Some('^'));
517 let raw_c = cs.next().expect("char of the form ^^X") as u32;
518 if let Some(raw_c) = raw_c.checked_sub(64) {
519 raw_c
520 } else {
521 raw_c + 64
522 }
523 .try_into()
524 .expect("TeX describes a valid character")
525 }
526 c => c,
527 }
528}
529
530fn parse_scaled(s: &str) -> core::Scaled {
531 let (neg, s) = match s.strip_prefix('-') {
532 Some(s) => (true, s),
533 None => (false, s),
534 };
535 let mut parts = s.split('.');
536 let i: i32 = parts
537 .next()
538 .expect("scaled has an integer part")
539 .parse()
540 .expect("integer part is an integer");
541 let mut f = [0_u8; 17];
542 for (k, c) in parts
543 .next()
544 .expect("scaled has a fractional part")
545 .chars()
546 .enumerate()
547 {
548 f[k] = c
549 .to_digit(10)
550 .expect("fractional part are digits")
551 .try_into()
552 .expect("digits are in the range [0,10) and always fit in u8");
553 }
554 let f = core::Scaled::from_decimal_digits(&f);
555 let sc = core::Scaled::new(i, f, core::ScaledUnit::Point).unwrap_or_else(|_| {
556 eprintln!(
557 "scaled number '{s}pt' is too big to parse; replacing with the largest scaled value {}",
558 core::Scaled::MAX_DIMEN
559 );
560 core::Scaled::MAX_DIMEN
561 });
562 if neg {
563 -sc
564 } else {
565 sc
566 }
567}
568
569const CONVERT_TEXT_TEMPLATE: &str = r"
570
571% User provided preamble.
572<preamble>
573
574% After showing a box, TeX stops and waits for user input.
575% The following command suppresses that behavior.
576\nonstopmode
577
578% Output the box description to the terminal, from which we'll read it.
579\tracingonline=1
580
581% Output up to 1 million nodes.
582\showboxbreadth=1000000
583% Output up to 100 nested boxes.
584\showboxdepth=100
585
586% Prints the contents on its own line in the terminal.
587\def\fullLineMessage#1{
588 {
589 \newlinechar=`@
590 \message{@#1@}
591 }
592}
593
594\def\printBox#1{
595 % Put the content we want to see in box 0.
596 \setbox0=<box_template>
597 % Add a start marker so we know where to begin in the log
598 \fullLineMessage{Texcraft: begin}
599 % Show the box!
600 \showbox0
601
602 % Add a start marker so we know where to end in the log
603 \fullLineMessage{Texcraft: end}
604}
605
606<print_calls>
607
608We add some text at the end.
609
610\end
611";
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616
617 struct MockTexEngine(String);
618
619 impl TexEngine for MockTexEngine {
620 fn run(&self, _: &str, _: &HashMap<PathBuf, Vec<u8>>) -> String {
621 self.0.clone()
622 }
623 }
624
625 #[test]
626 fn test_build_horizontal_lists() {
627 let log = r#"This is TeX, Version 3.141592653 (TeX Live 2024) (preloaded format=tex)
628(./tmp/test.tex
629
630Texcraft: begin
631> \box0=
632\hbox(6.94444+0.0)x56.66678
633.\tenrm M
634.\tenrm i
635.\tenrm n
636.\kern-0.27779
637.\tenrm t
638.\glue 3.33333 plus 1.66666 minus 1.11111
639.\tenrm a
640.\tenrm n
641.\tenrm d
642.\glue 3.33333 plus 1.66666 minus 1.11111
643.\tenrm m
644.\tenrm e
645
646! OK.
647\printBox ...Message {Texcraft: begin} \showbox 0
648 \par \fullLineMessage {Tex...
649l.31 \printBox{Mint and me}
650
651
652Texcraft: end
653 )
654(see the transcript file for additional information)
655No pages of output.
656Transcript written on test.log.
657"#;
658
659 let tex_engine = MockTexEngine(log.to_string());
660 let (got_fonts, got_list) = build_horizontal_lists(
661 &tex_engine,
662 &Default::default(),
663 &"",
664 &mut vec!["".to_string()].iter(),
665 );
666
667 let want_list = ds::HList {
668 height: parse_scaled("6.94444"),
669 width: parse_scaled("56.66678"),
670 depth: core::Scaled::ZERO,
671 list: vec![
672 ds::Char { char: 'M', font: 0 }.into(),
673 ds::Char { char: 'i', font: 0 }.into(),
674 ds::Char { char: 'n', font: 0 }.into(),
675 ds::Kern {
676 kind: ds::KernKind::Normal,
677 width: parse_scaled("-0.27779"),
678 }
679 .into(),
680 ds::Char { char: 't', font: 0 }.into(),
681 ds::Glue {
682 kind: ds::GlueKind::Normal,
683 value: core::Glue {
684 width: parse_scaled("3.33333"),
685 stretch: parse_scaled("1.66666"),
686 stretch_order: core::GlueOrder::Normal,
687 shrink: parse_scaled("1.11111"),
688 shrink_order: core::GlueOrder::Normal,
689 },
690 }
691 .into(),
692 ds::Char { char: 'a', font: 0 }.into(),
693 ds::Char { char: 'n', font: 0 }.into(),
694 ds::Char { char: 'd', font: 0 }.into(),
695 ds::Glue {
696 kind: ds::GlueKind::Normal,
697 value: core::Glue {
698 width: parse_scaled("3.33333"),
699 stretch: parse_scaled("1.66666"),
700 stretch_order: core::GlueOrder::Normal,
701 shrink: parse_scaled("1.11111"),
702 shrink_order: core::GlueOrder::Normal,
703 },
704 }
705 .into(),
706 ds::Char { char: 'm', font: 0 }.into(),
707 ds::Char { char: 'e', font: 0 }.into(),
708 ],
709 ..Default::default()
710 };
711 let want_fonts = {
712 let mut m = HashMap::new();
713 m.insert("tenrm".to_string(), 0);
714 m
715 };
716
717 assert_eq!(got_list, vec![want_list]);
718 assert_eq!(got_fonts, want_fonts);
719 }
720
721 #[test]
722 fn test_build_vertical_lists() {
723 let log = r#"
724This is TeX, Version 3.141592653 (TeX Live 2024) (preloaded format=tex)
725(./test.tex
726
727Texcraft: begin
728> \box0=
729\vbox(18.94444+0.0)x41.0
730.\hbox(6.94444+0.0)x41.0, glue set 0.26662
731..\tenrm M
732..\tenrm i
733..\tenrm n
734..\kern-0.27779
735..\tenrm t
736..\glue 3.33333 plus 1.66666 minus 1.11111
737..\tenrm a
738..\tenrm n
739..\tenrm d
740..\glue(\rightskip) 0.0
741.\penalty 300
742.\glue(\baselineskip) 7.69446
743.\hbox(4.30554+0.0)x41.0, glue set 28.2222fil
744..\tenrm m
745..\tenrm e
746..\penalty 10000
747..\glue(\parfillskip) 0.0 plus 1.0fil
748..\glue(\rightskip) 0.0
749
750! OK.
751\printBox ...Message {Texcraft: begin} \showbox 0
752 \par \fullLineMessage {Tex...
753l.31 \printBox{Mint and me}
754
755
756Texcraft: end
757 )
758(see the transcript file for additional information)
759No pages of output.
760Transcript written on test.log.
761"#;
762
763 let tex_engine = MockTexEngine(log.to_string());
764 let (got_fonts, got_list) = build_vertical_lists(
765 &tex_engine,
766 &Default::default(),
767 &"",
768 &mut vec!["".to_string()].iter(),
769 );
770
771 let want_list = ds::VList {
772 height: parse_scaled("18.94444"),
773 width: parse_scaled("41.0"),
774 depth: core::Scaled::ZERO,
775 shift_amount: core::Scaled::ZERO,
776 glue_ratio: ds::GlueRatio(0.0),
777 glue_sign: ds::GlueSign::Normal,
778 glue_order: core::GlueOrder::Normal,
779 list: vec![
780 ds::Vertical::HList(ds::HList {
781 height: parse_scaled("6.94444"),
782 width: parse_scaled("41.0"),
783 depth: core::Scaled::ZERO,
784 glue_ratio: ds::GlueRatio(0.26662),
785 glue_sign: ds::GlueSign::Stretching,
786 glue_order: core::GlueOrder::Normal,
787 list: vec![
788 ds::Char { char: 'M', font: 0 }.into(),
789 ds::Char { char: 'i', font: 0 }.into(),
790 ds::Char { char: 'n', font: 0 }.into(),
791 ds::Kern {
792 kind: ds::KernKind::Normal,
793 width: parse_scaled("-0.27779"),
794 }
795 .into(),
796 ds::Char { char: 't', font: 0 }.into(),
797 ds::Glue {
798 kind: ds::GlueKind::Normal,
799 value: core::Glue {
800 width: parse_scaled("3.33333"),
801 stretch: parse_scaled("1.66666"),
802 stretch_order: core::GlueOrder::Normal,
803 shrink: parse_scaled("1.11111"),
804 shrink_order: core::GlueOrder::Normal,
805 },
806 }
807 .into(),
808 ds::Char { char: 'a', font: 0 }.into(),
809 ds::Char { char: 'n', font: 0 }.into(),
810 ds::Char { char: 'd', font: 0 }.into(),
811 ds::Glue {
812 kind: ds::GlueKind::Normal,
813 value: core::Glue {
814 width: core::Scaled::ZERO,
815 stretch: core::Scaled::ZERO,
816 stretch_order: core::GlueOrder::Normal,
817 shrink: core::Scaled::ZERO,
818 shrink_order: core::GlueOrder::Normal,
819 },
820 }
821 .into(),
822 ],
823 ..Default::default()
824 }),
825 ds::Vertical::Penalty(ds::Penalty(300)),
826 ds::Vertical::Glue(ds::Glue {
827 kind: ds::GlueKind::Normal,
828 value: core::Glue {
829 width: parse_scaled("7.69446"),
830 stretch: core::Scaled::ZERO,
831 stretch_order: core::GlueOrder::Normal,
832 shrink: core::Scaled::ZERO,
833 shrink_order: core::GlueOrder::Normal,
834 },
835 }),
836 ds::Vertical::HList(ds::HList {
837 height: parse_scaled("4.30554"),
838 width: parse_scaled("41.0"),
839 depth: core::Scaled::ZERO,
840 glue_ratio: ds::GlueRatio(28.2222),
841 glue_sign: ds::GlueSign::Stretching,
842 glue_order: core::GlueOrder::Fil,
843 list: vec![
844 ds::Char { char: 'm', font: 0 }.into(),
845 ds::Char { char: 'e', font: 0 }.into(),
846 ds::Penalty(10000).into(),
847 ds::Glue {
848 kind: ds::GlueKind::Normal,
849 value: core::Glue {
850 width: core::Scaled::ZERO,
851 stretch: parse_scaled("1.0"),
852 stretch_order: core::GlueOrder::Fil,
853 shrink: core::Scaled::ZERO,
854 shrink_order: core::GlueOrder::Normal,
855 },
856 }
857 .into(),
858 ds::Glue {
859 kind: ds::GlueKind::Normal,
860 value: core::Glue {
861 width: core::Scaled::ZERO,
862 stretch: core::Scaled::ZERO,
863 stretch_order: core::GlueOrder::Normal,
864 shrink: core::Scaled::ZERO,
865 shrink_order: core::GlueOrder::Normal,
866 },
867 }
868 .into(),
869 ],
870 ..Default::default()
871 }),
872 ],
873 };
874 let want_fonts = {
875 let mut m = HashMap::new();
876 m.insert("tenrm".to_string(), 0);
877 m
878 };
879
880 assert_eq!(got_list, vec![want_list]);
881 assert_eq!(got_fonts, want_fonts);
882 }
883 #[test]
884 fn test_build_vertical_lists_2() {
885 let log = r#"
886This is TeX, Version 3.141592653 (TeX Live 2024) (preloaded format=tex)
887(./test.tex
888
889Texcraft: begin
890> \box0=
891\vbox(6.83331+0.0)x41.0
892.\hbox(6.83331+0.0)x41.0
893..\tenrm A
894..\hbox(6.83331+0.0)x48.08336
895...\tenrm B
896...\vbox(6.83331+0.0)x41.0
897....\hbox(6.83331+0.0)x41.0, glue set 6.13887fil
898.....\hbox(0.0+0.0)x20.0
899.....\tenrm C
900.....\hbox(6.83331+0.0)x7.6389
901......\tenrm D
902.....\penalty 10000
903.....\glue(\parfillskip) 0.0 plus 1.0fil
904.....\glue(\rightskip) 0.0
905..\penalty 10000
906..\glue(\parfillskip) 0.0 plus 1.0fil
907..\glue(\rightskip) 0.0
908..\rule(*+*)x5.0
909
910! OK.
911\printBox ...Message {Texcraft: begin} \showbox 0
912 \par \fullLineMessage {Tex...
913l.31 \printBox{Mint and me}
914
915
916Texcraft: end
917 )
918(see the transcript file for additional information)
919No pages of output.
920Transcript written on test.log.
921"#;
922
923 let tex_engine = MockTexEngine(log.to_string());
924 let (got_fonts, got_list) = build_vertical_lists(
925 &tex_engine,
926 &Default::default(),
927 &"",
928 &mut vec!["".to_string()].iter(),
929 );
930
931 let want_list = ds::VList {
932 height: parse_scaled("6.83331"),
933 width: parse_scaled("41.0"),
934 depth: core::Scaled::ZERO,
935 shift_amount: core::Scaled::ZERO,
936 glue_ratio: ds::GlueRatio(0.0),
937 glue_sign: ds::GlueSign::Normal,
938 glue_order: core::GlueOrder::Normal,
939 list: vec![ds::Vertical::HList(ds::HList {
940 height: parse_scaled("6.83331"),
941 width: parse_scaled("41.0"),
942 depth: core::Scaled::ZERO,
943 list: vec![
944 ds::Char { char: 'A', font: 0 }.into(),
945 ds::Horizontal::HList(ds::HList {
946 height: parse_scaled("6.83331"),
947 width: parse_scaled("48.08336"),
948 depth: core::Scaled::ZERO,
949 list: vec![
950 ds::Char { char: 'B', font: 0 }.into(),
951 ds::Horizontal::VList(ds::VList {
952 height: parse_scaled("6.83331"),
953 width: parse_scaled("41.0"),
954 depth: core::Scaled::ZERO,
955 list: vec![ds::Vertical::HList(ds::HList {
956 height: parse_scaled("6.83331"),
957 width: parse_scaled("41.0"),
958 depth: core::Scaled::ZERO,
959 glue_ratio: ds::GlueRatio(6.13887),
960 glue_sign: ds::GlueSign::Stretching,
961 glue_order: core::GlueOrder::Fil,
962 list: vec![
963 ds::Horizontal::HList(ds::HList {
964 height: core::Scaled::ZERO,
965 width: parse_scaled("20.0"),
966 depth: core::Scaled::ZERO,
967 ..Default::default()
968 }),
969 ds::Char { char: 'C', font: 0 }.into(),
970 ds::Horizontal::HList(ds::HList {
971 height: parse_scaled("6.83331"),
972 width: parse_scaled("7.6389"),
973 depth: core::Scaled::ZERO,
974 list: vec![ds::Char { char: 'D', font: 0 }.into()],
975 ..Default::default()
976 }),
977 ds::Penalty(10000).into(),
978 ds::Glue {
979 kind: ds::GlueKind::Normal,
980 value: core::Glue {
981 width: core::Scaled::ZERO,
982 stretch: parse_scaled("1.0"),
983 stretch_order: core::GlueOrder::Fil,
984 shrink: core::Scaled::ZERO,
985 shrink_order: core::GlueOrder::Normal,
986 },
987 }
988 .into(),
989 ds::Glue {
990 kind: ds::GlueKind::Normal,
991 value: core::Glue {
992 width: core::Scaled::ZERO,
993 stretch: core::Scaled::ZERO,
994 stretch_order: core::GlueOrder::Normal,
995 shrink: core::Scaled::ZERO,
996 shrink_order: core::GlueOrder::Normal,
997 },
998 }
999 .into(),
1000 ],
1001 ..Default::default()
1002 })],
1003 ..Default::default()
1004 }),
1005 ],
1006 ..Default::default()
1007 }),
1008 ds::Penalty(10000).into(),
1009 ds::Glue {
1010 kind: ds::GlueKind::Normal,
1011 value: core::Glue {
1012 width: core::Scaled::ZERO,
1013 stretch: parse_scaled("1.0"),
1014 stretch_order: core::GlueOrder::Fil,
1015 shrink: core::Scaled::ZERO,
1016 shrink_order: core::GlueOrder::Normal,
1017 },
1018 }
1019 .into(),
1020 ds::Glue {
1021 kind: ds::GlueKind::Normal,
1022 value: core::Glue {
1023 width: core::Scaled::ZERO,
1024 stretch: core::Scaled::ZERO,
1025 stretch_order: core::GlueOrder::Normal,
1026 shrink: core::Scaled::ZERO,
1027 shrink_order: core::GlueOrder::Normal,
1028 },
1029 }
1030 .into(),
1031 ds::Rule {
1032 height: ds::Rule::RUNNING,
1033 depth: ds::Rule::RUNNING,
1034 width: parse_scaled("5.0"),
1035 }
1036 .into(),
1037 ],
1038 ..Default::default()
1039 })],
1040 };
1041 let want_fonts = {
1042 let mut m = HashMap::new();
1043 m.insert("tenrm".to_string(), 0);
1044 m
1045 };
1046
1047 assert_eq!(got_fonts, want_fonts);
1048 assert_eq!(got_list, vec![want_list]);
1049 }
1050}