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 }
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 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 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 }
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 }
315}
316ftml_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 }
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 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 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();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}