Skip to main content

wiki_tui/renderer/
default_renderer.rs

1use ratatui::style::{Color, Modifier, Style};
2use textwrap::wrap_algorithms::{wrap_optimal_fit, Penalties};
3use tracing::warn;
4use wiki_api::{
5    document::{Data, Document, HeaderKind, Node, UnsupportedElement},
6    page::Link,
7};
8
9use crate::renderer::Word;
10
11use super::RenderedDocument;
12
13const DISAMBIGUATION_PADDING: u8 = 1;
14const DISAMBIGUATION_PREFIX: char = '|';
15
16const BLOCKQUOTE_PADDING: u8 = 4;
17
18const LIST_PADDING: u8 = 1;
19const LIST_PREFIX: char = '-';
20
21struct Renderer {
22    rendered_lines: Vec<Vec<Word>>,
23    links: Vec<(usize, usize)>,
24
25    current_line: Vec<Word>,
26    width: u16,
27
28    text_style: Style,
29
30    left_padding: u8,
31    prefix: Option<char>,
32}
33
34impl<'a> Renderer {
35    fn render_document(document: &'a Document, width: u16) -> RenderedDocument {
36        if document.nodes.is_empty() {
37            warn!("document contains no nodes, aborting the render");
38            return RenderedDocument {
39                lines: Vec::new(),
40                links: Vec::new(),
41            };
42        }
43
44        let mut renderer = Renderer {
45            rendered_lines: Vec::new(),
46            links: Vec::new(),
47
48            current_line: Vec::new(),
49            width,
50
51            text_style: Style::default(),
52
53            left_padding: 0,
54            prefix: None,
55        };
56
57        renderer.render_node(document.nth(0).unwrap());
58
59        RenderedDocument {
60            lines: renderer.rendered_lines,
61            links: renderer.links,
62        }
63    }
64
65    /// Returns whether the last word of the current line is a whitespace
66    fn is_last_whitespace(&self) -> bool {
67        self.current_line
68            .last()
69            .map(|last| last.index == usize::MAX)
70            .unwrap_or(false)
71    }
72
73    /// Returns whether the last rendered line is an empty one
74    ///
75    /// When the current line is not empty, this will return false
76    fn is_last_empty(&self) -> bool {
77        if !self.current_line.is_empty() {
78            false
79        } else {
80            self.rendered_lines
81                .last()
82                .map(|last| last.is_empty())
83                .unwrap_or(false)
84        }
85    }
86
87    /// Adds a whitespace to the end of the current line
88    ///
89    /// The whitespace word has an index of `usize::MAX` and a width of `0` to not interfere with text wrapping. Note: If there already is a whitespace at the end of the current line, no whitespace will be added!
90    fn add_whitespace(&mut self) {
91        if self
92            .current_line
93            .last()
94            .map(|word| word.index == usize::MAX)
95            .unwrap_or(false)
96        {
97            return;
98        }
99
100        self.current_line.push(self.n_whitespace(1));
101    }
102
103    /// Returns a Word containing n amount of whitespace
104    fn n_whitespace(&self, n: u8) -> Word {
105        Word {
106            index: usize::MAX,
107            content: String::new(),
108            style: Style::default(),
109            width: 0.0,
110            whitespace_width: n as f64,
111            penalty_width: 0.0,
112        }
113    }
114
115    /// Clears the current line
116    ///
117    /// When the current line is not empty already, it adds it to the rendered lines
118    fn clear_line(&mut self) {
119        if self.current_line.is_empty() {
120            return;
121        }
122
123        self.rendered_lines
124            .push(std::mem::take(&mut self.current_line));
125    }
126
127    /// Adds an empty line to the finished lines
128    ///
129    /// Clears the current line before adding the empty one
130    fn add_empty_line(&mut self) {
131        self.clear_line();
132        self.rendered_lines.push(Vec::new());
133    }
134
135    fn current_width(&self) -> usize {
136        let mut current_width: f64 = 0.0;
137        for word in self.current_line.iter() {
138            current_width = current_width + word.width + word.whitespace_width;
139        }
140        current_width as usize
141    }
142
143    /// Wraps and appends words
144    ///
145    /// This fills up the current line with words and wraps the remaining words into lines, appending them to the finished words. Note: This leaves the current line empty, except when there are not enough words to fill it up completely
146    fn wrap_append(&mut self, words: Vec<Word>) {
147        if words.is_empty() {
148            return;
149        }
150
151        let current_width = self.current_width() as f64;
152        let mut remaining_width = (self.width as f64) - current_width;
153
154        // if the first word doesn't fit onto the current line, the line wrapping algorithm gets confuesed.
155        // that means we have to clear it in this case
156        if words.first().map(|word| word.width).unwrap_or_default() > remaining_width {
157            remaining_width = self.width as f64;
158            self.clear_line();
159        }
160
161        if self.current_line.is_empty() {
162            remaining_width -= self.left_padding as f64;
163            self.current_line.push(self.n_whitespace(self.left_padding));
164            if let Some(prefix) = self.prefix {
165                self.current_line.push(Word {
166                    index: usize::MAX,
167                    content: prefix.to_string(),
168                    style: Style::default(),
169                    width: 1.0,
170                    whitespace_width: 1.0,
171                    penalty_width: 0.0,
172                });
173
174                remaining_width -= 2.0; // subtract 2: 1 char & 1 whitespace
175            }
176        }
177
178        let line_widths: [f64; 2] = [remaining_width, self.width as f64];
179        let mut wrapped_lines: Vec<Vec<Word>> =
180            wrap_optimal_fit(&words, &line_widths, &Penalties::default())
181                .unwrap()
182                .into_iter()
183                .map(|word| word.to_vec())
184                .collect();
185
186        self.current_line.append(&mut wrapped_lines.remove(0));
187
188        // add prefixes
189        if let Some(prefix) = self.prefix {
190            for line in wrapped_lines.iter_mut() {
191                line.insert(
192                    0,
193                    Word {
194                        index: usize::MAX,
195                        content: prefix.to_string(),
196                        style: Style::default(),
197                        width: 1.0,
198                        whitespace_width: 1.0,
199                        penalty_width: 0.0,
200                    },
201                );
202            }
203        }
204
205        // indent the current line
206        for line in wrapped_lines.iter_mut() {
207            line.insert(0, self.n_whitespace(self.left_padding));
208        }
209
210        if let Some(last_line) = wrapped_lines.pop() {
211            self.clear_line();
212            self.current_line = last_line;
213            self.rendered_lines.append(&mut wrapped_lines)
214        }
215    }
216
217    /// Adds an empty line only if the last line is not empty
218    fn ensure_empty_line(&mut self) {
219        if !self.is_last_empty() {
220            self.add_empty_line();
221        }
222    }
223
224    /// Adds a modifier to the current text style
225    fn add_modifier(&mut self, modifier: Modifier) {
226        self.text_style = self.text_style.add_modifier(modifier);
227    }
228
229    /// Removes a modifier from the current text style
230    fn remove_modifier(&mut self, modifier: Modifier) {
231        self.text_style = self.text_style.remove_modifier(modifier);
232    }
233
234    /// Changes the foreground color of the text style
235    fn set_text_fg(&mut self, color: Color) {
236        self.text_style = self.text_style.fg(color);
237    }
238
239    /// Resets the foreground color of the text style
240    fn reset_text_fg(&mut self) {
241        self.text_style.fg = None;
242    }
243
244    /// Adds n spaces to the left padding
245    fn add_n_padding(&mut self, n: u8) {
246        self.left_padding = self.left_padding.saturating_add(n);
247    }
248
249    /// Removes n spaces from the left padding
250    fn remove_n_padding(&mut self, n: u8) {
251        self.left_padding = self.left_padding.saturating_sub(n);
252    }
253
254    /// Sets the prefix to a given value
255    fn set_prefix(&mut self, prefix: char) {
256        self.prefix = Some(prefix);
257    }
258
259    /// Resets the prefix
260    fn reset_prefix(&mut self) {
261        self.prefix = None;
262    }
263
264    fn add_horizontal_line(&mut self) {
265        let remaining_width = (self.width as usize) - self.current_width();
266        let line = Word {
267            index: usize::MAX,
268            content: "─".repeat(remaining_width),
269            style: self.text_style,
270            width: remaining_width as f64,
271            whitespace_width: 0.0,
272            penalty_width: 0.0,
273        };
274        self.current_line.push(line);
275        self.clear_line();
276    }
277
278    fn render_children(&mut self, node: Node<'a>) {
279        for child in node.children() {
280            self.render_node(child);
281        }
282    }
283
284    fn render_section(&mut self, node: Node<'a>) {
285        if !matches!(node.data(), Data::Section { .. }) {
286            warn!("expected section data, got other data");
287            return;
288        }
289
290        self.ensure_empty_line();
291
292        self.render_children(node);
293
294        self.ensure_empty_line();
295    }
296
297    fn render_header(&mut self, node: Node<'a>) {
298        let Data::Header { kind, .. } = node.data() else {
299            warn!("expected header data, got other data");
300            return;
301        };
302
303        self.ensure_empty_line();
304
305        if !matches!(kind, &HeaderKind::Main | &HeaderKind::Sub) {
306            self.add_modifier(Modifier::BOLD);
307        }
308        self.set_text_fg(Color::Red);
309
310        self.render_children(node);
311
312        if !matches!(kind, &HeaderKind::Main | &HeaderKind::Sub) {
313            self.remove_modifier(Modifier::BOLD);
314        }
315        self.reset_text_fg();
316
317        if matches!(kind, &HeaderKind::Main | &HeaderKind::Sub) {
318            self.clear_line();
319            self.add_horizontal_line();
320        }
321
322        self.ensure_empty_line();
323    }
324
325    fn render_text(&mut self, node: Node<'a>) {
326        let contents = match node.data() {
327            Data::Text { contents } => contents,
328            _ => {
329                warn!("expected text data, got other data");
330                return;
331            }
332        };
333
334        self.render_string(contents, node.index());
335        self.render_children(node);
336    }
337
338    fn render_string(&mut self, content: &str, index: usize) {
339        const TEXT_SPECIAL_CHARACTERS: [char; 9] = [',', '.', ':', ';', '\"', '\'', '!', '@', '%'];
340        if content.starts_with(TEXT_SPECIAL_CHARACTERS) && self.is_last_whitespace() {
341            self.current_line.pop();
342        }
343
344        let has_trailing_whitespace = content.ends_with(' ');
345        let mut words: Vec<Word> = content
346            .split_whitespace()
347            .map(|word| Word {
348                index,
349                content: word.to_string(),
350                style: self.text_style,
351                width: word.chars().count() as f64,
352                whitespace_width: 1.0,
353                penalty_width: 0.0,
354            })
355            .collect();
356
357        if !has_trailing_whitespace {
358            if let Some(word) = words.last_mut() {
359                word.whitespace_width = 0.0;
360            }
361        }
362
363        self.wrap_append(words);
364    }
365
366    fn render_block_element(&mut self, node: Node<'a>) {
367        self.ensure_empty_line();
368        self.render_children(node);
369        self.ensure_empty_line();
370    }
371
372    fn render_span(&mut self, node: Node<'a>) {
373        self.render_children(node);
374        self.add_whitespace();
375    }
376
377    fn render_reflink(&mut self, node: Node<'a>) {
378        self.add_modifier(Modifier::ITALIC);
379        self.set_text_fg(Color::Gray);
380
381        self.render_children(node);
382
383        self.reset_text_fg();
384        self.remove_modifier(Modifier::ITALIC);
385
386        self.add_whitespace();
387    }
388
389    fn render_disambiguation(&mut self, node: Node<'a>) {
390        self.ensure_empty_line();
391
392        self.add_modifier(Modifier::ITALIC);
393        self.add_n_padding(DISAMBIGUATION_PADDING);
394        self.set_prefix(DISAMBIGUATION_PREFIX);
395
396        self.render_children(node);
397
398        self.reset_prefix();
399        self.remove_n_padding(DISAMBIGUATION_PADDING);
400        self.remove_modifier(Modifier::ITALIC);
401
402        self.ensure_empty_line();
403    }
404
405    fn render_block_quote(&mut self, node: Node<'a>) {
406        self.add_n_padding(BLOCKQUOTE_PADDING);
407
408        self.render_block_element(node);
409
410        self.remove_n_padding(BLOCKQUOTE_PADDING);
411    }
412
413    fn render_list(&mut self, node: Node<'a>) {
414        self.ensure_empty_line();
415
416        self.add_n_padding(LIST_PADDING);
417
418        self.render_children(node);
419
420        self.remove_n_padding(LIST_PADDING);
421
422        self.ensure_empty_line();
423    }
424
425    fn render_list_item(&mut self, node: Node<'a>) {
426        self.clear_line();
427        self.current_line.push(Word {
428            index: usize::MAX,
429            content: format!("{}{LIST_PREFIX}", " ".repeat(self.left_padding as usize)),
430            style: Style::default(),
431            width: 1.0,
432            whitespace_width: 1.0,
433            penalty_width: 0.0,
434        });
435        self.add_n_padding(2);
436
437        self.render_children(node);
438
439        self.remove_n_padding(2);
440        self.clear_line();
441    }
442
443    fn render_description_list_term(&mut self, node: Node<'a>) {
444        self.clear_line();
445        self.render_children(node);
446        self.clear_line();
447    }
448
449    fn render_description_list_description(&mut self, node: Node<'a>) {
450        self.clear_line();
451        self.render_children(node);
452        self.clear_line();
453    }
454
455    fn render_bold(&mut self, node: Node<'a>) {
456        self.add_modifier(Modifier::BOLD);
457
458        self.render_children(node);
459
460        self.remove_modifier(Modifier::BOLD);
461        self.add_whitespace();
462    }
463
464    fn render_italic(&mut self, node: Node<'a>) {
465        self.add_modifier(Modifier::ITALIC);
466        self.set_text_fg(Color::Blue);
467
468        self.render_children(node);
469
470        self.reset_text_fg();
471        self.remove_modifier(Modifier::ITALIC);
472        self.add_whitespace();
473    }
474
475    fn render_linebreak(&mut self, node: Node<'a>) {
476        self.clear_line();
477        self.render_children(node);
478    }
479
480    fn render_link(&mut self, node: Node<'a>, link: Link) {
481        self.links.push((self.rendered_lines.len(), node.index()));
482
483        match link {
484            Link::Internal(_) => self.render_wiki_link(node),
485            Link::Anchor(_) => self.render_wiki_link(node),
486            Link::RedLink(_) => self.render_red_link(node),
487            Link::MediaLink(_) => self.render_media_link(node),
488            Link::External(_) => self.render_external_link(node),
489            Link::ExternalToInternal(_) => self.render_external_link(node),
490        }
491    }
492
493    fn render_wiki_link(&mut self, node: Node<'a>) {
494        self.set_text_fg(Color::Blue);
495        self.render_children(node);
496        self.reset_text_fg();
497
498        self.add_whitespace();
499    }
500
501    fn render_red_link(&mut self, node: Node<'a>) {
502        self.add_modifier(Modifier::ITALIC);
503        self.set_text_fg(Color::Red);
504
505        self.render_children(node);
506
507        self.reset_text_fg();
508        self.remove_modifier(Modifier::ITALIC);
509        self.add_whitespace();
510    }
511
512    fn render_media_link(&mut self, node: Node<'a>) {
513        self.add_modifier(Modifier::ITALIC);
514        self.set_text_fg(Color::Blue);
515
516        self.render_children(node);
517
518        self.reset_text_fg();
519        self.remove_modifier(Modifier::ITALIC);
520        self.add_whitespace();
521    }
522
523    fn render_external_link(&mut self, node: Node<'a>) {
524        self.add_modifier(Modifier::ITALIC);
525
526        self.render_children(node);
527
528        self.remove_modifier(Modifier::ITALIC);
529        self.add_whitespace();
530    }
531
532    fn render_unsupported_element(
533        &mut self,
534        inline: bool,
535        element: &UnsupportedElement,
536        index: usize,
537    ) {
538        if inline {
539            self.add_modifier(Modifier::ITALIC);
540
541            self.add_whitespace();
542
543            self.set_text_fg(Color::DarkGray);
544            self.render_string("[x]", index);
545            self.reset_text_fg();
546
547            self.add_whitespace();
548
549            self.remove_modifier(Modifier::ITALIC);
550
551            return;
552        }
553
554        self.ensure_empty_line();
555        self.add_modifier(Modifier::ITALIC);
556
557        let message = match element {
558            UnsupportedElement::Table => "<Unsupported Element 'Table'>",
559            UnsupportedElement::Image => "<Unsupported Element 'Image'>",
560            UnsupportedElement::Figure => "<Unsupported Element 'Figure'>",
561            UnsupportedElement::MathElement => "<Unsupported Element 'Math Element'>",
562            UnsupportedElement::PreformattedText => "<Unsupported Element 'PreformattedText'>",
563        };
564
565        self.render_string(message, index);
566
567        self.remove_modifier(Modifier::ITALIC);
568        self.add_empty_line();
569    }
570
571    fn render_node(&mut self, node: Node<'a>) {
572        match node.data() {
573            Data::Section { id: _ } => self.render_section(node),
574            Data::Header { id: _, kind: _ } => self.render_header(node),
575            Data::Text { contents: _ } => self.render_text(node),
576            Data::Division => self.render_block_element(node),
577            Data::Paragraph => self.render_block_element(node),
578            Data::Span => self.render_span(node),
579            Data::Reflink => self.render_reflink(node),
580            Data::Hatnote => self.render_block_element(node),
581            Data::RedirectMessage => self.render_block_element(node),
582            Data::Disambiguation => self.render_disambiguation(node),
583            Data::Blockquote => self.render_block_quote(node),
584            Data::OrderedList => self.render_list(node),
585            Data::UnorderedList => self.render_list(node),
586            Data::ListItem => self.render_list_item(node),
587            Data::DescriptionList => self.render_block_element(node),
588            Data::DescriptionListTerm => self.render_description_list_term(node),
589            Data::DerscriptionListDescription => self.render_description_list_description(node),
590            Data::Bold => self.render_bold(node),
591            Data::Italic => self.render_italic(node),
592            Data::Linebreak => self.render_linebreak(node),
593            Data::Link(link) => self.render_link(node, link.clone()),
594            Data::Unknown => self.render_children(node),
595            Data::Unsupported(element) => {
596                self.render_unsupported_element(false, element, node.index())
597            }
598            Data::UnsupportedInline(element) => {
599                self.render_unsupported_element(true, element, node.index())
600            }
601        }
602    }
603}
604
605pub fn render_document(document: &Document, width: u16) -> RenderedDocument {
606    Renderer::render_document(document, width)
607}