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