1use flams_ontology::{
2 languages::Language,
3 narration::LOKind,
4 uris::{ArchiveId, DocumentElementURI, DocumentURI, SymbolURI, URI},
5};
6use flams_utils::CSS;
7use leptos::prelude::*;
8#[cfg(feature = "csr")]
9use std::borrow::Cow;
10
11#[cfg(feature = "omdoc")]
12use crate::components::omdoc::OMDoc;
13
14use crate::components::TOCElem;
15
16pub const DEFAULT_SERVER_URL: &str = "https://mathhub.info";
17
18macro_rules! get {
19 ($fn:ident($($arg:expr),*) = $res:pat => { $($code:tt)*}) => {{
20 use ::leptos::suspense::Suspense;
21 let r = ::leptos::prelude::Resource::new(|| (),move |()| $crate::remote::server_config.$fn($($arg),*));
22 ::leptos::prelude::view!{
23 <Suspense fallback=|| view!(<flams_web_utils::components::Spinner/>)>{move ||
24 if let Some(Ok($res)) = r.get() {
25 Some({$($code)*})
26 } else {None}
27 }</Suspense>
28 }
29 }}
30}
31
32pub(crate) use get;
33
34#[cfg(feature = "csr")]
35pub fn set_server_url(s: String) {
36 *server_config.server_url.lock() = s;
37}
38
39#[cfg(feature = "csr")]
40#[wasm_bindgen::prelude::wasm_bindgen]
41pub fn get_server_url() -> String {
43 server_config.server_url.lock().clone()
44}
45
46#[cfg(not(feature = "csr"))]
47pub fn get_server_url() -> String {
48 String::new()
49}
50
51#[cfg(any(feature = "hydrate", feature = "ssr"))]
52macro_rules! server_fun{
53 ($($ty:ty),* => $ret:ty) => {
54 fn($($ty),*) -> server_fun_ret!($ret)
55 };
56 (@URI$(,$ty:ty)* => $ret:ty) => {
57 server_fun!(Option<URI>,Option<String>,Option<ArchiveId>,Option<String>,Option<Language>,Option<String>,Option<String>,Option<String>,Option<String> $(,$ty)* => $ret)
58 };
59 (@DOCURI$(,$ty:ty)* => $ret:ty) => {
60 server_fun!(Option<DocumentURI>,Option<String>,Option<ArchiveId>,Option<String>,Option<Language>,Option<String> $(,$ty)* => $ret)
61 };
62 (@SYMURI$(,$ty:ty)* => $ret:ty) => {
63 server_fun!(Option<SymbolURI>,Option<ArchiveId>,Option<String>,Option<String>,Option<String> $(,$ty)* => $ret)
64 };
65}
66
67#[cfg(any(feature = "hydrate", feature = "ssr"))]
68macro_rules! server_fun_ret{
69 ($ret:ty) => {
70 std::pin::Pin<Box<dyn std::future::Future<Output=Result<$ret,leptos::prelude::ServerFnError<String>>> + Send>>
71 }
72}
73
74#[cfg(all(feature = "csr", not(any(feature = "hydrate", feature = "ssr"))))]
75#[macro_export]
76macro_rules! server_fun_ret {
77 ($ret:ty) => {
78 $ret
79 };
80}
81
82trait ServerFunArgs {
83 #[cfg(any(feature = "hydrate", feature = "ssr"))]
84 type DeTupledFun<R>;
85 type First: std::hash::Hash + std::fmt::Display + PartialEq + Eq + Clone;
86 type Extra;
87 #[cfg(feature = "csr")]
88 fn as_params(e: &Self::Extra) -> Cow<'static, str>;
89 #[cfg(any(feature = "hydrate", feature = "ssr"))]
90 fn call<R>(
91 uri: Self::First,
92 extra: Self::Extra,
93 f: &Self::DeTupledFun<R>,
94 ) -> server_fun_ret!(R);
95}
96
97#[cfg(all(feature = "csr", not(any(feature = "hydrate", feature = "ssr"))))]
98type URIArgs = URI;
99#[cfg(any(feature = "hydrate", feature = "ssr"))]
100type URIArgs = (
101 Option<URI>,
102 Option<String>,
103 Option<ArchiveId>,
104 Option<String>,
105 Option<Language>,
106 Option<String>,
107 Option<String>,
108 Option<String>,
109 Option<String>,
110);
111
112#[cfg(all(feature = "csr", not(any(feature = "hydrate", feature = "ssr"))))]
113type URIArgsWithContext = (URI, Option<URI>);
114#[cfg(any(feature = "hydrate", feature = "ssr"))]
115type URIArgsWithContext = (
116 Option<URI>,
117 Option<String>,
118 Option<ArchiveId>,
119 Option<String>,
120 Option<Language>,
121 Option<String>,
122 Option<String>,
123 Option<String>,
124 Option<String>,
125 Option<URI>,
126);
127
128#[allow(clippy::use_self)]
129impl ServerFunArgs for URIArgs {
130 #[cfg(any(feature = "hydrate", feature = "ssr"))]
131 type DeTupledFun<R> = server_fun!(@URI => R);
132 type First = URI;
133 type Extra = ();
134 #[cfg(feature = "csr")]
135 fn as_params((): &Self::Extra) -> Cow<'static, str> {
136 "".into()
137 }
138 #[cfg(any(feature = "hydrate", feature = "ssr"))]
139 #[inline]
140 fn call<R>(uri: URI, _: (), f: &Self::DeTupledFun<R>) -> server_fun_ret!(R) {
141 f(Some(uri), None, None, None, None, None, None, None, None)
142 }
143}
144
145#[allow(clippy::use_self)]
146impl ServerFunArgs for URIArgsWithContext {
147 #[cfg(any(feature = "hydrate", feature = "ssr"))]
148 type DeTupledFun<R> = server_fun!(@URI,Option<URI> => R);
149 type First = URI;
150 type Extra = Option<URI>;
151 #[cfg(feature = "csr")]
152 fn as_params(_: &Self::Extra) -> Cow<'static, str> {
153 "".into()
154 }
155 #[cfg(any(feature = "hydrate", feature = "ssr"))]
156 #[inline]
157 fn call<R>(uri: URI, c: Option<URI>, f: &Self::DeTupledFun<R>) -> server_fun_ret!(R) {
158 f(Some(uri), None, None, None, None, None, None, None, None, c)
159 }
160}
161
162#[cfg(all(feature = "csr", not(any(feature = "hydrate", feature = "ssr"))))]
163type DocURIArgs = DocumentURI;
164#[cfg(any(feature = "hydrate", feature = "ssr"))]
165type DocURIArgs = (
166 Option<DocumentURI>,
167 Option<String>,
168 Option<ArchiveId>,
169 Option<String>,
170 Option<Language>,
171 Option<String>,
172);
173#[allow(clippy::use_self)]
174impl ServerFunArgs for DocURIArgs {
175 #[cfg(any(feature = "hydrate", feature = "ssr"))]
176 type DeTupledFun<R> = server_fun!(@DOCURI => R);
177 type First = DocumentURI;
178 type Extra = ();
179 #[cfg(feature = "csr")]
180 fn as_params((): &Self::Extra) -> Cow<'static, str> {
181 "".into()
182 }
183 #[cfg(any(feature = "hydrate", feature = "ssr"))]
184 #[inline]
185 fn call<R>(uri: DocumentURI, _: (), f: &Self::DeTupledFun<R>) -> server_fun_ret!(R) {
186 f(Some(uri), None, None, None, None, None)
187 }
188}
189
190#[cfg(all(feature = "csr", not(any(feature = "hydrate", feature = "ssr"))))]
191type SymURIArgs = SymbolURI;
192#[cfg(any(feature = "hydrate", feature = "ssr"))]
193type SymURIArgs = (
194 Option<SymbolURI>,
195 Option<ArchiveId>,
196 Option<String>,
197 Option<String>,
198 Option<String>,
199);
200#[allow(clippy::use_self)]
201impl ServerFunArgs for SymURIArgs {
202 #[cfg(any(feature = "hydrate", feature = "ssr"))]
203 type DeTupledFun<R> = server_fun!(@SYMURI => R);
204 type First = SymbolURI;
205 type Extra = ();
206 #[cfg(feature = "csr")]
207 fn as_params((): &Self::Extra) -> Cow<'static, str> {
208 "".into()
209 }
210 #[cfg(any(feature = "hydrate", feature = "ssr"))]
211 #[inline]
212 fn call<R>(uri: SymbolURI, _: (), f: &Self::DeTupledFun<R>) -> server_fun_ret!(R) {
213 f(Some(uri), None, None, None, None)
214 }
215}
216
217#[cfg(all(feature = "csr", not(any(feature = "hydrate", feature = "ssr"))))]
218type LOArgs = (SymbolURI, bool);
219#[cfg(any(feature = "hydrate", feature = "ssr"))]
220type LOArgs = (
221 Option<SymbolURI>,
222 Option<ArchiveId>,
223 Option<String>,
224 Option<String>,
225 Option<String>,
226 bool,
227);
228impl ServerFunArgs for LOArgs {
229 #[cfg(any(feature = "hydrate", feature = "ssr"))]
230 type DeTupledFun<R> = server_fun!(@SYMURI,bool => R);
231 type First = SymbolURI;
232 type Extra = bool;
233 #[cfg(feature = "csr")]
234 fn as_params(b: &Self::Extra) -> Cow<'static, str> {
235 format!("&problems={b}").into()
236 }
237 #[cfg(any(feature = "hydrate", feature = "ssr"))]
238 #[inline]
239 fn call<R>(uri: SymbolURI, b: bool, f: &Self::DeTupledFun<R>) -> server_fun_ret!(R) {
240 f(Some(uri), None, None, None, None, b)
241 }
242}
243
244#[allow(clippy::struct_field_names)]
245struct Cache<T: ServerFunArgs, V: Clone + for<'de> serde::Deserialize<'de>> {
246 #[cfg(any(feature = "hydrate", feature = "csr"))]
247 cache: flams_utils::parking_lot::Mutex<flams_utils::prelude::HMap<T::First, V>>,
248 #[cfg(feature = "csr")]
249 url_frag: &'static str,
250 #[cfg(any(feature = "hydrate", feature = "ssr"))]
251 getter: std::sync::OnceLock<T::DeTupledFun<V>>,
252 #[cfg(feature = "ssr")]
253 phantom: std::marker::PhantomData<(T::First, V)>,
254 #[cfg(all(feature = "csr", not(feature = "ssr")))]
255 phantom: std::marker::PhantomData<T>,
256}
257
258impl<T: ServerFunArgs, V: Clone + std::fmt::Debug + for<'de> serde::Deserialize<'de>> Cache<T, V> {
259 #[allow(unused_variables)]
260 fn new(frag: &'static str) -> Self {
261 Self {
262 #[cfg(any(feature = "hydrate", feature = "csr"))]
263 cache: flams_utils::parking_lot::Mutex::new(flams_utils::prelude::HMap::default()),
264 #[cfg(feature = "csr")]
265 url_frag: frag,
266 #[cfg(any(feature = "hydrate", feature = "ssr"))]
267 getter: std::sync::OnceLock::new(),
268 #[cfg(feature = "ssr")]
269 phantom: std::marker::PhantomData,
270 #[cfg(all(feature = "csr", not(feature = "ssr")))]
271 phantom: std::marker::PhantomData,
272 }
273 }
274
275 #[cfg(feature = "csr")]
276 #[inline]
277 #[allow(clippy::needless_pass_by_value)]
278 fn url(&self, uri: &str, extra: Cow<'static, str>) -> String {
279 format!(
280 "{}/content/{}?uri={}{extra}",
281 server_config.server_url.lock(),
282 self.url_frag,
283 urlencoding::encode(uri)
284 )
285 }
286
287 #[allow(unreachable_code)]
290 #[allow(clippy::future_not_send)]
291 pub async fn call(&self, key: T::First, extra: T::Extra) -> Result<V, String> {
292 #[cfg(any(feature = "hydrate", feature = "csr"))]
293 {
294 {
295 let cache = self.cache.lock();
296 if let Some(v) = std::collections::HashMap::get(&*cache, &key) {
297 return Ok(v.clone());
298 }
299 }
300 }
301
302 #[cfg(feature = "csr")]
303 {
304 let ret: Result<V, _> =
305 ServerConfig::remote(self.url(&key.to_string(), T::as_params(&extra))).await;
306 if let Ok(v) = &ret {
307 let mut cache = self.cache.lock();
308 cache.insert(key.clone(), v.clone());
309 }
310 return ret;
311 }
312
313 #[cfg(any(feature = "hydrate", feature = "ssr"))]
314 {
315 let Some(f) = self.getter.get() else {
316 panic!("Uninitialized ftml-viewer!!")
317 };
318 return match T::call(key.clone(), extra, f).await {
319 Ok(r) => {
320 #[cfg(feature = "hydrate")]
321 {
322 std::collections::HashMap::insert(&mut self.cache.lock(), key, r.clone());
323 }
324 Ok(r)
325 }
326 Err(e) => Err(e.to_string()),
327 };
328 }
331 }
332}
333
334pub struct ServerConfig {
335 #[cfg(feature = "csr")]
336 pub server_url: flams_utils::parking_lot::Mutex<String>,
337 get_full_doc: Cache<DocURIArgs, (DocumentURI, Vec<CSS>, String)>,
338 get_fragment: Cache<URIArgsWithContext, (URI, Vec<CSS>, String)>,
339 #[cfg(feature = "omdoc")]
340 get_omdoc: Cache<URIArgs, (Vec<CSS>, OMDoc)>,
341 get_toc: Cache<DocURIArgs, (Vec<CSS>, Vec<TOCElem>)>,
342 get_los: Cache<LOArgs, Vec<(DocumentElementURI, LOKind)>>,
343 #[cfg(feature = "omdoc")]
344 get_notations: Cache<
345 URIArgs,
346 Vec<(
347 DocumentElementURI,
348 flams_ontology::narration::notations::Notation,
349 )>,
350 >,
351 get_solution: Cache<URIArgs, String>,
352}
353
354impl ServerConfig {
355 pub fn top_doc_url(&self, uri: &DocumentURI) -> String {
356 #[cfg(feature = "csr")]
357 {
358 format!(
359 "{}/?uri={}",
360 self.server_url.lock(),
361 urlencoding::encode(&uri.to_string())
362 )
363 }
364 #[cfg(not(feature = "csr"))]
365 {
366 format!("/?uri={}", urlencoding::encode(&uri.to_string()))
367 }
368 }
369
370 #[inline]
373 pub async fn inputref(&self, doc: DocumentURI) -> Result<(URI, Vec<CSS>, String), String> {
374 self.get_fragment
375 .call(URI::Narrative(doc.into()), None)
376 .await
377 }
378
379 #[inline]
382 pub async fn paragraph(
383 &self,
384 doc: DocumentElementURI,
385 ) -> Result<(URI, Vec<CSS>, String), String> {
386 self.get_fragment
387 .call(URI::Narrative(doc.into()), None)
388 .await
389 }
390
391 #[inline]
394 pub async fn definition(&self, uri: SymbolURI) -> Result<(Vec<CSS>, String), String> {
395 self.get_fragment
396 .call(URI::Content(uri.into()), None)
397 .await
398 .map(|(_, a, b)| (a, b))
399 }
400
401 #[inline]
404 pub async fn full_doc(
405 &self,
406 uri: DocumentURI,
407 ) -> Result<(DocumentURI, Vec<CSS>, String), String> {
408 self.get_full_doc.call(uri, ()).await
409 }
410
411 #[inline]
414 pub async fn get_toc(&self, uri: DocumentURI) -> Result<(Vec<CSS>, Vec<TOCElem>), String> {
415 self.get_toc.call(uri, ()).await
416 }
417
418 #[inline]
421 pub async fn get_los(
422 &self,
423 uri: SymbolURI,
424 problems: bool,
425 ) -> Result<Vec<(DocumentElementURI, LOKind)>, String> {
426 self.get_los.call(uri, problems).await
427 }
428
429 #[cfg(feature = "omdoc")]
432 #[inline]
433 pub async fn omdoc(&self, uri: flams_ontology::uris::URI) -> Result<(Vec<CSS>, OMDoc), String> {
434 self.get_omdoc.call(uri, ()).await
435 }
436
437 #[inline]
440 pub async fn solution(
441 &self,
442 uri: flams_ontology::uris::DocumentElementURI,
443 ) -> Result<flams_ontology::narration::problems::Solutions, String> {
444 use flams_utils::Hexable;
445 let r = self
446 .get_solution
447 .call(URI::Narrative(uri.into()), ())
448 .await?;
449 flams_ontology::narration::problems::Solutions::from_hex(&r).map_err(|e| e.to_string())
450 }
451
452 #[cfg(feature = "omdoc")]
455 #[inline]
456 pub async fn notations(
457 &self,
458 uri: flams_ontology::uris::URI,
459 ) -> Result<
460 Vec<(
461 DocumentElementURI,
462 flams_ontology::narration::notations::Notation,
463 )>,
464 String,
465 > {
466 let ret = self.get_notations.call(uri, ()).await;
467 ret
468 }
469
470 #[cfg(feature = "omdoc")]
471 pub fn get_notation(
472 &self,
473 uri: &flams_ontology::uris::DocumentElementURI,
474 ) -> Option<flams_ontology::narration::notations::Notation> {
475 #[cfg(any(feature = "csr", feature = "hydrate"))]
476 {
477 let lock = self.get_notations.cache.lock();
478 lock.values().flat_map(|v| v.iter()).find_map(|(u, n)| {
479 if u == uri {
480 Some(n.clone())
481 } else {
482 None
483 }
484 })
485 }
487 #[cfg(not(any(feature = "csr", feature = "hydrate")))]
488 {
489 unreachable!()
490 }
491 }
492
493 #[cfg(feature = "omdoc")]
494 pub async fn present(&self, t: flams_ontology::content::terms::Term) -> Result<String, String> {
495 use flams_ontology::content::terms::Term;
496 use flams_ontology::narration::notations::{Notation, PresentationError, Presenter};
497 use flams_ontology::uris::{ContentURI, NarrativeURI, URIOrRefTrait, URIRef, URIRefTrait};
498 use flams_utils::vecmap::VecSet;
499 #[cfg(any(feature = "csr", feature = "hydrate"))]
500 {
501 let syms: VecSet<_> = t.uri_iter().map(URIRef::owned).collect();
502 for s in syms {
503 match &s {
504 URI::Content(ContentURI::Symbol(_)) => self.load_notations(s).await,
505 URI::Narrative(NarrativeURI::Element(_)) => self.load_notations(s).await,
506 _ => (),
507 }
508 }
509
510 struct Pres<'p> {
511 string: String,
512 slf: &'p ServerConfig,
513 }
514 impl std::fmt::Write for Pres<'_> {
515 fn write_str(&mut self, s: &str) -> std::fmt::Result {
516 self.string.push_str(s);
517 Ok(())
518 }
519 }
520 impl Presenter for Pres<'_> {
521 type N = Notation;
522 fn get_notation(&mut self, uri: &SymbolURI) -> Option<Self::N> {
523 let lock = self.slf.get_notations.cache.lock();
524 lock.get(&uri.as_uri().owned())
525 .and_then(|v| v.first().map(|(_, n)| n.clone()))
526 }
527 fn get_op_notation(&mut self, uri: &SymbolURI) -> Option<Self::N> {
528 let lock = self.slf.get_notations.cache.lock();
529 lock.get(&uri.as_uri().owned()).and_then(|v| {
530 v.iter()
531 .find_map(|(_, n)| if n.is_op() { Some(n.clone()) } else { None })
532 })
533 }
534 fn get_variable_notation(&mut self, uri: &DocumentElementURI) -> Option<Self::N> {
535 let lock = self.slf.get_notations.cache.lock();
536 lock.get(&uri.as_uri().owned())
537 .and_then(|v| v.first().map(|(_, n)| n.clone()))
538 }
539 fn get_variable_op_notation(
540 &mut self,
541 uri: &DocumentElementURI,
542 ) -> Option<Self::N> {
543 let lock = self.slf.get_notations.cache.lock();
544 lock.get(&uri.as_uri().owned()).and_then(|v| {
545 v.iter()
546 .find_map(|(_, n)| if n.is_op() { Some(n.clone()) } else { None })
547 })
548 }
549 #[inline]
550 fn in_text(&self) -> bool {
551 false
552 }
553 }
554 let mut p = Pres {
555 string: String::new(),
556 slf: self,
557 };
558 return t
559 .present(&mut p)
560 .map(|()| p.string)
561 .map_err(|e| e.to_string());
562 }
563 #[cfg(feature = "ssr")]
564 {
565 todo!()
566 }
567 }
568
569 #[cfg(all(feature = "omdoc", any(feature = "csr", feature = "hydrate")))]
570 #[inline]
571 async fn load_notations(&self, uri: URI) {
572 if self.get_notations.cache.lock().get(&uri).is_some() {
573 return;
574 }
575 let _ = self.get_notations.call(uri, ()).await;
576 }
577
578 #[cfg(any(feature = "hydrate", feature = "ssr"))]
579 pub fn initialize(
580 fragment: server_fun!(@URI,Option<URI> => (URI,Vec<CSS>,String)),
581 full_doc: server_fun!(@DOCURI => (DocumentURI,Vec<CSS>,String)),
582 toc: server_fun!(@DOCURI => (Vec<CSS>,Vec<TOCElem>)),
583 omdoc: server_fun!(@URI => (Vec<CSS>,OMDoc)),
584 los: server_fun!(@SYMURI,bool => Vec<(DocumentElementURI,LOKind)>),
585 notations: server_fun!(@URI => Vec<(DocumentElementURI,flams_ontology::narration::notations::Notation)>),
586 solutions: server_fun!(@URI => String),
587 ) {
588 let _ = server_config.get_fragment.getter.set(fragment);
589 let _ = server_config.get_omdoc.getter.set(omdoc);
590 let _ = server_config.get_full_doc.getter.set(full_doc);
591 let _ = server_config.get_toc.getter.set(toc);
592 let _ = server_config.get_los.getter.set(los);
593 let _ = server_config.get_notations.getter.set(notations);
594 let _ = server_config.get_solution.getter.set(solutions);
595 }
596}
597
598impl Default for ServerConfig {
599 fn default() -> Self {
600 Self {
601 #[cfg(feature = "csr")]
602 server_url: flams_utils::parking_lot::Mutex::new(DEFAULT_SERVER_URL.to_string()),
603 get_fragment: Cache::new("fragment"),
604 get_full_doc: Cache::new("document"),
605 get_toc: Cache::new("toc"),
606 get_los: Cache::new("los"),
607 #[cfg(feature = "omdoc")]
608 get_omdoc: Cache::new("omdoc"),
609 #[cfg(feature = "omdoc")]
610 get_notations: Cache::new("notations"),
611 get_solution: Cache::new("solution"),
612 }
613 }
614}
615
616lazy_static::lazy_static! {
617 pub static ref server_config:ServerConfig = ServerConfig::default();
618}
619
620#[cfg(feature = "csr")]
623impl ServerConfig {
624 #[inline]
625 async fn remote<T: for<'a> serde::Deserialize<'a>>(url: String) -> Result<T, String> {
626 send_wrapper::SendWrapper::new(Box::pin(async move {
627 reqwasm::http::Request::get(&url)
628 .send()
629 .await
630 .map_err(|e| e.to_string())?
631 .json::<T>()
632 .await
633 .map_err(|e| e.to_string())
634 }))
635 .await
636 }
637}