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