ass_core/analysis/events/
line_breaks.rs1use alloc::{string::String, vec::Vec};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum LineBreakType {
11 Hard,
13 Soft,
15}
16
17#[derive(Debug, Clone)]
19pub struct LineBreakInfo {
20 pub position: usize,
22 pub break_type: LineBreakType,
24}
25
26#[derive(Debug, Clone)]
28pub struct TextWithLineBreaks {
29 pub text: String,
31 pub line_breaks: Vec<LineBreakInfo>,
33 pub nbsp_positions: Vec<usize>,
35}
36
37impl TextWithLineBreaks {
38 #[must_use]
40 pub fn from_text(text: &str, drawing_mode: bool) -> Self {
41 let mut plain_text = String::new();
42 let mut line_breaks = Vec::new();
43 let mut nbsp_positions = Vec::new();
44
45 let mut chars = text.chars().peekable();
46
47 while let Some(ch) = chars.next() {
48 if ch == '\\' {
49 if let Some(&next_ch) = chars.peek() {
50 match next_ch {
51 'N' => {
52 chars.next(); if !drawing_mode {
54 line_breaks.push(LineBreakInfo {
55 position: plain_text.len(),
56 break_type: LineBreakType::Hard,
57 });
58 plain_text.push('\n');
59 }
60 }
61 'n' => {
62 chars.next(); if !drawing_mode {
64 line_breaks.push(LineBreakInfo {
65 position: plain_text.len(),
66 break_type: LineBreakType::Soft,
67 });
68 plain_text.push('\n');
69 }
70 }
71 'h' => {
72 chars.next(); if !drawing_mode {
74 nbsp_positions.push(plain_text.len());
75 plain_text.push('\u{00A0}'); }
77 }
78 _ => {
79 plain_text.push(ch);
81 plain_text.push(next_ch);
82 chars.next();
83 }
84 }
85 } else {
86 plain_text.push(ch);
88 }
89 } else {
90 plain_text.push(ch);
91 }
92 }
93
94 Self {
95 text: plain_text,
96 line_breaks,
97 nbsp_positions,
98 }
99 }
100
101 #[must_use]
103 pub fn get_break_type_at(&self, position: usize) -> Option<LineBreakType> {
104 self.line_breaks
105 .iter()
106 .find(|lb| lb.position == position)
107 .map(|lb| lb.break_type)
108 }
109
110 #[must_use]
112 pub fn is_nbsp_at(&self, position: usize) -> bool {
113 self.nbsp_positions.contains(&position)
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn test_hard_line_break() {
123 let text = r"Line 1\NLine 2";
124 let processed = TextWithLineBreaks::from_text(text, false);
125
126 assert_eq!(processed.text, "Line 1\nLine 2");
127 assert_eq!(processed.line_breaks.len(), 1);
128 assert_eq!(processed.line_breaks[0].break_type, LineBreakType::Hard);
129 assert_eq!(processed.line_breaks[0].position, 6);
130 }
131
132 #[test]
133 fn test_soft_line_break() {
134 let text = r"Line 1\nLine 2";
135 let processed = TextWithLineBreaks::from_text(text, false);
136
137 assert_eq!(processed.text, "Line 1\nLine 2");
138 assert_eq!(processed.line_breaks.len(), 1);
139 assert_eq!(processed.line_breaks[0].break_type, LineBreakType::Soft);
140 assert_eq!(processed.line_breaks[0].position, 6);
141 }
142
143 #[test]
144 fn test_mixed_line_breaks() {
145 let text = r"Line 1\NLine 2\nLine 3";
146 let processed = TextWithLineBreaks::from_text(text, false);
147
148 assert_eq!(processed.text, "Line 1\nLine 2\nLine 3");
149 assert_eq!(processed.line_breaks.len(), 2);
150 assert_eq!(processed.line_breaks[0].break_type, LineBreakType::Hard);
151 assert_eq!(processed.line_breaks[1].break_type, LineBreakType::Soft);
152 }
153
154 #[test]
155 fn test_non_breaking_space() {
156 let text = r"Word1\hWord2";
157 let processed = TextWithLineBreaks::from_text(text, false);
158
159 assert_eq!(processed.text, "Word1\u{00A0}Word2");
160 assert_eq!(processed.nbsp_positions.len(), 1);
161 assert_eq!(processed.nbsp_positions[0], 5);
162 }
163
164 #[test]
165 fn test_drawing_mode_ignores_special() {
166 let text = r"Draw\NCommands\nHere\hIgnored";
167 let processed = TextWithLineBreaks::from_text(text, true);
168
169 assert_eq!(processed.text, "DrawCommandsHereIgnored");
170 assert_eq!(processed.line_breaks.len(), 0);
171 assert_eq!(processed.nbsp_positions.len(), 0);
172 }
173}