1#![cfg_attr(docsrs, feature(doc_cfg))]
3
4use flams_math_archives::{
7 artifacts::{ContentResult, FileOrString, FtmlFile, FtmlString},
8 backend::{AnyBackend, LocalBackend},
9 build_target,
10 formats::{BuildResult, BuildSpec},
11 source_format, Archive, LocallyBuilt, MathArchive,
12};
13pub use ftml5ever::FtmlResult;
14use ftml_ontology::{
15 domain::{declarations::AnyDeclarationRef, HasDeclarations},
16 utils::RefTree,
17 Ftml,
18};
19use ftml_uris::{DocumentUri, UriWithArchive, UriWithPath};
20
21source_format! { FTML {
22 name:"ftml",
23 description:"Flexiformal HTML",
24 targets:&[FTML_IMPORT.id(),FTML_CONTENT.id()],
25 file_extensions: &["html","html","xhtml"],
26 dependencies: |_| Vec::new()
27}}
28
29build_target! { FTML_IMPORT {
30 name:"import-ftml",
31 description:"imports existent FTML",
32 run: import
33}}
34
35build_target! { FTML_CONTENT {
36 name:"ftml-content",
37 description:"extracts content from FTML",
38 run: extract
39}}
40
41#[allow(clippy::needless_pass_by_value)]
42fn import(spec: BuildSpec) -> BuildResult {
43 match spec.source {
44 either::Either::Right(s) => BuildResult {
45 log: FileOrString::Str("ok".to_string().into_boxed_str()),
46 result: Ok(Some(
47 Box::new(FtmlString(s.to_string().into_boxed_str())) as _
48 )),
49 },
50 either::Either::Left(p) => BuildResult {
51 log: FileOrString::Str("ok".to_string().into_boxed_str()),
52 result: Ok(Some(Box::new(FtmlFile(p.to_path_buf())) as _)),
53 },
54 }
55}
56
57#[deprecated(note = "uses local archives only")]
58#[allow(clippy::too_many_lines)]
59#[allow(clippy::needless_pass_by_value)]
60fn extract(spec: BuildSpec) -> BuildResult {
61 let html: Result<String, _> = spec.backend.with_archive(spec.uri.archive_id(), |a| {
62 let Some(Archive::Local(a)) = a else {
63 return Err(BuildResult::err());
64 };
65 let path = a
66 .out_path_of(
67 spec.uri.path(),
68 &spec.uri.name,
69 Some(spec.rel_path),
70 spec.uri.language,
71 )
72 .join(FTML.name);
73 std::fs::read_to_string(path).map_err(|e| {
74 let mut err = BuildResult::err();
75 err.log = FileOrString::Str(e.to_string().into_boxed_str());
76 err
77 })
78 });
79 let html = match html {
80 Err(e) => return e,
81 Ok(h) => h,
82 };
83 let uri = spec.uri.clone();
84 let (lg, r) = flams_system::span_capture(|| build_ftml(spec.backend, &html, uri));
85 match r {
86 Err(e) => {
87 let mut err = BuildResult::err();
88 err.log = FileOrString::Str(format!("{lg}\n{e}").into_boxed_str());
89 err
90 }
91 Ok(FtmlResult {
92 ftml,
93 css,
94 errors,
95 doc,
96 body,
97 inner_offset,
98 }) => {
99 let has_errored = !errors.is_empty();
100 BuildResult {
101 log: FileOrString::Str(format!("{lg}\n{errors:?}").into_boxed_str()),
102 result: if has_errored {
103 Err(Vec::new())
104 } else {
105 Ok(Some(Box::new(ContentResult {
106 document: doc.document,
107 modules: doc.modules,
108 data: doc.data,
109 body,
110 inner_offset,
111 css,
112 ftml,
113 triples: doc.triples,
114 })))
115 },
116 }
117 }
118 }
119
120 }
131
132pub fn build_ftml(
134 backend: &AnyBackend,
135 html: &str,
136 uri: DocumentUri,
137) -> Result<FtmlResult, String> {
138 static CSS_SUBSTS: [(&str, &str); 1] = [(
139 "https://raw.githack.com/FlexiFormal/RusTeX/main/rustex/src/resources/rustex-v0-1-16.css",
140 "srv:/rustex-v0-1-16.css",
141 )];
142 let path = uri.path().cloned();
143 let archive = uri.archive_uri().clone();
144 let mut r = ftml5ever::run(
145 html,
146 |src| {
147 let path = std::path::Path::new(src);
148 if let Some(s) =
149 backend.archive_of(path, |a, rp| format!("srv:/img?a={}&rp={}", a.id(), rp))
150 {
151 return Some(s);
152 }
153 let kpsewhich = &*tex_engine::engine::filesystem::kpathsea::KPATHSEA;
154 let last = src.rsplit_once('/').map_or(src, |(_, p)| p);
155 kpsewhich.which(last).map_or_else(
156 || Some(format!("srv:/img?file={src}")),
157 |file| {
158 if file == path {
159 Some(format!("srv:/img?kpse={last}"))
160 } else {
161 None
162 }
163 },
164 )
165 },
166 |mut css| {
167 CSS_SUBSTS
168 .iter()
169 .find_map(|(old, new)| {
170 if css == *old {
171 Some((*new).to_string().into_boxed_str())
172 } else {
173 None
174 }
175 })
176 .or_else(|| {
177 if css.contains("://") {
178 return None;
179 }
180 let mut path = path.as_ref()?.as_ref();
181 loop {
182 if let Some(s) = css.strip_prefix("./") {
183 css = s;
184 } else if let Some(s) = css.strip_prefix("../") {
185 if path.is_empty() {
186 return None;
187 }
188 css = s;
189 path = if let Some((p, _)) = path.rsplit_once('/') {
190 p
191 } else {
192 ""
193 };
194 } else {
195 break;
196 }
197 }
198 Some(
199 format!(
200 "srv:/aux?a={}&f={path}{}{css}",
201 archive.id,
202 if path.is_empty() { "" } else { "/" }
203 )
204 .into_boxed_str(),
205 )
206 })
207 },
208 uri,
209 true,
210 );
211 if let Ok(dr) = r.as_mut() {
212 let content = &mut dr.doc;
213 for m in &content.modules {
214 let _ = m.initialize(&mut |m| backend.get_module(m).ok());
215 for e in m.dfs() {
216 if let AnyDeclarationRef::Morphism(m) = e {
217 content.triples.extend(m.triples());
218 }
219 }
220 }
221 }
222 r
223}