1use std::collections::{BTreeMap, HashMap};
13
14use crate::{
15 ligkern,
16 pl::ast::{DesignSize, ParameterNumber},
17 Char, ExtensibleRecipe, FixWord, Header, NamedParameter, NextLargerProgram,
18 NextLargerProgramWarning,
19};
20
21pub mod ast;
22pub mod cst;
23mod error;
24pub use error::*;
25
26pub const MAX_LIG_KERN_INSTRUCTIONS: u16 = (i16::MAX as u16) - 257;
49
50#[derive(Clone, Default, PartialEq, Eq, Debug)]
52pub struct CharDimensions {
53 pub width: Option<FixWord>,
54 pub height: Option<FixWord>,
55 pub depth: Option<FixWord>,
56 pub italic_correction: Option<FixWord>,
57}
58
59#[derive(Clone, Debug, PartialEq, Eq)]
61pub enum CharTag {
62 Ligature(u16),
63 List(Char),
64 Extension(ExtensibleRecipe),
65}
66
67impl CharTag {
68 pub fn ligature(&self) -> Option<u16> {
69 match self {
70 CharTag::Ligature(l) => Some(*l),
71 _ => None,
72 }
73 }
74 pub fn list(&self) -> Option<Char> {
75 match self {
76 CharTag::List(c) => Some(*c),
77 _ => None,
78 }
79 }
80 pub fn extension(&self) -> Option<ExtensibleRecipe> {
81 match self {
82 CharTag::Extension(u) => Some(u.clone()),
83 CharTag::Ligature(_) | CharTag::List(_) => None,
84 }
85 }
86}
87
88#[derive(PartialEq, Eq, Debug)]
90pub struct File {
91 pub header: Header,
92 pub design_units: FixWord,
93 pub char_dimens: BTreeMap<Char, CharDimensions>,
94 pub char_tags: BTreeMap<Char, CharTag>,
95 pub unset_char_tags: BTreeMap<Char, u8>,
97 pub lig_kern_program: ligkern::lang::Program,
98 pub params: Vec<FixWord>,
99
100 pub additional_widths: Vec<FixWord>,
111 pub additional_heights: Vec<FixWord>,
113 pub additional_depths: Vec<FixWord>,
115 pub additional_italics: Vec<FixWord>,
117}
118
119impl Default for File {
120 fn default() -> Self {
121 Self {
122 header: Header::pl_default(),
123 design_units: FixWord::ONE,
124 char_dimens: Default::default(),
125 char_tags: Default::default(),
126 unset_char_tags: Default::default(),
127 lig_kern_program: Default::default(),
128 params: Default::default(),
129 additional_widths: vec![],
130 additional_heights: vec![],
131 additional_depths: vec![],
132 additional_italics: vec![],
133 }
134 }
135}
136
137impl File {
138 pub fn from_pl_source_code(source: &str) -> (File, Vec<ParseWarning>) {
140 let (ast, mut errors) = ast::Ast::from_pl_source_code(source);
141 let file = File::from_ast(ast, &mut errors);
142 (file, errors)
143 }
144
145 pub fn lig_kern_entrypoints(&self, include_orphans: bool) -> HashMap<Char, u16> {
147 self.char_tags
148 .iter()
149 .filter(|(c, _)| self.char_dimens.contains_key(c) || include_orphans)
150 .filter_map(|d| match d.1 {
151 CharTag::Ligature(l) => Some((*d.0, *l)),
152 _ => None,
153 })
154 .collect()
155 }
156
157 pub fn clear_lig_kern_data(&mut self) {
159 self.char_tags = self
161 .char_tags
162 .iter()
163 .filter_map(|(c, tag)| match tag {
164 CharTag::Ligature(_) => None,
165 _ => Some((*c, tag.clone())),
166 })
167 .collect();
168 self.lig_kern_program = Default::default();
169 }
170
171 pub fn from_ast(ast: ast::Ast, errors: &mut Vec<error::ParseWarning>) -> File {
173 let mut file: File = Default::default();
174 let mut lig_kern_precedes = false;
175
176 let mut next_larger_span = HashMap::<Char, std::ops::Range<usize>>::new();
177
178 for node in ast.0 {
179 match node {
180 ast::Root::Checksum(v) => {
181 file.header.checksum = Some(v.data);
182 }
183 ast::Root::DesignSize(v) => {
184 if let DesignSize::Valid(design_size) = v.data {
185 file.header.design_size = design_size;
186 }
187 }
188 ast::Root::DesignUnits(v) => {
189 file.design_units = v.data;
190 }
191 ast::Root::CodingScheme(v) => {
192 file.header.character_coding_scheme = Some(v.data);
193 }
194 ast::Root::Family(v) => {
195 file.header.font_family = Some(v.data);
196 }
197 ast::Root::Face(v) => {
198 file.header.face = Some(v.data);
199 }
200 ast::Root::SevenBitSafeFlag(v) => {
201 file.header.seven_bit_safe = Some(v.data);
202 }
203 ast::Root::Header(v) => match v.left.0.checked_sub(18) {
204 None => errors.push(ParseWarning {
205 span: v.left_span.clone(),
206 knuth_pltotf_offset: Some(v.left_span.end),
207 kind: ParseWarningKind::HeaderIndexIsTooSmall,
208 }),
209 Some(i) => {
210 let i = i as usize;
211 if file.header.additional_data.len() <= i {
212 file.header.additional_data.resize(i + 1, 0);
213 }
214 file.header.additional_data[i] = v.right;
215 }
216 },
217 ast::Root::FontDimension(b) => {
218 for node in b.children {
219 let (number, value) = match node {
220 ast::FontDimension::NamedParam(named_param, v) => {
221 (named_param.number() as usize, v.data)
222 }
223 ast::FontDimension::IndexedParam(v) => (v.left.0 as usize, v.right),
224 ast::FontDimension::Comment(_) => continue,
225 };
226 let index = number - 1;
227 if file.params.len() < number {
228 file.params.resize(number, Default::default());
229 }
230 file.params[index] = value;
231 }
232 }
233 ast::Root::LigTable(b) => {
234 for node in b.children {
235 let mut insert_lig_kern_instruction = |instruction, span| {
236 if file.lig_kern_program.instructions.len()
237 < MAX_LIG_KERN_INSTRUCTIONS as usize
238 {
239 file.lig_kern_program.instructions.push(instruction);
240 } else {
241 errors.push(error::ParseWarning {
243 span,
244 knuth_pltotf_offset: None,
245 kind: ParseWarningKind::LigTableIsTooBig,
246 });
247 }
248 };
249 match node {
250 ast::LigTable::Label(v) => {
251 let u: u16 = file.lig_kern_program.instructions.len().try_into().expect("lig_kern_instructions.len()<= MAX_LIG_KERN_INSTRUCTIONS which is a u16");
252 match v.data {
253 ast::LigTableLabel::Char(c) => {
254 file.char_tags.insert(c, CharTag::Ligature(u));
256 }
257 ast::LigTableLabel::BoundaryChar => {
258 file.lig_kern_program.left_boundary_char_entrypoint =
259 Some(u);
260 }
261 }
262 lig_kern_precedes = false;
263 }
264 ast::LigTable::Lig(post_lig_operation, v) => {
265 insert_lig_kern_instruction(
266 ligkern::lang::Instruction {
267 next_instruction: Some(0),
268 right_char: v.left,
269 operation: ligkern::lang::Operation::Ligature {
270 char_to_insert: v.right,
271 post_lig_operation,
272 post_lig_tag_invalid: false,
273 },
274 },
276 v.left_span,
277 );
278 lig_kern_precedes = true;
279 }
280 ast::LigTable::Kern(v) => {
281 insert_lig_kern_instruction(
282 ligkern::lang::Instruction {
283 next_instruction: Some(0),
284 right_char: v.left,
285 operation: ligkern::lang::Operation::Kern(v.right),
286 },
288 v.left_span,
289 );
290 lig_kern_precedes = true;
291 }
292 ast::LigTable::Stop(_) => {
293 if lig_kern_precedes {
294 file.lig_kern_program
295 .instructions
296 .last_mut()
297 .unwrap()
298 .next_instruction = None;
299 } else {
300 }
302 lig_kern_precedes = false;
303 }
304 ast::LigTable::Skip(v) => {
305 if lig_kern_precedes {
306 file.lig_kern_program
307 .instructions
308 .last_mut()
309 .unwrap()
310 .next_instruction = Some(v.data.0);
311 } else {
312 }
314 lig_kern_precedes = false;
315 }
316 ast::LigTable::Comment(_) => {}
317 }
318 }
319 }
320 ast::Root::BoundaryChar(v) => {
321 file.lig_kern_program.right_boundary_char = Some(v.data);
322 }
323 ast::Root::Character(b) => {
324 let char_dimens = file.char_dimens.entry(b.data).or_default();
325 for node in b.children {
326 match node {
327 ast::Character::Width(v) => {
328 if let Some(additional_width) =
329 char_dimens.width.replace(v.data.unwrap_or(FixWord::ZERO))
330 {
331 file.additional_widths.push(additional_width);
332 }
333 }
334 ast::Character::Height(v) => {
335 if let Some(additional_height) = char_dimens.height.replace(v.data)
336 {
337 file.additional_heights.push(additional_height);
338 }
339 }
340 ast::Character::Depth(v) => {
341 if let Some(additional_depth) = char_dimens.depth.replace(v.data) {
342 file.additional_depths.push(additional_depth);
343 }
344 }
345 ast::Character::ItalicCorrection(v) => {
346 if let Some(additional_italic) =
347 char_dimens.italic_correction.replace(v.data)
348 {
349 file.additional_italics.push(additional_italic);
350 }
351 }
352 ast::Character::NextLarger(c) => {
353 file.char_tags.insert(b.data, CharTag::List(c.data));
355 next_larger_span.insert(b.data, c.data_span);
356 }
357 ast::Character::ExtensibleCharacter(e) => {
358 let mut recipe: ExtensibleRecipe = Default::default();
359 for node in e.children {
360 match node {
361 ast::ExtensibleCharacter::Top(v) => {
362 recipe.top = Some(v.data)
363 }
364 ast::ExtensibleCharacter::Middle(v) => {
365 recipe.middle = Some(v.data)
366 }
367 ast::ExtensibleCharacter::Bottom(v) => {
368 recipe.bottom = Some(v.data)
369 }
370 ast::ExtensibleCharacter::Replicated(v) => {
371 recipe.rep = v.data
372 }
373 ast::ExtensibleCharacter::Comment(_) => {}
374 }
375 }
376 file.char_tags.insert(b.data, CharTag::Extension(recipe));
378 }
379 ast::Character::Comment(_) => {}
380 }
381 }
382 }
383 ast::Root::Comment(_) => {}
384 }
385 }
386
387 errors.sort_by_key(|w| {
392 w.knuth_pltotf_offset
393 .expect("all warnings generated so far have an offset populated")
394 });
395
396 if let Some(final_instruction) = file.lig_kern_program.instructions.last_mut() {
398 if final_instruction.next_instruction == Some(0) {
399 final_instruction.next_instruction = None;
400 }
401 }
402
403 let lig_kern_seven_bit_safe = {
405 for err in crate::ligkern::CompiledProgram::compile(
406 &file.lig_kern_program,
407 file.header.design_size,
408 &[],
409 file.lig_kern_entrypoints(true), )
411 .1
412 {
413 errors.push(ParseWarning {
414 span: 0..0, knuth_pltotf_offset: None,
416 kind: ParseWarningKind::CycleInLigKernProgram(err),
417 });
418 file.clear_lig_kern_data();
419 }
420 file.lig_kern_program
421 .is_seven_bit_safe(file.lig_kern_entrypoints(false))
422 };
423
424 let next_larger_seven_bit_safe = {
426 let (program, next_larger_warnings) = NextLargerProgram::new(
427 file.char_tags
428 .iter()
429 .filter_map(|(c, t)| t.list().map(|next_larger| (*c, next_larger))),
430 |c| file.char_dimens.contains_key(&c),
431 false,
432 );
433 for warning in next_larger_warnings {
434 match &warning {
435 NextLargerProgramWarning::NonExistentCharacter {
436 original: _,
437 next_larger,
438 } => {
439 file.char_dimens.insert(
440 *next_larger,
441 CharDimensions {
442 width: Some(FixWord::ZERO),
443 ..Default::default()
444 },
445 );
446 }
447 NextLargerProgramWarning::InfiniteLoop { original, .. } => {
448 let discriminant = file
449 .char_tags
450 .remove(original)
451 .and_then(|t| t.list())
452 .expect("this char has a NEXTLARGER SPEC");
453 file.unset_char_tags.insert(*original, discriminant.0);
454 }
455 }
456 let span = next_larger_span
457 .get(&warning.bad_char())
458 .cloned()
459 .expect("every char with a next larger tag had a NEXTLARGER AST node");
460 errors.push(ParseWarning {
461 span,
462 knuth_pltotf_offset: None,
463 kind: ParseWarningKind::CycleInNextLargerProgram(warning),
464 });
465 }
466 program.is_seven_bit_safe()
467 };
468
469 let mut extensible_seven_bit_safe = true;
471 file.char_tags
472 .iter()
473 .filter_map(|(c, t)| t.extension().map(|t| (c, t)))
474 .for_each(|(c, e)| {
475 if c.is_seven_bit() && !e.is_seven_bit() {
476 extensible_seven_bit_safe = false;
477 }
478 for c in e.chars() {
479 if let std::collections::btree_map::Entry::Vacant(v) = file.char_dimens.entry(c)
480 {
481 v.insert(Default::default());
482 };
484 }
485 });
486
487 let seven_bit_safe =
488 lig_kern_seven_bit_safe && next_larger_seven_bit_safe && extensible_seven_bit_safe;
489 if file.header.seven_bit_safe == Some(true) && !seven_bit_safe {
490 errors.push(ParseWarning {
491 span: 0..0, knuth_pltotf_offset: None,
493 kind: ParseWarningKind::NotReallySevenBitSafe,
494 });
495 }
496 file.header.seven_bit_safe = Some(seven_bit_safe);
497
498 file
499 }
500}
501
502impl From<crate::File> for File {
503 fn from(tfm_file: crate::File) -> Self {
504 let char_dimens: BTreeMap<Char, CharDimensions> = tfm_file
505 .char_dimens
506 .iter()
507 .map(|(c, info)| {
508 (
509 *c,
510 CharDimensions {
511 width: match info.width_index {
512 super::WidthIndex::Invalid => None,
513 super::WidthIndex::Valid(n) => {
514 tfm_file.widths.get(n.get() as usize).copied()
515 }
516 },
517 height: if info.height_index == 0 {
518 None
519 } else {
520 tfm_file.heights.get(info.height_index as usize).copied()
521 },
522 depth: if info.depth_index == 0 {
523 None
524 } else {
525 tfm_file.depths.get(info.depth_index as usize).copied()
526 },
527 italic_correction: if info.italic_index == 0 {
528 None
529 } else {
530 tfm_file
531 .italic_corrections
532 .get(info.italic_index as usize)
533 .copied()
534 },
535 },
536 )
537 })
538 .collect();
539
540 let mut lig_kern_program = tfm_file.lig_kern_program;
541 lig_kern_program.pack_kerns(&tfm_file.kerns);
542 let lig_kern_entrypoints: HashMap<Char, u16> = tfm_file
543 .char_tags
544 .iter()
545 .filter_map(|(c, t)| {
546 t.ligature()
547 .and_then(|l| lig_kern_program.unpack_entrypoint(l).ok())
548 .map(|e| (*c, e))
549 })
550 .collect();
551
552 let char_tags = tfm_file
553 .char_tags
554 .into_iter()
555 .filter_map(|(c, tag)| match tag {
556 super::CharTag::Ligature(_) => lig_kern_entrypoints
557 .get(&c)
558 .map(|e| (c, CharTag::Ligature(*e))),
559 super::CharTag::List(l) => Some((c, CharTag::List(l))),
560 super::CharTag::Extension(i) => tfm_file
562 .extensible_chars
563 .get(i as usize)
564 .cloned()
565 .map(|t| (c, CharTag::Extension(t))),
566 })
567 .collect();
568
569 File {
570 header: tfm_file.header,
571 design_units: FixWord::ONE,
572 char_dimens,
573 char_tags,
574 unset_char_tags: Default::default(),
575 lig_kern_program,
576 params: tfm_file.params,
577 additional_widths: vec![],
578 additional_heights: vec![],
579 additional_depths: vec![],
580 additional_italics: vec![],
581 }
582 }
583}
584
585impl File {
586 pub fn lower(&self, char_display_format: CharDisplayFormat) -> ast::Ast {
588 let mut roots = vec![];
589
590 if let Some(font_family) = &self.header.font_family {
592 roots.push(ast::Root::Family(font_family.clone().into()))
593 }
594 if let Some(face) = self.header.face {
595 roots.push(ast::Root::Face(face.into()))
596 }
597 for (i, &u) in self.header.additional_data.iter().enumerate() {
598 let i: u8 = i.try_into().unwrap();
599 let i = i.checked_add(18).unwrap(); roots.push(ast::Root::Header((ast::DecimalU8(i), u).into()))
601 }
602 #[derive(Clone, Copy)]
603 enum FontType {
604 Vanilla,
605 TexMathSy,
606 TexMathEx,
607 }
608 let font_type = {
609 let scheme = match &self.header.character_coding_scheme {
610 None => String::new(),
611 Some(scheme) => scheme.to_uppercase(),
612 };
613 if scheme.starts_with("TEX MATH SY") {
614 FontType::TexMathSy
615 } else if scheme.starts_with("TEX MATH EX") {
616 FontType::TexMathEx
617 } else {
618 FontType::Vanilla
619 }
620 };
621 if let Some(scheme) = &self.header.character_coding_scheme {
622 roots.push(ast::Root::CodingScheme(scheme.clone().into()));
623 }
624 roots.extend([
625 ast::Root::DesignSize(
626 if self.header.design_size_valid {
627 DesignSize::Valid(self.header.design_size)
628 } else {
629 DesignSize::Invalid
630 }
631 .into(),
632 ),
633 ast::Root::Comment("DESIGNSIZE IS IN POINTS".into()),
634 ast::Root::Comment("OTHER SIZES ARE MULTIPLES OF DESIGNSIZE".into()),
635 ast::Root::Checksum(self.header.checksum.unwrap_or_default().into()),
636 ]);
637 if self.header.seven_bit_safe == Some(true) {
638 roots.push(ast::Root::SevenBitSafeFlag(true.into()));
639 }
640 let params: Vec<ast::FontDimension> = self
642 .params
643 .iter()
644 .enumerate()
645 .filter_map(|(i, ¶m)| {
646 let i: u16 = match (i + 1).try_into() {
647 Ok(i) => i,
648 Err(_) => return None,
649 };
650 let named_param = match (i, font_type) {
652 (1, _) => NamedParameter::Slant,
653 (2, _) => NamedParameter::Space,
654 (3, _) => NamedParameter::Stretch,
655 (4, _) => NamedParameter::Shrink,
656 (5, _) => NamedParameter::XHeight,
657 (6, _) => NamedParameter::Quad,
658 (7, _) => NamedParameter::ExtraSpace,
659 (8, FontType::TexMathSy) => NamedParameter::Num1,
660 (9, FontType::TexMathSy) => NamedParameter::Num2,
661 (10, FontType::TexMathSy) => NamedParameter::Num3,
662 (11, FontType::TexMathSy) => NamedParameter::Denom1,
663 (12, FontType::TexMathSy) => NamedParameter::Denom2,
664 (13, FontType::TexMathSy) => NamedParameter::Sup1,
665 (14, FontType::TexMathSy) => NamedParameter::Sup2,
666 (15, FontType::TexMathSy) => NamedParameter::Sup3,
667 (16, FontType::TexMathSy) => NamedParameter::Sub1,
668 (17, FontType::TexMathSy) => NamedParameter::Sub2,
669 (18, FontType::TexMathSy) => NamedParameter::SupDrop,
670 (19, FontType::TexMathSy) => NamedParameter::SubDrop,
671 (20, FontType::TexMathSy) => NamedParameter::Delim1,
672 (21, FontType::TexMathSy) => NamedParameter::Delim2,
673 (22, FontType::TexMathSy) => NamedParameter::AxisHeight,
674 (8, FontType::TexMathEx) => NamedParameter::DefaultRuleThickness,
675 (9, FontType::TexMathEx) => NamedParameter::BigOpSpacing1,
676 (10, FontType::TexMathEx) => NamedParameter::BigOpSpacing2,
677 (11, FontType::TexMathEx) => NamedParameter::BigOpSpacing3,
678 (12, FontType::TexMathEx) => NamedParameter::BigOpSpacing4,
679 (13, FontType::TexMathEx) => NamedParameter::BigOpSpacing5,
680 _ => {
681 let parameter_number = ParameterNumber(i);
682 return Some(ast::FontDimension::IndexedParam(
683 (parameter_number, param).into(),
684 ));
685 }
686 };
687 Some(ast::FontDimension::NamedParam(named_param, param.into()))
688 })
689 .collect();
690 if !params.is_empty() {
691 roots.push(ast::Root::FontDimension(((), params).into()));
692 }
693
694 if let Some(boundary_char) = self.lig_kern_program.right_boundary_char {
696 roots.push(ast::Root::BoundaryChar(boundary_char.into()));
697 }
698 let index_to_labels = {
699 let mut m = HashMap::<usize, Vec<Char>>::new();
700 for (c, tag) in &self.char_tags {
701 if let CharTag::Ligature(index) = tag {
702 m.entry(*index as usize).or_default().push(*c);
703 }
704 }
705 m.values_mut().for_each(|v| v.sort());
706 m
707 };
708 let mut l = Vec::<ast::LigTable>::new();
709 let build_lig_kern_op = |instruction: &ligkern::lang::Instruction| match instruction
710 .operation
711 {
712 ligkern::lang::Operation::Kern(kern) => {
713 Some(ast::LigTable::Kern((instruction.right_char, kern).into()))
714 }
715 ligkern::lang::Operation::KernAtIndex(_) => {
716 panic!("tfm::pl::File lig/kern programs cannot contain `KernAtIndex` operations. Use a `Kern` operation instead.");
717 }
718 ligkern::lang::Operation::EntrypointRedirect(_, _) => None,
719 ligkern::lang::Operation::Ligature {
720 char_to_insert,
721 post_lig_operation,
722 post_lig_tag_invalid: _,
723 } => Some(ast::LigTable::Lig(
724 post_lig_operation,
725 (instruction.right_char, char_to_insert).into(),
726 )),
727 };
728
729 let mut unreachable_elems: Option<String> = None;
731 let flush_unreachable_elems = |elems: &mut Option<String>, l: &mut Vec<ast::LigTable>| {
732 if let Some(elems) = elems.take() {
733 l.push(ast::LigTable::Comment(elems))
734 }
735 };
736 for (index, (reachable, instruction)) in self
737 .lig_kern_program
738 .reachable_iter(
739 self.char_tags
740 .iter()
741 .filter_map(|(c, t)| t.ligature().map(|l| (*c, l))),
742 )
743 .zip(&self.lig_kern_program.instructions)
744 .enumerate()
745 {
746 match reachable {
747 ligkern::lang::ReachableIterItem::Reachable { adjusted_skip } => {
748 flush_unreachable_elems(&mut unreachable_elems, &mut l);
749 if let Some(e) = self.lig_kern_program.left_boundary_char_entrypoint {
750 if e as usize == index {
751 l.push(ast::LigTable::Label(
752 ast::LigTableLabel::BoundaryChar.into(),
753 ));
754 }
755 }
756 for label in index_to_labels.get(&index).unwrap_or(&vec![]) {
757 l.push(ast::LigTable::Label(
758 ast::LigTableLabel::Char(*label).into(),
759 ));
760 }
761 if let Some(op) = build_lig_kern_op(instruction) {
762 l.push(op);
763 }
764 match adjusted_skip {
765 Some(i) => l.push(ast::LigTable::Skip(ast::DecimalU8(i).into())),
767 None => {
768 match instruction.next_instruction {
769 None => l.push(ast::LigTable::Stop(().into())),
770 Some(0) => {}
771 Some(i) => l.push(ast::LigTable::Skip(ast::DecimalU8(i).into())),
773 }
774 }
775 }
776 }
777 ligkern::lang::ReachableIterItem::Unreachable => {
778 let unreachable_elems = unreachable_elems.get_or_insert_with(|| {
779 " THIS PART OF THE PROGRAM IS NEVER USED!\n".to_string()
782 });
783 if let Some(op) = build_lig_kern_op(instruction) {
784 unreachable_elems
785 .push_str(&format!["{}", op.lower(char_display_format).display(6, 3)]);
786 }
787 }
788 ligkern::lang::ReachableIterItem::Passthrough => {}
789 }
790 }
791 flush_unreachable_elems(&mut unreachable_elems, &mut l);
792 if !self.lig_kern_program.instructions.is_empty() {
793 roots.push(ast::Root::LigTable(((), l).into()))
794 }
795
796 let ordered_chars = {
798 let mut v: Vec<Char> = self.char_dimens.keys().copied().collect();
799 v.sort();
800 v
801 };
802 for c in &ordered_chars {
803 let data = match self.char_dimens.get(c) {
804 None => continue, Some(data) => data,
806 };
807 let mut v = vec![];
808 match data.width {
809 None => {
810 v.push(ast::Character::Width(None.into()));
811 }
812 Some(width) => {
813 v.push(ast::Character::Width(Some(width).into()));
814 }
815 };
816 if let Some(height) = data.height {
817 v.push(ast::Character::Height(height.into()));
818 }
819 if let Some(depth) = data.depth {
820 v.push(ast::Character::Depth(depth.into()));
821 }
822 if let Some(italic_correction) = data.italic_correction {
823 v.push(ast::Character::ItalicCorrection(italic_correction.into()));
824 }
825
826 match self.char_tags.get(c) {
827 None => {}
828 Some(CharTag::Ligature(entrypoint)) => {
829 let l: Vec<cst::Node> = self
830 .lig_kern_program
831 .instructions_for_entrypoint(*entrypoint)
832 .map(|(_, b)| b)
833 .filter_map(build_lig_kern_op)
834 .map(|n| n.lower(char_display_format))
835 .collect();
836 if l.is_empty() {
837 v.push(ast::Character::Comment("\n".into()));
838 } else {
839 v.push(ast::Character::Comment(format![
840 "\n{}",
841 cst::Cst(l).display(6, 3)
842 ]));
843 }
844 }
845 Some(CharTag::List(c)) => v.push(ast::Character::NextLarger((*c).into())),
846 Some(CharTag::Extension(recipe)) => {
847 let mut r = vec![];
849 if let Some(top) = recipe.top {
850 r.push(ast::ExtensibleCharacter::Top(top.into()));
851 }
852 if let Some(middle) = recipe.middle {
853 r.push(ast::ExtensibleCharacter::Middle(middle.into()));
854 }
855 if let Some(bottom) = recipe.bottom {
856 r.push(ast::ExtensibleCharacter::Bottom(bottom.into()));
857 }
858 let rep = if self.char_dimens.contains_key(&recipe.rep) {
859 recipe.rep
860 } else {
861 *c
862 };
863 r.push(ast::ExtensibleCharacter::Replicated(rep.into()));
864 v.push(ast::Character::ExtensibleCharacter(((), r).into()))
865 }
866 }
867 roots.push(ast::Root::Character((*c, v).into()));
868 }
869
870 ast::Ast(roots)
871 }
872
873 pub fn display(&self, indent: usize, char_display_format: CharDisplayFormat) -> Display<'_> {
878 Display {
879 pl_file: self,
880 indent,
881 char_display_format,
882 }
883 }
884}
885
886pub struct Display<'a> {
890 pl_file: &'a File,
891 indent: usize,
892 char_display_format: CharDisplayFormat,
893}
894
895impl<'a> std::fmt::Display for Display<'a> {
896 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
897 let ast = self.pl_file.lower(self.char_display_format);
898 let cst = ast.lower(self.char_display_format);
899 let d = cst.display(0, self.indent);
900 d.fmt(f)
901 }
902}
903
904#[derive(Default, Debug, Clone, Copy)]
906pub enum CharDisplayFormat {
907 #[default]
911 Default,
912 Ascii,
915 Octal,
917}
918
919struct Chars<'a> {
921 s: &'a str,
922}
923
924impl<'a> Chars<'a> {
925 fn new(s: &'a str) -> Self {
926 Self { s }
927 }
928}
929
930impl<'a> Iterator for Chars<'a> {
931 type Item = char;
932 fn next(&mut self) -> Option<Self::Item> {
933 let mut iter = self.s.chars();
934 match iter.next() {
935 Some(mut c) => {
936 if c == '\r' {
937 let mut skipped = 1;
938 loop {
939 match iter.next() {
940 Some('\r') => {
941 skipped += 1;
942 continue;
943 }
944 Some('\n') => {
945 c = '\n';
946 self.s = &self.s[skipped..];
947 break;
948 }
949 _ => break,
950 }
951 }
952 }
953 self.s = &self.s[c.len_utf8()..];
954 Some(c)
955 }
956 None => None,
957 }
958 }
959 fn size_hint(&self) -> (usize, Option<usize>) {
960 (0, Some(self.s.len()))
961 }
962}
963
964#[cfg(test)]
965mod tests {
966 use super::*;
967 use crate::{Face, WidthIndex};
968
969 fn run_from_pl_source_code_test(source: &str, mut want: File) {
970 want.header.seven_bit_safe = Some(true);
971 let (got, errors) = File::from_pl_source_code(source);
972 assert_eq!(errors, vec![]);
973 assert_eq!(got, want);
974 }
975
976 macro_rules! from_pl_source_code_tests {
977 ( $( ($name: ident, $source: expr, $want: expr, ), )+ ) => {
978 $(
979 #[test]
980 fn $name() {
981 let source = $source;
982 let want = $want;
983 run_from_pl_source_code_test(source, want);
984 }
985 )+
986 };
987 }
988
989 from_pl_source_code_tests!(
990 (
991 checksum,
992 "(CHECKSUM H 7)",
993 File {
994 header: Header {
995 checksum: Some(0x7),
996 ..Header::pl_default()
997 },
998 ..Default::default()
999 },
1000 ),
1001 (
1002 checksum_0,
1003 "(CHECKSUM O 0)",
1004 File {
1005 header: Header {
1006 checksum: Some(0),
1007 ..Header::pl_default()
1008 },
1009 ..Default::default()
1010 },
1011 ),
1012 (
1013 design_size,
1014 "(DESIGNSIZE R 3.0)",
1015 File {
1016 header: Header {
1017 design_size: (FixWord::ONE * 3).into(),
1018 ..Header::pl_default()
1019 },
1020 ..Default::default()
1021 },
1022 ),
1023 (
1024 design_units,
1025 "(DESIGNUNITS R 2.0)",
1026 File {
1027 design_units: FixWord::ONE * 2,
1028 ..Default::default()
1029 },
1030 ),
1031 (
1032 coding_scheme,
1033 "(CODINGSCHEME Hola Mundo)",
1034 File {
1035 header: Header {
1036 character_coding_scheme: Some("Hola Mundo".into()),
1037 ..Header::pl_default()
1038 },
1039 ..Default::default()
1040 },
1041 ),
1042 (
1043 font_family,
1044 "(FAMILY Hello World)",
1045 File {
1046 header: Header {
1047 font_family: Some("Hello World".into()),
1048 ..Header::pl_default()
1049 },
1050 ..Default::default()
1051 },
1052 ),
1053 (
1054 face,
1055 "(FACE H 29)",
1056 File {
1057 header: Header {
1058 face: Some(Face::Other(0x29)),
1059 ..Header::pl_default()
1060 },
1061 ..Default::default()
1062 },
1063 ),
1064 (
1065 seven_bit_safe_flag,
1066 "(SEVENBITSAFEFLAG FALSE)",
1067 File {
1068 header: Header {
1069 seven_bit_safe: Some(false),
1070 ..Header::pl_default()
1071 },
1072 ..Default::default()
1073 },
1074 ),
1075 (
1076 additional_header_data,
1077 "(HEADER D 20 H 1234567)",
1078 File {
1079 header: Header {
1080 additional_data: vec![0, 0, 0x1234567],
1081 ..Header::pl_default()
1082 },
1083 ..Default::default()
1084 },
1085 ),
1086 (
1087 boundary_char,
1088 "(BOUNDARYCHAR C a)",
1089 File {
1090 lig_kern_program: ligkern::lang::Program {
1091 instructions: vec![],
1092 right_boundary_char: Some('a'.try_into().unwrap()),
1093 left_boundary_char_entrypoint: None,
1094 passthrough: Default::default(),
1095 },
1096 ..Default::default()
1097 },
1098 ),
1099 (
1100 named_param,
1101 "(FONTDIMEN (STRETCH D 13.0))",
1102 File {
1103 params: vec![FixWord::ZERO, FixWord::ZERO, FixWord::ONE * 13],
1104 ..Default::default()
1105 },
1106 ),
1107 (
1108 indexed_param,
1109 "(FONTDIMEN (PARAMETER D 2 D 15.0))",
1110 File {
1111 params: vec![FixWord::ZERO, FixWord::ONE * 15],
1112 ..Default::default()
1113 },
1114 ),
1115 (
1116 kern,
1117 "(LIGTABLE (KRN C r D 15.0))",
1118 File {
1119 lig_kern_program: ligkern::lang::Program {
1120 instructions: vec![ligkern::lang::Instruction {
1121 next_instruction: None,
1122 right_char: 'r'.try_into().unwrap(),
1123 operation: ligkern::lang::Operation::Kern(FixWord::ONE * 15),
1124 },],
1125 right_boundary_char: None,
1126 left_boundary_char_entrypoint: None,
1127 passthrough: Default::default(),
1128 },
1129 ..Default::default()
1130 },
1131 ),
1132 (
1133 kern_with_stop,
1134 "(LIGTABLE (KRN C r D 15.0) (STOP) (KRN C t D 15.0))",
1135 File {
1136 lig_kern_program: ligkern::lang::Program {
1137 instructions: vec![
1138 ligkern::lang::Instruction {
1139 next_instruction: None,
1140 right_char: 'r'.try_into().unwrap(),
1141 operation: ligkern::lang::Operation::Kern(FixWord::ONE * 15),
1142 },
1143 ligkern::lang::Instruction {
1144 next_instruction: None,
1145 right_char: 't'.try_into().unwrap(),
1146 operation: ligkern::lang::Operation::Kern(FixWord::ONE * 15),
1147 },
1148 ],
1149 right_boundary_char: None,
1150 left_boundary_char_entrypoint: None,
1151 passthrough: Default::default(),
1152 },
1153 ..Default::default()
1154 },
1155 ),
1156 (
1157 kern_with_skip,
1158 "(LIGTABLE (KRN C r D 15.0) (SKIP D 3))",
1159 File {
1160 lig_kern_program: ligkern::lang::Program {
1161 instructions: vec![ligkern::lang::Instruction {
1162 next_instruction: Some(3),
1163 right_char: 'r'.try_into().unwrap(),
1164 operation: ligkern::lang::Operation::Kern(FixWord::ONE * 15),
1165 },],
1166 right_boundary_char: None,
1167 left_boundary_char_entrypoint: None,
1168 passthrough: Default::default(),
1169 },
1170 ..Default::default()
1171 },
1172 ),
1173 (
1174 lig,
1175 "(LIGTABLE (LIG/> C r C t))",
1176 File {
1177 lig_kern_program: ligkern::lang::Program {
1178 instructions: vec![ligkern::lang::Instruction {
1179 next_instruction: None,
1180 right_char: 'r'.try_into().unwrap(),
1181 operation: ligkern::lang::Operation::Ligature {
1182 char_to_insert: 't'.try_into().unwrap(),
1183 post_lig_operation:
1184 ligkern::lang::PostLigOperation::RetainRightMoveToRight,
1185 post_lig_tag_invalid: false,
1186 },
1187 },],
1188 right_boundary_char: None,
1189 left_boundary_char_entrypoint: None,
1190 passthrough: Default::default(),
1191 },
1192 ..Default::default()
1193 },
1194 ),
1195 (
1196 lig_kern_entrypoints,
1197 "(LIGTABLE (LABEL C e) (KRN C r D 15.0) (LABEL C d))",
1198 File {
1199 lig_kern_program: ligkern::lang::Program {
1200 instructions: vec![ligkern::lang::Instruction {
1201 next_instruction: None,
1202 right_char: 'r'.try_into().unwrap(),
1203 operation: ligkern::lang::Operation::Kern(FixWord::ONE * 15),
1204 },],
1205 right_boundary_char: None,
1206 left_boundary_char_entrypoint: None,
1207 passthrough: Default::default(),
1208 },
1209 char_tags: BTreeMap::from([
1210 ('e'.try_into().unwrap(), CharTag::Ligature(0),),
1211 ('d'.try_into().unwrap(), CharTag::Ligature(1),),
1212 ]),
1213 ..Default::default()
1214 },
1215 ),
1216 (
1217 lig_kern_boundary_char_entrypoint,
1218 "(LIGTABLE (LABEL BOUNDARYCHAR) (KRN C r D 15.0))",
1219 File {
1220 lig_kern_program: ligkern::lang::Program {
1221 instructions: vec![ligkern::lang::Instruction {
1222 next_instruction: None,
1223 right_char: 'r'.try_into().unwrap(),
1224 operation: ligkern::lang::Operation::Kern(FixWord::ONE * 15),
1225 },],
1226 right_boundary_char: None,
1227 left_boundary_char_entrypoint: Some(0),
1228 passthrough: Default::default(),
1229 },
1230 ..Default::default()
1231 },
1232 ),
1233 (
1234 char_width,
1235 "(CHARACTER C r (CHARWD D 15.0))",
1236 File {
1237 char_dimens: BTreeMap::from([(
1238 'r'.try_into().unwrap(),
1239 CharDimensions {
1240 width: Some(FixWord::ONE * 15),
1241 ..Default::default()
1242 }
1243 )]),
1244 ..Default::default()
1245 },
1246 ),
1247 (
1248 char_height,
1249 "(CHARACTER C r (CHARHT D 15.0))",
1250 File {
1251 char_dimens: BTreeMap::from([(
1252 'r'.try_into().unwrap(),
1253 CharDimensions {
1254 height: Some(FixWord::ONE * 15),
1255 ..Default::default()
1256 }
1257 )]),
1258 ..Default::default()
1259 },
1260 ),
1261 (
1262 char_depth,
1263 "(CHARACTER C r (CHARDP D 15.0))",
1264 File {
1265 char_dimens: BTreeMap::from([(
1266 'r'.try_into().unwrap(),
1267 CharDimensions {
1268 depth: Some(FixWord::ONE * 15),
1269 ..Default::default()
1270 }
1271 )]),
1272 ..Default::default()
1273 },
1274 ),
1275 (
1276 char_italic_correction,
1277 "(CHARACTER C r (CHARIC D 15.0))",
1278 File {
1279 char_dimens: BTreeMap::from([(
1280 'r'.try_into().unwrap(),
1281 CharDimensions {
1282 italic_correction: Some(FixWord::ONE * 15),
1283 ..Default::default()
1284 }
1285 )]),
1286 ..Default::default()
1287 },
1288 ),
1289 (
1290 char_next_larger,
1291 "(CHARACTER C A) (CHARACTER C r (NEXTLARGER C A))",
1292 File {
1293 char_dimens: BTreeMap::from([
1294 (
1295 'r'.try_into().unwrap(),
1296 CharDimensions {
1297 ..Default::default()
1298 }
1299 ),
1300 (
1301 'A'.try_into().unwrap(),
1302 CharDimensions {
1303 ..Default::default()
1304 }
1305 ),
1306 ]),
1307 char_tags: BTreeMap::from([(
1308 'r'.try_into().unwrap(),
1309 CharTag::List('A'.try_into().unwrap()),
1310 )]),
1311 ..Default::default()
1312 },
1313 ),
1314 (
1315 char_extensible_recipe_empty,
1316 "(CHARACTER C r (VARCHAR))",
1317 File {
1318 char_dimens: BTreeMap::from([
1319 (Char(0), Default::default(),),
1320 ('r'.try_into().unwrap(), Default::default(),),
1321 ]),
1322 char_tags: BTreeMap::from([(
1323 'r'.try_into().unwrap(),
1324 CharTag::Extension(Default::default()),
1325 )]),
1326 ..Default::default()
1327 },
1328 ),
1329 (
1330 char_extensible_recipe_data,
1331 "(CHARACTER C r (VARCHAR (TOP O 1) (MID O 2) (BOT O 3) (REP O 4)))",
1332 File {
1333 char_dimens: BTreeMap::from([
1334 (Char(1), Default::default(),),
1335 (Char(2), Default::default(),),
1336 (Char(3), Default::default(),),
1337 (Char(4), Default::default(),),
1338 ('r'.try_into().unwrap(), Default::default(),),
1339 ]),
1340 char_tags: BTreeMap::from([(
1341 'r'.try_into().unwrap(),
1342 CharTag::Extension(ExtensibleRecipe {
1343 top: Some(Char(1)),
1344 middle: Some(Char(2)),
1345 bottom: Some(Char(3)),
1346 rep: Char(4),
1347 }),
1348 )]),
1349 ..Default::default()
1350 },
1351 ),
1352 );
1353
1354 fn run_from_tfm_file_test(tfm_file: crate::File, want: File) {
1355 let got: File = tfm_file.into();
1356 assert_eq!(got, want);
1357 }
1358
1359 macro_rules! from_tfm_file_tests {
1360 ( $( ($name: ident, $tfm_file: expr, $pl_file: expr, ), )+ ) => {
1361 $(
1362 #[test]
1363 fn $name() {
1364 let tfm_file = $tfm_file;
1365 let want = $pl_file;
1366 run_from_tfm_file_test(tfm_file, want);
1367 }
1368 )+
1369 };
1370 }
1371
1372 from_tfm_file_tests!((
1373 gap_in_chars,
1374 crate::File {
1375 char_dimens: BTreeMap::from([
1376 (
1377 Char('A'.try_into().unwrap()),
1378 crate::CharDimensions {
1379 width_index: WidthIndex::Valid(1.try_into().unwrap()),
1380 height_index: 0,
1381 depth_index: 0,
1382 italic_index: 0,
1383 }
1384 ),
1385 (
1386 Char('C'.try_into().unwrap()),
1387 crate::CharDimensions {
1388 width_index: WidthIndex::Valid(2.try_into().unwrap()),
1389 height_index: 0,
1390 depth_index: 0,
1391 italic_index: 0,
1392 }
1393 ),
1394 (
1395 Char('E'.try_into().unwrap()),
1396 crate::CharDimensions {
1397 width_index: WidthIndex::Invalid,
1398 height_index: 0,
1399 depth_index: 0,
1400 italic_index: 0,
1401 }
1402 ),
1403 ]),
1404 widths: vec![FixWord::ZERO, FixWord::ONE, FixWord::ONE * 2],
1405 ..Default::default()
1406 },
1407 File {
1408 char_dimens: BTreeMap::from([
1409 (
1410 'A'.try_into().unwrap(),
1411 CharDimensions {
1412 width: Some(FixWord::ONE),
1413 ..Default::default()
1414 }
1415 ),
1416 (
1417 'C'.try_into().unwrap(),
1418 CharDimensions {
1419 width: Some(FixWord::ONE * 2),
1420 ..Default::default()
1421 }
1422 ),
1423 (
1424 'E'.try_into().unwrap(),
1425 CharDimensions {
1426 width: None,
1427 ..Default::default()
1428 }
1429 ),
1430 ]),
1431 ..<File as From<crate::File>>::from(Default::default())
1432 },
1433 ),);
1434}