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