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