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