1use flams_ontology::{
2 content::terms::ArgMode,
3 uris::{ArchiveURITrait, ContentURI, DocumentElementURI, URIWithLanguage, URI},
4};
5use flams_web_utils::{
6 components::{Popover, PopoverSize},
7 do_css, inject_css,
8};
9use ftml_extraction::open::terms::{OpenArg, OpenTerm, PreVar, VarOrSym};
10use leptos::{
11 context::Provider,
12 either::{Either, EitherOf3},
13 prelude::*,
14};
15use leptos_posthoc::OriginalNode;
16
17use crate::{
18 components::{IntoLOs, LOs},
19 config::FTMLConfig,
20 ts::FragmentContinuation,
21 FTMLString,
22};
23
24#[cfg(feature = "omdoc")]
25enum DomTermArgs {
26 Open(Vec<Option<(ArgMode, either::Either<String, Vec<Option<String>>>)>>),
27 Closed(Vec<(ArgMode, either::Either<String, Vec<String>>)>),
28}
29
30#[derive(Clone)]
31pub(super) struct InTermState {
32 owner: VarOrSym,
33 is_hovered: RwSignal<bool>,
34 #[cfg(feature = "omdoc")]
35 args: RwSignal<DomTermArgs>,
36 replacable: bool,
38}
39
40#[derive(Clone)]
41struct SkipOne;
42
43impl InTermState {
44 fn new(owner: VarOrSym) -> Self {
45 Self {
46 owner,
47 is_hovered: RwSignal::new(false),
48 #[cfg(feature = "omdoc")]
49 args: RwSignal::new(DomTermArgs::Open(Vec::new())),
50 replacable: false,
52 }
53 }
54}
55
56#[cfg(feature = "omdoc")]
57mod term_replacing {
58 use super::super::do_components;
59 use super::{DomTermArgs, InTermState};
60 use crate::components::terms::SkipOne;
61 use flams_ontology::{
62 content::terms::ArgMode,
63 narration::notations::{PresentationError, PresenterArgs},
64 uris::{DocumentElementURI, URI},
65 };
66 use ftml_extraction::prelude::FTMLElements;
67 use leptos::{context::Provider, prelude::*};
68 use leptos_posthoc::{DomStringContMath, OriginalNode};
69
70 pub(crate) const DO_REPLACEMENTS: bool = true;
71
72 #[derive(Copy, Clone)]
73 struct ArgPres(RwSignal<DomTermArgs>);
74 impl PresenterArgs<String> for ArgPres {
75 fn single(
76 &self,
77 idx: u8,
78 _mode: ArgMode,
79 out: &mut String,
80 ) -> Result<(), PresentationError> {
81 self.0.with_untracked(|args| {
82 let DomTermArgs::Closed(v) = args else {
83 unreachable!()
84 };
85 let Some((_, either::Left(s))) = v.get((idx - 1) as usize) else {
86 return Err(PresentationError::ArgumentMismatch);
87 };
88 out.push_str(s);
89 Ok(())
90 })
91 }
92 fn sequence(
93 &self,
94 idx: u8,
95 _mode: ArgMode,
96 ) -> std::result::Result<
97 impl Iterator<Item = impl FnOnce(&mut String) -> Result<(), PresentationError>>,
98 PresentationError,
99 > {
100 self.0.with_untracked(|args| {
101 let DomTermArgs::Closed(v) = args else {
102 unreachable!()
103 };
104 let v = match v.get((idx - 1) as usize) {
105 None => return Err(PresentationError::ArgumentMismatch),
106 Some((_, either::Left(s))) => vec![s.clone()],
107 Some((_, either::Right(v))) => v.clone(),
108 };
109 let ret = v.into_iter().map(|s: String| {
110 move |out: &mut String| {
111 out.push_str(&s);
112 Ok(())
113 }
114 });
115 Ok(ret)
116 })
117 }
118 }
119
120 pub(super) fn replacable(
121 mut head: InTermState,
122 elements: FTMLElements,
123 orig: OriginalNode,
124 is_var: bool,
125 uri: URI,
126 notation_signal: RwSignal<Option<DocumentElementURI>>,
127 ) -> impl IntoView {
128 let args = head.args;
129 let parsed = RwSignal::new(false);
130
131 head.replacable = true;
132 let _ = Effect::new(move || {
135 if parsed.get() {
136 if args.with_untracked(|e| matches!(e, DomTermArgs::Open(_))) {
137 args.update(|args| {
138 let DomTermArgs::Open(v) = args else {
139 unreachable!()
140 };
141 let mut v = std::mem::take(v).into_iter();
143 let mut ret = Vec::new();
144 while let Some(Some((mode, s))) = v.next() {
145 match (mode, s) {
146 (ArgMode::Normal | ArgMode::Binding, either::Left(s)) => {
147 ret.push((mode, either::Left(s)))
148 }
149 (
150 ArgMode::Sequence | ArgMode::BindingSequence,
151 either::Right(v),
152 ) => {
153 let mut r = Vec::new();
154 let mut iter = v.into_iter();
155 while let Some(Some(s)) = iter.next() {
156 r.push(s);
157 }
158 for a in iter {
159 if a.is_some() {
160 tracing::error!(
161 "Missing argument in associative argument list"
162 );
163 }
164 }
165 ret.push((mode, either::Right(r)));
166 }
167 (ArgMode::Sequence | ArgMode::BindingSequence, either::Left(s)) => {
168 ret.push((mode, either::Right(vec![s])))
169 }
170 (ArgMode::Normal | ArgMode::Binding, _) => tracing::error!(
171 "Argument of mode {mode:?} has multiple entries"
172 ),
173 }
174 }
175 for e in v {
176 if e.is_some() {
177 tracing::error!("Missing argument in term");
178 }
179 }
180 *args = DomTermArgs::Closed(ret);
182 });
183 }
184 }
185 });
186
187 let substituted = RwSignal::new(false);
188
189 let oclone = orig; view! {<Provider value=Some(head)>{move || {
191 macro_rules! orig {
192 () => {{
193 substituted.update_untracked(|v| *v = false);
194 let o = oclone.deep_clone();
195 let r = leptos::either::EitherOf3::A(
196 do_components::<true>(0,elements.clone(),o).into_any()
198 );
200 parsed.set(true);
201 r
202 }};
203 }
204 if let Some(u) = notation_signal.get() {
205 if false {let rf = NodeRef::new();
207 let _ = Effect::new(move ||
208 if rf.get().is_some() {
209 substituted.set(false);
210 }
211 );
212 return leptos::either::EitherOf3::B({
213 view!(<mspace node_ref=rf style="display:content;"/>)
214 });
220 }
221
222 let Some(is_op) = args.with(|v| {
223 let DomTermArgs::Closed(v) = v else {
224 return None
225 };
226 Some(v.is_empty())
227 }) else {
228 return orig!();
229 };
230 let termstr = match (is_op,is_var) {
231 (true,true) => "OMV",
232 (true,_) => "OMID",
233 _ => "OMA"
234 };
235 let Some(notation) = crate::remote::server_config.get_notation(&u) else {
236 tracing::error!("Notation {u} not found");
237 return orig!()
238 };
239 let args = ArgPres(args);
241 let mut html = String::new();
242 if let Err(e) = notation.apply_cont(&mut html,None,termstr,&uri,false,&args) {
243 tracing::error!("Failed to apply notation {u}: {e}");
244 orig!()
245 } else {
246 substituted.update_untracked(|v| *v = true);
248 leptos::either::EitherOf3::C(view!{<Provider value=Some(SkipOne)>
249 <DomStringContMath html cont=crate::iterate class="ftml-comp-replaced"/>
251 </Provider>})
253 }
254 } else {
255 orig!()
256 }
257 }}</Provider>}
258 }
259}
260
261#[derive(Clone)]
262pub(super) struct DisablePopover;
263
264#[cfg(feature = "omdoc")]
265pub(super) fn math_term(
266 skip: usize,
267 mut elements: ftml_extraction::prelude::FTMLElements,
268 orig: OriginalNode,
269 t: OpenTerm,
270) -> impl IntoView {
271 use crate::config::FTMLConfig;
272
273 if term_replacing::DO_REPLACEMENTS {
274 Either::Left({
275 if use_context::<Option<SkipOne>>().flatten().is_some() {
276 tracing::debug!("Skipping");
277 let value: Option<SkipOne> = None;
278 EitherOf3::A(
279 view!(<Provider value>{super::do_components::<true>(skip+1,elements,orig).into_any()}</Provider>),
280 )
281 } else {
282 let head = InTermState::new(t.take_head());
283 let subst = use_context::<DisablePopover>().is_none();
284 if subst {
285 let uri = match &head.owner {
286 VarOrSym::S(s @ ContentURI::Symbol(_)) => {
287 Some((false, URI::Content(s.clone())))
288 }
289 VarOrSym::V(PreVar::Resolved(v)) => {
290 Some((true, URI::Narrative(v.clone().into())))
291 }
292 _ => None,
293 };
294 let notation_signal = uri
295 .as_ref()
296 .map(|(_, uri)| expect_context::<FTMLConfig>().get_forced_notation(&uri));
297 if let Some(notation_signal) = notation_signal {
298 let Some((is_var, uri)) = uri else {
299 unreachable!()
300 };
301 elements.elems.truncate(elements.elems.len() - (skip + 1));
303 return Either::Left(EitherOf3::C(term_replacing::replacable(
304 head,
305 elements,
306 orig,
307 is_var,
308 uri,
309 notation_signal,
310 )));
311 }
312 }
313
314 EitherOf3::B(
315 view!(<Provider value=Some(head)>{super::do_components::<true>(skip+1,elements,orig).into_any()}</Provider>),
316 )
317 }
318 })
319 } else {
320 Either::Right(do_term::<_, true>(t, move || {
321 super::do_components::<true>(skip + 1, elements, orig).into_any()
322 }))
323 }
324}
325
326pub(super) fn do_term<V: IntoView + 'static, const MATH: bool>(
327 t: OpenTerm,
328 children: impl FnOnce() -> V + Send + 'static,
329) -> impl IntoView + 'static {
330 let head = InTermState::new(t.take_head());
331 view! {
332 <Provider value=Some(head)>{
333 children()
334 }</Provider>
335 }
336}
337
338pub(super) fn do_definiendum<V: IntoView + 'static, const MATH: bool>(
339 children: impl FnOnce() -> V + Send + 'static,
340) -> impl IntoView {
341 let highlight: RwSignal<crate::HighlightOption> = expect_context();
342 let cls = Memo::new(move |_| match highlight.get() {
343 crate::HighlightOption::Colored | crate::HighlightOption::None => "ftml-def-comp",
344 crate::HighlightOption::Subtle => "ftml-def-comp-subtle",
345 crate::HighlightOption::Off => "",
346 });
347 if MATH {
348 leptos::either::Either::Left(view! {
349 <mrow class=cls>{children()}</mrow>
350 })
351 } else {
352 leptos::either::Either::Right(view! {
353 <span class=cls>{children()}</span>
354 })
355 }
356}
357
358pub(super) fn do_comp<V: IntoView + 'static, const MATH: bool>(
359 is_defi: bool,
360 mut children: impl FnMut() -> V + Send + 'static,
361) -> impl IntoView {
362 use crate::HighlightOption as HL;
363 use flams_web_utils::components::PopoverTrigger;
364 let in_term = use_context::<Option<InTermState>>().flatten();
366 if let Some(in_term) = in_term {
367 let is_hovered = in_term.is_hovered;
368 let is_var = matches!(in_term.owner, VarOrSym::V(_));
370 let highlight: RwSignal<HL> = expect_context();
371 let class =
372 Memo::new(
373 move |_| match (is_defi, is_hovered.get(), is_var, highlight.get()) {
374 (_, false, true, _) => "ftml-var-comp".to_string(),
375 (_, true, true, _) => "ftml-var-comp ftml-comp-hover".to_string(),
376 (true, false, _, HL::Colored | HL::None) => "ftml-def-comp".to_string(),
377 (true, false, _, HL::Subtle) => "ftml-def-comp-subtle".to_string(),
378 (true, true, _, HL::Colored | HL::None) => {
379 "ftml-def-comp ftml-comp-hover".to_string()
380 }
381 (true, true, _, HL::Subtle) => {
382 "ftml-def-comp-subtle ftml-comp-hover".to_string()
383 }
384 (_, false, false, HL::Colored | HL::None) => "ftml-comp".to_string(),
385 (_, false, false, HL::Subtle) => "ftml-comp-subtle".to_string(),
386 (_, true, false, HL::Subtle) => "ftml-comp-subtle ftml-comp-hover".to_string(),
387 (_, true, false, HL::Colored | HL::None) => {
388 "ftml-comp ftml-comp-hover".to_string()
389 }
390 (_, false, _, HL::Off) => "".to_string(),
391 (_, true, _, HL::Off) => "ftml-comp-hover".to_string(),
392 },
393 );
394 let do_popover = || use_context::<DisablePopover>().is_none();
395 let s = in_term.owner;
396 if do_popover() {
399 let ocp = expect_context::<crate::config::FTMLConfig>().get_on_click(&s);
400 let top_class = Memo::new(move |_| {
401 if is_hovered.get() {
402 "ftml-symbol-hover ftml-symbol-hover-hovered".to_string()
403 } else {
404 "ftml-symbol-hover ftml-symbol-hover-hidden".to_string()
405 }
406 });
407 let none: Option<FragmentContinuation> = None;
408 Either::Left(view!(
410 <Provider value=none>
411 <Popover class=top_class size=PopoverSize::Small
413 on_click_signal=ocp
414 on_open=move || is_hovered.set(true)
415 on_close=move || is_hovered.set(false)
416 >
417 <PopoverTrigger slot>{children().add_any_attr(leptos::tachys::html::class::class(move || class))
419 }</PopoverTrigger>
421 {match s {
424 VarOrSym::V(v) => EitherOf3::A(do_var_hover(v)),
425 VarOrSym::S(ContentURI::Symbol(s)) => EitherOf3::B(crate::remote::get!(definition(s.clone()) = (css,s) => {
426 for c in css { do_css(c); }
427 Some(view!(
428 <div style="color:black;background-color:white;padding:3px;max-width:600px;">
429 <FTMLString html=s/>
430 </div>
431 ))
432 })),
433 VarOrSym::S(ContentURI::Module(m)) =>
434 EitherOf3::C(view!{<div>"Module" {m.name().last_name().to_string()}</div>}),
435 }}</Popover>
437 </Provider>
438 ))
439 } else {
440 Either::Right(children())
441 }
442 } else {
443 Either::Right(children())
444 }
445}
446
447fn do_var_hover(v: PreVar) -> impl IntoView {
448 #[cfg(feature = "omdoc")]
449 match v {
450 PreVar::Unresolved(n) => leptos::either::Either::Left(
451 view! {<span>"Variable "{n.last_name().to_string()}</span>},
452 ),
453 PreVar::Resolved(u) => {
454 let name = u.name().last_name().to_string();
455 leptos::either::Either::Right(super::omdoc::doc_elem_name(u, Some("variable"), name))
456 }
457 }
458 #[cfg(not(feature = "omdoc"))]
459 view! {<span>"Variable "{v.name().last_name().to_string()}</span>}
460}
461
462#[allow(clippy::too_many_lines)]
463pub fn do_onclick(uri: VarOrSym) -> impl IntoView {
464 use thaw::{Combobox, ComboboxOption, ComboboxOptionGroup, Divider};
465 #[cfg(feature = "omdoc")]
466 let uriclone = uri.clone();
467 let s = match uri {
468 VarOrSym::V(v) => {
469 return EitherOf3::A(view! {<span>"Variable "{v.name().last_name().to_string()}</span>})
470 }
471 VarOrSym::S(ContentURI::Module(m)) => {
472 return EitherOf3::B(view! {<div>"Module" {m.name().last_name().to_string()}</div>})
473 }
474 VarOrSym::S(ContentURI::Symbol(s)) => s,
475 };
476 let name = s.name().last_name().to_string();
477
478 EitherOf3::C(crate::remote::get!(get_los(s.clone(),false) = v => {
479 let LOs {definitions,examples,..} = v.lo_sort();
480 let ex_off = definitions.len();
481 let selected = RwSignal::new(definitions.first().map(|_| "0".to_string()));
482 let definitions = StoredValue::new(definitions);
483 let examples = StoredValue::new(examples);
484 view!{
485 <div style="display:flex;flex-direction:row;">
486 <div style="font-weight:bold;">{name.clone()}</div>
487 <div style="margin-left:auto;"><Combobox selected_options=selected placeholder="Select Definition or Example">
488 <ComboboxOptionGroup label="Definitions">{
489 definitions.with_value(|v| v.iter().enumerate().map(|(i,d)| {
490 let line = lo_line(d);
491 let value = i.to_string();
492 view!{
493 <ComboboxOption text="" value>{line}</ComboboxOption>
494 }
495 }).collect_view())
496 }</ComboboxOptionGroup>
497 <ComboboxOptionGroup label="Examples">{
498 examples.with_value(|v| v.iter().enumerate().map(|(i,d)| {
499 let line = lo_line(d);
500 let value = (ex_off + i).to_string();
501 view!{
502 <ComboboxOption text="" value>{line}</ComboboxOption>
503 }
504 }).collect_view())
505 }</ComboboxOptionGroup>
506 </Combobox></div>
507 </div>
508 <div style="margin:5px;"><Divider/></div>
509 {move || {
510 let uri = selected.with(|s| s.as_ref().map(|s| {
511 let i: usize = s.parse().unwrap_or_else(|_| unreachable!());
512 if i < ex_off {
513 definitions.with_value(|v:&Vec<DocumentElementURI>| v.as_slice()[i].clone())
514 } else {
515 examples.with_value(|v:&Vec<DocumentElementURI>| v.as_slice()[i - ex_off].clone())
516 }
517 }));
518 uri.map(|uri| {
519 crate::remote::get!(paragraph(uri.clone()) = (_,css,html) => {
520 for c in css { do_css(c); }
521 let none: Option<FragmentContinuation> = None;
522 view!(<Provider value=none><div><FTMLString html=html/></div></Provider>)
523 })
524 })
525 }}
526 {#[cfg(feature="omdoc")]{
527 if term_replacing::DO_REPLACEMENTS {
528 let uri = match &uriclone {
529 VarOrSym::S(s@ContentURI::Symbol(_)) => Some((false,URI::Content(s.clone()))),
530 VarOrSym::V(PreVar::Resolved(v)) => Some((true,URI::Narrative(v.clone().into()))),
531 _ => None
532 };
533 uri.map(|(is_variable,uri)| {let uricl = uri.clone();crate::remote::get!(notations(uri.clone()) = v => {
534 if v.is_empty() { None } else {Some({
535 let uri = uricl.clone();
536 let notation_signal = expect_context::<FTMLConfig>().get_forced_notation(&uri);
537 let selected = RwSignal::new(if let Some(v) = notation_signal.get_untracked() {
539 v.to_string()
540 } else {
541 "None".to_string()
542 });
543 Effect::new(move || {
558 let Some(v) = selected.try_get() else {return};
559 if v == "None" { notation_signal.maybe_update(|f|
560 if f.is_some() {
561 *f = None; true
562 } else {false}
563 ); }
564 else {
565 let uri = v.parse().expect("This should be impossible");
566 notation_signal.maybe_update(|v| match v {
567 Some(e) if *e == uri => false,
568 _ => {
569 *v = Some(uri); true
570 }
571 })
572 }
573 });
574 let uri = uri.clone();
575 let v = v.clone();
576 Either::Right::<(),_>(view!{<div style="margin:5px;"><Divider/></div>
577 <div style="width:100%;"><div style="width:min-content;margin-left:auto;">
578 <Combobox selected_options=selected placeholder="Force Notation">
579 <ComboboxOption text="None" value="None">"None"</ComboboxOption>
580 {let uri = uri.clone();
581 v.into_iter().map(|(u,n)| {let html = n.display_ftml(false,is_variable,&uri).to_string();view!{
582 <ComboboxOption text="" value=u.to_string()>{
583 view!(
584 <Provider value=DisablePopover>
585 <crate::FTMLStringMath html/>
586 </Provider>
587 )
588 }</ComboboxOption>
589 }}).collect_view()
590 }
591 </Combobox>
592 </div></div>})
593 })}
595 })})
596 } else { None }
597 }}
598 }}))
599}
600
601pub fn lo_line(uri: &DocumentElementURI) -> impl IntoView + 'static {
602 let archive = uri.archive_id().to_string();
603 let name = uri.name().to_string();
604 let lang = uri.language().flag_svg();
605 view!(<div><span>"["{archive}"] "{name}" "</span><div style="display:contents;" inner_html=lang/></div>)
606}
607
608pub(super) fn do_arg<V: IntoView + 'static>(
609 orig: OriginalNode,
610 arg: OpenArg,
611 cont: impl FnOnce(OriginalNode) -> V + Send + 'static,
612) -> impl IntoView {
613 #[cfg(feature = "omdoc")]
614 {
615 use flams_ontology::ftml::FTMLKey;
616 let tm = use_context::<Option<InTermState>>().flatten();
617 if let Some(tm) = tm {
618 if tm.replacable {
619 tm.args.update_untracked(|args| {
620 if let DomTermArgs::Open(v) = args {
621 let (index, sub) = match arg.index {
622 either::Left(i) => ((i - 1) as usize, None),
623 either::Right((i, m)) => ((i - 1) as usize, Some((m - 1) as usize)),
624 };
625 if v.len() <= index {
626 v.resize(index + 1, None);
627 }
628 let entry = &mut v[index];
629 if let Some(sub) = sub {
630 if let (_, either::Right(subs)) =
631 entry.get_or_insert_with(|| (arg.mode, either::Right(Vec::new())))
632 {
633 if subs.len() <= sub {
634 subs.resize(sub + 1, None);
635 }
636 let entry = &mut subs[sub];
637 *entry = Some(orig.html_string());
638 } else {
639 tracing::error!("{} is not a list", FTMLKey::Arg.attr_name());
640 }
641 } else {
642 *entry = Some((arg.mode, either::Left(orig.html_string())));
643 }
644 }
645 })
646 }
647 } }
651
652 let value: Option<InTermState> = None;
653 let value_2: Option<SkipOne> = None;
654 view! {<Provider value><Provider value=value_2>{cont(orig)}</Provider></Provider>}
655}