ftml_viewer_components/components/
toc.rs1use 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))]
17pub 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")]
28pub enum TOCElem {
34 Section {
36 title: Option<String>,
37 uri: DocumentElementURI,
38 id: String,
39 children: Vec<TOCElem>,
40 },
41 SkippedSection {
42 children: Vec<TOCElem>,
43 },
44 Inputref {
47 uri: DocumentURI,
48 title: Option<String>,
49 id: String,
50 children: Vec<TOCElem>,
51 },
52 Paragraph {
53 styles: Vec<Name>,
55 kind: ParagraphKind,
56 },
57 Slide, }
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 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 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 <Anchor>{
247 toc.into_iter().map(|e| e.into_view(&mut gottos)).collect_view()
248 }</Anchor>
249 }
251}
252
253#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
254pub enum TOCSource {
255 #[default]
256 None,
257 Ready(Vec<TOCElem>),
258 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 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}