flams_router_content/
server_fns.rs

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