1use boxworks::ds;
9use common::font;
10
11pub struct Params {
12 pub space_factor_codes: SpaceFactorCodes,
13 pub space_skip: common::Glue,
14 pub extra_space_skip: common::Glue,
15}
16
17impl Params {
18 pub fn tex(&self) -> String {
20 let Params {
21 space_factor_codes,
22 space_skip,
23 extra_space_skip,
24 } = self;
25 _ = space_factor_codes;
26 format!(
27 r"
28 \spaceskip={space_skip}
29 \xspaceskip={extra_space_skip}
30 "
31 )
32 }
33}
34
35impl Default for Params {
36 fn default() -> Self {
37 Self::plain_tex_defaults()
38 }
39}
40
41impl Params {
42 pub fn plain_tex_defaults() -> Self {
43 Self {
44 space_factor_codes: SpaceFactorCodes::plain_tex_defaults(),
45 space_skip: common::Glue::ZERO,
46 extra_space_skip: common::Glue::ZERO,
47 }
48 }
49}
50
51pub struct TextPreprocessor {
52 current_font: font::Id,
53 space_factor: SpaceFactor,
54 pub params: Params,
55}
56
57impl TextPreprocessor {
58 pub fn new(params: Params) -> Self {
59 Self {
60 current_font: font::Id::NULL,
61 space_factor: Default::default(),
62 params,
63 }
64 }
65}
66
67pub struct SpaceFactorCodes(pub [i32; 256]);
68
69impl Default for SpaceFactorCodes {
70 fn default() -> Self {
71 Self::plain_tex_defaults()
72 }
73}
74
75impl SpaceFactorCodes {
76 pub fn plain_tex_defaults() -> Self {
77 let mut a = [1000_i32; 256];
78 for (c, value) in [
79 (')', 0),
81 ('\'', 0),
82 (']', 0),
83 ('.', 3000),
85 ('?', 3000),
86 ('!', 3000),
87 (':', 2000),
88 (';', 1500),
89 (',', 1250),
90 ] {
91 a[c as usize] = value;
92 }
93 for c in 'A'..='Z' {
94 a[c as usize] = 999;
96 }
97 Self(a)
98 }
99}
100
101#[derive(Debug, PartialEq, Eq, Clone, Copy)]
102pub struct SpaceFactor(pub i32);
103
104impl Default for SpaceFactor {
105 fn default() -> Self {
106 Self(1000)
107 }
108}
109
110impl SpaceFactor {
111 fn adjust(&mut self, c: char, codes: &SpaceFactorCodes) {
112 let new: i32 = codes.0.get(c as usize).copied().unwrap_or(1000);
114 if new > 0 && new <= 1000 {
115 self.0 = new;
116 } else if new > 1000 {
117 if self.0 < 1000 {
118 self.0 = 1000
119 } else {
120 self.0 = new
121 }
122 }
123 }
124}
125
126impl TextPreprocessor {
127 pub fn activate_font(&mut self, font: font::Id) {
128 self.current_font = font;
129 }
130
131 pub fn new_paragraph(&mut self) {
132 self.space_factor = Default::default();
133 }
134
135 pub fn add_word<Font: font::TextBuilder>(
136 &mut self,
137 font_repo: &font::Repo<Font>,
138 word: &str,
139 list: &mut Vec<ds::Horizontal>,
140 ) {
141 let font = font_repo.get(self.current_font);
143 for elem in font.build_text(word.chars(), Default::default()) {
144 use font::TextItem::*;
145 match elem {
146 Char(c) => {
147 list.push(
148 ds::Char {
149 char: c,
150 font: self.current_font,
151 }
152 .into(),
153 );
154 if c == '-' {
157 list.push(ds::Discretionary::default().into());
158 }
159 }
160 Kern(kern) => {
161 list.push(
162 ds::Kern {
163 width: kern,
164 kind: ds::KernKind::Normal,
165 }
166 .into(),
167 );
168 }
169 Ligature {
170 c,
171 original,
172 includes_left_boundary,
173 includes_right_boundary,
174 } => {
175 let ins_disc = original.as_ref().ends_with('-');
176 list.push(
177 ds::Ligature {
178 char: c,
179 font: self.current_font,
180 original_chars: original,
181 includes_left_boundary,
182 includes_right_boundary,
183 }
184 .into(),
185 );
186 if ins_disc {
189 list.push(ds::Discretionary::default().into());
190 }
191 }
192 }
193 }
194 for c in word.chars() {
198 self.space_factor.adjust(c, &self.params.space_factor_codes);
199 }
200 }
201
202 pub fn add_space<Font: font::Format>(
203 &mut self,
204 font_repo: &font::Repo<Font>,
205 list: &mut Vec<ds::Horizontal>,
206 ) {
207 let g = if self.space_factor == SpaceFactor::default() {
208 if !self.params.space_skip.is_zero() {
210 self.params.space_skip
211 } else {
212 font_repo.get(self.current_font).default_space()
214 }
215 } else {
216 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 let mut g = font_repo.get(self.current_font).default_space();
224 if self.space_factor.0 >= 2000 {
226 g.width += font_repo.get(self.current_font).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 pub fn add_text<Font: font::Format + font::TextBuilder>(
237 &mut self,
238 font_repo: &font::Repo<Font>,
239 text: &str,
240 list: &mut Vec<ds::Horizontal>,
241 ) {
242 self.new_paragraph();
243 let mut pending_space = text.chars().next().unwrap_or(' ').is_ascii_whitespace();
244 for word in text.split_ascii_whitespace() {
245 if pending_space {
246 self.add_space(font_repo, list);
247 }
248 let word = word.trim_matches(' ');
249 self.add_word(font_repo, word.trim_matches(' '), list);
250 pending_space = true;
251 }
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use std::collections::HashMap;
258
259 use super::*;
260 use boxworks_testing;
261 use boxworks_testing::assert_box_eq;
262 use boxworks_testing::assert_box_lossy_eq;
263
264 macro_rules! preprocessor_tests {
265 (
266 $namespace: ident,
267 $tfm: ident,
268 $( (
269 $name: ident,
270 $input: expr,
271 $want: expr,
272 $( params: Params {
273 $( $param_name: ident: $param_value: expr, )+
274 }, )?
275 ), )+ ) => {
276 mod $namespace {
277 use super::*;
278 $(
279 #[test]
280 fn $name() {
281 let tfm = super::$tfm;
282 let input = $input;
283 let want = $want;
284 let params = Params {
285 $( $(
286 $param_name: $param_value,
287 )+ )?
288 .. Params::plain_tex_defaults()
289 };
290 run_preprocessor_test(tfm, params, input, want)
291 }
292 )+
293 }
294 };
295 }
296
297 const TFM_CMR10: &'static [u8] = include_bytes!("../../tfm/corpus/computer-modern/cmr10.tfm");
298
299 preprocessor_tests!(
300 cmr10,
301 TFM_CMR10,
302 (
303 basic,
304 "second",
305 r#"
306 chars("second")
307 "#,
308 ),
309 (
310 basic_with_space,
311 "sec ond",
312 r#"
313 chars("sec")
314 glue(3.33333pt, 1.66666pt, 1.11111pt)
315 chars("ond")
316 "#,
317 ),
318 (
319 kern_ao,
320 "AO",
321 r#"
322 chars("A")
323 kern(-0.27779pt)
324 chars("O")
325 "#,
326 ),
327 (
328 kern_av,
329 "AV",
330 r#"
331 chars("A")
332 kern(-1.11113pt)
333 chars("V")
334 "#,
335 ),
336 (
337 ligature_1,
338 "ff",
339 r#"
340 lig("\u{b}", "ff")
341 "#,
342 ),
343 (
344 ligature_2,
345 "ffi",
346 r#"
347 lig("\u{e}", "ffi")
348 "#,
349 ),
350 (
351 ragged_right,
352 "a b. c",
353 r##"
354 chars("a")
355 glue(3.33298pt, 0.0pt, 0.0pt)
356 chars("b.")
357 glue(5.0pt, 0.0pt, 0.0pt)
358 chars("c")
359 "##,
360 params: Params {
361 space_skip: common::Glue {
362 width: common::Scaled::parse_from_string("3.33298pt").unwrap(),
363 ..Default::default()
364 },
365 extra_space_skip: common::Glue {
366 width: common::Scaled::parse_from_string("5.0pt").unwrap(),
367 ..Default::default()
368 },
369 },
370 ),
371 );
372
373 macro_rules! spacing_tests {
374 ( $( ( $name: ident, $input: expr, $want: expr, ), )+ ) => {
375 mod spacing {
376 $(
377 #[test]
378 fn $name() {
379 let tfm = super::TFM_CMR10;
380 let input = format!["{} a", $input];
381 let want = format![r#"
382 chars("{}")
383 {}
384 chars("a")
385 "#, $input, $want];
386 super::run_preprocessor_test(tfm, Default::default(), &input, &want)
387 }
388 )+
389 }
390 };
391 }
392
393 spacing_tests!(
394 (default_1, "a;", "glue(3.33333pt, 2.49998pt, 0.74074pt)",),
396 (default_2, "a,", "glue(3.33333pt, 2.08331pt, 0.88889pt)",),
397 (default_3, "a.", "glue(4.44444pt, 4.99997pt, 0.37036pt)",),
398 (default_4, "a:", "glue(4.44444pt, 3.33331pt, 0.55556pt)",),
399 (
405 adjust_space_factor_zero_zero,
406 "))",
407 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
408 ),
409 (
410 adjust_space_factor_zero_small,
411 ")A",
412 "glue(3.33333pt, 1.66498pt, 1.11221pt)",
413 ),
414 (
415 adjust_space_factor_zero_normal,
416 ")a",
417 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
418 ),
419 (
420 adjust_space_factor_zero_large,
421 ").",
422 "glue(4.44444pt, 4.99997pt, 0.37036pt)",
423 ),
424 (
425 adjust_space_factor_small_zero,
426 "A)",
427 "glue(3.33333pt, 1.66498pt, 1.11221pt)",
428 ),
429 (
430 adjust_space_factor_small_small,
431 "AA",
432 "glue(3.33333pt, 1.66498pt, 1.11221pt)",
433 ),
434 (
435 adjust_space_factor_small_normal,
436 "Aa",
437 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
438 ),
439 (
440 adjust_space_factor_small_large,
441 "A.",
442 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
443 ),
444 (
445 adjust_space_factor_normal_zero,
446 "a)",
447 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
448 ),
449 (
450 adjust_space_factor_normal_small,
451 "aA",
452 "glue(3.33333pt, 1.66498pt, 1.11221pt)",
453 ),
454 (
455 adjust_space_factor_normal_normal,
456 "aa",
457 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
458 ),
459 (
460 adjust_space_factor_normal_large,
461 "a.",
462 "glue(4.44444pt, 4.99997pt, 0.37036pt)",
463 ),
464 (
465 adjust_space_factor_large_zero,
466 ".)",
467 "glue(4.44444pt, 4.99997pt, 0.37036pt)",
468 ),
469 (
470 adjust_space_factor_large_small,
471 ".A",
472 "glue(3.33333pt, 1.66498pt, 1.11221pt)",
473 ),
474 (
475 adjust_space_factor_large_normal,
476 ".a",
477 "glue(3.33333pt, 1.66666pt, 1.11111pt)",
478 ),
479 (
480 adjust_space_factor_large_large,
481 "..",
482 "glue(4.44444pt, 4.99997pt, 0.37036pt)",
483 ),
484 );
485
486 const TFM_SMFEBSL: &'static [u8] = include_bytes!("../../tfm/corpus/ctan/smfebsl10-3.tfm");
487
488 preprocessor_tests!(
489 smfebsl,
490 TFM_SMFEBSL,
491 (
492 basic_with_space,
493 "sec ond",
494 r#"
495 chars("sec")
496 glue(4.78204pt, 2.39102pt, 1.19551pt)
497 chars("on")
498 kern(-0.49814pt)
499 chars("d")
500 "#,
501 ),
502 (
503 numbers_start_of_word,
504 "123B",
505 r##"
506 lig("$", "", includes_left_boundary="true")
507 chars("123")
508 lig("#", "")
509 chars("B")
510 "##,
511 ),
512 (
513 numbers_mid_word,
514 "A123B",
515 r##"
516 chars("A")
517 lig("$", "")
518 chars("123")
519 lig("#", "")
520 chars("B")
521 "##,
522 ),
523 (
524 numbers_end_of_word,
525 "A123",
526 r##"
527 chars("A")
528 lig("$", "")
529 chars("123")
530 lig("#", "", includes_right_boundary="true")
531 "##,
532 ),
533 );
534
535 fn run_preprocessor_test(tfm_bytes: &[u8], params: Params, input: &str, want: &str) {
536 if std::env::var("TEXCRAFT_VERIFY").unwrap_or_default() == "tex" {
537 use std::path::PathBuf;
538 let mut auxiliary_files: HashMap<PathBuf, Vec<u8>> = Default::default();
539 auxiliary_files.insert("customFont.tfm".into(), tfm_bytes.to_vec());
540 let preamble = format!(
541 r"
542 {}
543 \font \customFont customFont
544 \customFont
545 ",
546 params.tex(),
547 );
548 let mut tex_engine = boxworks::tex::new_tex_engine_binary("tex".to_string()).unwrap();
549 let (_, mut tex_got) = boxworks::tex::build_horizontal_lists(
550 tex_engine.as_mut(),
551 &auxiliary_files,
552 &preamble,
553 &mut [input.to_string()].iter(),
554 false,
555 );
556 let tex_got = tex_got.remove(0).list;
557 assert_box_lossy_eq!(want, tex_got);
561 return;
562 }
563
564 let tfm_font = tfm::Font::build_from_bytes(tfm_bytes)
565 .expect("tfm file is valid")
566 .0;
567 let mut font_repo: font::Repo<tfm::Font> = Default::default();
568 let font_id = font_repo.register(tfm_font);
569
570 let mut tp = TextPreprocessor::new(params);
571 tp.activate_font(font_id);
572 let mut got = vec![];
573 for word in input.split_inclusive(' ') {
574 tp.add_word(&font_repo, word.trim_matches(' '), &mut got);
575 if word.ends_with(" ") {
576 tp.add_space(&font_repo, &mut got);
577 }
578 }
579
580 assert_box_eq!(got, want);
581 }
582}