1use std::cmp;
2
3use itertools::Itertools;
4use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
5
6use ratatui::style::Color;
7use tree_sitter_highlight::HighlightEvent;
8
9use crate::{
10 highlight::{COLOR_MAP, HighlightInfo, highlight_code},
11 nodes::word::MetaData,
12};
13
14use super::word::{Word, WordType};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum TextNode {
18 Image,
19 Paragraph,
20 LineBreak,
21 Heading,
22 Task,
23 List,
24 Footnote,
25 Table(Vec<u16>, Vec<u16>),
27 CodeBlock,
28 Quote,
29 HorizontalSeparator,
30}
31
32pub(crate) const TABLE_CELL_PADDING: u16 = 1;
33
34#[derive(Debug, Clone)]
35pub struct TextComponent {
36 kind: TextNode,
37 content: Vec<Vec<Word>>,
38 meta_info: Vec<Word>,
39 height: u16,
40 offset: u16,
41 scroll_offset: u16,
42 focused: bool,
43 focused_index: usize,
44}
45
46impl TextComponent {
47 #[must_use]
48 pub fn new(kind: TextNode, content: Vec<Word>) -> Self {
49 let meta_info: Vec<Word> = content
50 .iter()
51 .filter(|c| !c.is_renderable() || c.kind() == WordType::FootnoteInline)
52 .cloned()
53 .collect();
54
55 let content = content.into_iter().filter(Word::is_renderable).collect();
56
57 Self {
58 kind,
59 content: vec![content],
60 meta_info,
61 height: 0,
62 offset: 0,
63 scroll_offset: 0,
64 focused: false,
65 focused_index: 0,
66 }
67 }
68
69 #[must_use]
70 pub fn new_formatted(kind: TextNode, content: Vec<Vec<Word>>) -> Self {
71 Self::new_formatted_with_meta(kind, content, Vec::new())
72 }
73
74 #[must_use]
75 pub fn new_formatted_with_meta(
76 kind: TextNode,
77 content: Vec<Vec<Word>>,
78 mut meta_info: Vec<Word>,
79 ) -> Self {
80 meta_info.extend(
81 content
82 .iter()
83 .flatten()
84 .filter(|c| !c.is_renderable())
85 .cloned(),
86 );
87
88 let content: Vec<Vec<Word>> = content
89 .into_iter()
90 .map(|c| c.into_iter().filter(Word::is_renderable).collect())
91 .collect();
92
93 Self {
94 kind,
95 height: content.len() as u16,
96 meta_info,
97 content,
98 offset: 0,
99 scroll_offset: 0,
100 focused: false,
101 focused_index: 0,
102 }
103 }
104
105 #[must_use]
106 pub fn kind(&self) -> TextNode {
107 self.kind.clone()
108 }
109
110 #[must_use]
111 pub fn content(&self) -> &Vec<Vec<Word>> {
112 &self.content
113 }
114
115 #[must_use]
116 pub fn content_as_lines(&self) -> Vec<String> {
117 if let TextNode::Table(widths, _) = self.kind() {
118 let column_count = widths.len();
119
120 let moved_content = self.content.chunks(column_count).collect::<Vec<_>>();
121
122 let mut lines = Vec::new();
123
124 moved_content.iter().for_each(|line| {
125 let temp = line
126 .iter()
127 .map(|c| c.iter().map(Word::content).join(""))
128 .join(" ");
129 lines.push(temp);
130 });
131
132 lines
133 } else {
134 self.content
135 .iter()
136 .map(|c| c.iter().map(Word::content).collect::<Vec<_>>().join(""))
137 .collect()
138 }
139 }
140
141 #[must_use]
142 pub fn content_as_bytes(&self) -> Vec<u8> {
143 match self.kind() {
144 TextNode::CodeBlock => self.content_as_lines().join("").as_bytes().to_vec(),
145 _ => {
146 let strings = self.content_as_lines();
147 let string = strings.join("\n");
148 string.as_bytes().to_vec()
149 }
150 }
151 }
152
153 #[must_use]
154 pub fn content_owned(self) -> Vec<Vec<Word>> {
155 self.content
156 }
157
158 #[must_use]
159 pub fn meta_info(&self) -> &Vec<Word> {
160 &self.meta_info
161 }
162
163 #[must_use]
164 pub fn height(&self) -> u16 {
165 self.height
166 }
167
168 #[must_use]
169 pub fn y_offset(&self) -> u16 {
170 self.offset
171 }
172
173 #[must_use]
174 pub fn scroll_offset(&self) -> u16 {
175 self.scroll_offset
176 }
177
178 pub fn set_y_offset(&mut self, y_offset: u16) {
179 self.offset = y_offset;
180 }
181
182 pub fn set_scroll_offset(&mut self, offset: u16) {
183 self.scroll_offset = offset;
184 }
185
186 #[must_use]
187 pub fn is_focused(&self) -> bool {
188 self.focused
189 }
190
191 pub fn deselect(&mut self) {
192 self.focused = false;
193 self.focused_index = 0;
194 self.content
195 .iter_mut()
196 .flatten()
197 .filter(|c| c.kind() == WordType::Selected)
198 .for_each(|c| {
199 c.clear_kind();
200 });
201 }
202
203 pub fn visually_select(&mut self, index: usize) -> Result<(), String> {
204 self.focused = true;
205 self.focused_index = index;
206
207 if index >= self.num_links() {
208 return Err(format!(
209 "Index out of bounds: {} >= {}",
210 index,
211 self.num_links()
212 ));
213 }
214
215 self.link_words_mut()
217 .get_mut(index)
218 .ok_or("index out of bounds")?
219 .iter_mut()
220 .for_each(|c| {
221 c.set_kind(WordType::Selected);
222 });
223 Ok(())
224 }
225
226 fn link_words_mut(&mut self) -> Vec<Vec<&mut Word>> {
227 let mut selection: Vec<Vec<&mut Word>> = Vec::new();
228 let mut iter = self.content.iter_mut().flatten().peekable();
229 while let Some(e) = iter.peek() {
230 if matches!(e.kind(), WordType::Link | WordType::FootnoteInline) {
231 selection.push(
232 iter.by_ref()
233 .take_while(|c| {
234 matches!(c.kind(), WordType::Link | WordType::FootnoteInline)
235 })
236 .collect(),
237 );
238 } else {
239 iter.next();
240 }
241 }
242 selection
243 }
244
245 #[must_use]
246 pub fn get_footnote(&self, search: &str) -> String {
247 self.content()
248 .iter()
249 .flatten()
250 .skip_while(|c| c.kind() != WordType::FootnoteData && c.content() != search)
251 .take_while(|c| c.kind() == WordType::Footnote)
252 .map(Word::content)
253 .collect()
254 }
255
256 pub fn highlight_link(&self) -> Result<&str, String> {
257 Ok(self
258 .meta_info()
259 .iter()
260 .filter(|c| matches!(c.kind(), WordType::LinkData | WordType::FootnoteInline))
261 .nth(self.focused_index)
262 .ok_or("index out of bounds")?
263 .content())
264 }
265
266 #[must_use]
267 pub fn num_links(&self) -> usize {
268 self.meta_info
269 .iter()
270 .filter(|c| matches!(c.kind(), WordType::LinkData | WordType::FootnoteInline))
271 .count()
272 }
273
274 #[must_use]
275 pub fn selected_heights(&self) -> Vec<usize> {
276 let mut heights = Vec::new();
277
278 if let TextNode::Table(widths, row_heights) = self.kind() {
279 let column_count = widths.len();
280 let iter = self.content.chunks(column_count).enumerate();
281
282 for (i, line) in iter {
283 if line
284 .iter()
285 .flatten()
286 .any(|c| c.kind() == WordType::Selected)
287 {
288 let offset = 1
289 + row_heights.iter().take(i).copied().sum::<u16>() as usize
290 + usize::from(i > 0);
291 heights.push(offset);
292 }
293 }
294 return heights;
295 }
296
297 for (i, line) in self.content.iter().enumerate() {
298 if line.iter().any(|c| c.kind() == WordType::Selected) {
299 heights.push(i);
300 }
301 }
302 heights
303 }
304
305 pub fn words_mut(&mut self) -> Vec<&mut Word> {
306 self.content.iter_mut().flatten().collect()
307 }
308
309 pub fn transform(&mut self, width: u16) {
310 match self.kind {
311 TextNode::List => {
312 transform_list(self, width);
313 }
314 TextNode::CodeBlock => {
315 transform_codeblock(self);
316 }
317 TextNode::Paragraph | TextNode::Task | TextNode::Quote => {
318 transform_paragraph(self, width);
319 }
320 TextNode::LineBreak | TextNode::Heading => {
321 self.height = 1;
322 }
323 TextNode::Table(_, _) => {
324 transform_table(self, width);
325 }
326 TextNode::HorizontalSeparator => self.height = 1,
327 TextNode::Image => unreachable!("Image should not be transformed"),
328 TextNode::Footnote => self.height = 0,
329 }
330 }
331}
332
333pub(crate) fn word_wrapping<'a>(
334 words: impl IntoIterator<Item = &'a Word>,
335 width: usize,
336 allow_hyphen: bool,
337) -> Vec<Vec<Word>> {
338 let enable_hyphen = allow_hyphen && width > 4;
339
340 let mut lines = Vec::new();
341 let mut line = Vec::new();
342 let mut line_len = 0;
343 for word in words {
344 let word_len = display_width(word.content());
345 if line_len + word_len <= width {
346 line_len += word_len;
347 line.push(word.clone());
348 } else if word_len <= width {
349 lines.push(line);
350 let mut word = word.clone();
351 let content = word.content().trim_start().to_owned();
352 word.set_content(content);
353
354 line_len = display_width(word.content());
355 line = vec![word];
356 } else {
357 let content = word.content().to_owned();
358
359 if width - line_len < 4 {
360 line_len = 0;
361 lines.push(line);
362 line = Vec::new();
363 }
364
365 let split_width = if enable_hyphen && !content.ends_with('-') {
366 width - line_len - 1
367 } else {
368 width - line_len
369 };
370
371 let (mut content, mut newline_content) = split_by_width(&content, split_width);
372 if enable_hyphen && !content.ends_with('-') && !content.is_empty() {
373 if let Some(last_char) = content.pop() {
374 newline_content.insert(0, last_char);
375 }
376 content.push('-');
377 }
378
379 line.push(Word::new(content, word.kind()));
380 lines.push(line);
381
382 while display_width(&newline_content) > width {
383 let split_width = if enable_hyphen && !newline_content.ends_with('-') {
384 width - 1
385 } else {
386 width
387 };
388 let (mut content, mut next_newline_content) =
389 split_by_width(&newline_content, split_width);
390 if enable_hyphen && !newline_content.ends_with('-') && !content.is_empty() {
391 if let Some(last_char) = content.pop() {
392 next_newline_content.insert(0, last_char);
393 }
394 content.push('-');
395 }
396
397 line = vec![Word::new(content, word.kind())];
398 lines.push(line);
399 newline_content = next_newline_content;
400 }
401
402 if newline_content.is_empty() {
403 line_len = 0;
404 line = Vec::new();
405 } else {
406 line_len = display_width(&newline_content);
407 line = vec![Word::new(newline_content, word.kind())];
408 }
409 }
410 }
411
412 if !line.is_empty() {
413 lines.push(line);
414 }
415
416 lines
417}
418
419fn display_width(text: &str) -> usize {
420 UnicodeWidthStr::width(text)
421}
422
423fn split_by_width(text: &str, max_width: usize) -> (String, String) {
424 if max_width == 0 {
425 return (String::new(), text.to_string());
426 }
427
428 let mut width = 0;
429 let mut split_idx = 0;
430 for (i, c) in text.char_indices() {
432 let char_width = UnicodeWidthChar::width(c).unwrap_or(0);
433 if width + char_width > max_width {
434 if split_idx == 0 {
435 split_idx = i + c.len_utf8();
436 }
437 break;
438 }
439 width += char_width;
440 split_idx = i + c.len_utf8();
441 if width == max_width {
442 break;
443 }
444 }
445
446 let (head, tail) = text.split_at(split_idx);
447 (head.to_string(), tail.to_string())
448}
449
450fn transform_paragraph(component: &mut TextComponent, width: u16) {
451 let width = match component.kind {
452 TextNode::Paragraph => width as usize - 1,
453 TextNode::Task => width as usize - 4,
454 TextNode::Quote => width as usize - 2,
455 _ => unreachable!(),
456 };
457
458 let mut lines = word_wrapping(component.content.iter().flatten(), width, true);
459
460 if component.kind() == TextNode::Quote {
461 let is_special_quote = !component.meta_info.is_empty();
462
463 for line in lines.iter_mut().skip(usize::from(is_special_quote)) {
464 line.insert(0, Word::new(" ".to_string(), WordType::Normal));
465 }
466 }
467
468 component.height = lines.len() as u16;
469 component.content = lines;
470}
471
472fn transform_codeblock(component: &mut TextComponent) {
473 let language = if let Some(word) = component.meta_info().first() {
474 word.content()
475 } else {
476 ""
477 };
478
479 let highlight = highlight_code(language, &component.content_as_bytes());
480
481 let content = component.content_as_lines().join("");
482
483 let mut new_content = Vec::new();
484
485 if language.is_empty() {
486 component.content.insert(
487 0,
488 vec![Word::new(String::new(), WordType::CodeBlock(Color::Reset))],
489 );
490 }
491 match highlight {
492 HighlightInfo::Highlighted(e) => {
493 let mut color = Color::Reset;
494 for event in e {
495 match event {
496 HighlightEvent::Source { start, end } => {
497 let word =
498 Word::new(content[start..end].to_string(), WordType::CodeBlock(color));
499 new_content.push(word);
500 }
501 HighlightEvent::HighlightStart(index) => {
502 color = COLOR_MAP[index.0];
503 }
504 HighlightEvent::HighlightEnd => color = Color::Reset,
505 }
506 }
507
508 let mut final_content = Vec::new();
510 let mut inner_content = Vec::new();
511 for word in new_content {
512 if word.content().contains('\n') {
513 let mut start = 0;
514 let mut end;
515 for (i, c) in word.content().char_indices() {
516 if c == '\n' {
517 end = i;
518 let new_word =
519 Word::new(word.content()[start..end].to_string(), word.kind());
520 inner_content.push(new_word);
521 start = i + 1;
522 final_content.push(inner_content);
523 inner_content = Vec::new();
524 } else if i == word.content().len() - 1 {
525 let new_word =
526 Word::new(word.content()[start..].to_string(), word.kind());
527 inner_content.push(new_word);
528 }
529 }
530 } else {
531 inner_content.push(word);
532 }
533 }
534
535 final_content.push(vec![Word::new(String::new(), WordType::CodeBlock(color))]);
536
537 component.content = final_content;
538 }
539 HighlightInfo::Unhighlighted => (),
540 }
541
542 let height = component.content.len() as u16;
543 component.height = height;
544}
545
546fn transform_list(component: &mut TextComponent, width: u16) {
547 let mut len = 0;
548 let mut lines = Vec::new();
549 let mut line = Vec::new();
550 let indent_iter = component
551 .meta_info
552 .iter()
553 .filter(|c| c.content().trim() == "");
554 let list_type_iter = component.meta_info.iter().filter(|c| {
555 matches!(
556 c.kind(),
557 WordType::MetaInfo(MetaData::OList | MetaData::UList)
558 )
559 });
560
561 let mut zip_iter = indent_iter.zip(list_type_iter);
562
563 let mut o_list_counter_stack = vec![0];
564 let mut max_stack_len = 1;
565 let mut indent = 0;
566 let mut extra_indent = 0;
567 let mut tmp = indent;
568 for word in component.content.iter_mut().flatten() {
569 let word_len = display_width(word.content());
570 if word_len + len < width as usize && word.kind() != WordType::ListMarker {
571 len += word_len;
572 line.push(word.clone());
573 } else {
574 let filler_content = if word.kind() == WordType::ListMarker {
575 indent = if let Some((meta, list_type)) = zip_iter.next() {
576 match tmp.cmp(&display_width(meta.content())) {
577 cmp::Ordering::Less => {
578 o_list_counter_stack.push(0);
579 max_stack_len += 1;
580 }
581 cmp::Ordering::Greater => {
582 o_list_counter_stack.pop();
583 }
584 cmp::Ordering::Equal => (),
585 }
586 if list_type.kind() == WordType::MetaInfo(MetaData::OList) {
587 let counter = o_list_counter_stack
588 .last_mut()
589 .expect("List parse error. Stack is empty");
590
591 *counter += 1;
592
593 word.set_content(format!("{counter}. "));
594
595 extra_indent = 1; } else {
597 extra_indent = 0;
598 }
599 tmp = display_width(meta.content());
600 tmp
601 } else {
602 0
603 };
604
605 " ".repeat(indent)
606 } else {
607 " ".repeat(indent + 2 + extra_indent)
608 };
609
610 let filler = Word::new(filler_content, WordType::Normal);
611
612 lines.push(line);
613 let content = word.content().trim_start().to_owned();
614 word.set_content(content);
615 len = display_width(word.content()) + display_width(filler.content());
616 line = vec![filler, word.to_owned()];
617 }
618 }
619 lines.push(line);
620 lines.retain(|l| l.iter().any(|c| c.content() != ""));
622
623 let mut indent_correction = vec![0; max_stack_len];
626 let mut indent_index: u32 = 0;
627 let mut indent_len = 0;
628
629 for line in &lines {
630 if !line[1]
631 .content()
632 .strip_prefix(['1', '2', '3', '4', '5', '6', '7', '8', '9'])
633 .is_some_and(|c| c.ends_with(". "))
634 {
635 continue;
636 }
637
638 match indent_len.cmp(&display_width(line[0].content())) {
639 cmp::Ordering::Less => {
640 indent_index += 1;
641 indent_len = display_width(line[0].content());
642 }
643 cmp::Ordering::Greater => {
644 indent_index = indent_index.saturating_sub(1);
645 indent_len = display_width(line[0].content());
646 }
647 cmp::Ordering::Equal => (),
648 }
649
650 indent_correction[indent_index as usize] = cmp::max(
651 indent_correction[indent_index as usize],
652 display_width(line[1].content()),
653 );
654 }
655
656 indent_index = 0;
660 indent_len = 0;
661 let mut unordered_list_skip = true; for line in &mut lines {
664 if line[1]
665 .content()
666 .strip_prefix(['1', '2', '3', '4', '5', '6', '7', '8', '9'])
667 .is_some_and(|c| c.ends_with(". "))
668 {
669 unordered_list_skip = false;
670 }
671
672 if line[1].content() == "• " || unordered_list_skip {
673 unordered_list_skip = true;
674 continue;
675 }
676
677 let amount = if line[1]
678 .content()
679 .strip_prefix(['1', '2', '3', '4', '5', '6', '7', '8', '9'])
680 .is_some_and(|c| c.ends_with(". "))
681 {
682 match indent_len.cmp(&display_width(line[0].content())) {
683 cmp::Ordering::Less => {
684 indent_index += 1;
685 indent_len = display_width(line[0].content());
686 }
687 cmp::Ordering::Greater => {
688 indent_index = indent_index.saturating_sub(1);
689 indent_len = display_width(line[0].content());
690 }
691 cmp::Ordering::Equal => (),
692 }
693 indent_correction[indent_index as usize]
694 .saturating_sub(display_width(line[1].content()))
695 + display_width(line[0].content())
696 } else {
697 (indent_correction[indent_index as usize] + display_width(line[0].content()))
699 .saturating_sub(3)
700 };
701
702 line[0].set_content(" ".repeat(amount));
703 }
704
705 component.height = lines.len() as u16;
706 component.content = lines;
707}
708
709fn table_styling_width(column_count: usize) -> u16 {
710 1 + column_count as u16 * (TABLE_CELL_PADDING * 2 + 1)
711}
712
713fn transform_table(component: &mut TextComponent, width: u16) {
714 let width = width.saturating_sub(1);
716 let content = &mut component.content;
717
718 let column_count = component
719 .meta_info
720 .iter()
721 .filter(|w| w.kind() == WordType::MetaInfo(MetaData::ColumnsCount))
722 .count();
723
724 if !content.len().is_multiple_of(column_count) || column_count == 0 {
725 component.height = 1;
726 component.kind = TextNode::Table(vec![], vec![]);
727 return;
728 }
729
730 assert!(
731 content.len().is_multiple_of(column_count),
732 "Invalid table cell distribution: content.len() = {}, column_count = {}",
733 content.len(),
734 column_count
735 );
736
737 let row_count = content.len() / column_count;
738
739 let widths = {
743 let mut widths = vec![0; column_count];
744 content.chunks(column_count).for_each(|row| {
745 row.iter().enumerate().for_each(|(col_i, entry)| {
746 let len = content_entry_len(entry);
747 if len > widths[col_i] as usize {
748 widths[col_i] = len as u16;
749 }
750 });
751 });
752
753 widths
754 };
755
756 let styling_width = table_styling_width(column_count);
757 let unbalanced_cells_width = widths.iter().sum::<u16>();
758
759 if width >= unbalanced_cells_width + styling_width {
763 component.height = row_count as u16 + 3;
764 component.kind = TextNode::Table(widths, vec![1; row_count]);
765 return;
766 }
767
768 let overflow_threshold = width.saturating_sub(styling_width) / column_count as u16;
772 let mut overflowing_columns = vec![];
773
774 let (overflowing_width, non_overflowing_width) = {
775 let mut overflowing_width = 0;
776 let mut non_overflowing_width = 0;
777
778 for (column_i, column_width) in widths.iter().enumerate() {
779 if *column_width > overflow_threshold {
780 overflowing_columns.push((column_i, column_width));
781
782 overflowing_width += column_width;
783 } else {
784 non_overflowing_width += column_width;
785 }
786 }
787
788 (overflowing_width, non_overflowing_width)
789 };
790
791 if overflowing_columns.is_empty() {
792 component.height = row_count as u16 + 3;
793 component.kind = TextNode::Table(widths, vec![1; row_count]);
794 return;
795 }
796
797 let mut available_balanced_width = width.saturating_sub(non_overflowing_width + styling_width);
801 let mut available_overflowing_width = overflowing_width;
802
803 let overflowing_column_min_width =
804 (available_balanced_width / (2 * overflowing_columns.len() as u16)).max(1);
805
806 let mut widths_balanced: Vec<u16> = widths.clone();
807 for (column_i, old_column_width) in overflowing_columns
808 .iter()
809 .sorted_by(|a, b| Ord::cmp(a.1, b.1))
812 {
813 let ratio = f32::from(**old_column_width) / f32::from(available_overflowing_width);
815 let mut balanced_column_width =
816 (ratio * f32::from(available_balanced_width)).floor() as u16;
817
818 if balanced_column_width < overflowing_column_min_width {
819 balanced_column_width = overflowing_column_min_width;
820 available_overflowing_width -= **old_column_width;
821 available_balanced_width =
822 available_balanced_width.saturating_sub(balanced_column_width);
823 }
824
825 widths_balanced[*column_i] = balanced_column_width;
826 }
827
828 let mut heights = vec![1; row_count];
832 for (row_i, row) in content
833 .iter_mut()
834 .chunks(column_count)
835 .into_iter()
836 .enumerate()
837 {
838 for (column_i, entry) in row.into_iter().enumerate() {
839 let lines = word_wrapping(
840 entry.drain(..).as_ref(),
841 widths_balanced[column_i] as usize,
842 true,
843 );
844
845 if heights[row_i] < lines.len() as u16 {
846 heights[row_i] = lines.len() as u16;
847 }
848
849 let _drop = std::mem::replace(entry, lines.into_iter().flatten().collect());
850 }
851 }
852
853 component.height = heights.iter().copied().sum::<u16>() + 3;
854
855 component.kind = TextNode::Table(widths_balanced, heights);
856}
857
858#[must_use]
859pub fn content_entry_len(words: &[Word]) -> usize {
860 words.iter().map(|word| display_width(word.content())).sum()
861}