Skip to main content

md_tui/nodes/
textcomponent.rs

1use std::cmp;
2
3use itertools::Itertools;
4use mermaid_text::render_with_width;
5use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
6
7use ratatui::style::Color;
8use tree_sitter_highlight::HighlightEvent;
9
10use crate::{
11    highlight::{HighlightInfo, highlight_code},
12    nodes::word::MetaData,
13    util::{colors::highlight_colors, general::GENERAL_CONFIG},
14};
15
16use super::word::{Word, WordType};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum TextNode {
20    Image,
21    Paragraph,
22    LineBreak,
23    Heading,
24    Task,
25    List,
26    Footnote,
27    /// (`widths_by_column`, `heights_by_row`)
28    Table(Vec<u16>, Vec<u16>),
29    CodeBlock,
30    Quote,
31    HorizontalSeparator,
32    DetailsSummary {
33        id: u32,
34        folded: bool,
35        body_len: usize,
36    },
37}
38
39pub(crate) const TABLE_CELL_PADDING: u16 = 1;
40
41#[derive(Debug, Clone)]
42pub struct TextComponent {
43    kind: TextNode,
44    content: Vec<Vec<Word>>,
45    meta_info: Vec<Word>,
46    height: u16,
47    offset: u16,
48    scroll_offset: u16,
49    focused: bool,
50    focused_index: usize,
51    owning_details_ids: Vec<u32>,
52    hidden: bool,
53}
54
55impl TextComponent {
56    #[must_use]
57    pub fn new(kind: TextNode, content: Vec<Word>) -> Self {
58        let meta_info: Vec<Word> = content
59            .iter()
60            .filter(|c| !c.is_renderable() || c.kind() == WordType::FootnoteInline)
61            .cloned()
62            .collect();
63
64        let content = content.into_iter().filter(Word::is_renderable).collect();
65
66        Self {
67            kind,
68            content: vec![content],
69            meta_info,
70            height: 0,
71            offset: 0,
72            scroll_offset: 0,
73            focused: false,
74            focused_index: 0,
75            owning_details_ids: Vec::new(),
76            hidden: false,
77        }
78    }
79
80    #[must_use]
81    pub fn new_formatted(kind: TextNode, content: Vec<Vec<Word>>) -> Self {
82        Self::new_formatted_with_meta(kind, content, Vec::new())
83    }
84
85    #[must_use]
86    pub fn new_formatted_with_meta(
87        kind: TextNode,
88        content: Vec<Vec<Word>>,
89        mut meta_info: Vec<Word>,
90    ) -> Self {
91        meta_info.extend(
92            content
93                .iter()
94                .flatten()
95                .filter(|c| !c.is_renderable())
96                .cloned(),
97        );
98
99        let content: Vec<Vec<Word>> = content
100            .into_iter()
101            .map(|c| c.into_iter().filter(Word::is_renderable).collect())
102            .collect();
103
104        Self {
105            kind,
106            height: content.len() as u16,
107            meta_info,
108            content,
109            offset: 0,
110            scroll_offset: 0,
111            focused: false,
112            focused_index: 0,
113            owning_details_ids: Vec::new(),
114            hidden: false,
115        }
116    }
117
118    #[must_use]
119    pub fn kind(&self) -> TextNode {
120        self.kind.clone()
121    }
122
123    #[must_use]
124    pub fn content(&self) -> &Vec<Vec<Word>> {
125        &self.content
126    }
127
128    #[must_use]
129    pub fn content_as_lines(&self) -> Vec<String> {
130        if let TextNode::Table(widths, _) = self.kind() {
131            let column_count = widths.len();
132
133            // Malformed tables have no columns (see `transform_table`) and
134            // `chunks` panics on a chunk size of zero
135            if column_count == 0 {
136                return Vec::new();
137            }
138
139            let moved_content = self.content.chunks(column_count).collect::<Vec<_>>();
140
141            let mut lines = Vec::new();
142
143            moved_content.iter().for_each(|line| {
144                let temp = line
145                    .iter()
146                    .map(|c| c.iter().map(Word::content).join(""))
147                    .join(" ");
148                lines.push(temp);
149            });
150
151            lines
152        } else {
153            self.content
154                .iter()
155                .map(|c| c.iter().map(Word::content).collect::<Vec<_>>().join(""))
156                .collect()
157        }
158    }
159
160    #[must_use]
161    pub fn content_as_bytes(&self) -> Vec<u8> {
162        match self.kind() {
163            TextNode::CodeBlock => self.content_as_lines().join("").as_bytes().to_vec(),
164            _ => {
165                let strings = self.content_as_lines();
166                let string = strings.join("\n");
167                string.as_bytes().to_vec()
168            }
169        }
170    }
171
172    #[must_use]
173    pub fn content_owned(self) -> Vec<Vec<Word>> {
174        self.content
175    }
176
177    #[must_use]
178    pub fn meta_info(&self) -> &Vec<Word> {
179        &self.meta_info
180    }
181
182    #[must_use]
183    pub fn height(&self) -> u16 {
184        if self.hidden { 0 } else { self.height }
185    }
186
187    #[must_use]
188    pub fn raw_height(&self) -> u16 {
189        self.height
190    }
191
192    #[must_use]
193    pub fn owning_details_ids(&self) -> &[u32] {
194        &self.owning_details_ids
195    }
196
197    pub fn prepend_owning_details_id(&mut self, id: u32) {
198        self.owning_details_ids.insert(0, id);
199    }
200
201    pub fn set_owning_details_ids(&mut self, ids: Vec<u32>) {
202        self.owning_details_ids = ids;
203    }
204
205    #[must_use]
206    pub fn is_hidden(&self) -> bool {
207        self.hidden
208    }
209
210    pub fn set_hidden(&mut self, hidden: bool) {
211        self.hidden = hidden;
212    }
213
214    /// If this component is a `DetailsSummary`, set its `folded` field.
215    /// Returns the new folded state on success, `None` if the component
216    /// is not a `DetailsSummary`.
217    pub fn set_details_folded(&mut self, folded: bool) -> Option<bool> {
218        if let TextNode::DetailsSummary {
219            id,
220            folded: _,
221            body_len,
222        } = self.kind.clone()
223        {
224            self.kind = TextNode::DetailsSummary {
225                id,
226                folded,
227                body_len,
228            };
229            Some(folded)
230        } else {
231            None
232        }
233    }
234
235    #[must_use]
236    pub fn y_offset(&self) -> u16 {
237        self.offset
238    }
239
240    #[must_use]
241    pub fn scroll_offset(&self) -> u16 {
242        self.scroll_offset
243    }
244
245    pub fn set_y_offset(&mut self, y_offset: u16) {
246        self.offset = y_offset;
247    }
248
249    pub fn set_scroll_offset(&mut self, offset: u16) {
250        self.scroll_offset = offset;
251    }
252
253    #[must_use]
254    pub fn is_focused(&self) -> bool {
255        self.focused
256    }
257
258    pub fn deselect(&mut self) {
259        self.focused = false;
260        self.focused_index = 0;
261        self.content
262            .iter_mut()
263            .flatten()
264            .filter(|c| c.kind() == WordType::Selected)
265            .for_each(|c| {
266                c.clear_kind();
267            });
268    }
269
270    /// Mark a `DetailsSummary` component as focused. Unlike
271    /// `visually_select`, no inner word changes kind — the renderer
272    /// reads `is_focused()` directly to apply selection styling to the
273    /// whole header line.
274    pub fn visually_select_summary(&mut self) {
275        self.focused = true;
276    }
277
278    /// Clear focus on a `DetailsSummary` component.
279    pub fn deselect_summary(&mut self) {
280        self.focused = false;
281    }
282
283    pub fn visually_select(&mut self, index: usize) -> Result<(), String> {
284        self.focused = true;
285        self.focused_index = index;
286
287        if index >= self.num_links() {
288            return Err(format!(
289                "Index out of bounds: {} >= {}",
290                index,
291                self.num_links()
292            ));
293        }
294
295        // Transform nth link to selected
296        self.link_words_mut()
297            .get_mut(index)
298            .ok_or("index out of bounds")?
299            .iter_mut()
300            .for_each(|c| {
301                c.set_kind(WordType::Selected);
302            });
303        Ok(())
304    }
305
306    fn link_words_mut(&mut self) -> Vec<Vec<&mut Word>> {
307        let mut selection: Vec<Vec<&mut Word>> = Vec::new();
308        let mut iter = self.content.iter_mut().flatten().peekable();
309        while let Some(e) = iter.peek() {
310            if matches!(e.kind(), WordType::Link | WordType::FootnoteInline) {
311                selection.push(
312                    iter.by_ref()
313                        .take_while(|c| {
314                            matches!(c.kind(), WordType::Link | WordType::FootnoteInline)
315                        })
316                        .collect(),
317                );
318            } else {
319                iter.next();
320            }
321        }
322        selection
323    }
324
325    #[must_use]
326    pub fn get_footnote(&self, search: &str) -> String {
327        self.content()
328            .iter()
329            .flatten()
330            .skip_while(|c| c.kind() != WordType::FootnoteData && c.content() != search)
331            .take_while(|c| c.kind() == WordType::Footnote)
332            .map(Word::content)
333            .collect()
334    }
335
336    pub fn highlight_link(&self) -> Result<&str, String> {
337        Ok(self
338            .meta_info()
339            .iter()
340            .filter(|c| matches!(c.kind(), WordType::LinkData | WordType::FootnoteInline))
341            .nth(self.focused_index)
342            .ok_or("index out of bounds")?
343            .content())
344    }
345
346    #[must_use]
347    pub fn num_links(&self) -> usize {
348        if self.hidden {
349            return 0;
350        }
351        self.meta_info
352            .iter()
353            .filter(|c| matches!(c.kind(), WordType::LinkData | WordType::FootnoteInline))
354            .count()
355    }
356
357    #[must_use]
358    pub fn selected_heights(&self) -> Vec<usize> {
359        let mut heights = Vec::new();
360        if self.hidden {
361            return heights;
362        }
363
364        if let TextNode::Table(widths, row_heights) = self.kind() {
365            let column_count = widths.len();
366
367            // Malformed tables have no columns (see `transform_table`) and
368            // render as a placeholder, so there are no positions to select.
369            // `chunks` panics on a chunk size of zero
370            if column_count == 0 {
371                return heights;
372            }
373
374            let iter = self.content.chunks(column_count).enumerate();
375
376            for (i, line) in iter {
377                if line
378                    .iter()
379                    .flatten()
380                    .any(|c| c.kind() == WordType::Selected)
381                {
382                    let offset = 1
383                        + row_heights.iter().take(i).copied().sum::<u16>() as usize
384                        + usize::from(i > 0);
385                    heights.push(offset);
386                }
387            }
388            return heights;
389        }
390
391        for (i, line) in self.content.iter().enumerate() {
392            if line.iter().any(|c| c.kind() == WordType::Selected) {
393                heights.push(i);
394            }
395        }
396        heights
397    }
398
399    pub fn words_mut(&mut self) -> Vec<&mut Word> {
400        self.content.iter_mut().flatten().collect()
401    }
402
403    pub fn transform(&mut self, width: u16) {
404        match self.kind {
405            TextNode::List => {
406                transform_list(self, width);
407            }
408            TextNode::CodeBlock => {
409                transform_codeblock(self);
410            }
411            TextNode::Paragraph | TextNode::Task | TextNode::Quote => {
412                transform_paragraph(self, width);
413            }
414            TextNode::LineBreak | TextNode::Heading | TextNode::DetailsSummary { .. } => {
415                self.height = 1;
416            }
417            TextNode::Table(_, _) => {
418                transform_table(self, width);
419            }
420            TextNode::HorizontalSeparator => self.height = 1,
421            TextNode::Image => unreachable!("Image should not be transformed"),
422            TextNode::Footnote => self.height = 0,
423        }
424    }
425}
426
427pub(crate) fn word_wrapping<'a>(
428    words: impl IntoIterator<Item = &'a Word>,
429    width: usize,
430    allow_hyphen: bool,
431) -> Vec<Vec<Word>> {
432    let enable_hyphen = allow_hyphen && width > 4;
433
434    let mut lines = Vec::new();
435    let mut line = Vec::new();
436    let mut line_len = 0;
437    for word in words {
438        let word_len = display_width(word.content());
439        if line_len + word_len <= width {
440            line_len += word_len;
441            line.push(word.clone());
442        } else if word_len <= width {
443            lines.push(line);
444            let mut word = word.clone();
445            let content = word.content().trim_start().to_owned();
446            word.set_content(content);
447
448            line_len = display_width(word.content());
449            line = vec![word];
450        } else {
451            let content = word.content().to_owned();
452
453            if width - line_len < 4 {
454                line_len = 0;
455                lines.push(line);
456                line = Vec::new();
457            }
458
459            let split_width = if enable_hyphen && !content.ends_with('-') {
460                width - line_len - 1
461            } else {
462                width - line_len
463            };
464
465            let (mut content, mut newline_content) = split_by_width(&content, split_width);
466            if enable_hyphen && !content.ends_with('-') && !content.is_empty() {
467                if let Some(last_char) = content.pop() {
468                    newline_content.insert(0, last_char);
469                }
470                content.push('-');
471            }
472
473            line.push(Word::new(content, word.kind()));
474            lines.push(line);
475
476            while display_width(&newline_content) > width {
477                let split_width = if enable_hyphen && !newline_content.ends_with('-') {
478                    width - 1
479                } else {
480                    width
481                };
482                let (mut content, mut next_newline_content) =
483                    split_by_width(&newline_content, split_width);
484                if enable_hyphen && !newline_content.ends_with('-') && !content.is_empty() {
485                    if let Some(last_char) = content.pop() {
486                        next_newline_content.insert(0, last_char);
487                    }
488                    content.push('-');
489                }
490
491                line = vec![Word::new(content, word.kind())];
492                lines.push(line);
493                newline_content = next_newline_content;
494            }
495
496            if newline_content.is_empty() {
497                line_len = 0;
498                line = Vec::new();
499            } else {
500                line_len = display_width(&newline_content);
501                line = vec![Word::new(newline_content, word.kind())];
502            }
503        }
504    }
505
506    if !line.is_empty() {
507        lines.push(line);
508    }
509
510    lines
511}
512
513fn display_width(text: &str) -> usize {
514    UnicodeWidthStr::width(text)
515}
516
517fn split_by_width(text: &str, max_width: usize) -> (String, String) {
518    if max_width == 0 {
519        return (String::new(), text.to_string());
520    }
521
522    let mut width = 0;
523    let mut split_idx = 0;
524    // Track the byte index where the visible width reaches (or just exceeds) max_width.
525    for (i, c) in text.char_indices() {
526        let char_width = UnicodeWidthChar::width(c).unwrap_or(0);
527        if width + char_width > max_width {
528            if split_idx == 0 {
529                split_idx = i + c.len_utf8();
530            }
531            break;
532        }
533        width += char_width;
534        split_idx = i + c.len_utf8();
535        if width == max_width {
536            break;
537        }
538    }
539
540    let (head, tail) = text.split_at(split_idx);
541    (head.to_string(), tail.to_string())
542}
543
544fn transform_paragraph(component: &mut TextComponent, width: u16) {
545    let width = match component.kind {
546        TextNode::Paragraph => width as usize - 1,
547        TextNode::Task => width as usize - 4,
548        TextNode::Quote => width as usize - 2,
549        _ => unreachable!(),
550    };
551
552    let mut lines = word_wrapping(component.content.iter().flatten(), width, true);
553
554    if component.kind() == TextNode::Quote {
555        let is_special_quote = !component.meta_info.is_empty();
556
557        for line in lines.iter_mut().skip(usize::from(is_special_quote)) {
558            line.insert(0, Word::new(" ".to_string(), WordType::Normal));
559        }
560    }
561
562    component.height = lines.len() as u16;
563    component.content = lines;
564}
565
566fn transform_codeblock(component: &mut TextComponent) {
567    let language = if let Some(word) = component.meta_info().first() {
568        word.content()
569    } else {
570        ""
571    };
572
573    let highlight = highlight_code(language, &component.content_as_bytes());
574
575    let content = component.content_as_lines().join("");
576
577    let mut new_content = Vec::new();
578
579    if language.is_empty() {
580        component.content.insert(
581            0,
582            vec![Word::new(String::new(), WordType::CodeBlock(Color::Reset))],
583        );
584    }
585    match highlight {
586        HighlightInfo::Highlighted(e) => {
587            let highlight_colors = highlight_colors();
588            let mut color = Color::Reset;
589            for event in e {
590                match event {
591                    HighlightEvent::Source { start, end } => {
592                        let word =
593                            Word::new(content[start..end].to_string(), WordType::CodeBlock(color));
594                        new_content.push(word);
595                    }
596                    HighlightEvent::HighlightStart(index) => {
597                        color = highlight_colors[index.0];
598                    }
599                    HighlightEvent::HighlightEnd => color = Color::Reset,
600                }
601            }
602
603            // Find all the new lines to split the content correctly
604            let mut final_content = Vec::new();
605            let mut inner_content = Vec::new();
606            for word in new_content {
607                if word.content().contains('\n') {
608                    let mut start = 0;
609                    let mut end;
610                    for (i, c) in word.content().char_indices() {
611                        if c == '\n' {
612                            end = i;
613                            let new_word =
614                                Word::new(word.content()[start..end].to_string(), word.kind());
615                            inner_content.push(new_word);
616                            start = i + 1;
617                            final_content.push(inner_content);
618                            inner_content = Vec::new();
619                        } else if i == word.content().len() - 1 {
620                            let new_word =
621                                Word::new(word.content()[start..].to_string(), word.kind());
622                            inner_content.push(new_word);
623                        }
624                    }
625                } else {
626                    inner_content.push(word);
627                }
628            }
629
630            final_content.push(vec![Word::new(String::new(), WordType::CodeBlock(color))]);
631
632            component.content = final_content;
633        }
634        HighlightInfo::Unhighlighted => (),
635        HighlightInfo::Mermaid => {
636            let Ok(output) = render_with_width(&content, Some(GENERAL_CONFIG.width as usize - 5))
637            else {
638                return;
639            };
640
641            let mut final_content = Vec::new();
642
643            final_content.push(vec![Word::new(String::new(), WordType::Normal)]);
644
645            for line in output.lines() {
646                final_content.push(vec![Word::new(line.to_owned(), WordType::Normal)]);
647            }
648
649            final_content.push(vec![Word::new(String::new(), WordType::Normal)]);
650
651            component.content = final_content;
652        }
653    }
654
655    // FInd the longest line
656
657    let max_line_len = component
658        .content()
659        .iter()
660        .map(|inner| inner.iter().fold(0, |acc, x| acc + x.content().width()))
661        .max()
662        .unwrap_or(0);
663
664    let height = component.content.len() as u16;
665    component.height = height;
666    component.meta_info.push(Word::new(
667        String::new(),
668        WordType::MetaInfo(MetaData::LineLength(max_line_len as u16)),
669    ));
670}
671
672fn transform_list(component: &mut TextComponent, width: u16) {
673    let mut len = 0;
674    let mut lines = Vec::new();
675    let mut line = Vec::new();
676    let indent_iter = component
677        .meta_info
678        .iter()
679        .filter(|c| c.content().trim() == "");
680    let list_type_iter = component.meta_info.iter().filter(|c| {
681        matches!(
682            c.kind(),
683            WordType::MetaInfo(MetaData::OList | MetaData::UList)
684        )
685    });
686
687    let mut zip_iter = indent_iter.zip(list_type_iter);
688
689    let mut o_list_counter_stack = vec![None];
690    let mut max_stack_len = 1;
691    let mut indent = 0;
692    let mut extra_indent = 0;
693    let mut tmp = indent;
694    for word in component.content.iter_mut().flatten() {
695        let word_len = display_width(word.content());
696        if word_len + len < width as usize && word.kind() != WordType::ListMarker {
697            len += word_len;
698            line.push(word.clone());
699        } else {
700            let filler_content = if word.kind() == WordType::ListMarker {
701                indent = if let Some((meta, list_type)) = zip_iter.next() {
702                    match tmp.cmp(&display_width(meta.content())) {
703                        cmp::Ordering::Less => {
704                            o_list_counter_stack.push(None);
705                            max_stack_len += 1;
706                        }
707                        cmp::Ordering::Greater => {
708                            o_list_counter_stack.pop();
709                        }
710                        cmp::Ordering::Equal => (),
711                    }
712                    if list_type.kind() == WordType::MetaInfo(MetaData::OList) {
713                        let counter = o_list_counter_stack
714                            .last_mut()
715                            .expect("List parse error. Stack is empty");
716                        let source_index = word
717                            .content()
718                            .trim_end_matches(['.', ' '])
719                            .parse::<u64>()
720                            .unwrap_or(1);
721                        let next_index = counter.map_or(source_index, |index| index + 1);
722                        *counter = Some(next_index);
723
724                        word.set_content(format!("{next_index}. "));
725
726                        extra_indent = 1; // Ordered list is longer than unordered and needs extra space
727                    } else {
728                        extra_indent = 0;
729                    }
730                    tmp = display_width(meta.content());
731                    tmp
732                } else {
733                    0
734                };
735
736                " ".repeat(indent)
737            } else {
738                " ".repeat(indent + 2 + extra_indent)
739            };
740
741            let filler = Word::new(filler_content, WordType::Normal);
742
743            lines.push(line);
744            let content = word.content().trim_start().to_owned();
745            word.set_content(content);
746            len = display_width(word.content()) + display_width(filler.content());
747            line = vec![filler, word.to_owned()];
748        }
749    }
750    lines.push(line);
751    // Remove empty lines
752    lines.retain(|l| l.iter().any(|c| c.content() != ""));
753
754    // Find out if there are ordered indexes longer than 3 chars. F.ex. `1. ` is three chars, but `10. ` is four chars.
755    // To align the list on the same column, we need to find the longest index and add the difference to the shorter indexes.
756    let mut indent_correction = vec![0; max_stack_len];
757    let mut indent_index: u32 = 0;
758    let mut indent_len = 0;
759
760    for line in &lines {
761        if !line[1]
762            .content()
763            .strip_prefix(['1', '2', '3', '4', '5', '6', '7', '8', '9'])
764            .is_some_and(|c| c.ends_with(". "))
765        {
766            continue;
767        }
768
769        match indent_len.cmp(&display_width(line[0].content())) {
770            cmp::Ordering::Less => {
771                indent_index += 1;
772                indent_len = display_width(line[0].content());
773            }
774            cmp::Ordering::Greater => {
775                indent_index = indent_index.saturating_sub(1);
776                indent_len = display_width(line[0].content());
777            }
778            cmp::Ordering::Equal => (),
779        }
780
781        indent_correction[indent_index as usize] = cmp::max(
782            indent_correction[indent_index as usize],
783            display_width(line[1].content()),
784        );
785    }
786
787    // Finally, apply the indent correction to the list for each ordered index which is shorter
788    // than the longest index.
789
790    indent_index = 0;
791    indent_len = 0;
792    let mut unordered_list_skip = true; // Skip unordered list items. They are already aligned.
793
794    for line in &mut lines {
795        if line[1]
796            .content()
797            .strip_prefix(['1', '2', '3', '4', '5', '6', '7', '8', '9'])
798            .is_some_and(|c| c.ends_with(". "))
799        {
800            unordered_list_skip = false;
801        }
802
803        if line[1].content() == "• " || unordered_list_skip {
804            unordered_list_skip = true;
805            continue;
806        }
807
808        let amount = if line[1]
809            .content()
810            .strip_prefix(['1', '2', '3', '4', '5', '6', '7', '8', '9'])
811            .is_some_and(|c| c.ends_with(". "))
812        {
813            match indent_len.cmp(&display_width(line[0].content())) {
814                cmp::Ordering::Less => {
815                    indent_index += 1;
816                    indent_len = display_width(line[0].content());
817                }
818                cmp::Ordering::Greater => {
819                    indent_index = indent_index.saturating_sub(1);
820                    indent_len = display_width(line[0].content());
821                }
822                cmp::Ordering::Equal => (),
823            }
824            indent_correction[indent_index as usize]
825                .saturating_sub(display_width(line[1].content()))
826                + display_width(line[0].content())
827        } else {
828            // -3 because that is the length of the shortest ordered index (1. )
829            (indent_correction[indent_index as usize] + display_width(line[0].content()))
830                .saturating_sub(3)
831        };
832
833        line[0].set_content(" ".repeat(amount));
834    }
835
836    component.height = lines.len() as u16;
837    component.content = lines;
838}
839
840fn table_styling_width(column_count: usize) -> u16 {
841    1 + column_count as u16 * (TABLE_CELL_PADDING * 2 + 1)
842}
843
844fn transform_table(component: &mut TextComponent, width: u16) {
845    // Subtract 1 to match the actual render area width (consistent with transform_paragraph)
846    let width = width.saturating_sub(1);
847    let content = &mut component.content;
848
849    let column_count = component
850        .meta_info
851        .iter()
852        .filter(|w| w.kind() == WordType::MetaInfo(MetaData::ColumnsCount))
853        .count();
854
855    if !content.len().is_multiple_of(column_count) || column_count == 0 {
856        component.height = 1;
857        component.kind = TextNode::Table(vec![], vec![]);
858        return;
859    }
860
861    assert!(
862        content.len().is_multiple_of(column_count),
863        "Invalid table cell distribution: content.len() = {}, column_count = {}",
864        content.len(),
865        column_count
866    );
867
868    let row_count = content.len() / column_count;
869
870    ///////////////////////////
871    // Find unbalanced width //
872    ///////////////////////////
873    let widths = {
874        let mut widths = vec![0; column_count];
875        content.chunks(column_count).for_each(|row| {
876            row.iter().enumerate().for_each(|(col_i, entry)| {
877                let len = content_entry_len(entry);
878                if len > widths[col_i] as usize {
879                    widths[col_i] = len as u16;
880                }
881            });
882        });
883
884        widths
885    };
886
887    let styling_width = table_styling_width(column_count);
888    let unbalanced_cells_width = widths.iter().sum::<u16>();
889
890    /////////////////////////////////////
891    // Return if unbalanced width fits //
892    /////////////////////////////////////
893    if width >= unbalanced_cells_width + styling_width {
894        component.height = row_count as u16 + 3;
895        component.kind = TextNode::Table(widths, vec![1; row_count]);
896        return;
897    }
898
899    //////////////////////////////
900    // Find overflowing columns //
901    //////////////////////////////
902    let overflow_threshold = width.saturating_sub(styling_width) / column_count as u16;
903    let mut overflowing_columns = vec![];
904
905    let (overflowing_width, non_overflowing_width) = {
906        let mut overflowing_width = 0;
907        let mut non_overflowing_width = 0;
908
909        for (column_i, column_width) in widths.iter().enumerate() {
910            if *column_width > overflow_threshold {
911                overflowing_columns.push((column_i, column_width));
912
913                overflowing_width += column_width;
914            } else {
915                non_overflowing_width += column_width;
916            }
917        }
918
919        (overflowing_width, non_overflowing_width)
920    };
921
922    if overflowing_columns.is_empty() {
923        component.height = row_count as u16 + 3;
924        component.kind = TextNode::Table(widths, vec![1; row_count]);
925        return;
926    }
927
928    /////////////////////////////////////////////
929    // Assign new width to overflowing columns //
930    /////////////////////////////////////////////
931    let mut available_balanced_width = width.saturating_sub(non_overflowing_width + styling_width);
932    let mut available_overflowing_width = overflowing_width;
933
934    let overflowing_column_min_width =
935        (available_balanced_width / (2 * overflowing_columns.len() as u16)).max(1);
936
937    let mut widths_balanced: Vec<u16> = widths.clone();
938    for (column_i, old_column_width) in overflowing_columns
939        .iter()
940        // Sorting ensures the smallest overflowing cells receive minimum area without the
941        // need for recalculating the larger cells
942        .sorted_by(|a, b| Ord::cmp(a.1, b.1))
943    {
944        // Ensure the longest cell gets the most amount of area
945        let ratio = f32::from(**old_column_width) / f32::from(available_overflowing_width);
946        let mut balanced_column_width =
947            (ratio * f32::from(available_balanced_width)).floor() as u16;
948
949        if balanced_column_width < overflowing_column_min_width {
950            balanced_column_width = overflowing_column_min_width;
951            available_overflowing_width -= **old_column_width;
952            available_balanced_width =
953                available_balanced_width.saturating_sub(balanced_column_width);
954        }
955
956        widths_balanced[*column_i] = balanced_column_width;
957    }
958
959    ////////////////////////////////////////
960    // Wrap words based on balanced width //
961    ////////////////////////////////////////
962    let mut heights = vec![1; row_count];
963    for (row_i, row) in content
964        .iter_mut()
965        .chunks(column_count)
966        .into_iter()
967        .enumerate()
968    {
969        for (column_i, entry) in row.into_iter().enumerate() {
970            let lines = word_wrapping(
971                entry.drain(..).as_ref(),
972                widths_balanced[column_i] as usize,
973                true,
974            );
975
976            if heights[row_i] < lines.len() as u16 {
977                heights[row_i] = lines.len() as u16;
978            }
979
980            let _drop = std::mem::replace(entry, lines.into_iter().flatten().collect());
981        }
982    }
983
984    component.height = heights.iter().copied().sum::<u16>() + 3;
985
986    component.kind = TextNode::Table(widths_balanced, heights);
987}
988
989#[must_use]
990pub fn content_entry_len(words: &[Word]) -> usize {
991    words.iter().map(|word| display_width(word.content())).sum()
992}