boxworks_text/
lib.rs

1//! # Boxworks text preprocessor
2//!
3//! This crate implements the logic that converts text (words and spaces)
4//! into horizontal list elements.
5//! It is implemented in the Chief Executive chapter in Knuth's
6//! TeX (starting in TeX.2021.1029).
7
8use boxworks::ds;
9use std::collections::HashMap;
10use tfm::ligkern;
11
12#[derive(Debug)]
13struct Font {
14    default_space: common::Glue,
15    extra_space: common::Scaled,
16    lig_kern_program: tfm::ligkern::CompiledProgram,
17}
18
19pub struct Params {
20    pub space_factor_codes: SpaceFactorCodes,
21    pub space_skip: common::Glue,
22    pub extra_space_skip: common::Glue,
23}
24
25impl Params {
26    /// Output the parameters in TeX format.
27    pub fn tex(&self) -> String {
28        let Params {
29            space_factor_codes,
30            space_skip,
31            extra_space_skip,
32        } = self;
33        _ = space_factor_codes;
34        format!(
35            r"
36            \spaceskip={space_skip}
37            \xspaceskip={extra_space_skip}
38        "
39        )
40    }
41}
42
43impl Default for Params {
44    fn default() -> Self {
45        Self::plain_tex_defaults()
46    }
47}
48
49impl Params {
50    pub fn plain_tex_defaults() -> Self {
51        Self {
52            space_factor_codes: SpaceFactorCodes::plain_tex_defaults(),
53            space_skip: common::Glue::ZERO,
54            extra_space_skip: common::Glue::ZERO,
55        }
56    }
57}
58pub struct TextPreprocessorImpl {
59    fonts: Vec<Font>,
60    // TODO: should be initialized to the null font
61    // TODO: should current_font be some kind of specific font identifier type.
62    current_font: u32,
63    space_factor: SpaceFactor,
64    pub params: Params,
65}
66
67impl TextPreprocessorImpl {
68    pub fn new(params: Params) -> Self {
69        Self {
70            fonts: vec![],
71            current_font: 0,
72            space_factor: Default::default(),
73            params,
74        }
75    }
76}
77
78pub struct SpaceFactorCodes(pub [i32; 256]);
79
80impl Default for SpaceFactorCodes {
81    fn default() -> Self {
82        Self::plain_tex_defaults()
83    }
84}
85
86impl SpaceFactorCodes {
87    pub fn plain_tex_defaults() -> Self {
88        let mut a = [1000_i32; 256];
89        for (c, value) in [
90            // From plain.tex
91            (')', 0),
92            ('\'', 0),
93            (']', 0),
94            // From \nonfrenchspacing in plain.tex
95            ('.', 3000),
96            ('?', 3000),
97            ('!', 3000),
98            (':', 2000),
99            (';', 1500),
100            (',', 1250),
101        ] {
102            a[c as usize] = value;
103        }
104        for c in 'A'..='Z' {
105            // INITTEX
106            a[c as usize] = 999;
107        }
108        Self(a)
109    }
110}
111
112#[derive(Debug, PartialEq, Eq, Clone, Copy)]
113pub struct SpaceFactor(pub i32);
114
115impl Default for SpaceFactor {
116    fn default() -> Self {
117        Self(1000)
118    }
119}
120
121impl SpaceFactor {
122    fn adjust(&mut self, c: char, codes: &SpaceFactorCodes) {
123        // TeX.2021.1034
124        let new: i32 = codes.0.get(c as usize).copied().unwrap_or(1000);
125        if new > 0 && new <= 1000 {
126            self.0 = new;
127        } else if new > 1000 {
128            if self.0 < 1000 {
129                self.0 = 1000
130            } else {
131                self.0 = new
132            }
133        }
134    }
135}
136
137impl TextPreprocessorImpl {
138    pub fn activate_font(&mut self, font: u32) {
139        self.current_font = font;
140    }
141}
142
143impl boxworks::TextPreprocessor for TextPreprocessorImpl {
144    fn new_paragraph(&mut self) {
145        self.space_factor = Default::default();
146    }
147
148    fn add_word(&mut self, word: &str, list: &mut Vec<ds::Horizontal>) {
149        let font = &self.fonts[self.current_font as usize];
150
151        struct Emitter<'a>(&'a mut Vec<ds::Horizontal>, u32);
152        impl<'a> ligkern::Emitter for Emitter<'a> {
153            fn emit_character(&mut self, c: char) {
154                self.0.push(
155                    ds::Char {
156                        char: c,
157                        font: self.1,
158                    }
159                    .into(),
160                );
161                // TeX.2021.1035
162                // TODO: \hyphenchar
163                if c == '-' {
164                    self.0.push(ds::Discretionary::default().into());
165                }
166            }
167            fn emit_kern(&mut self, kern: common::Scaled) {
168                self.0.push(
169                    ds::Kern {
170                        width: kern,
171                        kind: ds::KernKind::Normal,
172                    }
173                    .into(),
174                );
175            }
176            fn emit_ligature(&mut self, ligature: ligkern::Ligature) {
177                let ins_disc = ligature.original.as_ref().ends_with('-');
178                self.0.push(
179                    ds::Ligature {
180                        included_left_boundary: false,
181                        included_right_boundary: false,
182                        char: ligature.c,
183                        font: self.1,
184                        original_chars: ligature.original,
185                    }
186                    .into(),
187                );
188                // TeX.2021.1035
189                // TODO: \hyphenchar
190                if ins_disc {
191                    self.0.push(ds::Discretionary::default().into());
192                }
193            }
194        }
195
196        let mut e = Emitter(list, self.current_font);
197        font.lig_kern_program.run(word, &mut e);
198        // TODO: consider merging this loop with the loop in the lig/kern program.
199        // We can change the run method to accept a callback that is invoked for
200        // each character.
201        for c in word.chars() {
202            self.space_factor.adjust(c, &self.params.space_factor_codes);
203        }
204    }
205
206    fn add_space(&mut self, list: &mut Vec<ds::Horizontal>) {
207        let g = if self.space_factor == SpaceFactor::default() {
208            // TeX.2021.1041
209            if !self.params.space_skip.is_zero() {
210                self.params.space_skip
211            } else {
212                // TeX.2021.1042
213                self.fonts[self.current_font as usize].default_space
214            }
215        } else {
216            // TeX.2021.1043
217            if self.space_factor.0 >= 2000 && !self.params.extra_space_skip.is_zero() {
218                self.params.extra_space_skip
219            } else if !self.params.space_skip.is_zero() {
220                self.params.space_skip
221            } else {
222                // TeX.2021.1042
223                let mut g = self.fonts[self.current_font as usize].default_space;
224                // TeX.2021.1044
225                if self.space_factor.0 >= 2000 {
226                    g.width += self.fonts[self.current_font as usize].extra_space;
227                }
228                g.stretch = g.stretch.xn_over_d(self.space_factor.0, 1000).unwrap().0;
229                g.shrink = g.shrink.xn_over_d(1000, self.space_factor.0).unwrap().0;
230                g
231            }
232        };
233        list.push(ds::Horizontal::Glue(g.into()));
234    }
235}
236
237impl TextPreprocessorImpl {
238    pub fn register_font(
239        &mut self,
240        id: u32,
241        tfm_file: &tfm::File,
242        lig_kern_program: tfm::ligkern::CompiledProgram,
243    ) {
244        assert_eq!(id as usize, self.fonts.len());
245        self.fonts.push(Font {
246            default_space: common::Glue {
247                width: tfm_file
248                    .named_param_scaled(tfm::NamedParameter::Space)
249                    .unwrap(),
250                stretch: tfm_file
251                    .named_param_scaled(tfm::NamedParameter::Stretch)
252                    .unwrap(),
253                stretch_order: common::GlueOrder::Normal,
254                shrink: tfm_file
255                    .named_param_scaled(tfm::NamedParameter::Shrink)
256                    .unwrap(),
257                shrink_order: common::GlueOrder::Normal,
258            },
259            extra_space: tfm_file
260                .named_param_scaled(tfm::NamedParameter::ExtraSpace)
261                .unwrap(),
262            lig_kern_program,
263        });
264    }
265}
266
267#[derive(Debug, Default)]
268pub struct TfmFontRepo {
269    fonts: HashMap<u32, tfm::File>,
270}
271
272impl TfmFontRepo {
273    pub fn register_font(&mut self, id: u32, tfm_file: tfm::File) {
274        assert_eq!(id as usize, self.fonts.len());
275        self.fonts.insert(id, tfm_file);
276    }
277}
278
279impl boxworks::FontRepo for TfmFontRepo {
280    fn width(&self, c: char, font: u32) -> Option<common::Scaled> {
281        self.fonts[&font].width_utf8(c)
282    }
283    fn height(&self, c: char, font: u32) -> Option<common::Scaled> {
284        self.fonts[&font].height_utf8(c)
285    }
286    fn depth(&self, c: char, font: u32) -> Option<common::Scaled> {
287        self.fonts[&font].depth_utf8(c)
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use boxworks::TextPreprocessor;
295    use boxworks_testing;
296    use boxworks_testing::assert_box_eq;
297
298    macro_rules! preprocessor_tests {
299        (
300            $namespace: ident,
301            $tfm: ident,
302            $( (
303                $name: ident,
304                $input: expr,
305                $want: expr,
306                $( params: Params {
307                    $( $param_name: ident: $param_value: expr, )+
308                }, )?
309            ), )+ ) => {
310                mod $namespace {
311                    use super::*;
312                    $(
313                        #[test]
314                        fn $name() {
315                            let tfm = super::$tfm;
316                            let input = $input;
317                            let want = $want;
318                            let params = Params {
319                                $( $(
320                                    $param_name: $param_value,
321                                )+ )?
322                                .. Params::plain_tex_defaults()
323                            };
324                            run_preprocessor_test(tfm, params, input, want)
325                        }
326                    )+
327                }
328        };
329    }
330
331    const TFM_CMR10: &'static [u8] = include_bytes!("../../tfm/corpus/computer-modern/cmr10.tfm");
332
333    preprocessor_tests!(
334        cmr10,
335        TFM_CMR10,
336        (
337            basic,
338            "second",
339            r#"
340                chars("second", font=0)
341            "#,
342        ),
343        (
344            basic_with_space,
345            "sec ond",
346            r#"
347                chars("sec", font=0)
348                glue(3.33333pt, 1.66666pt, 1.11111pt)
349                chars("ond", font=0)
350            "#,
351        ),
352        (
353            kern_ao,
354            "AO",
355            r#"
356                chars("A", font=0)
357                kern(-0.27779pt)
358                chars("O", font=0)
359            "#,
360        ),
361        (
362            kern_av,
363            "AV",
364            r#"
365                chars("A", font=0)
366                kern(-1.11113pt)
367                chars("V", font=0)
368            "#,
369        ),
370        (
371            ligature_1,
372            "ff",
373            r#"
374                lig("\u{b}", "ff", font=0)
375            "#,
376        ),
377        (
378            ligature_2,
379            "ffi",
380            r#"
381                lig("\u{e}", "ffi", font=0)
382            "#,
383        ),
384        (
385            ragged_right,
386            "a b. c",
387            r##"
388                chars("a", font=0)
389                glue(3.33298pt, 0.0pt, 0.0pt)
390                chars("b.", font=0)
391                glue(5.0pt, 0.0pt, 0.0pt)
392                chars("c", font=0)
393            "##,
394            params: Params {
395                space_skip: common::Glue {
396                    width: common::Scaled::parse_from_string("3.33298pt").unwrap(),
397                    ..Default::default()
398                },
399                extra_space_skip: common::Glue {
400                    width: common::Scaled::parse_from_string("5.0pt").unwrap(),
401                    ..Default::default()
402                },
403            },
404        ),
405    );
406
407    macro_rules! spacing_tests {
408        ( $( ( $name: ident, $input: expr, $want: expr, ), )+ ) => {
409            mod spacing {
410                $(
411                    #[test]
412                    fn $name() {
413                        let tfm = super::TFM_CMR10;
414                        let input = format!["{} a", $input];
415                        let want =  format![r#"
416                            chars("{}", font=0)
417                            {}
418                            chars("a", font=0)
419                        "#, $input, $want];
420                        super::run_preprocessor_test(tfm, Default::default(), &input, &want)
421                    }
422                )+
423            }
424        };
425    }
426
427    spacing_tests!(
428        // These tests are testing the default space factors in plain.tex.
429        (default_1, "a;", "glue(3.33333pt, 2.49998pt, 0.74074pt)",),
430        (default_2, "a,", "glue(3.33333pt, 2.08331pt, 0.88889pt)",),
431        (default_3, "a.", "glue(4.44444pt, 4.99997pt, 0.37036pt)",),
432        (default_4, "a:", "glue(4.44444pt, 3.33331pt, 0.55556pt)",),
433        // The next tests are for the adjust_space_factor function.
434        // The SF is adjusted based on both its current value and the SF
435        // of the next character. We first test 16 possible cases where
436        // current and next are in the following 4 classes: zero, small
437        // (less than 1000), normal (1000), large (greater than 1000).
438        (
439            adjust_space_factor_zero_zero,
440            "))",
441            "glue(3.33333pt, 1.66666pt, 1.11111pt)",
442        ),
443        (
444            adjust_space_factor_zero_small,
445            ")A",
446            "glue(3.33333pt, 1.66498pt, 1.11221pt)",
447        ),
448        (
449            adjust_space_factor_zero_normal,
450            ")a",
451            "glue(3.33333pt, 1.66666pt, 1.11111pt)",
452        ),
453        (
454            adjust_space_factor_zero_large,
455            ").",
456            "glue(4.44444pt, 4.99997pt, 0.37036pt)",
457        ),
458        (
459            adjust_space_factor_small_zero,
460            "A)",
461            "glue(3.33333pt, 1.66498pt, 1.11221pt)",
462        ),
463        (
464            adjust_space_factor_small_small,
465            "AA",
466            "glue(3.33333pt, 1.66498pt, 1.11221pt)",
467        ),
468        (
469            adjust_space_factor_small_normal,
470            "Aa",
471            "glue(3.33333pt, 1.66666pt, 1.11111pt)",
472        ),
473        (
474            adjust_space_factor_small_large,
475            "A.",
476            "glue(3.33333pt, 1.66666pt, 1.11111pt)",
477        ),
478        (
479            adjust_space_factor_normal_zero,
480            "a)",
481            "glue(3.33333pt, 1.66666pt, 1.11111pt)",
482        ),
483        (
484            adjust_space_factor_normal_small,
485            "aA",
486            "glue(3.33333pt, 1.66498pt, 1.11221pt)",
487        ),
488        (
489            adjust_space_factor_normal_normal,
490            "aa",
491            "glue(3.33333pt, 1.66666pt, 1.11111pt)",
492        ),
493        (
494            adjust_space_factor_normal_large,
495            "a.",
496            "glue(4.44444pt, 4.99997pt, 0.37036pt)",
497        ),
498        (
499            adjust_space_factor_large_zero,
500            ".)",
501            "glue(4.44444pt, 4.99997pt, 0.37036pt)",
502        ),
503        (
504            adjust_space_factor_large_small,
505            ".A",
506            "glue(3.33333pt, 1.66498pt, 1.11221pt)",
507        ),
508        (
509            adjust_space_factor_large_normal,
510            ".a",
511            "glue(3.33333pt, 1.66666pt, 1.11111pt)",
512        ),
513        (
514            adjust_space_factor_large_large,
515            "..",
516            "glue(4.44444pt, 4.99997pt, 0.37036pt)",
517        ),
518    );
519
520    const TFM_SMFEBSL: &'static [u8] = include_bytes!("../../tfm/corpus/ctan/smfebsl10-3.tfm");
521
522    preprocessor_tests!(
523        smfebsl,
524        TFM_SMFEBSL,
525        (
526            basic_with_space,
527            "sec ond",
528            r#"
529                chars("sec", font=0)
530                glue(4.78204pt, 2.39102pt, 1.19551pt)
531                chars("on", font=0)
532                kern(-0.49814pt)
533                chars("d", font=0)
534            "#,
535        ),
536        (
537            numbers_start_of_word,
538            "123B",
539            r##"
540                lig("$", "|", font=0)
541                chars("123", font=0)
542                lig("#", "", font=0)
543                chars("B", font=0)
544            "##,
545        ),
546        (
547            numbers_mid_word,
548            "A123B",
549            r##"
550                chars("A", font=0)
551                lig("$", "", font=0)
552                chars("123", font=0)
553                lig("#", "", font=0)
554                chars("B", font=0)
555            "##,
556        ),
557        /*
558        (
559            numbers_end_of_word,
560            "A123",
561            r##"
562                chars("A", font=0)
563                lig("$", "", font=0)
564                chars("123", font=0)
565                lig("#", "|", font=0)
566            "##,
567        ),
568        */
569    );
570
571    fn run_preprocessor_test(tfm_bytes: &[u8], params: Params, input: &str, want: &str) {
572        let mut tfm_file = tfm::File::deserialize(tfm_bytes).0.unwrap();
573        let lig_kern_program =
574            tfm::ligkern::CompiledProgram::compile_from_tfm_file(&mut tfm_file).0;
575
576        let mut tp = TextPreprocessorImpl::new(params);
577        tp.register_font(0, &tfm_file, lig_kern_program);
578        tp.activate_font(0);
579        let mut got = vec![];
580        for word in input.split_inclusive(' ') {
581            tp.add_word(word.trim_matches(' '), &mut got);
582            if word.ends_with(" ") {
583                tp.add_space(&mut got);
584            }
585        }
586
587        assert_box_eq!(got, want);
588    }
589}