1use super::backend::{DrawBackend, FontFace, TextStyle};
11use super::RenderError;
12
13#[derive(Clone, Debug, PartialEq)]
15pub struct RichRun {
16 pub text: String,
17 pub bold: bool,
18 pub italic: bool,
19 pub shift: i8,
21 pub color: Option<(u8, u8, u8)>,
22}
23
24impl RichRun {
25 pub fn face(&self, base: FontFace) -> FontFace {
27 if self.bold || base == FontFace::Bold {
28 FontFace::Bold
29 } else if self.italic || base == FontFace::Italic {
30 FontFace::Italic
31 } else {
32 FontFace::Plain
33 }
34 }
35}
36
37#[derive(Clone, Copy, Default)]
38struct State {
39 bold: bool,
40 italic: bool,
41 shift: i8,
42 color: Option<(u8, u8, u8)>,
43}
44
45fn parse_color(css: &str) -> Option<(u8, u8, u8)> {
46 let v = css.split(';').find_map(|d| {
47 let (k, v) = d.split_once(':')?;
48 (k.trim().eq_ignore_ascii_case("color")).then(|| v.trim().to_string())
49 })?;
50 if let Some(hex) = v.strip_prefix('#') {
51 let n = |i: usize| u8::from_str_radix(hex.get(i..i + 2)?, 16).ok();
52 return match hex.len() {
53 6 => Some((n(0)?, n(2)?, n(4)?)),
54 3 => {
55 let d = |i: usize| {
56 u8::from_str_radix(hex.get(i..i + 1)?, 16)
57 .ok()
58 .map(|x| x * 17)
59 };
60 Some((d(0)?, d(1)?, d(2)?))
61 }
62 _ => None,
63 };
64 }
65 let named = [
66 ("black", (0, 0, 0)),
67 ("white", (255, 255, 255)),
68 ("red", (255, 0, 0)),
69 ("green", (0, 128, 0)),
70 ("blue", (0, 0, 255)),
71 ("grey", (128, 128, 128)),
72 ("gray", (128, 128, 128)),
73 ("orange", (255, 165, 0)),
74 ("purple", (128, 0, 128)),
75 ("darkred", (139, 0, 0)),
76 ("darkgreen", (0, 100, 0)),
77 ("darkblue", (0, 0, 139)),
78 ("navy", (0, 0, 128)),
79 ("steelblue", (70, 130, 180)),
80 ("firebrick", (178, 34, 34)),
81 ("forestgreen", (34, 139, 34)),
82 ("gold", (255, 215, 0)),
83 ("brown", (165, 42, 42)),
84 ];
85 let v = v.to_ascii_lowercase();
86 named.iter().find(|(n, _)| *n == v).map(|(_, c)| *c)
87}
88
89pub fn has_markup(text: &str) -> bool {
91 text.contains("**")
92 || text.contains('*')
93 || text.contains('_')
94 || text.contains('^')
95 || text.contains('~')
96 || text.contains('<')
97 || text.contains('\\')
98}
99
100pub fn parse(text: &str) -> Vec<RichRun> {
102 let mut runs: Vec<RichRun> = Vec::new();
103 let mut stack: Vec<State> = vec![State::default()];
104 let mut buf = String::new();
105 let flush = |buf: &mut String, runs: &mut Vec<RichRun>, st: State| {
106 if buf.is_empty() {
107 return;
108 }
109 let run = RichRun {
110 text: std::mem::take(buf),
111 bold: st.bold,
112 italic: st.italic,
113 shift: st.shift,
114 color: st.color,
115 };
116 match runs.last_mut() {
117 Some(last)
118 if last.bold == run.bold
119 && last.italic == run.italic
120 && last.shift == run.shift
121 && last.color == run.color =>
122 {
123 last.text.push_str(&run.text)
124 }
125 _ => runs.push(run),
126 }
127 };
128 let mut open_md: Vec<&'static str> = Vec::new();
130 let mut rest = text;
131 while let Some(ch) = rest.chars().next() {
132 let cur = *stack.last().unwrap();
133 if ch == '\\' {
135 if let Some(next) = rest[1..].chars().next() {
136 buf.push(next);
137 rest = &rest[1 + next.len_utf8()..];
138 } else {
139 buf.push('\\');
140 rest = &rest[1..];
141 }
142 continue;
143 }
144 let md = ["**", "*", "_", "^", "~"]
146 .into_iter()
147 .find(|d| rest.starts_with(d));
148 if let Some(d) = md {
149 let closing = open_md.last() == Some(&d);
150 if closing || rest[d.len()..].contains(d) {
152 flush(&mut buf, &mut runs, cur);
153 if closing {
154 open_md.pop();
155 stack.pop();
156 } else {
157 open_md.push(d);
158 let mut s = cur;
159 match d {
160 "**" => s.bold = true,
161 "*" | "_" => s.italic = true,
162 "^" => s.shift = 1,
163 _ => s.shift = -1,
164 }
165 stack.push(s);
166 }
167 rest = &rest[d.len()..];
168 continue;
169 }
170 }
171 if ch == '<' {
173 if let Some(end) = rest.find('>') {
174 let tag = &rest[1..end];
175 let lower = tag.trim().to_ascii_lowercase();
176 let (closing, name) = match lower.strip_prefix('/') {
177 Some(n) => (true, n.trim().to_string()),
178 None => (
179 false,
180 lower.split_whitespace().next().unwrap_or("").to_string(),
181 ),
182 };
183 if matches!(
184 name.as_str(),
185 "b" | "strong" | "i" | "em" | "sup" | "sub" | "span"
186 ) {
187 flush(&mut buf, &mut runs, cur);
188 if closing {
189 if stack.len() > 1 {
190 stack.pop();
191 }
192 } else {
193 let mut s = cur;
194 match name.as_str() {
195 "b" | "strong" => s.bold = true,
196 "i" | "em" => s.italic = true,
197 "sup" => s.shift = 1,
198 "sub" => s.shift = -1,
199 _ => {
200 if let Some(i) = tag.find("style=") {
202 let v = &tag[i + 6..];
203 let q = v.chars().next().unwrap_or('"');
204 let body = v[q.len_utf8()..].split(q).next().unwrap_or("");
205 if let Some(c) = parse_color(body) {
206 s.color = Some(c);
207 }
208 }
209 }
210 }
211 stack.push(s);
212 }
213 rest = &rest[end + 1..];
214 continue;
215 }
216 }
217 }
218 buf.push(ch);
219 rest = &rest[ch.len_utf8()..];
220 }
221 let cur = *stack.last().unwrap();
222 flush(&mut buf, &mut runs, cur);
223 runs
224}
225
226pub fn run_size(size: f64, shift: i8) -> f64 {
228 if shift == 0 {
229 size
230 } else {
231 size * 0.7
232 }
233}
234
235pub fn draw(
238 backend: &mut dyn DrawBackend,
239 rich: bool,
240 text: &str,
241 pos: (f64, f64),
242 style: &TextStyle,
243) -> Result<(), RenderError> {
244 if rich && has_markup(text) {
245 let runs = parse(text);
246 backend.draw_rich_text(&runs, pos, style)
247 } else {
248 backend.draw_text(text, pos, style)
249 }
250}
251
252pub fn plain(text: &str) -> String {
254 parse(text).into_iter().map(|r| r.text).collect()
255}
256
257pub type RunWidth<'a> = &'a dyn Fn(&str, f64, FontFace) -> f64;
259
260pub fn layout_runs(
265 runs: &[RichRun],
266 (x, y): (f64, f64),
267 style: &TextStyle,
268 width: RunWidth<'_>,
269) -> Vec<(String, (f64, f64), TextStyle)> {
270 use super::backend::TextAnchor;
271 let widths: Vec<f64> = runs
272 .iter()
273 .map(|r| width(&r.text, run_size(style.size, r.shift), r.face(style.face)))
274 .collect();
275 let total: f64 = widths.iter().sum();
276 let lead = match style.anchor {
277 TextAnchor::Start => 0.0,
278 TextAnchor::Middle => total / 2.0,
279 TextAnchor::End => total,
280 };
281 let angle = if style.angle.is_finite() {
282 style.angle
283 } else {
284 0.0
285 };
286 let (sin, cos) = angle.to_radians().sin_cos();
287 let mut along = -lead;
288 let mut out = Vec::with_capacity(runs.len());
289 for (run, w) in runs.iter().zip(&widths) {
290 let up = match run.shift {
292 1 => style.size * 0.33,
293 -1 => -style.size * 0.2,
294 _ => 0.0,
295 };
296 let px = x + along * cos + up * sin;
297 let py = y + along * sin - up * cos;
298 out.push((
299 run.text.clone(),
300 (px, py),
301 TextStyle {
302 size: run_size(style.size, run.shift),
303 face: run.face(style.face),
304 color: run.color.unwrap_or(style.color),
305 anchor: TextAnchor::Start,
306 ..style.clone()
307 },
308 ));
309 along += w;
310 }
311 out
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 type Summary = (String, bool, bool, i8, Option<(u8, u8, u8)>);
319
320 fn summary(t: &str) -> Vec<Summary> {
321 parse(t)
322 .into_iter()
323 .map(|r| (r.text, r.bold, r.italic, r.shift, r.color))
324 .collect()
325 }
326
327 #[test]
328 fn markdown_and_html() {
329 assert_eq!(
330 summary("a **b** *c* x^2^ H~2~O"),
331 vec![
332 ("a ".into(), false, false, 0, None),
333 ("b".into(), true, false, 0, None),
334 (" ".into(), false, false, 0, None),
335 ("c".into(), false, true, 0, None),
336 (" x".into(), false, false, 0, None),
337 ("2".into(), false, false, 1, None),
338 (" H".into(), false, false, 0, None),
339 ("2".into(), false, false, -1, None),
340 ("O".into(), false, false, 0, None),
341 ]
342 );
343 assert_eq!(
344 summary("<span style='color:#ff0000'>red <b>bold</b></span>!"),
345 vec![
346 ("red ".into(), false, false, 0, Some((255, 0, 0))),
347 ("bold".into(), true, false, 0, Some((255, 0, 0))),
348 ("!".into(), false, false, 0, None),
349 ]
350 );
351 }
352
353 #[test]
354 fn unmatched_delimiters_and_escapes_stay_literal() {
355 assert_eq!(plain("3 * 4 = 12"), "3 * 4 = 12");
356 assert_eq!(plain("snake_case"), "snake_case");
357 assert_eq!(plain(r"\*not italic\*"), "*not italic*");
358 assert_eq!(plain("a < b"), "a < b");
359 assert_eq!(plain("<u>x</u>"), "<u>x</u>");
360 }
361}