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