Skip to main content

flams_ftml/
lib.rs

1//#![feature(string_from_utf8_lossy_owned)]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3
4//mod parser;
5
6use 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    /*match build_ftml(backend, &html.0, uri, task.rel_path()) {
121        Err(e) => BuildResult {
122            log: Either::Left(e),
123            result: Err(Vec::new()),
124        },
125        Ok((r, s)) => BuildResult {
126            log: Either::Left(s),
127            result: Ok(BuildResultArtifact::Data(Box::new(r))),
128        },
129    }*/
130}
131
132/// # Errors
133pub 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}