sphinx_ultra/rst/
lines.rs1use std::ops::Deref;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ProcessedLine {
18 pub text: String,
19 pub src_start: u32,
21 pub src_end: u32,
22}
23
24impl ProcessedLine {
25 pub fn indent(&self) -> usize {
27 self.text.len() - self.text.trim_start_matches(' ').len()
28 }
29
30 pub fn is_blank(&self) -> bool {
31 self.text.is_empty()
32 }
33}
34
35#[derive(Debug, Clone, Default)]
36pub struct Lines(Vec<ProcessedLine>);
37
38impl Deref for Lines {
39 type Target = [ProcessedLine];
40
41 fn deref(&self) -> &[ProcessedLine] {
42 &self.0
43 }
44}
45
46fn is_line_boundary(c: char) -> bool {
47 matches!(
48 c,
49 '\n' | '\r' | '\x1c' | '\x1d' | '\x1e' | '\u{85}' | '\u{2028}' | '\u{2029}'
50 )
51}
52
53fn process_line(raw: &str) -> String {
54 let mut expanded = String::with_capacity(raw.len());
56 let mut col = 0usize;
57 for c in raw.chars() {
58 match c {
59 '\t' => {
60 let next_stop = (col / 8 + 1) * 8;
61 for _ in col..next_stop {
62 expanded.push(' ');
63 }
64 col = next_stop;
65 }
66 '\x0b' | '\x0c' => {
67 expanded.push(' ');
68 col += 1;
69 }
70 _ => {
71 expanded.push(c);
72 col += 1;
73 }
74 }
75 }
76 expanded.trim_end().to_string()
77}
78
79impl Lines {
80 pub fn new(source: &str) -> Lines {
81 let mut lines = Vec::new();
82 let bytes_len = source.len();
83 let mut line_start = 0usize;
84 let mut chars = source.char_indices().peekable();
85 while let Some((i, c)) = chars.next() {
86 if is_line_boundary(c) {
87 lines.push(ProcessedLine {
88 text: process_line(&source[line_start..i]),
89 src_start: line_start as u32,
90 src_end: i as u32,
91 });
92 if c == '\r' {
94 if let Some(&(_, '\n')) = chars.peek() {
95 chars.next();
96 }
97 }
98 line_start = match chars.peek() {
99 Some(&(j, _)) => j,
100 None => bytes_len,
101 };
102 }
103 }
104 if line_start < bytes_len {
105 lines.push(ProcessedLine {
106 text: process_line(&source[line_start..]),
107 src_start: line_start as u32,
108 src_end: bytes_len as u32,
109 });
110 }
111 Lines(lines)
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn tabs_expand_to_8_col_stops() {
121 assert_eq!(Lines::new("a\tb")[0].text, "a b");
122 assert_eq!(Lines::new("\ta")[0].text, " a");
123 assert_eq!(Lines::new("ab\tc")[0].text, "ab c");
124 assert_eq!(Lines::new("abcdefgh\tz")[0].text, "abcdefgh z");
125 assert_eq!(Lines::new("x\ty\tz")[0].text, "x y z");
126 }
127
128 #[test]
129 fn trailing_whitespace_stripped_and_spans_map_to_source() {
130 let src = "one \ntwo";
131 let l = Lines::new(src);
132 assert_eq!(l[0].text, "one");
133 assert_eq!(
134 &src[l[0].src_start as usize..l[0].src_end as usize],
135 "one "
136 );
137 assert_eq!(l[1].text, "two");
138 assert_eq!(&src[l[1].src_start as usize..l[1].src_end as usize], "two");
139 }
140
141 #[test]
142 fn vertical_tab_and_formfeed_become_spaces() {
143 assert_eq!(Lines::new("a\x0bb\x0cc")[0].text, "a b c");
144 }
145
146 #[test]
147 fn crlf_and_cr_split_without_stray_cr() {
148 let l = Lines::new("one\r\ntwo\rthree");
149 assert_eq!(l.len(), 3);
150 assert_eq!(l[0].text, "one");
151 assert_eq!(l[1].text, "two");
152 assert_eq!(l[2].text, "three");
153 }
154
155 #[test]
156 fn trailing_newline_produces_no_empty_last_line() {
157 assert_eq!(Lines::new("a\n").len(), 1);
158 let l = Lines::new("a\n\n");
159 assert_eq!(l.len(), 2);
160 assert!(l[1].is_blank());
161 }
162
163 #[test]
164 fn indent_counts_leading_spaces() {
165 let l = Lines::new(" four\n\tone-tab");
166 assert_eq!(l[0].indent(), 4);
167 assert_eq!(l[1].indent(), 8);
168 }
169}