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 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 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 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 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 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 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 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 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; }
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 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 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 fn ensure_empty_line(&mut self) {
219 if !self.is_last_empty() {
220 self.add_empty_line();
221 }
222 }
223
224 fn add_modifier(&mut self, modifier: Modifier) {
226 self.text_style = self.text_style.add_modifier(modifier);
227 }
228
229 fn remove_modifier(&mut self, modifier: Modifier) {
231 self.text_style = self.text_style.remove_modifier(modifier);
232 }
233
234 fn set_text_fg(&mut self, color: Color) {
236 self.text_style = self.text_style.fg(color);
237 }
238
239 fn reset_text_fg(&mut self) {
241 self.text_style.fg = None;
242 }
243
244 fn add_n_padding(&mut self, n: u8) {
246 self.left_padding = self.left_padding.saturating_add(n);
247 }
248
249 fn remove_n_padding(&mut self, n: u8) {
251 self.left_padding = self.left_padding.saturating_sub(n);
252 }
253
254 fn set_prefix(&mut self, prefix: char) {
256 self.prefix = Some(prefix);
257 }
258
259 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}