1#![allow(non_local_definitions)]
2
3use std::collections::HashMap;
4
5use flams_ontology::{
6 narration::problems::{
7 ProblemFeedback, ProblemFeedbackJson, ProblemResponse, SolutionData, Solutions,
8 },
9 uris::{DocumentElementURI, DocumentURI},
10};
11use ftml_viewer_components::{
12 components::{
13 documents::{
14 DocumentFromURI, DocumentString, FragmentFromURI, FragmentString, FragmentStringProps,
15 },
16 problem::ProblemState as OrigState,
17 Gotto, TOCElem, TOCSource,
18 },
19 ts::{
20 FragmentContinuation, InputRefContinuation, JFragCont, JInputRefCont, JOnSectTtl, JsOrRsF,
21 LeptosContext, NamedJsFunction, OnSectionTitle, TsTopCont,
22 },
23 AllowHovers, ProblemOptions,
24};
25use leptos::{either::Either, prelude::*};
26use wasm_bindgen::prelude::wasm_bindgen;
27
28#[wasm_bindgen(js_name = injectCss)]
29pub fn inject_css(mut css: flams_utils::CSS) {
30 if let flams_utils::CSS::Link(lnk) = &mut css {
31 if let Some(r) = lnk.strip_prefix("srv:") {
32 *lnk = format!("{}{r}", ftml_viewer_components::remote::get_server_url()).into();
33 }
34 }
35 flams_web_utils::do_css(css)
36}
37
38#[wasm_bindgen] pub fn set_debug_log() {
41 let _ = tracing_wasm::try_set_as_global_default();
42 console_error_panic_hook::set_once();
43}
44
45#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, tsify_next::Tsify)]
46#[tsify(into_wasm_abi, from_wasm_abi)]
47#[serde(tag = "type")]
48pub enum ProblemState {
50 Interactive {
52 #[serde(default)]
54 current_response: Option<ProblemResponse>,
55 #[serde(default)]
57 solution: Option<Box<[SolutionData]>>,
58 },
59 Finished {
61 #[serde(default)]
62 current_response: Option<ProblemResponse>,
63 },
64 Graded { feedback: ProblemFeedbackJson },
66}
67impl From<ProblemState> for OrigState {
68 fn from(value: ProblemState) -> Self {
69 match value {
70 ProblemState::Interactive {
71 current_response,
72 solution,
73 } => Self::Interactive {
74 current_response,
75 solution: solution.map(Solutions::from_solutions),
76 },
77 ProblemState::Finished { current_response } => Self::Finished { current_response },
78 ProblemState::Graded { feedback } => Self::Graded {
79 feedback: ProblemFeedback::from_json(feedback),
80 },
81 }
82 }
83}
84
85#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, tsify_next::Tsify)]
86#[tsify(into_wasm_abi, from_wasm_abi)]
87#[serde(transparent)]
88pub struct ProblemStates(pub HashMap<DocumentElementURI, ProblemState>);
89
90fn convert(
91 on_response: Option<JProblemCont>,
92 state: Option<ProblemStates>,
93) -> Option<ProblemOptions> {
94 if on_response.is_some() || state.is_some() {
95 Some(ProblemOptions {
96 on_response: on_response.map(|j| JsOrRsF::Js(j.get().into())),
97 states: state
98 .map(|e| e.0.into_iter().map(|(k, v)| (k, v.into())).collect())
99 .unwrap_or_default(),
100 })
101 } else {
102 None
103 }
104}
105
106#[wasm_bindgen]
107pub fn ftml_setup(
112 to: leptos::web_sys::HtmlElement,
113 children: TsTopCont,
114 allow_hovers: Option<bool>,
115 on_section_title: Option<JOnSectTtl>,
116 on_fragment: Option<JFragCont>,
117 on_inputref: Option<JInputRefCont>,
118 on_problem: Option<JProblemCont>,
119 problem_states: Option<ProblemStates>,
120) -> FTMLMountHandle {
121 let allow_hovers = allow_hovers.unwrap_or(true);
122 let children = children.to_cont();
123 let on_section_title = on_section_title.map(|f| OnSectionTitle(f.get().into()));
124 let on_fragment = on_fragment.map(|f| f.get().into());
125 let on_inputref = on_inputref.map(|f| f.get().into());
126 let problem_opts = convert(on_problem, problem_states);
127
128 FTMLMountHandle::new(to, move || {
129 view! {
130 <GlobalSetup allow_hovers on_fragment on_section_title on_inputref problem_opts>{
131 let ret = NodeRef::new();
132 ret.on_load(move |e| {
133 let owner = Owner::current().expect("Not in a leptos reactive context!");
134 if let Err(e) = children.apply(&(e,owner.into())) {
135 tracing::error!("Error calling continuation: {e}");
136 }
137 });
138 view!(<div node_ref = ret/>)
139 }</GlobalSetup>
140 }
141 })
142}
143
144#[allow(clippy::needless_pass_by_value)]
145#[wasm_bindgen]
146pub fn render_document(
149 to: leptos::web_sys::HtmlElement,
150 document: DocumentOptions,
151 context: Option<LeptosContext>,
152 allow_hovers: Option<bool>,
153 on_section_title: Option<JOnSectTtl>,
154 on_fragment: Option<JFragCont>,
155 on_inputref: Option<JInputRefCont>,
156 on_problem: Option<JProblemCont>,
157 problem_states: Option<ProblemStates>,
158) -> Result<FTMLMountHandle, String> {
159 fn inner(
160 to: leptos::web_sys::HtmlElement,
161 document: DocumentOptions,
162 allow_hovers: bool,
163 on_section_title: Option<JOnSectTtl>,
164 on_fragment: Option<JFragCont>,
165 on_inputref: Option<JInputRefCont>,
166 problem_opts: Option<ProblemOptions>,
167 ) -> Result<FTMLMountHandle, String> {
168 let on_section_title = on_section_title.map(|f| OnSectionTitle(f.get().into()));
169 let on_fragment = on_fragment.map(|f| f.get().into());
170 let on_inputref = on_inputref.map(|f| f.get().into());
171
172 let comp = move || match document {
173 DocumentOptions::HtmlString { html, gottos, toc } => {
174 let toc = toc.map_or(TOCSource::None, TOCSource::Ready);
175 let gottos = gottos.unwrap_or_default();
176 Either::Left(
177 view! {<GlobalSetup allow_hovers on_section_title on_fragment on_inputref problem_opts>
178 <DocumentString html gottos toc/>
179 </GlobalSetup>},
180 )
181 }
182 DocumentOptions::FromBackend { uri, gottos, toc } => {
183 let toc = toc.map_or(TOCSource::None, Into::into);
184 let gottos = gottos.unwrap_or_default();
185 Either::Right(
186 view! {<GlobalSetup allow_hovers on_section_title on_fragment on_inputref problem_opts>
187 <DocumentFromURI uri gottos toc/>
188 </GlobalSetup>},
189 )
190 }
191 };
192
193 Ok(FTMLMountHandle::new(to, move || comp()))
194 }
195 let allow_hovers = allow_hovers.unwrap_or(true);
196 let problem_opts = convert(on_problem, problem_states);
197 if let Some(context) = context {
198 context.with(move || {
199 inner(
200 to,
201 document,
202 allow_hovers,
203 on_section_title,
204 on_fragment,
205 on_inputref,
206 problem_opts,
207 )
208 })
209 } else {
210 inner(
211 to,
212 document,
213 allow_hovers,
214 on_section_title,
215 on_fragment,
216 on_inputref,
217 problem_opts,
218 )
219 }
220}
221
222#[allow(clippy::needless_pass_by_value)]
223#[wasm_bindgen]
224pub fn render_fragment(
227 to: leptos::web_sys::HtmlElement,
228 fragment: FragmentOptions,
229 context: Option<LeptosContext>,
230 allow_hovers: Option<bool>,
231 on_section_title: Option<JOnSectTtl>,
232 on_fragment: Option<JFragCont>,
233 on_inputref: Option<JInputRefCont>,
234 on_problem: Option<JProblemCont>,
235 problem_states: Option<ProblemStates>,
236) -> Result<FTMLMountHandle, String> {
237 fn inner(
238 to: leptos::web_sys::HtmlElement,
239 fragment: FragmentOptions,
240 allow_hovers: bool,
241 on_section_title: Option<JOnSectTtl>,
242 on_fragment: Option<JFragCont>,
243 on_inputref: Option<JInputRefCont>,
244 problem_opts: Option<ProblemOptions>,
245 ) -> Result<FTMLMountHandle, String> {
246 let _ = to.style().set_property(
247 "--rustex-this-width",
248 "var(--rustex-curr-width,min(800px,100%))",
249 );
250 let on_section_title = on_section_title.map(|f| OnSectionTitle(f.get().into()));
251 let on_fragment = on_fragment.map(|f| f.get().into());
252 let on_inputref = on_inputref.map(|f| f.get().into());
253
254 let comp = move || match fragment {
255 FragmentOptions::HtmlString { html, uri } => {
256 Either::Left(FragmentString(FragmentStringProps { html, uri }))
257 }
258 FragmentOptions::FromBackend { uri } => Either::Right(view! {<FragmentFromURI uri/>}),
259 };
260 Ok(FTMLMountHandle::new(to, move || {
261 view! {<GlobalSetup allow_hovers on_section_title on_fragment on_inputref problem_opts>
262 {comp()}
263 </GlobalSetup>}
264 }))
265 }
266 let allow_hovers = allow_hovers.unwrap_or(true);
267 let problem_opts = convert(on_problem, problem_states);
268 if let Some(context) = context {
269 context.with(move || {
270 inner(
271 to,
272 fragment,
273 allow_hovers,
274 on_section_title,
275 on_fragment,
276 on_inputref,
277 problem_opts,
278 )
279 })
280 } else {
281 inner(
282 to,
283 fragment,
284 allow_hovers,
285 on_section_title,
286 on_fragment,
287 on_inputref,
288 problem_opts,
289 )
290 }
291}
292
293#[component]
294fn GlobalSetup<V: IntoView + 'static>(
295 #[prop(default = true)] allow_hovers: bool,
296 #[prop(default=None)] on_section_title: Option<OnSectionTitle>,
297 #[prop(default=None)] on_fragment: Option<FragmentContinuation>,
298 #[prop(default=None)] on_inputref: Option<InputRefContinuation>,
299 #[prop(default=None)] problem_opts: Option<ProblemOptions>,
300 children: TypedChildren<V>,
301) -> impl IntoView {
302 use flams_web_utils::components::Themer;
303 use ftml_viewer_components::FTMLGlobalSetup;
304 use leptos::either::Either::{Left, Right};
306 console_error_panic_hook::set_once();
307 let children = children.into_inner();
308 let children = move || {
309 if let Some(on_section_title) = on_section_title {
310 provide_context(Some(on_section_title));
311 }
312 if let Some(on_fragment) = on_fragment {
313 provide_context(Some(on_fragment));
314 }
315 if let Some(on_inputref) = on_inputref {
316 provide_context(Some(on_inputref));
317 }
318 if let Some(problem_opts) = problem_opts {
319 provide_context(problem_opts);
320 }
321 provide_context(AllowHovers(allow_hovers));
322 children()
323 };
324
325 let children = move || {
326 if ftml_viewer_components::is_in_ftml() {
327 Left(children())
328 } else {
329 Right(view!(<FTMLGlobalSetup>{children()}</FTMLGlobalSetup>))
330 }
331 };
332
333 if with_context::<thaw::ConfigInjection, _>(|_| ()).is_none() {
335 Left(
336 view!(<Themer attr:style="font-family:inherit;font-size:inherit;font-weight:inherit;line-height:inherit;background-color:inherit;">{children()}</Themer>),
337 )
338 } else {
339 Right(children())
340 }
341 }
345
346#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, tsify_next::Tsify)]
347#[tsify(into_wasm_abi, from_wasm_abi)]
348#[serde(tag = "type")]
357pub enum DocumentOptions {
358 FromBackend {
359 uri: DocumentURI,
360 #[serde(default)]
361 gottos: Option<Vec<Gotto>>,
362 toc: Option<TOCOptions>,
364 },
365 HtmlString {
367 html: String,
368 #[serde(default)]
369 gottos: Option<Vec<Gotto>>,
370 toc: Option<Vec<TOCElem>>,
372 },
373}
374
375#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, tsify_next::Tsify)]
376#[tsify(into_wasm_abi, from_wasm_abi)]
377#[serde(tag = "type")]
378pub enum FragmentOptions {
385 FromBackend {
386 uri: DocumentElementURI,
387 },
388 HtmlString {
390 html: String,
391 #[serde(default)]
392 uri: Option<DocumentElementURI>,
393 },
394}
395
396#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, tsify_next::Tsify)]
397#[tsify(into_wasm_abi, from_wasm_abi)]
398pub enum TOCOptions {
402 #[serde(rename = "GET")]
403 GET,
404 Predefined(Vec<TOCElem>),
405}
406
407impl From<TOCOptions> for TOCSource {
408 fn from(value: TOCOptions) -> Self {
409 match value {
410 TOCOptions::GET => Self::Get,
411 TOCOptions::Predefined(toc) => Self::Ready(toc),
412 }
413 }
414}
415
416ftml_viewer_components::ts_function! {
417 JProblemCont ProblemCont @ "(r:ProblemResponse) => void"
418 = ProblemResponse => ()
419}
420
421#[wasm_bindgen]
439pub struct FTMLMountHandle {
440 mount: std::cell::Cell<
441 Option<leptos::prelude::UnmountHandle<leptos::tachys::view::any_view::AnyViewState>>,
442 >,
443}
444
445#[wasm_bindgen]
446impl FTMLMountHandle {
447 pub fn unmount(&self) -> Result<(), wasm_bindgen::JsError> {
450 if let Some(mount) = self.mount.take() {
451 drop(mount); }
453 Ok(())
454 }
455 fn new<V: IntoView + 'static>(
456 div: leptos::web_sys::HtmlElement,
457 f: impl FnOnce() -> V + 'static,
458 ) -> Self {
459 let handle = leptos::prelude::mount_to(div, move || f().into_any());
460 Self {
461 mount: std::cell::Cell::new(Some(handle)),
462 }
463 }
464}