ftml_viewer_components/
lib.rs

1#![allow(clippy::must_use_candidate)]
2#![allow(clippy::module_name_repetitions)]
3#![cfg_attr(docsrs, feature(doc_auto_cfg))]
4//#![feature(generic_const_exprs)]
5//#![feature(let_chains)]
6#![recursion_limit = "256"]
7
8//mod popover;
9
10pub 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(optional)] problems:Option<ProblemOptions>,
85    #[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    //provide_context(problems.unwrap_or_default());
125    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    //let config = config::ServerConfig::clone_static();
142    //provide_context(config);
143    #[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    // ---- no-ops --------
226    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)]
253/*
254#[cfg_attr(all(not(feature="csr"),not(feature="ts")),wasm_bindgen::prelude::wasm_bindgen(module="/ftml-top.js"))]
255#[cfg_attr(feature="ts",wasm_bindgen::prelude::wasm_bindgen(inline_js = r#"
256export function hasFtmlAttribute(node) {
257  if (typeof window === "undefined") { return false; }
258  if (node.tagName.toLowerCase() === "img") {
259    // replace "srv:" by server url
260    const attributes = node.attributes;
261    for (let i = 0; i < attributes.length; i++) {
262        if (attributes[i].name === 'data-flams-src') {
263            const src = attributes[i].value;
264            node.setAttribute('src',src.replace('srv:', window.FLAMS_SERVER_URL));
265            break;
266        }
267    }
268  }
269  //if (node.tagName.toLowerCase() === "section") {return true}
270  const attributes = node.attributes;
271  for (let i = 0; i < attributes.length; i++) {
272      if (attributes[i].name.startsWith('data-ftml-')) {
273          return true;
274      }
275  }
276  return false;
277}
278
279if (typeof window !== "undefined") {
280  window.FLAMS_SERVER_URL = "";
281}
282export function setServerUrl(url) {
283  if (typeof window !== "undefined") { window.FLAMS_SERVER_URL = url; }
284  set_server_url(url);
285}
286"#))]
287 */
288extern "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    //tracing::info!("iterating {}", e.outer_html());
307    #[cfg(any(feature = "csr", feature = "hydrate"))]
308    {
309        if !has_ftml_attribute(e) {
310            //tracing::trace!("No attributes");
311            return None;
312        }
313        //tracing::trace!("Has ftml attributes");
314        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            //tracing::trace!("got elements: {elements:?}");
321            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}