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 mut dir = std::env::temp_dir();
67 let thread = std::thread::current();
68 let thread_name = thread
69 .name()
70 .unwrap_or("texcraft_unknown_thread_name")
71 .replace("::", "__");
72 dir.push("texcraft_tex");
73 dir.push(thread_name);
74 std::fs::create_dir_all(&dir).unwrap();
75
76 for (file_name, content) in auxiliary_files {
77 let mut path = dir.clone();
78 path.push(file_name);
79 eprintln!("writing to {}", path.as_os_str().to_string_lossy());
80 std::fs::write(&path, content).unwrap_or_else(|_| {
81 panic![
82 "Unable to write auxiliary file {}",
83 file_name.to_string_lossy()
84 ]
85 });
86 }
87
88 let mut input_path = dir.clone();
89 input_path.push("tex-input");
90 input_path.set_extension("tex");
91 eprintln!("writing to {}", input_path.as_os_str().to_string_lossy());
92 std::fs::write(&input_path, tex_source_code).expect("Unable to write file");
93
94 let output = std::process::Command::new(&self.0)
95 .current_dir(&dir)
96 .arg(&input_path)
97 .output()
98 .expect("failed to run tex command");
99 eprintln!("{}", String::from_utf8(output.stderr).unwrap());
100
101 let stdout = String::from_utf8(output.stdout).expect("stdout output of TeX is utf-8");
102 if !output.status.success() {
103 eprintln!("Warning: TeX command seems to have failed. Consult the logs in the log file (replace .tex input file with .log)");
104 }
105 stdout
106 }
107}
108
109const HBOX_TEMPLATE: &str = include_str!("hbox_template.tex");
110
111pub fn build_horizontal_lists(
119 tex_engine: &dyn TexEngine,
120 auxiliary_files: &HashMap<PathBuf, Vec<u8>>,
121 preamble: &str,
122 contents: &mut dyn Iterator<Item = &String>,
123 hyphenate: bool,
124) -> (HashMap<String, u32>, Vec<ds::HBox>) {
125 let macro_calls: Vec<String> = contents
126 .map(|s| format!(r#"\buildAndPrintBoxes{{{s}}}"#))
127 .collect();
128 let tex_source_code = HBOX_TEMPLATE
129 .replace("<preamble>", preamble)
130 .replace("<print_calls>", ¯o_calls.join("\n\n"));
131 let output = tex_engine.run(&tex_source_code, auxiliary_files);
132
133 let mut fonts: HashMap<String, u32> = Default::default();
134 let mut tail: &str = &output;
135 let mut line_number = 0_usize;
136 enum Next {
137 First,
138 Second(ds::HBox),
139 Third(ds::HBox, ds::HBox),
140 }
141 let mut next = Next::First;
142 let mut h_boxes = vec![];
143 while let Some(line) = tail.split_inclusive('\n').next() {
144 line_number += 1;
145 tail = &tail[line.len()..];
146 if !line.trim().starts_with(match next {
147 Next::First => r"> \box253=",
148 Next::Second(_) => "### horizontal mode entered at line",
149 Next::Third(_, _) => "### current page:",
150 }) {
151 continue;
152 }
153 let mut iter = TexOutputIter {
154 s: tail,
155 depth: 0,
156 line_number,
157 };
158 next = match next {
159 Next::First => Next::Second(parse_h_box(&mut iter, &mut fonts).unwrap()),
160 Next::Second(h_box_1) => {
161 let h_box_2_list = parse_h_box_list(&mut iter, &mut fonts).unwrap();
162 let h_box_2 = ds::HBox {
163 height: h_box_1.height,
164 width: h_box_1.width,
165 depth: h_box_1.depth,
166 shift_amount: h_box_1.shift_amount,
167 list: h_box_2_list,
168 glue_ratio: h_box_1.glue_ratio,
169 glue_order: h_box_1.glue_order,
170 };
171 Next::Third(h_box_1, h_box_2)
172 }
173 Next::Third(h_box_1, h_box_2) => {
174 let mut list = parse_v_box_list(&mut iter, &mut fonts).unwrap();
175 let h_box_3_list = match list.remove(1) {
176 ds::Vertical::HBox(h_box_3) => {
177 let mut list = h_box_3.list;
178 list.pop();
180 list.pop();
181 list.pop();
182 list
183 }
184 _ => panic!("expected h_box, got {:?}", list[1]),
185 };
186 let h_box_3 = ds::HBox {
187 height: h_box_1.height,
188 width: h_box_1.width,
189 depth: h_box_1.depth,
190 shift_amount: h_box_1.shift_amount,
191 list: h_box_3_list,
192 glue_ratio: h_box_1.glue_ratio,
193 glue_order: h_box_1.glue_order,
194 };
195 h_boxes.push(if hyphenate { h_box_3 } else { h_box_2 });
196 Next::First
197 }
198 };
199 }
200 (fonts, h_boxes)
201}
202
203pub fn build_vertical_lists(
212 tex_engine: &dyn TexEngine,
213 auxiliary_files: &HashMap<PathBuf, Vec<u8>>,
214 preamble: &str,
215 widths: &[common::Scaled],
216 contents: &mut dyn Iterator<Item = &String>,
217) -> (HashMap<String, u32>, Vec<ds::VBox>) {
218 let last_width = *widths.last().expect("widths is non-empty");
219 let box_template = if widths.len() == 1 {
220 format!(r"\vbox{{\noindent \hsize={} #1}}", last_width)
221 } else {
222 let parshape_specs: String = widths.iter().map(|w| format!("0pt {w} ")).collect();
223 format!(
224 r"\vbox{{\noindent \hsize={} \parshape {} {}#1}}",
225 last_width,
226 widths.len(),
227 parshape_specs,
228 )
229 };
230 let macro_calls: Vec<String> = contents.map(|s| format!(r#"\printBox{{{s}}}"#)).collect();
231 let tex_source_code = CONVERT_TEXT_TEMPLATE
232 .replace("<preamble>", preamble)
233 .replace("<box_template>", &box_template)
234 .replace("<print_calls>", ¯o_calls.join("\n\n"));
235 let output = tex_engine.run(&tex_source_code, auxiliary_files);
236 let segments = extract_texcraft_segments(&output);
237 let mut fonts: HashMap<String, u32> = Default::default();
238 let vlists = segments
239 .map(|s| parse_v_box(&mut TexOutputIter::new(s), &mut fonts).unwrap())
240 .collect();
241 (fonts, vlists)
242}
243
244struct TexOutputIter<'tex> {
245 s: &'tex str,
246 depth: usize,
247 line_number: usize,
248}
249
250impl<'tex> Iterator for TexOutputIter<'tex> {
251 type Item = (usize, &'tex str);
252
253 fn next(&mut self) -> Option<Self::Item> {
254 let (line_number, line, n) = self.peek_impl()?;
255 self.s = &self.s[n..];
256 self.line_number += 1;
257 Some((line_number, line))
258 }
259}
260
261impl<'tex> TexOutputIter<'tex> {
262 fn new(mut s: &'tex str) -> Self {
263 let mut line_number = 1_usize;
264 loop {
265 let line = s
266 .split_inclusive('\n')
267 .next()
268 .expect("still searching for start of output");
269 s = &s[line.len()..];
270 line_number += 1;
271 if !line.starts_with(r"> \box0=") {
272 continue;
273 }
274 return Self {
275 s,
276 depth: 0,
277 line_number,
278 };
279 }
280 }
281 fn inner(&self) -> Self {
282 Self {
283 s: self.s,
284 depth: self.depth + 1,
285 line_number: self.line_number,
286 }
287 }
288 fn peek(&mut self) -> Option<(usize, &'tex str)> {
289 let (line_number, line, _) = self.peek_impl()?;
290 Some((line_number, line))
291 }
292 fn peek_impl(&mut self) -> Option<(usize, &'tex str, usize)> {
293 loop {
294 let line = self.s.split_inclusive('\n').next()?;
295 let n = line.len();
296 let line = line.trim_end();
297 if line.is_empty() {
298 self.s = "";
299 return None;
300 }
301 let line_depth = line.chars().take_while(|&c| c == '.').count();
302 use std::cmp::Ordering::*;
303 match line_depth.cmp(&self.depth) {
304 Less => {
305 self.s = "";
306 return None;
307 }
308 Equal => {
309 return Some((self.line_number, &line[line_depth..], n));
310 }
311 Greater => {
312 self.s = &self.s[n..];
313 self.line_number += 1;
314 }
315 }
316 }
317 }
318}
319
320fn extract_texcraft_segments(mut s: &str) -> impl Iterator<Item = &str> {
324 std::iter::from_fn(move || {
325 loop {
327 let line = s.split_inclusive('\n').next()?;
328 s = &s[line.len()..];
329 if line.starts_with("Texcraft: begin") {
330 break;
331 }
332 }
333 let next = s;
334 let mut next_len = 0_usize;
335 loop {
337 let line = s.split_inclusive('\n').next()?;
338 s = &s[line.len()..];
339 next_len += line.len();
340 if line.starts_with("Texcraft: end") {
341 return Some(&next[0..next_len]);
342 }
343 }
344 })
345}
346
347#[derive(Debug, PartialEq)]
349pub enum ErrorKind {
350 EmptyHlist,
352 MissingHboxPrefix,
354 MissingHboxHeightDepthSeparator,
356 MissingHboxDepthWidthSeparator,
358 KernMissingWidth,
360 InvalidPenaltyValue,
362 RuleMissingWidthSeparator,
364 DiscretionaryExpectedReplacingKeyword,
366 DiscretionaryMissingReplaceCount,
368 DiscretionaryInvalidReplaceCount,
370 MissingCharAfterFont,
372 LigatureMissingOriginalChars,
374 LigatureMissingClosingParen,
376 EmptyVlist,
378 MissingVboxPrefix,
380 MissingVboxHeightDepthSeparator,
382 MissingVboxDepthWidthSeparator,
384 UnknownVlistKeyword,
386}
387
388impl std::fmt::Display for ErrorKind {
389 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
390 match self {
391 ErrorKind::EmptyHlist => write!(f, "iterator was empty when an hlist was expected"),
392 ErrorKind::MissingHboxPrefix => write!(f, r"first line did not start with \hbox("),
393 ErrorKind::MissingHboxHeightDepthSeparator => {
394 write!(
395 f,
396 r"hbox dimension spec missing '+' between height and depth"
397 )
398 }
399 ErrorKind::MissingHboxDepthWidthSeparator => {
400 write!(
401 f,
402 r"hbox dimension spec missing ')x' between depth and width"
403 )
404 }
405 ErrorKind::KernMissingWidth => write!(f, r"\kern item had no width value"),
406 ErrorKind::InvalidPenaltyValue => {
407 write!(f, r"\penalty value could not be parsed as an integer")
408 }
409 ErrorKind::RuleMissingWidthSeparator => {
410 write!(
411 f,
412 r"\rule item had no 'x' separating height/depth from width"
413 )
414 }
415 ErrorKind::DiscretionaryExpectedReplacingKeyword => {
416 write!(
417 f,
418 r"\discretionary item had unexpected word where 'replacing' was expected"
419 )
420 }
421 ErrorKind::DiscretionaryMissingReplaceCount => {
422 write!(f, r"\discretionary replacing item had no replacement count")
423 }
424 ErrorKind::DiscretionaryInvalidReplaceCount => {
425 write!(
426 f,
427 r"\discretionary replacing count could not be parsed as an integer"
428 )
429 }
430 ErrorKind::MissingCharAfterFont => {
431 write!(f, "font command was not followed by a character")
432 }
433 ErrorKind::LigatureMissingOriginalChars => {
434 write!(f, "ligature item had no original chars after '(ligature'")
435 }
436 ErrorKind::LigatureMissingClosingParen => {
437 write!(f, "ligature original chars did not end with ')'")
438 }
439 ErrorKind::EmptyVlist => write!(f, "iterator was empty when a vlist was expected"),
440 ErrorKind::MissingVboxPrefix => write!(f, r"first line did not start with \vbox("),
441 ErrorKind::MissingVboxHeightDepthSeparator => {
442 write!(
443 f,
444 r"vbox dimension spec missing '+' between height and depth"
445 )
446 }
447 ErrorKind::MissingVboxDepthWidthSeparator => {
448 write!(
449 f,
450 r"vbox dimension spec missing ')x' between depth and width"
451 )
452 }
453 ErrorKind::UnknownVlistKeyword => write!(f, "vlist contained an unhandled keyword"),
454 }
455 }
456}
457
458impl std::error::Error for ErrorKind {}
459
460#[derive(Debug, PartialEq)]
464pub struct Error {
465 pub kind: ErrorKind,
466 pub line_number: usize,
467}
468
469impl std::fmt::Display for Error {
470 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471 write!(f, "line {}: {}", self.line_number, self.kind)
472 }
473}
474
475impl std::error::Error for Error {}
476
477fn parse_disc_elem(
478 line: &str,
479 line_number: usize,
480 fonts: &mut HashMap<String, u32>,
481) -> Result<ds::DiscretionaryElem, Error> {
482 let (keyword, tail) = keyword_and_tail(line).unwrap();
483 match keyword {
484 "kern" => {
485 let mut words = tail.split_ascii_whitespace();
486 let width = parse_scaled(words.next().ok_or(Error {
487 kind: ErrorKind::KernMissingWidth,
488 line_number,
489 })?);
490 Ok(ds::DiscretionaryElem::Kern(ds::Kern {
491 kind: ds::KernKind::Normal,
492 width,
493 }))
494 }
495 font_name => {
496 use std::collections::hash_map::Entry;
497 let num_fonts: u32 = fonts.len().try_into().expect("no more than 2^32 fonts");
498 let font = match fonts.entry(font_name.to_string()) {
499 Entry::Occupied(e) => *e.get(),
500 Entry::Vacant(e) => {
501 e.insert(num_fonts);
502 num_fonts
503 }
504 };
505 let mut words = tail.split_ascii_whitespace();
506 let char = parse_char(words.next().ok_or(Error {
507 kind: ErrorKind::MissingCharAfterFont,
508 line_number,
509 })?);
510 Ok(ds::DiscretionaryElem::Char(ds::Char { char, font }))
511 }
512 }
513}
514
515fn parse_h_box(
519 iter: &mut TexOutputIter,
520 fonts: &mut HashMap<String, u32>,
521) -> Result<ds::HBox, Error> {
522 let mut h_box = {
523 let line_number_hint = iter.line_number;
524 let (line_number, line) = iter.next().ok_or(Error {
525 kind: ErrorKind::EmptyHlist,
526 line_number: line_number_hint,
527 })?;
528 let s = line.strip_prefix(r"\hbox(").ok_or(Error {
529 kind: ErrorKind::MissingHboxPrefix,
530 line_number,
531 })?;
532 let i = s.find('+').ok_or(Error {
533 kind: ErrorKind::MissingHboxHeightDepthSeparator,
534 line_number,
535 })?;
536 let height = parse_scaled(&s[..i]);
537 let s = &s[i + 1..];
538 let i = s.find(")x").ok_or(Error {
539 kind: ErrorKind::MissingHboxDepthWidthSeparator,
540 line_number,
541 })?;
542 let depth = parse_scaled(&s[..i]);
543 let rest = &s[i + 2..];
544 let (width_str, glue_set_str) = if let Some(j) = rest.find(", glue set ") {
545 (&rest[..j], Some(&rest[j + 11..]))
546 } else {
547 (rest, None)
548 };
549 let width = parse_scaled(width_str);
550 let (glue_ratio, glue_order) = glue_set_str
551 .map(parse_glue_set)
552 .unwrap_or((ds::GlueRatio::default(), common::GlueOrder::Normal));
553 ds::HBox {
554 height,
555 width,
556 depth,
557 glue_ratio,
558 glue_order,
559 ..Default::default()
560 }
561 };
562 let mut iter = iter.inner();
563 h_box.list = parse_h_box_list(&mut iter, fonts)?;
564 Ok(h_box)
565}
566
567fn parse_h_box_list(
568 iter: &mut TexOutputIter,
569 fonts: &mut HashMap<String, u32>,
570) -> Result<Vec<ds::Horizontal>, Error> {
571 let mut list = vec![];
572 while let Some((line_number, line)) = iter.peek() {
573 let Some((keyword, tail)) = keyword_and_tail(line) else {
574 break;
575 };
576 let mut consume_line = true;
577 let elem: ds::Horizontal = match keyword {
578 "glue" => ds::Glue {
579 kind: ds::GlueKind::Normal,
580 value: parse_glue_value(tail),
581 }
582 .into(),
583 "kern" => {
584 let mut words = tail.split_ascii_whitespace();
585 let width = parse_scaled(words.next().ok_or(Error {
586 kind: ErrorKind::KernMissingWidth,
587 line_number,
588 })?);
589 ds::Kern {
590 kind: ds::KernKind::Normal,
591 width,
592 }
593 .into()
594 }
595 "penalty" => {
596 let value: i32 = tail.trim().parse().map_err(|_| Error {
597 kind: ErrorKind::InvalidPenaltyValue,
598 line_number,
599 })?;
600 ds::Penalty(value).into()
601 }
602 "rule" => {
603 let i = tail.find('x').ok_or(Error {
604 kind: ErrorKind::RuleMissingWidthSeparator,
605 line_number,
606 })?;
607 ds::Rule {
608 width: parse_scaled(&tail[i + 1..]),
609 ..Default::default()
610 }
611 .into()
612 }
613 "discretionary" => {
614 let mut words = tail.split_ascii_whitespace();
616 let replace_count = match words.next() {
617 None => 0,
618 Some(replacing) => {
619 if replacing != "replacing" {
620 return Err(Error {
621 kind: ErrorKind::DiscretionaryExpectedReplacingKeyword,
622 line_number,
623 });
624 }
625 let count_str = words.next().ok_or(Error {
626 kind: ErrorKind::DiscretionaryMissingReplaceCount,
627 line_number,
628 })?;
629 count_str.parse().map_err(|_| Error {
630 kind: ErrorKind::DiscretionaryInvalidReplaceCount,
631 line_number,
632 })?
633 }
634 };
635 iter.next();
637 consume_line = false;
638 let mut pre_break = vec![];
640 {
641 let mut pre_iter = iter.inner();
642 while let Some((ln, line)) = pre_iter.peek() {
643 pre_break.push(parse_disc_elem(line, ln, fonts)?);
644 pre_iter.next();
645 }
646 }
647 let mut post_break = vec![];
650 while let Some((ln, line)) = iter.peek() {
651 let Some(post_line) = line.strip_prefix('|') else {
652 break;
653 };
654 iter.next();
655 post_break.push(parse_disc_elem(post_line, ln, fonts)?);
656 }
657 ds::Discretionary {
658 pre_break,
659 post_break,
660 replace_count,
661 }
662 .into()
663 }
664 "hbox" => {
665 consume_line = false;
666 parse_h_box(iter, fonts)?.into()
667 }
668 "vbox" => {
669 consume_line = false;
670 parse_v_box(iter, fonts)?.into()
671 }
672 font_name => {
673 use std::collections::hash_map::Entry;
674 let num_fonts: u32 = fonts.len().try_into().expect("no more than 2^32 fonts");
675 let font = match fonts.entry(font_name.to_string()) {
676 Entry::Occupied(occupied_entry) => *occupied_entry.get(),
677 Entry::Vacant(vacant_entry) => {
678 vacant_entry.insert(num_fonts);
679 num_fonts
680 }
681 };
682 let mut words = tail.split_ascii_whitespace();
683 let char = parse_char(words.next().ok_or(Error {
684 kind: ErrorKind::MissingCharAfterFont,
685 line_number,
686 })?);
687 if words.next() == Some("(ligature") {
688 let og_chars = words.next().ok_or(Error {
689 kind: ErrorKind::LigatureMissingOriginalChars,
690 line_number,
691 })?;
692 let og_chars = og_chars.strip_suffix(')').ok_or(Error {
693 kind: ErrorKind::LigatureMissingClosingParen,
694 line_number,
695 })?;
696 ds::Ligature {
697 included_left_boundary: false,
698 included_right_boundary: false,
699 char,
700 font,
701 original_chars: og_chars.into(),
702 }
703 .into()
704 } else {
705 ds::Char { char, font }.into()
706 }
707 }
708 };
709 list.push(elem);
710 if consume_line {
711 iter.next();
712 }
713 }
714 Ok(list)
715}
716
717fn parse_v_box(
719 iter: &mut TexOutputIter,
720 fonts: &mut HashMap<String, u32>,
721) -> Result<ds::VBox, Error> {
722 let mut vlist = {
723 let line_number_hint = iter.line_number;
724 let (line_number, line) = iter.next().ok_or(Error {
725 kind: ErrorKind::EmptyVlist,
726 line_number: line_number_hint,
727 })?;
728 let s = line.strip_prefix(r"\vbox(").ok_or(Error {
729 kind: ErrorKind::MissingVboxPrefix,
730 line_number,
731 })?;
732 let i = s.find('+').ok_or(Error {
733 kind: ErrorKind::MissingVboxHeightDepthSeparator,
734 line_number,
735 })?;
736 let _height = parse_scaled(&s[..i]);
737 let s = &s[i + 1..];
738 let i = s.find(")x").ok_or(Error {
739 kind: ErrorKind::MissingVboxDepthWidthSeparator,
740 line_number,
741 })?;
742 let _depth = parse_scaled(&s[..i]);
743 let _width = parse_scaled(&s[i + 2..]);
744 let (width, height, depth) = (
746 common::Scaled::ZERO,
747 common::Scaled::ZERO,
748 common::Scaled::ZERO,
749 );
750 ds::VBox {
751 height,
752 width,
753 depth,
754 shift_amount: common::Scaled::ZERO,
755 list: vec![],
756 glue_ratio: Default::default(),
757 glue_order: common::GlueOrder::Normal,
758 }
759 };
760 let mut iter = iter.inner();
761 vlist.list = parse_v_box_list(&mut iter, fonts)?;
762 Ok(vlist)
763}
764
765fn parse_v_box_list(
766 iter: &mut TexOutputIter,
767 fonts: &mut HashMap<String, u32>,
768) -> Result<Vec<ds::Vertical>, Error> {
769 let mut list = vec![];
770 while let Some((line_number, line)) = iter.peek() {
771 let Some((keyword, tail)) = keyword_and_tail(line) else {
772 break;
773 };
774 let mut consume_line = true;
775 let elem: ds::Vertical = match keyword {
776 "hbox" => {
777 consume_line = false;
778 parse_h_box(iter, fonts)?.into()
779 }
780 "penalty" => {
781 let value: i32 = tail.trim().parse().map_err(|_| Error {
782 kind: ErrorKind::InvalidPenaltyValue,
783 line_number,
784 })?;
785 ds::Penalty(value).into()
786 }
787 "glue" => ds::Vertical::Glue(ds::Glue {
788 kind: ds::GlueKind::Normal,
789 value: parse_glue_value(tail),
790 }),
791 _ => {
792 return Err(Error {
793 kind: ErrorKind::UnknownVlistKeyword,
794 line_number,
795 })
796 }
797 };
798 list.push(elem);
799 if consume_line {
800 iter.next();
801 }
802 }
803 Ok(list)
804}
805
806fn keyword_and_tail(s: &str) -> Option<(&str, &str)> {
807 let mut c = s.chars();
808 if c.next() != Some('\\') {
809 return None;
810 }
811 let mut keyword_len = 0_usize;
812 for next in c {
813 if next.is_alphabetic() {
814 keyword_len += next.len_utf8();
815 } else {
816 break;
817 }
818 }
819 Some((&s[1..1 + keyword_len], s[1 + keyword_len..].trim()))
820}
821
822fn parse_glue_value(spec: &str) -> common::Glue {
827 let spec = if spec.starts_with('(') {
828 let close = spec.find(')').expect("named glue has ')'");
829 spec[close + 1..].trim_start()
830 } else {
831 spec.trim_start()
832 };
833 let mut words = spec.split_ascii_whitespace();
834 let width = parse_scaled(words.next().expect("glue has a width"));
835 let (stretch, stretch_order) = if words.next() == Some("plus") {
836 parse_glue_amount(words.next().expect("glue has stretch after plus"))
837 } else {
838 (common::Scaled::ZERO, common::GlueOrder::Normal)
839 };
840 let (shrink, shrink_order) = if words.next() == Some("minus") {
841 parse_glue_amount(words.next().expect("glue has shrink after minus"))
842 } else {
843 (common::Scaled::ZERO, common::GlueOrder::Normal)
844 };
845 common::Glue {
846 width,
847 stretch,
848 stretch_order,
849 shrink,
850 shrink_order,
851 }
852}
853
854fn parse_glue_amount(s: &str) -> (common::Scaled, common::GlueOrder) {
856 if let Some(s) = s.strip_suffix("filll") {
857 (parse_scaled(s), common::GlueOrder::Filll)
858 } else if let Some(s) = s.strip_suffix("fill") {
859 (parse_scaled(s), common::GlueOrder::Fill)
860 } else if let Some(s) = s.strip_suffix("fil") {
861 (parse_scaled(s), common::GlueOrder::Fil)
862 } else {
863 (parse_scaled(s), common::GlueOrder::Normal)
864 }
865}
866
867fn parse_glue_set(s: &str) -> (ds::GlueRatio, common::GlueOrder) {
869 let (neg, s) = if let Some(s) = s.strip_prefix("- ") {
870 (true, s)
871 } else {
872 (false, s)
873 };
874 let (s, order) = if let Some(s) = s.strip_suffix("filll") {
875 (s, common::GlueOrder::Filll)
876 } else if let Some(s) = s.strip_suffix("fill") {
877 (s, common::GlueOrder::Fill)
878 } else if let Some(s) = s.strip_suffix("fil") {
879 (s, common::GlueOrder::Fil)
880 } else {
881 (s, common::GlueOrder::Normal)
882 };
883 let mut ratio = ds::GlueRatio::from_float_str(s).expect("glue set ratio is a float");
884 if neg {
885 ratio.num.0 *= -1;
886 }
887 (ratio, order)
888}
889
890fn parse_char(s: &str) -> char {
891 let mut cs = s.chars();
892 match cs.next().expect("char has one character") {
893 '^' => {
894 assert_eq!(cs.next(), Some('^'));
895 let raw_c = cs.next().expect("char of the form ^^X") as u32;
896 if let Some(raw_c) = raw_c.checked_sub(64) {
897 raw_c
898 } else {
899 raw_c + 64
900 }
901 .try_into()
902 .expect("TeX describes a valid character")
903 }
904 c => c,
905 }
906}
907
908fn parse_scaled(s: &str) -> common::Scaled {
909 let (neg, s) = match s.strip_prefix('-') {
910 Some(s) => (true, s),
911 None => (false, s),
912 };
913 let mut parts = s.split('.');
914 let i: i32 = parts
915 .next()
916 .expect("scaled has an integer part")
917 .parse()
918 .expect("integer part is an integer");
919 let mut f = [0_u8; 17];
920 for (k, c) in parts
921 .next()
922 .expect("scaled has a fractional part")
923 .chars()
924 .enumerate()
925 {
926 f[k] = c
927 .to_digit(10)
928 .expect("fractional part are digits")
929 .try_into()
930 .expect("digits are in the range [0,10) and always fit in u8");
931 }
932 let f = common::Scaled::from_decimal_digits(&f);
933 let sc = common::Scaled::new(i, f, common::ScaledUnit::Point).unwrap_or_else(|_| {
934 eprintln!(
935 "scaled number '{s}pt' is too big to parse; replacing with the largest scaled value {}",
936 common::Scaled::MAX_DIMEN
937 );
938 common::Scaled::MAX_DIMEN
939 });
940 if neg {
941 -sc
942 } else {
943 sc
944 }
945}
946
947const CONVERT_TEXT_TEMPLATE: &str = r"
948
949% User provided preamble.
950<preamble>
951
952% After showing a box, TeX stops and waits for user input.
953% The following command suppresses that behavior.
954\nonstopmode
955
956% Output the box description to the terminal, from which we'll read it.
957\tracingonline=1
958
959% Output up to 1 million nodes.
960\showboxbreadth=1000000
961% Output up to 100 nested boxes.
962\showboxdepth=100
963
964% Prints the contents on its own line in the terminal.
965\def\fullLineMessage#1{
966 {
967 \newlinechar=`@
968 \message{@#1@}
969 }
970}
971
972\def\printBox#1{
973 % Put the content we want to see in box 0.
974 \setbox0=<box_template>
975 % Add a start marker so we know where to begin in the log
976 \fullLineMessage{Texcraft: begin}
977 % Show the box!
978 \showbox0
979
980 % Add a start marker so we know where to end in the log
981 \fullLineMessage{Texcraft: end}
982}
983
984<print_calls>
985
986We add some text at the end.
987
988\end
989";
990
991#[cfg(test)]
992mod tests {
993 use super::*;
994
995 fn parse_hbox_lang(source: &str) -> ds::HBox {
996 let mut list = crate::lang::parse_horizontal_list(source).unwrap();
997 assert_eq!(list.len(), 1);
998 match list.remove(0) {
999 ds::Horizontal::HBox(hbox) => hbox,
1000 other => panic!("expected hbox, got {other:?}"),
1001 }
1002 }
1003
1004 fn parse_vbox_lang(source: &str) -> ds::VBox {
1005 let mut list = crate::lang::parse_horizontal_list(source).unwrap();
1006 assert_eq!(list.len(), 1);
1007 match list.remove(0) {
1008 ds::Horizontal::VBox(mut vbox) => {
1009 vbox.width = common::Scaled::ZERO;
1011 vbox.height = common::Scaled::ZERO;
1012 vbox.depth = common::Scaled::ZERO;
1013 vbox.shift_amount = common::Scaled::ZERO;
1014 vbox
1015 }
1016 other => panic!("expected vbox, got {other:?}"),
1017 }
1018 }
1019
1020 struct MockTexEngine(String);
1021
1022 impl TexEngine for MockTexEngine {
1023 fn run(&self, _: &str, _: &HashMap<PathBuf, Vec<u8>>) -> String {
1024 self.0.clone()
1025 }
1026 }
1027
1028 #[test]
1029 fn test_build_horizontal_lists() {
1030 let log = include_str!("hbox_template_1.log");
1031
1032 let tex_engine = MockTexEngine(log.to_string());
1033 let (got_fonts, got_list) = build_horizontal_lists(
1034 &tex_engine,
1035 &Default::default(),
1036 &"",
1037 &mut vec!["".to_string()].iter(),
1038 false,
1039 );
1040
1041 let want_list = parse_hbox_lang(
1042 r#"
1043 hbox(
1044 height=6.94444pt,
1045 width=56.66678pt,
1046 content=[
1047 chars("Min")
1048 kern(-0.27779pt)
1049 chars("t")
1050 glue(3.33333pt, 1.66666pt, 1.11111pt)
1051 chars("and")
1052 glue(3.33333pt, 1.66666pt, 1.11111pt)
1053 chars("me")
1054 ]
1055 )
1056 "#,
1057 );
1058 let want_fonts = {
1059 let mut m = HashMap::new();
1060 m.insert("customFont".to_string(), 0);
1061 m
1062 };
1063
1064 assert_eq!(got_list, vec![want_list]);
1065 assert_eq!(got_fonts, want_fonts);
1066 }
1067
1068 #[test]
1069 fn test_discretionary_pre_and_post_break() {
1070 let input = r"> \box0=x
1071\hbox(6.94444+0.0)x10.0
1072.\discretionary replacing 3
1073..\tenrm d
1074..\tenrm e
1075..\tenrm f
1076.|\tenrm g
1077.|\tenrm h
1078";
1079 let mut fonts = Default::default();
1080 let got = parse_h_box(&mut TexOutputIter::new(input), &mut fonts).unwrap();
1081 let want = parse_hbox_lang(
1082 r#"hbox(
1083 height=6.94444pt,
1084 width=10.0pt,
1085 content=[
1086 disc(
1087 pre_break=[chars("def", font=0)],
1088 post_break=[chars("gh", font=0)],
1089 replace_count=3,
1090 )
1091 ]
1092 )"#,
1093 );
1094 assert_eq!(got, want);
1095 }
1096
1097 #[test]
1098 fn test_build_vertical_lists() {
1099 let log = r#"
1100This is TeX, Version 3.141592653 (TeX Live 2024) (preloaded format=tex)
1101(./test.tex
1102
1103Texcraft: begin
1104> \box0=
1105\vbox(18.94444+0.0)x41.0
1106.\hbox(6.94444+0.0)x41.0, glue set 0.26662
1107..\tenrm M
1108..\tenrm i
1109..\tenrm n
1110..\kern-0.27779
1111..\tenrm t
1112..\glue 3.33333 plus 1.66666 minus 1.11111
1113..\tenrm a
1114..\tenrm n
1115..\tenrm d
1116..\glue(\rightskip) 0.0
1117.\penalty 300
1118.\glue(\baselineskip) 7.69446
1119.\hbox(4.30554+0.0)x41.0, glue set 28.2222fil
1120..\tenrm m
1121..\tenrm e
1122..\penalty 10000
1123..\glue(\parfillskip) 0.0 plus 1.0fil
1124..\glue(\rightskip) 0.0
1125
1126! OK.
1127\printBox ...Message {Texcraft: begin} \showbox 0
1128 \par \fullLineMessage {Tex...
1129l.31 \printBox{Mint and me}
1130
1131
1132Texcraft: end
1133 )
1134(see the transcript file for additional information)
1135No pages of output.
1136Transcript written on test.log.
1137"#;
1138 let tex_engine = MockTexEngine(log.to_string());
1139 let (got_fonts, got_list) = build_vertical_lists(
1140 &tex_engine,
1141 &Default::default(),
1142 &"",
1143 &[common::Scaled::ONE * 41],
1144 &mut vec!["".to_string()].iter(),
1145 );
1146
1147 let want_list = parse_vbox_lang(
1148 r#"
1149 vbox(
1150 height=18.94444pt,
1151 width=41.0pt,
1152 content=[
1153 hbox(
1154 height=6.94444pt,
1155 width=41.0pt,
1156 glue_ratio="0.26662",
1157 content=[
1158 chars("Min")
1159 kern(-0.27779pt)
1160 chars("t")
1161 glue(3.33333pt, 1.66666pt, 1.11111pt)
1162 chars("and")
1163 glue()
1164 ]
1165 )
1166 penalty(300)
1167 glue(7.69446pt)
1168 hbox(
1169 height=4.30554pt,
1170 width=41.0pt,
1171 glue_ratio="28.2222",
1172 glue_order="fil",
1173 content=[
1174 chars("me")
1175 penalty(10000)
1176 glue(0.0pt, 1.0fil, 0.0pt)
1177 glue()
1178 ]
1179 )
1180 ]
1181 )
1182 "#,
1183 );
1184 let want_fonts = {
1185 let mut m = HashMap::new();
1186 m.insert("tenrm".to_string(), 0);
1187 m
1188 };
1189
1190 assert_eq!(got_list, vec![want_list]);
1191 assert_eq!(got_fonts, want_fonts);
1192 }
1193 #[test]
1194 fn test_build_vertical_lists_2() {
1195 let log = r#"
1196This is TeX, Version 3.141592653 (TeX Live 2024) (preloaded format=tex)
1197(./test.tex
1198
1199Texcraft: begin
1200> \box0=
1201\vbox(6.83331+0.0)x41.0
1202.\hbox(6.83331+0.0)x41.0
1203..\tenrm A
1204..\hbox(6.83331+0.0)x48.08336
1205...\tenrm B
1206...\vbox(6.83331+0.0)x41.0
1207....\hbox(6.83331+0.0)x41.0, glue set 6.13887fil
1208.....\hbox(0.0+0.0)x20.0
1209.....\tenrm C
1210.....\hbox(6.83331+0.0)x7.6389
1211......\tenrm D
1212.....\penalty 10000
1213.....\glue(\parfillskip) 0.0 plus 1.0fil
1214.....\glue(\rightskip) 0.0
1215..\penalty 10000
1216..\glue(\parfillskip) 0.0 plus 1.0fil
1217..\glue(\rightskip) 0.0
1218..\rule(*+*)x5.0
1219
1220! OK.
1221\printBox ...Message {Texcraft: begin} \showbox 0
1222 \par \fullLineMessage {Tex...
1223l.31 \printBox{Mint and me}
1224
1225
1226Texcraft: end
1227 )
1228(see the transcript file for additional information)
1229No pages of output.
1230Transcript written on test.log.
1231"#;
1232
1233 let tex_engine = MockTexEngine(log.to_string());
1234 let (got_fonts, got_list) = build_vertical_lists(
1235 &tex_engine,
1236 &Default::default(),
1237 &"",
1238 &[common::Scaled::ONE * 41],
1239 &mut vec!["".to_string()].iter(),
1240 );
1241
1242 let want_list = parse_vbox_lang(
1243 r#"
1244 vbox(
1245 height=6.83331pt,
1246 width=41.0pt,
1247 content=[
1248 hbox(
1249 height=6.83331pt,
1250 width=41.0pt,
1251 content=[
1252 chars("A")
1253 hbox(
1254 height=6.83331pt,
1255 width=48.08336pt,
1256 content=[
1257 chars("B")
1258 vbox(
1259 # todo
1260 # height=6.83331pt,
1261 # width=41.0pt,
1262 content=[
1263 hbox(
1264 height=6.83331pt,
1265 width=41.0pt,
1266 glue_ratio="6.13887",
1267 glue_order="fil",
1268 content=[
1269 hbox(width=20.0pt)
1270 chars("C")
1271 hbox(
1272 height=6.83331pt,
1273 width=7.6389pt,
1274 content=[chars("D")]
1275 )
1276 penalty(10000)
1277 glue(0.0pt, 1.0fil, 0.0pt)
1278 glue()
1279 ]
1280 )
1281 ]
1282 )
1283 ]
1284 )
1285 penalty(10000)
1286 glue(0.0pt, 1.0fil, 0.0pt)
1287 glue()
1288 rule(height="running", width=5.0pt, depth="running")
1289 ]
1290 )
1291 ]
1292 )
1293 "#,
1294 );
1295 let want_fonts = {
1296 let mut m = HashMap::new();
1297 m.insert("tenrm".to_string(), 0);
1298 m
1299 };
1300
1301 assert_eq!(got_fonts, want_fonts);
1302 assert_eq!(got_list, vec![want_list]);
1303 }
1304
1305 macro_rules! test_parse_hlist_error {
1306 ($(($name:ident, $input:expr, $expected:expr)),* $(,)?) => [$(
1307 #[test]
1308 fn $name() {
1309 let err = parse_h_box(&mut TexOutputIter::new($input), &mut Default::default())
1310 .unwrap_err();
1311 assert_eq!(err, $expected);
1312 }
1313 )*];
1314 }
1315
1316 test_parse_hlist_error![
1317 (
1318 test_empty_hlist,
1319 r"> \box0=
1320",
1321 Error {
1322 kind: ErrorKind::EmptyHlist,
1323 line_number: 2
1324 }
1325 ),
1326 (
1327 test_missing_hbox_prefix,
1328 r"> \box0=
1329\vbox(6.0+0.0)x10.0
1330",
1331 Error {
1332 kind: ErrorKind::MissingHboxPrefix,
1333 line_number: 2
1334 }
1335 ),
1336 (
1337 test_missing_height_depth_separator,
1338 r"> \box0=
1339\hbox(6.94444 no plus here)x10.0
1340",
1341 Error {
1342 kind: ErrorKind::MissingHboxHeightDepthSeparator,
1343 line_number: 2
1344 }
1345 ),
1346 (
1347 test_missing_depth_width_separator,
1348 r"> \box0=
1349\hbox(6.94444+0.0 no depth width)
1350",
1351 Error {
1352 kind: ErrorKind::MissingHboxDepthWidthSeparator,
1353 line_number: 2
1354 }
1355 ),
1356 (
1357 test_kern_missing_width,
1358 r"> \box0=
1359\hbox(6.94444+0.0)x10.0
1360.\kern
1361",
1362 Error {
1363 kind: ErrorKind::KernMissingWidth,
1364 line_number: 3
1365 }
1366 ),
1367 (
1368 test_invalid_penalty_value,
1369 r"> \box0=
1370\hbox(6.94444+0.0)x10.0
1371.\penalty abc
1372",
1373 Error {
1374 kind: ErrorKind::InvalidPenaltyValue,
1375 line_number: 3
1376 }
1377 ),
1378 (
1379 test_rule_missing_width_separator,
1380 r"> \box0=
1381\hbox(6.94444+0.0)x10.0
1382.\rule (*+*) 5.0
1383",
1384 Error {
1385 kind: ErrorKind::RuleMissingWidthSeparator,
1386 line_number: 3
1387 }
1388 ),
1389 (
1390 test_discretionary_expected_replacing_keyword,
1391 r"> \box0=
1392\hbox(6.94444+0.0)x10.0
1393.\discretionary wrong 3
1394",
1395 Error {
1396 kind: ErrorKind::DiscretionaryExpectedReplacingKeyword,
1397 line_number: 3
1398 }
1399 ),
1400 (
1401 test_discretionary_missing_replace_count,
1402 r"> \box0=
1403\hbox(6.94444+0.0)x10.0
1404.\discretionary replacing
1405",
1406 Error {
1407 kind: ErrorKind::DiscretionaryMissingReplaceCount,
1408 line_number: 3
1409 }
1410 ),
1411 (
1412 test_discretionary_invalid_replace_count,
1413 r"> \box0=
1414\hbox(6.94444+0.0)x10.0
1415.\discretionary replacing abc
1416",
1417 Error {
1418 kind: ErrorKind::DiscretionaryInvalidReplaceCount,
1419 line_number: 3
1420 }
1421 ),
1422 (
1423 test_missing_char_after_font,
1424 r"> \box0=
1425\hbox(6.94444+0.0)x10.0
1426.\tenrm
1427",
1428 Error {
1429 kind: ErrorKind::MissingCharAfterFont,
1430 line_number: 3
1431 }
1432 ),
1433 (
1434 test_ligature_missing_original_chars,
1435 r"> \box0=
1436\hbox(6.94444+0.0)x10.0
1437.\tenrm f (ligature
1438",
1439 Error {
1440 kind: ErrorKind::LigatureMissingOriginalChars,
1441 line_number: 3
1442 }
1443 ),
1444 (
1445 test_ligature_missing_closing_paren,
1446 r"> \box0=
1447\hbox(6.94444+0.0)x10.0
1448.\tenrm f (ligature fi
1449",
1450 Error {
1451 kind: ErrorKind::LigatureMissingClosingParen,
1452 line_number: 3
1453 }
1454 ),
1455 ];
1456
1457 macro_rules! test_parse_vlist_error {
1458 ($(($name:ident, $input:expr, $expected:expr)),* $(,)?) => [$(
1459 #[test]
1460 fn $name() {
1461 let err = parse_v_box(&mut TexOutputIter::new($input), &mut Default::default())
1462 .unwrap_err();
1463 assert_eq!(err, $expected);
1464 }
1465 )*];
1466 }
1467
1468 test_parse_vlist_error![
1469 (
1470 test_empty_vlist,
1471 r"> \box0=
1472",
1473 Error {
1474 kind: ErrorKind::EmptyVlist,
1475 line_number: 2
1476 }
1477 ),
1478 (
1479 test_missing_vbox_prefix,
1480 r"> \box0=
1481\hbox(6.94444+0.0)x10.0
1482",
1483 Error {
1484 kind: ErrorKind::MissingVboxPrefix,
1485 line_number: 2
1486 }
1487 ),
1488 (
1489 test_missing_vbox_height_depth_separator,
1490 r"> \box0=
1491\vbox(6.94444 no plus here)x10.0
1492",
1493 Error {
1494 kind: ErrorKind::MissingVboxHeightDepthSeparator,
1495 line_number: 2
1496 }
1497 ),
1498 (
1499 test_missing_vbox_depth_width_separator,
1500 r"> \box0=
1501\vbox(6.94444+0.0 no depth width)
1502",
1503 Error {
1504 kind: ErrorKind::MissingVboxDepthWidthSeparator,
1505 line_number: 2
1506 }
1507 ),
1508 (
1509 test_vlist_invalid_penalty_value,
1510 r"> \box0=
1511\vbox(6.94444+0.0)x10.0
1512.\penalty abc
1513",
1514 Error {
1515 kind: ErrorKind::InvalidPenaltyValue,
1516 line_number: 3
1517 }
1518 ),
1519 (
1520 test_unknown_vlist_keyword,
1521 r"> \box0=
1522\vbox(6.94444+0.0)x10.0
1523.\unknown stuff
1524",
1525 Error {
1526 kind: ErrorKind::UnknownVlistKeyword,
1527 line_number: 3
1528 }
1529 ),
1530 ];
1531}