flams_router_content/
server_fns.rs

1use std::str::FromStr;
2
3use flams_ontology::{
4    SlideElement,
5    languages::Language,
6    narration::{
7        LOKind,
8        notations::Notation,
9        problems::{ProblemFeedbackJson, ProblemResponse, Quiz, SolutionData},
10    },
11    uris::{
12        ArchiveId, ArchiveURITrait, ContentURI, ContentURITrait, DocumentElementURI, DocumentURI,
13        NarrativeURI, PathURITrait, SymbolURI, URI, URIRefTrait,
14    },
15};
16use flams_utils::{CSS, unwrap};
17use ftml_viewer_components::components::{TOCElem, omdoc::OMDoc};
18use leptos::prelude::*;
19
20#[cfg(feature = "ssr")]
21use flams_router_base::uris::{DocURIComponents, SymURIComponents, URIComponents};
22
23#[server(
24  prefix="/content",
25  endpoint="document",
26  input=server_fn::codec::GetUrl,
27  output=server_fn::codec::Json
28)]
29pub async fn document(
30    uri: Option<DocumentURI>,
31    rp: Option<String>,
32    a: Option<ArchiveId>,
33    p: Option<String>,
34    l: Option<Language>,
35    d: Option<String>,
36) -> Result<(DocumentURI, Vec<CSS>, String), ServerFnError<String>> {
37    let Result::<DocURIComponents, _>::Ok(comps) = (uri, rp, a, p, l, d).try_into() else {
38        return Err("invalid uri components".to_string().into());
39    };
40    let Some(uri) = comps.parse() else {
41        return Err("invalid uri".to_string().into());
42    };
43    server::document(uri).await
44}
45
46#[server(
47  prefix="/content",
48  endpoint="document_of",
49  input=server_fn::codec::GetUrl,
50  output=server_fn::codec::Json
51)]
52pub async fn document_of(uri: URI) -> Result<DocumentURI, ServerFnError<String>> {
53    use flams_system::backend::Backend;
54    let m = match uri {
55        URI::Base(_) | URI::Archive(_) | URI::Path(_) => {
56            return Err("not in a document".to_string().into());
57        }
58        URI::Narrative(NarrativeURI::Document(d)) => return Ok(d),
59        URI::Narrative(NarrativeURI::Element(d)) => return Ok(d.document().clone()),
60        URI::Content(ContentURI::Module(ref m)) => m,
61        URI::Content(ContentURI::Symbol(ref s)) => s.module(),
62    };
63    flams_system::backend::GlobalBackend::get().with_local_archive(m.archive_id(), |o| {
64        let Some(archive) = o else {
65            return Err(format!("no local archive {} found", m.archive_id()).into());
66        };
67        let mut mname = m.name().first_name().as_ref();
68        let mut file = archive.source_dir();
69        let maybe_step = if let Some(path) = m.path() {
70            for step in &path.steps()[..path.steps().len() - 1] {
71                file = file.join(step.as_ref());
72            }
73            Some(path.last_name().as_ref())
74        } else {
75            None
76        };
77        if let Some(step) = maybe_step {
78            if let Ok(mut d) = std::fs::read_dir(file.join(step)) {
79                if let Some(rp) = d.find_map::<String, _>(|p| {
80                    p.ok().and_then(|p| {
81                        let fnm = p.file_name();
82                        let name = fnm.as_os_str().as_encoded_bytes();
83                        let Some(name) = name.strip_prefix(mname.as_bytes()) else {
84                            return None;
85                        };
86                        let Some(name) = name.strip_prefix(&[b'.']) else {
87                            return None;
88                        };
89                        let Some(lang) = name.strip_suffix(b".tex") else {
90                            return None;
91                        };
92                        if Language::from_str(std::str::from_utf8(lang).ok()?).is_ok() {
93                            Some(
94                                p.path()
95                                    .as_os_str()
96                                    .to_str()?
97                                    .strip_prefix(archive.source_dir().as_os_str().to_str()?)?[1..]
98                                    .to_string(),
99                            )
100                        } else {
101                            None
102                        }
103                    })
104                }) {
105                    return DocumentURI::from_archive_relpath(m.archive_uri().owned(), &rp)
106                        .map_err(|e| e.to_string().into());
107                }
108                mname = step;
109            };
110        }
111        if let Ok(mut d) = std::fs::read_dir(file) {
112            if let Some(rp) = d.find_map::<String, _>(|p| {
113                p.ok().and_then(|p| {
114                    let fnm = p.file_name();
115                    let name = fnm.as_os_str().as_encoded_bytes();
116                    let Some(name) = name.strip_prefix(mname.as_bytes()) else {
117                        return None;
118                    };
119                    let Some(name) = name.strip_prefix(&[b'.']) else {
120                        return None;
121                    };
122                    let Some(lang) = name.strip_suffix(b".tex") else {
123                        return None;
124                    };
125                    if Language::from_str(std::str::from_utf8(lang).ok()?).is_ok() {
126                        Some(
127                            p.path()
128                                .as_os_str()
129                                .to_str()?
130                                .strip_prefix(archive.source_dir().as_os_str().to_str()?)?[1..]
131                                .to_string(),
132                        )
133                    } else {
134                        None
135                    }
136                })
137            }) {
138                return DocumentURI::from_archive_relpath(m.archive_uri().owned(), &rp)
139                    .map_err(|e| e.to_string().into());
140            }
141        };
142        Err("Not found".to_string().into())
143    })
144}
145
146#[server(
147  prefix="/content",
148  endpoint="toc",
149  input=server_fn::codec::GetUrl,
150  output=server_fn::codec::Json
151)]
152pub async fn toc(
153    uri: Option<DocumentURI>,
154    rp: Option<String>,
155    a: Option<ArchiveId>,
156    p: Option<String>,
157    l: Option<Language>,
158    d: Option<String>,
159) -> Result<(Vec<CSS>, Vec<TOCElem>), ServerFnError<String>> {
160    let Result::<DocURIComponents, _>::Ok(comps) = (uri, rp, a, p, l, d).try_into() else {
161        return Err("invalid uri components".to_string().into());
162    };
163    let Some(uri) = comps.parse() else {
164        return Err("invalid uri".to_string().into());
165    };
166    server::toc(uri).await
167}
168
169#[server(
170  prefix="/content",
171  endpoint="fragment",
172  input=server_fn::codec::GetUrl,
173  output=server_fn::codec::Json
174)]
175#[allow(clippy::many_single_char_names)]
176#[allow(clippy::too_many_arguments)]
177pub async fn fragment(
178    uri: Option<URI>,
179    rp: Option<String>,
180    a: Option<ArchiveId>,
181    p: Option<String>,
182    l: Option<Language>,
183    d: Option<String>,
184    e: Option<String>,
185    m: Option<String>,
186    s: Option<String>,
187    context: Option<URI>,
188) -> Result<(URI, Vec<CSS>, String), ServerFnError<String>> {
189    let Result::<URIComponents, _>::Ok(comps) = (uri, rp, a, p, l, d, e, m, s).try_into() else {
190        return Err("invalid uri components".to_string().into());
191    };
192    let Some(uri) = comps.parse() else {
193        return Err("invalid uri".to_string().into());
194    };
195    server::fragment(uri, context).await
196}
197
198#[server(
199  prefix="/content",
200  endpoint="los",
201  input=server_fn::codec::GetUrl,
202  output=server_fn::codec::Json
203)]
204#[allow(clippy::many_single_char_names)]
205#[allow(clippy::too_many_arguments)]
206pub async fn los(
207    uri: Option<SymbolURI>,
208    a: Option<ArchiveId>,
209    p: Option<String>,
210    m: Option<String>,
211    s: Option<String>,
212    problems: bool,
213) -> Result<Vec<(DocumentElementURI, LOKind)>, ServerFnError<String>> {
214    let Result::<SymURIComponents, _>::Ok(comps) = (uri, a, p, m, s).try_into() else {
215        return Err("invalid uri components".to_string().into());
216    };
217    let Some(uri) = comps.parse() else {
218        return Err("invalid uri".to_string().into());
219    };
220    server::los(uri, problems).await
221}
222
223#[server(
224  prefix="/content",
225  endpoint="notations",
226  input=server_fn::codec::GetUrl,
227  output=server_fn::codec::Json
228)]
229#[allow(clippy::many_single_char_names)]
230#[allow(clippy::too_many_arguments)]
231pub async fn notations(
232    uri: Option<URI>,
233    rp: Option<String>,
234    a: Option<ArchiveId>,
235    p: Option<String>,
236    l: Option<Language>,
237    d: Option<String>,
238    e: Option<String>,
239    m: Option<String>,
240    s: Option<String>,
241) -> Result<Vec<(DocumentElementURI, Notation)>, ServerFnError<String>> {
242    let Result::<URIComponents, _>::Ok(comps) = (uri, rp, a, p, l, d, e, m, s).try_into() else {
243        return Err("invalid uri components".to_string().into());
244    };
245    let Some(uri) = comps.parse() else {
246        return Err("invalid uri".to_string().into());
247    };
248    server::notations(uri).await
249}
250
251#[server(
252  prefix="/content",
253  endpoint="omdoc",
254  input=server_fn::codec::GetUrl,
255  output=server_fn::codec::Json
256)]
257#[allow(clippy::many_single_char_names)]
258#[allow(clippy::too_many_arguments)]
259pub async fn omdoc(
260    uri: Option<URI>,
261    rp: Option<String>,
262    a: Option<ArchiveId>,
263    p: Option<String>,
264    l: Option<Language>,
265    d: Option<String>,
266    e: Option<String>,
267    m: Option<String>,
268    s: Option<String>,
269) -> Result<(Vec<CSS>, OMDoc), ServerFnError<String>> {
270    let Result::<URIComponents, _>::Ok(comps) = (uri, rp, a, p, l, d, e, m, s).try_into() else {
271        return Err("invalid uri components".to_string().into());
272    };
273    let Some(uri) = comps.parse() else {
274        return Err("invalid uri".to_string().into());
275    };
276    server::omdoc(uri).await
277}
278
279#[server(
280  prefix="/content",
281  endpoint="title",
282  input=server_fn::codec::GetUrl,
283  output=server_fn::codec::Json
284)]
285#[allow(clippy::many_single_char_names)]
286#[allow(clippy::too_many_arguments)]
287pub async fn title(
288    uri: Option<URI>,
289    rp: Option<String>,
290    a: Option<ArchiveId>,
291    p: Option<String>,
292    l: Option<Language>,
293    d: Option<String>,
294    e: Option<String>,
295    m: Option<String>,
296    s: Option<String>,
297) -> Result<(Vec<CSS>, String), ServerFnError<String>> {
298    let Result::<URIComponents, _>::Ok(comps) = (uri, rp, a, p, l, d, e, m, s).try_into() else {
299        return Err("invalid uri components".to_string().into());
300    };
301    let Some(uri) = comps.parse() else {
302        return Err("invalid uri".to_string().into());
303    };
304    server::title(uri).await
305}
306
307#[server(
308  prefix="/content",
309  endpoint="quiz",
310  input=server_fn::codec::GetUrl,
311  output=server_fn::codec::Json
312)]
313#[allow(clippy::many_single_char_names)]
314#[allow(clippy::too_many_arguments)]
315pub async fn get_quiz(
316    uri: Option<DocumentURI>,
317    rp: Option<String>,
318    a: Option<ArchiveId>,
319    p: Option<String>,
320    l: Option<Language>,
321    d: Option<String>,
322) -> Result<Quiz, ServerFnError<String>> {
323    let Result::<DocURIComponents, _>::Ok(comps) = (uri, rp, a, p, l, d).try_into() else {
324        return Err("invalid uri components".to_string().into());
325    };
326    let Some(uri) = comps.parse() else {
327        return Err("invalid uri".to_string().into());
328    };
329    server::get_quiz(uri).await
330}
331
332#[server(prefix = "/content", endpoint = "grade_enc",
333    input=server_fn::codec::Json,
334    output=server_fn::codec::Json
335)]
336pub async fn grade_enc(
337    submissions: Vec<(String, Vec<Option<ProblemResponse>>)>,
338) -> Result<Vec<Vec<ProblemFeedbackJson>>, ServerFnError<String>> {
339    tokio::task::spawn_blocking(move || {
340        let mut ret = Vec::new();
341        for (sol, resps) in submissions {
342            let mut ri = Vec::new();
343            let sol = flams_ontology::narration::problems::Solutions::from_jstring(&sol)
344                .ok_or_else(|| format!("Invalid solution string: {sol}"))?;
345            for resp in resps {
346                let r = if let Some(resp) = resp {
347                    sol.check_response(&resp).ok_or_else(|| {
348                        "Response {resp:?} does not match solution {sol:?}".to_string()
349                    })?
350                } else {
351                    sol.default_feedback()
352                };
353                ri.push(r.to_json());
354            }
355            ret.push(ri)
356        }
357        Ok(ret)
358    })
359    .await
360    .map_err(|e| e.to_string())?
361}
362
363#[server(prefix = "/content", endpoint = "grade",
364    input=server_fn::codec::Json,
365    output=server_fn::codec::Json
366)]
367pub async fn grade(
368    submissions: Vec<(Box<[SolutionData]>, Vec<Option<ProblemResponse>>)>,
369) -> Result<Vec<Vec<ProblemFeedbackJson>>, ServerFnError<String>> {
370    tokio::task::spawn_blocking(move || {
371        let mut ret = Vec::new();
372        for (sol, resps) in submissions {
373            let mut ri = Vec::new();
374            let sol = flams_ontology::narration::problems::Solutions::from_solutions(sol);
375            for resp in resps {
376                let r = if let Some(resp) = resp {
377                    sol.check_response(&resp).ok_or_else(|| {
378                        "Response {resp:?} does not match solution {sol:?}".to_string()
379                    })?
380                } else {
381                    sol.default_feedback()
382                };
383                ri.push(r.to_json());
384            }
385            ret.push(ri)
386        }
387        Ok(ret)
388    })
389    .await
390    .map_err(|e| e.to_string())?
391}
392
393#[server(prefix = "/content", endpoint = "solution",
394    input=server_fn::codec::GetUrl
395)]
396#[allow(clippy::many_single_char_names)]
397#[allow(clippy::too_many_arguments)]
398pub async fn solution(
399    uri: Option<URI>,
400    rp: Option<String>,
401    a: Option<ArchiveId>,
402    p: Option<String>,
403    l: Option<Language>,
404    d: Option<String>,
405    e: Option<String>,
406) -> Result<String, ServerFnError<String>> {
407    use flams_ontology::uris::NarrativeURI;
408    use flams_utils::Hexable;
409    use flams_web_utils::blocking_server_fn;
410    let Result::<URIComponents, _>::Ok(comps) = (uri, rp, a, p, l, d, e, None, None).try_into()
411    else {
412        return Err("invalid uri components".to_string().into());
413    };
414    let Some(URI::Narrative(NarrativeURI::Element(uri))) = comps.parse() else {
415        return Err("invalid uri".to_string().into());
416    };
417    blocking_server_fn(move || {
418        let s = server::get_solution(&uri)?;
419        s.as_hex().map_err(|e| e.to_string())
420    })
421    .await
422}
423
424#[server(
425  prefix="/content",
426  endpoint="slides",
427  input=server_fn::codec::GetUrl,
428  output=server_fn::codec::Json
429)]
430#[allow(clippy::many_single_char_names)]
431#[allow(clippy::too_many_arguments)]
432pub async fn slides_view(
433    uri: Option<URI>,
434    rp: Option<String>,
435    a: Option<ArchiveId>,
436    p: Option<String>,
437    l: Option<Language>,
438    d: Option<String>,
439    e: Option<String>,
440    m: Option<String>,
441    s: Option<String>,
442) -> Result<(Vec<CSS>, Vec<SlideElement>), ServerFnError<String>> {
443    let Result::<URIComponents, _>::Ok(comps) = (uri, rp, a, p, l, d, e, m, s).try_into() else {
444        return Err("invalid uri components".to_string().into());
445    };
446    let Some(uri) = comps.parse() else {
447        return Err("invalid uri".to_string().into());
448    };
449    server::slides(uri).await
450}
451
452#[cfg(feature = "ssr")]
453mod server {
454    use crate::ssr::{backend, insert_base_url};
455    use flams_ontology::{
456        Checked, SlideElement,
457        content::{ContentReference, declarations::Declaration},
458        narration::{
459            DocumentElement, LOKind, NarrationTrait, NarrativeReference,
460            notations::Notation,
461            paragraphs::LogicalParagraph,
462            problems::{Problem, Quiz, Solutions},
463            sections::Section,
464        },
465        rdf::ontologies::ulo2,
466        uris::{
467            ContentURI, DocumentElementURI, DocumentURI, NarrativeURI, SymbolURI, URI,
468            URIOrRefTrait,
469        },
470    };
471    use flams_system::backend::{Backend, GlobalBackend, rdf::sparql};
472    use flams_utils::{CSS, unwrap, vecmap::VecSet};
473    use flams_web_utils::{blocking_server_fn, not_found};
474    use ftml_viewer_components::components::{
475        TOCElem,
476        omdoc::{
477            OMDoc,
478            narration::{OMDocDocument, OMDocDocumentElement},
479        },
480    };
481    use leptos::prelude::*;
482
483    pub async fn document(
484        uri: DocumentURI,
485    ) -> Result<(DocumentURI, Vec<CSS>, String), ServerFnError<String>> {
486        let Some((css, doc)) = backend!(get_html_body!(&uri, true)) else {
487            not_found!("Document {uri} not found");
488        };
489        let html = format!(
490            "<div{}</div>",
491            doc.strip_prefix("<body")
492                .and_then(|s| s.strip_suffix("</body>"))
493                .unwrap_or("")
494        );
495        Ok((uri, insert_base_url(css), html))
496    }
497
498    pub async fn toc(uri: DocumentURI) -> Result<(Vec<CSS>, Vec<TOCElem>), ServerFnError<String>> {
499        let Some(doc) = backend!(get_document!(&uri)) else {
500            not_found!("Document {uri} not found");
501        };
502        Ok(crate::toc::from_document(&doc).await)
503    }
504
505    pub async fn fragment(
506        uri: URI,
507        _: Option<URI>,
508    ) -> Result<(URI, Vec<CSS>, String), ServerFnError<String>> {
509        match &uri {
510            URI::Narrative(NarrativeURI::Document(duri)) => {
511                let Some((css, html)) = backend!(get_html_body!(duri, false)) else {
512                    not_found!("Document {duri} not found");
513                };
514                Ok((uri, insert_base_url(filter_paras(css)), html))
515            }
516            URI::Narrative(NarrativeURI::Element(euri)) => {
517                let Some(e) = backend!(get_document_element!(euri)) else {
518                    not_found!("Document Element {euri} not found");
519                };
520                match e.as_ref() {
521                    DocumentElement::Paragraph(LogicalParagraph { range, .. })
522                    | DocumentElement::Problem(Problem { range, .. }) => {
523                        let Some((css, html)) =
524                            backend!(get_html_fragment!(euri.document(), *range))
525                        else {
526                            not_found!("Paragraph HTML fragment {euri} not found");
527                        };
528                        Ok((uri, insert_base_url(filter_paras(css)), html))
529                    }
530                    DocumentElement::Section(flams_ontology::narration::sections::Section {
531                        range,
532                        ..
533                    }) => {
534                        let Some((css, html)) =
535                            backend!(get_html_fragment!(euri.document(), *range))
536                        else {
537                            not_found!("Section HTML fragment {euri} not found");
538                        };
539                        Ok((uri, insert_base_url(filter_paras(css)), html))
540                    }
541                    _ => return Err("not a paragraph".to_string().into()),
542                }
543            }
544            URI::Content(ContentURI::Symbol(suri)) => get_definitions(suri.clone())
545                .await
546                .ok_or_else(|| not_found!(!"No definition for {suri} not found"))
547                .map(|(css, b)| (uri, insert_base_url(filter_paras(css)), b)),
548            URI::Base(_) => return Err("TODO: base".to_string().into()),
549            URI::Archive(_) => return Err("TODO: archive".to_string().into()),
550            URI::Path(_) => return Err("TODO: path".to_string().into()),
551            URI::Content(ContentURI::Module(_)) => return Err("TODO: module".to_string().into()),
552        }
553    }
554
555    pub async fn los(
556        uri: SymbolURI,
557        problems: bool,
558    ) -> Result<Vec<(DocumentElementURI, LOKind)>, ServerFnError<String>> {
559        blocking_server_fn(move || {
560            Ok(GlobalBackend::get()
561                .triple_store()
562                .los(&uri, problems)
563                .map(|i| i.collect())
564                .unwrap_or_default())
565        })
566        .await
567    }
568
569    pub async fn notations(
570        uri: URI,
571    ) -> Result<Vec<(DocumentElementURI, Notation)>, ServerFnError<String>> {
572        let v = match uri {
573            URI::Content(ContentURI::Symbol(uri)) => {
574                blocking_server_fn(move || {
575                    Ok(backend!(get_notations SYNC!(&uri)).unwrap_or_default())
576                })
577                .await
578            }
579            URI::Narrative(NarrativeURI::Element(uri)) => {
580                blocking_server_fn(move || {
581                    Ok(backend!(get_var_notations SYNC!(&uri)).unwrap_or_default())
582                })
583                .await
584            }
585            _ => return Err(format!("Not a symbol or variable URI: {uri}").into()),
586        }?;
587        Ok(v.0)
588    }
589
590    pub async fn title(uri: URI) -> Result<(Vec<CSS>, String), ServerFnError<String>> {
591        match uri {
592            uri @ (URI::Base(_) | URI::Archive(_) | URI::Path(_) | URI::Content(_)) => {
593                Err(format!("Not a URI of an element that can have a title: {uri}").into())
594            }
595            URI::Narrative(NarrativeURI::Document(uri)) => {
596                let Some(doc) = backend!(get_document!(&uri)) else {
597                    not_found!("Document {uri} not found");
598                };
599                Ok((Vec::new(), doc.title().unwrap_or_default().to_string()))
600            }
601            URI::Narrative(NarrativeURI::Element(uri)) => {
602                let Some(e): Option<NarrativeReference<DocumentElement<Checked>>> =
603                    backend!(get_document_element!(&uri))
604                else {
605                    not_found!("Document Element {uri} not found");
606                };
607                match e.as_ref() {
608                    DocumentElement::Section(Section { title, .. })
609                    | DocumentElement::Paragraph(LogicalParagraph { title, .. })
610                    | DocumentElement::Problem(Problem { title, .. }) => {
611                        let Some(title) = title else {
612                            return Ok((Vec::new(), String::new()));
613                        };
614                        backend!(get_html_fragment!(uri.document(), *title))
615                            .ok_or_else(|| format!("Error retrieving title").into())
616                    }
617                    _ => Err(format!("Narrative element has no title").into()),
618                }
619            }
620        }
621    }
622
623    pub async fn omdoc(uri: URI) -> Result<(Vec<CSS>, OMDoc), ServerFnError<String>> {
624        let mut css = VecSet::default();
625        match uri {
626            uri @ (URI::Base(_) | URI::Archive(_) | URI::Path(_)) => {
627                Ok((insert_base_url(css.0), OMDoc::Other(uri.to_string())))
628            }
629            URI::Narrative(NarrativeURI::Document(uri)) => {
630                let Some(doc) = backend!(get_document!(&uri)) else {
631                    not_found!("Document {uri} not found");
632                };
633                let (css, r) = backend!(backend => {
634                  let r = OMDocDocument::from_document(&doc, backend,&mut css);
635                  (css,r)
636                }{
637                  blocking_server_fn(move || {
638                    let r = OMDocDocument::from_document(&doc, backend,&mut css);
639                    Ok((css,r))
640                  }).await?
641                });
642                Ok((insert_base_url(css.0), r.into()))
643            }
644            URI::Narrative(NarrativeURI::Element(uri)) => {
645                let Some(e): Option<NarrativeReference<DocumentElement<Checked>>> =
646                    backend!(get_document_element!(&uri))
647                else {
648                    not_found!("Document Element {uri} not found");
649                };
650                let (css, r) = backend!(backend => {
651                  let r = OMDocDocumentElement::from_element(e.as_ref(),backend, &mut css);
652                  (css,r)
653                }{
654                  blocking_server_fn(move || {
655                    let r = OMDocDocumentElement::from_element(e.as_ref(),backend,&mut css);
656                    Ok((css,r))
657                  }).await?
658                });
659                let Some(r) = r else {
660                    not_found!("Document Element {uri} not found");
661                };
662                Ok((insert_base_url(css.0), r.into()))
663            }
664            URI::Content(ContentURI::Module(uri)) => {
665                let Some(m) = backend!(get_module!(&uri)) else {
666                    not_found!("Module {uri} not found");
667                };
668                let r = backend!(backend => {
669                  OMDoc::from_module_like(&m, backend)
670                }{
671                  blocking_server_fn(move || {
672                    Ok(OMDoc::from_module_like(&m, backend))
673                  }).await?
674                });
675                Ok((Vec::new(), r))
676            }
677            URI::Content(ContentURI::Symbol(uri)) => {
678                let Some(s): Option<ContentReference<Declaration>> =
679                    backend!(get_declaration!(&uri))
680                else {
681                    not_found!("Declaration {uri} not found");
682                };
683                return Err(format!("TODO: {uri}").into());
684            }
685        }
686    }
687
688    pub async fn get_quiz(uri: DocumentURI) -> Result<Quiz, ServerFnError<String>> {
689        use flams_system::backend::docfile::QuizExtension;
690        let Some(doc) = backend!(get_document!(&uri)) else {
691            not_found!("Document {uri} not found");
692        };
693        blocking_server_fn(move || {
694            let be = if flams_system::settings::Settings::get().lsp {
695                let Some(state) = flams_lsp::STDIOLSPServer::global_state() else {
696                    return Err("no lsp server".to_string());
697                };
698                doc.as_quiz(state.backend())
699            } else {
700                doc.as_quiz(flams_system::backend::GlobalBackend::get())
701            };
702            let mut be = be.map_err(|e| format!("{e:#}"))?;
703            be.css = insert_base_url(std::mem::take(&mut be.css));
704            Ok(be)
705        })
706        .await
707    }
708
709    pub async fn slides(uri: URI) -> Result<(Vec<CSS>, Vec<SlideElement>), ServerFnError<String>> {
710        fn from_children(
711            top: &DocumentURI,
712            children: &[DocumentElement<Checked>],
713            css: &mut VecSet<CSS>,
714            backend: &impl Backend,
715        ) -> Result<Vec<SlideElement>, String> {
716            let mut stack =
717                smallvec::SmallVec::<(_, _, _, Option<DocumentElementURI>), 2>::default();
718            let mut ret = Vec::new();
719            let mut curr = children.iter();
720
721            loop {
722                let Some(next) = curr.next() else {
723                    if let Some((a, b, c, u)) = stack.pop() {
724                        curr = a;
725                        if let Some(mut b) = b {
726                            std::mem::swap(&mut ret, &mut b);
727                            ret.push(SlideElement::Section {
728                                title: c,
729                                children: b,
730                                uri: unwrap!(u),
731                            });
732                        }
733                        continue;
734                    }
735                    break;
736                };
737                match next {
738                    DocumentElement::Slide { range, uri, .. } => {
739                        let Some((c, html)) = backend.get_html_fragment(top, *range) else {
740                            return Err(format!("Missing fragment for slide {uri}"));
741                        };
742                        for c in c {
743                            css.insert(c);
744                        }
745                        ret.push(SlideElement::Slide {
746                            html,
747                            uri: uri.clone(),
748                        });
749                    }
750                    DocumentElement::Paragraph(p) => {
751                        let Some((c, html)) = backend.get_html_fragment(top, p.range) else {
752                            return Err(format!("Missing fragment for paragraph {}", p.uri));
753                        };
754                        for c in c {
755                            css.insert(c);
756                        }
757                        ret.push(SlideElement::Paragraph {
758                            html,
759                            uri: p.uri.clone(),
760                        });
761                    }
762                    DocumentElement::DocumentReference { target, .. } => {
763                        ret.push(SlideElement::Inputref {
764                            uri: target.id().into_owned(),
765                        })
766                    }
767                    DocumentElement::Section(s) => {
768                        let title = if let Some(t) = s.title {
769                            let Some((c, html)) = backend.get_html_fragment(top, t) else {
770                                return Err(format!("Missing title for section {}", s.uri));
771                            };
772                            for c in c {
773                                css.insert(c);
774                            }
775                            Some(html)
776                        } else {
777                            None
778                        };
779                        stack.push((
780                            std::mem::replace(&mut curr, s.children().iter()),
781                            Some(std::mem::replace(&mut ret, Vec::new())),
782                            title,
783                            Some(s.uri.clone()),
784                        ));
785                    }
786                    o => {
787                        let chs = o.children();
788                        if !chs.is_empty() {
789                            stack.push((
790                                std::mem::replace(&mut curr, chs.iter()),
791                                None,
792                                None,
793                                None,
794                            ));
795                        }
796                    }
797                }
798            }
799            Ok(ret)
800        }
801
802        let Some(doe) = (match &uri {
803            URI::Narrative(NarrativeURI::Document(uri)) => {
804                backend!(get_document!(uri)).map(either::Either::Left)
805            }
806            URI::Narrative(NarrativeURI::Element(uri)) => {
807                backend!(get_document_element!(uri)).map(either::Either::Right)
808            }
809            _ => return Err("Not a narrative URI".to_string().into()),
810        }) else {
811            not_found!("Element {uri} not found");
812        };
813        blocking_server_fn(move || {
814            let (chs, top) = match &doe {
815                either::Either::Left(d) => (d.children(), d.uri()),
816                either::Either::Right(e) => {
817                    let e: &NarrativeReference<DocumentElement<Checked>> = e;
818                    (e.as_ref().children(), e.top().uri())
819                }
820            };
821            let mut css = VecSet::default();
822
823            let r = if flams_system::settings::Settings::get().lsp {
824                let Some(state) = flams_lsp::STDIOLSPServer::global_state() else {
825                    return Err("no lsp server".to_string());
826                };
827                from_children(top, chs, &mut css, state.backend())
828            } else {
829                from_children(
830                    top,
831                    chs,
832                    &mut css,
833                    flams_system::backend::GlobalBackend::get(),
834                )
835            }?;
836            Ok((insert_base_url(css.0), r))
837        })
838        .await
839    }
840
841    pub fn get_solution(uri: &DocumentElementURI) -> Result<Solutions, String> {
842        use flams_system::backend::Backend;
843        match backend!(get_document_element(&uri)) {
844            Some(rf) => {
845                let e: &Problem<Checked> = rf.as_ref();
846                let sol = match backend!(get_reference(&e.solutions)) {
847                    Ok(sol) => sol,
848                    Err(e) => return Err(format!("solutions not found: {e}")),
849                };
850                Ok(sol)
851            }
852            _ => not_found!("Problem {uri} not found"),
853        }
854    }
855
856    async fn get_definitions(uri: SymbolURI) -> Option<(Vec<CSS>, String)> {
857        let b = GlobalBackend::get();
858        let query = sparql::Select {
859            subject: sparql::Var('x'),
860            pred: ulo2::DEFINES.into_owned(),
861            object: uri.to_iri(),
862        }
863        .into();
864        //println!("Getting definitions using query: {}",query);
865        let iter = b
866            .triple_store()
867            .query(query)
868            .map(|r| r.into_uris())
869            .unwrap_or_default()
870            .collect::<Vec<_>>();
871        for uri in iter {
872            if let Some(def) = b.get_document_element_async(&uri).await {
873                let LogicalParagraph { range, .. } = def.as_ref();
874                if let Some((css, r)) = b.get_html_fragment_async(uri.document(), *range).await {
875                    return Some((insert_base_url(filter_paras(css)), r));
876                }
877            }
878        }
879        None
880    }
881
882    pub(crate) fn filter_paras(mut v: Vec<CSS>) -> Vec<CSS> {
883        const CSSS: [&str; 11] = [
884            "ftml-part",
885            "ftml-chapter",
886            "ftml-section",
887            "ftml-subsection",
888            "ftml-subsubsection",
889            "ftml-paragraph",
890            "ftml-definition",
891            "ftml-assertion",
892            "ftml-example",
893            "ftml-problem",
894            "ftml-subproblem",
895        ];
896        v.retain(|c| match c {
897            CSS::Class { name, .. } => !CSSS.iter().any(|s| name.starts_with(s)),
898            _ => true,
899        });
900        v
901    }
902}
903
904#[server(prefix = "/content/legacy", endpoint = "uris")]
905pub async fn uris(uris: Vec<String>) -> Result<Vec<Option<URI>>, ServerFnError<String>> {
906    use flams_ontology::uris::{
907        ArchiveURI, ArchiveURITrait, BaseURI, ModuleURI, URIOrRefTrait, URIRefTrait,
908    };
909    use flams_system::backend::{Backend, GlobalBackend};
910
911    const MATHHUB: &str = "http://mathhub.info";
912    const META: &str = "http://mathhub.info/sTeX/meta";
913    const URTHEORIES: &str = "http://cds.omdoc.org/urtheories";
914
915    lazy_static::lazy_static! {
916      static ref MATHHUB_INFO: BaseURI = BaseURI::new_unchecked("http://mathhub.info/:sTeX");
917      static ref META_URI: ArchiveURI = flams_ontology::metatheory::URI.archive_uri().owned();//ArchiveURI::new(MATHHUB_INFO.clone(),ArchiveId::new("sTeX/meta-inf"));
918      static ref UR_URI: ArchiveURI = ArchiveURI::new(BaseURI::new_unchecked("http://cds.omdoc.org"),ArchiveId::new("MMT/urtheories"));
919      static ref MY_ARCHIVE: ArchiveURI = ArchiveURI::new(BaseURI::new_unchecked("http://mathhub.info"),ArchiveId::new("my/archive"));
920      static ref INJECTING: ArchiveURI = ArchiveURI::new(MATHHUB_INFO.clone(),ArchiveId::new("Papers/22-CICM-Injecting-Formal-Mathematics"));
921      static ref TUG: ArchiveURI = ArchiveURI::new(MATHHUB_INFO.clone(),ArchiveId::new("Papers/22-TUG-sTeX"));
922    }
923
924    fn split(p: &str) -> Option<(ArchiveURI, usize)> {
925        if p.starts_with(META) {
926            return Some((META_URI.clone(), 29));
927        }
928        if p == URTHEORIES {
929            return Some((UR_URI.clone(), 31));
930        }
931        if p == "http://mathhub.info/my/archive" {
932            return Some((MY_ARCHIVE.clone(), 30));
933        }
934        if p == "http://kwarc.info/Papers/stex-mmt/paper" {
935            return Some((INJECTING.clone(), 34));
936        }
937        if p == "http://kwarc.info/Papers/tug/paper" {
938            return Some((TUG.clone(), 34));
939        }
940        if p.starts_with("file://") {
941            return Some((ArchiveURI::no_archive(), 7));
942        }
943        if let Some(mut p) = p.strip_prefix(MATHHUB) {
944            let mut i = MATHHUB.len();
945            if let Some(s) = p.strip_prefix('/') {
946                p = s;
947                i += 1;
948            }
949            return split_old(p, i);
950        }
951        GlobalBackend::get().with_archives(|mut tree| {
952            tree.find_map(|a| {
953                let base = a.uri();
954                let base = base.base().as_ref();
955                if p.starts_with(base) {
956                    let l = base.len();
957                    let np = &p[l..];
958                    let id = a.id().as_ref();
959                    if np.starts_with(id) {
960                        Some((a.uri().owned(), l + id.len()))
961                    } else {
962                        None
963                    }
964                } else {
965                    None
966                }
967            })
968        })
969    }
970
971    fn split_old(p: &str, len: usize) -> Option<(ArchiveURI, usize)> {
972        GlobalBackend::get().with_archives(|mut tree| {
973            tree.find_map(|a| {
974                if p.starts_with(a.id().as_ref()) {
975                    let mut l = a.id().as_ref().len();
976                    let np = &p[l..];
977                    if np.starts_with('/') {
978                        l += 1;
979                    }
980                    Some((a.uri().owned(), len + l))
981                } else {
982                    None
983                }
984            })
985        })
986    }
987
988    fn get_doc_uri(pathstr: &str) -> Option<DocumentURI> {
989        let pathstr = pathstr.strip_suffix(".tex").unwrap_or(pathstr);
990        let (p, mut m) = pathstr.rsplit_once('/')?;
991        let (a, l) = split(p)?;
992        let mut path = if l < p.len() { &p[l..] } else { "" };
993        if path.starts_with('/') {
994            path = &path[1..];
995        }
996        let lang = Language::from_rel_path(m);
997        m = m.strip_suffix(&format!(".{lang}")).unwrap_or(m);
998        ((a % path).ok()? & (m, lang)).ok()
999    }
1000
1001    fn get_mod_uri(pathstr: &str) -> Option<ModuleURI> {
1002        let (mut p, mut m) = pathstr.rsplit_once('?')?;
1003        m = m.strip_suffix("-module").unwrap_or(m);
1004        if p.bytes().last() == Some(b'/') {
1005            p = &p[..p.len() - 1];
1006        }
1007        let (a, l) = split(p)?;
1008        let mut path = if l < p.len() { &p[l..] } else { "" };
1009        if path.starts_with('/') {
1010            path = &path[1..];
1011        }
1012        ((a % path).ok()? | m).ok()
1013    }
1014
1015    fn get_sym_uri(pathstr: &str) -> Option<SymbolURI> {
1016        let (m, s) = match pathstr.split_once('[') {
1017            Some((m, s)) => {
1018                let (m, _) = m.rsplit_once('?')?;
1019                let (a, b) = s.rsplit_once(']')?;
1020                let am = get_mod_uri(a)?;
1021                let name = (am.name().clone() / b).ok()?;
1022                let module = get_mod_uri(m)?;
1023                return Some(module | name);
1024            }
1025            None => pathstr.rsplit_once('?')?,
1026        };
1027        let m = get_mod_uri(m)?;
1028        (m | s).ok()
1029    }
1030
1031    Ok(uris
1032        .into_iter()
1033        .map(|s| {
1034            get_sym_uri(&s).map_or_else(
1035                || {
1036                    get_mod_uri(&s).map_or_else(
1037                        || get_doc_uri(&s).map(|d| URI::Narrative(d.into())),
1038                        |s| Some(URI::Content(s.into())),
1039                    )
1040                },
1041                |s| Some(URI::Content(s.into())),
1042            )
1043        })
1044        .collect())
1045}