1use 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
15pub 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#[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 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
78pub 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
91pub trait FontRepo {
96 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
116fn 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 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
189pub fn get_fontname<S>() -> command::BuiltIn<S>
191where
192 S: HasComponent<FontComponent>,
193{
194 command::BuiltIn::new_expansion(fontname_primitive_fn)
195}
196
197fn 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 let font_name: String = font_info.font_name.to_string();
214 input.push_string_tokens(token, &font_name);
215 Ok(())
216}
217
218pub struct ScriptFontMarker;
220
221pub 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
228pub struct ScriptScriptFontMarker;
230
231pub 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
238pub struct TextFontMarker;
240
241pub 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 ("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)), ])
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 ])
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)), 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