kobo_core/html_text/
lines.rs1#[derive(Debug, Clone, PartialEq)]
9pub struct Line {
10 pub text: String,
11 pub start: usize,
12 pub end: usize,
13 pub width: f32,
14}
15
16pub fn lines(text: &str, max_chars: usize) -> Vec<Line> {
19 let mut out: Vec<Line> = Vec::new();
20 let mut line_start: usize = 0;
21 let mut line_text = String::new();
22 let mut line_chars = 0usize;
23 let mut i = 0usize;
24 let bytes = text.as_bytes();
25 let n = bytes.len();
26 while i < n {
27 while i < n {
28 let ch_len = text[i..].chars().next().map_or(0, |c| c.len_utf8());
29 if ch_len == 0 {
30 break;
31 }
32 if text[i..].chars().next().is_some_and(|c| c.is_whitespace()) {
33 i += ch_len;
34 } else {
35 break;
36 }
37 }
38 let word_start = i;
39 while i < n {
40 let Some(ch) = text[i..].chars().next() else {
41 break;
42 };
43 if ch.is_whitespace() {
44 break;
45 }
46 i += ch.len_utf8();
47 }
48 if i == word_start {
49 break;
50 }
51 let word = &text[word_start..i];
52 let wc = word.chars().count();
53 if !line_text.is_empty() && line_chars + 1 + wc > max_chars {
54 out.push(Line {
55 text: line_text,
56 start: line_start,
57 end: word_start,
58 width: 0.0,
59 });
60 line_start = word_start;
61 line_text = String::new();
62 line_chars = 0;
63 }
64 if line_text.is_empty() {
65 line_text.push_str(word);
66 line_chars = wc;
67 } else {
68 line_text.push(' ');
69 line_text.push_str(word);
70 line_chars += 1 + wc;
71 }
72 }
73 if !line_text.is_empty() {
74 out.push(Line {
75 text: line_text,
76 start: line_start,
77 end: n,
78 width: 0.0,
79 });
80 }
81 out
82}
83
84pub fn sentence_index_at(text: &str, byte_offset: usize) -> usize {
88 let upto = byte_offset.min(text.len());
89 text[..upto].matches(['.', '!', '?']).count()
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn linebreak_wraps_to_max_chars_with_ranges() {
98 let text = "alpha beta gamma delta epsilon zeta eta theta";
99 let ls = lines(text, 15);
100 for w in &ls {
101 assert!(w.text.chars().count() <= 15, "line too long: {:?}", w.text);
102 }
103 assert_eq!(ls.first().unwrap().start, 0);
104 assert_eq!(ls.last().unwrap().end, text.len());
105 for w in ls.windows(2) {
106 assert_eq!(w[0].end, w[1].start);
107 }
108 assert_eq!(
109 ls.iter()
110 .map(|l| l.text.as_str())
111 .collect::<Vec<_>>()
112 .join(" "),
113 text
114 );
115 }
116
117 #[test]
118 fn sentence_index_advances_on_punctuation() {
119 let text = "First sentence. Second one! And a third?";
120 assert_eq!(sentence_index_at(text, 0), 0);
121 assert_eq!(sentence_index_at(text, text.find("Second").unwrap()), 1);
122 assert_eq!(sentence_index_at(text, text.find("And").unwrap()), 2);
123 assert_eq!(sentence_index_at(text, text.len()), 3);
124 }
125}