1#![allow(clippy::must_use_candidate)]
2#![allow(clippy::module_name_repetitions)]
3#![cfg_attr(docsrs, feature(doc_auto_cfg))]
4#![recursion_limit = "256"]
7
8pub mod components;
11pub mod config;
12mod extractor;
13pub mod remote;
14pub mod ts;
15
16use crate::extractor::NodeAttrs;
17pub use components::problem::ProblemOptions;
18use components::{
19 counters::SectionCounters, inputref::InInputRef, FTMLComponents, TOCElem, TOCSource,
20};
21use config::{FTMLConfig, IdPrefix};
22use extractor::DOMExtractor;
23use flams_ontology::{
24 narration::{
25 paragraphs::ParagraphKind,
26 problems::{CognitiveDimension, ProblemResponse, Solutions},
27 sections::SectionLevel,
28 },
29 uris::{DocumentElementURI, DocumentURI, NarrativeURI, URI},
30};
31use flams_utils::{prelude::HMap, vecmap::VecMap};
32use flams_web_utils::{components::wait_local, do_css, inject_css};
33use ftml_extraction::{open::terms::VarOrSym, prelude::*};
34use leptos::prelude::*;
35use leptos::tachys::view::any_view::AnyView;
36use leptos::web_sys::Element;
37use leptos_posthoc::{DomStringCont, DomStringContMath};
38use wasm_bindgen::prelude::wasm_bindgen;
39
40use crate::ts::{FragmentContinuation, InputRefContinuation, OnSectionTitle};
41
42#[inline]
43pub fn is_in_ftml() -> bool {
44 with_context::<FTMLConfig, _>(|_| ()).is_some()
45}
46
47#[derive(Copy, Clone, PartialEq, Eq)]
48pub struct AllowHovers(pub bool);
49impl AllowHovers {
50 pub fn get() -> bool {
51 use_context::<Self>().map(|s| s.0).unwrap_or(true)
52 }
53}
54
55#[derive(Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
56pub enum HighlightOption {
57 Colored,
58 Subtle,
59 Off,
60 None,
61}
62impl HighlightOption {
63 fn as_str(self) -> &'static str {
64 match self {
65 Self::Colored => "colored",
66 Self::Subtle => "subtle",
67 Self::Off => "off",
68 Self::None => "none",
69 }
70 }
71 fn from_str(s: &str) -> Option<Self> {
72 match s {
73 "colored" => Some(Self::Colored),
74 "subtle" => Some(Self::Subtle),
75 "off" => Some(Self::Off),
76 "none" => Some(Self::None),
77 _ => None,
78 }
79 }
80}
81
82#[component(transparent)]
83pub fn FTMLGlobalSetup<Ch: IntoView + 'static>(
84 #[prop(default=None)] allow_hovers: Option<bool>,
86 #[prop(default=None)] on_section_title: Option<OnSectionTitle>,
87 #[prop(default=None)] on_fragment: Option<FragmentContinuation>,
88 #[prop(default=None)] on_inpuref: Option<InputRefContinuation>,
89 #[prop(default=None)] problem_opts: Option<ProblemOptions>,
90 children: TypedChildren<Ch>,
91) -> impl IntoView {
92 let children = children.into_inner();
93 if allow_hovers.is_some_and(|e| !e) {
94 provide_context(AllowHovers(false))
95 }
96 #[cfg(any(feature = "csr", feature = "hydrate"))]
97 provide_context(RwSignal::new(DOMExtractor::default()));
98 provide_context(SectionCounters::default());
99 provide_context(NarrativeURI::Document(DocumentURI::no_doc()));
100 provide_context(FTMLConfig::new());
101 provide_context(RwSignal::new(None::<Vec<TOCElem>>));
102
103 #[cfg(any(feature = "csr", feature = "hydrate"))]
104 let hl_opt = {
105 let r = <gloo_storage::LocalStorage as gloo_storage::Storage>::get("highlight_option")
106 .map_or_else(|_| HighlightOption::Colored, |e| e);
107 let r = RwSignal::new(r);
108 Effect::new(move || {
109 let r = r.get();
110 let _ =
111 <gloo_storage::LocalStorage as gloo_storage::Storage>::set("highlight_option", r);
112 });
113 r
114 };
115 #[cfg(not(any(feature = "csr", feature = "hydrate")))]
116 let hl_opt = RwSignal::new(HighlightOption::Colored);
117 provide_context(hl_opt);
118 provide_context(on_fragment);
119 provide_context(on_section_title);
120 provide_context(on_inpuref);
121 if let Some(problem_opts) = problem_opts {
122 provide_context(problem_opts);
123 }
124 children()
126}
127
128#[component]
129pub fn FTMLDocumentSetup<Ch: IntoView + 'static>(
130 uri: DocumentURI,
131 #[prop(default=None)] allow_hovers: Option<bool>,
132 #[prop(default=None)] on_section_title: Option<OnSectionTitle>,
133 #[prop(default=None)] on_fragment: Option<FragmentContinuation>,
134 #[prop(default=None)] on_inpuref: Option<InputRefContinuation>,
135 #[prop(default=None)] problem_opts: Option<ProblemOptions>,
136 children: TypedChildren<Ch>,
137) -> impl IntoView {
138 use crate::components::navigation::{Nav, NavElems, URLFragment};
139 let children = children.into_inner();
140 inject_css("ftml-comp", include_str!("components/comp.css"));
141 #[cfg(any(feature = "csr", feature = "hydrate"))]
144 provide_context(RwSignal::new(DOMExtractor::default()));
145 if allow_hovers.is_some_and(|e| !e) {
146 provide_context(AllowHovers(false))
147 }
148 provide_context(InInputRef(false));
149 provide_context(RwSignal::new(NavElems {
150 ids: HMap::default(),
151 titles: HMap::default(),
152 initialized: RwSignal::new(false),
153 }));
154 provide_context(IdPrefix(String::new()));
155 provide_context(SectionCounters::default());
156 provide_context(RwSignal::new(None::<Vec<TOCElem>>));
157 provide_context(URLFragment::new());
158 provide_context(NarrativeURI::Document(uri));
159 if let Some(on_section_title) = on_section_title {
160 provide_context(Some(on_section_title));
161 }
162 if let Some(on_fragment) = on_fragment {
163 provide_context(Some(on_fragment));
164 }
165 if let Some(on_inpuref) = on_inpuref {
166 provide_context(Some(on_inpuref));
167 }
168 if let Some(problem_opts) = problem_opts {
169 provide_context(problem_opts);
170 }
171 let r = children();
172 view! {
173 <Nav/>
174 {r}
175 }
176}
177
178#[component]
179pub fn FTMLString(html: String) -> impl IntoView {
180 view!(<DomStringCont html cont=iterate/>)
181}
182#[component]
183pub fn FTMLStringMath(html: String) -> impl IntoView {
184 view!(<math><DomStringContMath html cont=iterate/></math>)
185}
186
187pub static RULES: [FTMLExtractionRule<DOMExtractor>; 54] = [
188 rule(FTMLTag::Section),
189 rule(FTMLTag::SkipSection),
190 rule(FTMLTag::Term),
191 rule(FTMLTag::Arg),
192 rule(FTMLTag::InputRef),
193 rule(FTMLTag::Slide),
194 rule(FTMLTag::Style),
195 rule(FTMLTag::CounterParent),
196 rule(FTMLTag::Counter),
197 rule(FTMLTag::Comp),
198 rule(FTMLTag::VarComp),
199 rule(FTMLTag::MainComp),
200 rule(FTMLTag::DefComp),
201 rule(FTMLTag::Definiendum),
202 rule(FTMLTag::IfInputref),
203 rule(FTMLTag::Problem),
204 rule(FTMLTag::SubProblem),
205 rule(FTMLTag::ProblemHint),
206 rule(FTMLTag::ProblemSolution),
207 rule(FTMLTag::ProblemGradingNote),
208 rule(FTMLTag::ProblemMultipleChoiceBlock),
209 rule(FTMLTag::ProblemSingleChoiceBlock),
210 rule(FTMLTag::ProblemChoice),
211 rule(FTMLTag::ProblemFillinsol),
212 rule(FTMLTag::SetSectionLevel),
213 rule(FTMLTag::Title),
214 rule(FTMLTag::Definition),
215 rule(FTMLTag::Paragraph),
216 rule(FTMLTag::Assertion),
217 rule(FTMLTag::Example),
218 rule(FTMLTag::Proof),
219 rule(FTMLTag::SubProof),
220 rule(FTMLTag::ProofHide),
221 rule(FTMLTag::ProofBody),
222 rule(FTMLTag::ProofTitle),
223 rule(FTMLTag::SubproofTitle),
224 rule(FTMLTag::SlideNumber),
225 rule(FTMLTag::ArgMode),
227 rule(FTMLTag::NotationId),
228 rule(FTMLTag::Head),
229 rule(FTMLTag::Language),
230 rule(FTMLTag::Metatheory),
231 rule(FTMLTag::Signature),
232 rule(FTMLTag::Args),
233 rule(FTMLTag::Macroname),
234 rule(FTMLTag::Inline),
235 rule(FTMLTag::Fors),
236 rule(FTMLTag::Id),
237 rule(FTMLTag::NotationFragment),
238 rule(FTMLTag::Precedence),
239 rule(FTMLTag::Role),
240 rule(FTMLTag::Argprecs),
241 rule(FTMLTag::Autogradable),
242 rule(FTMLTag::AnswerClassPts),
243];
244
245#[cfg_attr(
246 all(feature = "csr", not(feature = "ts")),
247 wasm_bindgen::prelude::wasm_bindgen
248)]
249#[cfg_attr(
250 any(not(feature = "csr"), feature = "ts"),
251 wasm_bindgen::prelude::wasm_bindgen(module = "/ftml-top.js")
252)]
253extern "C" {
289 #[wasm_bindgen::prelude::wasm_bindgen(js_name = "hasFtmlAttribute")]
290 fn has_ftml_attribute(node: &leptos::web_sys::Node) -> bool;
291}
292
293#[cfg(feature = "hydrate")]
294#[wasm_bindgen::prelude::wasm_bindgen(
295 inline_js = "export function init_flams_url() { window.FLAMS_SERVER_URL=\"\";}\ninit_flams_url();"
296)]
297extern "C" {
298 pub fn init_flams_url();
299}
300
301#[allow(clippy::missing_const_for_fn)]
302#[allow(unreachable_code)]
303#[allow(clippy::needless_return)]
304#[allow(unused_variables)]
305pub fn iterate(e: &Element) -> Option<impl FnOnce() -> AnyView> {
306 #[cfg(any(feature = "csr", feature = "hydrate"))]
308 {
309 if !has_ftml_attribute(e) {
310 return None;
312 }
313 let sig = expect_context::<RwSignal<DOMExtractor>>();
315 let r = sig.update_untracked(|extractor| {
316 let mut attrs = NodeAttrs::new(e);
317 RULES.applicable_rules(extractor, &mut attrs)
318 });
319 return r.map(|elements| {
320 let in_math = flams_web_utils::mathml::is(&e.tag_name()).is_some();
322 let orig = e.clone().into();
323 move || view!(<FTMLComponents orig elements in_math/>).into_any()
324 });
325 }
326 #[cfg(not(any(feature = "csr", feature = "hydrate")))]
327 {
328 None::<fn() -> AnyView>
329 }
330}
331
332#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
333#[cfg_attr(feature = "ts", derive(tsify_next::Tsify))]
334#[cfg_attr(feature = "ts", tsify(into_wasm_abi, from_wasm_abi))]
335#[serde(tag = "type")]
336pub enum FragmentKind {
337 Section(SectionLevel),
338 Paragraph(ParagraphKind),
339 Slide,
340 Problem {
341 is_sub_problem: bool,
342 is_autogradable: bool,
343 },
344}
345impl From<ParagraphKind> for FragmentKind {
346 fn from(value: ParagraphKind) -> Self {
347 Self::Paragraph(value)
348 }
349}
350impl From<SectionLevel> for FragmentKind {
351 fn from(value: SectionLevel) -> Self {
352 Self::Section(value)
353 }
354}