Skip to main content

flams_router_content/
server_fns.rs

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