1use alloc::string::String;
40use alloc::vec::Vec;
41
42pub const LR_PREPEND: u8 = 0;
44pub const LR_PREPEND_FIRST: u8 = 1;
45pub const LR_APPEND: u8 = 2;
46pub const LR_APPEND_LAST: u8 = 3;
47pub const LR_ASSIGN: u8 = 4;
48
49pub const GENERICS: [&str; 10] = ["sans-serif", "serif", "monospace", "cursive", "fantasy", "system-ui", "emoji", "math", "sans", "mono"];
51
52const FILES_MAX: usize = 256;
53const FILE_MAX: usize = 1 << 20;
54const DEPTH_MAX: u32 = 4;
55
56pub fn is_generic(name: &str) -> bool {
58 GENERICS.iter().any(|g| g.eq_ignore_ascii_case(name.trim()))
59}
60
61fn same(a: &str, b: &str) -> bool {
62 a.trim().eq_ignore_ascii_case(b.trim())
63}
64
65#[derive(Clone, Debug)]
66enum Rule {
67 Alias { family: String, prefer: Vec<String>, accept: Vec<String>, default: Vec<String> },
68 Lang { lang: Option<String>, family: Option<String>, mode: u8, families: Vec<String> },
69}
70
71impl Rule {
72 fn family(&self) -> Option<&str> {
73 match self {
74 Rule::Alias { family, .. } => Some(family),
75 Rule::Lang { family, .. } => family.as_deref(),
76 }
77 }
78}
79
80#[derive(Clone, Debug, Default)]
82pub struct FontConf {
83 files: Vec<Rule>,
84 builtin: Vec<Rule>,
85 nfiles: usize,
86}
87
88impl FontConf {
89 pub fn new() -> FontConf {
90 FontConf::default()
91 }
92
93 pub fn system() -> FontConf {
95 let mut c = FontConf::new();
96 match plat::env("jfgui_FONTCONF") {
97 Some(p) => {
98 c.load(&p);
99 }
100 None => {
101 #[cfg(target_os = "linux")]
102 c.load("/etc/fonts/fonts.conf");
103 }
104 }
105 c.builtin = builtin();
106 c
107 }
108
109 pub fn files(&self) -> usize {
111 self.nfiles
112 }
113
114 pub fn load(&mut self, path: &str) -> bool {
116 let mut seen = Vec::new();
117 self.load_file(path, &mut seen)
118 }
119
120 fn load_file(&mut self, path: &str, seen: &mut Vec<String>) -> bool {
121 let norm = path_norm(path);
122 if seen.len() >= FILES_MAX || seen.iter().any(|s| *s == norm) {
123 return false;
124 }
125 seen.push(norm);
126 let Some(data) = plat::read_file(path) else { return false };
127 if data.len() > FILE_MAX {
128 return false;
129 }
130 let text = String::from_utf8_lossy(&data);
131 let dir = path.rsplit_once(['/', '\\']).map_or("", |x| x.0);
132 let root = parse_xml(&text);
133 self.nfiles += 1;
134 for e in &root.kids {
135 self.element(e, dir, seen);
136 }
137 true
138 }
139
140 pub fn parse(&mut self, text: &str, dir: &str) -> usize {
142 let n = self.files.len();
143 let root = parse_xml(text);
144 let mut seen = Vec::new();
145 for e in &root.kids {
146 self.element(e, dir, &mut seen);
147 }
148 self.files.len() - n
149 }
150
151 pub fn add_alias(&mut self, family: &str, prefer: &[&str], accept: &[&str], default: &[&str]) {
153 let v = |l: &[&str]| l.iter().map(|s| String::from(*s)).collect();
154 self.files.push(Rule::Alias { family: String::from(family), prefer: v(prefer), accept: v(accept), default: v(default) });
155 }
156
157 pub fn add_lang(&mut self, lang: Option<&str>, family: Option<&str>, mode: u8, families: &[&str]) {
159 self.files.push(Rule::Lang {
160 lang: lang.map(lang_norm),
161 family: family.map(String::from),
162 mode,
163 families: families.iter().map(|s| String::from(*s)).collect(),
164 });
165 }
166
167 fn element(&mut self, e: &Elem, dir: &str, seen: &mut Vec<String>) {
168 match e.name.as_str() {
169 "fontconfig" => {
170 for k in &e.kids {
171 self.element(k, dir, seen);
172 }
173 }
174 "include" => self.include(e, dir, seen),
175 "alias" => {
176 let Some(family) = e.kid("family").map(|f| f.text.trim()).filter(|t| !t.is_empty()) else { return };
177 let list = |name: &str| -> Vec<String> {
178 e.kids.iter().filter(|k| k.name == name).flat_map(|k| k.kids.iter()).filter(|f| f.name == "family")
179 .map(|f| String::from(f.text.trim())).filter(|t| !t.is_empty()).collect()
180 };
181 self.files.push(Rule::Alias { family: String::from(family), prefer: list("prefer"), accept: list("accept"), default: list("default") });
182 }
183 "match" => {
184 if e.attr("target").is_some_and(|t| t != "pattern") {
185 return;
186 }
187 let (mut lang, mut family) = (None, None);
188 for t in e.kids.iter().filter(|k| k.name == "test") {
189 let cmp = t.attr("compare").unwrap_or("eq");
190 let val = t.kid("string").map(|s| String::from(s.text.trim()));
191 match (t.attr("name"), cmp, val) {
192 (Some("lang"), "eq" | "contains", Some(v)) if lang.is_none() => lang = Some(lang_norm(&v)),
193 (Some("family"), "eq", Some(v)) if family.is_none() => family = Some(v),
194 _ => return, }
196 }
197 for ed in e.kids.iter().filter(|k| k.name == "edit" && k.attr("name") == Some("family")) {
198 let mode = match ed.attr("mode").unwrap_or("assign") {
199 "prepend" => LR_PREPEND,
200 "prepend_first" => LR_PREPEND_FIRST,
201 "append" => LR_APPEND,
202 "append_last" => LR_APPEND_LAST,
203 "assign" | "assign_replace" => LR_ASSIGN,
204 _ => continue,
205 };
206 let families: Vec<String> =
207 ed.kids.iter().filter(|s| s.name == "string").map(|s| String::from(s.text.trim())).filter(|t| !t.is_empty()).collect();
208 if !families.is_empty() && (mode != LR_ASSIGN || family.is_some()) {
210 self.files.push(Rule::Lang { lang: lang.clone(), family: family.clone(), mode, families });
211 }
212 }
213 }
214 _ => {}
215 }
216 }
217
218 fn include(&mut self, e: &Elem, dir: &str, seen: &mut Vec<String>) {
219 let p = e.text.trim();
220 if p.is_empty() {
221 return;
222 }
223 let path = if e.attr("prefix") == Some("xdg") {
224 match plat::env("XDG_CONFIG_HOME") {
225 Some(x) if x.starts_with('/') => alloc::format!("{}/{}", x, p),
226 _ => match plat::env("HOME") {
227 Some(h) => alloc::format!("{}/.config/{}", h, p),
228 None => return,
229 },
230 }
231 } else if let Some(rest) = p.strip_prefix("~/") {
232 match plat::env("HOME") {
233 Some(h) => alloc::format!("{}/{}", h, rest),
234 None => return,
235 }
236 } else if p.starts_with('/') || dir.is_empty() {
237 String::from(p)
238 } else {
239 alloc::format!("{}/{}", dir, p)
240 };
241 if plat::file_exists(&path) == 2 {
242 let mut names = Vec::new();
243 plat::dir_list(&path, &mut |name, is_dir, _| {
244 if !is_dir && name.ends_with(".conf") {
245 names.push(String::from(name));
246 }
247 });
248 names.sort();
249 for n in names {
250 self.load_file(&alloc::format!("{}/{}", path, n), seen);
251 }
252 } else {
253 self.load_file(&path, seen);
254 }
255 }
256
257 pub fn candidates(&self, family: &str, lang: &str) -> Vec<String> {
260 let langs = lang_keys(lang);
261 let mut out: Vec<String> = Vec::new();
262 for (k, layer) in [&self.files, &self.builtin].into_iter().enumerate() {
263 let mut memo = Memo { done: Vec::new(), steps: 0 };
265 for f in expand_list(layer, family, &langs, &mut memo) {
266 if !out.iter().any(|o| same(o, &f)) && (k == 0 || !same(&f, family)) {
267 out.push(f);
268 }
269 }
270 }
271 out
272 }
273
274 pub fn lang_families(&self, lang: &str) -> Vec<String> {
276 let all = self.candidates("sans-serif", "");
277 let mut out: Vec<String> = self.candidates("sans-serif", lang).into_iter().filter(|f| !all.iter().any(|a| same(a, f))).collect();
278 out.retain(|f| !is_generic(f));
279 out
280 }
281}
282
283struct Memo {
286 done: Vec<(String, u32, (Vec<String>, Vec<String>))>,
287 steps: u32,
288}
289
290const STEPS_MAX: u32 = 2000;
291
292fn expand_list(rules: &[Rule], family: &str, langs: &[String], memo: &mut Memo) -> Vec<String> {
293 let (mut out, mut last) = expand(rules, family, langs, 0, memo);
294 let mut i = 0;
295 while i < last.len() {
296 let f = last[i].clone();
297 i += 1;
298 if out.iter().any(|o| same(o, &f)) {
299 continue;
300 }
301 if !same(&f, family) && memo.steps < STEPS_MAX && rules.iter().any(|r| r.family().is_some_and(|x| same(x, &f))) {
302 memo.steps += 1;
303 let (parts, more) = expand(rules, &f, langs, 1, memo);
304 for x in parts {
305 if !out.iter().any(|o| same(o, &x)) {
306 out.push(x);
307 }
308 }
309 last.extend(more);
310 } else {
311 out.push(f);
312 }
313 }
314 out
315}
316
317fn expand(rules: &[Rule], family: &str, langs: &[String], depth: u32, memo: &mut Memo) -> (Vec<String>, Vec<String>) {
319 let mut list: Vec<String> = alloc::vec![String::from(family)];
320 let mut last: Vec<String> = Vec::new();
321 let mut fpos = Some(0usize);
322 for r in rules {
323 let Some(p) = fpos else { break };
324 if r.family().is_some_and(|f| !same(f, family)) {
325 continue;
326 }
327 match r {
328 Rule::Alias { prefer, accept, default, .. } => {
329 insert(&mut list, p, prefer);
330 let p = p + prefer.len();
331 insert(&mut list, p + 1, accept);
332 last.extend(default.iter().cloned());
333 fpos = Some(p);
334 }
335 Rule::Lang { lang, mode, families, .. } => {
336 if lang.as_ref().is_some_and(|l| !langs.iter().any(|k| k == l)) {
337 continue;
338 }
339 match *mode {
340 LR_PREPEND => {
341 insert(&mut list, p, families);
342 fpos = Some(p + families.len());
343 }
344 LR_PREPEND_FIRST => {
345 insert(&mut list, 0, families);
346 fpos = Some(p + families.len());
347 }
348 LR_APPEND => insert(&mut list, p + 1, families),
349 LR_APPEND_LAST => last.extend(families.iter().cloned()),
350 _ => {
351 list.remove(p);
352 insert(&mut list, p, families);
353 fpos = None; }
355 }
356 }
357 }
358 }
359 let mut out: Vec<String> = Vec::new();
361 for f in list {
362 let own = depth < DEPTH_MAX && !same(&f, family) && rules.iter().any(|r| r.family().is_some_and(|x| same(x, &f)));
363 let (parts, more) = if own && memo.steps < STEPS_MAX {
364 let key = f.trim().to_ascii_lowercase();
365 if let Some(d) = memo.done.iter().find(|d| d.1 == depth + 1 && d.0 == key) {
366 d.2.clone()
367 } else {
368 memo.steps += 1;
369 let r = expand(rules, &f, langs, depth + 1, memo);
370 memo.done.push((key, depth + 1, r.clone()));
371 r
372 }
373 } else {
374 (alloc::vec![f], Vec::new())
375 };
376 for x in parts {
377 if !out.iter().any(|o| same(o, &x)) {
378 out.push(x);
379 }
380 }
381 last.extend(more);
382 }
383 (out, last)
384}
385
386fn insert(list: &mut Vec<String>, at: usize, items: &[String]) {
387 let at = at.min(list.len());
388 for (k, it) in items.iter().enumerate() {
389 list.insert(at + k, it.clone());
390 }
391}
392
393fn path_norm(p: &str) -> String {
395 let mut parts: Vec<&str> = Vec::new();
396 for s in p.split(['/', '\\']) {
397 match s {
398 "." => {}
399 ".." if parts.last().is_some_and(|l| !l.is_empty() && *l != "..") => {
400 parts.pop();
401 }
402 _ => parts.push(s),
403 }
404 }
405 parts.join("/")
406}
407
408fn lang_norm(l: &str) -> String {
410 l.trim().to_ascii_lowercase().replace('_', "-")
411}
412
413fn lang_keys(lang: &str) -> Vec<String> {
415 let l = lang_norm(lang);
416 let mut out: Vec<String> = Vec::new();
417 if l.is_empty() {
418 return out;
419 }
420 let mut cur = l.as_str();
421 loop {
422 out.push(String::from(cur));
423 match cur.rfind('-') {
424 Some(i) => cur = &cur[..i],
425 None => break,
426 }
427 }
428 if l == "zh" || l.starts_with("zh-hans") || l.starts_with("zh-cn") || l.starts_with("zh-sg") {
429 out.push(String::from("zh-cn"));
430 }
431 if l.starts_with("zh-hk") || l.starts_with("zh-mo") || l.starts_with("zh-hant-hk") || l.starts_with("zh-hant-mo") {
432 out.push(String::from("zh-hk"));
433 out.push(String::from("zh-tw"));
434 }
435 if l.starts_with("zh-hant") || l.starts_with("zh-tw") {
436 out.push(String::from("zh-tw"));
437 }
438 out
439}
440
441#[cfg(windows)]
444const GEN: &[(&str, &[&str])] = &[
445 ("sans-serif", &["Segoe UI", "Arial", "Tahoma", "Verdana", "Microsoft Sans Serif"]),
446 ("serif", &["Times New Roman", "Cambria", "Georgia"]),
447 ("monospace", &["Cascadia Mono", "Consolas", "Courier New", "Lucida Console"]),
448 ("cursive", &["Segoe Script", "Comic Sans MS", "Segoe Print"]),
449 ("fantasy", &["Impact", "Gabriola"]),
450 ("system-ui", &["Segoe UI Variable Text", "Segoe UI"]),
451 ("emoji", &["Segoe UI Emoji"]),
452 ("math", &["Cambria Math"]),
453 ("sans", &["sans-serif"]),
454 ("mono", &["monospace"]),
455 ("Helvetica", &["Arial"]),
456 ("Times", &["Times New Roman"]),
457 ("Courier", &["Courier New"]),
458 ("Liberation Sans", &["Arial"]),
459 ("Liberation Serif", &["Times New Roman"]),
460 ("Liberation Mono", &["Courier New"]),
461 ("DejaVu Sans", &["Segoe UI", "Verdana"]),
462 ("DejaVu Serif", &["Cambria", "Georgia"]),
463 ("DejaVu Sans Mono", &["Consolas", "Courier New"]),
464];
465
466#[cfg(windows)]
467const LANGS: &[(&str, &str, &[&str])] = &[
468 ("ja", "sans-serif", &["Yu Gothic UI", "Meiryo UI", "Yu Gothic", "Meiryo", "MS UI Gothic"]),
469 ("ja", "serif", &["Yu Mincho", "MS Mincho"]),
470 ("ja", "monospace", &["MS Gothic"]),
471 ("zh-cn", "sans-serif", &["Microsoft YaHei UI", "Microsoft YaHei", "SimHei"]),
472 ("zh-cn", "serif", &["SimSun"]),
473 ("zh-cn", "monospace", &["NSimSun"]),
474 ("zh-tw", "sans-serif", &["Microsoft JhengHei UI", "Microsoft JhengHei"]),
475 ("zh-tw", "serif", &["PMingLiU", "MingLiU"]),
476 ("zh-tw", "monospace", &["MingLiU"]),
477 ("zh-hk", "serif", &["PMingLiU_HKSCS", "MingLiU_HKSCS"]),
478 ("ko", "sans-serif", &["Malgun Gothic", "Gulim"]),
479 ("ko", "serif", &["Batang"]),
480 ("ko", "monospace", &["GulimChe"]),
481 ("th", "sans-serif", &["Leelawadee UI", "Tahoma"]),
482 ("lo", "sans-serif", &["Leelawadee UI"]),
483 ("km", "sans-serif", &["Leelawadee UI", "Khmer UI"]),
484 ("hi", "sans-serif", &["Nirmala UI"]),
485 ("mr", "sans-serif", &["Nirmala UI"]),
486 ("ne", "sans-serif", &["Nirmala UI"]),
487 ("bn", "sans-serif", &["Nirmala UI"]),
488 ("ta", "sans-serif", &["Nirmala UI"]),
489 ("te", "sans-serif", &["Nirmala UI"]),
490 ("gu", "sans-serif", &["Nirmala UI"]),
491 ("pa", "sans-serif", &["Nirmala UI"]),
492 ("am", "sans-serif", &["Ebrima"]),
493 ("he", "sans-serif", &["Segoe UI", "Arial"]),
494 ("ar", "sans-serif", &["Segoe UI", "Tahoma"]),
495 ("fa", "sans-serif", &["Segoe UI", "Tahoma"]),
496];
497
498#[cfg(not(windows))]
499const GEN: &[(&str, &[&str])] = &[
500 ("sans-serif", &["DejaVu Sans", "Noto Sans", "Liberation Sans", "Ubuntu", "Cantarell", "FreeSans"]),
501 ("serif", &["DejaVu Serif", "Noto Serif", "Liberation Serif", "FreeSerif"]),
502 ("monospace", &["DejaVu Sans Mono", "Noto Sans Mono", "Liberation Mono", "Ubuntu Mono", "FreeMono"]),
503 ("cursive", &["URW Chancery L", "Z003"]),
504 ("fantasy", &["Impact"]),
505 ("system-ui", &["Cantarell", "Ubuntu", "Noto Sans", "DejaVu Sans"]),
506 ("emoji", &["Noto Color Emoji", "Noto Emoji"]),
507 ("math", &["Noto Sans Math", "DejaVu Math TeX Gyre", "STIX Two Math"]),
508 ("sans", &["sans-serif"]),
509 ("mono", &["monospace"]),
510 ("Arial", &["Liberation Sans"]),
511 ("Helvetica", &["Liberation Sans"]),
512 ("Times New Roman", &["Liberation Serif"]),
513 ("Times", &["Liberation Serif"]),
514 ("Courier New", &["Liberation Mono"]),
515 ("Courier", &["Liberation Mono"]),
516 ("Segoe UI", &["Noto Sans", "DejaVu Sans"]),
517 ("Consolas", &["DejaVu Sans Mono", "Liberation Mono"]),
518];
519
520#[cfg(not(windows))]
521const LANGS: &[(&str, &str, &[&str])] = &[
522 ("ja", "sans-serif", &["Noto Sans CJK JP", "IPAGothic", "TakaoGothic", "VL Gothic"]),
523 ("ja", "serif", &["Noto Serif CJK JP", "IPAMincho", "TakaoMincho"]),
524 ("ja", "monospace", &["Noto Sans Mono CJK JP", "IPAGothic"]),
525 ("zh-cn", "sans-serif", &["Noto Sans CJK SC", "WenQuanYi Micro Hei", "Droid Sans Fallback"]),
526 ("zh-cn", "serif", &["Noto Serif CJK SC", "AR PL UMing CN"]),
527 ("zh-cn", "monospace", &["Noto Sans Mono CJK SC", "WenQuanYi Micro Hei Mono"]),
528 ("zh-tw", "sans-serif", &["Noto Sans CJK TC", "WenQuanYi Micro Hei"]),
529 ("zh-tw", "serif", &["Noto Serif CJK TC", "AR PL UMing TW"]),
530 ("zh-tw", "monospace", &["Noto Sans Mono CJK TC"]),
531 ("zh-hk", "sans-serif", &["Noto Sans CJK HK", "Noto Sans CJK TC"]),
532 ("ko", "sans-serif", &["Noto Sans CJK KR", "NanumGothic", "UnDotum"]),
533 ("ko", "serif", &["Noto Serif CJK KR", "NanumMyeongjo", "UnBatang"]),
534 ("ko", "monospace", &["Noto Sans Mono CJK KR", "NanumGothicCoding"]),
535 ("th", "sans-serif", &["Noto Sans Thai", "Loma", "Garuda"]),
536 ("hi", "sans-serif", &["Noto Sans Devanagari", "Lohit Devanagari"]),
537 ("mr", "sans-serif", &["Noto Sans Devanagari", "Lohit Marathi"]),
538 ("bn", "sans-serif", &["Noto Sans Bengali", "Lohit Bengali"]),
539 ("ta", "sans-serif", &["Noto Sans Tamil", "Lohit Tamil"]),
540 ("he", "sans-serif", &["Noto Sans Hebrew", "DejaVu Sans"]),
541 ("ar", "sans-serif", &["Noto Sans Arabic", "Noto Naskh Arabic", "DejaVu Sans"]),
542 ("fa", "sans-serif", &["Noto Sans Arabic", "Vazirmatn", "DejaVu Sans"]),
543 ("am", "sans-serif", &["Noto Sans Ethiopic", "Abyssinica SIL"]),
544];
545
546fn builtin() -> Vec<Rule> {
547 let s = |l: &[&str]| -> Vec<String> { l.iter().map(|x| String::from(*x)).collect() };
548 let mut out = Vec::new();
549 for (lang, fam, list) in LANGS {
551 out.push(Rule::Lang { lang: Some(String::from(*lang)), family: Some(String::from(*fam)), mode: LR_PREPEND, families: s(list) });
552 }
553 for (fam, list) in GEN {
554 out.push(Rule::Alias { family: String::from(*fam), prefer: Vec::new(), accept: Vec::new(), default: s(list) });
555 }
556 out
557}
558
559#[derive(Default, Debug)]
563struct Elem {
564 name: String,
565 attrs: Vec<(String, String)>,
566 text: String,
567 kids: Vec<Elem>,
568}
569
570impl Elem {
571 fn attr(&self, n: &str) -> Option<&str> {
572 self.attrs.iter().find(|a| a.0 == n).map(|a| a.1.as_str())
573 }
574 fn kid(&self, n: &str) -> Option<&Elem> {
575 self.kids.iter().find(|k| k.name == n)
576 }
577}
578
579fn unescape(s: &str) -> String {
580 if !s.contains('&') {
581 return String::from(s);
582 }
583 let mut out = String::new();
584 let mut rest = s;
585 while let Some(i) = rest.find('&') {
586 out.push_str(&rest[..i]);
587 let tail = &rest[i..];
588 let Some(j) = tail.find(';').filter(|&j| j <= 10) else {
589 out.push('&');
590 rest = &tail[1..];
591 continue;
592 };
593 let ent = &tail[1..j];
594 let c = match ent {
595 "amp" => Some('&'),
596 "lt" => Some('<'),
597 "gt" => Some('>'),
598 "quot" => Some('"'),
599 "apos" => Some('\''),
600 _ if ent.starts_with("#x") => u32::from_str_radix(&ent[2..], 16).ok().and_then(char::from_u32),
601 _ if ent.starts_with('#') => ent[1..].parse::<u32>().ok().and_then(char::from_u32),
602 _ => None,
603 };
604 match c {
605 Some(c) => out.push(c),
606 None => out.push_str(&tail[..=j]),
607 }
608 rest = &tail[j + 1..];
609 }
610 out.push_str(rest);
611 out
612}
613
614fn parse_xml(s: &str) -> Elem {
616 let mut stack: Vec<Elem> = alloc::vec![Elem::default()];
617 let mut rest = s;
618 while !rest.is_empty() {
619 let Some(lt) = rest.find('<') else {
620 if let Some(top) = stack.last_mut() {
621 top.text.push_str(&unescape(rest));
622 }
623 break;
624 };
625 if lt > 0 {
626 if let Some(top) = stack.last_mut() {
627 top.text.push_str(&unescape(&rest[..lt]));
628 }
629 }
630 rest = &rest[lt..];
631 let skip_to = |r: &str, end: &str| r.find(end).map(|i| i + end.len()).unwrap_or(r.len());
633 if rest.starts_with("<!--") {
634 rest = &rest[skip_to(rest, "-->")..];
635 continue;
636 }
637 if rest.starts_with("<?") {
638 rest = &rest[skip_to(rest, "?>")..];
639 continue;
640 }
641 if rest.starts_with("<![CDATA[") {
642 let end = rest.find("]]>").unwrap_or(rest.len());
643 if let Some(top) = stack.last_mut() {
644 top.text.push_str(&rest[9.min(end)..end]);
645 }
646 rest = &rest[(end + 3).min(rest.len())..];
647 continue;
648 }
649 if rest.starts_with("<!") {
650 rest = &rest[skip_to(rest, ">")..];
651 continue;
652 }
653 let Some(gt) = rest.find('>') else { break };
654 let tag = &rest[1..gt];
655 rest = &rest[gt + 1..];
656 if let Some(name) = tag.strip_prefix('/') {
657 let name = name.trim();
658 if let Some(k) = stack.iter().rposition(|e| e.name == name).filter(|&k| k > 0) {
660 while stack.len() > k {
661 let e = stack.pop().unwrap_or_default();
662 if let Some(parent) = stack.last_mut() {
663 parent.kids.push(e);
664 }
665 }
666 }
667 continue;
668 }
669 let empty = tag.ends_with('/');
670 let tag = tag.trim_end_matches('/');
671 let name_end = tag.find(|c: char| c.is_ascii_whitespace()).unwrap_or(tag.len());
672 let mut e = Elem { name: String::from(&tag[..name_end]), ..Elem::default() };
673 let mut a = &tag[name_end..];
674 loop {
675 a = a.trim_start();
676 let Some(eq) = a.find('=') else { break };
677 let key = a[..eq].trim();
678 let v = a[eq + 1..].trim_start();
679 let Some(q) = v.chars().next().filter(|c| *c == '"' || *c == '\'') else { break };
680 let Some(close) = v[1..].find(q) else { break };
681 e.attrs.push((String::from(key), unescape(&v[1..1 + close])));
682 a = &v[close + 2..];
683 }
684 if empty {
685 if let Some(top) = stack.last_mut() {
686 top.kids.push(e);
687 }
688 } else if stack.len() < 64 {
689 stack.push(e);
690 }
691 }
692 while stack.len() > 1 {
693 let e = stack.pop().unwrap_or_default();
694 if let Some(parent) = stack.last_mut() {
695 parent.kids.push(e);
696 }
697 }
698 stack.pop().unwrap_or_default()
699}