ftml_viewer_components/components/
toc.rs

1//#![allow(non_local_definitions)]
2
3use crate::components::navigation::NavElems;
4use flams_ontology::{
5    narration::paragraphs::ParagraphKind,
6    uris::{DocumentElementURI, DocumentURI, Name, NarrativeURI},
7};
8use flams_utils::{time::Timestamp, CSS};
9use flams_web_utils::do_css;
10use leptos::{
11    either::{Either, EitherOf4},
12    prelude::*,
13};
14
15#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
16#[cfg_attr(feature = "ts", derive(tsify_next::Tsify))]
17/// A section that has been "covered" at the specified timestamp; will be marked accordingly
18/// in the TOC.
19pub struct Gotto {
20    pub uri: DocumentElementURI,
21    #[serde(default)]
22    pub timestamp: Option<Timestamp>,
23}
24
25#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
26#[cfg_attr(feature = "ts", derive(tsify_next::Tsify))]
27#[serde(tag = "type")]
28/// An entry in a table of contents. Either:
29/// 1. a section; the title is assumed to be an HTML string, or
30/// 2. an inputref to some other document; the URI is the one for the
31///    inputref itself; not the referenced Document. For the TOC,
32///    which document is inputrefed is actually irrelevant.
33pub enum TOCElem {
34    /// A section; the title is assumed to be an HTML string
35    Section {
36        title: Option<String>,
37        uri: DocumentElementURI,
38        id: String,
39        children: Vec<TOCElem>,
40    },
41    SkippedSection {
42        children: Vec<TOCElem>,
43    },
44    /// An inputref to some other document; the URI is the one for the
45    /// referenced Document.
46    Inputref {
47        uri: DocumentURI,
48        title: Option<String>,
49        id: String,
50        children: Vec<TOCElem>,
51    },
52    Paragraph {
53        //uri:DocumentElementURI,
54        styles: Vec<Name>,
55        kind: ParagraphKind,
56    },
57    Slide, //{uri:DocumentElementURI}
58}
59
60pub trait TOCIter<'a> {
61    fn elem_iter(&'a self) -> std::slice::Iter<'a, TOCElem>;
62    fn iter_elems(&'a self) -> impl Iterator<Item = &'a TOCElem> {
63        struct TOCIterator<'b> {
64            curr: std::slice::Iter<'b, TOCElem>,
65            stack: Vec<std::slice::Iter<'b, TOCElem>>,
66        }
67        impl<'b> Iterator for TOCIterator<'b> {
68            type Item = &'b TOCElem;
69            fn next(&mut self) -> Option<Self::Item> {
70                loop {
71                    if let Some(elem) = self.curr.next() {
72                        let children: &'b [_] = match elem {
73                            TOCElem::Section { children, .. }
74                            | TOCElem::Inputref { children, .. }
75                            | TOCElem::SkippedSection { children } => children,
76                            _ => return Some(elem),
77                        };
78                        self.stack
79                            .push(std::mem::replace(&mut self.curr, children.iter()));
80                        return Some(elem);
81                    } else if let Some(s) = self.stack.pop() {
82                        self.curr = s;
83                    } else {
84                        return None;
85                    }
86                }
87            }
88        }
89        TOCIterator {
90            curr: self.elem_iter(),
91            stack: Vec::new(),
92        }
93    }
94    fn do_titles(&'a self) {
95        NavElems::update_untracked(|nav| {
96            for e in self.iter_elems() {
97                if let TOCElem::Inputref {
98                    title: Some(title),
99                    uri,
100                    ..
101                } = e
102                {
103                    nav.set_title(uri.clone(), title.clone());
104                }
105            }
106            nav.initialized.set(true);
107        });
108    }
109}
110impl<'a, A> TOCIter<'a> for &'a A
111where
112    A: std::ops::Deref<Target = [TOCElem]>,
113{
114    #[inline]
115    fn elem_iter(&'a self) -> std::slice::Iter<'a, TOCElem> {
116        self.deref().iter()
117    }
118}
119impl<'a> TOCIter<'a> for &'a [TOCElem] {
120    #[inline]
121    fn elem_iter(&'a self) -> std::slice::Iter<'a, TOCElem> {
122        self.iter()
123    }
124}
125
126impl TOCElem {
127    fn into_view(self, gottos: &mut Gottos) -> impl IntoView + use<> {
128        use flams_web_utils::components::{AnchorLink, Header};
129        use leptos_posthoc::DomStringCont;
130        let style = if gottos.current.is_some() {
131            "background-color:var(--colorPaletteYellowBorder1);"
132        } else {
133            ""
134        };
135        let after = gottos.current.as_ref().and_then(|e| e.timestamp).map(|ts| {
136            view! {
137                <sup><i>" Covered: "{ts.into_date().to_string()}</i></sup>
138            }
139        });
140        match self {
141            Self::Section {
142                title: Some(title),
143                id,
144                children,
145                uri,
146                ..
147            } => {
148                gottos.next(&uri);
149                let id = format!("#{id}");
150                let ch = children
151                    .into_iter()
152                    .map(|e| e.into_view(gottos))
153                    .collect_view();
154                Some(Either::Left(view! {
155                  <AnchorLink href=id>
156                    <Header slot>
157                      <div style=style><DomStringCont html=title cont=crate::iterate/>{after}</div>
158                    </Header>
159                    {ch}
160                  </AnchorLink>
161                }))
162            }
163            Self::Section {
164                title: None,
165                children,
166                uri,
167                ..
168            } => {
169                gottos.next(&uri);
170                Some(Either::Right(
171                    children
172                        .into_iter()
173                        .map(|e| e.into_view(gottos))
174                        .collect_view()
175                        .into_any(),
176                ))
177            }
178            Self::Inputref { children, .. } | Self::SkippedSection { children } => {
179                Some(Either::Right(
180                    children
181                        .into_iter()
182                        .map(|e| e.into_view(gottos))
183                        .collect_view()
184                        .into_any(),
185                ))
186            }
187            _ => None,
188        }
189    }
190}
191
192struct Gottos {
193    current: Option<Gotto>,
194    iter: std::vec::IntoIter<Gotto>,
195}
196impl Gottos {
197    fn next(&mut self, uri: &DocumentElementURI) {
198        if let Some(c) = self.current.as_ref() {
199            if c.uri == *uri {
200                loop {
201                    self.current = self.iter.next();
202                    if let Some(c) = &self.current {
203                        if c.uri != *uri {
204                            break;
205                        }
206                    } else {
207                        break;
208                    }
209                }
210            }
211        }
212    }
213}
214
215#[component]
216pub fn Toc(
217    #[prop(optional)] css: Vec<CSS>,
218    toc: Vec<TOCElem>,
219    mut gottos: Vec<Gotto>,
220) -> impl IntoView {
221    use flams_web_utils::components::Anchor;
222    use thaw::Scrollbar;
223    for css in css {
224        do_css(css);
225    }
226    //let max = with_context::<SectionCounters>(|ctrs| ctrs.max).unwrap_or(SectionLevel::Section);
227
228    gottos.retain(|e| {
229        toc.as_slice().iter_elems().any(|s| {
230            if let TOCElem::Section { uri, .. } = s {
231                *uri == e.uri
232            } else {
233                false
234            }
235        })
236    });
237    //gottos.sort_by_key(|k| k.timestamp.unwrap_or_default());
238    let mut gottos = gottos.into_iter();
239    let current = gottos.next();
240    let mut gottos = Gottos {
241        current,
242        iter: gottos,
243    };
244    view! {
245      //<div><Scrollbar style="max-height: 400px;">
246      <Anchor>{
247        toc.into_iter().map(|e| e.into_view(&mut gottos)).collect_view()
248      }</Anchor>
249      //</Scrollbar></div>
250    }
251}
252
253#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
254pub enum TOCSource {
255    #[default]
256    None,
257    Ready(Vec<TOCElem>),
258    //Loading(Resource<Result<(Vec<CSS>,Vec<TOCElem>),ServerFnError<String>>>),
259    Get,
260}
261
262#[allow(clippy::match_wildcard_for_single_variants)]
263pub fn do_toc<V: IntoView + 'static>(
264    toc: TOCSource,
265    gottos: Vec<Gotto>,
266    wrap: impl FnOnce(Option<AnyView>) -> V,
267) -> impl IntoView {
268    use TOCIter;
269
270    /* ------------------------
271    let gottos = vec![Gotto {
272        uri: "https://mathhub.info?a=courses/FAU/AI/course&p=course/sec&d=ml&l=en&e=section"
273            .parse()
274            .unwrap(),
275        timestamp: Some(Timestamp::now()),
276    }];
277    // ------------------------ */
278    match toc {
279        TOCSource::None => EitherOf4::A(wrap(None)),
280        TOCSource::Ready(toc) => {
281            let ctw = expect_context::<RwSignal<Option<Vec<TOCElem>>>>();
282            ctw.set(Some(toc.clone()));
283            EitherOf4::B(view! {
284                {toc.as_slice().do_titles()}
285                {wrap(Some(view!(<Toc toc gottos/>).into_any()))}
286            })
287        }
288        TOCSource::Get => match expect_context() {
289            NarrativeURI::Document(uri) => {
290                let r = Resource::new(
291                    || (),
292                    move |()| crate::remote::server_config.get_toc(uri.clone()),
293                );
294                EitherOf4::C(view! {
295                    {move || r.with(|r| if let Some(Ok((_,toc))) = r {
296                        toc.as_slice().do_titles();
297                        let ctw = expect_context::<RwSignal::<Option<Vec<TOCElem>>>>();
298                        ctw.set(Some(toc.clone()));
299                    })}
300                    {wrap(Some((move || r.get().map_or_else(
301                        || Either::Left(view!(<flams_web_utils::components::Spinner/>)),
302                        |r| Either::Right(match r {
303                            Ok((css,toc)) => {
304                                for c in css { do_css(c); }
305                                Some(view!(<Toc toc gottos=gottos.clone()/>))
306                            }
307                            Err(e) => {
308                                tracing::error!(e);
309                                None
310                            }
311                        })
312                    )).into_any()))}
313                })
314            }
315            _ => EitherOf4::D(wrap(None)),
316        },
317    }
318}