md_tui/nodes/
root.rs

1use crate::search::{compare_heading, find_and_mark};
2
3use super::{
4    image::ImageComponent,
5    textcomponent::{TextComponent, TextNode},
6    word::{Word, WordType},
7};
8
9pub struct ComponentRoot {
10    file_name: Option<String>,
11    components: Vec<Component>,
12    is_focused: bool,
13}
14
15impl ComponentRoot {
16    #[must_use]
17    pub fn new(file_name: Option<String>, components: Vec<Component>) -> Self {
18        Self {
19            file_name,
20            components,
21            is_focused: false,
22        }
23    }
24
25    #[must_use]
26    pub fn children(&self) -> Vec<&Component> {
27        self.components.iter().collect()
28    }
29
30    pub fn children_mut(&mut self) -> Vec<&mut Component> {
31        self.components.iter_mut().collect()
32    }
33
34    #[must_use]
35    pub fn components(&self) -> Vec<&TextComponent> {
36        self.components
37            .iter()
38            .filter_map(|c| match c {
39                Component::TextComponent(comp) => Some(comp),
40                Component::Image(_) => None,
41            })
42            .collect()
43    }
44
45    pub fn components_mut(&mut self) -> Vec<&mut TextComponent> {
46        self.components
47            .iter_mut()
48            .filter_map(|c| match c {
49                Component::TextComponent(comp) => Some(comp),
50                Component::Image(_) => None,
51            })
52            .collect()
53    }
54
55    #[must_use]
56    pub fn file_name(&self) -> Option<&str> {
57        self.file_name.as_deref()
58    }
59
60    #[must_use]
61    pub fn words(&self) -> Vec<&Word> {
62        self.components
63            .iter()
64            .filter_map(|c| match c {
65                Component::TextComponent(comp) => Some(comp),
66                Component::Image(_) => None,
67            })
68            .flat_map(|c| c.content().iter().flatten())
69            .collect()
70    }
71
72    pub fn find_and_mark(&mut self, search: &str) {
73        let mut words = self
74            .components
75            .iter_mut()
76            .filter_map(|c| match c {
77                Component::TextComponent(comp) => Some(comp),
78                Component::Image(_) => None,
79            })
80            .flat_map(|c| c.words_mut())
81            .collect::<Vec<_>>();
82        find_and_mark(search, &mut words);
83    }
84
85    #[must_use]
86    pub fn search_results_heights(&self) -> Vec<usize> {
87        self.components
88            .iter()
89            .filter_map(|c| match c {
90                Component::TextComponent(comp) => Some(comp),
91                Component::Image(_) => None,
92            })
93            .flat_map(|c| {
94                let mut heights = c.selected_heights();
95                heights.iter_mut().for_each(|h| *h += c.y_offset() as usize);
96                heights
97            })
98            .collect()
99    }
100
101    pub fn clear(&mut self) {
102        self.file_name = None;
103        self.components.clear();
104    }
105
106    pub fn select(&mut self, index: usize) -> Result<u16, String> {
107        self.deselect();
108        self.is_focused = true;
109        let mut count = 0;
110        for comp in self.components.iter_mut().filter_map(|f| match f {
111            Component::TextComponent(comp) => Some(comp),
112            Component::Image(_) => None,
113        }) {
114            let link_inside_comp = index - count < comp.num_links();
115            if link_inside_comp {
116                comp.visually_select(index - count)?;
117                return Ok(comp.y_offset());
118            }
119            count += comp.num_links();
120        }
121        Err(format!("Index out of bounds: {index} >= {count}"))
122    }
123
124    pub fn deselect(&mut self) {
125        self.is_focused = false;
126        for comp in self.components.iter_mut().filter_map(|f| match f {
127            Component::TextComponent(comp) => Some(comp),
128            Component::Image(_) => None,
129        }) {
130            comp.deselect();
131        }
132    }
133
134    #[must_use]
135    pub fn find_footnote(&self, search: &str) -> String {
136        let footnote = self
137            .components
138            .iter()
139            .filter_map(|f| match f {
140                Component::TextComponent(text_component) => {
141                    if text_component.kind() == TextNode::Footnote {
142                        Some(text_component)
143                    } else {
144                        None
145                    }
146                }
147                Component::Image(_) => None,
148            })
149            .filter(|f| {
150                if let Some(foot_ref) = f.meta_info().iter().next() {
151                    foot_ref.content() == search
152                } else {
153                    false
154                }
155            })
156            .flat_map(|f| f.content().iter().flatten())
157            .filter(|f| f.kind() == WordType::Footnote)
158            .map(Word::content)
159            .collect::<String>();
160
161        if footnote.is_empty() {
162            String::from("Footnote not found")
163        } else {
164            footnote
165        }
166    }
167
168    #[must_use]
169    pub fn link_index_and_height(&self) -> Vec<(usize, u16)> {
170        let mut indexes = Vec::new();
171        let mut count = 0;
172        self.components
173            .iter()
174            .filter_map(|f| match f {
175                Component::TextComponent(comp) => Some(comp),
176                Component::Image(_) => None,
177            })
178            .for_each(|comp| {
179                let height = comp.y_offset();
180                comp.content().iter().enumerate().for_each(|(index, row)| {
181                    row.iter().for_each(|c| {
182                        if matches!(
183                            c.kind(),
184                            WordType::Link | WordType::Selected | WordType::FootnoteInline
185                        ) {
186                            indexes.push((count, height + index as u16));
187                            count += 1;
188                        }
189                    });
190                });
191            });
192
193        indexes
194    }
195
196    /// Sets the y offset of the components
197    pub fn set_scroll(&mut self, scroll: u16) {
198        let mut y_offset = 0;
199        for component in &mut self.components {
200            component.set_y_offset(y_offset);
201            component.set_scroll_offset(scroll);
202            y_offset += component.height();
203        }
204    }
205
206    pub fn heading_offset(&self, heading: &str) -> Result<u16, String> {
207        let mut y_offset = 0;
208        for component in &self.components {
209            match component {
210                Component::TextComponent(comp) => {
211                    if comp.kind() == TextNode::Heading
212                        && compare_heading(&heading[1..], comp.content())
213                    {
214                        return Ok(y_offset);
215                    }
216                    y_offset += comp.height();
217                }
218                Component::Image(e) => y_offset += e.height(),
219            }
220        }
221        Err(format!("Heading not found: {heading}"))
222    }
223
224    /// Return the content of the components, where each element a line
225    #[must_use]
226    pub fn content(&self) -> Vec<String> {
227        self.components()
228            .iter()
229            .flat_map(|c| c.content_as_lines())
230            .collect()
231    }
232
233    #[must_use]
234    pub fn selected(&self) -> &str {
235        let block = self
236            .components
237            .iter()
238            .filter_map(|f| match f {
239                Component::TextComponent(comp) => Some(comp),
240                Component::Image(_) => None,
241            })
242            .find(|c| c.is_focused())
243            .unwrap();
244        block.highlight_link().unwrap()
245    }
246
247    #[must_use]
248    pub fn selected_underlying_type(&self) -> WordType {
249        let selected = self
250            .components
251            .iter()
252            .filter_map(|f| match f {
253                Component::TextComponent(comp) => Some(comp),
254                Component::Image(_) => None,
255            })
256            .find(|c| c.is_focused())
257            .unwrap()
258            .content()
259            .iter()
260            .flatten()
261            .filter(|c| c.kind() == WordType::Selected)
262            .collect::<Vec<_>>();
263
264        selected.first().unwrap().previous_type()
265    }
266
267    /// Transforms the content of the components to fit the given width
268    pub fn transform(&mut self, width: u16) {
269        for component in self.components_mut() {
270            component.transform(width);
271        }
272    }
273
274    /// Because of the parsing, every table has a missing newline at the end
275    #[must_use]
276    pub fn add_missing_components(self) -> Self {
277        let mut components = Vec::new();
278        let mut iter = self.components.into_iter().peekable();
279        while let Some(component) = iter.next() {
280            let kind = component.kind();
281            components.push(component);
282            if let Some(next) = iter.peek()
283                && kind != TextNode::LineBreak
284                && next.kind() != TextNode::LineBreak
285            {
286                components.push(Component::TextComponent(TextComponent::new(
287                    TextNode::LineBreak,
288                    Vec::new(),
289                )));
290            }
291        }
292        Self {
293            file_name: self.file_name,
294            components,
295            is_focused: self.is_focused,
296        }
297    }
298
299    #[must_use]
300    pub fn height(&self) -> u16 {
301        self.components.iter().map(ComponentProps::height).sum()
302    }
303
304    #[must_use]
305    pub fn num_links(&self) -> usize {
306        self.components
307            .iter()
308            .filter_map(|f| match f {
309                Component::TextComponent(comp) => Some(comp),
310                Component::Image(_) => None,
311            })
312            .map(TextComponent::num_links)
313            .sum()
314    }
315}
316
317pub trait ComponentProps {
318    fn height(&self) -> u16;
319    fn set_y_offset(&mut self, y_offset: u16);
320    fn set_scroll_offset(&mut self, scroll: u16);
321    fn kind(&self) -> TextNode;
322}
323
324pub enum Component {
325    TextComponent(TextComponent),
326    Image(ImageComponent),
327}
328
329impl From<TextComponent> for Component {
330    fn from(comp: TextComponent) -> Self {
331        Component::TextComponent(comp)
332    }
333}
334
335impl ComponentProps for Component {
336    fn height(&self) -> u16 {
337        match self {
338            Component::TextComponent(comp) => comp.height(),
339            Component::Image(comp) => comp.height(),
340        }
341    }
342
343    fn set_y_offset(&mut self, y_offset: u16) {
344        match self {
345            Component::TextComponent(comp) => comp.set_y_offset(y_offset),
346            Component::Image(comp) => comp.set_y_offset(y_offset),
347        }
348    }
349
350    fn set_scroll_offset(&mut self, scroll: u16) {
351        match self {
352            Component::TextComponent(comp) => comp.set_scroll_offset(scroll),
353            Component::Image(comp) => comp.set_scroll_offset(scroll),
354        }
355    }
356
357    fn kind(&self) -> TextNode {
358        match self {
359            Component::TextComponent(comp) => comp.kind(),
360            Component::Image(comp) => comp.kind(),
361        }
362    }
363}