Skip to main content

katex_parser/unicode/
block.rs

1use crate::unicode::unicode_width::{is_east_asian_wide, is_zero_width_mark};
2
3/// A 2D text block where every line has equal width.
4/// Supports vertical stacking, horizontal juxtaposition with baseline
5/// alignment, and delimiter wrapping — building blocks for block-mode
6/// Unicode rendering of matrices, fractions, delimited expressions, etc.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct Block {
9    pub lines: Vec<String>,
10    pub width: usize,
11    pub baseline: usize,
12}
13
14impl Block {
15    pub fn from(s: &str) -> Block {
16        let raw: Vec<&str> = s.split('\n').collect();
17        let width = raw.iter().map(|l| display_width(l)).max().unwrap_or(0);
18        let lines: Vec<String> = raw.iter().map(|l| pad_right(l, width)).collect();
19        Block {
20            lines,
21            width,
22            baseline: 0,
23        }
24    }
25
26    pub fn empty() -> Block {
27        Block {
28            lines: Vec::new(),
29            width: 0,
30            baseline: 0,
31        }
32    }
33
34    pub fn render(&self) -> String {
35        self.lines.join("\n")
36    }
37
38    pub fn height(&self) -> usize {
39        self.lines.len()
40    }
41
42    pub fn width(&self) -> usize {
43        self.width
44    }
45
46    pub fn baseline(&self) -> usize {
47        self.baseline
48    }
49
50    pub fn pad_to(&self, target_width: usize) -> Block {
51        if target_width <= self.width {
52            return self.clone();
53        }
54        let lines: Vec<String> = self.lines.iter().map(|l| pad_right(l, target_width)).collect();
55        Block {
56            lines,
57            width: target_width,
58            baseline: self.baseline,
59        }
60    }
61
62    pub fn center(&self, target_width: usize) -> Block {
63        if self.lines.is_empty() || target_width <= self.width {
64            return self.clone();
65        }
66        let lines: Vec<String> = self.lines.iter().map(|l| center_text(l, target_width)).collect();
67        Block {
68            lines,
69            width: target_width,
70            baseline: self.baseline,
71        }
72    }
73
74    /// Vertical concatenation. The narrower block is padded to match width.
75    pub fn above(&self, other: &Block) -> Block {
76        let new_w = self.width.max(other.width);
77        let self_lines: Vec<String> = self.lines.iter().map(|l| pad_right(l, new_w)).collect();
78        let other_lines: Vec<String> = other.lines.iter().map(|l| pad_right(l, new_w)).collect();
79        let mut lines = self_lines;
80        lines.extend(other_lines);
81        Block {
82            lines,
83            width: new_w,
84            baseline: self.baseline,
85        }
86    }
87
88    /// Horizontal concatenation, aligning blocks at their baseline positions.
89    pub fn beside(&self, other: &Block) -> Block {
90        let self_h = self.lines.len();
91        let other_h = other.lines.len();
92        let new_h = self_h.max(other_h);
93        let self_bl = self.baseline;
94        let other_bl = other.baseline;
95        let diff = self_bl.abs_diff(other_bl);
96        let top_pad_left = if self_bl < other_bl { diff } else { 0 };
97        let top_pad_right = if other_bl < self_bl { diff } else { 0 };
98        let result_h = new_h.max(self_bl).max(other_bl);
99        let self_padded = vpad_at(self, result_h, top_pad_left);
100        let other_padded = vpad_at(other, result_h, top_pad_right);
101        let new_w = self.width + other.width;
102        let result_bl = self_bl.max(other_bl);
103        let lines: Vec<String> = (0..result_h)
104            .map(|i| self_padded[i].clone() + &other_padded[i])
105            .collect();
106        Block {
107            lines,
108            width: new_w,
109            baseline: result_bl,
110        }
111    }
112
113    pub fn append_left(&self, label: &Block) -> Block {
114        label.beside(self)
115    }
116
117    pub fn append_right(&self, label: &Block) -> Block {
118        self.beside(label)
119    }
120
121    /// Enclose every line with `left` and `right` delimiters.
122    pub fn enclose(&self, left: &str, right: &str) -> Block {
123        if self.lines.is_empty() {
124            return Block::from(&format!("{left}{right}"));
125        }
126        let lines: Vec<String> = self
127            .lines
128            .iter()
129            .map(|l| format!("{left}{l}{right}"))
130            .collect();
131        Block {
132            lines,
133            width: self.width + display_width(left) + display_width(right),
134            baseline: self.baseline,
135        }
136    }
137
138    /// Build a single-row block from cell text strings, each padded to the
139    /// corresponding column width, then joined by `gap`.
140    pub fn row(cells: &[String], widths: &[usize], gap: &str) -> Block {
141        let joined: Vec<String> = cells
142            .iter()
143            .enumerate()
144            .map(|(j, c)| pad_right(c, widths[j]))
145            .collect();
146        Block::from(&joined.join(gap))
147    }
148}
149
150pub fn pad_right(s: &str, width: usize) -> String {
151    let dw = display_width(s);
152    if width <= dw {
153        return s.to_string();
154    }
155    format!("{s}{}", " ".repeat(width - dw))
156}
157
158/// Compute the maximum width per column across all rows of cell strings.
159#[allow(dead_code)]
160pub fn column_widths(cells: &[Vec<String>]) -> Vec<usize> {
161    column_max_widths(cells, |s| display_width(s))
162}
163
164/// Compute the maximum width per column of any row-shaped table, where each
165/// cell's width comes from `width_of`.
166pub fn column_max_widths<W>(rows: &[Vec<W>], width_of: impl Fn(&W) -> usize) -> Vec<usize> {
167    let max_cols = rows.iter().map(|row| row.len()).max().unwrap_or(0);
168    let mut widths = vec![0usize; max_cols];
169    for row in rows {
170        for (j, cell) in row.iter().enumerate() {
171            let w = width_of(cell);
172            if w > widths[j] {
173                widths[j] = w;
174            }
175        }
176    }
177    widths
178}
179
180pub(crate) fn center_text(s: &str, width: usize) -> String {
181    let dw = display_width(s);
182    if width <= dw {
183        return s.to_string();
184    }
185    let need = width - dw;
186    let left = need / 2;
187    let right = need - left;
188    format!("{}{s}{}", " ".repeat(left), " ".repeat(right))
189}
190
191/// Terminal display width (in columns) of a string, counting full-width
192/// characters as 2 and combining/zero-width marks as 0. Unlike `len()`, this
193/// is based on Unicode code points, so astral-plane characters (e.g. math
194/// alphanumerics like 𝟙) count as their on-screen width of 1.
195pub fn display_width(s: &str) -> usize {
196    s.chars().map(char_width).sum()
197}
198
199fn char_width(c: char) -> usize {
200    let code = c as u32;
201    if code == 0 {
202        return 0;
203    }
204    if is_zero_width_mark(code) {
205        0
206    } else if is_east_asian_wide(code) {
207        2
208    } else {
209        1
210    }
211}
212
213/// Vertically pad a block so it has exactly `target` rows, inserting blank
214/// lines at the top so the block's content starts at row `top_offset`.
215fn vpad_at(b: &Block, target: usize, top_offset: usize) -> Vec<String> {
216    let h = b.lines.len();
217    if h >= target {
218        return b.lines.clone();
219    }
220    let empty = " ".repeat(b.width);
221    let mut top = vec![empty.clone(); top_offset];
222    let bottom = vec![empty; target.saturating_sub(top_offset.saturating_add(h))];
223    top.extend(b.lines.clone());
224    top.extend(bottom);
225    top
226}