ftml_viewer_components/
ts.rs

1use crate::FragmentKind;
2use flams_ontology::{
3    narration::{paragraphs::ParagraphKind, problems::ProblemResponse, sections::SectionLevel},
4    uris::{DocumentElementURI, DocumentURI},
5};
6use flams_utils::unwrap;
7use leptos::prelude::*;
8use std::marker::PhantomData;
9use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
10use web_sys::HtmlDivElement;
11
12pub trait AsTs {
13    fn as_ts(&self) -> JsValue;
14}
15pub trait FromTs: Sized {
16    fn from_ts(v: JsValue) -> Result<Self, JsValue>;
17}
18
19impl AsTs for String {
20    #[inline]
21    fn as_ts(&self) -> JsValue {
22        JsValue::from_str(self)
23    }
24}
25impl FromTs for String {
26    #[inline]
27    fn from_ts(v: JsValue) -> Result<Self, JsValue> {
28        v.as_string().map_or(Err(v), Ok)
29    }
30}
31
32impl AsTs for DocumentElementURI {
33    #[inline]
34    fn as_ts(&self) -> JsValue {
35        JsValue::from_str(self.to_string().as_str())
36    }
37}
38impl FromTs for DocumentElementURI {
39    #[inline]
40    fn from_ts(v: JsValue) -> Result<Self, JsValue> {
41        v.as_string()
42            .and_then(|s| s.parse().ok())
43            .map_or(Err(v), Ok)
44    }
45}
46impl AsTs for DocumentURI {
47    #[inline]
48    fn as_ts(&self) -> JsValue {
49        JsValue::from_str(self.to_string().as_str())
50    }
51}
52impl FromTs for DocumentURI {
53    #[inline]
54    fn from_ts(v: JsValue) -> Result<Self, JsValue> {
55        v.as_string()
56            .and_then(|s| s.parse().ok())
57            .map_or(Err(v), Ok)
58    }
59}
60impl AsTs for ParagraphKind {
61    #[inline]
62    fn as_ts(&self) -> JsValue {
63        JsValue::from_str(self.as_str())
64    }
65}
66impl FromTs for ParagraphKind {
67    #[inline]
68    fn from_ts(v: JsValue) -> Result<Self, JsValue> {
69        v.as_string()
70            .and_then(|s| s.parse().ok())
71            .map_or(Err(v), Ok)
72    }
73}
74
75impl AsTs for FragmentKind {
76    #[inline]
77    fn as_ts(&self) -> JsValue {
78        unwrap!(web_sys::js_sys::JSON::parse(&unwrap!(serde_json::to_string(self).ok())).ok())
79    }
80}
81impl FromTs for FragmentKind {
82    #[inline]
83    fn from_ts(v: JsValue) -> Result<Self, JsValue> {
84        v.as_string()
85            .and_then(|s| serde_json::from_str(&s).ok())
86            .map_or(Err(v), Ok)
87    }
88}
89
90impl AsTs for SectionLevel {
91    #[inline]
92    fn as_ts(&self) -> JsValue {
93        serde_wasm_bindgen::to_value(self).expect("unreachable")
94    }
95}
96
97impl AsTs for () {
98    #[inline]
99    fn as_ts(&self) -> JsValue {
100        JsValue::NULL
101    }
102}
103impl FromTs for () {
104    #[inline]
105    fn from_ts(_: JsValue) -> Result<Self, JsValue> {
106        Ok(())
107    }
108}
109
110impl AsTs for HtmlDivElement {
111    fn as_ts(&self) -> JsValue {
112        self.clone().into()
113    }
114}
115impl FromTs for HtmlDivElement {
116    fn from_ts(v: JsValue) -> Result<Self, JsValue> {
117        use wasm_bindgen::JsCast;
118        v.dyn_into()
119    }
120}
121
122impl AsTs for ProblemResponse {
123    fn as_ts(&self) -> JsValue {
124        self.clone().into()
125    }
126}
127
128impl<T: AsTs> AsTs for Option<T> {
129    #[inline]
130    fn as_ts(&self) -> JsValue {
131        match self {
132            None => wasm_bindgen::JsValue::UNDEFINED,
133            Some(v) => v.as_ts(),
134        }
135    }
136}
137impl<T: FromTs> FromTs for Option<T> {
138    #[inline]
139    fn from_ts(v: JsValue) -> Result<Self, JsValue> {
140        if v.is_null() || v.is_undefined() {
141            Ok(None)
142        } else {
143            T::from_ts(v).map(Some)
144        }
145    }
146}
147
148pub trait JsFunArgable: Sized {
149    fn call_js<R: FromTs>(&self, j: &JsFun<Self, R>) -> Result<JsValue, JsValue>;
150}
151
152impl<T: AsTs> JsFunArgable for T {
153    #[inline]
154    fn call_js<R: FromTs>(&self, j: &JsFun<Self, R>) -> Result<JsValue, JsValue> {
155        j.js.call1(&JsValue::UNDEFINED, &self.as_ts())
156    }
157}
158impl<T1: AsTs, T2: AsTs> JsFunArgable for (T1, T2) {
159    #[inline]
160    fn call_js<R: FromTs>(&self, j: &JsFun<Self, R>) -> Result<JsValue, JsValue> {
161        j.js.call2(&JsValue::UNDEFINED, &self.0.as_ts(), &self.1.as_ts())
162    }
163}
164impl<T1: AsTs, T2: AsTs, T3: AsTs> JsFunArgable for (T1, T2, T3) {
165    #[inline]
166    fn call_js<R: FromTs>(&self, j: &JsFun<Self, R>) -> Result<JsValue, JsValue> {
167        j.js.call3(
168            &JsValue::UNDEFINED,
169            &self.0.as_ts(),
170            &self.1.as_ts(),
171            &self.2.as_ts(),
172        )
173    }
174}
175
176pub struct JsFun<Args: JsFunArgable, R: FromTs> {
177    pub js: send_wrapper::SendWrapper<web_sys::js_sys::Function>,
178    pub ret: PhantomData<send_wrapper::SendWrapper<(Args, R)>>,
179}
180// unsafe impl<Args:JsFunArgable,R:Tsable> Send for JsFun<Args,R> {}
181
182impl<Args: JsFunArgable, R: FromTs> Clone for JsFun<Args, R> {
183    #[inline]
184    fn clone(&self) -> Self {
185        Self {
186            js: self.js.clone(),
187            ret: PhantomData,
188        }
189    }
190}
191impl<Args: JsFunArgable, R: FromTs> AsTs for JsFun<Args, R> {
192    fn as_ts(&self) -> JsValue {
193        (&*self.js).clone().into()
194    }
195}
196impl<Args: JsFunArgable, R: FromTs> FromTs for JsFun<Args, R> {
197    fn from_ts(v: JsValue) -> Result<Self, JsValue> {
198        Ok(Self {
199            js: send_wrapper::SendWrapper::new(web_sys::js_sys::Function::from(v)),
200            ret: PhantomData,
201        })
202    }
203}
204
205impl<Args: JsFunArgable, R: FromTs> JsFun<Args, R> {
206    #[inline]
207    pub fn apply(&self, args: &Args) -> Result<R, JsValue> {
208        args.call_js(self).and_then(|r| R::from_ts(r))
209    }
210}
211
212pub trait JsFunLike<Args: JsFunArgable, R: FromTs>:
213    Fn(&Args) -> Result<R, String> + 'static + Send + Sync
214{
215    fn bx_clone(&self) -> Box<dyn JsFunLike<Args, R>>;
216}
217impl<
218        Args: JsFunArgable,
219        R: FromTs,
220        T: Fn(&Args) -> Result<R, String> + Clone + 'static + Send + Sync,
221    > JsFunLike<Args, R> for T
222{
223    #[inline]
224    fn bx_clone(&self) -> Box<dyn JsFunLike<Args, R>> {
225        Box::new(self.clone())
226    }
227}
228
229pub enum JsOrRsF<Args: JsFunArgable, R: FromTs> {
230    Rs(Box<dyn JsFunLike<Args, R>>),
231    Js(JsFun<Args, R>),
232}
233impl<Args: JsFunArgable + 'static, R: FromTs + 'static> Clone for JsOrRsF<Args, R> {
234    #[inline]
235    fn clone(&self) -> Self {
236        match self {
237            Self::Rs(s) => {
238                let b = (&**s).bx_clone();
239                Self::Rs(b)
240            }
241            Self::Js(j) => Self::Js(j.clone()),
242        }
243    }
244}
245impl<Args: JsFunArgable, R: FromTs> JsOrRsF<Args, R> {
246    #[inline]
247    pub fn apply(&self, args: &Args) -> Result<R, String> {
248        match self {
249            Self::Rs(r) => r(args),
250            Self::Js(j) => j.apply(args).map_err(|e| e.as_string().unwrap_or_default()),
251        }
252    }
253    #[inline]
254    pub fn new(f: impl Fn(&Args) -> Result<R, String> + 'static + Clone + Send + Sync) -> Self {
255        Self::Rs(Box::new(f))
256    }
257}
258impl<Args: JsFunArgable, R: FromTs> From<JsFun<Args, R>> for JsOrRsF<Args, R> {
259    #[inline]
260    fn from(value: JsFun<Args, R>) -> Self {
261        Self::Js(value)
262    }
263}
264
265impl<Args: JsFunArgable, R: FromTs> FromTs for JsOrRsF<Args, R> {
266    fn from_ts(v: JsValue) -> Result<Self, JsValue> {
267        let f: JsFun<Args, R> = JsFun::from_ts(v)?;
268        Ok(Self::Js(f.into()))
269    }
270}
271
272pub trait NamedJsFunction {
273    type Args: JsFunArgable;
274    type R: FromTs;
275    type Base;
276
277    #[cfg(feature = "ts")]
278    fn get(self) -> Self::Base;
279}
280
281pub use send_wrapper::SendWrapper as SendWrapperReexported;
282
283#[macro_export]
284macro_rules! ts_function {
285    ($name:ident $nameB:ident @ $ts_type:literal = $args:ty => $ret:ty) => {
286        #[cfg(feature = "ts")]
287        #[::wasm_bindgen::prelude::wasm_bindgen]
288        extern "C" {
289            #[::wasm_bindgen::prelude::wasm_bindgen(extends = ::leptos::web_sys::js_sys::Function)]
290            #[::wasm_bindgen::prelude::wasm_bindgen(typescript_type = $ts_type)]
291            pub type $name;
292        }
293
294        #[cfg(not(feature = "ts"))]
295        #[derive(Clone)]
296        pub struct $name;
297
298        impl $crate::ts::NamedJsFunction for $name {
299            type Args = $args;
300            type R = $ret;
301            type Base = $crate::ts::JsFun<$args, $ret>;
302            #[cfg(feature = "ts")]
303            fn get(self) -> Self::Base {
304                $crate::ts::JsFun {
305                    js: $crate::ts::SendWrapperReexported::new(self.into()),
306                    ret: ::std::marker::PhantomData,
307                }
308            }
309        }
310        pub type $nameB = $crate::ts::JsOrRsF<$args, $ret>;
311    };
312}
313
314#[wasm_bindgen]
315#[derive(Clone)]
316pub struct LeptosContext {
317    inner: std::sync::Arc<std::sync::Mutex<Option<Owner>>>,
318}
319impl LeptosContext {
320    pub fn with<R>(&self, f: impl FnOnce() -> R) -> R {
321        if let Some(Some(o)) = self.inner.lock().ok().as_deref().cloned() {
322            o.with(f)
323        } else {
324            tracing::warn!("Leptos context already cleaned up!");
325            f()
326        }
327    }
328}
329
330/*
331std::panic::catch_unwind
332leptos::web_sys::js_sys::JsString::try_from(&wasm_bindgen::JSValue)
333leptos::web_sys::js_sys::JSON::stringify()
334fn() -> Result<T, wasm_bindgen::JsError> <- will throw js error
335"For more complex error handling, JsError implements From<T> where T: std::error::Error"
336
337let msg = match info.payload().downcast_ref::<&'static str>() {
338    Some(s) => *s,
339    None => match info.payload().downcast_ref::<String>() {
340        Some(s) => &s[..],
341        None => "Box<dyn Any>",
342    },
343};
344*/
345
346#[wasm_bindgen]
347impl LeptosContext {
348    /// Cleans up the reactive system.
349    /// Not calling this is a memory leak
350    pub fn cleanup(&self) -> Result<(), wasm_bindgen::JsError> {
351        if let Some(mount) = self.inner.try_lock().ok().and_then(|mut l| l.take()) {
352            mount.cleanup(); //flams_web_utils::try_catch(move || mount.cleanup())?;
353        }
354        Ok(())
355    }
356
357    pub fn wasm_clone(&self) -> Self {
358        self.clone()
359    }
360}
361
362impl From<Owner> for LeptosContext {
363    #[inline]
364    fn from(value: Owner) -> Self {
365        Self {
366            inner: std::sync::Arc::new(std::sync::Mutex::new(Some(value))),
367        }
368    }
369}
370
371impl AsTs for LeptosContext {
372    #[inline]
373    fn as_ts(&self) -> JsValue {
374        JsValue::from(self.clone())
375    }
376}
377
378#[wasm_bindgen(typescript_custom_section)]
379const TS_CONT_FUN: &'static str =
380    r#"export type LeptosContinuation = (e:HTMLDivElement,o:LeptosContext) => void;"#;
381pub type TsCont = JsOrRsF<(HtmlDivElement, LeptosContext), ()>;
382
383impl<Args: JsFunArgable + 'static> JsOrRsF<Args, Option<TsCont>> {
384    pub fn wrap<T: IntoView>(args: &Args, children: T) -> impl IntoView {
385        if let Some(slf) = expect_context::<Option<Self>>() {
386            match slf.apply(args) {
387                Ok(Some(cont)) => {
388                    let owner = Owner::current()
389                        .expect("Not in a leptos reactive context!")
390                        .into();
391                    let rf = NodeRef::new();
392                    rf.on_load(move |elem| {
393                        if let Err(err) = cont.apply(&(elem, owner)) {
394                            tracing::error!("Error calling continuation: {err}");
395                        }
396                    });
397                    leptos::either::Either::Left(view! {<div node_ref=rf>{children}</div>})
398                }
399                Ok(None) => leptos::either::Either::Right(children),
400                Err(e) => {
401                    tracing::error!("Error calling continuation: {e}");
402                    leptos::either::Either::Right(children)
403                }
404            }
405        } else {
406            leptos::either::Either::Right(children)
407        }
408    }
409}
410
411ts_function! {
412  TsTopCont LCont @ "LeptosContinuation"
413  = (HtmlDivElement,LeptosContext) => ()
414}
415
416impl TsTopCont {
417    #[inline]
418    #[cfg(feature = "ts")]
419    pub fn to_cont(self) -> TsCont {
420        JsOrRsF::Js(self.get())
421    }
422}
423
424impl TsCont {
425    pub fn view(self) -> impl IntoView {
426        let ret = NodeRef::new();
427        ret.on_load(move |e| {
428            let owner = Owner::current().expect("Not in a leptos reactive context!");
429            if let Err(e) = self.apply(&(e, owner.into())) {
430                tracing::error!("Error calling continuation: {e}");
431            }
432        });
433        view!(<div node_ref = ret/>)
434    }
435    pub fn res_into_view(f: Result<Option<Self>, String>) -> impl IntoView {
436        match f {
437            Err(e) => {
438                tracing::error!("Error getting continuation: {e}");
439                None
440            }
441            Ok(None) => None,
442            Ok(Some(f)) => Some(f.view()),
443        }
444    }
445}
446
447ts_function! {
448  JFragCont FragmentContinuation @ "(uri: DocumentElementURI,kind:FragmentKind) => (LeptosContinuation | undefined)"
449  = (DocumentElementURI,FragmentKind) => Option<TsCont>
450}
451
452ts_function! {
453  JOnSectTtl OnSectionTitleFn @ "(uri: DocumentElementURI,lvl:SectionLevel) => (LeptosContinuation | undefined)"
454  = (DocumentElementURI,SectionLevel) => Option<TsCont>
455}
456
457ts_function! {
458  JInputRefCont InputRefContinuation @ "(uri: DocumentURI) => (LeptosContinuation | undefined)"
459  = DocumentURI => Option<TsCont>
460}
461
462#[derive(Clone)]
463pub struct OnSectionTitle(pub OnSectionTitleFn);
464
465/*
466ts_function! {
467  JParaCont ParagraphContinuation @ "(uri: DocumentElementURI,kind:ParagraphKind) => (LeptosContinuation | undefined)"
468  = (DocumentElementURI,ParagraphKind) => Option<TsCont>
469}
470
471ts_function! {
472  JSectCont SectionContinuationFn @ "(uri: DocumentElementURI,lvl:SectionLevel) => (LeptosContinuation | undefined)"
473  = (DocumentElementURI,SectionLevel) => Option<TsCont>
474}
475
476
477ts_function! {
478  JSlideCont SlideContinuation @ "(uri: DocumentElementURI) => (LeptosContinuation | undefined)"
479  = DocumentElementURI => Option<TsCont>
480}
481
482#[derive(Clone)]
483pub struct SectionContinuation(pub SectionContinuationFn);
484
485impl SectionContinuation {
486    pub fn wrap<T: IntoView>(
487        args: &(DocumentElementURI, SectionLevel),
488        children: T,
489    ) -> impl IntoView {
490        if let Some(slf) = expect_context::<Option<Self>>() {
491            match slf.0.apply(args) {
492                Ok(Some(cont)) => {
493                    let owner = Owner::current()
494                        .expect("Not in a leptos reactive context!")
495                        .into();
496                    let rf = NodeRef::new();
497                    rf.on_load(move |elem| {
498                        if let Err(err) = cont.apply(&(elem, owner)) {
499                            tracing::error!("Error calling continuation: {err}");
500                        }
501                    });
502                    leptos::either::Either::Left(view! {<div node_ref=rf>{children}</div>})
503                }
504                Ok(None) => leptos::either::Either::Right(children),
505                Err(e) => {
506                    tracing::error!("Error calling continuation: {e}");
507                    leptos::either::Either::Right(children)
508                }
509            }
510        } else {
511            leptos::either::Either::Right(children)
512        }
513    }
514}
515 */