1use common::FontFormat;
7use texlang::command;
8use texlang::error;
9use texlang::prelude as txl;
10use texlang::token;
11use texlang::traits::*;
12use texlang::vm;
13
14pub 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#[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 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
77pub 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
90pub trait FontRepo {
95 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
115fn 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 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
188pub fn get_fontname<S>() -> command::BuiltIn<S>
190where
191 S: HasComponent<FontComponent>,
192{
193 command::BuiltIn::new_expansion(fontname_primitive_fn)
194}
195
196fn 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 let font_name: String = font_info.font_name.to_string();
213 input.push_string_tokens(token, &font_name);
214 Ok(())
215}
216
217pub struct ScriptFontMarker;
219
220pub 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
227pub struct ScriptScriptFontMarker;
229
230pub 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
237pub struct TextFontMarker;
239
240pub 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 ("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), ])
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 ])
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), 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