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