1mod global;
2mod sandbox;
3mod temp;
4
5use crate::{
6 Archive, BackendError, BuildableArchive, LocalArchive, MathArchive,
7 artifacts::{Artifact, FileOrString},
8 formats::{BuildTarget, BuildTargetId},
9 manager::ArchiveOrGroup,
10 utils::{
11 AsyncEngine,
12 errors::ArtifactSaveError,
13 path_ext::{PathExt, RelPath},
14 },
15};
16use ftml_ontology::{
17 domain::{SharedDeclaration, declarations::IsDeclaration, modules::ModuleLike},
18 narrative::{
19 DocDataRef, DocumentRange, SharedDocumentElement,
20 documents::Document,
21 elements::{DocumentElement, IsDocumentElement, Notation},
22 },
23 utils::Css,
24};
25use ftml_uris::{
26 ArchiveId, DocumentElementUri, DocumentUri, IsDomainUri, IsNarrativeUri, ModuleUri, NamedUri,
27 SymbolUri, UriPath,
28};
29pub use global::*;
30pub use sandbox::*;
31use std::path::Path;
32pub use temp::*;
33
34static SETTINGS: std::sync::LazyLock<
35 std::sync::Arc<
36 parking_lot::RwLock<Option<fn() -> &'static rustc_hash::FxHashMap<Box<str>, Box<str>>>>,
37 >,
38> = std::sync::LazyLock::new(|| std::sync::Arc::new(parking_lot::RwLock::new(None)));
39pub fn set_fonts(get: fn() -> &'static rustc_hash::FxHashMap<Box<str>, Box<str>>) {
40 *SETTINGS.write() = Some(get);
41}
42
43pub(crate) fn replace_css(mut css: Box<[Css]>) -> Box<[Css]> {
44 let fonts = *SETTINGS.read();
45 if let Some(fonts) = fonts {
46 let fonts = fonts();
47 for c in &mut css {
48 if let Css::Link(url) = c
49 && let Some(new) = fonts.get(url)
50 {
51 *url = format!("srv:/fonts/{new}").into_boxed_str();
52 }
53 }
54 }
55 css
56}
57
58pub trait LocalBackend: Send + Sync {
59 type ArchiveIter<'a>: IntoIterator<Item = &'a Archive>
60 where
61 Self: Sized;
62
63 fn get_document(&self, uri: &DocumentUri) -> Result<Document, BackendError>;
65
66 fn get_document_async<A: AsyncEngine>(
68 &self,
69 uri: &DocumentUri,
70 ) -> impl Future<Output = Result<Document, BackendError>> + Send + use<Self, A>
71 where
72 Self: Sized;
73
74 fn get_module(&self, uri: &ModuleUri) -> Result<ModuleLike, BackendError>;
76
77 fn get_module_async<A: AsyncEngine>(
79 &self,
80 uri: &ModuleUri,
81 ) -> impl Future<Output = Result<ModuleLike, BackendError>> + Send + use<Self, A>
82 where
83 Self: Sized;
84
85 fn with_archive_or_group<R>(
86 &self,
87 id: &ArchiveId,
88 f: impl FnOnce(Option<&ArchiveOrGroup>) -> R,
89 ) -> R
90 where
91 Self: Sized;
92 fn with_archives<R>(&self, f: impl FnOnce(Self::ArchiveIter<'_>) -> R) -> R
93 where
94 Self: Sized;
95 fn with_archive<R>(&self, id: &ArchiveId, f: impl FnOnce(Option<&Archive>) -> R) -> R
96 where
97 Self: Sized;
98 fn get_html_body(&self, d: &DocumentUri) -> Result<(Box<[Css]>, Box<str>), BackendError>;
100
101 fn get_html_body_async<A: AsyncEngine>(
103 &self,
104 d: &ftml_uris::DocumentUri,
105 ) -> impl Future<Output = Result<(Box<[ftml_ontology::utils::Css]>, Box<str>), BackendError>>
106 + Send
107 + use<Self, A>
108 where
109 Self: Sized;
110
111 fn get_html_body_inner(&self, d: &DocumentUri) -> Result<(Box<[Css]>, Box<str>), BackendError>;
113
114 fn get_html_body_inner_async<A: AsyncEngine>(
116 &self,
117 d: &ftml_uris::DocumentUri,
118 ) -> impl Future<Output = Result<(Box<[ftml_ontology::utils::Css]>, Box<str>), BackendError>>
119 + Send
120 + use<Self, A>
121 where
122 Self: Sized;
123
124 fn get_html_full(&self, d: &DocumentUri) -> Result<Box<str>, BackendError>;
126
127 fn get_html_fragment(
129 &self,
130 d: &DocumentUri,
131 range: DocumentRange,
132 ) -> Result<(Box<[Css]>, Box<str>), BackendError>;
133
134 fn get_html_fragment_async<A: AsyncEngine>(
136 &self,
137 d: &ftml_uris::DocumentUri,
138 range: ftml_ontology::narrative::DocumentRange,
139 ) -> impl Future<Output = Result<(Box<[ftml_ontology::utils::Css]>, Box<str>), BackendError>>
140 + Send
141 + use<Self, A>
142 where
143 Self: Sized;
144
145 fn get_reference<T: bincode::Decode<()>>(&self, rf: &DocDataRef<T>) -> Result<T, BackendError>
147 where
148 Self: Sized;
149
150 fn get_declaration<T: IsDeclaration>(
152 &self,
153 uri: &SymbolUri,
154 ) -> Result<SharedDeclaration<T>, BackendError>
155 where
156 Self: Sized,
157 {
158 if uri.module.name().is_simple() {
159 let m = self.get_module(uri.module_uri())?;
160 return m
161 .get_as(uri.name())
162 .ok_or_else(|| BackendError::NotFound(uri.clone().into()));
163 }
164 let uri = uri.clone().simple_module();
165 let m = self.get_module(uri.module_uri())?;
166 m.get_as(uri.name())
167 .ok_or(BackendError::NotFound(uri.into()))
168 }
169
170 fn save(
172 &self,
173 in_doc: &ftml_uris::DocumentUri,
174 rel_path: Option<&UriPath>,
175 log: FileOrString,
176 from: BuildTargetId,
177 result: Option<Box<dyn Artifact>>,
178 ) -> std::result::Result<(), ArtifactSaveError>;
179
180 fn get_document_element(
182 &self,
183 uri: &DocumentElementUri,
184 ) -> Result<SharedDocumentElement<DocumentElement>, BackendError>
185 where
186 Self: Sized,
187 {
188 let d = self.get_document(uri.document_uri())?;
189 d.get(uri.name())
190 .ok_or_else(|| BackendError::NotFound(uri.clone().into()))
191 }
192
193 async fn get_document_element_async<A: AsyncEngine>(
195 &self,
196 uri: &DocumentElementUri,
197 ) -> Result<SharedDocumentElement<DocumentElement>, BackendError>
198 where
199 Self: Sized,
200 {
201 let d = self.get_document_async::<A>(uri.document_uri()).await?;
202 d.get(uri.name())
203 .ok_or_else(|| BackendError::NotFound(uri.clone().into()))
204 }
205
206 fn get_typed_document_element<T: IsDocumentElement>(
208 &self,
209 uri: &DocumentElementUri,
210 ) -> Result<SharedDocumentElement<T>, BackendError>
211 where
212 Self: Sized,
213 {
214 let d = self.get_document(uri.document_uri())?;
215 d.get_as(uri.name())
216 .ok_or_else(|| BackendError::NotFound(uri.clone().into()))
217 }
218
219 async fn get_typed_document_element_async<A: AsyncEngine, T: IsDocumentElement>(
221 &self,
222 uri: &DocumentElementUri,
223 ) -> Result<SharedDocumentElement<T>, BackendError>
224 where
225 Self: Sized,
226 {
227 let d = self.get_document_async::<A>(uri.document_uri()).await?;
228 d.get_as(uri.name())
229 .ok_or_else(|| BackendError::NotFound(uri.clone().into()))
230 }
231
232 fn uri_of(&self, p: &Path) -> Option<DocumentUri>
233 where
234 Self: Sized,
235 {
236 self.archive_of_source(p, |a, rel_path| {
237 let str = rel_path.as_os_str().to_str()?;
238 DocumentUri::from_archive_relpath(a.uri().clone(), str).ok()
239 })
240 .flatten()
241 }
242
243 fn archive_of_source<R>(
244 &self,
245 p: &Path,
246 mut f: impl FnMut(&LocalArchive, RelPath) -> R,
247 ) -> Option<R>
248 where
249 Self: Sized,
250 {
251 self.with_archives(|archives| {
252 for a in archives {
253 let Archive::Local(a) = a else { continue };
254 if p.relative_to(&a.path()).is_some() {
255 let rel_path = p.relative_to(&a.source_dir())?;
256 return Some(f(a, rel_path));
257 }
258 }
259 None
260 })
261 }
262
263 fn archive_of<R>(&self, p: &Path, mut f: impl FnMut(&LocalArchive, RelPath) -> R) -> Option<R>
264 where
265 Self: Sized,
266 {
267 self.with_archives(|archives| {
268 for a in archives {
269 let Archive::Local(a) = a else { continue };
270 if let Some(rp) = p.relative_to(&a.path()) {
271 return Some(f(a, rp));
272 }
273 }
274 None
275 })
276 }
277
278 fn with_local_archive<R>(&self, id: &ArchiveId, f: impl FnOnce(Option<&LocalArchive>) -> R) -> R
279 where
280 Self: Sized,
281 {
282 self.with_archive(id, |a| {
283 f(a.and_then(|a| match a {
284 Archive::Local(a) => Some(&**a),
285 Archive::Ext(..) => None,
286 }))
287 })
288 }
289
290 fn with_buildable_archive<R>(
291 &self,
292 id: &ArchiveId,
293 f: impl FnOnce(Option<&dyn BuildableArchive>) -> R,
294 ) -> R
295 where
296 Self: Sized,
297 {
298 self.with_archive(id, |a| {
299 f(a.and_then(|a| match a {
300 Archive::Local(a) => Some(&**a as _),
301 Archive::Ext(_, e) => e.buildable(),
302 }))
303 })
304 }
305
306 #[cfg(feature = "rdf")]
307 fn get_notations<E: AsyncEngine>(
308 &self,
309 uri: &SymbolUri,
310 ) -> impl Iterator<Item = (DocumentElementUri, Notation)>
311 where
312 Self: Sized;
313
314 #[cfg(feature = "rdf")]
315 fn get_var_notations<E: AsyncEngine>(
316 &self,
317 uri: &DocumentElementUri,
318 ) -> impl Iterator<Item = (DocumentElementUri, Notation)>
319 where
320 Self: Sized;
321
322 #[cfg(feature = "img")]
323 fn export_html(&self, uri: &DocumentUri, to: &Path) -> Result<(), String>
324 where
325 Self: Sized,
326 {
327 crate::images::html_export(uri, to, self)
328 }
329}
330
331#[derive(Clone, Debug)]
332pub enum AnyBackend {
333 Global,
334 Temp(TemporaryBackend),
335 Sandbox(SandboxedBackend),
336}
337
338impl AnyBackend {
339 #[must_use]
340 pub fn mathhub(&self) -> &Path {
342 match self {
343 Self::Global | Self::Temp(_) => crate::mathhub::mathhubs()
344 .iter()
345 .next()
346 .expect("No mathhubs found"),
347 Self::Sandbox(sb) => sb.root(),
348 }
349 }
350
351 pub fn mathhubs(&self) -> impl Iterator<Item = &Path> {
352 match self {
353 Self::Global | Self::Temp(_) => {
354 either::Left(crate::mathhub::mathhubs().iter().copied())
355 }
356 Self::Sandbox(sb) => either::Right(
357 std::iter::once(sb.root()).chain(crate::mathhub::mathhubs().iter().copied()),
358 ),
359 }
360 }
361
362 #[cfg(feature = "rdf")]
363 pub fn add_triples(&self, doc: &DocumentUri, triples: Vec<ulo::rdf_types::Triple>) {
364 use ftml_uris::FtmlUri;
365 if matches!(*self, Self::Global) {
366 GlobalBackend
367 .get()
368 .triple_store()
369 .add_graph(&doc.to_iri(), triples.into_iter());
370 }
371 }
372}
373
374impl LocalBackend for AnyBackend {
375 type ArchiveIter<'a> = either::Either<
376 std::slice::Iter<'a, Archive>,
377 <SandboxedBackend as LocalBackend>::ArchiveIter<'a>,
378 >;
379
380 fn save(
381 &self,
382 in_doc: &ftml_uris::DocumentUri,
383 rel_path: Option<&UriPath>,
384 log: FileOrString,
385 from: BuildTargetId,
386 result: Option<Box<dyn Artifact>>,
387 ) -> std::result::Result<(), ArtifactSaveError> {
388 if let Some(rp) = rel_path
389 && let Some(res) = result.as_ref()
390 {
391 save_top(self, in_doc, rp, &**res);
392 }
393 match self {
394 Self::Global => GlobalBackend.save(in_doc, rel_path, log, from, result),
395 Self::Temp(b) => b.save(in_doc, rel_path, log, from, result),
396 Self::Sandbox(b) => b.save(in_doc, rel_path, log, from, result),
397 }
398 }
399
400 fn with_archives<R>(&self, f: impl FnOnce(Self::ArchiveIter<'_>) -> R) -> R
401 where
402 Self: Sized,
403 {
404 match self {
405 Self::Global => GlobalBackend.with_archives(|i| f(either::Either::Left(i.iter()))),
406 Self::Temp(b) => b.with_archives(f),
407 Self::Sandbox(b) => b.with_archives(|i| f(either::Either::Right(i))),
408 }
409 }
410
411 fn get_reference<T: bincode::Decode<()>>(
412 &self,
413 rf: &ftml_ontology::narrative::DocDataRef<T>,
414 ) -> Result<T, BackendError>
415 where
416 Self: Sized,
417 {
418 match self {
419 Self::Global => GlobalBackend.get_reference(rf),
420 Self::Temp(b) => b.get_reference(rf),
421 Self::Sandbox(b) => b.get_reference(rf),
422 }
423 }
424
425 fn get_document(&self, uri: &ftml_uris::DocumentUri) -> Result<Document, BackendError> {
426 match self {
427 Self::Global => GlobalBackend.get_document(uri),
428 Self::Temp(b) => b.get_document(uri),
429 Self::Sandbox(b) => b.get_document(uri),
430 }
431 }
432
433 #[allow(clippy::future_not_send)]
434 fn get_document_async<A: AsyncEngine>(
435 &self,
436 uri: &DocumentUri,
437 ) -> impl Future<Output = Result<Document, BackendError>> + Send + use<A>
438 where
439 Self: Sized,
440 {
441 match self {
442 Self::Global => either::Left(GlobalBackend.get_document_async::<A>(uri)),
443 Self::Temp(b) => either::Right(either::Left(b.get_document_async::<A>(uri))),
444 Self::Sandbox(b) => either::Right(either::Right(b.get_document_async::<A>(uri))),
445 }
446 }
447
448 fn get_html_body(
449 &self,
450 d: &ftml_uris::DocumentUri,
451 ) -> Result<(Box<[ftml_ontology::utils::Css]>, Box<str>), BackendError> {
452 match self {
453 Self::Global => GlobalBackend.get_html_body(d),
454 Self::Temp(b) => b.get_html_body(d),
455 Self::Sandbox(b) => b.get_html_body(d),
456 }
457 }
458
459 fn get_html_body_async<A: AsyncEngine>(
460 &self,
461 d: &ftml_uris::DocumentUri,
462 ) -> impl Future<Output = Result<(Box<[ftml_ontology::utils::Css]>, Box<str>), BackendError>>
463 + Send
464 + use<A>
465 where
466 Self: Sized,
467 {
468 match self {
469 Self::Global => either::Left(GlobalBackend.get_html_body_async::<A>(d)),
470 Self::Temp(b) => either::Right(either::Left(b.get_html_body_async::<A>(d))),
471 Self::Sandbox(b) => either::Right(either::Right(b.get_html_body_async::<A>(d))),
472 }
473 }
474
475 fn get_html_body_inner(
476 &self,
477 d: &ftml_uris::DocumentUri,
478 ) -> Result<(Box<[ftml_ontology::utils::Css]>, Box<str>), BackendError> {
479 match self {
480 Self::Global => GlobalBackend.get_html_body_inner(d),
481 Self::Temp(b) => b.get_html_body_inner(d),
482 Self::Sandbox(b) => b.get_html_body_inner(d),
483 }
484 }
485
486 fn get_html_body_inner_async<A: AsyncEngine>(
487 &self,
488 d: &ftml_uris::DocumentUri,
489 ) -> impl Future<Output = Result<(Box<[ftml_ontology::utils::Css]>, Box<str>), BackendError>>
490 + use<A>
491 + Send
492 where
493 Self: Sized,
494 {
495 match self {
496 Self::Global => either::Left(GlobalBackend.get_html_body_inner_async::<A>(d)),
497 Self::Temp(b) => either::Right(either::Left(b.get_html_body_inner_async::<A>(d))),
498 Self::Sandbox(b) => either::Right(either::Right(b.get_html_body_inner_async::<A>(d))),
499 }
500 }
501
502 fn get_html_full(&self, d: &ftml_uris::DocumentUri) -> Result<Box<str>, BackendError> {
503 match self {
504 Self::Global => GlobalBackend.get_html_full(d),
505 Self::Temp(b) => b.get_html_full(d),
506 Self::Sandbox(b) => b.get_html_full(d),
507 }
508 }
509
510 fn get_html_fragment(
511 &self,
512 d: &ftml_uris::DocumentUri,
513 range: ftml_ontology::narrative::DocumentRange,
514 ) -> Result<(Box<[ftml_ontology::utils::Css]>, Box<str>), BackendError> {
515 match self {
516 Self::Global => GlobalBackend.get_html_fragment(d, range),
517 Self::Temp(b) => b.get_html_fragment(d, range),
518 Self::Sandbox(b) => b.get_html_fragment(d, range),
519 }
520 }
521
522 fn get_html_fragment_async<A: AsyncEngine>(
523 &self,
524 d: &ftml_uris::DocumentUri,
525 range: ftml_ontology::narrative::DocumentRange,
526 ) -> impl Future<Output = Result<(Box<[ftml_ontology::utils::Css]>, Box<str>), BackendError>>
527 + Send
528 + use<A> {
529 match self {
530 Self::Global => either::Left(GlobalBackend.get_html_fragment_async::<A>(d, range)),
531 Self::Temp(b) => either::Right(either::Left(b.get_html_fragment_async::<A>(d, range))),
532 Self::Sandbox(b) => {
533 either::Right(either::Right(b.get_html_fragment_async::<A>(d, range)))
534 }
535 }
536 }
537
538 fn get_module(&self, uri: &ftml_uris::ModuleUri) -> Result<ModuleLike, BackendError> {
539 match self {
540 Self::Global => GlobalBackend.get_module(uri),
541 Self::Temp(b) => b.get_module(uri),
542 Self::Sandbox(b) => b.get_module(uri),
543 }
544 }
545
546 fn get_module_async<A: AsyncEngine>(
547 &self,
548 uri: &ModuleUri,
549 ) -> impl Future<Output = Result<ModuleLike, BackendError>> + Send + use<A>
550 where
551 Self: Sized,
552 {
553 match self {
554 Self::Global => either::Left(GlobalBackend.get_module_async::<A>(uri)),
555 Self::Temp(b) => either::Right(either::Left(b.get_module_async::<A>(uri))),
556 Self::Sandbox(b) => either::Right(either::Right(b.get_module_async::<A>(uri))),
557 }
558 }
559
560 fn with_archive_or_group<R>(
561 &self,
562 id: &ftml_uris::ArchiveId,
563 f: impl FnOnce(Option<&ArchiveOrGroup>) -> R,
564 ) -> R
565 where
566 Self: Sized,
567 {
568 match self {
569 Self::Global => GlobalBackend.with_archive_or_group(id, f),
570 Self::Temp(b) => b.with_archive_or_group(id, f),
571 Self::Sandbox(b) => b.with_archive_or_group(id, f),
572 }
573 }
574
575 fn with_archive<R>(&self, id: &ftml_uris::ArchiveId, f: impl FnOnce(Option<&Archive>) -> R) -> R
576 where
577 Self: Sized,
578 {
579 match self {
580 Self::Global => GlobalBackend.with_archive(id, f),
581 Self::Temp(b) => b.with_archive(id, f),
582 Self::Sandbox(b) => b.with_archive(id, f),
583 }
584 }
585
586 #[cfg(feature = "rdf")]
587 #[inline]
588 fn get_notations<E: AsyncEngine>(
589 &self,
590 uri: &ftml_uris::SymbolUri,
591 ) -> impl Iterator<
592 Item = (
593 ftml_uris::DocumentElementUri,
594 ftml_ontology::narrative::elements::Notation,
595 ),
596 >
597 where
598 Self: Sized,
599 {
600 match self {
601 Self::Temp(b) => either::Left(b.get_notations::<E>(uri)),
602 _ => either::Right(GlobalBackend.get_notations::<E>(uri)),
603 }
604 }
606
607 #[cfg(feature = "rdf")]
608 #[inline]
609 fn get_var_notations<E: AsyncEngine>(
610 &self,
611 uri: &ftml_uris::DocumentElementUri,
612 ) -> impl Iterator<
613 Item = (
614 ftml_uris::DocumentElementUri,
615 ftml_ontology::narrative::elements::Notation,
616 ),
617 >
618 where
619 Self: Sized,
620 {
621 match self {
622 Self::Temp(b) => either::Left(b.get_var_notations::<E>(uri)),
623 _ => either::Right(GlobalBackend.get_var_notations::<E>(uri)),
624 }
625 }
626}
627
628fn save_top(slf: &AnyBackend, uri: &DocumentUri, rel_path: &UriPath, result: &dyn Artifact) {
629 for e in inventory::iter::<crate::FlamsExtension>() {
630 (e.on_build_result)(slf, uri, rel_path, result);
631 }
632}