flams_flodown/
lib.rs

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