Skip to main content

flams_flodown/
lib.rs

1#![allow(clippy::must_use_candidate)]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![recursion_limit = "256"]
4
5#[cfg(any(
6    all(feature = "ssr", feature = "hydrate", not(feature = "docs-only")),
7    not(any(feature = "ssr", feature = "hydrate"))
8))]
9compile_error!("exactly one of the features \"ssr\" or \"hydrate\" must be enabled");
10
11pub mod math;
12mod module_picker;
13
14use flams_router_base::maybe_lazy;
15use flams_router_content::Views;
16use ftml_backend::{FtmlBackend, GlobalBackend};
17use ftml_dom::{FtmlViews, utils::css::CssExt};
18use ftml_ontology::{
19    domain::{HasDeclarations, declarations::AnyDeclarationRef, modules::ModuleLike},
20    utils::Css,
21};
22use ftml_uris::{DocumentUri, Id, ModuleUri, SymbolUri};
23use leptos::prelude::*;
24
25maybe_lazy!(FloDownEditor = flodown_editor());
26
27//#[component]
28pub fn flodown_editor() -> AnyView {
29    #[cfg(feature = "hydrate")]
30    math::TeXClient::provide();
31
32    Css::Link("/rustex.css".to_string().into_boxed_str()).inject();
33    Css::Link(
34        "https://fonts.googleapis.com/css2?family=STIX+Two+Text"
35            .to_string()
36            .into_boxed_str(),
37    )
38    .inject();
39
40    let modules = RwSignal::new(rustc_hash::FxHashSet::default());
41    let symbols = RwSignal::new(rustc_hash::FxHashMap::default());
42    let action = Action::new(move |v: &rustc_hash::FxHashSet<_>| {
43        let v = v.iter().cloned().collect();
44        async move {
45            let syms = get_symbols(v).await;
46            symbols.set(syms);
47        }
48    });
49    let _ = Effect::new(move || {
50        modules.with(|modules| {
51            action.dispatch(modules.clone());
52        });
53    });
54
55    view! {
56        {editor(symbols)}
57        <div style="width:100%;display:flex;flex-direction:row;">
58            <div style="width:45%;border:1px solid black;">
59                <strong>Symbols:</strong>
60                <span>
61                {move ||
62                    symbols.with(|s| {
63                        let mut s = s.iter().collect::<Vec<_>>();
64                        s.sort_by_key(|(a,_)| *a);
65                        ftml_components::components::content::CommaSep("",
66                            s.into_iter().map(|(id,uri)| ftml_components::components::content::symbol_uri(id.to_string(), uri))
67                        ).into_view().attr("style", "display:inline;")
68                    }   )
69                }
70                </span>
71            </div>
72            <div style="width:45%">
73                {module_picker::picker(modules)}
74            </div>
75        </div>
76    }.into_any()
77}
78
79async fn get_symbols(mut todos: Vec<ModuleUri>) -> rustc_hash::FxHashMap<Id, SymbolUri> {
80    let mut dones = rustc_hash::FxHashSet::default();
81    let mut ret = rustc_hash::FxHashMap::default();
82    while let Some(next) = todos.pop() {
83        if dones.contains(&next) {
84            continue;
85        }
86        dones.insert(next.clone());
87        if let Ok(ModuleLike::Module(m)) = flams_router_content::backend::FtmlBackend::get()
88            .get_module(next)
89            .await
90        {
91            for d in m.declarations() {
92                match d {
93                    AnyDeclarationRef::Symbol(s) => {
94                        ret.insert(
95                            unsafe { s.uri.name().as_ref().parse().unwrap_unchecked() },
96                            s.uri.clone(),
97                        );
98                        if let Some(mac) = &s.data.macroname {
99                            ret.insert(mac.clone(), s.uri.clone());
100                        }
101                    }
102                    AnyDeclarationRef::MathStructure(s) => {
103                        ret.insert(
104                            unsafe { s.uri.name().as_ref().parse().unwrap_unchecked() },
105                            s.uri.clone(),
106                        );
107                        if let Some(mac) = &s.macroname {
108                            ret.insert(mac.clone(), s.uri.clone());
109                        }
110                    }
111                    AnyDeclarationRef::Import { uri: m, .. } => {
112                        todos.push(m.clone());
113                    }
114                    _ => (),
115                }
116            }
117        }
118    }
119    ret
120}
121
122fn editor(symbols: RwSignal<rustc_hash::FxHashMap<Id, SymbolUri>>) -> AnyView {
123    let csr = RwSignal::new(false);
124    #[cfg(feature = "hydrate")]
125    let _ = Effect::new(move || csr.set(true));
126    let checked = RwSignal::new(false);
127    let text = RwSignal::new(DEMO.to_string());
128
129    //ftml_components::config::FtmlConfig::set_toc_source(ftml_dom::structure::TocSource::None);
130    Views::setup_document(
131        DocumentUri::no_doc().clone(),
132        ftml_components::SidebarPosition::None,
133        false,
134        ftml_dom::toc::TocSource::None,
135        move || {
136            view! {
137                <div><input type="checkbox" on:change:target=move |ev| {
138                      checked.set(ev.target().checked());
139                }/>"LaTeX"</div>
140                <div style="width:100%;display:flex;flex-direction:row;">
141                    <textarea
142                        style="width:48%;max-width:48%;min-width:48%;min-height:200px;"
143                        on:input:target=move |ev| {
144                            text.set(ev.target().value());
145                        }
146                        prop:value=text
147                    />
148                    <div
149                        style="width:48%;max-width:48%;min-width:48%;text-align:left;border:1px solid black"
150                        //inner_html=move || text.with(|txt| flodown::to_html(txt))
151                    >
152                        {move ||
153                            if csr.get() {
154                                Some(if checked.get() {
155                                    view!(<pre>{text.with(|txt| flodown::to_latex(txt))}</pre>).into_any()
156                                } else {
157                                    md_html(text,symbols)
158                                })
159                            } else {
160                                None
161                            }
162                        }
163                    </div>
164                </div>
165            }.into_any()
166        },
167    )
168}
169
170fn md_html(
171    md: RwSignal<String>,
172    symbols: RwSignal<rustc_hash::FxHashMap<Id, SymbolUri>>,
173) -> AnyView {
174    let owner = leptos::prelude::Owner::current().expect("not in a reactive context");
175
176    let actual = RwSignal::new(String::new());
177    let signals = RwSignal::new(Vec::<(usize, RwSignal<Option<Result<String, String>>>)>::new());
178    Effect::new(move || {
179        #[cfg(feature = "hydrate")]
180        {
181            signals.update_untracked(Vec::clear);
182            math::TeXClient::reset();
183            let s = md.with(|txt| {
184                symbols.with(|symbols| {
185                    flodown::to_html_with_math_and_symbols(
186                        txt,
187                        symbols,
188                        |s, out| {
189                            use std::fmt::Write;
190                            let (i, rs) = owner.with(|| math::TeXClient::inline_math(s));
191                            signals.update_untracked(|v| v.push((i, rs)));
192                            let _ = write!(out, "<!--math{i}--> ...");
193                        },
194                        |s, out| {
195                            use std::fmt::Write;
196                            let (i, rs) = owner.with(|| math::TeXClient::block_math(s));
197                            signals.update_untracked(|v| v.push((i, rs)));
198                            let _ = write!(out, "<!--math{i}--> ...");
199                        },
200                    )
201                })
202            });
203            actual.set(s);
204            signals.notify();
205        }
206    });
207    Effect::new(move || {
208        #[cfg(feature = "hydrate")]
209        {
210            signals.with(|v| {
211                for (i, sig) in v {
212                    if let Some(v) = sig.get() {
213                        match v {
214                            Ok(s) => actual.update_untracked(|a| {
215                                *a = a.replace(&format!("<!--math{i}--> ..."), &s)
216                            }),
217                            Err(e) => actual.update_untracked(|a| {
218                                *a = a.replace(
219                                    &format!("<!--math{i}--> ..."),
220                                    &format!("<span style=\"background-color:red;\">{e}</span>"),
221                                );
222                            }),
223                        }
224                    }
225                }
226            });
227            actual.notify();
228        }
229    });
230    (move || actual.with(|txt| Views::render_ftml(txt.clone(), None))).into_any()
231}
232
233static DEMO: &str = r#"
234::: definition title="Gödel's Incompleteness Theorem"
235  foo @[sym](CS) @[sym](CS,computer science)
236  @[def](uncertain)
237:::
238
239@[definition](this is a @[sym](mind) inline definition for @[def](CS,computer science))
240
241a *b* **blubb** \*bla\* blubb _bla_ __blubb__ ~bla~ ^blubb^ ~~bla~~ ==blubb==
242$inline math$ and $$block math$$ and such, and `inline code` and
243```javascript
244some
245  code blocks
246    with
247  indentation
248```
249
250go visit [mathhub](https://mathhub.info)
251
252- foo
253  - bar
254
255- blubb
256
2571. btw
2581. this works
259  1. too
260  1. and this
2611) and this
262
263foo bar
264"#;