texlang_font/
lib.rs

1//! # Font loading subsystem for Texlang
2//!
3//! This crate implements font loading and
4//! font variable management for Texlang.
5
6use common::FontFormat;
7use texlang::command;
8use texlang::error;
9use texlang::prelude as txl;
10use texlang::token;
11use texlang::traits::*;
12use texlang::vm;
13
14/// Get the `\nullfont` command.
15pub fn get_nullfont<S>() -> command::BuiltIn<S> {
16    command::BuiltIn::new_font(common::FontId::NULL)
17}
18
19static FONT_TAG: command::StaticTag = command::StaticTag::new();
20
21/// Component needed to use the `\font` command.
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub struct FontComponent {
24    font_infos: Vec<FontInfo>,
25    next_id: common::FontId,
26}
27
28impl FontComponent {
29    pub fn get_command_ref_for_font<S: HasComponent<FontComponent>>(
30        state: &S,
31        font: common::FontId,
32    ) -> Option<token::CommandRef> {
33        // TODO: this needs to return the special frozen command ref
34        // Main problem: where is this registered?
35        // vm.frozen_command_register(command, "name") -> CsName
36        // vm.frozen_command_update_name(CsName, "newName")
37        // where "nullfont" just means the thing that is returned from \string
38        // we may need to update this though? E.g. when a font is given
39        // a new CSname \font a path/to/file \font b path/to/file
40        // TODO: also need to update this when \font reruns
41        let font_info = state.component().font_infos.get(font.0 as usize).unwrap();
42        Some(font_info.command_ref)
43    }
44    pub fn is_current_font_command<S: HasComponent<FontComponent>>(
45        state: &S,
46        tag: command::Tag,
47    ) -> bool {
48        _ = state;
49        tag == FONT_TAG.get()
50    }
51    pub fn initialize<S: HasComponent<FontComponent>>(vm: &mut vm::VM<S>) {
52        let cs_name = vm.cs_name_interner_mut().get_or_intern("nullfont");
53        vm.state.component_mut().font_infos.push(FontInfo {
54            command_ref: token::CommandRef::ControlSequence(cs_name),
55            font_name: "nullfont".to_string(),
56            path: None,
57        });
58    }
59}
60
61#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
62struct FontInfo {
63    command_ref: token::CommandRef,
64    font_name: String,
65    path: Option<std::path::PathBuf>,
66}
67
68impl Default for FontComponent {
69    fn default() -> Self {
70        Self {
71            font_infos: vec![],
72            next_id: common::FontId(1),
73        }
74    }
75}
76
77/// Get the `\font` command.
78pub fn get_font<S>() -> command::BuiltIn<S>
79where
80    S: TexlangState + texlang_common::HasFileSystem + HasComponent<FontComponent> + HasFontRepo,
81{
82    command::BuiltIn::new_execution(font_primitive_fn).with_tag(FONT_TAG.get())
83}
84
85pub trait HasFontRepo {
86    type FontRepo: FontRepo;
87    fn font_repo_mut(&mut self) -> &mut Self::FontRepo;
88}
89
90/// A font repository is where font data is stored.
91///
92/// We currently envisage that typesetting engines will contain
93/// a font repo that they will use for getting font metric data.
94pub trait FontRepo {
95    /// Format of files that are store in this repo
96    type Format: common::FontFormat;
97    fn add_font(&mut self, id: common::FontId, font: Self::Format);
98}
99
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub struct NoOpFontRepo<T>(std::marker::PhantomData<T>);
102
103impl<T> Default for NoOpFontRepo<T> {
104    fn default() -> Self {
105        Self(Default::default())
106    }
107}
108
109impl<T: common::FontFormat> FontRepo for NoOpFontRepo<T> {
110    type Format = T;
111
112    fn add_font(&mut self, _: common::FontId, _: Self::Format) {}
113}
114
115/// TeX.2014.1257
116fn font_primitive_fn<S>(_: token::Token, input: &mut vm::ExecutionInput<S>) -> txl::Result<()>
117where
118    S: TexlangState + texlang_common::HasFileSystem + HasComponent<FontComponent> + HasFontRepo,
119{
120    type FontFormat<S> = <<S as HasFontRepo>::FontRepo as FontRepo>::Format;
121    let scope = TexlangState::variable_assignment_scope_hook(input.state_mut());
122    let (command_ref_or, _, file_location) = <(
123        Option<token::CommandRef>,
124        texlang::parse::OptionalEquals,
125        texlang::parse::FileLocation,
126    )>::parse(input)?;
127    let (path, tfm_bytes) = match texlang_common::read_file_to_bytes(
128        input.vm(),
129        file_location,
130        FontFormat::<S>::DEFAULT_FILE_EXTENSION,
131    ) {
132        Ok(ok) => ok,
133        Err(err) => {
134            return input.error(err);
135        }
136    };
137
138    let font = match FontFormat::<S>::parse(&tfm_bytes) {
139        Ok(font) => font,
140        Err(err) => {
141            let err = FontError {
142                inner: Box::new(err),
143            };
144            return input.error(err);
145        }
146    };
147
148    let Some(command_ref) = command_ref_or else {
149        return Ok(());
150    };
151
152    // TODO: scan the font_size_specification, section 1258
153    // TODO: does this happen before or after file reading?
154    let component = input.state_mut().component_mut();
155    let id = component.next_id;
156    component.next_id = common::FontId(component.next_id.0.checked_add(1).unwrap());
157
158    input.state_mut().font_repo_mut().add_font(id, font);
159    input.state_mut().component_mut().font_infos.push(FontInfo {
160        command_ref,
161        font_name: match path.with_extension("").file_name() {
162            Some(file_name) => file_name.to_string_lossy().into(),
163            None => "".to_string(),
164        },
165        path: Some(path),
166    });
167    input
168        .commands_map_mut()
169        .insert(command_ref, command::Command::Font(id), scope);
170    Ok(())
171}
172
173#[derive(Debug)]
174struct FontError {
175    inner: Box<dyn std::error::Error>,
176}
177
178impl error::TexError for FontError {
179    fn kind(&self) -> error::Kind {
180        error::Kind::FailedPrecondition
181    }
182
183    fn title(&self) -> String {
184        format!("Font file is invalid: {}", self.inner)
185    }
186}
187
188/// Get the `\fontname` command.
189pub fn get_fontname<S>() -> command::BuiltIn<S>
190where
191    S: HasComponent<FontComponent>,
192{
193    command::BuiltIn::new_expansion(fontname_primitive_fn)
194}
195
196/// TeX.2014.1257
197fn fontname_primitive_fn<S>(
198    token: token::Token,
199    input: &mut vm::ExpansionInput<S>,
200) -> txl::Result<()>
201where
202    S: HasComponent<FontComponent>,
203{
204    let font = common::FontId::parse(input)?;
205    let font_info = input
206        .state()
207        .component()
208        .font_infos
209        .get(font.0 as usize)
210        .expect("font has been defined");
211    // Would be nice to avoid the allocation here
212    let font_name: String = font_info.font_name.to_string();
213    input.push_string_tokens(token, &font_name);
214    Ok(())
215}
216
217/// Registers marker for the `\scriptfont` command.
218pub struct ScriptFontMarker;
219
220/// Get the `\scriptfont` command.
221pub fn get_scriptfont<
222    S: HasComponent<texlang_stdlib::registers::Component<common::FontId, 16, ScriptFontMarker>>,
223>() -> command::BuiltIn<S> {
224    texlang_stdlib::registers::new_registers_command()
225}
226
227/// Registers marker for the `\scriptscriptfont` command.
228pub struct ScriptScriptFontMarker;
229
230/// Get the `\scriptscriptfont` command.
231pub fn get_scriptscriptfont<
232    S: HasComponent<texlang_stdlib::registers::Component<common::FontId, 16, ScriptScriptFontMarker>>,
233>() -> command::BuiltIn<S> {
234    texlang_stdlib::registers::new_registers_command()
235}
236
237/// Registers marker for the `\textfont` command.
238pub struct TextFontMarker;
239
240/// Get the `\textfont` command.
241pub fn get_textfont<
242    S: HasComponent<texlang_stdlib::registers::Component<common::FontId, 16, TextFontMarker>>,
243>() -> command::BuiltIn<S> {
244    texlang_stdlib::registers::new_registers_command()
245}
246
247#[cfg(test)]
248mod tests {
249    use std::{cell::RefCell, collections::HashMap, rc::Rc};
250
251    use super::*;
252    use texlang::{command, implement_has_component, vm::TexlangState};
253    use texlang_testing::*;
254
255    #[derive(Debug, PartialEq, Eq)]
256    struct MockFont(u8);
257    #[derive(Debug)]
258    struct MockFontError;
259    impl std::error::Error for MockFontError {}
260    impl std::fmt::Display for MockFontError {
261        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262            write!(f, "invalid font file")
263        }
264    }
265    impl common::FontFormat for MockFont {
266        const DEFAULT_FILE_EXTENSION: &'static str = "mock";
267        type Error = MockFontError;
268        fn parse(b: &[u8]) -> Result<Self, Self::Error> {
269            match b.first().copied() {
270                None => Err(MockFontError {}),
271                Some(u) => Ok(MockFont(u)),
272            }
273        }
274    }
275
276    #[derive(Debug, PartialEq, Eq)]
277    enum Record {
278        AddFont(common::FontId, MockFont),
279        EnableFont(common::FontId),
280    }
281    #[derive(Default)]
282    struct Recorder {
283        records: Vec<Record>,
284    }
285    impl FontRepo for Recorder {
286        type Format = MockFont;
287        fn add_font(&mut self, id: common::FontId, font: Self::Format) {
288            self.records.push(Record::AddFont(id, font));
289        }
290    }
291
292    #[derive(Default)]
293    struct State {
294        records: Recorder,
295        font: FontComponent,
296        script_font: texlang_stdlib::registers::Component<common::FontId, 16, ScriptFontMarker>,
297        script_script_font:
298            texlang_stdlib::registers::Component<common::FontId, 16, ScriptScriptFontMarker>,
299        text_font: texlang_stdlib::registers::Component<common::FontId, 16, TextFontMarker>,
300        registers: texlang_stdlib::registers::Component<i32, 256>,
301        prefix: texlang_stdlib::prefix::Component,
302        testing: texlang_testing::TestingComponent,
303        file_system: Rc<RefCell<texlang_common::InMemoryFileSystem>>,
304    }
305    impl TexlangState for State {
306        fn enable_font_hook(&mut self, font: common::FontId) {
307            self.records.records.push(Record::EnableFont(font));
308        }
309        fn variable_assignment_scope_hook(
310            state: &mut Self,
311        ) -> texcraft_stdext::collections::groupingmap::Scope {
312            texlang_stdlib::prefix::variable_assignment_scope_hook(state)
313        }
314        fn recoverable_error_hook(
315            &self,
316            recoverable_error: error::TracedTexError,
317        ) -> Result<(), Box<dyn error::TexError>> {
318            texlang_testing::TestingComponent::recoverable_error_hook(self, recoverable_error)
319        }
320        fn is_current_font_command(&self, tag: command::Tag) -> bool {
321            FontComponent::is_current_font_command(self, tag)
322        }
323    }
324    impl texlang_stdlib::the::TheCompatible for State {
325        fn get_command_ref_for_font(&self, font: common::FontId) -> Option<token::CommandRef> {
326            FontComponent::get_command_ref_for_font(self, font)
327        }
328    }
329    implement_has_component![State {
330        font: FontComponent,
331        script_font: texlang_stdlib::registers::Component<common::FontId, 16, ScriptFontMarker>,
332        script_script_font: texlang_stdlib::registers::Component<common::FontId, 16, ScriptScriptFontMarker>,
333        text_font: texlang_stdlib::registers::Component<common::FontId, 16, TextFontMarker>,
334        registers: texlang_stdlib::registers::Component<i32, 256>,
335        prefix: texlang_stdlib::prefix::Component,
336        testing: texlang_testing::TestingComponent,
337    }];
338    impl HasFontRepo for State {
339        type FontRepo = Recorder;
340        fn font_repo_mut(&mut self) -> &mut Self::FontRepo {
341            &mut self.records
342        }
343    }
344    impl texlang_common::HasFileSystem for State {
345        fn file_system(&self) -> Rc<RefCell<dyn texlang_common::FileSystem>> {
346            self.file_system.clone()
347        }
348    }
349
350    fn built_in_commands() -> HashMap<&'static str, command::BuiltIn<State>> {
351        HashMap::from([
352            ("font", get_font()),
353            ("fontname", get_fontname()),
354            ("nullfont", get_nullfont()),
355            ("scriptfont", get_scriptfont()),
356            ("scriptscriptfont", get_scriptscriptfont()),
357            ("textfont", get_textfont()),
358            //
359            ("count", texlang_stdlib::registers::get_count()),
360            ("def", texlang_stdlib::def::get_def()),
361            ("global", texlang_stdlib::prefix::get_global()),
362            ("the", texlang_stdlib::the::get_the()),
363        ])
364    }
365
366    fn custom_vm_initialization(vm: &mut vm::VM<State>) {
367        FontComponent::initialize(vm);
368        vm.state
369            .prefix
370            .register_globally_prefixable_command(FONT_TAG.get());
371        let mut fs =
372            texlang_common::InMemoryFileSystem::new(&vm.working_directory.as_ref().unwrap());
373        fs.add_bytes_file("a.mock", &[1]);
374        fs.add_bytes_file("b.mock", &[2]);
375        fs.add_bytes_file("invalid.mock", &[]);
376        vm.state.file_system = Rc::new(RefCell::new(fs));
377    }
378
379    fn want_records(want: Vec<Record>) -> impl Fn(&State) {
380        move |state: &State| {
381            assert_eq!(state.records.records, want);
382        }
383    }
384
385    test_suite![
386        @options(
387            TestOption::BuiltInCommands(built_in_commands),
388            TestOption::CustomVMInitialization(custom_vm_initialization),
389        ),
390        state_tests(
391            (
392                nullfont,
393                r"\nullfont",
394                want_records(vec![
395                    Record::EnableFont(common::FontId::NULL),
396                ]),
397            ),
398            (
399                load_one_font,
400                r"\font \fontA a \fontA",
401                want_records(vec![
402                    Record::AddFont(common::FontId(1), MockFont(1)),
403                    Record::EnableFont(common::FontId(1)),
404                ]),
405            ),
406            (
407                load_one_font_extension,
408                r"\font \fontA a.mock \fontA",
409                want_records(vec![
410                    Record::AddFont(common::FontId(1), MockFont(1)),
411                    Record::EnableFont(common::FontId(1)),
412                ])
413            ),
414            (
415                enable_nesting_1,
416                r"\font \fontA a \nullfont{\fontA}",
417                want_records(vec![
418                    Record::AddFont(common::FontId(1), MockFont(1)),
419                    Record::EnableFont(common::FontId::NULL),
420                    Record::EnableFont(common::FontId(1)),
421                    Record::EnableFont(common::FontId::NULL),
422                ])
423            ),
424            (
425                enable_nesting_2,
426                r"\font\fontA a \font\fontB b \nullfont\fontB{\fontA}",
427                want_records(vec![
428                    Record::AddFont(common::FontId(1), MockFont(1)),
429                    Record::AddFont(common::FontId(2), MockFont(2)),
430                    Record::EnableFont(common::FontId::NULL),
431                    Record::EnableFont(common::FontId(2)),
432                    Record::EnableFont(common::FontId(1)),
433                    Record::EnableFont(common::FontId(2)),
434                ])
435            ),
436            (
437                enable_nesting_3,
438                r"\font\fontA a \font\fontB b \nullfont{\fontA\fontB}",
439                want_records(vec![
440                    Record::AddFont(common::FontId(1), MockFont(1)),
441                    Record::AddFont(common::FontId(2), MockFont(2)),
442                    Record::EnableFont(common::FontId::NULL),
443                    Record::EnableFont(common::FontId(1)),
444                    Record::EnableFont(common::FontId(2)),
445                    Record::EnableFont(common::FontId::NULL),
446                ])
447            ),
448            (
449                local_definition_and_enable,
450                r"\def\fontA{macro}{\font\fontA a \fontA}\fontA",
451                want_records(vec![
452                    Record::AddFont(common::FontId(1), MockFont(1)),
453                    Record::EnableFont(common::FontId(1)),
454                    Record::EnableFont(common::FontId::NULL),  // end group
455                    // The second \fontA expands the macro, doesn't enable the font
456                ])
457            ),
458            (
459                global_enable,
460                r"{\font\fontA a \global\fontA}",
461                want_records(vec![
462                    Record::AddFont(common::FontId(1), MockFont(1)),
463                    Record::EnableFont(common::FontId(1)),
464                    // End group doesn't re-enable the null font.
465                ])
466            ),
467            (
468                global_definition,
469                r"\def\fontA{macro}{\global\font\fontA a \fontA}\fontA",
470                want_records(vec![
471                    Record::AddFont(common::FontId(1), MockFont(1)),
472                    Record::EnableFont(common::FontId(1)),
473                    Record::EnableFont(common::FontId::NULL),  // end group
474                    Record::EnableFont(common::FontId(1)),
475                ])
476            ),
477            (
478                variable_defaults_to_null_font,
479                r"\the\textfont3",
480                want_records(vec![
481                    Record::EnableFont(common::FontId::NULL),
482                ])
483            ),
484            (
485                current_font_defaults_to_null_font,
486                r"\the\font",
487                want_records(vec![
488                    Record::EnableFont(common::FontId::NULL),
489                ])
490            ),
491            (
492                current_font_after_change,
493                r"\font\fontA a \fontA \the\font",
494                want_records(vec![
495                    Record::AddFont(common::FontId(1), MockFont(1)),
496                    Record::EnableFont(common::FontId(1)),
497                    Record::EnableFont(common::FontId(1)),
498                ])
499            ),
500            (
501                variable_assignment_1,
502                r"\font\fontA a \textfont3=\fontA \the\textfont3",
503                want_records(vec![
504                    Record::AddFont(common::FontId(1), MockFont(1)),
505                    Record::EnableFont(common::FontId(1)),
506                ])
507            ),
508            (
509                variable_assignment_1_with_the,
510                r"\font\fontA a \textfont3=\the\fontA \the\textfont3",
511                want_records(vec![
512                    Record::AddFont(common::FontId(1), MockFont(1)),
513                    Record::EnableFont(common::FontId(1)),
514                ])
515            ),
516            (
517                variable_assignment_2,
518                r"\font\fontA a \scriptfont3=\fontA \textfont3=\scriptfont3 \the\textfont3",
519                want_records(vec![
520                    Record::AddFont(common::FontId(1), MockFont(1)),
521                    Record::EnableFont(common::FontId(1)),
522                ])
523            ),
524            (
525                variable_assignment_2_with_the,
526                r"\font\fontA a \scriptfont3=\fontA \textfont3=\the\scriptfont3 \the\textfont3",
527                want_records(vec![
528                    Record::AddFont(common::FontId(1), MockFont(1)),
529                    Record::EnableFont(common::FontId(1)),
530                ])
531            ),
532            (
533                variable_assignment_3,
534                r"\font\fontA a \fontA \textfont3=\font \the\textfont3",
535                want_records(vec![
536                    Record::AddFont(common::FontId(1), MockFont(1)),
537                    Record::EnableFont(common::FontId(1)),
538                    Record::EnableFont(common::FontId(1)),
539                ])
540            ),
541            (
542                variable_assignment_3_with_the,
543                r"\font\fontA a \fontA \textfont3=\the\font \the\textfont3",
544                want_records(vec![
545                    Record::AddFont(common::FontId(1), MockFont(1)),
546                    Record::EnableFont(common::FontId(1)),
547                    Record::EnableFont(common::FontId(1)),
548                ])
549            ),
550            (
551                variable_nesting,
552                r"\font\fontA a \font\fontB b \textfont3=\fontA { \textfont3=\fontB } \the\textfont3",
553                want_records(vec![
554                    Record::AddFont(common::FontId(1), MockFont(1)),
555                    Record::AddFont(common::FontId(2), MockFont(2)),
556                    Record::EnableFont(common::FontId(1)),
557                ])
558            ),
559            (
560                variable_global,
561                r"\font\fontA a \font\fontB b \textfont3=\fontA { \global\textfont3=\fontB } \the\textfont3",
562                want_records(vec![
563                    Record::AddFont(common::FontId(1), MockFont(1)),
564                    Record::AddFont(common::FontId(2), MockFont(2)),
565                    Record::EnableFont(common::FontId(2)),
566                ])
567            ),
568        ),
569        expansion_equality_tests(
570            (
571                fontname_1,
572                r"\font\fontA a b\fontname\fontA",
573                r"ba",
574            ),
575            (
576                fontname_2,
577                r"\font\fontA a \fontname\font\fontA-\fontname\font",
578                r"nullfont-a",
579            ),
580            (
581                fontname_nullfont,
582                r"\fontname\nullfont",
583                r"nullfont",
584            ),
585        ),
586        recoverable_failure_tests(
587            (
588                font_file_does_not_exist,
589                r"\font\fontA doesNotExist ",
590                r"",
591            ),
592            (
593                font_file_not_provided,
594                r"\def\A{Hello}\font\fontA\def\A{Hola}\A",
595                r"Hola",
596            ),
597            (
598                font_file_is_invalid,
599                r"\font\fontA invalid ",
600                r"",
601            ),
602            (
603                font_command_missing_control_sequence,
604                r"\font a word2 word3",
605                r"word2 word3",
606            ),
607            (
608                bad_assignment_character,
609                r"\textfont 1 = A",
610                r"A",
611            ),
612            (
613                bad_assignment_variable_int,
614                r"\textfont 1 = \count 2 3 \the \count 2",
615                r"3",
616            ),
617            (
618                bad_assignment_execution,
619                r"\textfont 1 = \def \A {Hello}\A",
620                r"Hello",
621            ),
622        ),
623    ];
624}
625
626/*
627TODOs
628
629static_cs_name: \def\fontA{haha}\the\font % still works
630Similar:
631{
632    \textfont 0 = \nullfont
633
634    \def \nullfont{nullfont macro invoked}
635
636    Here: \expandafter \string \the \textfont 0
637
638    \nullfont{}
639
640    \the \textfont 0
641}
642
643
644
645
646fontname: \fontname \the \font etc.
647\skewchar\fontA?
648
649integer_cast_fails: \count 1 = \fontA  (\fontA still gets enabled)
650
651string: \string \fontA
652string_of_wierd_control_sequence: \expandafter \string \the \textfont 3
653    where the control sequence that \textfont 3 was defined under has
654    been redefined.
655
656wierd control sequence not matched in macros:
657    \def \test #1\fontA{Captured-#1-}
658    \test Hello \fontA  % prints Hello
659    \expandafter\test \the\scriptfont 0 \fontA  % \the\scriptfont is not matched!
660    % and so \fontA gets enabled because it's returend in the macro expansion
661
662if, ifx especially for all these wierd tokens
663
664\font\A a
665\font\B a
666\the \A returns \B
667*/