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}
180impl<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#[wasm_bindgen]
347impl LeptosContext {
348 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(); }
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