1use alloc::format;
2use alloc::string::{String, ToString};
3
4use mathtex_font::{
5 FontData, FontError, FontQuery, FontSystem, NoFontSystem, ShapeRequest, ShapedText,
6};
7use mathtex_ir::{Fragment, FragmentKind, FragmentMetadata};
8
9use crate::emit::IrEmitter;
10use crate::format::{FormatImage, FormatResourceKind, SessionState};
11use crate::generated::{
12 generated_node_to_fragment, unpack_packaged_format, GeneratedFontSystemAdapter,
13 GeneratedFormatCache, GeneratedPlatformAdapter, GeneratedResourceProvider,
14};
15use crate::platform::{LimitError, Platform};
16use crate::primitive::{PrimitiveEntry, PrimitiveRegistry, PrimitiveRegistryError};
17use crate::profile::{EngineKind, EngineProfile, EngineSemantics};
18use crate::resource::{Resource, ResourceError, ResourceKind, ResourceProvider, ResourceRequest};
19
20type NativeFontTable = alloc::vec::Vec<(
22 mathtex_portable_engine_generated::PortableFontHandle,
23 String,
24 i32,
25)>;
26
27#[derive(Clone, Debug)]
29pub struct Engine<P, R, H, F = NoFontSystem> {
30 profile: P,
31 semantics: EngineSemantics,
32 format: FormatImage,
33 generated_format: GeneratedFormatCache,
34 resources: R,
35 platform: H,
36 fonts: F,
37 primitives: PrimitiveRegistry,
38 native_font_table: NativeFontTable,
39}
40
41impl<P, R, H> Engine<P, R, H>
42where
43 P: EngineProfile,
44 R: ResourceProvider,
45 H: Platform,
46{
47 pub fn new(
49 profile: P,
50 format: FormatImage,
51 resources: R,
52 platform: H,
53 ) -> Result<Self, BuildError> {
54 let semantics = EngineSemantics::from_profile(&profile);
55 let primitives = PrimitiveRegistry::from_specs(semantics.primitives())
56 .map_err(BuildError::InvalidPrimitives)?;
57 let generated_format = generated_format_for(&semantics, &format, &resources)
58 .map_err(BuildError::FormatPreloadFailed)?;
59 Ok(Self {
60 profile,
61 semantics,
62 generated_format,
63 format,
64 resources,
65 platform,
66 fonts: NoFontSystem,
67 primitives,
68 native_font_table: NativeFontTable::new(),
69 })
70 }
71}
72
73impl<P, R, H, F> Engine<P, R, H, F>
74where
75 P: EngineProfile,
76 R: ResourceProvider,
77 H: Platform,
78 F: FontSystem,
79{
80 pub fn new_with_fonts(
82 profile: P,
83 format: FormatImage,
84 resources: R,
85 platform: H,
86 fonts: F,
87 ) -> Result<Self, BuildError> {
88 let semantics = EngineSemantics::from_profile(&profile);
89 let primitives = PrimitiveRegistry::from_specs(semantics.primitives())
90 .map_err(BuildError::InvalidPrimitives)?;
91 let generated_format = generated_format_for(&semantics, &format, &resources)
92 .map_err(BuildError::FormatPreloadFailed)?;
93 Ok(Self {
94 profile,
95 semantics,
96 generated_format,
97 format,
98 resources,
99 platform,
100 fonts,
101 primitives,
102 native_font_table: NativeFontTable::new(),
103 })
104 }
105
106 pub fn from_packaged_with_fonts(
111 profile: P,
112 packaged: &[u8],
113 resources: R,
114 platform: H,
115 fonts: F,
116 ) -> Result<Self, BuildError> {
117 let semantics = EngineSemantics::from_profile(&profile);
118 let primitives = PrimitiveRegistry::from_specs(semantics.primitives())
119 .map_err(BuildError::InvalidPrimitives)?;
120 let Some((format_bytes, native_font_table)) = unpack_packaged_format(packaged) else {
121 return Err(BuildError::InvalidPackagedFormat);
122 };
123 let Some(generated_format) = GeneratedFormatCache::from_bytes(&format_bytes) else {
124 return Err(BuildError::InvalidPackagedFormat);
125 };
126 let format = FormatImage::empty("packaged", profile.id());
127 let engine = Self {
128 profile,
129 semantics,
130 generated_format,
131 format,
132 resources,
133 platform,
134 fonts,
135 primitives,
136 native_font_table,
137 };
138 engine.validate_packaged_fonts()?;
139 Ok(engine)
140 }
141
142 fn validate_packaged_fonts(&self) -> Result<(), BuildError> {
144 if self.native_font_table.is_empty() {
145 return Ok(());
146 }
147 let mut probe = self
148 .generated_format
149 .instantiate(
150 generated_profile_for(&self.semantics),
151 GeneratedResourceProvider::new(&self.resources),
152 )
153 .with_platform(GeneratedPlatformAdapter::new(&self.platform))
154 .with_font_platform(GeneratedFontSystemAdapter::new(&self.fonts));
155 if probe.restore_native_font_table(&self.native_font_table) {
156 Ok(())
157 } else {
158 Err(BuildError::PackagedFontsUnavailable)
159 }
160 }
161
162 pub fn builder(profile: P) -> EngineBuilder<P, (), (), NoFontSystem> {
164 EngineBuilder::new(profile)
165 }
166
167 #[must_use]
169 pub fn new_session(&self) -> EngineSession<'_, P, R, H, F> {
170 EngineSession {
171 engine: self,
172 state: self.format.instantiate_session_state(),
173 }
174 }
175
176 #[must_use]
178 pub fn profile(&self) -> &P {
179 &self.profile
180 }
181
182 #[must_use]
184 pub fn semantics(&self) -> &EngineSemantics {
185 &self.semantics
186 }
187
188 #[must_use]
190 pub fn format(&self) -> &FormatImage {
191 &self.format
192 }
193
194 #[must_use]
196 pub fn generated_format(&self) -> &GeneratedFormatCache {
197 &self.generated_format
198 }
199
200 #[must_use]
202 pub fn resources(&self) -> &R {
203 &self.resources
204 }
205
206 #[must_use]
208 pub fn platform(&self) -> &H {
209 &self.platform
210 }
211
212 #[must_use]
214 pub fn fonts(&self) -> &F {
215 &self.fonts
216 }
217
218 #[must_use]
220 pub fn primitives(&self) -> &PrimitiveRegistry {
221 &self.primitives
222 }
223}
224
225#[derive(Clone, Debug)]
227#[must_use]
228pub struct EngineBuilder<P, R, H, F = NoFontSystem> {
229 profile: P,
230 format: Option<FormatImage>,
231 packaged: Option<alloc::vec::Vec<u8>>,
232 resources: Option<R>,
233 platform: Option<H>,
234 fonts: F,
235}
236
237impl<P> EngineBuilder<P, (), (), NoFontSystem> {
238 pub fn new(profile: P) -> Self {
240 Self {
241 profile,
242 format: None,
243 packaged: None,
244 resources: None,
245 platform: None,
246 fonts: NoFontSystem,
247 }
248 }
249}
250
251impl<P, R, H, F> EngineBuilder<P, R, H, F> {
252 pub fn format(mut self, format: FormatImage) -> Self {
254 self.format = Some(format);
255 self
256 }
257
258 pub fn packaged_format(mut self, packaged: alloc::vec::Vec<u8>) -> Self {
261 self.packaged = Some(packaged);
262 self
263 }
264
265 pub fn resources<NextR>(self, resources: NextR) -> EngineBuilder<P, NextR, H, F> {
267 EngineBuilder {
268 profile: self.profile,
269 format: self.format,
270 packaged: self.packaged,
271 resources: Some(resources),
272 platform: self.platform,
273 fonts: self.fonts,
274 }
275 }
276
277 pub fn platform<NextH>(self, platform: NextH) -> EngineBuilder<P, R, NextH, F> {
279 EngineBuilder {
280 profile: self.profile,
281 format: self.format,
282 packaged: self.packaged,
283 resources: self.resources,
284 platform: Some(platform),
285 fonts: self.fonts,
286 }
287 }
288
289 pub fn fonts<NextF>(self, fonts: NextF) -> EngineBuilder<P, R, H, NextF> {
291 EngineBuilder {
292 profile: self.profile,
293 format: self.format,
294 packaged: self.packaged,
295 resources: self.resources,
296 platform: self.platform,
297 fonts,
298 }
299 }
300}
301
302impl<P, R, H, F> EngineBuilder<P, R, H, F>
303where
304 P: EngineProfile,
305 R: ResourceProvider,
306 H: Platform,
307 F: FontSystem,
308{
309 pub fn build(self) -> Result<Engine<P, R, H, F>, BuildError> {
311 if let Some(packaged) = self.packaged {
312 if self.format.is_some() {
313 return Err(BuildError::AmbiguousFormat);
314 }
315 let resources = self.resources.expect("resources are required by type");
316 let platform = self.platform.expect("platform is required by type");
317 return Engine::from_packaged_with_fonts(
318 self.profile,
319 &packaged,
320 resources,
321 platform,
322 self.fonts,
323 );
324 }
325 let format = self.format.ok_or(BuildError::MissingFormat)?;
326 if format.profile != self.profile.id() {
327 return Err(BuildError::ProfileFormatMismatch {
328 profile: self.profile.id().0,
329 format_profile: format.profile.0,
330 });
331 }
332
333 let resources = self.resources.expect("resources are required by type");
334 let platform = self.platform.expect("platform is required by type");
335
336 Engine::new_with_fonts(self.profile, format, resources, platform, self.fonts)
337 }
338}
339
340#[derive(Clone, Debug)]
342pub struct EngineSession<'a, P, R, H, F = NoFontSystem> {
343 engine: &'a Engine<P, R, H, F>,
344 state: SessionState,
345}
346
347impl<P, R, H, F> EngineSession<'_, P, R, H, F>
348where
349 P: EngineProfile,
350 R: ResourceProvider,
351 H: Platform,
352 F: FontSystem,
353{
354 pub fn layout_fragment(&mut self, input: &str) -> Result<Fragment, EngineError> {
356 self.layout_fragment_input(FragmentInput::math_inline(input))
357 }
358
359 pub fn layout_fragment_input(&mut self, input: FragmentInput) -> Result<Fragment, EngineError> {
361 self.check_input_limits(input.body.as_str())?;
362 let generated_source = GeneratedFragmentSource::new(&input);
363 let metadata = FragmentMetadata {
364 engine_profile: self.engine.semantics.profile.0.to_string(),
365 format_id: self.engine.format.id.clone(),
366 fragment_kind: input.kind,
367 };
368
369 let mut generated = self.generated_engine();
370 if !generated.begin_primary_input(
371 input.source_name.as_str(),
372 generated_source.tex.clone().into_bytes(),
373 ) {
374 return Err(EngineError::Layout {
375 message: "generated TeX engine rejected primary input".to_string(),
376 });
377 }
378 generated.begin_fragment_capture();
379 let ran_main_control = generated.run_main_control();
380 generated.end_fragment_capture();
381 if !ran_main_control {
382 let message = match generated.last_error_message() {
384 Some(error) => format!("generated TeX engine reported an error: {error}"),
385 None => generated_abort_error(generated.last_abort_status()),
386 };
387 return Err(EngineError::Layout { message });
388 }
389
390 self.check_resource_request_limits(generated.resource_request_count())?;
391 if let Some(message) = generated_transcript_error(generated.transcript_bytes()) {
392 return Err(EngineError::Layout { message });
393 }
394
395 let root = generated
396 .captured_fragment_root()
397 .ok_or_else(|| EngineError::Layout {
398 message: "generated TeX engine did not capture a fragment root".to_string(),
399 })?;
400 let fragment = generated_node_to_fragment(&generated, root, metadata).ok_or_else(|| {
401 EngineError::Layout {
402 message: "generated TeX engine could not convert captured fragment root"
403 .to_string(),
404 }
405 })?;
406 self.check_layout_node_limits(fragment.nodes.len())?;
407 Ok(fragment)
408 }
409
410 #[must_use]
412 pub fn generated_engine(&self) -> mathtex_portable_engine_generated::PortableTexEngine<'_> {
413 let mut engine = self
414 .engine
415 .generated_format
416 .instantiate(
417 generated_profile_for(&self.engine.semantics),
418 GeneratedResourceProvider::new(&self.engine.resources),
419 )
420 .with_platform(GeneratedPlatformAdapter::new(&self.engine.platform))
421 .with_font_platform(GeneratedFontSystemAdapter::new(&self.engine.fonts));
422 if !self.engine.native_font_table.is_empty() {
423 let restored = engine.restore_native_font_table(&self.engine.native_font_table);
425 debug_assert!(restored, "validated native font table failed to rebind");
426 }
427 engine
428 }
429
430 pub fn load_input(&self, name: &str) -> Result<Resource, ResourceError> {
432 self.engine.resources.read(name, ResourceKind::TexInput)
433 }
434
435 pub fn load_package(&self, name: &str) -> Result<Resource, ResourceError> {
437 self.engine.resources.read(name, ResourceKind::Package)
438 }
439
440 pub fn load_class(&self, name: &str) -> Result<Resource, ResourceError> {
442 self.engine.resources.read_class(name)
443 }
444
445 pub fn load_font_definition(&self, name: &str) -> Result<Resource, ResourceError> {
447 self.engine.resources.read_font_definition(name)
448 }
449
450 pub fn load_package_support(&self, name: &str) -> Result<Resource, ResourceError> {
452 self.engine.resources.read_package_support(name)
453 }
454
455 pub fn load_font_resource(&self, name: &str) -> Result<Resource, ResourceError> {
457 self.engine.resources.read_font(name)
458 }
459
460 pub fn load_encoding(&self, name: &str) -> Result<Resource, ResourceError> {
462 self.engine.resources.read_encoding(name)
463 }
464
465 pub fn load_map(&self, name: &str) -> Result<Resource, ResourceError> {
467 self.engine.resources.read_map(name)
468 }
469
470 pub fn load_config(&self, name: &str) -> Result<Resource, ResourceError> {
472 self.engine.resources.read_config(name)
473 }
474
475 pub fn load_asset(&self, package: &str, name: &str) -> Result<Resource, ResourceError> {
477 self.engine.resources.read_asset(package, name)
478 }
479
480 pub fn load_format_image(&self, name: &str) -> Result<Resource, ResourceError> {
482 self.engine.resources.read_format_image(name)
483 }
484
485 pub fn load_font(&self, query: &FontQuery) -> Result<FontData, FontError> {
487 self.engine.fonts.load_font(query)
488 }
489
490 pub fn shape_text(&self, request: &ShapeRequest<'_>) -> Result<ShapedText, FontError> {
492 self.engine.fonts.shape_text(request)
493 }
494
495 #[must_use]
497 pub fn primitive(&self, name: &str) -> Option<&PrimitiveEntry> {
498 self.engine.primitives.get(name)
499 }
500
501 #[must_use]
503 pub fn semantics(&self) -> &EngineSemantics {
504 &self.engine.semantics
505 }
506
507 #[must_use]
509 pub fn ir_emitter(&self) -> IrEmitter {
510 IrEmitter::new(FragmentMetadata {
511 engine_profile: self.engine.semantics.profile.0.to_string(),
512 format_id: self.engine.format.id.clone(),
513 fragment_kind: FragmentKind::MathInline,
514 })
515 }
516
517 #[must_use]
519 pub fn ir_emitter_for(&self, kind: FragmentKind) -> IrEmitter {
520 IrEmitter::new(FragmentMetadata {
521 engine_profile: self.engine.semantics.profile.0.to_string(),
522 format_id: self.engine.format.id.clone(),
523 fragment_kind: kind,
524 })
525 }
526
527 #[must_use]
529 pub fn state(&self) -> &SessionState {
530 &self.state
531 }
532
533 #[must_use]
535 pub fn state_mut(&mut self) -> &mut SessionState {
536 &mut self.state
537 }
538
539 fn check_input_limits(&self, input: &str) -> Result<(), EngineError> {
540 let actual = input.len();
541 let limit = self.engine.platform.limits().max_input_bytes;
542 if actual > limit {
543 return Err(EngineError::Limit(LimitError::InputTooLarge {
544 actual,
545 limit,
546 }));
547 }
548 Ok(())
549 }
550
551 fn check_resource_request_limits(&self, actual: usize) -> Result<(), EngineError> {
552 let limit = self.engine.platform.limits().max_resource_requests;
553 if actual > limit {
554 return Err(EngineError::Limit(LimitError::TooManyResourceRequests {
555 actual,
556 limit,
557 }));
558 }
559 Ok(())
560 }
561
562 fn check_layout_node_limits(&self, actual: usize) -> Result<(), EngineError> {
563 let limit = self.engine.platform.limits().max_layout_nodes;
564 if actual > limit {
565 return Err(EngineError::Limit(LimitError::TooManyLayoutNodes {
566 actual,
567 limit,
568 }));
569 }
570 Ok(())
571 }
572}
573
574fn generated_profile_for(
575 semantics: &EngineSemantics,
576) -> mathtex_portable_engine_generated::EngineProfile {
577 mathtex_portable_engine_generated::EngineProfile {
578 id: semantics.profile.0,
579 kind: match semantics.kind {
580 EngineKind::Tex => mathtex_portable_engine_generated::EngineProfileKind::Tex,
581 EngineKind::Etex => mathtex_portable_engine_generated::EngineProfileKind::Etex,
582 EngineKind::Xetex => mathtex_portable_engine_generated::EngineProfileKind::Xetex,
583 },
584 etex: semantics.extensions.etex,
585 xetex: semantics.extensions.xetex,
586 unicode_scalars: semantics.catcodes.unicode_scalars,
587 unicode_math: semantics.mathcodes.unicode_math,
588 native_fonts: semantics.fonts.host_native_fonts,
589 }
590}
591
592fn generated_format_for<R>(
593 semantics: &EngineSemantics,
594 format: &FormatImage,
595 resources: &R,
596) -> Result<GeneratedFormatCache, FormatPreloadError>
597where
598 R: ResourceProvider,
599{
600 let profile = generated_profile_for(semantics);
601 let base = GeneratedFormatCache::initialized(profile);
602 if !format_has_generated_preload_resources(format) {
603 return Ok(base);
604 }
605
606 let provider = CachedGeneratedFormatResources { format, resources };
607 let mut engine = base.instantiate(profile, GeneratedResourceProvider::new(provider));
608
609 for resource in generated_format_preload_resources(format) {
610 let mut bytes = Vec::with_capacity(resource.bytes.len() + GENERATED_FORMAT_FINALIZER.len());
611 bytes.extend_from_slice(resource.bytes.as_slice());
612 bytes.extend_from_slice(GENERATED_FORMAT_FINALIZER);
613 if !engine.begin_primary_input(resource.name.as_str(), bytes) {
614 return Err(FormatPreloadError::InputRejected {
615 name: resource.name.clone(),
616 });
617 }
618 let completed = engine.run_format_initialization();
619 if let Some(request) = generated_failed_resource_request(&engine) {
620 return Err(FormatPreloadError::Resource {
621 name: resource.name.clone(),
622 resource: request.name.clone(),
623 });
624 }
625 if let Some(message) = generated_transcript_error(engine.transcript_bytes()) {
626 return Err(FormatPreloadError::Transcript {
627 name: resource.name.clone(),
628 message,
629 });
630 }
631 if !completed {
632 if let Some(message) = generated_transcript_abort_error(engine.transcript_bytes()) {
633 return Err(FormatPreloadError::Transcript {
634 name: resource.name.clone(),
635 message,
636 });
637 }
638 return Err(FormatPreloadError::Aborted {
639 name: resource.name.clone(),
640 status: engine.last_abort_status(),
641 });
642 }
643 }
644 engine.finalize_trie();
646 Ok(GeneratedFormatCache::from_engine_owned(engine))
647}
648
649const GENERATED_FORMAT_FINALIZER: &[u8] = br"\dump";
650
651fn generated_failed_resource_request<'a>(
652 engine: &'a mathtex_portable_engine_generated::PortableTexEngine<'_>,
653) -> Option<&'a mathtex_portable_engine_generated::PortableResourceRequestRecord> {
654 engine.resource_request_records().iter().find(|request| {
655 request.byte_len.is_none() && !is_optional_generated_resource_probe(request)
656 })
657}
658
659fn is_optional_generated_resource_probe(
660 request: &mathtex_portable_engine_generated::PortableResourceRequestRecord,
661) -> bool {
662 let basename = generated_resource_probe_basename(request.name.as_str());
663 basename == "texsys.aux" || basename == "babel-texput.cfg"
664}
665
666fn generated_resource_probe_basename(mut name: &str) -> &str {
667 loop {
668 if let Some(stripped) = name.strip_prefix("./") {
669 name = stripped;
670 continue;
671 }
672 if let Some(stripped) = name.strip_prefix("[]") {
673 name = stripped;
674 continue;
675 }
676 if let Some(stripped) = name.strip_prefix(':') {
677 name = stripped;
678 continue;
679 }
680 break name;
681 }
682}
683
684fn generated_format_preload_resources(
685 format: &FormatImage,
686) -> impl Iterator<Item = &crate::format::FormatResource> {
687 match &format.state {
688 crate::format::FormatState::Structured(snapshot) => Some(snapshot.resources.iter()),
689 _ => None,
690 }
691 .into_iter()
692 .flatten()
693 .filter(|resource| {
694 matches!(
695 resource.kind,
696 FormatResourceKind::TexInput | FormatResourceKind::Package
697 )
698 })
699}
700
701fn format_has_generated_preload_resources(format: &FormatImage) -> bool {
702 let crate::format::FormatState::Structured(snapshot) = &format.state else {
703 return false;
704 };
705 snapshot.resources.iter().any(|resource| {
706 matches!(
707 resource.kind,
708 FormatResourceKind::TexInput | FormatResourceKind::Package
709 )
710 })
711}
712
713#[derive(Clone, Copy, Debug)]
714struct CachedGeneratedFormatResources<'a, R> {
715 format: &'a FormatImage,
716 resources: &'a R,
717}
718
719impl<R> ResourceProvider for CachedGeneratedFormatResources<'_, R>
720where
721 R: ResourceProvider,
722{
723 fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
724 if matches!(request.kind, ResourceKind::TexInput | ResourceKind::Package) {
725 if let Some(resource) = cached_format_resource(self.format, request) {
726 return Ok(resource);
727 }
728 }
729 self.resources.read_request(request)
730 }
731}
732
733fn cached_format_resource(format: &FormatImage, request: &ResourceRequest) -> Option<Resource> {
734 let crate::format::FormatState::Structured(snapshot) = &format.state else {
735 return None;
736 };
737 snapshot
738 .resources
739 .iter()
740 .find(|resource| {
741 format_resource_kind_to_resource_kind(resource.kind) == Some(request.kind)
742 && resource.name == request.canonical_name()
743 })
744 .map(|resource| Resource {
745 canonical_name: resource.name.clone(),
746 kind: request.kind,
747 bytes: resource.bytes.clone(),
748 })
749}
750
751fn format_resource_kind_to_resource_kind(kind: FormatResourceKind) -> Option<ResourceKind> {
752 match kind {
753 FormatResourceKind::TexInput => Some(ResourceKind::TexInput),
754 FormatResourceKind::Package => Some(ResourceKind::Package),
755 }
756}
757
758struct GeneratedFragmentSource {
759 tex: String,
760}
761
762impl GeneratedFragmentSource {
763 fn new(input: &FragmentInput) -> Self {
764 let mut tex = String::with_capacity(input.body.len() + 96);
765 match input.kind {
766 FragmentKind::Text => {
767 tex.push_str(r"\hbox{");
768 }
769 FragmentKind::MathInline => {
770 tex.push_str(r"\hbox{$");
771 }
772 FragmentKind::MathDisplay => {
773 tex.push_str(r"\hbox{$\displaystyle ");
774 }
775 _ => {
776 tex.push_str(r"\hbox{");
777 }
778 }
779
780 tex.push_str(input.body.as_str());
781
782 match input.kind {
784 FragmentKind::Text => tex.push_str(r"\relax}\csname @@end\endcsname\end"),
785 FragmentKind::MathInline | FragmentKind::MathDisplay => {
786 tex.push_str(r"\relax$}\csname @@end\endcsname\end")
787 }
788 _ => tex.push_str(r"\relax}\csname @@end\endcsname\end"),
789 }
790
791 Self { tex }
792 }
793}
794
795fn generated_transcript_error(transcript: &[u8]) -> Option<String> {
796 let transcript = core::str::from_utf8(transcript).ok()?;
797 for (error_start, _) in transcript.match_indices('!').rev() {
798 let error_tail = &transcript[error_start..];
799 let error = error_tail
800 .lines()
801 .next()
802 .unwrap_or("generated TeX engine reported an error")
803 .trim();
804 if !error.starts_with("! ") {
805 continue;
806 }
807 let has_line_context = error_tail
808 .lines()
809 .skip(1)
810 .take(4)
811 .any(|line| line.trim_start().starts_with("l."));
812 let has_compact_line_context = error_tail
813 .get(..error_tail.len().min(512))
814 .is_some_and(|tail| tail.contains("l."));
815 if has_line_context || has_compact_line_context || error.contains("Emergency stop") {
816 return Some(format!("generated TeX engine reported an error: {error}"));
817 }
818 }
819 None
820}
821
822fn generated_transcript_abort_error(transcript: &[u8]) -> Option<String> {
823 let transcript = core::str::from_utf8(transcript).ok()?;
824 transcript
825 .match_indices('!')
826 .rev()
827 .find_map(|(error_start, _)| {
828 let error = transcript[error_start..].lines().next()?.trim();
829 error
830 .starts_with("! ")
831 .then(|| format!("generated TeX engine aborted after error: {error}"))
832 })
833}
834
835fn generated_abort_error(status: Option<i32>) -> String {
836 match status {
837 Some(status) => format!("generated TeX engine aborted with status {status}"),
838 None => "generated TeX engine aborted".to_string(),
839 }
840}
841
842#[derive(Clone, Debug, PartialEq, Eq)]
844pub struct FragmentInput {
845 pub source_name: String,
847 pub body: String,
849 pub kind: FragmentKind,
851}
852
853impl FragmentInput {
854 #[must_use]
856 pub fn math_inline(body: impl Into<String>) -> Self {
857 Self::new("input", body, FragmentKind::MathInline)
858 }
859
860 #[must_use]
862 pub fn math_display(body: impl Into<String>) -> Self {
863 Self::new("input", body, FragmentKind::MathDisplay)
864 }
865
866 #[must_use]
868 pub fn text(body: impl Into<String>) -> Self {
869 Self::new("input", body, FragmentKind::Text)
870 }
871
872 #[must_use]
874 pub fn new(
875 source_name: impl Into<String>,
876 body: impl Into<String>,
877 kind: FragmentKind,
878 ) -> Self {
879 Self {
880 source_name: source_name.into(),
881 body: body.into(),
882 kind,
883 }
884 }
885}
886
887#[derive(Clone, Debug, PartialEq, Eq)]
889#[non_exhaustive]
890pub enum BuildError {
891 MissingFormat,
893 InvalidPrimitives(PrimitiveRegistryError),
895 FormatPreloadFailed(FormatPreloadError),
897 ProfileFormatMismatch {
899 profile: &'static str,
901 format_profile: &'static str,
903 },
904 InvalidPackagedFormat,
906 PackagedFontsUnavailable,
908 AmbiguousFormat,
910}
911
912#[derive(Clone, Debug, PartialEq, Eq)]
914#[non_exhaustive]
915pub enum FormatPreloadError {
916 InputRejected {
918 name: String,
920 },
921 Aborted {
923 name: String,
925 status: Option<i32>,
927 },
928 Resource {
930 name: String,
932 resource: String,
934 },
935 Transcript {
937 name: String,
939 message: String,
941 },
942}
943
944#[derive(Clone, Debug, PartialEq, Eq)]
946#[non_exhaustive]
947pub enum EngineError {
948 Limit(LimitError),
950 Parse {
952 message: String,
954 },
955 Layout {
957 message: String,
959 },
960 Resource {
962 message: String,
964 },
965}
966
967#[cfg(test)]
968mod tests {
969 use alloc::borrow::Cow;
970 use alloc::vec::Vec;
971
972 use super::*;
973 use crate::{
974 EngineKind, EnginePatch, EngineProfile, ExtensionPolicy, FormatImage, FormatSnapshot,
975 HostLimits, InMemoryResourceProvider, LimitError, MacroDefinition, NoopPlatform,
976 PrimitiveKind, PrimitiveOpcode, PrimitiveRegistryError, PrimitiveSpec, ProfileId,
977 ResourceFontSystem, TexProfile, XetexProfile,
978 };
979 use mathtex_font::InMemoryFontSystem;
980 use mathtex_ir::{ByteSpan, Direction, FragmentKind, LayoutNodeKind, Length};
981
982 const TEST_FORMAT_INPUT: &str = "mathtex-test-format.tex";
983 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}} \def\hostbox#1{\Uhostbox #1\relax}";
984 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}} \def\hostbox#1{\Uhostbox #1\relax} \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";
985
986 fn test_format(
987 id: &'static str,
988 profile: ProfileId,
989 setup: &'static [u8],
990 ) -> (FormatImage, InMemoryResourceProvider) {
991 let resources = InMemoryResourceProvider::new().with_resource(
992 TEST_FORMAT_INPUT,
993 ResourceKind::TexInput,
994 setup,
995 );
996 let format = FormatImage::initializer(id, profile)
997 .preload_tex_input(TEST_FORMAT_INPUT)
998 .build(&resources)
999 .expect("test format should preload setup");
1000 (format, resources)
1001 }
1002
1003 fn basic_test_format(profile: ProfileId) -> (FormatImage, InMemoryResourceProvider) {
1004 test_format("latex", profile, TEST_FORMAT_SETUP)
1005 }
1006
1007 fn math_test_format(profile: ProfileId) -> (FormatImage, InMemoryResourceProvider) {
1008 test_format("plain-math", profile, TEST_MATH_FONT_SETUP)
1009 }
1010
1011 #[derive(Clone, Copy, Debug)]
1012 struct PlainFixtureResources;
1013
1014 impl PlainFixtureResources {
1015 const TFMS: &'static [(&'static str, &'static [u8])] = &[
1016 (
1017 "cmbsy10",
1018 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmbsy10.tfm"),
1019 ),
1020 (
1021 "cmbx10",
1022 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmbx10.tfm"),
1023 ),
1024 (
1025 "cmbx5",
1026 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmbx5.tfm"),
1027 ),
1028 (
1029 "cmbx6",
1030 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmbx6.tfm"),
1031 ),
1032 (
1033 "cmbx7",
1034 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmbx7.tfm"),
1035 ),
1036 (
1037 "cmbx8",
1038 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmbx8.tfm"),
1039 ),
1040 (
1041 "cmbx9",
1042 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmbx9.tfm"),
1043 ),
1044 (
1045 "cmcsc10",
1046 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmcsc10.tfm"),
1047 ),
1048 (
1049 "cmdunh10",
1050 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmdunh10.tfm"),
1051 ),
1052 (
1053 "cmex10",
1054 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmex10.tfm"),
1055 ),
1056 (
1057 "cmmi10",
1058 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmmi10.tfm"),
1059 ),
1060 (
1061 "cmmi5",
1062 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmmi5.tfm"),
1063 ),
1064 (
1065 "cmmi6",
1066 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmmi6.tfm"),
1067 ),
1068 (
1069 "cmmi7",
1070 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmmi7.tfm"),
1071 ),
1072 (
1073 "cmmi8",
1074 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmmi8.tfm"),
1075 ),
1076 (
1077 "cmmi9",
1078 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmmi9.tfm"),
1079 ),
1080 (
1081 "cmmib10",
1082 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmmib10.tfm"),
1083 ),
1084 (
1085 "cmr10",
1086 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmr10.tfm"),
1087 ),
1088 (
1089 "cmr5",
1090 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmr5.tfm"),
1091 ),
1092 (
1093 "cmr6",
1094 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmr6.tfm"),
1095 ),
1096 (
1097 "cmr7",
1098 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmr7.tfm"),
1099 ),
1100 (
1101 "cmr8",
1102 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmr8.tfm"),
1103 ),
1104 (
1105 "cmr9",
1106 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmr9.tfm"),
1107 ),
1108 (
1109 "cmsl10",
1110 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsl10.tfm"),
1111 ),
1112 (
1113 "cmsl8",
1114 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsl8.tfm"),
1115 ),
1116 (
1117 "cmsl9",
1118 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsl9.tfm"),
1119 ),
1120 (
1121 "cmsltt10",
1122 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsltt10.tfm"),
1123 ),
1124 (
1125 "cmss10",
1126 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmss10.tfm"),
1127 ),
1128 (
1129 "cmssbx10",
1130 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmssbx10.tfm"),
1131 ),
1132 (
1133 "cmssi10",
1134 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmssi10.tfm"),
1135 ),
1136 (
1137 "cmssq8",
1138 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmssq8.tfm"),
1139 ),
1140 (
1141 "cmssqi8",
1142 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmssqi8.tfm"),
1143 ),
1144 (
1145 "cmsy10",
1146 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsy10.tfm"),
1147 ),
1148 (
1149 "cmsy5",
1150 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsy5.tfm"),
1151 ),
1152 (
1153 "cmsy6",
1154 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsy6.tfm"),
1155 ),
1156 (
1157 "cmsy7",
1158 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsy7.tfm"),
1159 ),
1160 (
1161 "cmsy8",
1162 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsy8.tfm"),
1163 ),
1164 (
1165 "cmsy9",
1166 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmsy9.tfm"),
1167 ),
1168 (
1169 "cmti10",
1170 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmti10.tfm"),
1171 ),
1172 (
1173 "cmti7",
1174 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmti7.tfm"),
1175 ),
1176 (
1177 "cmti8",
1178 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmti8.tfm"),
1179 ),
1180 (
1181 "cmti9",
1182 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmti9.tfm"),
1183 ),
1184 (
1185 "cmtt10",
1186 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmtt10.tfm"),
1187 ),
1188 (
1189 "cmtt8",
1190 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmtt8.tfm"),
1191 ),
1192 (
1193 "cmtt9",
1194 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmtt9.tfm"),
1195 ),
1196 (
1197 "cmu10",
1198 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/cmu10.tfm"),
1199 ),
1200 (
1201 "manfnt",
1202 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/tfm/manfnt.tfm"),
1203 ),
1204 ];
1205
1206 fn base_name(name: &str) -> &str {
1207 name.rsplit(['/', '\\']).next().unwrap_or(name)
1208 }
1209
1210 fn font_name(name: &str) -> &str {
1211 let name = Self::base_name(name);
1212 name.strip_suffix(".tfm").unwrap_or(name)
1213 }
1214 }
1215
1216 impl ResourceProvider for PlainFixtureResources {
1217 fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
1218 let name = request.canonical_name();
1219 let base_name = Self::base_name(name.as_str());
1220 let bytes = match request.kind {
1221 ResourceKind::TexInput if base_name == "plain.tex" => Some(
1222 include_bytes!("../../../vendor/mathtex-fixtures/plain-tex/base/plain.tex")
1223 .as_slice(),
1224 ),
1225 ResourceKind::TexInput if base_name == "hyphen" || base_name == "hyphen.tex" => {
1226 Some(
1227 include_bytes!(
1228 "../../../vendor/mathtex-fixtures/plain-tex/hyphen/hyphen.tex"
1229 )
1230 .as_slice(),
1231 )
1232 }
1233 ResourceKind::Font => {
1234 let font_name = Self::font_name(base_name);
1235 Self::TFMS
1236 .iter()
1237 .find_map(|(name, bytes)| (*name == font_name).then_some(*bytes))
1238 }
1239 _ => None,
1240 };
1241
1242 bytes
1243 .map(|bytes| Resource::from_request(request, bytes.to_vec()))
1244 .ok_or_else(|| ResourceError::NotFound {
1245 name,
1246 kind: request.kind,
1247 })
1248 }
1249 }
1250
1251 fn assert_real_plain_format_loads<R>(resources: R)
1252 where
1253 R: ResourceProvider + Copy,
1254 {
1255 let profile = TexProfile;
1256 let format = FormatImage::plain(profile.id())
1257 .build(&resources)
1258 .expect("real plain.tex should resolve");
1259 let engine = EngineBuilder::new(profile)
1260 .format(format)
1261 .resources(resources)
1262 .platform(NoopPlatform::default())
1263 .build()
1264 .expect("engine should build with real plain format");
1265 let mut session = engine.new_session();
1266
1267 let fragment = session
1268 .layout_fragment_input(FragmentInput::text(r"\tenrm A"))
1269 .expect("real plain format should define plain font macros");
1270 let has_glyph_node = fragment
1271 .nodes
1272 .iter()
1273 .any(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_)));
1274
1275 assert_eq!(fragment.metadata.format_id, "plain");
1276 assert!(has_glyph_node);
1277 }
1278
1279 #[cfg(feature = "std")]
1280 #[derive(Clone, Copy, Debug)]
1281 struct HostResolverTestResources;
1282
1283 #[cfg(feature = "std")]
1284 impl HostResolverTestResources {
1285 fn is_available() -> bool {
1286 Self::resolve("plain.tex").is_some()
1287 && Self::resolve("hyphen.tex").is_some()
1288 && Self::resolve("cmr10.tfm").is_some()
1289 }
1290
1291 fn is_latex_available() -> bool {
1292 Self::resolve("latex.ltx").is_some() && Self::resolve("cmr10.tfm").is_some()
1293 }
1294
1295 fn resolve(name: &str) -> Option<Vec<u8>> {
1296 let output = std::process::Command::new("kpsewhich")
1297 .arg(name)
1298 .output()
1299 .ok()?;
1300 if !output.status.success() {
1301 return None;
1302 }
1303 let path = core::str::from_utf8(&output.stdout).ok()?.trim();
1304 if path.is_empty() {
1305 return None;
1306 }
1307 std::fs::read(path).ok()
1308 }
1309
1310 fn candidate_names(request: &ResourceRequest) -> Vec<String> {
1311 let name = normalize_host_resolver_name(request.canonical_name());
1312 let mut candidates = Vec::with_capacity(4);
1313 candidates.push(name.clone());
1314
1315 if std::path::Path::new(&name).extension().is_none() {
1316 match request.kind {
1317 ResourceKind::Package => candidates.push(format!("{name}.sty")),
1318 ResourceKind::Class => candidates.push(format!("{name}.cls")),
1319 ResourceKind::FontDefinition => candidates.push(format!("{name}.fd")),
1320 ResourceKind::Font => {
1321 candidates.push(format!("{name}.tfm"));
1322 candidates.push(format!("{name}.otf"));
1323 candidates.push(format!("{name}.ttf"));
1324 }
1325 ResourceKind::Encoding => candidates.push(format!("{name}.enc")),
1326 ResourceKind::Map => candidates.push(format!("{name}.map")),
1327 _ => {}
1328 }
1329 }
1330
1331 candidates
1332 }
1333 }
1334
1335 #[cfg(feature = "std")]
1336 fn normalize_host_resolver_name(mut name: String) -> String {
1337 while let Some(stripped) = name.strip_prefix("./") {
1338 name = stripped.to_string();
1339 }
1340 name
1341 }
1342
1343 #[cfg(feature = "std")]
1344 impl ResourceProvider for HostResolverTestResources {
1345 fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
1346 for candidate in Self::candidate_names(request) {
1347 if let Some(bytes) = Self::resolve(&candidate) {
1348 return Ok(Resource::from_request(request, bytes));
1349 }
1350 }
1351
1352 Err(ResourceError::NotFound {
1353 name: request.canonical_name(),
1354 kind: request.kind,
1355 })
1356 }
1357 }
1358
1359 #[test]
1360 fn builder_rejects_format_for_different_profile() {
1361 let format = FormatImage::empty("latex", XetexProfile.id());
1362
1363 let error = EngineBuilder::new(TexProfile)
1364 .format(format)
1365 .resources(InMemoryResourceProvider::new())
1366 .platform(NoopPlatform::default())
1367 .build()
1368 .expect_err("profile mismatch must be rejected");
1369
1370 assert_eq!(
1371 error,
1372 BuildError::ProfileFormatMismatch {
1373 profile: "tex",
1374 format_profile: "xetex",
1375 }
1376 );
1377 }
1378
1379 #[test]
1380 fn transcript_error_detection_requires_tex_error_shape() {
1381 assert_eq!(
1382 generated_transcript_error(b"LaTeX says hello! This is only a message."),
1383 None
1384 );
1385 assert_eq!(
1386 generated_transcript_error(b"before,! Emergency stop.\nl.12 \\bad\n"),
1387 Some("generated TeX engine reported an error: ! Emergency stop.".to_string())
1388 );
1389 }
1390
1391 #[test]
1392 fn builder_reports_generated_format_preload_failure() {
1393 let profile = TexProfile;
1394 let resources = InMemoryResourceProvider::new().with_resource(
1395 TEST_FORMAT_INPUT,
1396 ResourceKind::TexInput,
1397 br"\input missing-format-dependency.tex".to_vec(),
1398 );
1399 let format = FormatImage::initializer("missing-dependency", profile.id())
1400 .preload_tex_input(TEST_FORMAT_INPUT)
1401 .build(&resources)
1402 .expect("format initializer should capture preloaded resource bytes");
1403
1404 let error = EngineBuilder::new(profile)
1405 .format(format)
1406 .resources(resources)
1407 .platform(NoopPlatform::default())
1408 .build()
1409 .expect_err("missing nested preload resource must fail engine build");
1410
1411 match error {
1412 BuildError::FormatPreloadFailed(FormatPreloadError::Resource { name, resource }) => {
1413 assert_eq!(name, TEST_FORMAT_INPUT);
1414 assert_eq!(resource, "missing-format-dependency.tex");
1415 }
1416 other => panic!("unexpected preload error: {other:?}"),
1417 }
1418 }
1419
1420 #[test]
1421 fn session_instantiates_generated_engine_from_cached_format() {
1422 let format = FormatImage::empty("latex", XetexProfile.id());
1423 let engine = EngineBuilder::new(XetexProfile)
1424 .format(format)
1425 .resources(InMemoryResourceProvider::new())
1426 .platform(NoopPlatform::default())
1427 .build()
1428 .expect("engine should build");
1429 let session = engine.new_session();
1430 let generated = session.generated_engine();
1431
1432 assert_eq!(
1433 generated.profile(),
1434 mathtex_portable_engine_generated::EngineProfile::xetex()
1435 );
1436 assert!(!generated.profile().native_fonts);
1437 assert_eq!(generated.resource_request_count(), 0);
1438 }
1439
1440 #[derive(Clone, Debug)]
1441 struct CustomSemanticProfile {
1442 fonts: crate::FontSemantics,
1443 extensions: ExtensionPolicy,
1444 }
1445
1446 impl EngineProfile for CustomSemanticProfile {
1447 fn id(&self) -> ProfileId {
1448 ProfileId("custom-semantic")
1449 }
1450
1451 fn kind(&self) -> EngineKind {
1452 EngineKind::Xetex
1453 }
1454
1455 fn primitives(&self) -> &[PrimitiveSpec] {
1456 &[]
1457 }
1458
1459 fn catcode_defaults(&self) -> crate::CatcodeDefaults {
1460 crate::CatcodeDefaults {
1461 unicode_scalars: self.extensions.xetex,
1462 }
1463 }
1464
1465 fn mathcode_defaults(&self) -> crate::MathcodeDefaults {
1466 crate::MathcodeDefaults {
1467 unicode_math: false,
1468 }
1469 }
1470
1471 fn register_defaults(&self) -> crate::RegisterDefaults {
1472 crate::RegisterDefaults::extended()
1473 }
1474
1475 fn font_semantics(&self) -> crate::FontSemantics {
1476 self.fonts
1477 }
1478
1479 fn extension_policy(&self) -> ExtensionPolicy {
1480 self.extensions
1481 }
1482 }
1483
1484 #[test]
1485 fn generated_engine_uses_custom_profile_semantic_flags() {
1486 let profile = CustomSemanticProfile {
1487 fonts: crate::FontSemantics::tex(),
1488 extensions: ExtensionPolicy {
1489 etex: true,
1490 xetex: true,
1491 },
1492 };
1493 let format = FormatImage::empty("custom-semantic", profile.id());
1494 let engine = EngineBuilder::new(profile)
1495 .format(format)
1496 .resources(InMemoryResourceProvider::new())
1497 .platform(NoopPlatform::default())
1498 .build()
1499 .expect("engine should build");
1500 let generated = engine.new_session().generated_engine().profile();
1501
1502 assert_eq!(generated.id, "custom-semantic");
1503 assert_eq!(
1504 generated.kind,
1505 mathtex_portable_engine_generated::EngineProfileKind::Xetex
1506 );
1507 assert!(generated.etex);
1508 assert!(generated.xetex);
1509 assert!(generated.unicode_scalars);
1510 assert!(!generated.unicode_math);
1511 assert!(!generated.native_fonts);
1512 }
1513
1514 #[derive(Clone, Debug)]
1515 struct PrimitiveProfile {
1516 primitives: Vec<PrimitiveSpec>,
1517 }
1518
1519 impl EngineProfile for PrimitiveProfile {
1520 fn id(&self) -> ProfileId {
1521 ProfileId("primitive-profile")
1522 }
1523
1524 fn kind(&self) -> EngineKind {
1525 EngineKind::Etex
1526 }
1527
1528 fn primitives(&self) -> &[PrimitiveSpec] {
1529 &self.primitives
1530 }
1531
1532 fn catcode_defaults(&self) -> crate::CatcodeDefaults {
1533 crate::CatcodeDefaults::default()
1534 }
1535
1536 fn mathcode_defaults(&self) -> crate::MathcodeDefaults {
1537 crate::MathcodeDefaults::default()
1538 }
1539
1540 fn register_defaults(&self) -> crate::RegisterDefaults {
1541 crate::RegisterDefaults::default()
1542 }
1543
1544 fn font_semantics(&self) -> crate::FontSemantics {
1545 crate::FontSemantics::default()
1546 }
1547
1548 fn extension_policy(&self) -> crate::ExtensionPolicy {
1549 crate::ExtensionPolicy::default()
1550 }
1551 }
1552
1553 #[test]
1554 fn engine_exposes_profile_primitive_dispatch_entries() {
1555 let profile = PrimitiveProfile {
1556 primitives: alloc::vec![PrimitiveSpec {
1557 name: Cow::Borrowed("hbox"),
1558 opcode: PrimitiveOpcode(44),
1559 kind: PrimitiveKind::Layout,
1560 }],
1561 };
1562 let format = FormatImage::empty("plain", profile.id());
1563 let engine = EngineBuilder::new(profile)
1564 .format(format)
1565 .resources(InMemoryResourceProvider::new())
1566 .platform(NoopPlatform::default())
1567 .build()
1568 .expect("engine should build");
1569 let session = engine.new_session();
1570
1571 let primitive = session.primitive("hbox").expect("hbox primitive");
1572
1573 assert_eq!(primitive.opcode, PrimitiveOpcode(44));
1574 assert_eq!(primitive.kind, PrimitiveKind::Layout);
1575 assert!(session.primitive("missing").is_none());
1576 }
1577
1578 #[test]
1579 fn engine_caches_runtime_semantics_for_shared_tex_core() {
1580 let profile = TexProfile;
1581 let format = FormatImage::empty("plain", profile.id());
1582 let engine = EngineBuilder::new(profile)
1583 .format(format)
1584 .resources(InMemoryResourceProvider::new())
1585 .platform(NoopPlatform::default())
1586 .build()
1587 .expect("engine should build");
1588 let session = engine.new_session();
1589
1590 assert_eq!(engine.semantics().profile, ProfileId("tex"));
1591 assert_eq!(session.semantics(), engine.semantics());
1592 assert!(!engine.semantics().has_patch(EnginePatch::Xetex));
1593 assert!(!engine.semantics().is_xetex());
1594 assert!(!engine.semantics().catcodes.unicode_scalars);
1595 }
1596
1597 #[test]
1598 fn xetex_profile_is_runtime_patch_over_shared_core() {
1599 let profile = XetexProfile;
1600 let format = FormatImage::empty("latex", profile.id());
1601 let engine = EngineBuilder::new(profile)
1602 .format(format)
1603 .resources(InMemoryResourceProvider::new())
1604 .platform(NoopPlatform::default())
1605 .build()
1606 .expect("engine should build");
1607 let session = engine.new_session();
1608
1609 assert_eq!(engine.semantics().profile, ProfileId("xetex"));
1610 assert_eq!(engine.semantics().kind, EngineKind::Xetex);
1611 assert!(engine.semantics().has_patch(EnginePatch::Etex));
1612 assert!(engine.semantics().has_patch(EnginePatch::Xetex));
1613 assert!(engine.semantics().is_xetex());
1614 assert!(engine.semantics().catcodes.unicode_scalars);
1615 assert!(engine.semantics().fonts.shaped_text);
1616 assert!(session.primitive("XeTeXrevision").is_some());
1617 }
1618
1619 #[derive(Clone, Debug)]
1620 struct PolicyProfile {
1621 primitives: Vec<PrimitiveSpec>,
1622 extensions: ExtensionPolicy,
1623 }
1624
1625 impl EngineProfile for PolicyProfile {
1626 fn id(&self) -> ProfileId {
1627 ProfileId("policy-profile")
1628 }
1629
1630 fn kind(&self) -> EngineKind {
1631 EngineKind::Xetex
1632 }
1633
1634 fn primitives(&self) -> &[PrimitiveSpec] {
1635 &self.primitives
1636 }
1637
1638 fn catcode_defaults(&self) -> crate::CatcodeDefaults {
1639 crate::CatcodeDefaults {
1640 unicode_scalars: self.extensions.xetex,
1641 }
1642 }
1643
1644 fn mathcode_defaults(&self) -> crate::MathcodeDefaults {
1645 crate::MathcodeDefaults {
1646 unicode_math: self.extensions.xetex,
1647 }
1648 }
1649
1650 fn register_defaults(&self) -> crate::RegisterDefaults {
1651 crate::RegisterDefaults::extended()
1652 }
1653
1654 fn font_semantics(&self) -> crate::FontSemantics {
1655 crate::FontSemantics {
1656 unicode_fonts: self.extensions.xetex,
1657 shaped_text: self.extensions.xetex,
1658 unicode_math_fonts: self.extensions.xetex,
1659 host_native_fonts: false,
1660 }
1661 }
1662
1663 fn extension_policy(&self) -> crate::ExtensionPolicy {
1664 self.extensions
1665 }
1666 }
1667
1668 #[test]
1669 fn custom_profile_extension_policy_becomes_runtime_patch_flags() {
1670 let profile = PolicyProfile {
1671 primitives: alloc::vec![PrimitiveSpec {
1672 name: Cow::Borrowed("customxetex"),
1673 opcode: PrimitiveOpcode(800),
1674 kind: PrimitiveKind::Extension,
1675 }],
1676 extensions: ExtensionPolicy {
1677 etex: false,
1678 xetex: true,
1679 },
1680 };
1681 let format = FormatImage::empty("custom", profile.id());
1682 let engine = EngineBuilder::new(profile)
1683 .format(format)
1684 .resources(InMemoryResourceProvider::new())
1685 .platform(NoopPlatform::default())
1686 .build()
1687 .expect("engine should build");
1688
1689 assert!(engine.semantics().has_patch(EnginePatch::Etex));
1690 assert!(engine.semantics().has_patch(EnginePatch::Xetex));
1691 assert!(engine.primitives().get("customxetex").is_some());
1692 }
1693
1694 #[test]
1695 fn engine_rejects_duplicate_primitive_names() {
1696 let profile = PrimitiveProfile {
1697 primitives: alloc::vec![
1698 PrimitiveSpec {
1699 name: Cow::Borrowed("input"),
1700 opcode: PrimitiveOpcode(1),
1701 kind: PrimitiveKind::Resource,
1702 },
1703 PrimitiveSpec {
1704 name: Cow::Borrowed("input"),
1705 opcode: PrimitiveOpcode(2),
1706 kind: PrimitiveKind::Resource,
1707 },
1708 ],
1709 };
1710 let format = FormatImage::empty("plain", profile.id());
1711
1712 let error = EngineBuilder::new(profile)
1713 .format(format)
1714 .resources(InMemoryResourceProvider::new())
1715 .platform(NoopPlatform::default())
1716 .build()
1717 .expect_err("duplicate primitive names should fail");
1718
1719 assert_eq!(
1720 error,
1721 BuildError::InvalidPrimitives(PrimitiveRegistryError::DuplicateName {
1722 name: "input".to_string(),
1723 })
1724 );
1725 }
1726
1727 #[test]
1728 fn session_loads_packages_through_configured_provider() {
1729 let profile = TexProfile;
1730 let format = FormatImage::empty("latex", profile.id());
1731 let resources = InMemoryResourceProvider::new().with_resource(
1732 "amsmath.sty",
1733 ResourceKind::Package,
1734 br"\ProvidesPackage{amsmath}".to_vec(),
1735 );
1736 let engine = EngineBuilder::new(profile)
1737 .format(format)
1738 .resources(resources)
1739 .platform(NoopPlatform::default())
1740 .build()
1741 .expect("engine should build");
1742
1743 let session = engine.new_session();
1744 let package = session
1745 .load_package("amsmath.sty")
1746 .expect("package should be loaded through provider");
1747
1748 assert_eq!(package.kind, ResourceKind::Package);
1749 assert_eq!(package.bytes, br"\ProvidesPackage{amsmath}".to_vec());
1750 }
1751
1752 #[test]
1753 fn session_loads_typed_resources_through_provider() {
1754 let profile = TexProfile;
1755 let format = FormatImage::empty("latex", profile.id());
1756 let mut resources = InMemoryResourceProvider::new()
1757 .with_resource("article.cls", ResourceKind::Class, b"class")
1758 .with_resource("ot1cmr.fd", ResourceKind::FontDefinition, b"fd")
1759 .with_resource("size10.clo", ResourceKind::PackageSupport, b"support")
1760 .with_resource("latinmodern-math.otf", ResourceKind::Font, b"font")
1761 .with_resource("t1.enc", ResourceKind::Encoding, b"encoding")
1762 .with_resource("pdftex.map", ResourceKind::Map, b"map")
1763 .with_resource("texmf.cnf", ResourceKind::Config, b"config")
1764 .with_resource("latex.fmt", ResourceKind::FormatImage, b"format");
1765 resources.insert_request(
1766 crate::ProviderResourceRequest::asset("mhchem", "arrows.dat"),
1767 b"asset",
1768 );
1769 let engine = EngineBuilder::new(profile)
1770 .format(format)
1771 .resources(resources)
1772 .platform(NoopPlatform::default())
1773 .build()
1774 .expect("engine should build");
1775 let session = engine.new_session();
1776
1777 let class = session.load_class("article.cls").expect("class");
1778 let font_definition = session
1779 .load_font_definition("ot1cmr.fd")
1780 .expect("font definition");
1781 let package_support = session
1782 .load_package_support("size10.clo")
1783 .expect("package support");
1784 let font = session
1785 .load_font_resource("latinmodern-math.otf")
1786 .expect("font");
1787 let encoding = session.load_encoding("t1.enc").expect("encoding");
1788 let map = session.load_map("pdftex.map").expect("map");
1789 let config = session.load_config("texmf.cnf").expect("config");
1790 let asset = session.load_asset("mhchem", "arrows.dat").expect("asset");
1791 let format_image = session
1792 .load_format_image("latex.fmt")
1793 .expect("format image");
1794
1795 assert_eq!(class.kind, ResourceKind::Class);
1796 assert_eq!(class.bytes, b"class");
1797 assert_eq!(font_definition.kind, ResourceKind::FontDefinition);
1798 assert_eq!(font_definition.bytes, b"fd");
1799 assert_eq!(package_support.kind, ResourceKind::PackageSupport);
1800 assert_eq!(package_support.bytes, b"support");
1801 assert_eq!(font.kind, ResourceKind::Font);
1802 assert_eq!(font.bytes, b"font");
1803 assert_eq!(encoding.kind, ResourceKind::Encoding);
1804 assert_eq!(encoding.bytes, b"encoding");
1805 assert_eq!(map.kind, ResourceKind::Map);
1806 assert_eq!(map.bytes, b"map");
1807 assert_eq!(config.kind, ResourceKind::Config);
1808 assert_eq!(config.bytes, b"config");
1809 assert_eq!(asset.kind, ResourceKind::Asset);
1810 assert_eq!(asset.canonical_name, "mhchem/arrows.dat");
1811 assert_eq!(asset.bytes, b"asset");
1812 assert_eq!(format_image.kind, ResourceKind::FormatImage);
1813 assert_eq!(format_image.bytes, b"format");
1814 }
1815
1816 #[test]
1817 fn repeated_sessions_reuse_cached_format_metadata() {
1818 let profile = TexProfile;
1819 let format = FormatImage::empty("latex+amsmath", profile.id());
1820 let engine = EngineBuilder::new(profile)
1821 .format(format)
1822 .resources(InMemoryResourceProvider::new())
1823 .platform(NoopPlatform::default())
1824 .build()
1825 .expect("engine should build");
1826
1827 let first = engine.new_session();
1828 let second = engine.new_session();
1829
1830 let first_ir = first.ir_emitter().finish();
1831 let second_ir = second.ir_emitter().finish();
1832
1833 assert_eq!(first_ir.metadata.format_id, "latex+amsmath");
1834 assert_eq!(second_ir.metadata.format_id, "latex+amsmath");
1835 assert_eq!(first_ir.metadata.engine_profile, "tex");
1836 assert_eq!(second_ir.metadata.engine_profile, "tex");
1837 }
1838
1839 #[test]
1840 fn layout_fragment_input_uses_generated_engine_without_manual_core() {
1841 let profile = TexProfile;
1842 let (format, resources) = basic_test_format(profile.id());
1843 let resources = resources
1844 .with_resource(
1845 "cmr10",
1846 ResourceKind::Font,
1847 include_bytes!("../../../vendor/texlive-source/texk/web2c/tests/cmr10.tfm")
1848 .to_vec(),
1849 )
1850 .with_resource(
1851 "cmmi10",
1852 ResourceKind::Font,
1853 include_bytes!(
1854 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmmi10.tfm"
1855 )
1856 .to_vec(),
1857 )
1858 .with_resource(
1859 "cmmi10.tfm",
1860 ResourceKind::Font,
1861 include_bytes!(
1862 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmmi10.tfm"
1863 )
1864 .to_vec(),
1865 )
1866 .with_resource(
1867 "cmsy10",
1868 ResourceKind::Font,
1869 include_bytes!(
1870 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmsy10.tfm"
1871 )
1872 .to_vec(),
1873 )
1874 .with_resource(
1875 "cmsy10.tfm",
1876 ResourceKind::Font,
1877 include_bytes!(
1878 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmsy10.tfm"
1879 )
1880 .to_vec(),
1881 )
1882 .with_resource(
1883 "cmex10",
1884 ResourceKind::Font,
1885 include_bytes!(
1886 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmex10.tfm"
1887 )
1888 .to_vec(),
1889 )
1890 .with_resource(
1891 "cmex10.tfm",
1892 ResourceKind::Font,
1893 include_bytes!(
1894 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmex10.tfm"
1895 )
1896 .to_vec(),
1897 );
1898 let engine = EngineBuilder::new(profile)
1899 .format(format)
1900 .resources(resources)
1901 .platform(NoopPlatform::default())
1902 .build()
1903 .expect("engine should build");
1904 let mut session = engine.new_session();
1905
1906 let body = r"\font\tenrm=cmr10 \tenrm A";
1907 let fragment = session
1908 .layout_fragment_input(FragmentInput::new("equation.tex", body, FragmentKind::Text))
1909 .expect("generated engine should layout a boxed text fragment");
1910 let _glyph_node = fragment
1911 .nodes
1912 .iter()
1913 .find(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_)))
1914 .expect("glyph run should be emitted");
1915
1916 assert_eq!(fragment.metadata.fragment_kind, FragmentKind::Text);
1917 }
1918
1919 #[test]
1920 fn layout_rejects_input_that_exceeds_platform_limit() {
1921 let profile = TexProfile;
1922 let format = FormatImage::empty("latex", profile.id());
1923 let platform = NoopPlatform::with_limits(HostLimits {
1924 max_input_bytes: 4,
1925 ..HostLimits::default()
1926 });
1927 let engine = EngineBuilder::new(profile)
1928 .format(format)
1929 .resources(InMemoryResourceProvider::new())
1930 .platform(platform)
1931 .build()
1932 .expect("engine should build");
1933 let mut session = engine.new_session();
1934
1935 let error = session
1936 .layout_fragment("12345")
1937 .expect_err("input should exceed limit");
1938
1939 assert_eq!(
1940 error,
1941 EngineError::Limit(LimitError::InputTooLarge {
1942 actual: 5,
1943 limit: 4,
1944 })
1945 );
1946 }
1947
1948 #[test]
1949 fn layout_fragment_loads_tex_inputs_inside_generated_engine() {
1950 let profile = TexProfile;
1951 let (format, resources) = basic_test_format(profile.id());
1952 let resources = resources.with_resource(
1953 "snippet.tex",
1954 ResourceKind::TexInput,
1955 br"\vrule width 1pt height 2pt depth 0pt".to_vec(),
1956 );
1957 let engine = EngineBuilder::new(profile)
1958 .format(format)
1959 .resources(resources)
1960 .platform(NoopPlatform::default())
1961 .build()
1962 .expect("engine should build");
1963 let mut session = engine.new_session();
1964
1965 let body = r"\input snippet.tex";
1966 let fragment = session
1967 .layout_fragment_input(FragmentInput::text(body))
1968 .expect("generated engine should own input loading");
1969 assert!(fragment
1970 .nodes
1971 .iter()
1972 .any(|node| matches!(node.kind, LayoutNodeKind::Rule(_))));
1973 }
1974
1975 #[test]
1976 fn layout_fragment_loads_packages_inside_generated_engine() {
1977 let profile = TexProfile;
1978 let (format, resources) = basic_test_format(profile.id());
1979 let resources = resources
1980 .with_resource(
1981 "cmr10",
1982 ResourceKind::Font,
1983 include_bytes!("../../../vendor/texlive-source/texk/web2c/tests/cmr10.tfm")
1984 .to_vec(),
1985 )
1986 .with_resource(
1987 "testpkg.sty",
1988 ResourceKind::Package,
1989 br"\def\pkgchar{A}".to_vec(),
1990 );
1991 let engine = EngineBuilder::new(profile)
1992 .format(format)
1993 .resources(resources)
1994 .platform(NoopPlatform::default())
1995 .build()
1996 .expect("engine should build");
1997 let mut session = engine.new_session();
1998
1999 let body = r"\font\tenrm=cmr10 \tenrm A \usepackage{testpkg}\pkgchar";
2000 let fragment = session
2001 .layout_fragment_input(FragmentInput::text(body))
2002 .expect("generated engine should expand package loading through TeX");
2003 assert!(fragment
2004 .nodes
2005 .iter()
2006 .any(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_))));
2007 }
2008
2009 #[test]
2010 fn generated_format_cache_reuses_preloaded_package_state() {
2011 let profile = TexProfile;
2012 let format_resources = InMemoryResourceProvider::new()
2013 .with_resource(TEST_FORMAT_INPUT, ResourceKind::TexInput, TEST_FORMAT_SETUP)
2014 .with_resource(
2015 "cachedpkg.sty",
2016 ResourceKind::Package,
2017 br"\def\cachedpkgchar{A}".to_vec(),
2018 );
2019 let format = FormatImage::initializer("latex+cachedpkg", profile.id())
2020 .preload_tex_input(TEST_FORMAT_INPUT)
2021 .preload_package("cachedpkg")
2022 .build(&format_resources)
2023 .expect("format should preload package bytes");
2024 let resources = InMemoryResourceProvider::new().with_resource(
2025 "cmr10",
2026 ResourceKind::Font,
2027 include_bytes!("../../../vendor/texlive-source/texk/web2c/tests/cmr10.tfm").to_vec(),
2028 );
2029 let engine = EngineBuilder::new(profile)
2030 .format(format)
2031 .resources(resources)
2032 .platform(NoopPlatform::default())
2033 .build()
2034 .expect("engine should build");
2035 let mut session = engine.new_session();
2036
2037 let body = r"\font\tenrm=cmr10 \tenrm \cachedpkgchar";
2038 let fragment = session
2039 .layout_fragment_input(FragmentInput::text(body))
2040 .expect("preloaded generated package macro should be cached");
2041 let has_glyph_node = fragment
2042 .nodes
2043 .iter()
2044 .any(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_)));
2045
2046 assert_eq!(fragment.metadata.format_id, "latex+cachedpkg");
2047 assert!(has_glyph_node);
2048 assert!(!fragment
2049 .source_map
2050 .sources
2051 .iter()
2052 .any(|source| source.name == "cachedpkg.sty"));
2053 }
2054
2055 #[test]
2056 fn generated_format_cache_reuses_preloaded_tex_input_state() {
2057 let profile = TexProfile;
2058 let format_resources = InMemoryResourceProvider::new()
2059 .with_resource(TEST_FORMAT_INPUT, ResourceKind::TexInput, TEST_FORMAT_SETUP)
2060 .with_resource(
2061 "plain.tex",
2062 ResourceKind::TexInput,
2063 br"\def\plainchar{A}".to_vec(),
2064 );
2065 let format = FormatImage::initializer("plain", profile.id())
2066 .preload_tex_input(TEST_FORMAT_INPUT)
2067 .preload_tex_input("plain.tex")
2068 .build(&format_resources)
2069 .expect("format should preload TeX input bytes");
2070 let resources = InMemoryResourceProvider::new().with_resource(
2071 "cmr10",
2072 ResourceKind::Font,
2073 include_bytes!("../../../vendor/texlive-source/texk/web2c/tests/cmr10.tfm").to_vec(),
2074 );
2075 let engine = EngineBuilder::new(profile)
2076 .format(format)
2077 .resources(resources)
2078 .platform(NoopPlatform::default())
2079 .build()
2080 .expect("engine should build");
2081 let mut session = engine.new_session();
2082
2083 let body = r"\font\tenrm=cmr10 \tenrm \plainchar";
2084 let fragment = session
2085 .layout_fragment_input(FragmentInput::text(body))
2086 .expect("preloaded generated TeX input macro should be cached");
2087 let has_glyph_node = fragment
2088 .nodes
2089 .iter()
2090 .any(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_)));
2091
2092 assert_eq!(fragment.metadata.format_id, "plain");
2093 assert!(has_glyph_node);
2094 assert!(!fragment
2095 .source_map
2096 .sources
2097 .iter()
2098 .any(|source| source.name == "plain.tex"));
2099 }
2100
2101 #[test]
2102 fn math_fragment_uses_format_owned_math_catcodes_and_fonts() {
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 fragment = session
2145 .layout_fragment_input(FragmentInput::math_inline("x^2"))
2146 .expect("generated engine should layout superscript math from format catcodes");
2147 let glyph_count = fragment
2148 .nodes
2149 .iter()
2150 .filter(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_)))
2151 .count();
2152
2153 assert_eq!(fragment.metadata.format_id, "plain-math");
2154 assert!(glyph_count >= 2);
2155 }
2156
2157 fn math_fixture() -> (FormatImage, InMemoryResourceProvider) {
2158 let (format, resources) = math_test_format(TexProfile.id());
2159 let resources = resources
2160 .with_resource(
2161 "cmr10",
2162 ResourceKind::Font,
2163 include_bytes!("../../../vendor/texlive-source/texk/web2c/tests/cmr10.tfm")
2164 .to_vec(),
2165 )
2166 .with_resource(
2167 "cmmi10",
2168 ResourceKind::Font,
2169 include_bytes!(
2170 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmmi10.tfm"
2171 )
2172 .to_vec(),
2173 )
2174 .with_resource(
2175 "cmsy10",
2176 ResourceKind::Font,
2177 include_bytes!(
2178 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmsy10.tfm"
2179 )
2180 .to_vec(),
2181 )
2182 .with_resource(
2183 "cmex10",
2184 ResourceKind::Font,
2185 include_bytes!(
2186 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmex10.tfm"
2187 )
2188 .to_vec(),
2189 );
2190 (format, resources)
2191 }
2192
2193 #[derive(Debug, Default)]
2194 struct HostBoxTestPlatform {
2195 diagnostics: crate::platform::NoopDiagnosticSink,
2196 requests: alloc::rc::Rc<core::cell::RefCell<Vec<crate::HostBoxRequest>>>,
2197 response: Option<crate::HostBox>,
2198 }
2199
2200 impl Platform for HostBoxTestPlatform {
2201 fn diagnostics(&self) -> &dyn crate::DiagnosticSink {
2202 &self.diagnostics
2203 }
2204
2205 fn host_box(&self, request: crate::HostBoxRequest) -> Option<crate::HostBox> {
2206 self.requests.borrow_mut().push(request);
2207 self.response.clone()
2208 }
2209 }
2210
2211 fn host_box_session(
2212 response: Option<crate::HostBox>,
2213 ) -> (
2214 alloc::rc::Rc<core::cell::RefCell<Vec<crate::HostBoxRequest>>>,
2215 Engine<TexProfile, InMemoryResourceProvider, HostBoxTestPlatform>,
2216 ) {
2217 let (format, resources) = math_fixture();
2218 let requests = alloc::rc::Rc::new(core::cell::RefCell::new(Vec::new()));
2219 let platform = HostBoxTestPlatform {
2220 diagnostics: crate::platform::NoopDiagnosticSink,
2221 requests: requests.clone(),
2222 response,
2223 };
2224 let engine = EngineBuilder::new(TexProfile)
2225 .format(format)
2226 .resources(resources)
2227 .platform(platform)
2228 .build()
2229 .expect("engine should build");
2230 (requests, engine)
2231 }
2232
2233 #[test]
2234 fn host_box_without_host_typesets_a_deterministic_zero_size_box() {
2235 let (_, engine) = host_box_session(None);
2236 let mut session = engine.new_session();
2237 let with_box = session
2238 .layout_fragment_input(FragmentInput::math_inline(r"a\hostbox{5}b"))
2239 .expect("\\hostbox should fall back to an empty box without a host");
2240 let with_box_again = session
2241 .layout_fragment_input(FragmentInput::math_inline(r"a\hostbox{5}b"))
2242 .expect("fallback rendering should be repeatable");
2243 let plain = session
2244 .layout_fragment_input(FragmentInput::math_inline("ab"))
2245 .expect("plain math should render");
2246
2247 assert_eq!(with_box.surface.width, plain.surface.width);
2249 assert_eq!(with_box.surface, with_box_again.surface);
2250 assert_eq!(with_box.nodes.len(), with_box_again.nodes.len());
2251 }
2252
2253 #[test]
2254 fn host_box_places_host_metrics_and_render_data_as_one_ord_atom() {
2255 let response = crate::HostBox {
2256 width: 5 * 65536,
2257 height: 12 * 65536,
2258 depth: 3 * 65536,
2259 runs: Vec::from([crate::HostBoxRun {
2260 font_name: "HostFont".into(),
2261 font_size: 10 * 65536,
2262 glyphs: Vec::from([crate::HostBoxGlyph {
2263 glyph: 42,
2264 x: 0,
2265 y: 0,
2266 advance: 5 * 65536,
2267 }]),
2268 }]),
2269 rules: Vec::from([crate::HostBoxRule {
2270 x: 0,
2271 y: -2 * 65536,
2272 width: 5 * 65536,
2273 height: 26214,
2274 }]),
2275 };
2276 let (requests, engine) = host_box_session(Some(response));
2277 let mut session = engine.new_session();
2278 let fragment = session
2279 .layout_fragment_input(FragmentInput::math_inline(r"a\hostbox{7}b"))
2280 .expect("host box math should render");
2281 let plain = session
2282 .layout_fragment_input(FragmentInput::math_inline("ab"))
2283 .expect("plain math should render");
2284
2285 let host_runs: Vec<_> = fragment
2286 .nodes
2287 .iter()
2288 .filter_map(|node| match &node.kind {
2289 LayoutNodeKind::GlyphRun(run) if run.font.name == "HostFont" => Some(run),
2290 _ => None,
2291 })
2292 .collect();
2293 assert_eq!(host_runs.len(), 1, "exactly one host glyph run expected");
2294 assert_eq!(host_runs[0].glyphs.len(), 1);
2295 assert_eq!(host_runs[0].glyphs[0].glyph_id.0, 42);
2296 assert!(host_runs[0].font.id.0 >= 0x8000_0000);
2298 assert_eq!(
2299 host_runs[0].font.size,
2300 Length::from_scaled_points(10 * 65536)
2301 );
2302 let host_rules = fragment
2303 .nodes
2304 .iter()
2305 .filter(|node| {
2306 matches!(
2307 &node.kind,
2308 LayoutNodeKind::Rule(rule) if rule.size.width == Length::from_scaled_points(5 * 65536)
2309 && rule.size.height == Length::from_scaled_points(26214)
2310 )
2311 })
2312 .count();
2313 assert_eq!(host_rules, 1, "exactly one host rule expected");
2314
2315 assert_eq!(
2317 fragment.surface.width.0,
2318 plain.surface.width.0 + 5 * 65536,
2319 "host box should add its width with Ord spacing"
2320 );
2321 assert_eq!(fragment.surface.baseline.0, 12 * 65536);
2323 assert_eq!(fragment.surface.height.0, (12 + 3) * 65536);
2324
2325 let requests = requests.borrow();
2326 assert_eq!(requests.len(), 1);
2327 assert_eq!(requests[0].token, 7);
2328 assert_eq!(requests[0].style, crate::HostBoxStyle::Text);
2329 assert_eq!(requests[0].font_size, 10 * 65536);
2330 }
2331
2332 #[test]
2333 fn host_box_keeps_width_through_fraction_rebox() {
2334 let response = crate::HostBox {
2335 width: 5 * 65536,
2336 height: 8 * 65536,
2337 depth: 0,
2338 runs: Vec::from([crate::HostBoxRun {
2339 font_name: "HostFont".into(),
2340 font_size: 10 * 65536,
2341 glyphs: Vec::from([crate::HostBoxGlyph {
2342 glyph: 42,
2343 x: 0,
2344 y: 0,
2345 advance: 5 * 65536,
2346 }]),
2347 }]),
2348 rules: Vec::new(),
2349 };
2350 let (_, engine) = host_box_session(Some(response));
2351 let mut session = engine.new_session();
2352 let fragment = session
2354 .layout_fragment_input(FragmentInput::math_inline(r"{ab\over\hostbox{7}}"))
2355 .expect("fraction with host box denominator should render");
2356
2357 let host_width = Length::from_scaled_points(5 * 65536);
2358 let host_boxes: Vec<_> = fragment
2359 .nodes
2360 .iter()
2361 .filter(|node| {
2362 matches!(&node.kind, LayoutNodeKind::Box(b) if b.metrics.width == host_width)
2363 })
2364 .collect();
2365 assert_eq!(
2366 host_boxes.len(),
2367 1,
2368 "rebox dissolves the shell, one record box remains"
2369 );
2370 let host_box = host_boxes[0];
2371 let parent = fragment
2372 .nodes
2373 .iter()
2374 .find_map(|node| match &node.kind {
2375 LayoutNodeKind::Box(b) if b.children.contains(&host_box.id) => Some(b),
2376 _ => None,
2377 })
2378 .expect("record box should sit inside the reboxed line");
2379 let expected = (parent.metrics.width.0 - 5 * 65536) / 2;
2381 let offset = host_box.origin.x.0;
2382 assert!(
2383 (offset - expected).abs() <= 2,
2384 "record box must start at (line - box)/2 = {expected}, got {offset}"
2385 );
2386 assert!(offset > 0, "reboxed line must indent the record box");
2387 assert!(fragment.nodes.iter().any(|node| {
2389 matches!(&node.kind, LayoutNodeKind::GlyphRun(run) if run.font.name == "HostFont")
2390 }));
2391 }
2392
2393 #[test]
2394 fn host_box_reports_style_and_size_context_per_site() {
2395 let (requests, engine) = host_box_session(None);
2396 let mut session = engine.new_session();
2397 session
2398 .layout_fragment_input(FragmentInput::math_inline(r"\hostbox{1}"))
2399 .expect("inline math host box should render");
2400 session
2401 .layout_fragment_input(FragmentInput::math_display(r"\hostbox{2}"))
2402 .expect("display math host box should render");
2403 session
2404 .layout_fragment_input(FragmentInput::math_inline(r"x^{\hostbox{3}}"))
2405 .expect("script host box should render");
2406 session
2407 .layout_fragment_input(FragmentInput::math_inline(r"x^{y^{\hostbox{4}}}"))
2408 .expect("scriptscript host box should render");
2409 session
2410 .layout_fragment_input(FragmentInput::math_inline(r"\text{\tenrm\hostbox{5}}"))
2411 .expect("hbox content host box should render");
2412
2413 let requests = requests.borrow();
2414 let by_token = |token: i32| {
2415 requests
2416 .iter()
2417 .find(|request| request.token == token)
2418 .copied()
2419 .unwrap_or_else(|| panic!("token {token} should reach the host"))
2420 };
2421 assert_eq!(by_token(1).style, crate::HostBoxStyle::Text);
2422 assert_eq!(by_token(2).style, crate::HostBoxStyle::Text);
2423 assert_eq!(by_token(3).style, crate::HostBoxStyle::Script);
2424 assert_eq!(by_token(4).style, crate::HostBoxStyle::ScriptScript);
2425 assert_eq!(by_token(5).style, crate::HostBoxStyle::Text);
2426 assert_eq!(by_token(5).font_size, 10 * 65536);
2428 }
2429
2430 #[cfg(feature = "std")]
2431 #[test]
2432 fn real_plain_format_loads_through_generated_engine_when_texlive_is_available() {
2433 if !HostResolverTestResources::is_available() {
2434 return;
2435 }
2436
2437 assert_real_plain_format_loads(HostResolverTestResources);
2438 }
2439
2440 #[test]
2441 fn real_plain_format_loads_from_vendored_fixture() {
2442 assert_real_plain_format_loads(PlainFixtureResources);
2443 }
2444
2445 #[test]
2446 fn packaged_format_roundtrip_lays_out_fragments() {
2447 let format = FormatImage::plain(TexProfile.id())
2448 .build(&PlainFixtureResources)
2449 .expect("real plain.tex should resolve");
2450 let booted = EngineBuilder::new(TexProfile)
2451 .format(format)
2452 .resources(PlainFixtureResources)
2453 .platform(NoopPlatform::default())
2454 .build()
2455 .expect("engine should build with real plain format");
2456 let mut baseline_session = booted.new_session();
2457 let baseline = baseline_session
2458 .layout_fragment_input(FragmentInput::text(r"\tenrm A"))
2459 .expect("booted engine should lay out the fragment");
2460
2461 let packaged =
2462 crate::generated::pack_packaged_format(&booted.generated_format().to_bytes(), &[]);
2463 let engine = EngineBuilder::new(TexProfile)
2464 .packaged_format(packaged)
2465 .resources(PlainFixtureResources)
2466 .platform(NoopPlatform::default())
2467 .build()
2468 .expect("packaged engine should build without format initialization");
2469 let mut session = engine.new_session();
2470 let fragment = session
2471 .layout_fragment_input(FragmentInput::text(r"\tenrm A"))
2472 .expect("packaged format should lay out fragments");
2473 assert_eq!(fragment.nodes.len(), baseline.nodes.len());
2474 }
2475
2476 #[cfg(feature = "std")]
2477 #[test]
2478 fn real_latex_format_loads_through_generated_engine_when_texlive_is_available() {
2479 if !HostResolverTestResources::is_latex_available() {
2480 return;
2481 }
2482
2483 let profile = XetexProfile;
2484 let format = FormatImage::latex(profile.id())
2485 .build(&HostResolverTestResources)
2486 .expect("real latex.ltx should resolve");
2487 let engine = EngineBuilder::new(profile)
2488 .format(format)
2489 .resources(HostResolverTestResources)
2490 .platform(NoopPlatform::default())
2491 .build()
2492 .expect("engine should build with real latex format");
2493 let mut session = engine.new_session();
2494
2495 let fragment = session
2496 .layout_fragment_input(FragmentInput::text(r"\font\tenrm=cmr10 \tenrm A"))
2497 .expect("real latex format should leave the engine usable for fragments");
2498 let has_glyph_node = fragment
2499 .nodes
2500 .iter()
2501 .any(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_)));
2502
2503 assert_eq!(fragment.metadata.format_id, "latex");
2504 assert!(has_glyph_node);
2505 }
2506
2507 #[test]
2508 fn math_fragment_supports_text_macro_inside_generated_engine() {
2509 let profile = TexProfile;
2510 let (format, resources) = math_test_format(profile.id());
2511 let resources = resources
2512 .with_resource(
2513 "cmr10",
2514 ResourceKind::Font,
2515 include_bytes!("../../../vendor/texlive-source/texk/web2c/tests/cmr10.tfm")
2516 .to_vec(),
2517 )
2518 .with_resource(
2519 "cmmi10",
2520 ResourceKind::Font,
2521 include_bytes!(
2522 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmmi10.tfm"
2523 )
2524 .to_vec(),
2525 )
2526 .with_resource(
2527 "cmsy10",
2528 ResourceKind::Font,
2529 include_bytes!(
2530 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmsy10.tfm"
2531 )
2532 .to_vec(),
2533 )
2534 .with_resource(
2535 "cmex10",
2536 ResourceKind::Font,
2537 include_bytes!(
2538 "../../../vendor/texlive-source/texk/web2c/tests/generated-tfm/cmex10.tfm"
2539 )
2540 .to_vec(),
2541 );
2542 let engine = EngineBuilder::new(profile)
2543 .format(format)
2544 .resources(resources)
2545 .platform(NoopPlatform::default())
2546 .build()
2547 .expect("engine should build");
2548 let mut session = engine.new_session();
2549
2550 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}";
2551 let fragment = session
2552 .layout_fragment_input(FragmentInput::math_inline(body))
2553 .expect("generated engine should allow text boxes inside math");
2554 let _glyph_node = fragment
2555 .nodes
2556 .iter()
2557 .find(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_)))
2558 .expect("text macro should emit glyph IR");
2559
2560 assert_eq!(fragment.metadata.fragment_kind, FragmentKind::MathInline);
2561 }
2562
2563 #[test]
2564 fn generated_package_loading_resolves_package_assets() {
2565 let profile = TexProfile;
2566 let (format, resources) = basic_test_format(profile.id());
2567 let mut resources = resources
2568 .with_resource(
2569 "cmr10",
2570 ResourceKind::Font,
2571 include_bytes!("../../../vendor/texlive-source/texk/web2c/tests/cmr10.tfm")
2572 .to_vec(),
2573 )
2574 .with_resource(
2575 "assetpkg.sty",
2576 ResourceKind::Package,
2577 br"\input data.dat\relax".to_vec(),
2578 );
2579 resources.insert_request(
2580 crate::ProviderResourceRequest::asset("assetpkg", "data.dat"),
2581 br"\def\assetpkgchar{A}".to_vec(),
2582 );
2583 let engine = EngineBuilder::new(profile)
2584 .format(format)
2585 .resources(resources)
2586 .platform(NoopPlatform::default())
2587 .build()
2588 .expect("engine should build");
2589 let mut session = engine.new_session();
2590
2591 let body = r"\font\tenrm=cmr10 \usepackage{assetpkg}\tenrm \assetpkgchar";
2592 let fragment = session
2593 .layout_fragment_input(FragmentInput::text(body))
2594 .expect("generated engine should load package-owned TeX assets");
2595
2596 assert!(fragment
2597 .nodes
2598 .iter()
2599 .any(|node| matches!(node.kind, LayoutNodeKind::GlyphRun(_))));
2600 }
2601
2602 #[test]
2603 fn generated_layout_does_not_expand_legacy_host_macros() {
2604 let profile = TexProfile;
2605 let format = FormatImage::with_snapshot(
2606 "latex+macros",
2607 profile.id(),
2608 FormatSnapshot {
2609 macros: alloc::vec![MacroDefinition {
2610 name: "speedtext".into(),
2611 replacement: br"\text{speed}".to_vec(),
2612 }],
2613 ..FormatSnapshot::default()
2614 },
2615 );
2616 let engine = EngineBuilder::new(profile)
2617 .format(format)
2618 .resources(InMemoryResourceProvider::new())
2619 .platform(NoopPlatform::default())
2620 .build()
2621 .expect("engine should build");
2622 let mut session = engine.new_session();
2623
2624 let error = session
2625 .layout_fragment(r"a+\speedtext")
2626 .expect_err("generated engine should not use legacy host macro tables");
2627
2628 match error {
2629 EngineError::Layout { message } => {
2630 assert!(message.contains("generated TeX engine reported an error"));
2631 }
2632 other => panic!("unexpected error: {other:?}"),
2633 }
2634 }
2635
2636 #[test]
2637 fn sessions_load_and_shape_text_through_font_system() {
2638 let profile = XetexProfile;
2639 let format = FormatImage::empty("latex", profile.id());
2640 let fonts = InMemoryFontSystem::new()
2641 .with_font("Latin Modern Math", b"font")
2642 .with_fallback_shaping();
2643 let engine = EngineBuilder::new(profile)
2644 .format(format)
2645 .resources(InMemoryResourceProvider::new())
2646 .platform(NoopPlatform::default())
2647 .fonts(fonts)
2648 .build()
2649 .expect("engine should build");
2650 let session = engine.new_session();
2651
2652 let font = session
2653 .load_font(&FontQuery {
2654 family: "Latin Modern Math".to_string(),
2655 size: Length::from_scaled_points(655_360),
2656 math: true,
2657 })
2658 .expect("font should load");
2659 let shaped = session
2660 .shape_text(&ShapeRequest {
2661 font: font.id,
2662 text: "xy",
2663 direction: Direction::LeftToRight,
2664 source: Some(ByteSpan { start: 10, end: 12 }),
2665 script: None,
2666 features: Vec::new(),
2667 })
2668 .expect("text should shape through font system");
2669
2670 assert_eq!(font.canonical_name, "Latin Modern Math");
2671 assert_eq!(shaped.glyphs.len(), 2);
2672 assert_eq!(
2673 shaped.glyphs[0].cluster,
2674 Some(ByteSpan { start: 10, end: 11 })
2675 );
2676 assert_eq!(
2677 shaped.glyphs[1].cluster,
2678 Some(ByteSpan { start: 11, end: 12 })
2679 );
2680 }
2681
2682 #[test]
2683 fn sessions_can_load_fonts_from_resource_provider_adapter() {
2684 let profile = XetexProfile;
2685 let format = FormatImage::empty("latex", profile.id());
2686 let font_resources = InMemoryResourceProvider::new().with_resource(
2687 "Latin Modern Math.otf",
2688 ResourceKind::Font,
2689 b"font-bytes",
2690 );
2691 let engine = EngineBuilder::new(profile)
2692 .format(format)
2693 .resources(InMemoryResourceProvider::new())
2694 .platform(NoopPlatform::default())
2695 .fonts(ResourceFontSystem::new(font_resources))
2696 .build()
2697 .expect("engine should build");
2698 let session = engine.new_session();
2699
2700 let font = session
2701 .load_font(&FontQuery {
2702 family: "Latin Modern Math.otf".to_string(),
2703 size: Length::from_scaled_points(655_360),
2704 math: true,
2705 })
2706 .expect("font should load from ResourceProvider");
2707
2708 assert_eq!(font.canonical_name, "Latin Modern Math.otf");
2709 assert_eq!(&**font.bytes().expect("library owned bytes"), b"font-bytes");
2710 assert!(session
2711 .shape_text(&ShapeRequest {
2712 font: font.id,
2713 text: "x",
2714 direction: Direction::LeftToRight,
2715 source: None,
2716 script: None,
2717 features: Vec::new(),
2718 })
2719 .is_err());
2720 }
2721
2722 #[test]
2723 fn sessions_clone_cached_format_state_without_sharing_mutations() {
2724 let profile = TexProfile;
2725 let format = FormatImage::with_snapshot(
2726 "latex+text",
2727 profile.id(),
2728 FormatSnapshot {
2729 macros: alloc::vec![MacroDefinition {
2730 name: "text".into(),
2731 replacement: b"initial".to_vec(),
2732 }],
2733 ..FormatSnapshot::default()
2734 },
2735 );
2736 let engine = EngineBuilder::new(profile)
2737 .format(format)
2738 .resources(InMemoryResourceProvider::new())
2739 .platform(NoopPlatform::default())
2740 .build()
2741 .expect("engine should build");
2742
2743 let mut first = engine.new_session();
2744 let second = engine.new_session();
2745
2746 first
2747 .state_mut()
2748 .define_macro("text", b"session-local".to_vec());
2749
2750 assert_eq!(
2751 first
2752 .state()
2753 .macro_definition("text")
2754 .expect("first session macro")
2755 .replacement,
2756 b"session-local".to_vec()
2757 );
2758 assert_eq!(
2759 second
2760 .state()
2761 .macro_definition("text")
2762 .expect("second session macro")
2763 .replacement,
2764 b"initial".to_vec()
2765 );
2766
2767 match &engine.format().state {
2768 crate::FormatState::Structured(snapshot) => {
2769 assert_eq!(snapshot.macros[0].replacement, b"initial".to_vec());
2770 }
2771 other => panic!("unexpected format state: {other:?}"),
2772 }
2773 }
2774
2775 #[test]
2776 fn session_creates_metadata_seeded_ir_emitter() {
2777 let profile = TexProfile;
2778 let format = FormatImage::empty("latex", profile.id());
2779 let engine = EngineBuilder::new(profile)
2780 .format(format)
2781 .resources(InMemoryResourceProvider::new())
2782 .platform(NoopPlatform::default())
2783 .build()
2784 .expect("engine should build");
2785 let session = engine.new_session();
2786
2787 let fragment = session.ir_emitter().finish();
2788
2789 assert_eq!(fragment.metadata.engine_profile, "tex");
2790 assert_eq!(fragment.metadata.format_id, "latex");
2791 assert_eq!(fragment.metadata.fragment_kind, FragmentKind::MathInline);
2792 }
2793
2794 #[test]
2795 fn session_creates_mode_specific_ir_emitter() {
2796 let profile = TexProfile;
2797 let format = FormatImage::empty("latex", profile.id());
2798 let engine = EngineBuilder::new(profile)
2799 .format(format)
2800 .resources(InMemoryResourceProvider::new())
2801 .platform(NoopPlatform::default())
2802 .build()
2803 .expect("engine should build");
2804 let session = engine.new_session();
2805
2806 let fragment = session.ir_emitter_for(FragmentKind::Text).finish();
2807
2808 assert_eq!(fragment.metadata.fragment_kind, FragmentKind::Text);
2809 }
2810}