1#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub struct Style {
19 pub bold: bool,
20 pub italic: bool,
21 pub underline: bool,
22 pub strike: bool,
23 pub color: Option<u32>,
25}
26
27fn apply(attrs: &str, mut style: Style) -> Option<Style> {
30 let mut any = false;
31 for a in attrs.split_whitespace() {
32 any = true;
33 match a {
34 "b" => style.bold = true,
35 "i" => style.italic = true,
36 "u" => style.underline = true,
37 "s" => style.strike = true,
38 hex if hex.len() == 7 && hex.starts_with('#') => {
39 style.color = Some(u32::from_str_radix(&hex[1..], 16).ok()?)
40 }
41 _ => return None,
42 }
43 }
44 any.then_some(style)
45}
46
47fn has_markup(s: &str) -> bool {
50 s.bytes().any(|b| matches!(b, b'{' | b'}' | 0xC2))
52}
53
54enum Token<'a> {
56 Char(char),
57 Open(&'a str),
59 Close,
60}
61
62fn tokens(s: &str) -> impl Iterator<Item = Token<'_>> {
64 pieces(s).map(|(t, _)| t)
65}
66
67struct Pieces<'a> {
70 s: &'a str,
71 i: usize,
72 depth: usize,
73}
74
75impl<'a> Iterator for Pieces<'a> {
76 type Item = (Token<'a>, &'a str);
77
78 fn next(&mut self) -> Option<Self::Item> {
79 let (s, i) = (self.s, self.i);
80 let b = s.as_bytes();
81 let c = s[i..].chars().next()?;
82 match c {
83 '{' if b.get(i + 1) == Some(&b'{') => {
84 self.i += 2;
85 return Some((Token::Char('{'), &s[i..i + 2]));
86 }
87 '{' => {
88 if let Some(bar) = s[i + 1..].find(['|', '{', '}']).map(|p| p + i + 1)
89 && b[bar] == b'|'
90 && apply(&s[i + 1..bar], Style::default()).is_some()
91 {
92 self.depth += 1;
93 self.i = bar + 1;
94 return Some((Token::Open(&s[i + 1..bar]), &s[i..=bar]));
95 }
96 }
97 '}' if self.depth > 0 => {
98 self.depth -= 1;
99 self.i += 1;
100 return Some((Token::Close, &s[i..=i]));
101 }
102 '}' if b.get(i + 1) == Some(&b'}') => {
103 self.i += 2;
104 return Some((Token::Char('}'), &s[i..i + 2]));
105 }
106 _ => {}
107 }
108 self.i += c.len_utf8();
109 Some((Token::Char(c), &s[i..i + c.len_utf8()]))
110 }
111}
112
113fn pieces(s: &str) -> Pieces<'_> {
114 Pieces { s, i: 0, depth: 0 }
115}
116
117pub fn words(s: &str) -> Vec<&str> {
119 let b = s.as_bytes();
122 let mut out = Vec::new();
123 let (mut start, mut i, mut depth) = (0, 0, 0usize);
124 while i < b.len() {
125 match b[i] {
126 b'{' if b.get(i + 1) == Some(&b'{') => i += 2,
127 b'{' => match s[i + 1..].find(['|', '{', '}']).map(|p| p + i + 1) {
128 Some(bar)
129 if b[bar] == b'|' && apply(&s[i + 1..bar], Style::default()).is_some() =>
130 {
131 depth += 1;
132 i = bar + 1;
133 }
134 _ => i += 1,
135 },
136 b'}' if depth > 0 => {
137 depth -= 1;
138 i += 1;
139 }
140 b'}' if b.get(i + 1) == Some(&b'}') => i += 2,
141 b' ' => {
142 out.push(&s[start..i]);
143 start = i + 1;
144 i += 1;
145 }
146 _ => i += 1,
147 }
148 }
149 out.push(&s[start..]);
150 out
151}
152
153pub fn glyphs(s: &str) -> Vec<(&str, Option<char>)> {
156 pieces(s)
157 .map(|(t, raw)| match t {
158 Token::Char(c) => (raw, Some(c)),
159 _ => (raw, None),
160 })
161 .collect()
162}
163
164pub fn map_attrs(s: &str, f: impl Fn(&str) -> Option<String>) -> String {
168 let mut out = String::with_capacity(s.len());
169 let b = s.as_bytes();
170 let mut i = 0;
171 while i < s.len() {
172 if b[i] == b'{' && b.get(i + 1) == Some(&b'{') {
173 out.push_str("{{");
174 i += 2;
175 continue;
176 }
177 if b[i] == b'{'
178 && let Some(bar) = s[i + 1..].find(['|', '{', '}']).map(|p| p + i + 1)
179 && b[bar] == b'|'
180 {
181 let words: Vec<String> = s[i + 1..bar]
182 .split_whitespace()
183 .map(|w| f(w).unwrap_or_else(|| w.to_string()))
184 .collect();
185 out.push('{');
186 out.push_str(&words.join(" "));
187 out.push('|');
188 i = bar + 1;
189 continue;
190 }
191 let c = s[i..].chars().next().unwrap_or_default();
192 out.push(c);
193 i += c.len_utf8();
194 }
195 out
196}
197
198pub fn runs(s: &str) -> Vec<(String, Style)> {
200 let mut out: Vec<(String, Style)> = Vec::new();
201 let mut stack = vec![Style::default()];
202 for t in tokens(s) {
203 match t {
204 Token::Char(c) => {
205 let st = *stack.last().unwrap_or(&Style::default());
206 match out.last_mut() {
207 Some((text, last)) if *last == st => text.push(c),
208 _ => out.push((c.to_string(), st)),
209 }
210 }
211 Token::Open(a) => {
212 let top = *stack.last().unwrap_or(&Style::default());
213 stack.push(apply(a, top).unwrap_or(top));
214 }
215 Token::Close => {
216 if stack.len() > 1 {
217 stack.pop();
218 }
219 }
220 }
221 }
222 out
223}
224
225pub fn plain(s: &str) -> String {
227 runs(s).into_iter().map(|(t, _)| t).collect()
228}
229
230pub fn width(s: &str, mut advance: impl FnMut(char) -> f32) -> f32 {
233 if !has_markup(s) {
234 return s.chars().map(advance).sum();
235 }
236 let mut w = 0.0;
239 let mut outer: Vec<Style> = Vec::new();
240 let mut style = Style::default();
241 let mut legacy_bold = false;
242 let mut code = false;
243 for t in tokens(s) {
244 match t {
245 Token::Char(c) => {
246 if code {
247 code = false;
248 match c.to_ascii_lowercase() {
249 'l' => legacy_bold = true,
250 k if k == 'r' || k.is_ascii_hexdigit() => legacy_bold = false,
251 _ => {}
252 }
253 continue;
254 }
255 if c == '§' {
256 code = true;
257 continue;
258 }
259 w += advance(c) + if style.bold || legacy_bold { 1.0 } else { 0.0 };
260 }
261 Token::Open(a) => {
262 outer.push(style);
263 let next = apply(a, style).unwrap_or(style);
264 if next != style {
265 (legacy_bold, code) = (false, false);
266 }
267 style = next;
268 }
269 Token::Close => {
270 if let Some(prev) = outer.pop() {
271 if prev != style {
272 (legacy_bold, code) = (false, false);
273 }
274 style = prev;
275 }
276 }
277 }
278 }
279 w
280}
281
282pub fn open_spans(s: &str) -> Vec<String> {
286 let mut stack = Vec::new();
287 for t in tokens(s) {
288 match t {
289 Token::Open(a) => stack.push(a.to_string()),
290 Token::Close => {
291 stack.pop();
292 }
293 Token::Char(_) => {}
294 }
295 }
296 stack
297}
298
299pub fn reopen(spans: &[String]) -> String {
301 spans.iter().map(|a| format!("{{{a}|")).collect()
302}
303
304#[cfg(test)]
305mod tests {
306 fn width_via_runs(s: &str, advance: impl Fn(char) -> f32) -> f32 {
308 let mut w = 0.0;
309 for (text, st) in super::runs(s) {
310 let mut legacy_bold = false;
311 let mut chars = text.chars();
312 while let Some(c) = chars.next() {
313 if c == '§' {
314 match chars.next().map(|k| k.to_ascii_lowercase()) {
315 Some('l') => legacy_bold = true,
316 Some(k) if k == 'r' || k.is_ascii_hexdigit() => legacy_bold = false,
317 _ => {}
318 }
319 continue;
320 }
321 w += advance(c) + if st.bold || legacy_bold { 1.0 } else { 0.0 };
322 }
323 }
324 w
325 }
326
327 fn words_via_pieces(s: &str) -> Vec<&str> {
329 let (mut out, mut start, mut at) = (Vec::new(), 0, 0);
330 for (t, raw) in super::pieces(s) {
331 if matches!(t, super::Token::Char(' ')) {
332 out.push(&s[start..at]);
333 start = at + 1;
334 }
335 at += raw.len();
336 }
337 out.push(&s[start..]);
338 out
339 }
340
341 #[test]
342 fn words_split_as_pieces_do() {
343 for s in [
344 "a b c",
345 "{b #ff0000|bold words} x",
346 "{{a b}} c",
347 "{nope b|x} y",
348 "ünï ç {i|é ö}",
349 "{b|unclosed a b",
350 " lead trail ",
351 ] {
352 assert_eq!(super::words(s), words_via_pieces(s), "{s:?}");
353 }
354 }
355
356 #[test]
357 fn width_measures_as_runs_do() {
358 let adv = |c: char| if c == 'i' { 2.0 } else { 6.0 };
359 for s in [
360 "plain",
361 "{b|bold} x",
362 "{b|a {#ff0000|b} c}d",
363 "§lbold§r then",
364 "{{x}} }}",
365 "{nope|x}",
366 "{b|unclosed",
367 "a§",
368 "{b|§lx} y",
369 "ünï {i|ç}",
370 ] {
371 assert_eq!(super::width(s, adv), width_via_runs(s, adv), "{s:?}");
372 }
373 }
374
375 use super::*;
376
377 #[test]
378 fn parses_spans() {
379 let r = runs("a {b|bold {#8d6acc|purple}} {{x}} {not a span}");
380 assert_eq!(r[0], ("a ".to_string(), Style::default()));
381 assert_eq!(r[1].0, "bold ");
382 assert!(r[1].1.bold);
383 assert_eq!(
384 r[2],
385 (
386 "purple".to_string(),
387 Style {
388 bold: true,
389 color: Some(0x8d6acc),
390 ..Default::default()
391 }
392 )
393 );
394 assert_eq!(r[3].0, " {x} {not a span}");
395 assert_eq!(plain("{i|x}y"), "xy");
396 }
397
398 #[test]
399 fn producers_map_their_words() {
400 let s = map_attrs("a {accent|term} {{x}} {b link=m:e|y}", |w| match w {
401 "accent" => Some("#8d6acc".into()),
402 w if w.starts_with("link=") => Some("u #2f5fbf".into()),
403 _ => None,
404 });
405 assert_eq!(s, "a {#8d6acc|term} {{x}} {b u #2f5fbf|y}");
406 }
407
408 #[test]
409 fn spans_carry_over_lines() {
410 assert_eq!(open_spans("a {b|b {#ffffff|c} d"), ["b"]);
411 assert_eq!(reopen(&["b".into(), "#123456".into()]), "{b|{#123456|");
412 assert_eq!(width("{b|ab}", |_| 5.0), 12.0, "bold is one wider a glyph");
413 assert_eq!(width("§5ab", |_| 5.0), 10.0);
414 }
415}