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
45enum Token<'a> {
47 Char(char),
48 Open(&'a str),
50 Close,
51}
52
53fn tokens(s: &str) -> Vec<Token<'_>> {
55 pieces(s).into_iter().map(|(t, _)| t).collect()
56}
57
58fn pieces(s: &str) -> Vec<(Token<'_>, &str)> {
60 let mut out = Vec::new();
61 let mut depth = 0usize;
62 let mut i = 0;
63 let b = s.as_bytes();
64 while i < s.len() {
65 let c = s[i..].chars().next().unwrap_or_default();
66 match c {
67 '{' if b.get(i + 1) == Some(&b'{') => {
68 out.push((Token::Char('{'), &s[i..i + 2]));
69 i += 2;
70 continue;
71 }
72 '{' => {
73 if let Some(bar) = s[i + 1..].find(['|', '{', '}']).map(|p| p + i + 1)
74 && b[bar] == b'|'
75 && apply(&s[i + 1..bar], Style::default()).is_some()
76 {
77 out.push((Token::Open(&s[i + 1..bar]), &s[i..=bar]));
78 depth += 1;
79 i = bar + 1;
80 continue;
81 }
82 }
83 '}' if depth > 0 => {
84 out.push((Token::Close, &s[i..=i]));
85 depth -= 1;
86 i += 1;
87 continue;
88 }
89 '}' if b.get(i + 1) == Some(&b'}') => {
90 out.push((Token::Char('}'), &s[i..i + 2]));
91 i += 2;
92 continue;
93 }
94 _ => {}
95 }
96 out.push((Token::Char(c), &s[i..i + c.len_utf8()]));
97 i += c.len_utf8();
98 }
99 out
100}
101
102pub fn words(s: &str) -> Vec<&str> {
104 let mut out = Vec::new();
105 let mut start = 0;
106 let mut at = 0;
107 for (t, raw) in pieces(s) {
108 if matches!(t, Token::Char(' ')) {
109 out.push(&s[start..at]);
110 start = at + 1;
111 }
112 at += raw.len();
113 }
114 out.push(&s[start..]);
115 out
116}
117
118pub fn glyphs(s: &str) -> Vec<(&str, Option<char>)> {
121 pieces(s)
122 .into_iter()
123 .map(|(t, raw)| match t {
124 Token::Char(c) => (raw, Some(c)),
125 _ => (raw, None),
126 })
127 .collect()
128}
129
130pub fn map_attrs(s: &str, f: impl Fn(&str) -> Option<String>) -> String {
134 let mut out = String::with_capacity(s.len());
135 let b = s.as_bytes();
136 let mut i = 0;
137 while i < s.len() {
138 if b[i] == b'{' && b.get(i + 1) == Some(&b'{') {
139 out.push_str("{{");
140 i += 2;
141 continue;
142 }
143 if b[i] == b'{'
144 && let Some(bar) = s[i + 1..].find(['|', '{', '}']).map(|p| p + i + 1)
145 && b[bar] == b'|'
146 {
147 let words: Vec<String> = s[i + 1..bar].split_whitespace().map(|w| f(w).unwrap_or_else(|| w.to_string())).collect();
148 out.push('{');
149 out.push_str(&words.join(" "));
150 out.push('|');
151 i = bar + 1;
152 continue;
153 }
154 let c = s[i..].chars().next().unwrap_or_default();
155 out.push(c);
156 i += c.len_utf8();
157 }
158 out
159}
160
161pub fn runs(s: &str) -> Vec<(String, Style)> {
163 let mut out: Vec<(String, Style)> = Vec::new();
164 let mut stack = vec![Style::default()];
165 for t in tokens(s) {
166 match t {
167 Token::Char(c) => {
168 let st = *stack.last().unwrap_or(&Style::default());
169 match out.last_mut() {
170 Some((text, last)) if *last == st => text.push(c),
171 _ => out.push((c.to_string(), st)),
172 }
173 }
174 Token::Open(a) => {
175 let top = *stack.last().unwrap_or(&Style::default());
176 stack.push(apply(a, top).unwrap_or(top));
177 }
178 Token::Close => {
179 if stack.len() > 1 {
180 stack.pop();
181 }
182 }
183 }
184 }
185 out
186}
187
188pub fn plain(s: &str) -> String {
190 runs(s).into_iter().map(|(t, _)| t).collect()
191}
192
193pub fn width(s: &str, mut advance: impl FnMut(char) -> f32) -> f32 {
196 let mut w = 0.0;
197 for (text, st) in runs(s) {
198 let mut legacy_bold = false;
199 let mut chars = text.chars();
200 while let Some(c) = chars.next() {
201 if c == '§' {
202 match chars.next().map(|k| k.to_ascii_lowercase()) {
203 Some('l') => legacy_bold = true,
204 Some(k) if k == 'r' || k.is_ascii_hexdigit() => legacy_bold = false,
205 _ => {}
206 }
207 continue;
208 }
209 w += advance(c) + if st.bold || legacy_bold { 1.0 } else { 0.0 };
210 }
211 }
212 w
213}
214
215pub fn open_spans(s: &str) -> Vec<String> {
219 let mut stack = Vec::new();
220 for t in tokens(s) {
221 match t {
222 Token::Open(a) => stack.push(a.to_string()),
223 Token::Close => {
224 stack.pop();
225 }
226 Token::Char(_) => {}
227 }
228 }
229 stack
230}
231
232pub fn reopen(spans: &[String]) -> String {
234 spans.iter().map(|a| format!("{{{a}|")).collect()
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn parses_spans() {
243 let r = runs("a {b|bold {#8d6acc|purple}} {{x}} {not a span}");
244 assert_eq!(r[0], ("a ".to_string(), Style::default()));
245 assert_eq!(r[1].0, "bold ");
246 assert!(r[1].1.bold);
247 assert_eq!(r[2], ("purple".to_string(), Style { bold: true, color: Some(0x8d6acc), ..Default::default() }));
248 assert_eq!(r[3].0, " {x} {not a span}");
249 assert_eq!(plain("{i|x}y"), "xy");
250 }
251
252 #[test]
253 fn producers_map_their_words() {
254 let s = map_attrs("a {accent|term} {{x}} {b link=m:e|y}", |w| match w {
255 "accent" => Some("#8d6acc".into()),
256 w if w.starts_with("link=") => Some("u #2f5fbf".into()),
257 _ => None,
258 });
259 assert_eq!(s, "a {#8d6acc|term} {{x}} {b u #2f5fbf|y}");
260 }
261
262 #[test]
263 fn spans_carry_over_lines() {
264 assert_eq!(open_spans("a {b|b {#ffffff|c} d"), ["b"]);
265 assert_eq!(reopen(&["b".into(), "#123456".into()]), "{b|{#123456|");
266 assert_eq!(width("{b|ab}", |_| 5.0), 12.0, "bold is one wider a glyph");
267 assert_eq!(width("§5ab", |_| 5.0), 10.0);
268 }
269}