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