ftml_viewer_components/
remote.rs

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]
41/// gets the current server url
42pub 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    /// #### Errors
288    /// #### Panics
289    #[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            //return T::call(key,extra,f).await.map_err(|e| e.to_string());
329            //return f(Some(URI::Narrative(doc.into())),None,None,None,None,None,None,None,None).await.map_err(|e| e.to_string());
330        }
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    /// #### Errors
371    /// #### Panics
372    #[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    /// #### Errors
380    /// #### Panics
381    #[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    /// #### Errors
392    /// #### Panics
393    #[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    /// #### Errors
402    /// #### Panics
403    #[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    /// #### Errors
412    /// #### Panics
413    #[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    /// #### Errors
419    /// #### Panics
420    #[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    /// #### Errors
430    /// #### Panics
431    #[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    /// #### Errors
438    /// #### Panics
439    #[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    /// #### Errors
453    /// #### Panics
454    #[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            //.expect("Notation not found; this should not happen")
486        }
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// URLs
621
622#[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}