1use crate::font::{ChemGlyph, FaceStyle, ScriptRole};
14
15pub fn parse_label_markup(raw: &str, base: FaceStyle) -> Vec<ChemGlyph> {
20 let (bold0, italic0) = face_flags(base);
21 let mut out = Vec::new();
22 parse_into(raw, &mut out, bold0, italic0, false);
23 out
24}
25
26fn face_flags(style: FaceStyle) -> (bool, bool) {
27 match style {
28 FaceStyle::Regular => (false, false),
29 FaceStyle::Bold => (true, false),
30 FaceStyle::Italic => (false, true),
31 FaceStyle::BoldItalic => (true, true),
32 }
33}
34
35fn push_char(out: &mut Vec<ChemGlyph>, ch: char, role: ScriptRole, bold: bool, italic: bool) {
36 let (ch, role) = match unicode_script_base(ch) {
37 Some((base, mapped)) if role == ScriptRole::Normal => (base, mapped),
38 Some((base, _)) => (base, role),
39 None => (ch, role),
40 };
41 out.push(ChemGlyph {
42 ch,
43 role,
44 face: FaceStyle::from_flags(bold, italic),
45 });
46}
47
48fn parse_into(src: &str, out: &mut Vec<ChemGlyph>, bold: bool, italic: bool, in_math: bool) {
49 let chars: Vec<char> = src.chars().collect();
50 let mut i = 0usize;
51 while i < chars.len() {
52 let c = chars[i];
53
54 if c == '$' {
56 if let Some(j) = find_char(&chars, i + 1, '$') {
57 let inner: String = chars[i + 1..j].iter().collect();
58 parse_into(&inner, out, bold, italic, true);
59 i = j + 1;
60 continue;
61 }
62 push_char(out, c, ScriptRole::Normal, bold, italic);
63 i += 1;
64 continue;
65 }
66
67 if c == '\\' {
69 if i + 1 >= chars.len() {
70 push_char(out, '\\', ScriptRole::Normal, bold, italic);
71 i += 1;
72 continue;
73 }
74 let nxt = chars[i + 1];
75 if matches!(nxt, '\\' | '{' | '}' | '$' | '*' | '_' | '^') {
76 push_char(out, nxt, ScriptRole::Normal, bold, italic);
77 i += 2;
78 continue;
79 }
80 if nxt.is_ascii_alphabetic() {
81 let (name, after) = read_command_name(&chars, i + 1);
82 if let Some(sym) = lookup_symbol(&name) {
83 for ch in sym.chars() {
84 push_char(out, ch, ScriptRole::Normal, bold, italic);
85 }
86 i = after;
87 continue;
88 }
89 if let Some(style_kind) = lookup_style_cmd(&name) {
90 if after < chars.len() && chars[after] == '{' {
91 if let Some((body, end)) = read_braced(&chars, after) {
92 let (nb, ni) = match style_kind {
93 StyleKind::Bold => (true, italic),
94 StyleKind::Italic => (bold, true),
95 };
96 parse_into(&body, out, nb, ni, in_math);
97 i = end;
98 continue;
99 }
100 }
101 push_char(out, '\\', ScriptRole::Normal, bold, italic);
103 for ch in name.chars() {
104 push_char(out, ch, ScriptRole::Normal, bold, italic);
105 }
106 i = after;
107 continue;
108 }
109 push_char(out, '\\', ScriptRole::Normal, bold, italic);
111 for ch in name.chars() {
112 push_char(out, ch, ScriptRole::Normal, bold, italic);
113 }
114 i = after;
115 continue;
116 }
117 push_char(out, '\\', ScriptRole::Normal, bold, italic);
118 i += 1;
119 continue;
120 }
121
122 if c == '*' && i + 1 < chars.len() && chars[i + 1] == '*' {
124 if let Some(j) = find_str(&chars, i + 2, &['*', '*']) {
125 let inner: String = chars[i + 2..j].iter().collect();
126 parse_into(&inner, out, true, italic, in_math);
127 i = j + 2;
128 continue;
129 }
130 push_char(out, '*', ScriptRole::Normal, bold, italic);
131 push_char(out, '*', ScriptRole::Normal, bold, italic);
132 i += 2;
133 continue;
134 }
135
136 if c == '*' {
138 if let Some(j) = find_italic_close(&chars, i + 1) {
139 let inner: String = chars[i + 1..j].iter().collect();
140 parse_into(&inner, out, bold, true, in_math);
141 i = j + 1;
142 continue;
143 }
144 push_char(out, '*', ScriptRole::Normal, bold, italic);
145 i += 1;
146 continue;
147 }
148
149 if c == '^' || c == '_' {
151 let allow_bare = c == '^' || in_math;
152 let role = if c == '_' {
153 ScriptRole::Subscript
154 } else {
155 ScriptRole::Superscript
156 };
157 if i + 1 < chars.len() && chars[i + 1] == '{' {
158 let (body, end) = read_braced(&chars, i + 1).unwrap_or_else(|| (String::new(), i + 1));
159 for ch in body.chars() {
160 push_char(out, ch, role, bold, italic);
161 }
162 i = end;
163 continue;
164 }
165 if allow_bare && i + 1 < chars.len() {
166 push_char(out, chars[i + 1], role, bold, italic);
167 i += 2;
168 continue;
169 }
170 push_char(out, c, ScriptRole::Normal, bold, italic);
172 i += 1;
173 continue;
174 }
175
176 push_char(out, c, ScriptRole::Normal, bold, italic);
177 i += 1;
178 }
179}
180
181#[derive(Clone, Copy)]
182enum StyleKind {
183 Bold,
184 Italic,
185}
186
187fn lookup_style_cmd(name: &str) -> Option<StyleKind> {
188 match name {
189 "textbf" | "mathbf" | "bf" => Some(StyleKind::Bold),
190 "textit" | "mathit" | "it" | "emph" => Some(StyleKind::Italic),
191 _ => None,
192 }
193}
194
195fn lookup_symbol(name: &str) -> Option<&'static str> {
196 SYMBOLS.iter().find(|(n, _)| *n == name).map(|(_, u)| *u)
198}
199
200fn read_command_name(chars: &[char], start: usize) -> (String, usize) {
201 let mut j = start;
202 while j < chars.len() && chars[j].is_ascii_alphabetic() {
203 j += 1;
204 }
205 let raw: String = chars[start..j].iter().collect();
206 let mut name = raw.clone();
208 let mut end = j;
209 while !name.is_empty()
210 && lookup_symbol(&name).is_none()
211 && lookup_style_cmd(&name).is_none()
212 {
213 name.pop();
214 end -= 1;
215 }
216 if name.is_empty() {
217 (raw, j)
218 } else {
219 (name, end)
220 }
221}
222
223fn read_braced(chars: &[char], open_idx: usize) -> Option<(String, usize)> {
224 if open_idx >= chars.len() || chars[open_idx] != '{' {
225 return None;
226 }
227 let mut depth = 0i32;
228 let mut j = open_idx;
229 while j < chars.len() {
230 match chars[j] {
231 '{' => depth += 1,
232 '}' => {
233 depth -= 1;
234 if depth == 0 {
235 let body: String = chars[open_idx + 1..j].iter().collect();
236 return Some((body, j + 1));
237 }
238 }
239 _ => {}
240 }
241 j += 1;
242 }
243 None
244}
245
246fn find_char(chars: &[char], from: usize, target: char) -> Option<usize> {
247 (from..chars.len()).find(|&i| chars[i] == target)
248}
249
250fn find_str(chars: &[char], from: usize, pat: &[char]) -> Option<usize> {
251 let n = pat.len();
252 if n == 0 || from + n > chars.len() {
253 return None;
254 }
255 (from..=chars.len() - n).find(|&i| chars[i..i + n] == *pat)
256}
257
258fn find_italic_close(chars: &[char], from: usize) -> Option<usize> {
259 let mut j = from;
260 while j < chars.len() {
261 if chars[j] == '\\' {
262 j += 2;
263 continue;
264 }
265 if chars[j] == '*' {
266 if j + 1 < chars.len() && chars[j + 1] == '*' {
268 j += 2;
269 continue;
270 }
271 return Some(j);
272 }
273 j += 1;
274 }
275 None
276}
277
278fn unicode_script_base(ch: char) -> Option<(char, ScriptRole)> {
279 let sub = match ch {
280 '₀' => '0',
281 '₁' => '1',
282 '₂' => '2',
283 '₃' => '3',
284 '₄' => '4',
285 '₅' => '5',
286 '₆' => '6',
287 '₇' => '7',
288 '₈' => '8',
289 '₉' => '9',
290 _ => '\0',
291 };
292 if sub != '\0' {
293 return Some((sub, ScriptRole::Subscript));
294 }
295 let sup = match ch {
296 '⁰' => '0',
297 '¹' => '1',
298 '²' => '2',
299 '³' => '3',
300 '⁴' => '4',
301 '⁵' => '5',
302 '⁶' => '6',
303 '⁷' => '7',
304 '⁸' => '8',
305 '⁹' => '9',
306 '⁺' => '+',
307 '⁻' => '-',
308 _ => '\0',
309 };
310 if sup != '\0' {
311 return Some((sup, ScriptRole::Superscript));
312 }
313 None
314}
315
316const SYMBOLS: &[(&str, &str)] = &[
318 ("alpha", "α"),
319 ("beta", "β"),
320 ("gamma", "γ"),
321 ("delta", "δ"),
322 ("epsilon", "ε"),
323 ("varepsilon", "ε"),
324 ("zeta", "ζ"),
325 ("eta", "η"),
326 ("theta", "θ"),
327 ("vartheta", "ϑ"),
328 ("iota", "ι"),
329 ("kappa", "κ"),
330 ("lambda", "λ"),
331 ("mu", "μ"),
332 ("nu", "ν"),
333 ("xi", "ξ"),
334 ("omicron", "ο"),
335 ("pi", "π"),
336 ("varpi", "ϖ"),
337 ("rho", "ρ"),
338 ("varrho", "ϱ"),
339 ("sigma", "σ"),
340 ("varsigma", "ς"),
341 ("tau", "τ"),
342 ("upsilon", "υ"),
343 ("phi", "φ"),
344 ("varphi", "ϕ"),
345 ("chi", "χ"),
346 ("psi", "ψ"),
347 ("omega", "ω"),
348 ("Alpha", "Α"),
349 ("Beta", "Β"),
350 ("Gamma", "Γ"),
351 ("Delta", "Δ"),
352 ("Epsilon", "Ε"),
353 ("Zeta", "Ζ"),
354 ("Eta", "Η"),
355 ("Theta", "Θ"),
356 ("Iota", "Ι"),
357 ("Kappa", "Κ"),
358 ("Lambda", "Λ"),
359 ("Mu", "Μ"),
360 ("Nu", "Ν"),
361 ("Xi", "Ξ"),
362 ("Omicron", "Ο"),
363 ("Pi", "Π"),
364 ("Rho", "Ρ"),
365 ("Sigma", "Σ"),
366 ("Tau", "Τ"),
367 ("Upsilon", "Υ"),
368 ("Phi", "Φ"),
369 ("Chi", "Χ"),
370 ("Psi", "Ψ"),
371 ("Omega", "Ω"),
372 ("degree", "°"),
373 ("circ", "°"),
374 ("pm", "±"),
375 ("mp", "∓"),
376 ("times", "×"),
377 ("cdot", "·"),
378 ("ast", "∗"),
379 ("dagger", "†"),
380 ("ddagger", "‡"),
381 ("prime", "′"),
382 ("infty", "∞"),
383 ("approx", "≈"),
384 ("neq", "≠"),
385 ("ne", "≠"),
386 ("leq", "≤"),
387 ("le", "≤"),
388 ("geq", "≥"),
389 ("ge", "≥"),
390 ("rightarrow", "→"),
391 ("to", "→"),
392 ("leftarrow", "←"),
393 ("leftrightarrow", "↔"),
394 ("uparrow", "↑"),
395 ("downarrow", "↓"),
396 ("micro", "µ"),
397 ("AA", "Å"),
398 ("angstrom", "Å"),
399];
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 fn text_roles(raw: &str) -> Vec<(char, ScriptRole, FaceStyle)> {
406 parse_label_markup(raw, FaceStyle::Regular)
407 .into_iter()
408 .map(|g| (g.ch, g.role, g.face))
409 .collect()
410 }
411
412 #[test]
413 fn my_name_literal_underscore() {
414 let g = parse_label_markup("my_name", FaceStyle::Regular);
415 assert_eq!(g.len(), 7);
416 assert!(g.iter().all(|x| x.role == ScriptRole::Normal));
417 assert_eq!(g.iter().map(|x| x.ch).collect::<String>(), "my_name");
418 }
419
420 #[test]
421 fn bare_r_1_literal_dollar_r_1_subscript() {
422 let bare = parse_label_markup("R_1", FaceStyle::Regular);
423 assert_eq!(bare.iter().map(|x| x.ch).collect::<String>(), "R_1");
424 assert_eq!(bare[1].role, ScriptRole::Normal);
425
426 let math = parse_label_markup("$R_1$", FaceStyle::Regular);
427 assert_eq!(math.len(), 2);
428 assert_eq!(math[0].ch, 'R');
429 assert_eq!(math[1].ch, '1');
430 assert_eq!(math[1].role, ScriptRole::Subscript);
431 }
432
433 #[test]
434 fn braced_sub_outside_math() {
435 let g = parse_label_markup("H_{2}", FaceStyle::Regular);
436 assert_eq!(g.len(), 2);
437 assert_eq!(g[1].role, ScriptRole::Subscript);
438 assert_eq!(g[1].ch, '2');
439 }
440
441 #[test]
442 fn caret_super_bare() {
443 let g = parse_label_markup("R^2", FaceStyle::Regular);
444 assert_eq!(g[1].role, ScriptRole::Superscript);
445 assert_eq!(g[1].ch, '2');
446 }
447
448 #[test]
449 fn greek_and_delta() {
450 let g = parse_label_markup(r"\alpha-\beta", FaceStyle::Regular);
451 assert_eq!(g.iter().map(|x| x.ch).collect::<String>(), "α-β");
452 let d = parse_label_markup(r"\Delta\DeltaG", FaceStyle::Regular);
453 assert_eq!(d.iter().map(|x| x.ch).collect::<String>(), "ΔΔG");
454 }
455
456 #[test]
457 fn markdown_bold_italic() {
458 let g = parse_label_markup("**Et**OH", FaceStyle::Regular);
459 assert_eq!(g[0].face, FaceStyle::Bold);
460 assert_eq!(g[1].face, FaceStyle::Bold);
461 assert_eq!(g[2].face, FaceStyle::Regular);
462 let it = parse_label_markup("*cis*", FaceStyle::Regular);
463 assert!(it.iter().all(|x| x.face == FaceStyle::Italic));
464 }
465
466 #[test]
467 fn math_with_alpha_sub() {
468 let g = parse_label_markup(r"$\alpha_D$", FaceStyle::Regular);
469 assert_eq!(g[0].ch, 'α');
470 assert_eq!(g[1].ch, 'D');
471 assert_eq!(g[1].role, ScriptRole::Subscript);
472 }
473
474 #[test]
475 fn base_face_or() {
476 let g = parse_label_markup("*cis*", FaceStyle::Bold);
477 assert!(g.iter().all(|x| x.face == FaceStyle::BoldItalic));
478 }
479
480 #[test]
481 fn roles_smoke() {
482 let t = text_roles("$R^{2+}$");
483 assert_eq!(t[0].1, ScriptRole::Normal);
484 assert_eq!(t[1].1, ScriptRole::Superscript);
485 assert_eq!(t[2].1, ScriptRole::Superscript);
486 }
487
488 #[test]
489 fn face_flags_all_variants() {
490 assert_eq!(face_flags(FaceStyle::Italic), (false, true));
491 assert_eq!(face_flags(FaceStyle::BoldItalic), (true, true));
492 let g = parse_label_markup("A", FaceStyle::Italic);
493 assert_eq!(g[0].face, FaceStyle::Italic);
494 }
495
496 #[test]
497 fn unclosed_dollar_is_literal() {
498 let g = parse_label_markup("$R_1", FaceStyle::Regular);
499 assert_eq!(g.iter().map(|x| x.ch).collect::<String>(), "$R_1");
500 }
501
502 #[test]
503 fn backslash_escapes_and_trailing() {
504 let g = parse_label_markup(r"\_\{ \} \$ \* \\", FaceStyle::Regular);
505 let s: String = g.iter().map(|x| x.ch).collect();
506 assert!(s.contains('_'));
507 assert!(s.contains('{'));
508 assert!(s.contains('\\'));
509 let trail = parse_label_markup(r"x\", FaceStyle::Regular);
510 assert_eq!(trail.last().unwrap().ch, '\\');
511 let lone = parse_label_markup(r"\1", FaceStyle::Regular);
512 assert_eq!(lone[0].ch, '\\');
513 assert_eq!(lone[1].ch, '1');
514 }
515
516 #[test]
517 fn tex_style_commands() {
518 let g = parse_label_markup(r"\textbf{Et}OH", FaceStyle::Regular);
519 assert_eq!(g[0].face, FaceStyle::Bold);
520 assert_eq!(g[1].face, FaceStyle::Bold);
521 assert_eq!(g[2].face, FaceStyle::Regular);
522 let it = parse_label_markup(r"\textit{cis}", FaceStyle::Regular);
523 assert!(it.iter().all(|x| x.face == FaceStyle::Italic));
524 let no_brace = parse_label_markup(r"\textbf x", FaceStyle::Regular);
525 assert_eq!(no_brace[0].ch, '\\');
526 assert!(no_brace.iter().any(|x| x.ch == 'b'));
527 }
528
529 #[test]
530 fn unknown_command_kept_literal() {
531 let g = parse_label_markup(r"\notasymbol", FaceStyle::Regular);
532 assert_eq!(g[0].ch, '\\');
533 assert_eq!(g.iter().map(|x| x.ch).collect::<String>(), r"\notasymbol");
534 }
535
536 #[test]
537 fn unclosed_markdown_bold_literal() {
538 let g = parse_label_markup("**Et", FaceStyle::Regular);
539 assert_eq!(g.iter().map(|x| x.ch).collect::<String>(), "**Et");
540 }
541
542 #[test]
543 fn italic_close_skips_escape_and_double_star() {
544 let g = parse_label_markup(r"*a\*b**c*", FaceStyle::Regular);
545 assert!(g.iter().any(|x| x.face == FaceStyle::Italic));
546 let s: String = g.iter().map(|x| x.ch).collect();
547 assert!(s.contains('a'));
548 assert!(s.contains('c') || s.contains('b'));
549 }
550
551 #[test]
552 fn unicode_script_digits_normalize() {
553 let sub = parse_label_markup("H₂O", FaceStyle::Regular);
554 assert_eq!(sub[1].ch, '2');
555 assert_eq!(sub[1].role, ScriptRole::Subscript);
556 let sup = parse_label_markup("R²⁺", FaceStyle::Regular);
557 assert_eq!(sup[1].ch, '2');
558 assert_eq!(sup[1].role, ScriptRole::Superscript);
559 assert_eq!(sup[2].ch, '+');
560 assert_eq!(sup[2].role, ScriptRole::Superscript);
561 let nested = parse_label_markup("R^{²}", FaceStyle::Regular);
563 assert_eq!(nested[1].ch, '2');
564 assert_eq!(nested[1].role, ScriptRole::Superscript);
565 }
566
567 #[test]
568 fn unclosed_braced_script() {
569 let g = parse_label_markup("H_{2", FaceStyle::Regular);
570 assert!(!g.is_empty());
572 }
573}