okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
0d0b12c
main
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 { 11pub column : usize , 12pub ch : char , 13pub 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 { 19pub line : usize , 20pub continued : bool , 21pub 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 )> { 27let mut offset =0 ; 28let mut lines =Vec :: new (); 29for linein source. split_inclusive ( '\n' ) { 30let 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 ( 38lines : & [( usize , & str )], 39first : usize , 40colors : & Colors , 41columns : usize , 42tab_width : usize , 43) ->Vec < Row > { 44let mut rows =Vec :: new (); 45let mut cursor = colors. cursor ( lines. first (). map_or ( 0 , |( offset, _) |* offset)); 46for ( index, ( offset, text)) in lines. iter (). enumerate () { 47let line = first + index; 48let mut row =Row { 49 line, 50continued : false , 51glyphs : Vec :: new (), 52}; 53let mut column =0 ; 54for ( at, ch) in text. char_indices () { 55let color = cursor. at ( offset + at); 56let ( ch, width) =match ch{ 57'\t' =>( ' ' , tab_width - column % tab_width), 58 chif ch. is_control () =>( '\u{FFFD}' , 1 ), 59// Combining marks have no cell of their own; they are dropped. 60 ch =>match ch. width () { 61Some ( 0 ) =>continue , 62 width =>( ch, width. unwrap_or ( 1 )), 63}, 64}; 65if column + width > columns && column >0 { 66 rows. push ( std:: mem:: replace ( 67& mut row, 68Row { 69 line, 70continued : true , 71glyphs : Vec :: new (), 72}, 73)); 74 column =0 ; 75} 76if 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 >> { 90let count = rows. len (). div_ceil ( max_rows). max ( 1 ); 91let size = rows. len (). div_ceil ( count). max ( 1 ); 92let mut pages =Vec :: new (); 93let mut start =0 ; 94while start < rows. len () { 95let mut end =( start + size). min ( rows. len ()); 96if end < rows. len () && rows[ end]. continued { 97let 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{ 108use super :: * ; 109use arborium_theme:: theme:: builtin; 110 111fn plain ( source : & str , columns : usize ) ->Vec < Row > { 112let colors =Colors :: new ( source, None , & builtin:: github_light ()); 113rows ( & lines ( source), 1 , & colors, columns, 4 ) 114} 115 116fn text ( row : & Row ) ->String { 117let mut text =String :: new (); 118for glyphin & row. glyphs { 119while text. chars (). count () < glyph. column { 120 text. push ( ' ' ); 121} 122 text. push ( glyph. ch ); 123} 124 text 125} 126 127# [ test ] 128fn lines_keep_offsets_and_drop_line_endings () { 129assert_eq! ( 130lines ( "a\r\nbc\n\nd" ), 131vec! [( 0 , "a" ), ( 3 , "bc" ), ( 6 , "" ), ( 7 , "d" )] 132); 133assert_eq! ( lines ( "a\n" ), vec! [( 0 , "a" )]); 134assert! ( lines ( "" ). is_empty ()); 135} 136 137# [ test ] 138fn tabs_stop_at_multiples_of_the_tab_width () { 139let rows =plain ( "a\tb\n\tc" , 79 ); 140assert_eq! ( text ( & rows[ 0 ]), "a b" ); 141assert_eq! ( text ( & rows[ 1 ]), " c" ); 142} 143 144# [ test ] 145fn long_lines_wrap_and_wide_characters_never_split () { 146let rows =plain ( "abcdefg\n漢字漢" , 3 ); 147let wrapped: Vec < _ > = rows 148. iter () 149. map ( |row|( row. line , row. continued , text ( row))) 150. collect (); 151assert_eq! ( 152 wrapped, 153vec! [ 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); 162assert_eq! ( rows[ 3 ]. glyphs [ 0 ]. column , 0 ); 163} 164 165# [ test ] 166fn control_characters_are_visible_and_empty_lines_keep_a_row () { 167let rows =plain ( "a\u{1b}b\n\nc\u{301}" , 79 ); 168assert_eq! ( text ( & rows[ 0 ]), "a\u{FFFD}b" ); 169assert_eq! ( rows[ 1 ]. glyphs , vec! []); 170assert_eq! ( text ( & rows[ 2 ]), "c" ); 171} 172 173# [ test ] 174fn pages_break_between_lines_unless_one_line_fills_a_page () { 175let rows =plain ( "aa\nbbbbbb\ncc\ndddddddddd" , 2 ); 176// rows: a(1) b b b(2) c(3) d d d d d(4) 177assert_eq! ( pages ( & rows, 3 ), vec! [ 0 ..1 , 1 ..4 , 4 ..5 , 5 ..8 , 8 ..10 ]); 178} 179 180# [ test ] 181fn a_range_just_over_one_page_splits_evenly () { 182let rows =plain ( & "x\n" . repeat ( 81 ), 79 ); 183assert_eq! ( pages ( & rows, 80 ), vec! [ 0 ..41 , 41 ..81 ]); 184assert_eq! ( pages ( & rows[ ..80 ], 80 ), vec! [ 0 ..80 ]); 185} 186}