1use std::fmt;
4use std::ops::Range;
5
6use unicode_segmentation::UnicodeSegmentation;
7
8use super::tab::DEFAULT_TAB_POLICY;
9use super::{Grapheme, TabPolicy};
10use crate::TextStyle;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct TextSpan {
18 text: String,
19 style: TextStyle,
20}
21
22impl TextSpan {
23 pub fn new(text: impl Into<String>, style: TextStyle) -> Self {
25 Self {
26 text: text.into(),
27 style,
28 }
29 }
30
31 pub fn text(&self) -> &str {
33 &self.text
34 }
35
36 pub const fn style(&self) -> &TextStyle {
38 &self.style
39 }
40}
41
42impl From<String> for TextSpan {
43 fn from(text: String) -> Self {
44 Self::new(text, TextStyle::new())
45 }
46}
47
48impl From<&str> for TextSpan {
49 fn from(text: &str) -> Self {
50 Self::new(text, TextStyle::new())
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct StyledTextError {
57 span: usize,
58 byte_offset: usize,
59}
60
61impl StyledTextError {
62 pub const fn span(&self) -> usize {
64 self.span
65 }
66
67 pub const fn byte_offset(&self) -> usize {
69 self.byte_offset
70 }
71}
72
73impl fmt::Display for StyledTextError {
74 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75 write!(
76 formatter,
77 "text span {} ends at byte {}, inside a grapheme cluster",
78 self.span, self.byte_offset
79 )
80 }
81}
82
83impl std::error::Error for StyledTextError {}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86struct SpanRange {
87 range: Range<usize>,
88 style: TextStyle,
89}
90
91#[derive(Debug, Clone, Default, PartialEq, Eq)]
99pub struct StyledText {
100 text: String,
101 spans: Vec<SpanRange>,
102 tab_policy: Option<TabPolicy>,
103}
104
105impl StyledText {
106 pub fn new(text: impl Into<String>, style: TextStyle) -> Self {
114 let text = text.into();
115 validate_source(&text);
116 let spans = (!text.is_empty())
117 .then_some(SpanRange {
118 range: 0..text.len(),
119 style,
120 })
121 .into_iter()
122 .collect();
123 Self {
124 text,
125 spans,
126 tab_policy: None,
127 }
128 }
129
130 pub fn try_from_spans<I, S>(spans: I) -> Result<Self, StyledTextError>
137 where
138 I: IntoIterator<Item = S>,
139 S: Into<TextSpan>,
140 {
141 let spans: Vec<TextSpan> = spans.into_iter().map(Into::into).collect();
142 let mut text = String::new();
143 let mut boundaries = Vec::new();
144 for (index, span) in spans.iter().enumerate() {
145 text.push_str(&span.text);
146 if !span.text.is_empty() && index + 1 < spans.len() {
147 boundaries.push((index, text.len()));
148 }
149 }
150 validate_source(&text);
151
152 let grapheme_boundaries: Vec<usize> = text
153 .grapheme_indices(true)
154 .map(|(offset, _)| offset)
155 .chain(std::iter::once(text.len()))
156 .collect();
157 if let Some((span, byte_offset)) = boundaries
158 .into_iter()
159 .find(|(_, offset)| grapheme_boundaries.binary_search(offset).is_err())
160 {
161 return Err(StyledTextError { span, byte_offset });
162 }
163
164 let mut ranges: Vec<SpanRange> = Vec::new();
165 let mut offset = 0;
166 for span in spans {
167 let start = offset;
168 offset += span.text.len();
169 if start == offset {
170 continue;
171 }
172 if let Some(previous) = ranges.last_mut()
173 && previous.style == span.style
174 {
175 previous.range.end = offset;
176 } else {
177 ranges.push(SpanRange {
178 range: start..offset,
179 style: span.style,
180 });
181 }
182 }
183 Ok(Self {
184 text,
185 spans: ranges,
186 tab_policy: None,
187 })
188 }
189
190 pub fn as_str(&self) -> &str {
192 &self.text
193 }
194
195 pub fn spans(&self) -> impl Iterator<Item = (&str, &TextStyle)> {
197 self.spans
198 .iter()
199 .map(|span| (&self.text[span.range.clone()], &span.style))
200 }
201
202 pub fn tab_policy(mut self, tab_policy: TabPolicy) -> Self {
207 self.tab_policy = Some(tab_policy);
208 self
209 }
210
211 pub fn reset_tab_policy(mut self) -> Self {
213 self.tab_policy = None;
214 self
215 }
216
217 pub const fn get_tab_policy(&self) -> Option<&TabPolicy> {
219 self.tab_policy.as_ref()
220 }
221
222 pub(crate) fn lines(&self) -> Vec<Vec<StyledTextGrapheme<'_>>> {
224 let policy = self.tab_policy.as_ref().unwrap_or(&DEFAULT_TAB_POLICY);
225 let mut lines = Vec::new();
226 let mut line = Vec::new();
227 let mut span = 0;
228 for (offset, symbol) in self.text.grapheme_indices(true) {
229 if symbol == "\n" {
230 lines.push(std::mem::take(&mut line));
231 continue;
232 }
233 while self.spans[span].range.end <= offset {
234 span += 1;
235 }
236 if symbol == "\t" {
237 append_tab(&mut line, policy, &self.spans[span].style);
238 continue;
239 }
240 line.push(StyledTextGrapheme {
241 grapheme: Grapheme::new(symbol),
242 style: &self.spans[span].style,
243 });
244 }
245 if !line.is_empty() || !self.text.ends_with('\n') {
246 lines.push(line);
247 }
248 if lines.is_empty() {
249 lines.push(Vec::new());
250 }
251 lines
252 }
253
254 pub(crate) fn uniform_style(&self) -> Option<&TextStyle> {
255 (self.spans.len() == 1).then(|| &self.spans[0].style)
256 }
257}
258
259fn validate_source(text: &str) {
260 assert!(
261 !text
262 .chars()
263 .any(|character| character.is_control() && character != '\n' && character != '\t'),
264 "styled text must not carry terminal control characters other than newline and tab: {text:?}"
265 );
266}
267
268fn append_tab<'a>(
269 line: &mut Vec<StyledTextGrapheme<'a>>,
270 policy: &'a TabPolicy,
271 style: &'a TextStyle,
272) {
273 let mut occupied = 0;
274 if let Some(marker) = policy.marker() {
275 for symbol in marker.graphemes(true) {
276 let grapheme = Grapheme::new(symbol);
277 occupied += grapheme.width();
278 line.push(StyledTextGrapheme { grapheme, style });
279 }
280 }
281 for _ in occupied..usize::from(policy.width()) {
282 line.push(StyledTextGrapheme {
283 grapheme: Grapheme::space(),
284 style,
285 });
286 }
287}
288
289impl From<String> for StyledText {
290 fn from(text: String) -> Self {
291 Self::new(text, TextStyle::new())
292 }
293}
294
295impl From<&str> for StyledText {
296 fn from(text: &str) -> Self {
297 Self::new(text, TextStyle::new())
298 }
299}
300
301#[derive(Debug, Clone, Copy)]
302pub(crate) struct StyledTextGrapheme<'a> {
303 pub grapheme: &'a Grapheme,
304 pub style: &'a TextStyle,
305}
306
307impl StyledTextGrapheme<'_> {
308 pub fn width(self) -> usize {
309 self.grapheme.width()
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use crate::Color;
317
318 #[test]
319 fn strings_become_default_spans() {
320 let owned = TextSpan::from(String::from("owned"));
321 let borrowed = TextSpan::from("borrowed");
322
323 assert_eq!(owned, TextSpan::new("owned", TextStyle::new()));
324 assert_eq!(borrowed, TextSpan::new("borrowed", TextStyle::new()));
325 }
326
327 #[test]
328 fn canonicalizes_empty_and_adjacent_equal_spans() {
329 let red = TextStyle::new().foreground(Color::RED);
330 let text = StyledText::try_from_spans([
331 TextSpan::new("a", red.clone()),
332 TextSpan::new("", TextStyle::new()),
333 TextSpan::new("b", red.clone()),
334 ])
335 .unwrap();
336
337 assert_eq!(text.as_str(), "ab");
338 assert_eq!(text.spans().collect::<Vec<_>>(), [("ab", &red)]);
339 }
340
341 #[test]
342 fn rejects_a_span_boundary_inside_a_whole_text_grapheme() {
343 let error = StyledText::try_from_spans([
344 TextSpan::new("e", TextStyle::new()),
345 TextSpan::new("\u{301}", TextStyle::new().bold()),
346 ])
347 .unwrap_err();
348
349 assert_eq!(error.span(), 0);
350 assert_eq!(error.byte_offset(), 1);
351 }
352
353 #[test]
354 fn assigns_styles_after_segmenting_the_whole_text() {
355 let red = TextStyle::new().foreground(Color::RED);
356 let text = StyledText::try_from_spans([
357 TextSpan::new("日", red.clone()),
358 TextSpan::new("👩💻", TextStyle::new()),
359 ])
360 .unwrap();
361 let lines = text.lines();
362
363 assert_eq!(lines[0].len(), 2);
364 assert_eq!(lines[0][0].grapheme.as_str(), "日");
365 assert_eq!(lines[0][0].style, &red);
366 assert_eq!(lines[0][1].grapheme.as_str(), "👩💻");
367 }
368}