1use alloc::format;
2use alloc::string::{String, ToString};
3
4use mathtex_font::{
5 FontData, FontError, FontQuery, FontSystem, NoFontSystem, ShapeRequest, ShapedText,
6};
7use mathtex_ir::{
8 Fragment, FragmentKind, FragmentMetadata,
9};
10
11use crate::emit::IrEmitter;
12use crate::format::{FormatImage, FormatResourceKind, SessionState};
13use crate::generated::{
14 generated_node_to_fragment, GeneratedFontSystemAdapter, GeneratedFormatCache,
15 GeneratedPlatformAdapter, GeneratedResourceProvider,
16};
17use crate::platform::{LimitError, Platform};
18use crate::primitive::{PrimitiveEntry, PrimitiveRegistry, PrimitiveRegistryError};
19use crate::profile::{EngineKind, EngineProfile, EngineSemantics};
20use crate::resource::{Resource, ResourceError, ResourceKind, ResourceProvider, ResourceRequest};
21
22#[derive(Clone, Debug)]
24pub struct Engine<P, R, H, F = NoFontSystem> {
25 profile: P,
26 semantics: EngineSemantics,
27 format: FormatImage,
28 generated_format: GeneratedFormatCache,
29 resources: R,
30 platform: H,
31 fonts: F,
32 primitives: PrimitiveRegistry,
33}
34
35impl<P, R, H> Engine<P, R, H>
36where
37 P: EngineProfile,
38 R: ResourceProvider,
39 H: Platform,
40{
41 pub fn new(
43 profile: P,
44 format: FormatImage,
45 resources: R,
46 platform: H,
47 ) -> Result<Self, BuildError> {
48 let semantics = EngineSemantics::from_profile(&profile);
49 let primitives = PrimitiveRegistry::from_specs(semantics.primitives())
50 .map_err(BuildError::InvalidPrimitives)?;
51 let generated_format = generated_format_for(&semantics, &format, &resources)
52 .map_err(BuildError::FormatPreloadFailed)?;
53 Ok(Self {
54 profile,
55 semantics,
56 generated_format,
57 format,
58 resources,
59 platform,
60 fonts: NoFontSystem,
61 primitives,
62 })
63 }
64}
65
66impl<P, R, H, F> Engine<P, R, H, F>
67where
68 P: EngineProfile,
69 R: ResourceProvider,
70 H: Platform,
71 F: FontSystem,
72{
73 pub fn new_with_fonts(
75 profile: P,
76 format: FormatImage,
77 resources: R,
78 platform: H,
79 fonts: F,
80 ) -> Result<Self, BuildError> {
81 let semantics = EngineSemantics::from_profile(&profile);
82 let primitives = PrimitiveRegistry::from_specs(semantics.primitives())
83 .map_err(BuildError::InvalidPrimitives)?;
84 let generated_format = generated_format_for(&semantics, &format, &resources)
85 .map_err(BuildError::FormatPreloadFailed)?;
86 Ok(Self {
87 profile,
88 semantics,
89 generated_format,
90 format,
91 resources,
92 platform,
93 fonts,
94 primitives,
95 })
96 }
97
98 pub fn builder(profile: P) -> EngineBuilder<P, (), (), NoFontSystem> {
100 EngineBuilder::new(profile)
101 }
102
103 #[must_use]
105 pub fn new_session(&self) -> EngineSession<'_, P, R, H, F> {
106 EngineSession {
107 engine: self,
108 state: self.format.instantiate_session_state(),
109 }
110 }
111
112 #[must_use]
114 pub fn profile(&self) -> &P {
115 &self.profile
116 }
117
118 #[must_use]
120 pub fn semantics(&self) -> &EngineSemantics {
121 &self.semantics
122 }
123
124 #[must_use]
126 pub fn format(&self) -> &FormatImage {
127 &self.format
128 }
129
130 #[must_use]
132 pub fn generated_format(&self) -> &GeneratedFormatCache {
133 &self.generated_format
134 }
135
136 #[must_use]
138 pub fn resources(&self) -> &R {
139 &self.resources
140 }
141
142 #[must_use]
144 pub fn platform(&self) -> &H {
145 &self.platform
146 }
147
148 #[must_use]
150 pub fn fonts(&self) -> &F {
151 &self.fonts
152 }
153
154 #[must_use]
156 pub fn primitives(&self) -> &PrimitiveRegistry {
157 &self.primitives
158 }
159}
160
161#[derive(Clone, Debug)]
163#[must_use]
164pub struct EngineBuilder<P, R, H, F = NoFontSystem> {
165 profile: P,
166 format: Option<FormatImage>,
167 resources: Option<R>,
168 platform: Option<H>,
169 fonts: F,
170}
171
172impl<P> EngineBuilder<P, (), (), NoFontSystem> {
173 pub fn new(profile: P) -> Self {
175 Self {
176 profile,
177 format: None,
178 resources: None,
179 platform: None,
180 fonts: NoFontSystem,
181 }
182 }
183}
184
185impl<P, R, H, F> EngineBuilder<P, R, H, F> {
186 pub fn format(mut self, format: FormatImage) -> Self {
188 self.format = Some(format);
189 self
190 }
191
192 pub fn resources<NextR>(self, resources: NextR) -> EngineBuilder<P, NextR, H, F> {
194 EngineBuilder {
195 profile: self.profile,
196 format: self.format,
197 resources: Some(resources),
198 platform: self.platform,
199 fonts: self.fonts,
200 }
201 }
202
203 pub fn platform<NextH>(self, platform: NextH) -> EngineBuilder<P, R, NextH, F> {
205 EngineBuilder {
206 profile: self.profile,
207 format: self.format,
208 resources: self.resources,
209 platform: Some(platform),
210 fonts: self.fonts,
211 }
212 }
213
214 pub fn fonts<NextF>(self, fonts: NextF) -> EngineBuilder<P, R, H, NextF> {
216 EngineBuilder {
217 profile: self.profile,
218 format: self.format,
219 resources: self.resources,
220 platform: self.platform,
221 fonts,
222 }
223 }
224}
225
226impl<P, R, H, F> EngineBuilder<P, R, H, F>
227where
228 P: EngineProfile,
229 R: ResourceProvider,
230 H: Platform,
231 F: FontSystem,
232{
233 pub fn build(self) -> Result<Engine<P, R, H, F>, BuildError> {
235 let format = self.format.ok_or(BuildError::MissingFormat)?;
236 if format.profile != self.profile.id() {
237 return Err(BuildError::ProfileFormatMismatch {
238 profile: self.profile.id().0,
239 format_profile: format.profile.0,
240 });
241 }
242
243 let resources = self.resources.expect("resources are required by type");
244 let platform = self.platform.expect("platform is required by type");
245
246 Engine::new_with_fonts(self.profile, format, resources, platform, self.fonts)
247 }
248}
249
250#[derive(Clone, Debug)]
252pub struct EngineSession<'a, P, R, H, F = NoFontSystem> {
253 engine: &'a Engine<P, R, H, F>,
254 state: SessionState,
255}
256
257impl<P, R, H, F> EngineSession<'_, P, R, H, F>
258where
259 P: EngineProfile,
260 R: ResourceProvider,
261 H: Platform,
262 F: FontSystem,
263{
264 pub fn layout_fragment(&mut self, input: &str) -> Result<Fragment, EngineError> {
266 self.layout_fragment_input(FragmentInput::math_inline(input))
267 }
268
269 pub fn layout_fragment_input(&mut self, input: FragmentInput) -> Result<Fragment, EngineError> {
271 self.check_input_limits(input.body.as_str())?;
272 let generated_source = GeneratedFragmentSource::new(&input);
273 let metadata = FragmentMetadata {
274 engine_profile: self.engine.semantics.profile.0.to_string(),
275 format_id: self.engine.format.id.clone(),
276 fragment_kind: input.kind,
277 };
278
279 let mut generated = self.generated_engine();
280 if !generated.begin_primary_input(
281 input.source_name.as_str(),
282 generated_source.tex.clone().into_bytes(),
283 ) {
284 return Err(EngineError::Layout {
285 message: "generated TeX engine rejected primary input".to_string(),
286 });
287 }
288 generated.begin_fragment_capture();
289 let ran_main_control = generated.run_main_control();
290 generated.end_fragment_capture();
291 if !ran_main_control {
292 let message = match generated.last_error_message() {
294 Some(error) => format!("generated TeX engine reported an error: {error}"),
295 None => generated_abort_error(generated.last_abort_status()),
296 };
297 return Err(EngineError::Layout { message });
298 }
299
300 self.check_resource_request_limits(generated.resource_request_count())?;
301 if let Some(message) = generated_transcript_error(generated.transcript_bytes()) {
302 return Err(EngineError::Layout { message });
303 }
304
305 let root = generated
306 .captured_fragment_root()
307 .ok_or_else(|| EngineError::Layout {
308 message: "generated TeX engine did not capture a fragment root".to_string(),
309 })?;
310 let fragment =
311 generated_node_to_fragment(&generated, root, metadata).ok_or_else(|| {
312 EngineError::Layout {
313 message: "generated TeX engine could not convert captured fragment root"
314 .to_string(),
315 }
316 })?;
317 self.check_layout_node_limits(fragment.nodes.len())?;
318 Ok(fragment)
319 }
320
321 #[must_use]
323 pub fn generated_engine(&self) -> mathtex_portable_engine_generated::PortableTexEngine<'_> {
324 self.engine
325 .generated_format
326 .instantiate(
327 generated_profile_for(&self.engine.semantics),
328 GeneratedResourceProvider::new(&self.engine.resources),
329 )
330 .with_platform(GeneratedPlatformAdapter::new(&self.engine.platform))
331 .with_font_platform(GeneratedFontSystemAdapter::new(&self.engine.fonts))
332 }
333
334 pub fn load_input(&self, name: &str) -> Result<Resource, ResourceError> {
336 self.engine.resources.read(name, ResourceKind::TexInput)
337 }
338
339 pub fn load_package(&self, name: &str) -> Result<Resource, ResourceError> {
341 self.engine.resources.read(name, ResourceKind::Package)
342 }
343
344 pub fn load_class(&self, name: &str) -> Result<Resource, ResourceError> {
346 self.engine.resources.read_class(name)
347 }
348
349 pub fn load_font_definition(&self, name: &str) -> Result<Resource, ResourceError> {
351 self.engine.resources.read_font_definition(name)
352 }
353
354 pub fn load_package_support(&self, name: &str) -> Result<Resource, ResourceError> {
356 self.engine.resources.read_package_support(name)
357 }
358
359 pub fn load_font_resource(&self, name: &str) -> Result<Resource, ResourceError> {
361 self.engine.resources.read_font(name)
362 }
363
364 pub fn load_encoding(&self, name: &str) -> Result<Resource, ResourceError> {
366 self.engine.resources.read_encoding(name)
367 }
368
369 pub fn load_map(&self, name: &str) -> Result<Resource, ResourceError> {
371 self.engine.resources.read_map(name)
372 }
373
374 pub fn load_config(&self, name: &str) -> Result<Resource, ResourceError> {
376 self.engine.resources.read_config(name)
377 }
378
379 pub fn load_asset(&self, package: &str, name: &str) -> Result<Resource, ResourceError> {
381 self.engine.resources.read_asset(package, name)
382 }
383
384 pub fn load_format_image(&self, name: &str) -> Result<Resource, ResourceError> {
386 self.engine.resources.read_format_image(name)
387 }
388
389 pub fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
391 self.engine.fonts.load_font(query)
392 }
393
394 pub fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
396 self.engine.fonts.shape_text(request)
397 }
398
399 #[must_use]
401 pub fn primitive(&self, name: &str) -> Option<&PrimitiveEntry> {
402 self.engine.primitives.get(name)
403 }
404
405 #[must_use]
407 pub fn semantics(&self) -> &EngineSemantics {
408 &self.engine.semantics
409 }
410
411 #[must_use]
413 pub fn ir_emitter(&self) -> IrEmitter {
414 IrEmitter::new(FragmentMetadata {
415 engine_profile: self.engine.semantics.profile.0.to_string(),
416 format_id: self.engine.format.id.clone(),
417 fragment_kind: FragmentKind::MathInline,
418 })
419 }
420
421 #[must_use]
423 pub fn ir_emitter_for(&self, kind: FragmentKind) -> IrEmitter {
424 IrEmitter::new(FragmentMetadata {
425 engine_profile: self.engine.semantics.profile.0.to_string(),
426 format_id: self.engine.format.id.clone(),
427 fragment_kind: kind,
428 })
429 }
430
431 #[must_use]
433 pub fn state(&self) -> &SessionState {
434 &self.state
435 }
436
437 #[must_use]
439 pub fn state_mut(&mut self) -> &mut SessionState {
440 &mut self.state
441 }
442
443 fn check_input_limits(&self, input: &str) -> Result<(), EngineError> {
444 let actual = input.len();
445 let limit = self.engine.platform.limits().max_input_bytes;
446 if actual > limit {
447 return Err(EngineError::Limit(LimitError::InputTooLarge {
448 actual,
449 limit,
450 }));
451 }
452 Ok(())
453 }
454
455 fn check_resource_request_limits(&self, actual: usize) -> Result<(), EngineError> {
456 let limit = self.engine.platform.limits().max_resource_requests;
457 if actual > limit {
458 return Err(EngineError::Limit(LimitError::TooManyResourceRequests {
459 actual,
460 limit,
461 }));
462 }
463 Ok(())
464 }
465
466 fn check_layout_node_limits(&self, actual: usize) -> Result<(), EngineError> {
467 let limit = self.engine.platform.limits().max_layout_nodes;
468 if actual > limit {
469 return Err(EngineError::Limit(LimitError::TooManyLayoutNodes {
470 actual,
471 limit,
472 }));
473 }
474 Ok(())
475 }
476}
477
478fn generated_profile_for(
479 semantics: &EngineSemantics,
480) -> mathtex_portable_engine_generated::EngineProfile {
481 mathtex_portable_engine_generated::EngineProfile {
482 id: semantics.profile.0,
483 kind: match semantics.kind {
484 EngineKind::Tex => mathtex_portable_engine_generated::EngineProfileKind::Tex,
485 EngineKind::Etex => mathtex_portable_engine_generated::EngineProfileKind::Etex,
486 EngineKind::Xetex => mathtex_portable_engine_generated::EngineProfileKind::Xetex,
487 },
488 etex: semantics.extensions.etex,
489 xetex: semantics.extensions.xetex,
490 unicode_scalars: semantics.catcodes.unicode_scalars,
491 unicode_math: semantics.mathcodes.unicode_math,
492 native_fonts: semantics.fonts.host_native_fonts,
493 }
494}
495
496fn generated_format_for<R>(
497 semantics: &EngineSemantics,
498 format: &FormatImage,
499 resources: &R,
500) -> Result<GeneratedFormatCache, FormatPreloadError>
501where
502 R: ResourceProvider,
503{
504 let profile = generated_profile_for(semantics);
505 let base = GeneratedFormatCache::initialized(profile);
506 if !format_has_generated_preload_resources(format) {
507 return Ok(base);
508 }
509
510 let provider = CachedGeneratedFormatResources { format, resources };
511 let mut engine = base.instantiate(profile, GeneratedResourceProvider::new(provider));
512
513 for resource in generated_format_preload_resources(format) {
514 let mut bytes = Vec::with_capacity(resource.bytes.len() + GENERATED_FORMAT_FINALIZER.len());
515 bytes.extend_from_slice(resource.bytes.as_slice());
516 bytes.extend_from_slice(GENERATED_FORMAT_FINALIZER);
517 if !engine.begin_primary_input(resource.name.as_str(), bytes) {
518 return Err(FormatPreloadError::InputRejected {
519 name: resource.name.clone(),
520 });
521 }
522 let completed = engine.run_format_initialization();
523 if let Some(request) = generated_failed_resource_request(&engine) {
524 return Err(FormatPreloadError::Resource {
525 name: resource.name.clone(),
526 resource: request.name.clone(),
527 });
528 }
529 if let Some(message) = generated_transcript_error(engine.transcript_bytes()) {
530 return Err(FormatPreloadError::Transcript {
531 name: resource.name.clone(),
532 message,
533 });
534 }
535 if !completed {
536 if let Some(message) = generated_transcript_abort_error(engine.transcript_bytes()) {
537 return Err(FormatPreloadError::Transcript {
538 name: resource.name.clone(),
539 message,
540 });
541 }
542 return Err(FormatPreloadError::Aborted {
543 name: resource.name.clone(),
544 status: engine.last_abort_status(),
545 });
546 }
547 }
548 engine.finalize_trie();
550 Ok(GeneratedFormatCache::from_engine_owned(engine))
551}
552
553const GENERATED_FORMAT_FINALIZER: &[u8] = br"\dump";
554
555fn generated_failed_resource_request<'a>(
556 engine: &'a mathtex_portable_engine_generated::PortableTexEngine<'_>,
557) -> Option<&'a mathtex_portable_engine_generated::PortableResourceRequestRecord> {
558 engine.resource_request_records().iter().find(|request| {
559 request.byte_len.is_none() && !is_optional_generated_resource_probe(request)
560 })
561}
562
563fn is_optional_generated_resource_probe(
564 request: &mathtex_portable_engine_generated::PortableResourceRequestRecord,
565) -> bool {
566 let basename = generated_resource_probe_basename(request.name.as_str());
567 basename == "texsys.aux" || basename == "babel-texput.cfg"
568}
569
570fn generated_resource_probe_basename(mut name: &str) -> &str {
571 loop {
572 if let Some(stripped) = name.strip_prefix("./") {
573 name = stripped;
574 continue;
575 }
576 if let Some(stripped) = name.strip_prefix("[]") {
577 name = stripped;
578 continue;
579 }
580 if let Some(stripped) = name.strip_prefix(':') {
581 name = stripped;
582 continue;
583 }
584 break name;
585 }
586}
587
588fn generated_format_preload_resources(
589 format: &FormatImage,
590) -> impl Iterator<Item = &crate::format::FormatResource> {
591 match &format.state {
592 crate::format::FormatState::Structured(snapshot) => Some(snapshot.resources.iter()),
593 _ => None,
594 }
595 .into_iter()
596 .flatten()
597 .filter(|resource| {
598 matches!(
599 resource.kind,
600 FormatResourceKind::TexInput | FormatResourceKind::Package
601 )
602 })
603}
604
605fn format_has_generated_preload_resources(format: &FormatImage) -> bool {
606 let crate::format::FormatState::Structured(snapshot) = &format.state else {
607 return false;
608 };
609 snapshot.resources.iter().any(|resource| {
610 matches!(
611 resource.kind,
612 FormatResourceKind::TexInput | FormatResourceKind::Package
613 )
614 })
615}
616
617#[derive(Clone, Copy, Debug)]
618struct CachedGeneratedFormatResources<'a, R> {
619 format: &'a FormatImage,
620 resources: &'a R,
621}
622
623impl<R> ResourceProvider for CachedGeneratedFormatResources<'_, R>
624where
625 R: ResourceProvider,
626{
627 fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
628 if matches!(request.kind, ResourceKind::TexInput | ResourceKind::Package) {
629 if let Some(resource) = cached_format_resource(self.format, request) {
630 return Ok(resource);
631 }
632 }
633 self.resources.read_request(request)
634 }
635}
636
637fn cached_format_resource(format: &FormatImage, request: &ResourceRequest) -> Option<Resource> {
638 let crate::format::FormatState::Structured(snapshot) = &format.state else {
639 return None;
640 };
641 snapshot
642 .resources
643 .iter()
644 .find(|resource| {
645 format_resource_kind_to_resource_kind(resource.kind) == Some(request.kind)
646 && resource.name == request.canonical_name()
647 })
648 .map(|resource| Resource {
649 canonical_name: resource.name.clone(),
650 kind: request.kind,
651 bytes: resource.bytes.clone(),
652 })
653}
654
655fn format_resource_kind_to_resource_kind(kind: FormatResourceKind) -> Option<ResourceKind> {
656 match kind {
657 FormatResourceKind::TexInput => Some(ResourceKind::TexInput),
658 FormatResourceKind::Package => Some(ResourceKind::Package),
659 }
660}
661
662struct GeneratedFragmentSource {
663 tex: String,
664}
665
666impl GeneratedFragmentSource {
667 fn new(input: &FragmentInput) -> Self {
668 let mut tex = String::with_capacity(input.body.len() + 96);
669 match input.kind {
670 FragmentKind::Text => {
671 tex.push_str(r"\hbox{");
672 }
673 FragmentKind::MathInline => {
674 tex.push_str(r"\hbox{$");
675 }
676 FragmentKind::MathDisplay => {
677 tex.push_str(r"\hbox{$\displaystyle ");
678 }
679 _ => {
680 tex.push_str(r"\hbox{");
681 }
682 }
683
684 tex.push_str(input.body.as_str());
685
686 match input.kind {
688 FragmentKind::Text => tex.push_str(r"\relax}\csname @@end\endcsname\end"),
689 FragmentKind::MathInline | FragmentKind::MathDisplay => {
690 tex.push_str(r"\relax$}\csname @@end\endcsname\end")
691 }
692 _ => tex.push_str(r"\relax}\csname @@end\endcsname\end"),
693 }
694
695 Self { tex }
696 }
697}
698
699fn generated_transcript_error(transcript: &[u8]) -> Option<String> {
700 let transcript = core::str::from_utf8(transcript).ok()?;
701 for (error_start, _) in transcript.match_indices('!').rev() {
702 let error_tail = &transcript[error_start..];
703 let error = error_tail
704 .lines()
705 .next()
706 .unwrap_or("generated TeX engine reported an error")
707 .trim();
708 if !error.starts_with("! ") {
709 continue;
710 }
711 let has_line_context = error_tail
712 .lines()
713 .skip(1)
714 .take(4)
715 .any(|line| line.trim_start().starts_with("l."));
716 let has_compact_line_context = error_tail
717 .get(..error_tail.len().min(512))
718 .is_some_and(|tail| tail.contains("l."));
719 if has_line_context || has_compact_line_context || error.contains("Emergency stop") {
720 return Some(format!("generated TeX engine reported an error: {error}"));
721 }
722 }
723 None
724}
725
726fn generated_transcript_abort_error(transcript: &[u8]) -> Option<String> {
727 let transcript = core::str::from_utf8(transcript).ok()?;
728 transcript
729 .match_indices('!')
730 .rev()
731 .find_map(|(error_start, _)| {
732 let error = transcript[error_start..].lines().next()?.trim();
733 error
734 .starts_with("! ")
735 .then(|| format!("generated TeX engine aborted after error: {error}"))
736 })
737}
738
739fn generated_abort_error(status: Option<i32>) -> String {
740 match status {
741 Some(status) => format!("generated TeX engine aborted with status {status}"),
742 None => "generated TeX engine aborted".to_string(),
743 }
744}
745
746#[derive(Clone, Debug, PartialEq, Eq)]
748pub struct FragmentInput {
749 pub source_name: String,
751 pub body: String,
753 pub kind: FragmentKind,
755}
756
757impl FragmentInput {
758 #[must_use]
760 pub fn math_inline(body: impl Into<String>) -> Self {
761 Self::new("input", body, FragmentKind::MathInline)
762 }
763
764 #[must_use]
766 pub fn math_display(body: impl Into<String>) -> Self {
767 Self::new("input", body, FragmentKind::MathDisplay)
768 }
769
770 #[must_use]
772 pub fn text(body: impl Into<String>) -> Self {
773 Self::new("input", body, FragmentKind::Text)
774 }
775
776 #[must_use]
778 pub fn new(
779 source_name: impl Into<String>,
780 body: impl Into<String>,
781 kind: FragmentKind,
782 ) -> Self {
783 Self {
784 source_name: source_name.into(),
785 body: body.into(),
786 kind,
787 }
788 }
789}
790
791#[derive(Clone, Debug, PartialEq, Eq)]
793#[non_exhaustive]
794pub enum BuildError {
795 MissingFormat,
797 InvalidPrimitives(PrimitiveRegistryError),
799 FormatPreloadFailed(FormatPreloadError),
801 ProfileFormatMismatch {
803 profile: &'static str,
805 format_profile: &'static str,
807 },
808}
809
810#[derive(Clone, Debug, PartialEq, Eq)]
812#[non_exhaustive]
813pub enum FormatPreloadError {
814 InputRejected {
816 name: String,
818 },
819 Aborted {
821 name: String,
823 status: Option<i32>,
825 },
826 Resource {
828 name: String,
830 resource: String,
832 },
833 Transcript {
835 name: String,
837 message: String,
839 },
840}
841
842#[derive(Clone, Debug, PartialEq, Eq)]
844#[non_exhaustive]
845pub enum EngineError {
846 Limit(LimitError),
848 Parse {
850 message: String,
852 },
853 Layout {
855 message: String,
857 },
858 Resource {
860 message: String,
862 },
863}
864
865#[cfg(test)]
866mod tests {
867 use alloc::borrow::Cow;
868 use alloc::vec::Vec;
869
870 use super::*;
871 use crate::{
872 EngineKind, EnginePatch, EngineProfile, ExtensionPolicy, FormatImage, FormatSnapshot,
873 HostLimits, InMemoryResourceProvider, LimitError, MacroDefinition, NoopPlatform,
874 PrimitiveKind, PrimitiveOpcode, PrimitiveRegistryError, PrimitiveSpec, ProfileId,
875 ResourceFontSystem, TexProfile, XetexProfile,
876 };
877 use mathtex_font::InMemoryFontSystem;
878 use mathtex_ir::{ByteSpan, Direction, FragmentKind, LayoutNodeKind, Length};
879
880 const TEST_FORMAT_INPUT: &str = "mathtex-test-format.tex";
881 const TEST_FORMAT_SETUP: &[u8] = br"\catcode`\{=1 \catcode`\}=2 \catcode`\#=6 \catcode`\$=3 \catcode`\^=7 \catcode`\_=8 \def\usepackage#1{\input #1.sty\relax} \def\text#1{\hbox{#1}}";
882 const TEST_MATH_FONT_SETUP: &[u8] = br"\catcode`\{=1 \catcode`\}=2 \catcode`\#=6 \catcode`\$=3 \catcode`\^=7 \catcode`\_=8 \def\usepackage#1{\input #1.sty\relax} \def\text#1{\hbox{#1}} \font\tenrm=cmr10 \font\teni=cmmi10 \font\tensy=cmsy10 \font\tenex=cmex10 \textfont0=\tenrm \scriptfont0=\tenrm \scriptscriptfont0=\tenrm \textfont1=\teni \scriptfont1=\teni \scriptscriptfont1=\teni \textfont2=\tensy \scriptfont2=\tensy \scriptscriptfont2=\tensy \textfont3=\tenex \scriptfont3=\tenex \scriptscriptfont3=\tenex";
883
884 fn test_format(
885 id: &'static str,
886 profile: ProfileId,
887 setup: &'static [u8],
888 ) -> (FormatImage, InMemoryResourceProvider) {
889 let resources = InMemoryResourceProvider::new().with_resource(
890 TEST_FORMAT_INPUT,
891 ResourceKind::TexInput,
892 setup,
893 );
894 let format = FormatImage::initializer(id, profile)
895 .preload_tex_input(TEST_FORMAT_INPUT)
896 .build(&resources)
897 .expect("test format should preload setup");
898 (format, resources)
899 }
900
901 fn basic_test_format(profile: ProfileId) -> (FormatImage, InMemoryResourceProvider) {
902 test_format("latex", profile, TEST_FORMAT_SETUP)
903 }
904
905 fn math_test_format(profile: ProfileId) -> (FormatImage, InMemoryResourceProvider) {
906 test_format("plain-math", profile, TEST_MATH_FONT_SETUP)
907 }
908
909 #[derive(Clone, Copy, Debug)]
910 struct PlainFixtureResources;
911
912 impl PlainFixtureResources {
913 const TFMS: &'static [(&'static str, &'static [u8])] = &[
914 (
915 "cmbsy10",
916 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmbsy10.tfm"),
917 ),
918 (
919 "cmbx10",
920 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmbx10.tfm"),
921 ),
922 (
923 "cmbx5",
924 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmbx5.tfm"),
925 ),
926 (
927 "cmbx6",
928 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmbx6.tfm"),
929 ),
930 (
931 "cmbx7",
932 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmbx7.tfm"),
933 ),
934 (
935 "cmbx8",
936 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmbx8.tfm"),
937 ),
938 (
939 "cmbx9",
940 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmbx9.tfm"),
941 ),
942 (
943 "cmcsc10",
944 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmcsc10.tfm"),
945 ),
946 (
947 "cmdunh10",
948 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmdunh10.tfm"),
949 ),
950 (
951 "cmex10",
952 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmex10.tfm"),
953 ),
954 (
955 "cmmi10",
956 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmmi10.tfm"),
957 ),
958 (
959 "cmmi5",
960 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmmi5.tfm"),
961 ),
962 (
963 "cmmi6",
964 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmmi6.tfm"),
965 ),
966 (
967 "cmmi7",
968 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmmi7.tfm"),
969 ),
970 (
971 "cmmi8",
972 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmmi8.tfm"),
973 ),
974 (
975 "cmmi9",
976 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmmi9.tfm"),
977 ),
978 (
979 "cmmib10",
980 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmmib10.tfm"),
981 ),
982 (
983 "cmr10",
984 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmr10.tfm"),
985 ),
986 (
987 "cmr5",
988 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmr5.tfm"),
989 ),
990 (
991 "cmr6",
992 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmr6.tfm"),
993 ),
994 (
995 "cmr7",
996 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmr7.tfm"),
997 ),
998 (
999 "cmr8",
1000 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmr8.tfm"),
1001 ),
1002 (
1003 "cmr9",
1004 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmr9.tfm"),
1005 ),
1006 (
1007 "cmsl10",
1008 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsl10.tfm"),
1009 ),
1010 (
1011 "cmsl8",
1012 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsl8.tfm"),
1013 ),
1014 (
1015 "cmsl9",
1016 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsl9.tfm"),
1017 ),
1018 (
1019 "cmsltt10",
1020 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsltt10.tfm"),
1021 ),
1022 (
1023 "cmss10",
1024 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmss10.tfm"),
1025 ),
1026 (
1027 "cmssbx10",
1028 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmssbx10.tfm"),
1029 ),
1030 (
1031 "cmssi10",
1032 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmssi10.tfm"),
1033 ),
1034 (
1035 "cmssq8",
1036 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmssq8.tfm"),
1037 ),
1038 (
1039 "cmssqi8",
1040 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmssqi8.tfm"),
1041 ),
1042 (
1043 "cmsy10",
1044 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsy10.tfm"),
1045 ),
1046 (
1047 "cmsy5",
1048 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsy5.tfm"),
1049 ),
1050 (
1051 "cmsy6",
1052 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsy6.tfm"),
1053 ),
1054 (
1055 "cmsy7",
1056 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsy7.tfm"),
1057 ),
1058 (
1059 "cmsy8",
1060 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsy8.tfm"),
1061 ),
1062 (
1063 "cmsy9",
1064 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsy9.tfm"),
1065 ),
1066 (
1067 "cmti10",
1068 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmti10.tfm"),
1069 ),
1070 (
1071 "cmti7",
1072 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmti7.tfm"),
1073 ),
1074 (
1075 "cmti8",
1076 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmti8.tfm"),
1077 ),
1078 (
1079 "cmti9",
1080 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmti9.tfm"),
1081 ),
1082 (
1083 "cmtt10",
1084 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmtt10.tfm"),
1085 ),
1086 (
1087 "cmtt8",
1088 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmtt8.tfm"),
1089 ),
1090 (
1091 "cmtt9",
1092 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmtt9.tfm"),
1093 ),
1094 (
1095 "cmu10",
1096 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmu10.tfm"),
1097 ),
1098 (
1099 "manfnt",
1100 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/manfnt.tfm"),
1101 ),
1102 ];
1103
1104 fn base_name(name: &str) -> &str {
1105 name.rsplit(['/', '\\']).next().unwrap_or(name)
1106 }
1107
1108 fn font_name(name: &str) -> &str {
1109 let name = Self::base_name(name);
1110 name.strip_suffix(".tfm").unwrap_or(name)
1111 }
1112 }
1113
1114 impl ResourceProvider for PlainFixtureResources {
1115 fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
1116 let name = request.canonical_name();
1117 let base_name = Self::base_name(name.as_str());
1118 let bytes = match request.kind {
1119 ResourceKind::TexInput if base_name == "plain.tex" => Some(
1120 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/base/plain.tex")
1121 .as_slice(),
1122 ),
1123 ResourceKind::TexInput if base_name == "hyphen" || base_name == "hyphen.tex" => {
1124 Some(
1125 include_bytes!(
1126 "../../../vendor/mathtex-fixtures/plain-tex/hyphen/hyphen.tex"
1127 )
1128 .as_slice(),
1129 )
1130 }
1131 ResourceKind::Font => {
1132 let font_name = Self::font_name(base_name);
1133 Self::TFMS
1134 .iter()
1135 .find_map(|(name, bytes)| (*name == font_name).then_some(*bytes))
1136 }
1137 _ => None,
1138 };
1139
1140 bytes
1141 .map(|bytes| Resource::from_request(request, bytes.to_vec()))
1142 .ok_or_else(|| ResourceError::NotFound {
1143 name,
1144 kind: request.kind,
1145 })
1146 }
1147 }
1148
1149 fn assert_real_plain_format_loads<R>(resources: R)
1150 where
1151 R: ResourceProvider + Copy,
1152 {
1153 let profile = TexProfile;
1154 let format = FormatImage::plain(profile.id())
1155 .build(&resources)
1156 .expect("real plain.tex should resolve");
1157 let engine = EngineBuilder::new(profile)
1158 .format(format)
1159 .resources(resources)
1160 .platform(NoopPlatform::default())
1161 .build()
1162 .expect("engine should build with real plain format");
1163 let mut session = engine.new_session();
1164
1165 let fragment = session
1166 .layout_fragment_input(FragmentInput::text(r"\tenrm A"))
1167 .expect("real plain format should define plain font macros");
1168 let has_glyph_node = fragment
1169 .nodes
1170 .iter()
1171 .any(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_)));
1172
1173 assert_eq!(fragment.metadata.format_id, "plain");
1174 assert!(has_glyph_node);
1175 }
1176
1177 #[cfg(feature = "std")]
1178 #[derive(Clone, Copy, Debug)]
1179 struct HostResolverTestResources;
1180
1181 #[cfg(feature = "std")]
1182 impl HostResolverTestResources {
1183 fn is_available() -> bool {
1184 Self::resolve("plain.tex").is_some()
1185 && Self::resolve("hyphen.tex").is_some()
1186 && Self::resolve("cmr10.tfm").is_some()
1187 }
1188
1189 fn is_latex_available() -> bool {
1190 Self::resolve("latex.ltx").is_some() && Self::resolve("cmr10.tfm").is_some()
1191 }
1192
1193 fn resolve(name: &str) -> Option<Vec<u8>> {
1194 let output = std::process::Command::new("kpsewhich")
1195 .arg(name)
1196 .output()
1197 .ok()?;
1198 if !output.status.success() {
1199 return None;
1200 }
1201 let path = core::str::from_utf8(&output.stdout).ok()?.trim();
1202 if path.is_empty() {
1203 return None;
1204 }
1205 std::fs::read(path).ok()
1206 }
1207
1208 fn candidate_names(request: &ResourceRequest) -> Vec<String> {
1209 let name = normalize_host_resolver_name(request.canonical_name());
1210 let mut candidates = Vec::with_capacity(4);
1211 candidates.push(name.clone());
1212
1213 if std::path::Path::new(&name).extension().is_none() {
1214 match request.kind {
1215 ResourceKind::Package => candidates.push(format!("{name}.sty")),
1216 ResourceKind::Class => candidates.push(format!("{name}.cls")),
1217 ResourceKind::FontDefinition => candidates.push(format!("{name}.fd")),
1218 ResourceKind::Font => {
1219 candidates.push(format!("{name}.tfm"));
1220 candidates.push(format!("{name}.otf"));
1221 candidates.push(format!("{name}.ttf"));
1222 }
1223 ResourceKind::Encoding => candidates.push(format!("{name}.enc")),
1224 ResourceKind::Map => candidates.push(format!("{name}.map")),
1225 _ => {}
1226 }
1227 }
1228
1229 candidates
1230 }
1231 }
1232
1233 #[cfg(feature = "std")]
1234 fn normalize_host_resolver_name(mut name: String) -> String {
1235 while let Some(stripped) = name.strip_prefix("./") {
1236 name = stripped.to_string();
1237 }
1238 name
1239 }
1240
1241 #[cfg(feature = "std")]
1242 impl ResourceProvider for HostResolverTestResources {
1243 fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
1244 for candidate in Self::candidate_names(request) {
1245 if let Some(bytes) = Self::resolve(&candidate) {
1246 return Ok(Resource::from_request(request, bytes));
1247 }
1248 }
1249
1250 Err(ResourceError::NotFound {
1251 name: request.canonical_name(),
1252 kind: request.kind,
1253 })
1254 }
1255 }
1256
1257 #[test]
1258 fn builder_rejects_format_for_different_profile() {
1259 let format = FormatImage::empty("latex", XetexProfile.id());
1260
1261 let error = EngineBuilder::new(TexProfile)
1262 .format(format)
1263 .resources(InMemoryResourceProvider::new())
1264 .platform(NoopPlatform::default())
1265 .build()
1266 .expect_err("profile mismatch must be rejected");
1267
1268 assert_eq!(
1269 error,
1270 BuildError::ProfileFormatMismatch {
1271 profile: "tex",
1272 format_profile: "xetex",
1273 }
1274 );
1275 }
1276
1277 #[test]
1278 fn transcript_error_detection_requires_tex_error_shape() {
1279 assert_eq!(
1280 generated_transcript_error(b"LaTeX says hello! This is only a message."),
1281 None
1282 );
1283 assert_eq!(
1284 generated_transcript_error(b"before,! Emergency stop.\nl.12 \\bad\n"),
1285 Some("generated TeX engine reported an error: ! Emergency stop.".to_string())
1286 );
1287 }
1288
1289 #[test]
1290 fn builder_reports_generated_format_preload_failure() {
1291 let profile = TexProfile;
1292 let resources = InMemoryResourceProvider::new().with_resource(
1293 TEST_FORMAT_INPUT,
1294 ResourceKind::TexInput,
1295 br"\input missing-format-dependency.tex".to_vec(),
1296 );
1297 let format = FormatImage::initializer("missing-dependency", profile.id())
1298 .preload_tex_input(TEST_FORMAT_INPUT)
1299 .build(&resources)
1300 .expect("format initializer should capture preloaded resource bytes");
1301
1302 let error = EngineBuilder::new(profile)
1303 .format(format)
1304 .resources(resources)
1305 .platform(NoopPlatform::default())
1306 .build()
1307 .expect_err("missing nested preload resource must fail engine build");
1308
1309 match error {
1310 BuildError::FormatPreloadFailed(FormatPreloadError::Resource { name, resource }) => {
1311 assert_eq!(name, TEST_FORMAT_INPUT);
1312 assert_eq!(resource, "missing-format-dependency.tex");
1313 }
1314 other => panic!("unexpected preload error: {other:?}"),
1315 }
1316 }
1317
1318 #[test]
1319 fn session_instantiates_generated_engine_from_cached_format() {
1320 let format = FormatImage::empty("latex", XetexProfile.id());
1321 let engine = EngineBuilder::new(XetexProfile)
1322 .format(format)
1323 .resources(InMemoryResourceProvider::new())
1324 .platform(NoopPlatform::default())
1325 .build()
1326 .expect("engine should build");
1327 let session = engine.new_session();
1328 let generated = session.generated_engine();
1329
1330 assert_eq!(
1331 generated.profile(),
1332 mathtex_portable_engine_generated::EngineProfile::xetex()
1333 );
1334 assert!(!generated.profile().native_fonts);
1335 assert_eq!(generated.resource_request_count(), 0);
1336 }
1337
1338 #[derive(Clone, Debug)]
1339 struct CustomSemanticProfile {
1340 fonts: crate::FontSemantics,
1341 extensions: ExtensionPolicy,
1342 }
1343
1344 impl EngineProfile for CustomSemanticProfile {
1345 fn id(&self) -> ProfileId {
1346 ProfileId("custom-semantic")
1347 }
1348
1349 fn kind(&self) -> EngineKind {
1350 EngineKind::Xetex
1351 }
1352
1353 fn primitives(&self) -> &[PrimitiveSpec] {
1354 &[]
1355 }
1356
1357 fn catcode_defaults(&self) -> crate::CatcodeDefaults {
1358 crate::CatcodeDefaults {
1359 unicode_scalars: self.extensions.xetex,
1360 }
1361 }
1362
1363 fn mathcode_defaults(&self) -> crate::MathcodeDefaults {
1364 crate::MathcodeDefaults {
1365 unicode_math: false,
1366 }
1367 }
1368
1369 fn register_defaults(&self) -> crate::RegisterDefaults {
1370 crate::RegisterDefaults::extended()
1371 }
1372
1373 fn font_semantics(&self) -> crate::FontSemantics {
1374 self.fonts
1375 }
1376
1377 fn extension_policy(&self) -> ExtensionPolicy {
1378 self.extensions
1379 }
1380 }
1381
1382 #[test]
1383 fn generated_engine_uses_custom_profile_semantic_flags() {
1384 let profile = CustomSemanticProfile {
1385 fonts: crate::FontSemantics::tex(),
1386 extensions: ExtensionPolicy {
1387 etex: true,
1388 xetex: true,
1389 },
1390 };
1391 let format = FormatImage::empty("custom-semantic", profile.id());
1392 let engine = EngineBuilder::new(profile)
1393 .format(format)
1394 .resources(InMemoryResourceProvider::new())
1395 .platform(NoopPlatform::default())
1396 .build()
1397 .expect("engine should build");
1398 let generated = engine.new_session().generated_engine().profile();
1399
1400 assert_eq!(generated.id, "custom-semantic");
1401 assert_eq!(
1402 generated.kind,
1403 mathtex_portable_engine_generated::EngineProfileKind::Xetex
1404 );
1405 assert!(generated.etex);
1406 assert!(generated.xetex);
1407 assert!(generated.unicode_scalars);
1408 assert!(!generated.unicode_math);
1409 assert!(!generated.native_fonts);
1410 }
1411
1412 #[derive(Clone, Debug)]
1413 struct PrimitiveProfile {
1414 primitives: Vec<PrimitiveSpec>,
1415 }
1416
1417 impl EngineProfile for PrimitiveProfile {
1418 fn id(&self) -> ProfileId {
1419 ProfileId("primitive-profile")
1420 }
1421
1422 fn kind(&self) -> EngineKind {
1423 EngineKind::Etex
1424 }
1425
1426 fn primitives(&self) -> &[PrimitiveSpec] {
1427 &self.primitives
1428 }
1429
1430 fn catcode_defaults(&self) -> crate::CatcodeDefaults {
1431 crate::CatcodeDefaults::default()
1432 }
1433
1434 fn mathcode_defaults(&self) -> crate::MathcodeDefaults {
1435 crate::MathcodeDefaults::default()
1436 }
1437
1438 fn register_defaults(&self) -> crate::RegisterDefaults {
1439 crate::RegisterDefaults::default()
1440 }
1441
1442 fn font_semantics(&self) -> crate::FontSemantics {
1443 crate::FontSemantics::default()
1444 }
1445
1446 fn extension_policy(&self) -> crate::ExtensionPolicy {
1447 crate::ExtensionPolicy::default()
1448 }
1449 }
1450
1451 #[test]
1452 fn engine_exposes_profile_primitive_dispatch_entries() {
1453 let profile = PrimitiveProfile {
1454 primitives: alloc::vec![PrimitiveSpec {
1455 name: Cow::Borrowed("hbox"),
1456 opcode: PrimitiveOpcode(44),
1457 kind: PrimitiveKind::Layout,
1458 }],
1459 };
1460 let format = FormatImage::empty("plain", profile.id());
1461 let engine = EngineBuilder::new(profile)
1462 .format(format)
1463 .resources(InMemoryResourceProvider::new())
1464 .platform(NoopPlatform::default())
1465 .build()
1466 .expect("engine should build");
1467 let session = engine.new_session();
1468
1469 let primitive = session.primitive("hbox").expect("hbox primitive");
1470
1471 assert_eq!(primitive.opcode, PrimitiveOpcode(44));
1472 assert_eq!(primitive.kind, PrimitiveKind::Layout);
1473 assert!(session.primitive("missing").is_none());
1474 }
1475
1476 #[test]
1477 fn engine_caches_runtime_semantics_for_shared_tex_core() {
1478 let profile = TexProfile;
1479 let format = FormatImage::empty("plain", profile.id());
1480 let engine = EngineBuilder::new(profile)
1481 .format(format)
1482 .resources(InMemoryResourceProvider::new())
1483 .platform(NoopPlatform::default())
1484 .build()
1485 .expect("engine should build");
1486 let session = engine.new_session();
1487
1488 assert_eq!(engine.semantics().profile, ProfileId("tex"));
1489 assert_eq!(session.semantics(), engine.semantics());
1490 assert!(!engine.semantics().has_patch(EnginePatch::Xetex));
1491 assert!(!engine.semantics().is_xetex());
1492 assert!(!engine.semantics().catcodes.unicode_scalars);
1493 }
1494
1495 #[test]
1496 fn xetex_profile_is_runtime_patch_over_shared_core() {
1497 let profile = XetexProfile;
1498 let format = FormatImage::empty("latex", profile.id());
1499 let engine = EngineBuilder::new(profile)
1500 .format(format)
1501 .resources(InMemoryResourceProvider::new())
1502 .platform(NoopPlatform::default())
1503 .build()
1504 .expect("engine should build");
1505 let session = engine.new_session();
1506
1507 assert_eq!(engine.semantics().profile, ProfileId("xetex"));
1508 assert_eq!(engine.semantics().kind, EngineKind::Xetex);
1509 assert!(engine.semantics().has_patch(EnginePatch::Etex));
1510 assert!(engine.semantics().has_patch(EnginePatch::Xetex));
1511 assert!(engine.semantics().is_xetex());
1512 assert!(engine.semantics().catcodes.unicode_scalars);
1513 assert!(engine.semantics().fonts.shaped_text);
1514 assert!(session.primitive("XeTeXrevision").is_some());
1515 }
1516
1517 #[derive(Clone, Debug)]
1518 struct PolicyProfile {
1519 primitives: Vec<PrimitiveSpec>,
1520 extensions: ExtensionPolicy,
1521 }
1522
1523 impl EngineProfile for PolicyProfile {
1524 fn id(&self) -> ProfileId {
1525 ProfileId("policy-profile")
1526 }
1527
1528 fn kind(&self) -> EngineKind {
1529 EngineKind::Xetex
1530 }
1531
1532 fn primitives(&self) -> &[PrimitiveSpec] {
1533 &self.primitives
1534 }
1535
1536 fn catcode_defaults(&self) -> crate::CatcodeDefaults {
1537 crate::CatcodeDefaults {
1538 unicode_scalars: self.extensions.xetex,
1539 }
1540 }
1541
1542 fn mathcode_defaults(&self) -> crate::MathcodeDefaults {
1543 crate::MathcodeDefaults {
1544 unicode_math: self.extensions.xetex,
1545 }
1546 }
1547
1548 fn register_defaults(&self) -> crate::RegisterDefaults {
1549 crate::RegisterDefaults::extended()
1550 }
1551
1552 fn font_semantics(&self) -> crate::FontSemantics {
1553 crate::FontSemantics {
1554 unicode_fonts: self.extensions.xetex,
1555 shaped_text: self.extensions.xetex,
1556 unicode_math_fonts: self.extensions.xetex,
1557 host_native_fonts: false,
1558 }
1559 }
1560
1561 fn extension_policy(&self) -> crate::ExtensionPolicy {
1562 self.extensions
1563 }
1564 }
1565
1566 #[test]
1567 fn custom_profile_extension_policy_becomes_runtime_patch_flags() {
1568 let profile = PolicyProfile {
1569 primitives: alloc::vec![PrimitiveSpec {
1570 name: Cow::Borrowed("customxetex"),
1571 opcode: PrimitiveOpcode(800),
1572 kind: PrimitiveKind::Extension,
1573 }],
1574 extensions: ExtensionPolicy {
1575 etex: false,
1576 xetex: true,
1577 },
1578 };
1579 let format = FormatImage::empty("custom", profile.id());
1580 let engine = EngineBuilder::new(profile)
1581 .format(format)
1582 .resources(InMemoryResourceProvider::new())
1583 .platform(NoopPlatform::default())
1584 .build()
1585 .expect("engine should build");
1586
1587 assert!(engine.semantics().has_patch(EnginePatch::Etex));
1588 assert!(engine.semantics().has_patch(EnginePatch::Xetex));
1589 assert!(engine.primitives().get("customxetex").is_some());
1590 }
1591
1592 #[test]
1593 fn engine_rejects_duplicate_primitive_names() {
1594 let profile = PrimitiveProfile {
1595 primitives: alloc::vec![
1596 PrimitiveSpec {
1597 name: Cow::Borrowed("input"),
1598 opcode: PrimitiveOpcode(1),
1599 kind: PrimitiveKind::Resource,
1600 },
1601 PrimitiveSpec {
1602 name: Cow::Borrowed("input"),
1603 opcode: PrimitiveOpcode(2),
1604 kind: PrimitiveKind::Resource,
1605 },
1606 ],
1607 };
1608 let format = FormatImage::empty("plain", profile.id());
1609
1610 let error = EngineBuilder::new(profile)
1611 .format(format)
1612 .resources(InMemoryResourceProvider::new())
1613 .platform(NoopPlatform::default())
1614 .build()
1615 .expect_err("duplicate primitive names should fail");
1616
1617 assert_eq!(
1618 error,
1619 BuildError::InvalidPrimitives(PrimitiveRegistryError::DuplicateName {
1620 name: "input".to_string(),
1621 })
1622 );
1623 }
1624
1625 #[test]
1626 fn session_loads_packages_through_configured_provider() {
1627 let profile = TexProfile;
1628 let format = FormatImage::empty("latex", profile.id());
1629 let resources = InMemoryResourceProvider::new().with_resource(
1630 "amsmath.sty",
1631 ResourceKind::Package,
1632 br"\ProvidesPackage{amsmath}".to_vec(),
1633 );
1634 let engine = EngineBuilder::new(profile)
1635 .format(format)
1636 .resources(resources)
1637 .platform(NoopPlatform::default())
1638 .build()
1639 .expect("engine should build");
1640
1641 let session = engine.new_session();
1642 let package = session
1643 .load_package("amsmath.sty")
1644 .expect("package should be loaded through provider");
1645
1646 assert_eq!(package.kind, ResourceKind::Package);
1647 assert_eq!(package.bytes, br"\ProvidesPackage{amsmath}".to_vec());
1648 }
1649
1650 #[test]
1651 fn session_loads_typed_resources_through_provider() {
1652 let profile = TexProfile;
1653 let format = FormatImage::empty("latex", profile.id());
1654 let mut resources = InMemoryResourceProvider::new()
1655 .with_resource("article.cls", ResourceKind::Class, b"class")
1656 .with_resource("ot1cmr.fd", ResourceKind::FontDefinition, b"fd")
1657 .with_resource("size10.clo", ResourceKind::PackageSupport, b"support")
1658 .with_resource("latinmodern-math.otf", ResourceKind::Font, b"font")
1659 .with_resource("t1.enc", ResourceKind::Encoding, b"encoding")
1660 .with_resource("pdftex.map", ResourceKind::Map, b"map")
1661 .with_resource("texmf.cnf", ResourceKind::Config, b"config")
1662 .with_resource("latex.fmt", ResourceKind::FormatImage, b"format");
1663 resources.insert_request(
1664 crate::ProviderResourceRequest::asset("mhchem", "arrows.dat"),
1665 b"asset",
1666 );
1667 let engine = EngineBuilder::new(profile)
1668 .format(format)
1669 .resources(resources)
1670 .platform(NoopPlatform::default())
1671 .build()
1672 .expect("engine should build");
1673 let session = engine.new_session();
1674
1675 let class = session.load_class("article.cls").expect("class");
1676 let font_definition = session
1677 .load_font_definition("ot1cmr.fd")
1678 .expect("font definition");
1679 let package_support = session
1680 .load_package_support("size10.clo")
1681 .expect("package support");
1682 let font = session
1683 .load_font_resource("latinmodern-math.otf")
1684 .expect("font");
1685 let encoding = session.load_encoding("t1.enc").expect("encoding");
1686 let map = session.load_map("pdftex.map").expect("map");
1687 let config = session.load_config("texmf.cnf").expect("config");
1688 let asset = session.load_asset("mhchem", "arrows.dat").expect("asset");
1689 let format_image = session
1690 .load_format_image("latex.fmt")
1691 .expect("format image");
1692
1693 assert_eq!(class.kind, ResourceKind::Class);
1694 assert_eq!(class.bytes, b"class");
1695 assert_eq!(font_definition.kind, ResourceKind::FontDefinition);
1696 assert_eq!(font_definition.bytes, b"fd");
1697 assert_eq!(package_support.kind, ResourceKind::PackageSupport);
1698 assert_eq!(package_support.bytes, b"support");
1699 assert_eq!(font.kind, ResourceKind::Font);
1700 assert_eq!(font.bytes, b"font");
1701 assert_eq!(encoding.kind, ResourceKind::Encoding);
1702 assert_eq!(encoding.bytes, b"encoding");
1703 assert_eq!(map.kind, ResourceKind::Map);
1704 assert_eq!(map.bytes, b"map");
1705 assert_eq!(config.kind, ResourceKind::Config);
1706 assert_eq!(config.bytes, b"config");
1707 assert_eq!(asset.kind, ResourceKind::Asset);
1708 assert_eq!(asset.canonical_name, "mhchem/arrows.dat");
1709 assert_eq!(asset.bytes, b"asset");
1710 assert_eq!(format_image.kind, ResourceKind::FormatImage);
1711 assert_eq!(format_image.bytes, b"format");
1712 }
1713
1714 #[test]
1715 fn repeated_sessions_reuse_cached_format_metadata() {
1716 let profile = TexProfile;
1717 let format = FormatImage::empty("latex+amsmath", profile.id());
1718 let engine = EngineBuilder::new(profile)
1719 .format(format)
1720 .resources(InMemoryResourceProvider::new())
1721 .platform(NoopPlatform::default())
1722 .build()
1723 .expect("engine should build");
1724
1725 let first = engine.new_session();
1726 let second = engine.new_session();
1727
1728 let first_ir = first.ir_emitter().finish();
1729 let second_ir = second.ir_emitter().finish();
1730
1731 assert_eq!(first_ir.metadata.format_id, "latex+amsmath");
1732 assert_eq!(second_ir.metadata.format_id, "latex+amsmath");
1733 assert_eq!(first_ir.metadata.engine_profile, "tex");
1734 assert_eq!(second_ir.metadata.engine_profile, "tex");
1735 }
1736
1737 #[test]
1738 fn layout_fragment_input_uses_generated_engine_without_manual_core() {
1739 let profile = TexProfile;
1740 let (format, resources) = basic_test_format(profile.id());
1741 let resources = resources
1742 .with_resource(
1743 "cmr10",
1744 ResourceKind::Font,
1745 include_bytes!("../../../vendor/texlive-source/texk/web2c/tests/cmr10.tfm")
1746 .to_vec(),
1747 )
1748 .with_resource(
1749 "cmmi10",
1750 ResourceKind::Font,
1751 include_bytes!(
1752 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmmi10.tfm"
1753 )
1754 .to_vec(),
1755 )
1756 .with_resource(
1757 "cmmi10.tfm",
1758 ResourceKind::Font,
1759 include_bytes!(
1760 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmmi10.tfm"
1761 )
1762 .to_vec(),
1763 )
1764 .with_resource(
1765 "cmsy10",
1766 ResourceKind::Font,
1767 include_bytes!(
1768 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmsy10.tfm"
1769 )
1770 .to_vec(),
1771 )
1772 .with_resource(
1773 "cmsy10.tfm",
1774 ResourceKind::Font,
1775 include_bytes!(
1776 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmsy10.tfm"
1777 )
1778 .to_vec(),
1779 )
1780 .with_resource(
1781 "cmex10",
1782 ResourceKind::Font,
1783 include_bytes!(
1784 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmex10.tfm"
1785 )
1786 .to_vec(),
1787 )
1788 .with_resource(
1789 "cmex10.tfm",
1790 ResourceKind::Font,
1791 include_bytes!(
1792 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmex10.tfm"
1793 )
1794 .to_vec(),
1795 );
1796 let engine = EngineBuilder::new(profile)
1797 .format(format)
1798 .resources(resources)
1799 .platform(NoopPlatform::default())
1800 .build()
1801 .expect("engine should build");
1802 let mut session = engine.new_session();
1803
1804 let body = r"\font\tenrm=cmr10 \tenrm A";
1805 let fragment = session
1806 .layout_fragment_input(FragmentInput::new("equation.tex", body, FragmentKind::Text))
1807 .expect("generated engine should layout a boxed text fragment");
1808 let _glyph_node = fragment
1809 .nodes
1810 .iter()
1811 .find(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_)))
1812 .expect("glyph run should be emitted");
1813
1814 assert_eq!(fragment.metadata.fragment_kind, FragmentKind::Text);
1815 }
1816
1817 #[test]
1818 fn layout_rejects_input_that_exceeds_platform_limit() {
1819 let profile = TexProfile;
1820 let format = FormatImage::empty("latex", profile.id());
1821 let platform = NoopPlatform::with_limits(HostLimits {
1822 max_input_bytes: 4,
1823 ..HostLimits::default()
1824 });
1825 let engine = EngineBuilder::new(profile)
1826 .format(format)
1827 .resources(InMemoryResourceProvider::new())
1828 .platform(platform)
1829 .build()
1830 .expect("engine should build");
1831 let mut session = engine.new_session();
1832
1833 let error = session
1834 .layout_fragment("12345")
1835 .expect_err("input should exceed limit");
1836
1837 assert_eq!(
1838 error,
1839 EngineError::Limit(LimitError::InputTooLarge {
1840 actual: 5,
1841 limit: 4,
1842 })
1843 );
1844 }
1845
1846 #[test]
1847 fn layout_fragment_loads_tex_inputs_inside_generated_engine() {
1848 let profile = TexProfile;
1849 let (format, resources) = basic_test_format(profile.id());
1850 let resources = resources.with_resource(
1851 "snippet.tex",
1852 ResourceKind::TexInput,
1853 br"\vrule width 1pt height 2pt depth 0pt".to_vec(),
1854 );
1855 let engine = EngineBuilder::new(profile)
1856 .format(format)
1857 .resources(resources)
1858 .platform(NoopPlatform::default())
1859 .build()
1860 .expect("engine should build");
1861 let mut session = engine.new_session();
1862
1863 let body = r"\input snippet.tex";
1864 let fragment = session
1865 .layout_fragment_input(FragmentInput::text(body))
1866 .expect("generated engine should own input loading");
1867 assert!(fragment
1868 .nodes
1869 .iter()
1870 .any(|node| matches!(node.kind, LayoutNodeKind::Rule(_))));
1871 }
1872
1873 #[test]
1874 fn layout_fragment_loads_packages_inside_generated_engine() {
1875 let profile = TexProfile;
1876 let (format, resources) = basic_test_format(profile.id());
1877 let resources = resources
1878 .with_resource(
1879 "cmr10",
1880 ResourceKind::Font,
1881 include_bytes!("../../../vendor/texlive-source/texk/web2c/tests/cmr10.tfm")
1882 .to_vec(),
1883 )
1884 .with_resource(
1885 "testpkg.sty",
1886 ResourceKind::Package,
1887 br"\def\pkgchar{A}".to_vec(),
1888 );
1889 let engine = EngineBuilder::new(profile)
1890 .format(format)
1891 .resources(resources)
1892 .platform(NoopPlatform::default())
1893 .build()
1894 .expect("engine should build");
1895 let mut session = engine.new_session();
1896
1897 let body = r"\font\tenrm=cmr10 \tenrm A \usepackage{testpkg}\pkgchar";
1898 let fragment = session
1899 .layout_fragment_input(FragmentInput::text(body))
1900 .expect("generated engine should expand package loading through TeX");
1901 assert!(fragment
1902 .nodes
1903 .iter()
1904 .any(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_))));
1905 }
1906
1907 #[test]
1908 fn generated_format_cache_reuses_preloaded_package_state() {
1909 let profile = TexProfile;
1910 let format_resources = InMemoryResourceProvider::new()
1911 .with_resource(TEST_FORMAT_INPUT, ResourceKind::TexInput, TEST_FORMAT_SETUP)
1912 .with_resource(
1913 "cachedpkg.sty",
1914 ResourceKind::Package,
1915 br"\def\cachedpkgchar{A}".to_vec(),
1916 );
1917 let format = FormatImage::initializer("latex+cachedpkg", profile.id())
1918 .preload_tex_input(TEST_FORMAT_INPUT)
1919 .preload_package("cachedpkg")
1920 .build(&format_resources)
1921 .expect("format should preload package bytes");
1922 let resources = InMemoryResourceProvider::new().with_resource(
1923 "cmr10",
1924 ResourceKind::Font,
1925 include_bytes!("../../../vendor/texlive-source/texk/web2c/tests/cmr10.tfm").to_vec(),
1926 );
1927 let engine = EngineBuilder::new(profile)
1928 .format(format)
1929 .resources(resources)
1930 .platform(NoopPlatform::default())
1931 .build()
1932 .expect("engine should build");
1933 let mut session = engine.new_session();
1934
1935 let body = r"\font\tenrm=cmr10 \tenrm \cachedpkgchar";
1936 let fragment = session
1937 .layout_fragment_input(FragmentInput::text(body))
1938 .expect("preloaded generated package macro should be cached");
1939 let has_glyph_node = fragment
1940 .nodes
1941 .iter()
1942 .any(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_)));
1943
1944 assert_eq!(fragment.metadata.format_id, "latex+cachedpkg");
1945 assert!(has_glyph_node);
1946 assert!(!fragment
1947 .source_map
1948 .sources
1949 .iter()
1950 .any(|source| source.name == "cachedpkg.sty"));
1951 }
1952
1953 #[test]
1954 fn generated_format_cache_reuses_preloaded_tex_input_state() {
1955 let profile = TexProfile;
1956 let format_resources = InMemoryResourceProvider::new()
1957 .with_resource(TEST_FORMAT_INPUT, ResourceKind::TexInput, TEST_FORMAT_SETUP)
1958 .with_resource(
1959 "plain.tex",
1960 ResourceKind::TexInput,
1961 br"\def\plainchar{A}".to_vec(),
1962 );
1963 let format = FormatImage::initializer("plain", profile.id())
1964 .preload_tex_input(TEST_FORMAT_INPUT)
1965 .preload_tex_input("plain.tex")
1966 .build(&format_resources)
1967 .expect("format should preload TeX input bytes");
1968 let resources = InMemoryResourceProvider::new().with_resource(
1969 "cmr10",
1970 ResourceKind::Font,
1971 include_bytes!("../../../vendor/texlive-source/texk/web2c/tests/cmr10.tfm").to_vec(),
1972 );
1973 let engine = EngineBuilder::new(profile)
1974 .format(format)
1975 .resources(resources)
1976 .platform(NoopPlatform::default())
1977 .build()
1978 .expect("engine should build");
1979 let mut session = engine.new_session();
1980
1981 let body = r"\font\tenrm=cmr10 \tenrm \plainchar";
1982 let fragment = session
1983 .layout_fragment_input(FragmentInput::text(body))
1984 .expect("preloaded generated TeX input macro should be cached");
1985 let has_glyph_node = fragment
1986 .nodes
1987 .iter()
1988 .any(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_)));
1989
1990 assert_eq!(fragment.metadata.format_id, "plain");
1991 assert!(has_glyph_node);
1992 assert!(!fragment
1993 .source_map
1994 .sources
1995 .iter()
1996 .any(|source| source.name == "plain.tex"));
1997 }
1998
1999 #[test]
2000 fn math_fragment_uses_format_owned_math_catcodes_and_fonts() {
2001 let profile = TexProfile;
2002 let (format, resources) = math_test_format(profile.id());
2003 let resources = resources
2004 .with_resource(
2005 "cmr10",
2006 ResourceKind::Font,
2007 include_bytes!("../../../vendor/texlive-source/texk/web2c/tests/cmr10.tfm")
2008 .to_vec(),
2009 )
2010 .with_resource(
2011 "cmmi10",
2012 ResourceKind::Font,
2013 include_bytes!(
2014 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmmi10.tfm"
2015 )
2016 .to_vec(),
2017 )
2018 .with_resource(
2019 "cmsy10",
2020 ResourceKind::Font,
2021 include_bytes!(
2022 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmsy10.tfm"
2023 )
2024 .to_vec(),
2025 )
2026 .with_resource(
2027 "cmex10",
2028 ResourceKind::Font,
2029 include_bytes!(
2030 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmex10.tfm"
2031 )
2032 .to_vec(),
2033 );
2034 let engine = EngineBuilder::new(profile)
2035 .format(format)
2036 .resources(resources)
2037 .platform(NoopPlatform::default())
2038 .build()
2039 .expect("engine should build");
2040 let mut session = engine.new_session();
2041
2042 let fragment = session
2043 .layout_fragment_input(FragmentInput::math_inline("x^2"))
2044 .expect("generated engine should layout superscript math from format catcodes");
2045 let glyph_count = fragment
2046 .nodes
2047 .iter()
2048 .filter(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_)))
2049 .count();
2050
2051 assert_eq!(fragment.metadata.format_id, "plain-math");
2052 assert!(glyph_count >= 2);
2053 }
2054
2055 #[cfg(feature = "std")]
2056 #[test]
2057 fn real_plain_format_loads_through_generated_engine_when_texlive_is_available() {
2058 if !HostResolverTestResources::is_available() {
2059 return;
2060 }
2061
2062 assert_real_plain_format_loads(HostResolverTestResources);
2063 }
2064
2065 #[test]
2066 fn real_plain_format_loads_from_vendored_fixture() {
2067 assert_real_plain_format_loads(PlainFixtureResources);
2068 }
2069
2070 #[cfg(feature = "std")]
2071 #[test]
2072 fn real_latex_format_loads_through_generated_engine_when_texlive_is_available() {
2073 if !HostResolverTestResources::is_latex_available() {
2074 return;
2075 }
2076
2077 let profile = XetexProfile;
2078 let format = FormatImage::latex(profile.id())
2079 .build(&HostResolverTestResources)
2080 .expect("real latex.ltx should resolve");
2081 let engine = EngineBuilder::new(profile)
2082 .format(format)
2083 .resources(HostResolverTestResources)
2084 .platform(NoopPlatform::default())
2085 .build()
2086 .expect("engine should build with real latex format");
2087 let mut session = engine.new_session();
2088
2089 let fragment = session
2090 .layout_fragment_input(FragmentInput::text(r"\font\tenrm=cmr10 \tenrm A"))
2091 .expect("real latex format should leave the engine usable for fragments");
2092 let has_glyph_node = fragment
2093 .nodes
2094 .iter()
2095 .any(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_)));
2096
2097 assert_eq!(fragment.metadata.format_id, "latex");
2098 assert!(has_glyph_node);
2099 }
2100
2101 #[test]
2102 fn math_fragment_supports_text_macro_inside_generated_engine() {
2103 let profile = TexProfile;
2104 let (format, resources) = math_test_format(profile.id());
2105 let resources = resources
2106 .with_resource(
2107 "cmr10",
2108 ResourceKind::Font,
2109 include_bytes!("../../../vendor/texlive-source/texk/web2c/tests/cmr10.tfm")
2110 .to_vec(),
2111 )
2112 .with_resource(
2113 "cmmi10",
2114 ResourceKind::Font,
2115 include_bytes!(
2116 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmmi10.tfm"
2117 )
2118 .to_vec(),
2119 )
2120 .with_resource(
2121 "cmsy10",
2122 ResourceKind::Font,
2123 include_bytes!(
2124 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmsy10.tfm"
2125 )
2126 .to_vec(),
2127 )
2128 .with_resource(
2129 "cmex10",
2130 ResourceKind::Font,
2131 include_bytes!(
2132 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmex10.tfm"
2133 )
2134 .to_vec(),
2135 );
2136 let engine = EngineBuilder::new(profile)
2137 .format(format)
2138 .resources(resources)
2139 .platform(NoopPlatform::default())
2140 .build()
2141 .expect("engine should build");
2142 let mut session = engine.new_session();
2143
2144 let body = r"\font\tenrm=cmr10 \font\teni=cmmi10 \font\tensy=cmsy10 \font\tenex=cmex10 \textfont0=\tenrm \scriptfont0=\tenrm \scriptscriptfont0=\tenrm \textfont1=\teni \scriptfont1=\teni \scriptscriptfont1=\teni \textfont2=\tensy \scriptfont2=\tensy \scriptscriptfont2=\tensy \textfont3=\tenex \scriptfont3=\tenex \scriptscriptfont3=\tenex \text{\tenrm A}";
2145 let fragment = session
2146 .layout_fragment_input(FragmentInput::math_inline(body))
2147 .expect("generated engine should allow text boxes inside math");
2148 let _glyph_node = fragment
2149 .nodes
2150 .iter()
2151 .find(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_)))
2152 .expect("text macro should emit glyph IR");
2153
2154 assert_eq!(fragment.metadata.fragment_kind, FragmentKind::MathInline);
2155 }
2156
2157 #[test]
2158 fn generated_package_loading_resolves_package_assets() {
2159 let profile = TexProfile;
2160 let (format, resources) = basic_test_format(profile.id());
2161 let mut resources = resources
2162 .with_resource(
2163 "cmr10",
2164 ResourceKind::Font,
2165 include_bytes!("../../../vendor/texlive-source/texk/web2c/tests/cmr10.tfm")
2166 .to_vec(),
2167 )
2168 .with_resource(
2169 "assetpkg.sty",
2170 ResourceKind::Package,
2171 br"\input data.dat\relax".to_vec(),
2172 );
2173 resources.insert_request(
2174 crate::ProviderResourceRequest::asset("assetpkg", "data.dat"),
2175 br"\def\assetpkgchar{A}".to_vec(),
2176 );
2177 let engine = EngineBuilder::new(profile)
2178 .format(format)
2179 .resources(resources)
2180 .platform(NoopPlatform::default())
2181 .build()
2182 .expect("engine should build");
2183 let mut session = engine.new_session();
2184
2185 let body = r"\font\tenrm=cmr10 \usepackage{assetpkg}\tenrm \assetpkgchar";
2186 let fragment = session
2187 .layout_fragment_input(FragmentInput::text(body))
2188 .expect("generated engine should load package-owned TeX assets");
2189
2190 assert!(fragment
2191 .nodes
2192 .iter()
2193 .any(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_))));
2194 }
2195
2196 #[test]
2197 fn generated_layout_does_not_expand_legacy_host_macros() {
2198 let profile = TexProfile;
2199 let format = FormatImage::with_snapshot(
2200 "latex+macros",
2201 profile.id(),
2202 FormatSnapshot {
2203 macros: alloc::vec![MacroDefinition {
2204 name: "speedtext".into(),
2205 replacement: br"\text{speed}".to_vec(),
2206 }],
2207 ..FormatSnapshot::default()
2208 },
2209 );
2210 let engine = EngineBuilder::new(profile)
2211 .format(format)
2212 .resources(InMemoryResourceProvider::new())
2213 .platform(NoopPlatform::default())
2214 .build()
2215 .expect("engine should build");
2216 let mut session = engine.new_session();
2217
2218 let error = session
2219 .layout_fragment(r"a+\speedtext")
2220 .expect_err("generated engine should not use legacy host macro tables");
2221
2222 match error {
2223 EngineError::Layout { message } => {
2224 assert!(message.contains("generated TeX engine reported an error"));
2225 }
2226 other => panic!("unexpected error: {other:?}"),
2227 }
2228 }
2229
2230 #[test]
2231 fn sessions_load_and_shape_text_through_font_system() {
2232 let profile = XetexProfile;
2233 let format = FormatImage::empty("latex", profile.id());
2234 let fonts = InMemoryFontSystem::new()
2235 .with_font("Latin Modern Math", b"font")
2236 .with_fallback_shaping();
2237 let engine = EngineBuilder::new(profile)
2238 .format(format)
2239 .resources(InMemoryResourceProvider::new())
2240 .platform(NoopPlatform::default())
2241 .fonts(fonts)
2242 .build()
2243 .expect("engine should build");
2244 let session = engine.new_session();
2245
2246 let font = session
2247 .load_font(&FontQuery {
2248 family: "Latin Modern Math".to_string(),
2249 size: Length::from_scaled_points(655_360),
2250 math: true,
2251 })
2252 .expect("font should load");
2253 let shaped = session
2254 .shape_text(&ShapeRequest {
2255 font: font.id,
2256 text: "xy",
2257 direction: Direction::LeftToRight,
2258 source: Some(ByteSpan { start: 10, end: 12 }),
2259 script: None,
2260 features: Vec::new(),
2261 })
2262 .expect("text should shape through font system");
2263
2264 assert_eq!(font.canonical_name, "Latin Modern Math");
2265 assert_eq!(shaped.glyphs.len(), 2);
2266 assert_eq!(
2267 shaped.glyphs[0].cluster,
2268 Some(ByteSpan { start: 10, end: 11 })
2269 );
2270 assert_eq!(
2271 shaped.glyphs[1].cluster,
2272 Some(ByteSpan { start: 11, end: 12 })
2273 );
2274 }
2275
2276 #[test]
2277 fn sessions_can_load_fonts_from_resource_provider_adapter() {
2278 let profile = XetexProfile;
2279 let format = FormatImage::empty("latex", profile.id());
2280 let font_resources = InMemoryResourceProvider::new().with_resource(
2281 "Latin Modern Math.otf",
2282 ResourceKind::Font,
2283 b"font-bytes",
2284 );
2285 let engine = EngineBuilder::new(profile)
2286 .format(format)
2287 .resources(InMemoryResourceProvider::new())
2288 .platform(NoopPlatform::default())
2289 .fonts(ResourceFontSystem::new(font_resources))
2290 .build()
2291 .expect("engine should build");
2292 let session = engine.new_session();
2293
2294 let font = session
2295 .load_font(&FontQuery {
2296 family: "Latin Modern Math.otf".to_string(),
2297 size: Length::from_scaled_points(655_360),
2298 math: true,
2299 })
2300 .expect("font should load from ResourceProvider");
2301
2302 assert_eq!(font.canonical_name, "Latin Modern Math.otf");
2303 assert_eq!(&**font.bytes().expect("library owned bytes"), b"font-bytes");
2304 assert!(session
2305 .shape_text(&ShapeRequest {
2306 font: font.id,
2307 text: "x",
2308 direction: Direction::LeftToRight,
2309 source: None,
2310 script: None,
2311 features: Vec::new(),
2312 })
2313 .is_err());
2314 }
2315
2316 #[test]
2317 fn sessions_clone_cached_format_state_without_sharing_mutations() {
2318 let profile = TexProfile;
2319 let format = FormatImage::with_snapshot(
2320 "latex+text",
2321 profile.id(),
2322 FormatSnapshot {
2323 macros: alloc::vec![MacroDefinition {
2324 name: "text".into(),
2325 replacement: b"initial".to_vec(),
2326 }],
2327 ..FormatSnapshot::default()
2328 },
2329 );
2330 let engine = EngineBuilder::new(profile)
2331 .format(format)
2332 .resources(InMemoryResourceProvider::new())
2333 .platform(NoopPlatform::default())
2334 .build()
2335 .expect("engine should build");
2336
2337 let mut first = engine.new_session();
2338 let second = engine.new_session();
2339
2340 first
2341 .state_mut()
2342 .define_macro("text", b"session-local".to_vec());
2343
2344 assert_eq!(
2345 first
2346 .state()
2347 .macro_definition("text")
2348 .expect("first session macro")
2349 .replacement,
2350 b"session-local".to_vec()
2351 );
2352 assert_eq!(
2353 second
2354 .state()
2355 .macro_definition("text")
2356 .expect("second session macro")
2357 .replacement,
2358 b"initial".to_vec()
2359 );
2360
2361 match &engine.format().state {
2362 crate::FormatState::Structured(snapshot) => {
2363 assert_eq!(snapshot.macros[0].replacement, b"initial".to_vec());
2364 }
2365 other => panic!("unexpected format state: {other:?}"),
2366 }
2367 }
2368
2369 #[test]
2370 fn session_creates_metadata_seeded_ir_emitter() {
2371 let profile = TexProfile;
2372 let format = FormatImage::empty("latex", profile.id());
2373 let engine = EngineBuilder::new(profile)
2374 .format(format)
2375 .resources(InMemoryResourceProvider::new())
2376 .platform(NoopPlatform::default())
2377 .build()
2378 .expect("engine should build");
2379 let session = engine.new_session();
2380
2381 let fragment = session.ir_emitter().finish();
2382
2383 assert_eq!(fragment.metadata.engine_profile, "tex");
2384 assert_eq!(fragment.metadata.format_id, "latex");
2385 assert_eq!(fragment.metadata.fragment_kind, FragmentKind::MathInline);
2386 }
2387
2388 #[test]
2389 fn session_creates_mode_specific_ir_emitter() {
2390 let profile = TexProfile;
2391 let format = FormatImage::empty("latex", profile.id());
2392 let engine = EngineBuilder::new(profile)
2393 .format(format)
2394 .resources(InMemoryResourceProvider::new())
2395 .platform(NoopPlatform::default())
2396 .build()
2397 .expect("engine should build");
2398 let session = engine.new_session();
2399
2400 let fragment = session.ir_emitter_for(FragmentKind::Text).finish();
2401
2402 assert_eq!(fragment.metadata.fragment_kind, FragmentKind::Text);
2403 }
2404}