1#![allow(unexpected_cfgs)]
2#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_cfg))]
3#![doc = include_str!("../README.md")]
4#![cfg_attr(doc,doc = document_features::document_features!())]
8
9pub mod archive_iter;
10pub mod artifacts;
11pub mod backend;
12pub mod document_file;
13pub mod formats;
14pub mod manager;
15pub mod manifest;
16pub mod mathhub;
17pub mod source_files;
18#[cfg(feature = "rdf")]
19pub mod triple_store;
20pub mod utils;
21pub use flams_backend_types as types;
22#[cfg(feature = "img")]
23pub mod images;
24
25#[cfg(feature = "rdf")]
26use crate::triple_store::RDFStore;
27use crate::{
28 artifacts::{Artifact, ContentResult, ContentUpdate, FileOrString},
29 formats::{BuildTargetId, SourceFormat, SourceFormatId},
30 manifest::RepositoryData,
31 source_files::{FileStates, SourceDir},
32 utils::{
33 AsyncEngine,
34 errors::{ArtifactSaveError, BackendError, FileError},
35 ignore_source::IgnoreSource,
36 path_ext::PathExt,
37 },
38};
39use flams_backend_types::{
40 archive_json::{ArchiveIndex, Institution},
41 archives::FileStateSummary,
42};
43use ftml_ontology::{domain::modules::Module, narrative::documents::Document};
44use ftml_uris::{
45 ArchiveId, ArchiveUri, DocumentUri, IsDomainUri, Language, ModuleUri, SimpleUriName, UriName,
46 UriPath, UriWithArchive, UriWithPath,
47};
48use std::{
49 hint::unreachable_unchecked,
50 path::{Path, PathBuf},
51 str::{self, FromStr},
52};
53
54pub struct FlamsExtension {
55 pub name: &'static str,
56 pub on_start: fn(),
57 pub on_build_result: fn(&backend::AnyBackend, &DocumentUri, &UriPath, &dyn Artifact),
58 pub on_reload: fn(),
59}
60
61inventory::collect!(FlamsExtension);
62
63type Result<T> = std::result::Result<T, BackendError>;
64pub trait MathArchive {
82 fn uri(&self) -> &ArchiveUri;
83 fn path(&self) -> &Path;
84 fn is_meta(&self) -> bool;
85
86 #[inline]
87 fn id(&self) -> &ArchiveId {
88 self.uri().archive_id()
89 }
90
91 fn load_module(&self, path: Option<&UriPath>, name: &UriName) -> Result<Module>;
93
94 fn load_module_async<A: AsyncEngine>(
96 &self,
97 path: Option<&UriPath>,
98 name: &UriName,
99 ) -> impl Future<Output = Result<Module>> + 'static + use<Self, A>
100 where
101 Self: Sized;
102
103 }
128
129pub trait ExternalArchive: Send + Sync + MathArchive + std::any::Any + std::fmt::Debug {
130 #[inline]
131 fn local_out(&self) -> Option<&dyn LocallyBuilt> {
132 None
133 }
134 #[inline]
135 fn buildable(&self) -> Option<&dyn BuildableArchive> {
136 None
137 }
138
139 fn load_document(
140 &self,
141 path: Option<&UriPath>,
142 name: &str,
143 language: Language,
144 ) -> Option<Document>;
145}
146
147#[derive(Debug)]
148pub enum Archive {
149 Local(Box<LocalArchive>),
150 Ext(&'static ArchiveKind, Box<dyn ExternalArchive>),
151}
152impl Archive {
153 fn buildable(&self) -> Option<&dyn BuildableArchive> {
154 match self {
155 Self::Local(l) => Some(&**l as _),
156 Self::Ext(_, a) => a.buildable(),
157 }
158 }
159}
160
161impl std::hash::Hash for Archive {
162 #[inline]
163 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
164 self.uri().hash(state);
165 }
166}
167impl std::borrow::Borrow<ArchiveUri> for Archive {
168 #[inline]
169 fn borrow(&self) -> &ArchiveUri {
170 self.uri()
171 }
172}
173impl PartialEq for Archive {
174 #[inline]
175 fn eq(&self, other: &Self) -> bool {
176 *self.uri() == *other.uri()
177 }
178}
179impl Eq for Archive {}
180
181impl MathArchive for Archive {
182 fn uri(&self) -> &ArchiveUri {
183 match self {
184 Self::Local(a) => a.uri(),
185 Self::Ext(_, a) => a.uri(),
186 }
187 }
188 fn path(&self) -> &Path {
189 match self {
190 Self::Local(a) => a.path(),
191 Self::Ext(_, a) => a.path(),
192 }
193 }
194 fn is_meta(&self) -> bool {
195 match self {
196 Self::Local(a) => a.is_meta(),
197 Self::Ext(_, a) => a.is_meta(),
198 }
199 }
200 fn load_module(&self, path: Option<&UriPath>, name: &UriName) -> Result<Module> {
201 match self {
202 Self::Local(a) => a.load_module(path, name),
203 Self::Ext(_, a) => a.load_module(path, name),
204 }
205 }
206 fn load_module_async<A: AsyncEngine>(
207 &self,
208 path: Option<&UriPath>,
209 name: &UriName,
210 ) -> impl Future<Output = Result<Module>> + 'static + use<A> {
211 match self {
212 Self::Local(a) => a.load_module_async::<A>(path, name),
213 Self::Ext(_, a) => todo!(),
214 }
215 }
216}
217
218#[derive(Copy, Clone, Debug)]
219pub struct ArchiveKind {
220 pub name: &'static str,
221 #[allow(clippy::type_complexity)]
222 make_new: fn(RepositoryData, &Path) -> std::result::Result<Box<dyn ExternalArchive>, String>,
223}
224
225impl ArchiveKind {
226 #[inline]
227 pub fn all() -> impl Iterator<Item = &'static Self> {
228 inventory::iter.into_iter()
229 }
230 #[must_use]
231 pub fn get(name: &str) -> Option<&'static Self> {
232 Self::all().find(|e| e.name == name)
233 }
234}
235inventory::collect!(ArchiveKind);
236#[macro_export]
237macro_rules! archive_kind {
238 ($i:ident { $($t:tt)* }) => {
239 pub static $i : $crate::ArchiveKind = $crate::ArchiveKind { $($t)* };
240 $crate::formats::__reexport::submit!{ $i }
241 };
242}
243
244pub trait BuildableArchive: MathArchive {
245 fn file_state(&self) -> FileStates;
246 fn formats(&self) -> &[SourceFormatId];
247 fn get_log(&self, relative_path: &str, target: BuildTargetId) -> PathBuf;
248
249 #[allow(clippy::too_many_arguments)]
250 fn save(
252 &self,
253 in_doc: &ftml_uris::DocumentUri,
254 rel_path: Option<&UriPath>,
255 log: FileOrString,
256 from: BuildTargetId,
257 result: Option<Box<dyn Artifact>>,
258 #[cfg(feature = "rdf")] relational: &RDFStore,
259 #[cfg(feature = "rdf")] load: bool,
260 ) -> std::result::Result<(), ArtifactSaveError>;
261
262 #[cfg(feature = "rdf")]
263 fn save_triples(
264 &self,
265 in_doc: &ftml_uris::DocumentUri,
266 rel_path: Option<&UriPath>,
267 relational: &RDFStore,
268 load: bool,
269 iter: Vec<ulo::rdf_types::Triple>,
270 );
271
272 fn escape_module_name(&self, in_path: &Path, name: &str) -> PathBuf {
273 in_path.join(name.replace('*', "__AST__"))
274 }
275}
276
277pub trait LocallyBuilt: BuildableArchive {
278 fn out_dir(&self) -> &Path;
279
280 fn out_path_of(
281 &self,
282 path: Option<&UriPath>,
283 doc_name: &SimpleUriName,
284 rel_path: Option<&UriPath>,
285 language: Language,
286 ) -> PathBuf;
287
288 fn document_file(
289 &self,
290 path: Option<&UriPath>,
291 rel_path: Option<&UriPath>,
292 doc_name: &SimpleUriName,
293 language: Language,
294 ) -> PathBuf {
295 self.out_path_of(path, doc_name, rel_path, language)
296 .join("content")
297 }
298
299 fn save_modules(&self, modules: &[Module]) -> std::result::Result<(), ArtifactSaveError> {
300 for m in modules {
301 let path = m.uri.path();
302 let name = m.uri.module_name();
303 let out = path.map_or_else(
304 || self.out_dir().join(".modules"),
305 |n| self.out_dir().join_uri_path(n).join(".modules"),
306 );
307 std::fs::create_dir_all(&out)
308 .map_err(|e| ArtifactSaveError::Fs(FileError::Creation(out.clone(), e)))?;
309 let out = self.escape_module_name(&out, name.as_ref());
310 let file = std::fs::File::create(&out)
311 .map_err(|e| ArtifactSaveError::Fs(FileError::Creation(out, e)))?;
312 let mut buf = std::io::BufWriter::new(file);
313 bincode::encode_into_std_write(m, &mut buf, bincode::config::standard())?;
314 }
316 Ok(())
317 }
318}
319
320#[derive(Debug)]
321pub struct LocalArchive {
322 pub(crate) uri: ArchiveUri,
323 pub(crate) out_path: PathBuf,
324 pub(crate) source: Option<Box<str>>,
325 pub(crate) formats: smallvec::SmallVec<SourceFormatId, 1>,
327 pub(crate) file_state: parking_lot::RwLock<SourceDir>,
329 pub ignore: IgnoreSource,
332 #[cfg(feature = "git")]
333 pub(crate) is_managed: std::sync::OnceLock<Option<flams_git::GitUrl>>,
334}
335impl MathArchive for LocalArchive {
336 #[inline]
337 fn uri(&self) -> &ArchiveUri {
338 &self.uri
339 }
340 fn path(&self) -> &Path {
341 self.out_path
342 .parent()
343 .expect("out path of an archive *must* have a parent")
344 }
345
346 fn is_meta(&self) -> bool {
347 self.uri.archive_id().is_meta()
348 }
349
350 fn load_module(&self, path: Option<&UriPath>, name: &UriName) -> Result<Module> {
351 let out = path.map_or_else(
352 || self.out_dir().join(".modules"),
353 |n| self.out_dir().join_uri_path(n).join(".modules"),
354 );
355 let out = self.escape_module_name(&out, name.as_ref());
356 if !out.exists() {
357 return Err(BackendError::NotFound(
358 ((self.uri.clone() / path.cloned()) | name.clone()).into(),
359 ));
360 }
361 let file = std::io::BufReader::new(std::fs::File::open(out)?);
362 let ret: Module = bincode::decode_from_reader(file, bincode::config::standard())?;
363 Ok(ret)
364 }
365
366 fn load_module_async<A: AsyncEngine>(
367 &self,
368 path: Option<&UriPath>,
369 name: &UriName,
370 ) -> impl Future<Output = Result<Module>> + 'static + use<A>
371 where
372 Self: Sized,
373 {
374 let out = path.map_or_else(
375 || self.out_dir().join(".modules"),
376 |n| self.out_dir().join_uri_path(n).join(".modules"),
377 );
378 let out = self.escape_module_name(&out, name.as_ref());
379 let uri = (self.uri.clone() / path.cloned()) | name.clone();
380 A::block_on(move || {
381 if !out.exists() {
382 return Err(BackendError::NotFound(uri.into()));
383 }
384 let file = std::io::BufReader::new(std::fs::File::open(out)?);
385 let ret = bincode::decode_from_reader(file, bincode::config::standard())?;
386 Ok(ret)
387 })
388 }
389}
390impl BuildableArchive for LocalArchive {
391 #[inline]
392 fn file_state(&self) -> FileStates {
393 self.file_state.read().state().clone()
394 }
395
396 #[inline]
397 fn formats(&self) -> &[SourceFormatId] {
398 &self.formats
399 }
400 fn get_log(&self, relative_path: &str, target: BuildTargetId) -> PathBuf {
401 use std::str::FromStr;
402 let rel_path = if let Some((first, lang)) = relative_path.rsplit_once('.')
403 && Language::from_str(lang).is_err()
404 {
405 first
406 } else {
407 relative_path
408 };
409 self.out_dir()
410 .join(rel_path)
411 .join(target.name)
412 .with_extension("log")
413 }
414
415 fn save(
416 &self,
417 in_doc: &ftml_uris::DocumentUri,
418 rel_path: Option<&UriPath>,
419 log: FileOrString,
420 from: BuildTargetId,
421 result: Option<Box<dyn Artifact>>,
422 #[cfg(feature = "rdf")] relational: &RDFStore,
423 #[cfg(feature = "rdf")] load: bool,
424 ) -> std::result::Result<(), ArtifactSaveError> {
425 let out = self.out_path_of(in_doc.path(), &in_doc.name, rel_path, in_doc.language);
426 if let Err(e) = std::fs::create_dir_all(&out) {
427 return Err(ArtifactSaveError::Fs(FileError::Creation(out, e)));
428 }
429 let logfile = out.join(from.name).with_extension("log");
430 match log {
431 FileOrString::File(f) => f.rename_safe(&logfile)?,
432 FileOrString::Str(s) => {
433 if let Err(e) = std::fs::write(&logfile, s.as_bytes()) {
434 return Err(ArtifactSaveError::Fs(FileError::Write(logfile, e)));
435 }
436 }
437 }
438 let Some(mut res) = result else { return Ok(()) };
439 let outfile = out.join(res.kind());
440 if res.as_any_mut().downcast_mut::<ContentUpdate>().is_some() {
441 let e = unsafe {
443 res.into_any()
444 .downcast::<ContentUpdate>()
445 .unwrap_unchecked()
446 };
447 if let Some(d) = e.document {
448 let mut cr = ContentResult::read(outfile.clone())?;
449 cr.document = d;
451 cr.write(&outfile)?;
452 }
453 if !e.modules.is_empty() {
454 self.save_modules(&e.modules)?;
455 }
456 return Ok(());
457 }
458 res.write(&outfile)?;
459 if let Some(e) = res.as_any_mut().downcast_mut::<ContentResult>() {
460 #[cfg(feature = "rdf")]
461 self.save_triples(
462 in_doc,
463 rel_path,
464 relational,
465 load,
466 std::mem::take(&mut e.triples),
467 );
468 self.save_modules(&e.modules)?;
469 }
470 Ok(())
471 }
472
473 #[cfg(feature = "rdf")]
474 fn save_triples(
475 &self,
476 in_doc: &ftml_uris::DocumentUri,
477 rel_path: Option<&UriPath>,
478 relational: &RDFStore,
479 load: bool,
480 iter: Vec<ulo::rdf_types::Triple>,
481 ) {
482 use ftml_uris::FtmlUri;
483 let out = self.out_path_of(in_doc.path(), &in_doc.name, rel_path, in_doc.language);
484 let _ = std::fs::create_dir_all(&out);
485 let out = out.join("index.ttl");
486 relational.export(iter.into_iter(), &out, in_doc);
487 if load {
488 relational.load(&out, in_doc.to_iri());
490 }
491 }
492}
493
494impl LocallyBuilt for LocalArchive {
495 #[inline]
496 fn out_dir(&self) -> &Path {
497 &self.out_path
498 }
499
500 fn out_path_of(
501 &self,
502 path: Option<&UriPath>,
503 doc_name: &SimpleUriName,
504 rel_path: Option<&UriPath>,
505 language: Language,
506 ) -> PathBuf {
507 if let Some(rp) = rel_path {
508 use std::str::FromStr;
509 let mut steps = rp.steps();
510 let Some(mut last) = steps.next_back() else {
511 unsafe { unreachable_unchecked() }
513 };
514 let out = steps.fold(self.out_dir().to_path_buf(), |p, s| p.join(s));
515 if let Some((first, lang)) = last.rsplit_once('.')
516 && Language::from_str(lang).is_err()
517 {
518 last = first;
519 }
520 return out.join(last);
521 }
522 self.rel_path_of(path, doc_name, language).map_or_else(
523 || {
524 let lang: &'static str = language.into();
525 let p = path.map_or_else(
526 || self.out_path.join(doc_name.as_ref()),
527 |n| self.out_path.join_uri_path(n).join(doc_name.as_ref()),
528 );
529 let mp = p.with_added_extension(lang);
530 if mp.exists() {
531 mp
532 } else {
533 let mp2 = p.with_extension(lang);
534 if mp2 != mp && mp2.exists() { mp2 } else { p }
535 }
536 },
537 |rel_path| {
538 self.out_path.join(rel_path)
541 },
542 )
543 }
544}
545impl LocalArchive {
546 pub fn document_of(&self, path: Option<&UriPath>, name: &UriName) -> Option<DocumentUri> {
547 let mut mname = name.first();
548 let mut file = self.source_dir();
549 let maybe_step = if let Some(path) = path {
550 let mut steps = path.steps();
551 let _ = steps.next_back();
552 for step in steps {
553 file = file.join(step);
554 }
555 path.steps().next_back()
556 } else {
557 None
558 };
559 if let Some(step) = maybe_step
560 && let Ok(mut d) = std::fs::read_dir(file.join(step))
561 {
562 if let Some(rp) = d.find_map::<String, _>(|p| {
563 p.ok().and_then(|p| {
564 let fnm = p.file_name();
565 let name = fnm.as_os_str().as_encoded_bytes();
566 let name = name.strip_prefix(mname.as_bytes())?.strip_prefix(b".")?;
567 let lang = self.formats.iter().find_map(|f| {
568 f.file_extensions.iter().find_map(|e| {
569 name.strip_suffix(e.as_bytes())
570 .and_then(|s| s.strip_suffix(b"."))
571 })
572 })?;
573 if Language::from_str(std::str::from_utf8(lang).ok()?).is_ok() {
574 Some(
575 p.path()
576 .as_os_str()
577 .to_str()?
578 .strip_prefix(self.source_dir().as_os_str().to_str()?)?[1..]
579 .to_string(),
580 )
581 } else {
582 None
583 }
584 })
585 }) {
586 return DocumentUri::from_archive_relpath(self.uri.clone(), &rp).ok();
587 }
588 mname = step;
589 };
590
591 if let Ok(mut d) = std::fs::read_dir(file) {
592 if let Some(rp) = d.find_map::<String, _>(|p| {
593 p.ok().and_then(|p| {
594 let fnm = p.file_name();
595 let name = fnm.as_os_str().as_encoded_bytes();
596 let Some(name) = name.strip_prefix(mname.as_bytes()) else {
597 return None;
598 };
599 let Some(name) = name.strip_prefix(b".") else {
600 return None;
601 };
602 let Some(lang) = name.strip_suffix(b".tex") else {
603 return None;
604 };
605 if Language::from_str(std::str::from_utf8(lang).ok()?).is_ok() {
606 Some(
607 p.path()
608 .as_os_str()
609 .to_str()?
610 .strip_prefix(self.source_dir().as_os_str().to_str()?)?[1..]
611 .to_string(),
612 )
613 } else {
614 None
615 }
616 })
617 }) {
618 return DocumentUri::from_archive_relpath(self.uri.clone(), &rp).ok();
619 }
620 };
621 None
622 }
623
624 #[cfg(feature = "git")]
625 pub fn git_url(&self, on_host: &url::Url) -> Option<&flams_git::GitUrl> {
626 self.is_managed
627 .get_or_init(|| {
628 let Ok(repo) = flams_git::repos::GitRepo::open(self.path()) else {
629 return None;
630 };
631 on_host.host_str().and_then(|s| repo.is_managed(s))
632 })
633 .as_ref()
634 }
635
636 pub fn state_summary(&self) -> FileStateSummary {
637 self.file_state.read().state().summarize()
638 }
639
640 #[must_use]
641 pub fn source_dir(&self) -> PathBuf {
642 self.path().join(self.source.as_deref().unwrap_or("source"))
643 }
644
645 #[must_use]
647 pub fn manifest_of(p: &Path) -> Option<PathBuf> {
648 for e in std::fs::read_dir(p).ok()? {
649 let Ok(e) = e else { continue };
650 let Ok(md) = e.metadata() else { continue };
651 if md.is_dir() && e.file_name().eq_ignore_ascii_case("meta-inf") {
652 return crate::archive_iter::find_manifest(&e.path());
653 }
654 }
655 None
656 }
657
658 #[inline]
659 #[must_use]
660 fn out_dir_of(p: &Path) -> PathBuf
661 where
662 Self: Sized,
663 {
664 p.join(".flams")
665 }
666
667 #[inline]
668 pub fn with_sources<R>(&self, f: impl FnOnce(&SourceDir) -> R) -> R {
669 f(&self.file_state.read())
670 }
671
672 pub fn update_sources(&self) {
673 let dir = SourceDir::new(&self.source_dir(), &self.ignore, self.formats());
674 let mut state = self.file_state.write();
675 state.update(dir);
676 }
677
678 pub fn rel_path_of(
680 &self,
681 path: Option<&UriPath>,
682 doc_name: &SimpleUriName,
683 language: Language,
684 ) -> Option<PathBuf> {
685 let dir = path.map_or_else(|| self.source_dir(), |n| self.source_dir().join_uri_path(n));
686 for f in std::fs::read_dir(&dir)
687 .ok()?
688 .filter_map(std::result::Result::ok)
689 {
690 let Ok(m) = f.metadata() else { continue };
691 if !m.is_file() {
692 continue;
693 }
694 let fname = f.file_name();
695 let Some(name) = fname.to_str() else { continue };
696 let Some((_, ext)) = name.rsplit_once('.') else {
697 continue;
698 };
699 if !self
700 .formats
701 .iter()
702 .flat_map(|sf| sf.file_extensions.iter())
703 .any(|e| *e == ext)
704 {
705 continue;
706 }
707
708 if !name.starts_with(doc_name.as_ref()) {
709 continue;
710 }
711 let rest = &name[doc_name.as_ref().len()..];
712 if !rest.is_empty() && !rest.starts_with('.') {
713 continue;
714 }
715 let rest = rest.strip_prefix('.').unwrap_or(rest);
716 if rest.contains('.') {
717 let lang: &'static str = language.into();
718 if !rest.starts_with(lang) {
719 continue;
720 }
721 }
722 let path = f
723 .path()
724 .strip_prefix(self.source_dir())
725 .ok()?
726 .with_extension("");
727 return Some(path);
728 }
729 None
730 }
731}