1#![forbid(unsafe_code)]
22
23use rotulus_layout::{Attrs, ColorRef, ParsedText, Span, Style};
24use std::ops::Range;
25
26const BOLD: u8 = 0x02;
27const COLOR: u8 = 0x03;
28const HEX_COLOR: u8 = 0x04;
29const RESET: u8 = 0x0f;
30const MONOSPACE: u8 = 0x11;
31const REVERSE: u8 = 0x16;
32const ITALIC: u8 = 0x1d;
33const STRIKETHROUGH: u8 = 0x1e;
34const UNDERLINE: u8 = 0x1f;
35
36const EXTENDED: [u32; 83] = [
38 0x470000, 0x472100, 0x474700, 0x324700, 0x004700, 0x00472c, 0x004747, 0x002747, 0x000047,
39 0x2e0047, 0x470047, 0x47002a, 0x740000, 0x743a00, 0x747400, 0x517400, 0x007400, 0x007449,
40 0x007474, 0x004074, 0x000074, 0x4b0074, 0x740074, 0x740045, 0xb50000, 0xb56300, 0xb5b500,
41 0x7db500, 0x00b500, 0x00b571, 0x00b5b5, 0x0063b5, 0x0000b5, 0x7500b5, 0xb500b5, 0xb5006b,
42 0xff0000, 0xff8c00, 0xffff00, 0xb2ff00, 0x00ff00, 0x00ffa0, 0x00ffff, 0x008cff, 0x0000ff,
43 0xa500ff, 0xff00ff, 0xff0098, 0xff5959, 0xffb459, 0xffff71, 0xcfff60, 0x6fff6f, 0x65ffc9,
44 0x6dffff, 0x59b4ff, 0x5959ff, 0xc459ff, 0xff66ff, 0xff59bc, 0xff9c9c, 0xffd39c, 0xffff9c,
45 0xe2ff9c, 0x9cff9c, 0x9cffdb, 0x9cffff, 0x9cd3ff, 0x9c9cff, 0xdc9cff, 0xff9cff, 0xff94d3,
46 0x000000, 0x131313, 0x282828, 0x363636, 0x4d4d4d, 0x656565, 0x818181, 0x9f9f9f, 0xbcbcbc,
47 0xe2e2e2, 0xffffff,
48];
49
50pub const STANDARD: [u32; 16] = [
53 0xffffff, 0x000000, 0x00007f, 0x009300, 0xff0000, 0x7f0000, 0x9c009c, 0xfc7f00, 0xffff00,
54 0x00fc00, 0x009393, 0x00ffff, 0x0000fc, 0xff00ff, 0x7f7f7f, 0xd2d2d2,
55];
56
57pub fn color(n: u8) -> ColorRef {
59 match n {
60 0..=15 => ColorRef::Palette(n),
61 16..=98 => ColorRef::Rgb(EXTENDED[usize::from(n) - 16]),
62 _ => ColorRef::Default,
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct Segment {
69 pub range: Range<usize>,
71 pub style: Style,
72}
73
74pub fn segments(text: &str) -> Vec<Segment> {
79 let b = text.as_bytes();
80 let mut out: Vec<Segment> = Vec::new();
81 let mut style = Style::default();
82 let mut start = 0usize;
83 let mut i = 0usize;
84
85 let flush = |from: usize, to: usize, style: Style, out: &mut Vec<Segment>| {
86 if from >= to {
87 return;
88 }
89 match out.last_mut() {
90 Some(last) if last.style == style && last.range.end == from => last.range.end = to,
91 _ => out.push(Segment {
92 range: from..to,
93 style,
94 }),
95 }
96 };
97
98 while i < b.len() {
99 let c = b[i];
100 let toggle = match c {
101 BOLD => Some(Attrs::BOLD),
102 ITALIC => Some(Attrs::ITALIC),
103 UNDERLINE => Some(Attrs::UNDERLINE),
104 STRIKETHROUGH => Some(Attrs::STRIKETHROUGH),
105 MONOSPACE => Some(Attrs::CODE),
106 REVERSE => Some(Attrs::REVERSE),
107 _ => None,
108 };
109 let is_code = toggle.is_some() || matches!(c, COLOR | HEX_COLOR | RESET);
110 if !is_code {
111 i += 1;
112 continue;
113 }
114 flush(start, i, style, &mut out);
115 i += 1;
116 if let Some(a) = toggle {
117 style.attrs = if style.attrs.contains(a) {
118 style.attrs.remove(a)
119 } else {
120 style.attrs.union(a)
121 };
122 } else if c == RESET {
123 style = Style::default();
124 } else if c == COLOR {
125 match digits(b, i) {
126 Some((fg, n)) => {
127 i += n;
128 style.fg = color(fg);
129 if b.get(i) == Some(&b',') {
132 if let Some((bg, n)) = digits(b, i + 1) {
133 i += 1 + n;
134 style.bg = color(bg);
135 }
136 }
137 }
138 None => {
139 style.fg = ColorRef::Default;
140 style.bg = ColorRef::Default;
141 }
142 }
143 } else {
144 match hex(b, i) {
145 Some(fg) => {
146 i += 6;
147 style.fg = ColorRef::Rgb(fg);
148 if b.get(i) == Some(&b',') {
149 if let Some(bg) = hex(b, i + 1) {
150 i += 7;
151 style.bg = ColorRef::Rgb(bg);
152 }
153 }
154 }
155 None => {
156 style.fg = ColorRef::Default;
157 style.bg = ColorRef::Default;
158 }
159 }
160 }
161 start = i;
162 }
163 flush(start, b.len(), style, &mut out);
164 out
165}
166
167fn digits(b: &[u8], i: usize) -> Option<(u8, usize)> {
169 let d0 = *b.get(i).filter(|c| c.is_ascii_digit())?;
170 match b.get(i + 1).filter(|c| c.is_ascii_digit()) {
171 Some(d1) => Some(((d0 - b'0') * 10 + (d1 - b'0'), 2)),
172 None => Some((d0 - b'0', 1)),
173 }
174}
175
176fn hex(b: &[u8], i: usize) -> Option<u32> {
178 let s = b.get(i..i + 6)?;
179 if !s.iter().all(u8::is_ascii_hexdigit) {
180 return None;
181 }
182 u32::from_str_radix(std::str::from_utf8(s).ok()?, 16).ok()
183}
184
185pub fn parse(text: &str) -> ParsedText {
187 let mut out = ParsedText::default();
188 for seg in segments(text) {
189 let start = out.text.len();
190 out.text.push_str(&text[seg.range]);
191 if seg.style != Style::default() {
192 out.spans.push(Span {
193 range: start..out.text.len(),
194 style: seg.style,
195 });
196 }
197 }
198 out
199}
200
201pub fn strip(text: &str) -> String {
204 segments(text).into_iter().map(|s| &text[s.range]).collect()
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 fn styled(text: &str) -> Vec<(&str, Style)> {
212 segments(text)
213 .into_iter()
214 .map(|s| (&text[s.range], s.style))
215 .collect()
216 }
217
218 fn with(attrs: Attrs) -> Style {
219 Style::default().with_attrs(attrs)
220 }
221
222 #[test]
223 fn plain_text_is_one_default_segment() {
224 assert_eq!(styled("hello there"), [("hello there", Style::default())]);
225 }
226
227 #[test]
228 fn toggles_turn_on_and_off() {
229 assert_eq!(
230 styled("a\x02b\x02c"),
231 [
232 ("a", Style::default()),
233 ("b", with(Attrs::BOLD)),
234 ("c", Style::default())
235 ]
236 );
237 assert_eq!(
238 styled("\x1ditalic\x1d \x1funder\x1f \x1estrike\x1e \x11mono"),
239 [
240 ("italic", with(Attrs::ITALIC)),
241 (" ", Style::default()),
242 ("under", with(Attrs::UNDERLINE)),
243 (" ", Style::default()),
244 ("strike", with(Attrs::STRIKETHROUGH)),
245 (" ", Style::default()),
246 ("mono", with(Attrs::CODE)),
247 ]
248 );
249 }
250
251 #[test]
252 fn toggles_nest() {
253 assert_eq!(
254 styled("\x02b\x1dbi\x02i"),
255 [
256 ("b", with(Attrs::BOLD)),
257 ("bi", with(Attrs::BOLD.union(Attrs::ITALIC))),
258 ("i", with(Attrs::ITALIC)),
259 ]
260 );
261 }
262
263 #[test]
264 fn colors_by_number() {
265 let s = styled("\x034red\x03 plain");
266 assert_eq!(
267 s[0],
268 ("red", Style::default().with_fg(ColorRef::Palette(4)))
269 );
270 assert_eq!(s[1], (" plain", Style::default()));
271
272 let s = styled("\x0304,12on blue");
273 assert_eq!(s[0].0, "on blue");
274 assert_eq!(s[0].1.fg, ColorRef::Palette(4));
275 assert_eq!(s[0].1.bg, ColorRef::Palette(12));
276
277 assert_eq!(
278 styled("\x0352x")[0].1.fg,
279 ColorRef::Rgb(0xff0000),
280 "extended colors are RGB"
281 );
282 assert_eq!(
283 styled("\x0399x")[0].1.fg,
284 ColorRef::Default,
285 "99 is the default"
286 );
287 }
288
289 #[test]
290 fn a_comma_without_a_background_is_text() {
291 let s = styled("\x034,hello");
292 assert_eq!(
293 s[0],
294 (",hello", Style::default().with_fg(ColorRef::Palette(4)))
295 );
296 }
297
298 #[test]
299 fn a_third_digit_is_text() {
300 let s = styled("\x03123");
301 assert_eq!(s[0], ("3", Style::default().with_fg(ColorRef::Palette(12))));
302 }
303
304 #[test]
305 fn a_bare_color_code_resets_colors_but_not_attributes() {
306 let s = styled("\x02\x034,5x\x03y");
307 assert_eq!(s[1].0, "y");
308 assert_eq!(s[1].1, with(Attrs::BOLD));
309 }
310
311 #[test]
312 fn hex_colors() {
313 let s = styled("\x04ff8800,000000hi");
314 assert_eq!(s[0].0, "hi");
315 assert_eq!(s[0].1.fg, ColorRef::Rgb(0xff8800));
316 assert_eq!(s[0].1.bg, ColorRef::Rgb(0x000000));
317 assert_eq!(
318 styled("\x04zzz")[0],
319 ("zzz", Style::default()),
320 "not hex: a reset, then text"
321 );
322 }
323
324 #[test]
325 fn reset_clears_everything() {
326 let s = styled("\x02\x1d\x034x\x0fy");
327 assert_eq!(s.last().unwrap(), &("y", Style::default()));
328 }
329
330 #[test]
331 fn reverse_is_carried_for_the_renderer() {
332 assert_eq!(styled("\x16rev")[0], ("rev", with(Attrs::REVERSE)));
333 }
334
335 #[test]
336 fn multibyte_text_survives_on_boundaries() {
337 let text = "\x02ñandú\x02 → ok";
338 for seg in segments(text) {
339 assert!(text.is_char_boundary(seg.range.start) && text.is_char_boundary(seg.range.end));
340 }
341 assert_eq!(strip(text), "ñandú → ok");
342 }
343
344 #[test]
345 fn parse_joins_segments_into_styled_text() {
346 let p = parse("a \x02bold\x02 b");
347 assert_eq!(p.text, "a bold b");
348 assert_eq!(p.spans.len(), 1);
349 assert_eq!(&p.text[p.spans[0].range.clone()], "bold");
350 p.debug_assert_well_formed();
351 }
352
353 #[test]
354 fn codes_alone_produce_nothing() {
355 assert!(segments("\x02\x03\x0f").is_empty());
356 assert_eq!(parse("\x02\x02").text, "");
357 }
358}