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 >
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"#;