okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petview: see code as highlighted images for a final review pass0d0b12c

main
5.8 KiB186 linesraw
1//! Source lines laid out on a fixed grid: tabs expanded to stops, wide
2//! characters given two cells, and long lines wrapped at the column limit.
3
4use crate::highlight::Colors;
5use arborium_theme::theme::Color;
6use unicode_width::UnicodeWidthChar;
7
8/// One glyph at a grid column; a wide glyph also covers the next column.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct Glyph {
11    pub column: usize,
12    pub ch: char,
13    pub color: Color,
14}
15
16/// One row of the image: a source line, or the continuation of a wrapped one.
17#[derive(Debug, Clone, PartialEq)]
18pub struct Row {
19    pub line: usize,
20    pub continued: bool,
21    pub glyphs: Vec<Glyph>,
22}
23
24/// The 1-based lines of `source` with the byte offset each starts at.
25/// A final newline ends the last line rather than starting an empty one.
26pub fn lines(source: &str) -> Vec<(usize, &str)> {
27    let mut offset = 0;
28    let mut lines = Vec::new();
29    for line in source.split_inclusive('\n') {
30        let text = line.strip_suffix('\n').unwrap_or(line);
31        lines.push((offset, text.strip_suffix('\r').unwrap_or(text)));
32        offset += line.len();
33    }
34    lines
35}
36
37pub fn rows(
38    lines: &[(usize, &str)],
39    first: usize,
40    colors: &Colors,
41    columns: usize,
42    tab_width: usize,
43) -> Vec<Row> {
44    let mut rows = Vec::new();
45    let mut cursor = colors.cursor(lines.first().map_or(0, |(offset, _)| *offset));
46    for (index, (offset, text)) in lines.iter().enumerate() {
47        let line = first + index;
48        let mut row = Row {
49            line,
50            continued: false,
51            glyphs: Vec::new(),
52        };
53        let mut column = 0;
54        for (at, ch) in text.char_indices() {
55            let color = cursor.at(offset + at);
56            let (ch, width) = match ch {
57                '\t' => (' ', tab_width - column % tab_width),
58                ch if ch.is_control() => ('\u{FFFD}', 1),
59                // Combining marks have no cell of their own; they are dropped.
60                ch => match ch.width() {
61                    Some(0) => continue,
62                    width => (ch, width.unwrap_or(1)),
63                },
64            };
65            if column + width > columns && column > 0 {
66                rows.push(std::mem::replace(
67                    &mut row,
68                    Row {
69                        line,
70                        continued: true,
71                        glyphs: Vec::new(),
72                    },
73                ));
74                column = 0;
75            }
76            if ch != ' ' {
77                row.glyphs.push(Glyph { column, ch, color });
78            }
79            column += width.min(columns);
80        }
81        rows.push(row);
82    }
83    rows
84}
85
86/// Consecutive rows that fit one image, as evenly sized as the fewest pages
87/// allow, so a range just over one page is not a full page and a sliver.
88/// A page ends at a line boundary unless one line is taller than a page.
89pub fn pages(rows: &[Row], max_rows: usize) -> Vec<std::ops::Range<usize>> {
90    let count = rows.len().div_ceil(max_rows).max(1);
91    let size = rows.len().div_ceil(count).max(1);
92    let mut pages = Vec::new();
93    let mut start = 0;
94    while start < rows.len() {
95        let mut end = (start + size).min(rows.len());
96        if end < rows.len() && rows[end].continued {
97            let boundary = (start + 1..end).rev().find(|&index| !rows[index].continued);
98            end = boundary.unwrap_or(end);
99        }
100        pages.push(start..end);
101        start = end;
102    }
103    pages
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use arborium_theme::theme::builtin;
110
111    fn plain(source: &str, columns: usize) -> Vec<Row> {
112        let colors = Colors::new(source, None, &builtin::github_light());
113        rows(&lines(source), 1, &colors, columns, 4)
114    }
115
116    fn text(row: &Row) -> String {
117        let mut text = String::new();
118        for glyph in &row.glyphs {
119            while text.chars().count() < glyph.column {
120                text.push(' ');
121            }
122            text.push(glyph.ch);
123        }
124        text
125    }
126
127    #[test]
128    fn lines_keep_offsets_and_drop_line_endings() {
129        assert_eq!(
130            lines("a\r\nbc\n\nd"),
131            vec![(0, "a"), (3, "bc"), (6, ""), (7, "d")]
132        );
133        assert_eq!(lines("a\n"), vec![(0, "a")]);
134        assert!(lines("").is_empty());
135    }
136
137    #[test]
138    fn tabs_stop_at_multiples_of_the_tab_width() {
139        let rows = plain("a\tb\n\tc", 79);
140        assert_eq!(text(&rows[0]), "a   b");
141        assert_eq!(text(&rows[1]), "    c");
142    }
143
144    #[test]
145    fn long_lines_wrap_and_wide_characters_never_split() {
146        let rows = plain("abcdefg\n漢字漢", 3);
147        let wrapped: Vec<_> = rows
148            .iter()
149            .map(|row| (row.line, row.continued, text(row)))
150            .collect();
151        assert_eq!(
152            wrapped,
153            vec![
154                (1, false, "abc".into()),
155                (1, true, "def".into()),
156                (1, true, "g".into()),
157                (2, false, "漢".into()),
158                (2, true, "字".into()),
159                (2, true, "漢".into()),
160            ]
161        );
162        assert_eq!(rows[3].glyphs[0].column, 0);
163    }
164
165    #[test]
166    fn control_characters_are_visible_and_empty_lines_keep_a_row() {
167        let rows = plain("a\u{1b}b\n\nc\u{301}", 79);
168        assert_eq!(text(&rows[0]), "a\u{FFFD}b");
169        assert_eq!(rows[1].glyphs, vec![]);
170        assert_eq!(text(&rows[2]), "c");
171    }
172
173    #[test]
174    fn pages_break_between_lines_unless_one_line_fills_a_page() {
175        let rows = plain("aa\nbbbbbb\ncc\ndddddddddd", 2);
176        // rows: a(1) b b b(2) c(3) d d d d d(4)
177        assert_eq!(pages(&rows, 3), vec![0..1, 1..4, 4..5, 5..8, 8..10]);
178    }
179
180    #[test]
181    fn a_range_just_over_one_page_splits_evenly() {
182        let rows = plain(&"x\n".repeat(81), 79);
183        assert_eq!(pages(&rows, 80), vec![0..41, 41..81]);
184        assert_eq!(pages(&rows[..80], 80), vec![0..80]);
185    }
186}