1use alloc::collections::BTreeMap;
53use alloc::format;
54use alloc::string::String;
55use alloc::vec::Vec;
56
57use crate::md_doc::{MdBox, MdField, MdForm, MdOption, MF_BUTTON, MF_CHECKBOX, MF_HIDDEN, MF_PASSWORD, MF_RADIO, MF_RESET, MF_SELECT, MF_SUBMIT, MF_TEXT, MF_TEXTAREA};
58use crate::md_dom::{parse_sels, sel_match, void, Dom, MdNode, Sel, SelKey, K_ELEM, K_TEXT, NIL};
59use crate::rich_doc::*;
60
61fn esc(s: &str, out: &mut String) {
62 for c in s.chars() {
63 match c {
64 '&' => out.push_str("&"),
65 '<' => out.push_str("<"),
66 '>' => out.push_str(">"),
67 '"' => out.push_str("""),
68 _ => out.push(c),
69 }
70 }
71}
72
73fn css_family(f: &str) -> String {
75 if matches!(f, "serif" | "sans-serif" | "monospace" | "cursive" | "fantasy" | "system-ui") {
76 String::from(f)
77 } else {
78 format!("'{}'", f.replace('\'', ""))
79 }
80}
81
82fn run_html(t: &str, f: &Fmt, out: &mut String) {
83 let mut close: Vec<&str> = Vec::new();
84 if !f.link.is_empty() {
85 out.push_str("<a href=\"");
86 esc(&f.link, out);
87 out.push_str("\">");
88 close.push("</a>");
89 }
90 let mut css = String::new();
91 if f.color != 0 {
92 css.push_str(&format!("color:{};", color_hex(f.color)));
93 }
94 if f.bg != 0 {
95 css.push_str(&format!("background-color:{};", color_hex(f.bg)));
96 }
97 if f.size != 0 {
98 css.push_str(&format!("font-size:{}pt;", size_text(f.size)));
99 }
100 if !f.font.is_empty() {
101 css.push_str(&format!("font-family:{};", css_family(&f.font)));
102 }
103 if !css.is_empty() {
104 css.pop();
105 out.push_str("<span style=\"");
106 esc(&css, out);
107 out.push_str("\">");
108 close.push("</span>");
109 }
110 for (bit, open, end) in [(RF_BOLD, "<b>", "</b>"), (RF_ITALIC, "<i>", "</i>"), (RF_UNDER, "<u>", "</u>"), (RF_STRIKE, "<s>", "</s>")] {
111 if f.flags & bit != 0 {
112 out.push_str(open);
113 close.push(end);
114 }
115 }
116 esc(t, out);
117 for c in close.iter().rev() {
118 out.push_str(c);
119 }
120}
121
122fn para_content(p: &Para, out: &mut String) {
123 if p.is_empty() {
124 out.push_str("<br>");
125 return;
126 }
127 for (t, f) in p.runs() {
128 run_html(t, f, out);
129 }
130}
131
132fn align_css(a: u8) -> &'static str {
133 match a {
134 RA_CENTER => "text-align:center",
135 RA_RIGHT => "text-align:right",
136 RA_JUSTIFY => "text-align:justify",
137 _ => "",
138 }
139}
140
141pub(crate) fn html_of(paras: &[Para], font: &str, size: u16) -> String {
143 let mut out = String::from("<div style=\"white-space:pre-wrap");
144 if !font.is_empty() {
145 out.push_str(";font-family:");
146 esc(&css_family(font), &mut out);
147 }
148 if size != 0 {
149 out.push_str(&format!(";font-size:{}pt", size_text(size)));
150 }
151 out.push_str("\">");
152 let mut lists: Vec<(u8, bool)> = Vec::new();
154 let close_to = |lists: &mut Vec<(u8, bool)>, n: usize, out: &mut String| {
155 while lists.len() > n {
156 let (k, li) = lists.pop().unwrap();
157 if li {
158 out.push_str("</li>");
159 }
160 out.push_str(if k == LS_NUMBER { "</ol>" } else { "</ul>" });
161 }
162 };
163 for p in paras {
164 if p.list == LS_NONE {
165 close_to(&mut lists, 0, &mut out);
166 let (tag, quote) = match p.style {
167 PS_H1 => ("h1", false),
168 PS_H2 => ("h2", false),
169 PS_H3 => ("h3", false),
170 PS_QUOTE => ("p", true),
171 _ => ("p", false),
172 };
173 if quote {
174 out.push_str("<blockquote>");
175 }
176 let mut css = String::from(align_css(p.align));
177 if p.level > 0 {
178 if !css.is_empty() {
179 css.push(';');
180 }
181 css.push_str(&format!("margin-left:{}em", p.level as u32 * 2));
182 }
183 out.push('<');
184 out.push_str(tag);
185 if !css.is_empty() {
186 out.push_str(" style=\"");
187 out.push_str(&css);
188 out.push('"');
189 }
190 out.push('>');
191 para_content(p, &mut out);
192 out.push_str("</");
193 out.push_str(tag);
194 out.push('>');
195 if quote {
196 out.push_str("</blockquote>");
197 }
198 continue;
199 }
200 let depth = p.level as usize + 1;
201 close_to(&mut lists, depth, &mut out);
202 if lists.len() == depth && lists[depth - 1].0 != p.list {
203 close_to(&mut lists, depth - 1, &mut out);
204 }
205 if lists.len() == depth {
206 if lists[depth - 1].1 {
207 out.push_str("</li>");
208 lists[depth - 1].1 = false;
209 }
210 }
211 while lists.len() < depth {
212 let d = lists.len();
213 let kind = if d + 1 == depth { p.list } else { LS_BULLET };
214 out.push_str(if kind == LS_NUMBER {
215 match d % 3 {
216 0 => "<ol>",
217 1 => "<ol type=\"a\">",
218 _ => "<ol type=\"i\">",
219 }
220 } else {
221 "<ul>"
222 });
223 lists.push((kind, false));
224 if d + 1 < depth {
225 out.push_str("<li>");
227 lists[d].1 = true;
228 }
229 }
230 out.push_str("<li");
231 let css = align_css(p.align);
232 if !css.is_empty() {
233 out.push_str(" style=\"");
234 out.push_str(css);
235 out.push('"');
236 }
237 out.push('>');
238 para_content(p, &mut out);
239 lists[depth - 1].1 = true;
240 }
241 close_to(&mut lists, 0, &mut out);
242 out.push_str("</div>");
243 out
244}
245
246pub(crate) fn clip_html(paras: &[Para], font: &str, size: u16) -> String {
248 let mut d = RichDoc::from_paras(paras.to_vec());
249 d.font = String::from(font);
250 d.size = size;
251 let json = d.to_json().replace("--", "-\\u002d");
253 format!("<!--jfrich {}-->{}", json, html_of(paras, font, size))
254}
255
256impl RichDoc {
257 pub fn to_html(&self) -> String {
259 html_of(self.paras(), &self.font, self.size)
260 }
261
262 pub fn from_html(s: &str) -> RichDoc {
265 if let Some(d) = embedded(s) {
266 return d;
267 }
268 let mut r = Reader::new();
269 r.run(s);
270 RichDoc::from_paras(r.finish())
271 }
272}
273
274fn embedded(s: &str) -> Option<RichDoc> {
276 let i = s.find("<!--jfrich ")?;
277 let rest = &s[i + 11..];
278 let j = rest.find("-->")?;
279 RichDoc::from_json(&rest[..j])
280}
281
282pub(crate) fn html_read(s: &str) -> HRead {
285 let d = Dom::parse(s);
286 let sheet = Sheet::of(&d, &BTreeMap::new());
287 dom_read(&d, &sheet, false)
288}
289
290pub(crate) fn dom_read(d: &Dom, sheet: &Sheet, handles: bool) -> HRead {
293 let mut r = Reader::new();
294 r.ext = Some(Ext::default());
295 r.css = sheet.budget(d.nodes.len());
296 r.walk(d, 0, false, handles, sheet);
297 r.cap_end();
298 r.label_end();
299 let mut e = r.ext.take().unwrap_or_default();
300 let mut ids: BTreeMap<String, usize> = BTreeMap::new();
302 for (i, f) in e.fields.iter().enumerate().rev() {
303 if !f.id.is_empty() {
304 ids.insert(f.id.clone(), i);
305 }
306 }
307 for (id, text) in core::mem::take(&mut e.labels) {
308 if let Some(f) = ids.get(&id).and_then(|&i| e.fields.get_mut(i)) {
309 if f.label.is_empty() && !matches!(f.kind, MF_SUBMIT | MF_RESET | MF_BUTTON) {
310 f.label = text;
311 }
312 }
313 }
314 ids.clear();
315 for (i, f) in e.forms.iter().enumerate().rev() {
316 if !f.id.is_empty() {
317 ids.insert(f.id.clone(), i);
318 }
319 }
320 for (k, id) in core::mem::take(&mut e.form_of) {
321 if let (Some(&i), Some(f)) = (ids.get(&id), e.fields.get_mut(k)) {
322 f.form = i as u32 + 1;
323 }
324 }
325 for (k, t) in core::mem::take(&mut e.titles) {
326 if let Some(f) = e.fields.get_mut(k).filter(|f| f.label.is_empty()) {
327 f.label = t;
328 }
329 }
330 let mut paras = r.finish();
331 if paras.len() == 1 && paras[0].is_empty() && e.items.is_empty() {
332 paras.clear();
333 }
334 HRead {
335 paras,
336 ext: e.items,
337 forms: e.forms,
338 fields: e.fields,
339 nodes: e.para_nodes,
340 ranges: e.ranges,
341 boxes: e.boxes,
342 para_boxes: e.para_boxes,
343 rule_boxes: e.rule_boxes,
344 }
345}
346
347pub(crate) fn dom_para(d: &Dom, sheet: &Sheet, l: u32) -> Option<Para> {
350 let mut r = Reader::new();
351 r.ext = Some(Ext::default());
352 r.css = sheet.budget(64);
353 let mut chain = Vec::new();
354 let mut x = d.nodes[l as usize].parent;
355 while x != NIL {
356 chain.push(x);
357 x = d.nodes[x as usize].parent;
358 }
359 let mut f = Fmt::default();
361 for &a in chain.iter().rev() {
362 let n = &d.nodes[a as usize];
363 if n.kind == K_ELEM && pushes_fmt(&n.name) {
364 let eff = sheet.attrs_of(d, a, &mut r.css);
365 f = fmt_with(f, &n.name, eff.as_deref().unwrap_or(&n.attrs), true);
366 }
367 }
368 r.fmts.push((String::from("#"), f));
369 r.walk(d, l, true, true, sheet);
370 if r.out.len() != 1 {
371 return None;
372 }
373 r.out.pop()
374}
375
376fn pushes_fmt(tag: &str) -> bool {
379 !matches!(
380 tag,
381 "ul" | "ol" | "form" | "label" | "textarea" | "select" | "button" | "br" | "img" | "hr" | "meta" | "link" | "input" | "script" | "style"
382 | "head" | "title" | "template" | "noscript" | "svg"
383 )
384}
385
386fn display_none(style: &str) -> bool {
388 let mut none = false;
389 for decl in style.split(';') {
390 if let Some((p, v)) = decl.split_once(':') {
391 if p.trim().eq_ignore_ascii_case("display") {
392 none = v.trim().trim_end_matches("!important").trim().eq_ignore_ascii_case("none");
393 }
394 }
395 }
396 none
397}
398
399pub(crate) struct HRead {
406 pub paras: Vec<Para>,
407 pub ext: Vec<(usize, HExt)>,
408 pub forms: Vec<MdForm>,
409 pub fields: Vec<MdField>,
410 pub nodes: Vec<MdNode>,
411 pub ranges: Vec<(u32, u32, u32)>,
412 pub boxes: Vec<MdBox>,
413 pub para_boxes: Vec<Vec<u32>>,
414 pub rule_boxes: Vec<(usize, Vec<u32>)>,
415}
416
417#[derive(Clone, Debug, PartialEq)]
419pub(crate) enum HExt {
420 Field { byte: Option<usize>, n: usize },
423 Task(bool),
425 Rule,
427 Image { byte: usize, src: String, alt: String, w: u32, h: u32 },
429 Pre { block: u32, lang: String },
432 Heading(u8),
434 Quote(u8),
436 Row { table: u32, head: bool, cells: Vec<(usize, u8)> },
439 Item(u32),
441}
442
443#[derive(Default)]
445struct Ext {
446 items: Vec<(usize, HExt)>,
447 pre: u32,
448 lang: String,
449 table: u32,
450 cells: Vec<(usize, u8)>,
451 head: bool,
452 thead: bool,
453 item: Option<u32>,
454 nums: Vec<u32>,
456 cell: bool,
458 forms: Vec<MdForm>,
460 fields: Vec<MdField>,
461 form: Option<u32>,
462 label: Option<(String, String, Option<usize>)>,
464 labels: Vec<(String, String)>,
466 form_of: Vec<(usize, String)>,
467 titles: Vec<(usize, String)>,
468 cap: Option<usize>,
471 opt_val: bool,
472 raw: Option<usize>,
474 para_nodes: Vec<MdNode>,
477 ranges: Vec<(u32, u32, u32)>,
478 boxes: Vec<MdBox>,
481 open_boxes: Vec<u32>,
482 para_boxes: Vec<Vec<u32>>,
483 rule_boxes: Vec<(usize, Vec<u32>)>,
484}
485
486const NEST_MAX: usize = 64;
488const FORMS_MAX: usize = 4096;
490const FIELDS_MAX: usize = 0xFFFE;
491const OPTIONS_MAX: usize = 4096;
492
493const CAP_TEXT: usize = 8192;
495
496fn push_cap(s: &mut String, t: &str) {
498 let mut n = CAP_TEXT.saturating_sub(s.len()).min(t.len());
499 while !t.is_char_boundary(n) {
500 n -= 1;
501 }
502 s.push_str(&t[..n]);
503}
504
505fn collapse(s: &str) -> String {
508 let mut out = String::with_capacity(s.len());
509 for w in s.split(|c: char| c.is_whitespace() && c != '\u{A0}').filter(|w| !w.is_empty()) {
510 if !out.is_empty() {
511 out.push(' ');
512 }
513 out.push_str(&w.replace('\u{A0}', " "));
514 }
515 out
516}
517
518pub(crate) fn entity(name: &str) -> Option<char> {
521 if let Some(num) = name.strip_prefix('#') {
522 let v = if let Some(h) = num.strip_prefix(['x', 'X']) { u32::from_str_radix(h, 16).ok()? } else { num.parse().ok()? };
523 return char::from_u32(v).map(|c| if c == '\0' { '\u{FFFD}' } else { c });
524 }
525 Some(match name {
526 "amp" => '&',
527 "lt" => '<',
528 "gt" => '>',
529 "quot" => '"',
530 "apos" => '\'',
531 "nbsp" => '\u{A0}',
532 "ensp" => '\u{2002}',
533 "emsp" => '\u{2003}',
534 "thinsp" => '\u{2009}',
535 "ndash" => '–',
536 "mdash" => '—',
537 "hellip" => '…',
538 "lsquo" => '‘',
539 "rsquo" => '’',
540 "sbquo" => '‚',
541 "ldquo" => '“',
542 "rdquo" => '”',
543 "bdquo" => '„',
544 "laquo" => '«',
545 "raquo" => '»',
546 "bull" => '•',
547 "middot" => '·',
548 "copy" => '©',
549 "reg" => '®',
550 "trade" => '™',
551 "euro" => '€',
552 "deg" => '°',
553 "times" => '×',
554 "divide" => '÷',
555 "auml" => 'ä',
556 "ouml" => 'ö',
557 "uuml" => 'ü',
558 "Auml" => 'Ä',
559 "Ouml" => 'Ö',
560 "Uuml" => 'Ü',
561 "szlig" => 'ß',
562 "eacute" => 'é',
563 "egrave" => 'è',
564 "agrave" => 'à',
565 "ccedil" => 'ç',
566 "shy" => '\u{AD}',
567 _ => return None,
568 })
569}
570
571pub(crate) fn decode(s: &str) -> String {
573 if !s.contains('&') {
574 return String::from(s);
575 }
576 let mut out = String::with_capacity(s.len());
577 let mut rest = s;
578 while let Some(i) = rest.find('&') {
579 out.push_str(&rest[..i]);
580 rest = &rest[i..];
581 let end = rest[1..].find(|c: char| c == ';' || c == '&' || c == '<' || c.is_whitespace()).map(|j| j + 1);
582 match end {
583 Some(j) if rest.as_bytes()[j] == b';' && j <= 12 => match entity(&rest[1..j]) {
584 Some(c) => {
585 out.push(c);
586 rest = &rest[j + 1..];
587 }
588 None => {
589 out.push('&');
590 rest = &rest[1..];
591 }
592 },
593 _ => {
594 out.push('&');
595 rest = &rest[1..];
596 }
597 }
598 }
599 out.push_str(rest);
600 out
601}
602
603pub(crate) fn attrs(s: &str) -> Vec<(String, String)> {
605 let b = s.as_bytes();
606 let mut v = Vec::new();
607 let mut i = 0;
608 while i < b.len() {
609 while i < b.len() && (b[i].is_ascii_whitespace() || b[i] == b'/') {
610 i += 1;
611 }
612 let n0 = i;
613 while i < b.len() && !b[i].is_ascii_whitespace() && b[i] != b'=' && b[i] != b'/' {
614 i += 1;
615 }
616 if n0 == i {
617 break;
618 }
619 let name = s[n0..i].to_ascii_lowercase();
620 while i < b.len() && b[i].is_ascii_whitespace() {
621 i += 1;
622 }
623 let mut val = String::new();
624 if i < b.len() && b[i] == b'=' {
625 i += 1;
626 while i < b.len() && b[i].is_ascii_whitespace() {
627 i += 1;
628 }
629 if i < b.len() && (b[i] == b'"' || b[i] == b'\'') {
630 let q = b[i];
631 let v0 = i + 1;
632 i += 1;
633 while i < b.len() && b[i] != q {
634 i += 1;
635 }
636 val = decode(&s[v0..i.min(b.len())]);
637 i += 1;
638 } else {
639 let v0 = i;
640 while i < b.len() && !b[i].is_ascii_whitespace() {
641 i += 1;
642 }
643 val = decode(&s[v0..i]);
644 }
645 }
646 v.push((name, val));
647 }
648 v
649}
650
651fn css_size(v: &str, base: u16) -> Option<u16> {
653 let v = v.trim().to_ascii_lowercase();
654 let num = |s: &str| -> Option<f32> { s.trim().parse::<f32>().ok() };
655 let base = if base == 0 { 110 } else { base } as f32;
656 let t = if let Some(n) = v.strip_suffix("pt") {
657 num(n)? * 10.0
658 } else if let Some(n) = v.strip_suffix("px") {
659 num(n)? * 7.5
660 } else if let Some(n) = v.strip_suffix("rem") {
661 num(n)? * 110.0
662 } else if let Some(n) = v.strip_suffix("em") {
663 num(n)? * base
664 } else if let Some(n) = v.strip_suffix('%') {
665 num(n)? * base / 100.0
666 } else {
667 match v.as_str() {
668 "xx-small" => 70.0,
669 "x-small" => 75.0,
670 "small" => 100.0,
671 "medium" => 120.0,
672 "large" => 135.0,
673 "x-large" => 180.0,
674 "xx-large" => 240.0,
675 "smaller" => base * 0.83,
676 "larger" => base * 1.2,
677 _ => num(&v)? * 7.5,
678 }
679 };
680 (t >= 10.0 && t <= 4000.0).then_some((t + 0.5) as u16)
681}
682
683fn css_color(v: &str) -> Option<u32> {
685 let v = v.trim().to_ascii_lowercase();
686 if v.starts_with('#') {
687 return parse_hex_color(&v);
688 }
689 if let Some(inner) = v.strip_prefix("rgba(").or_else(|| v.strip_prefix("rgb(")) {
690 let inner = inner.trim_end_matches(')');
691 let parts: Vec<&str> = inner.split([',', ' ', '/']).filter(|p| !p.is_empty()).collect();
692 if parts.len() < 3 {
693 return None;
694 }
695 let c = |s: &str| -> Option<u32> {
696 if let Some(p) = s.strip_suffix('%') {
697 Some((p.trim().parse::<f32>().ok()? * 2.55).clamp(0.0, 255.0) as u32)
698 } else {
699 Some(s.trim().parse::<f32>().ok()?.clamp(0.0, 255.0) as u32)
700 }
701 };
702 let a = match parts.get(3) {
703 Some(s) if s.ends_with('%') => (s.trim_end_matches('%').parse::<f32>().ok()? * 2.55) as u32,
704 Some(s) => (s.parse::<f32>().ok()? * 255.0) as u32,
705 None => 255,
706 };
707 if a == 0 {
708 return None; }
710 return Some(a.min(255) << 24 | c(parts[0])? << 16 | c(parts[1])? << 8 | c(parts[2])?);
711 }
712 let rgb = match v.as_str() {
713 "black" => 0x000000,
714 "white" => 0xFFFFFF,
715 "red" => 0xFF0000,
716 "green" => 0x008000,
717 "blue" => 0x0000FF,
718 "yellow" => 0xFFFF00,
719 "orange" => 0xFFA500,
720 "purple" => 0x800080,
721 "gray" | "grey" => 0x808080,
722 "silver" => 0xC0C0C0,
723 "maroon" => 0x800000,
724 "navy" => 0x000080,
725 "teal" => 0x008080,
726 "olive" => 0x808000,
727 "lime" => 0x00FF00,
728 "aqua" | "cyan" => 0x00FFFF,
729 "fuchsia" | "magenta" => 0xFF00FF,
730 _ => return None,
731 };
732 Some(0xFF00_0000 | rgb)
733}
734
735#[derive(Clone)]
737struct Block {
738 tag: String,
739 align: u8,
740 style: u8,
741 pre: bool,
742 node: MdNode,
743}
744
745struct Reader {
746 out: Vec<Para>,
747 cur: Para,
748 started: bool,
750 space: bool,
752 fmts: Vec<(String, Fmt)>,
753 blocks: Vec<Block>,
754 lists: Vec<u8>,
756 item: Option<(u8, u8)>,
758 skip: Option<String>,
759 ext: Option<Ext>,
761 node: MdNode,
763 css: usize,
764}
765
766const BLOCKS: [&str; 22] = [
767 "p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "li", "blockquote", "pre", "tr", "dt", "dd", "section", "article", "header", "footer",
768 "table", "address", "figure", "center",
769];
770
771impl Reader {
772 fn new() -> Reader {
773 Reader {
774 out: Vec::new(),
775 cur: Para::new(),
776 started: false,
777 space: false,
778 fmts: Vec::new(),
779 blocks: Vec::new(),
780 lists: Vec::new(),
781 item: None,
782 skip: None,
783 ext: None,
784 node: MdNode::NONE,
785 css: 0,
786 }
787 }
788
789 fn walk(&mut self, d: &Dom, top: u32, own: bool, handles: bool, sheet: &Sheet) {
796 let mut st: Vec<(u32, Option<(usize, bool)>)> = Vec::new();
798 let push_kids = |st: &mut Vec<(u32, Option<(usize, bool)>)>, x: u32| {
799 let mut k = d.nodes[x as usize].last;
800 while k != NIL {
801 st.push((k, None));
802 k = d.nodes[k as usize].prev;
803 }
804 };
805 if own {
806 st.push((top, None));
807 } else {
808 push_kids(&mut st, top);
809 }
810 while let Some((x, end)) = st.pop() {
811 let n = &d.nodes[x as usize];
812 if let Some((p0, boxed)) = end {
813 self.close(&n.name);
814 if let Some(e) = self.ext.as_mut() {
815 if boxed {
816 e.open_boxes.pop();
817 }
818 if handles && BLOCKS.contains(&n.name.as_str()) {
819 e.ranges.push((x, p0 as u32, self.out.len() as u32));
820 }
821 }
822 continue;
823 }
824 if n.kind == K_TEXT {
825 self.text_dec(&n.name);
826 continue;
827 }
828 if n.kind != K_ELEM || d.hidden_attr(x) {
829 continue;
830 }
831 let eff = if sheet.is_empty() { None } else { sheet.attrs_of(d, x, &mut self.css) };
832 let at: &[(String, String)] = eff.as_deref().unwrap_or(&n.attrs);
833 let style = at.iter().find(|a| a.0 == "style").map(|a| a.1.as_str());
834 if style.is_some_and(display_none) {
835 continue;
836 }
837 self.node = if handles { d.handle(x) } else { MdNode::NONE };
838 let depth = self.blocks.iter().filter(|b| b.tag == "blockquote").count() + self.lists.len();
840 self.open(&n.name, at, false);
841 if void(&n.name) {
842 continue;
843 }
844 if self.skip.is_some() {
845 self.raw_text_dec(&d.text_of(x));
847 self.skip = None;
848 self.close(&n.name);
849 continue;
850 }
851 let mut boxed = false;
852 if let (Some(e), Some(s)) = (self.ext.as_mut(), style) {
853 if box_tag(&n.name) {
854 if let Some(mut b) = box_of(s) {
855 b.depth = depth.min(255) as u8;
856 b.node = self.node;
857 e.open_boxes.push(e.boxes.len() as u32);
858 e.boxes.push(b);
859 boxed = true;
860 }
861 }
862 }
863 st.push((x, Some((self.out.len(), boxed))));
864 push_kids(&mut st, x);
865 }
866 }
867
868 fn fmt(&self) -> Fmt {
869 self.fmts.last().map(|f| f.1.clone()).unwrap_or_default()
870 }
871
872 fn pre(&self) -> bool {
873 self.blocks.iter().any(|b| b.pre)
874 }
875
876 fn flush(&mut self, force: bool) {
878 if !self.started && !force {
879 return;
880 }
881 let mut p = core::mem::take(&mut self.cur);
882 if self.space && !p.is_empty() {
884 let t: String = String::from(p.text().trim_end_matches(' '));
885 let mut q = Para::new();
886 for (s, e, f) in p.spans() {
887 let e = e.min(t.len());
888 if s < e {
889 q.push(&t[s..e], f);
890 }
891 }
892 q.mark = p.mark.clone();
893 p = q;
894 }
895 let b = self.blocks.last();
896 p.align = self.blocks.iter().rev().map(|b| b.align).find(|&a| a != RA_LEFT).unwrap_or(RA_LEFT);
897 p.style = b.map_or(PS_NORMAL, |_| self.blocks.iter().rev().map(|b| b.style).find(|&s| s != PS_NORMAL).unwrap_or(PS_NORMAL));
898 if let Some((kind, level)) = self.item {
899 p.list = kind;
900 p.level = level;
901 }
902 p.mark = self.fmt();
903 if self.ext.is_some() {
904 self.ext_para();
905 }
906 self.out.push(p);
907 self.cur = Para::new();
908 self.started = false;
909 self.space = false;
910 }
911
912 fn ext_para(&mut self) {
914 let at = self.out.len();
915 let pre = self.pre();
916 let level = self.blocks.iter().rev().find_map(|b| {
917 let t = b.tag.as_bytes();
918 (t.len() == 2 && t[0] == b'h' && (b'1'..=b'6').contains(&t[1])).then(|| t[1] - b'0')
919 });
920 let quote = self.blocks.iter().filter(|b| b.tag == "blockquote").count().min(255) as u8;
921 let row = self.blocks.last().is_some_and(|b| b.tag == "tr");
922 let node = self.blocks.last().map_or(MdNode::NONE, |b| b.node);
923 let Some(e) = self.ext.as_mut() else { return };
924 e.para_nodes.push(node);
925 e.para_boxes.push(e.open_boxes.clone());
926 if pre {
927 e.items.push((at, HExt::Pre { block: e.pre, lang: e.lang.clone() }));
928 }
929 if let Some(l) = level {
930 e.items.push((at, HExt::Heading(l)));
931 }
932 if quote > 0 {
933 e.items.push((at, HExt::Quote(quote)));
934 }
935 if let Some(n) = e.item.take() {
936 e.items.push((at, HExt::Item(n)));
937 }
938 if row && !e.cells.is_empty() {
939 let cells = core::mem::take(&mut e.cells);
940 e.items.push((at, HExt::Row { table: e.table, head: e.head || e.thead, cells }));
941 }
942 e.cells.clear();
943 e.head = false;
944 }
945
946 fn ext_open(&mut self, tag: &str, at: &[(String, String)]) {
948 let attr = |k: &str| at.iter().find(|a| a.0 == k).map(|a| a.1.as_str());
949 let num = |k: &str| attr(k).and_then(|v| v.trim().trim_end_matches("px").parse::<u32>().ok()).unwrap_or(0).min(100_000);
950 match tag {
951 "img" => {
952 let f = self.fmt();
953 let byte = self.cur.len();
954 self.cur.push("\u{FFFC}", &f);
955 self.started = true;
956 self.space = false;
957 let (src, alt) = (String::from(attr("src").unwrap_or("")), String::from(attr("alt").unwrap_or("")));
958 let (w, h) = (num("width"), num("height"));
959 let n = self.out.len();
960 if let Some(e) = self.ext.as_mut() {
961 e.items.push((n, HExt::Image { byte, src, alt, w, h }));
962 }
963 }
964 "td" | "th" => {
965 let f = self.fmt();
966 let first = self.ext.as_ref().is_none_or(|e| e.cells.is_empty());
967 if !first {
968 self.cur.push("\t", &f);
969 }
970 self.started = true;
971 self.space = false;
972 let byte = self.cur.len();
973 let a = attr("align").or_else(|| {
974 attr("style").and_then(|v| v.split(';').filter_map(|d| d.split_once(':')).find(|(p, _)| p.trim() == "text-align").map(|(_, x)| x.trim()))
975 });
976 let align = match a.map(|s| s.to_ascii_lowercase()).as_deref() {
977 Some("left") | Some("start") => 1,
978 Some("center") => 2,
979 Some("right") | Some("end") => 3,
980 _ => 0,
981 };
982 if let Some(e) = self.ext.as_mut() {
983 e.cells.push((byte, align));
984 e.head |= tag == "th";
985 e.cell = true;
986 }
987 }
988 _ => {
989 let Some(e) = self.ext.as_mut() else { return };
990 match tag {
991 "table" => {
992 e.table += 1;
993 e.thead = false;
994 }
995 "thead" => e.thead = true,
996 "tbody" | "tfoot" => e.thead = false,
997 "pre" => {
998 e.pre += 1;
999 e.lang.clear();
1000 }
1001 "code" => {
1002 if let Some(c) = attr("class") {
1003 for w in c.split_whitespace() {
1004 if let Some(l) = w.strip_prefix("language-").or_else(|| w.strip_prefix("lang-")) {
1005 e.lang = String::from(l);
1006 }
1007 }
1008 }
1009 }
1010 "ol" => {
1011 let start = attr("start").and_then(|v| v.trim().parse::<u32>().ok()).unwrap_or(1);
1012 e.nums.push(start.min(999_999_999));
1013 }
1014 "ul" => e.nums.push(0),
1015 "li" => {
1016 let n = match e.nums.last_mut() {
1017 Some(n) if *n > 0 => {
1018 let v = *n;
1019 *n = n.saturating_add(1);
1020 v
1021 }
1022 _ => 0,
1023 };
1024 e.item = Some(n);
1025 }
1026 _ => {}
1027 }
1028 }
1029 }
1030 }
1031
1032 fn text(&mut self, raw: &str) {
1033 self.text_dec(&decode(raw));
1034 }
1035
1036 fn text_dec(&mut self, t: &str) {
1038 if let Some(e) = self.ext.as_mut() {
1041 if let Some(fd) = e.cap.and_then(|f| e.fields.get_mut(f)) {
1042 if fd.kind != MF_SELECT {
1043 push_cap(&mut fd.label, &t);
1044 } else if let Some(o) = fd.options.last_mut() {
1045 push_cap(&mut o.label, &t);
1046 }
1047 return;
1048 }
1049 if let Some(l) = e.label.as_mut() {
1050 push_cap(&mut l.1, &t);
1051 }
1052 }
1053 if self.pre() {
1054 let f = self.fmt();
1055 let mut first = true;
1056 for line in lines_of(&t) {
1057 if !first {
1058 self.flush(true);
1059 }
1060 first = false;
1061 self.cur.push(&line.replace('\u{A0}', " "), &f);
1062 self.started = true;
1063 }
1064 return;
1065 }
1066 let mut s = String::new();
1067 for c in t.chars() {
1068 if c.is_whitespace() && c != '\u{A0}' {
1069 let at_start = !self.started && s.is_empty();
1070 if !self.space && !at_start {
1071 s.push(' ');
1072 self.space = true;
1073 }
1074 } else {
1075 s.push(if c == '\u{A0}' { ' ' } else { c });
1076 self.space = false;
1077 }
1078 }
1079 if !s.is_empty() {
1080 let f = self.fmt();
1081 self.cur.push(&s, &f);
1082 self.started = true;
1083 }
1084 }
1085
1086 fn fmt_for(&self, tag: &str, at: &[(String, String)]) -> Fmt {
1088 fmt_with(self.fmt(), tag, at, self.ext.is_some())
1089 }
1090
1091 fn in_cell(&self, tag: &str) -> bool {
1093 self.ext.as_ref().is_some_and(|e| e.cell) && !matches!(tag, "tr" | "td" | "th" | "table" | "thead" | "tbody" | "tfoot")
1094 }
1095
1096 fn field_new(&mut self, mut fd: MdField, at: &[(String, String)]) -> Option<usize> {
1099 let attr = |k: &str| at.iter().find(|a| a.0 == k).map(|a| a.1.as_str());
1100 let n = self.ext.as_ref()?.fields.len();
1101 if n >= FIELDS_MAX {
1102 return None;
1103 }
1104 fd.name = String::from(attr("name").unwrap_or(""));
1105 fd.id = String::from(attr("id").unwrap_or("").trim());
1106 fd.node = self.node;
1107 fd.disabled = attr("disabled").is_some();
1108 fd.readonly = attr("readonly").is_some();
1109 if !matches!(fd.kind, MF_SUBMIT | MF_RESET | MF_BUTTON) {
1110 fd.label = collapse(attr("aria-label").unwrap_or(""));
1111 }
1112 let byte = if fd.kind == MF_HIDDEN {
1113 None
1114 } else {
1115 let f = self.fmt();
1116 let b = self.cur.len();
1117 self.cur.push("\u{FFFC}", &f);
1118 self.started = true;
1119 self.space = false;
1120 Some(b)
1121 };
1122 let para = self.out.len();
1123 let e = self.ext.as_mut()?;
1124 fd.form = e.form.map_or(0, |i| i + 1);
1125 if let Some(id) = attr("form") {
1126 e.form_of.push((n, String::from(id.trim())));
1127 }
1128 if let Some(t) = attr("title") {
1129 e.titles.push((n, collapse(t)));
1130 }
1131 if let Some(l) = e.label.as_mut() {
1132 if l.2.is_none() {
1133 l.2 = Some(n);
1134 }
1135 }
1136 e.fields.push(fd);
1137 e.items.push((para, HExt::Field { byte, n }));
1138 Some(n)
1139 }
1140
1141 fn option_end(fd: &mut MdField, had_value: bool) {
1144 if let Some(o) = fd.options.last_mut() {
1145 o.label = collapse(&o.label);
1146 if !had_value {
1147 o.value = o.label.clone();
1148 }
1149 }
1150 }
1151
1152 fn cap_end(&mut self) {
1154 let Some(e) = self.ext.as_mut() else { return };
1155 let Some(f) = e.cap.take() else { return };
1156 let had_value = e.opt_val;
1157 let Some(fd) = e.fields.get_mut(f) else { return };
1158 if fd.kind == MF_SELECT {
1159 Self::option_end(fd, had_value);
1160 if fd.selected < 0 && !fd.options.is_empty() {
1161 fd.selected = 0;
1162 }
1163 } else {
1164 fd.label = collapse(&fd.label);
1165 }
1166 }
1167
1168 fn label_end(&mut self) {
1171 let Some(e) = self.ext.as_mut() else { return };
1172 let Some((to, text, field)) = e.label.take() else { return };
1173 let text = collapse(&text);
1174 if text.is_empty() {
1175 return;
1176 }
1177 if !to.is_empty() {
1178 e.labels.push((to, text));
1179 } else if let Some(fd) = field.and_then(|f| e.fields.get_mut(f)) {
1180 if fd.label.is_empty() && !matches!(fd.kind, MF_SUBMIT | MF_RESET | MF_BUTTON) {
1181 fd.label = text;
1182 }
1183 }
1184 }
1185
1186 fn raw_text(&mut self, s: &str) {
1189 if self.ext.as_ref().is_some_and(|e| e.raw.is_some()) {
1190 self.raw_text_dec(&decode(s));
1191 }
1192 }
1193
1194 fn raw_text_dec(&mut self, s: &str) {
1196 let Some(e) = self.ext.as_mut() else { return };
1197 let Some(fd) = e.raw.take().and_then(|f| e.fields.get_mut(f)) else { return };
1198 let t = s.replace("\r\n", "\n").replace('\r', "\n");
1199 fd.value = String::from(t.strip_prefix('\n').unwrap_or(&t));
1200 }
1201
1202 fn form_open(&mut self, tag: &str, at: &[(String, String)]) -> bool {
1205 let attr = |k: &str| at.iter().find(|a| a.0 == k).map(|a| a.1.as_str());
1206 let num = |k: &str| attr(k).and_then(|v| v.trim().parse::<u32>().ok()).unwrap_or(0).min(100_000);
1207 let started = self.started;
1208 let Some(e) = self.ext.as_mut() else { return false };
1209 if let Some(fd) = e.cap.and_then(|f| e.fields.get_mut(f)) {
1212 match tag {
1213 "input" | "select" | "textarea" | "button" => {}
1214 "option" if fd.kind == MF_SELECT => {
1215 if fd.options.len() < OPTIONS_MAX {
1216 Self::option_end(fd, e.opt_val);
1217 if attr("selected").is_some() {
1218 fd.selected = fd.options.len() as i32;
1219 }
1220 fd.options.push(MdOption { value: String::from(attr("value").unwrap_or("")), label: String::new() });
1221 e.opt_val = attr("value").is_some();
1222 }
1223 return true;
1224 }
1225 "img" if fd.kind != MF_SELECT => {
1226 push_cap(&mut fd.label, attr("alt").unwrap_or(""));
1227 return true;
1228 }
1229 _ => return true,
1230 }
1231 self.cap_end();
1232 }
1233 let Some(e) = self.ext.as_mut() else { return false };
1234 let field = |kind: u8| MdField { kind, selected: -1, ..MdField::default() };
1235 match tag {
1236 "form" => {
1237 if e.form.is_none() && e.forms.len() < FORMS_MAX {
1239 let method = if attr("method").is_some_and(|m| m.trim().eq_ignore_ascii_case("post")) { "post" } else { "get" };
1240 e.forms.push(MdForm {
1241 action: String::from(attr("action").unwrap_or("").trim()),
1242 method: String::from(method),
1243 id: String::from(attr("id").unwrap_or("").trim()),
1244 name: String::from(attr("name").unwrap_or("")),
1245 node: self.node,
1246 });
1247 e.form = Some(e.forms.len() as u32 - 1);
1248 if !self.in_cell(tag) {
1249 self.flush(false);
1250 }
1251 }
1252 }
1253 "label" => {
1254 self.label_end();
1255 if let Some(e) = self.ext.as_mut() {
1256 e.label = Some((String::from(attr("for").unwrap_or("").trim()), String::new(), None));
1257 }
1258 }
1259 "input" => {
1260 let ty = attr("type").unwrap_or("text").trim().to_ascii_lowercase();
1261 let kind = match ty.as_str() {
1262 "password" => MF_PASSWORD,
1263 "checkbox" => MF_CHECKBOX,
1264 "radio" => MF_RADIO,
1265 "hidden" => MF_HIDDEN,
1266 "submit" | "image" => MF_SUBMIT,
1267 "reset" => MF_RESET,
1268 "button" => MF_BUTTON,
1269 _ => MF_TEXT,
1270 };
1271 let checked = attr("checked").is_some();
1272 if kind == MF_CHECKBOX && attr("disabled").is_some() && e.item.is_some() && !started {
1274 e.items.push((self.out.len(), HExt::Task(checked)));
1275 return true;
1276 }
1277 let value = attr("value");
1278 let mut fd = field(kind);
1279 fd.checked = checked;
1280 fd.value = String::from(match kind {
1281 MF_CHECKBOX | MF_RADIO => value.unwrap_or("on"),
1282 _ => value.unwrap_or(""),
1283 });
1284 if kind == MF_TEXT || kind == MF_PASSWORD {
1285 fd.value.retain(|c| c != '\r' && c != '\n');
1286 }
1287 if matches!(kind, MF_SUBMIT | MF_RESET | MF_BUTTON) {
1288 fd.label = collapse(if ty == "image" { attr("alt").unwrap_or("") } else { value.unwrap_or("") });
1289 }
1290 fd.placeholder = collapse(attr("placeholder").unwrap_or(""));
1291 fd.size = num("size");
1292 fd.maxlength = num("maxlength");
1293 self.field_new(fd, at);
1294 }
1295 "textarea" => {
1296 let mut fd = field(MF_TEXTAREA);
1297 fd.placeholder = collapse(attr("placeholder").unwrap_or(""));
1298 fd.size = num("cols");
1299 fd.rows = num("rows");
1300 fd.maxlength = num("maxlength");
1301 let n = self.field_new(fd, at);
1302 if let Some(e) = self.ext.as_mut() {
1303 e.raw = n;
1304 }
1305 self.skip = Some(String::from("textarea"));
1307 }
1308 "select" | "button" => {
1309 let kind = match (tag, attr("type").map(|t| t.trim().to_ascii_lowercase()).as_deref()) {
1310 ("select", _) => MF_SELECT,
1311 (_, Some("reset")) => MF_RESET,
1312 (_, Some("button")) => MF_BUTTON,
1313 _ => MF_SUBMIT,
1314 };
1315 let mut fd = field(kind);
1316 if kind != MF_SELECT {
1317 fd.value = String::from(attr("value").unwrap_or(""));
1318 }
1319 let n = self.field_new(fd, at);
1320 if let Some(e) = self.ext.as_mut() {
1321 e.cap = n;
1322 e.opt_val = false;
1323 }
1324 }
1325 _ => return false,
1326 }
1327 true
1328 }
1329
1330 fn form_close(&mut self, tag: &str) -> bool {
1332 let Some(e) = self.ext.as_ref() else { return false };
1333 if let Some(f) = e.cap {
1334 let select = e.fields.get(f).is_some_and(|fd| fd.kind == MF_SELECT);
1335 match tag {
1336 "select" if select => {}
1337 "button" if !select => {}
1338 "form" => {}
1339 _ => return true,
1341 }
1342 self.cap_end();
1343 if tag != "form" {
1344 return true;
1345 }
1346 }
1347 match tag {
1348 "label" => self.label_end(),
1349 "form" => {
1350 if self.ext.as_ref().is_some_and(|e| e.form.is_some()) {
1351 if !self.in_cell(tag) {
1352 self.flush(false);
1353 }
1354 if let Some(e) = self.ext.as_mut() {
1355 e.form = None;
1356 }
1357 }
1358 }
1359 _ => return false,
1360 }
1361 true
1362 }
1363
1364 fn open(&mut self, tag: &str, at: &[(String, String)], selfclose: bool) {
1365 if self.ext.is_some() && self.form_open(tag, at) {
1366 return;
1367 }
1368 if self.in_cell(tag) && (BLOCKS.contains(&tag) || matches!(tag, "br" | "hr" | "ul" | "ol")) {
1369 if !self.space && self.started {
1370 self.text(" ");
1371 }
1372 return;
1373 }
1374 match tag {
1375 "script" | "style" | "head" | "title" | "template" | "noscript" | "svg" => {
1376 if !selfclose {
1377 self.skip = Some(String::from(tag));
1378 }
1379 return;
1380 }
1381 "br" => {
1382 self.flush(true);
1383 return;
1384 }
1385 "img" | "hr" | "meta" | "link" | "input" => {
1386 if tag == "hr" {
1387 self.flush(false);
1388 if let Some(e) = self.ext.as_mut() {
1389 e.items.push((self.out.len(), HExt::Rule));
1390 e.rule_boxes.push((self.out.len(), e.open_boxes.clone()));
1391 }
1392 } else if tag == "img" && self.ext.is_some() {
1393 self.ext_open(tag, at);
1394 }
1395 return;
1396 }
1397 "ul" | "ol" => {
1398 self.flush(false);
1399 if self.lists.len() < NEST_MAX {
1400 self.lists.push(if tag == "ol" { LS_NUMBER } else { LS_BULLET });
1401 if self.ext.is_some() {
1402 self.ext_open(tag, at);
1403 }
1404 }
1405 return;
1406 }
1407 _ => {}
1408 }
1409 if self.blocks.len() >= NEST_MAX || self.fmts.len() >= NEST_MAX {
1411 return;
1412 }
1413 let block = BLOCKS.contains(&tag);
1414 if self.ext.is_some() && matches!(tag, "table" | "thead" | "tbody" | "tfoot" | "code") {
1415 self.ext_open(tag, at);
1416 }
1417 if block {
1418 self.flush(false);
1419 let mut align = RA_LEFT;
1420 for (k, v) in at {
1421 let v = v.to_ascii_lowercase();
1422 let a = if k == "align" {
1423 Some(v.as_str())
1424 } else if k == "style" {
1425 v.split(';').filter_map(|d| d.split_once(':')).find(|(p, _)| p.trim() == "text-align").map(|(_, x)| x.trim())
1426 } else {
1427 None
1428 };
1429 align = match a {
1430 Some("center") => RA_CENTER,
1431 Some("right") | Some("end") => RA_RIGHT,
1432 Some("justify") => RA_JUSTIFY,
1433 _ => align,
1434 };
1435 }
1436 if tag == "center" {
1437 align = RA_CENTER;
1438 }
1439 let style = match tag {
1440 "h1" => PS_H1,
1441 "h2" => PS_H2,
1442 "h3" | "h4" | "h5" | "h6" => PS_H3,
1443 "blockquote" => PS_QUOTE,
1444 _ => PS_NORMAL,
1445 };
1446 let pre = tag == "pre"
1447 || at.iter().any(|(k, v)| k == "style" && v.to_ascii_lowercase().replace(' ', "").contains("white-space:pre"));
1448 self.blocks.push(Block { tag: String::from(tag), align, style, pre, node: self.node });
1449 if tag == "li" {
1450 let kind = self.lists.last().copied().unwrap_or(LS_BULLET);
1451 self.item = Some((kind, (self.lists.len().max(1) - 1).min(LEVEL_MAX as usize) as u8));
1452 }
1453 if self.ext.is_some() && matches!(tag, "pre" | "li") {
1454 self.ext_open(tag, at);
1455 }
1456 }
1457 if self.ext.is_some() && matches!(tag, "td" | "th") {
1458 self.ext_open(tag, at);
1459 }
1460 if selfclose {
1461 if block {
1462 self.close(tag);
1463 }
1464 return;
1465 }
1466 let f = self.fmt_for(tag, at);
1467 self.fmts.push((String::from(tag), f));
1468 }
1469
1470 fn close(&mut self, tag: &str) {
1471 if self.ext.is_some() && self.form_close(tag) {
1472 return;
1473 }
1474 if let Some(i) = self.fmts.iter().rposition(|f| f.0 == tag) {
1475 self.fmts.truncate(i);
1476 }
1477 if self.in_cell(tag) && (BLOCKS.contains(&tag) || matches!(tag, "ul" | "ol")) {
1478 return;
1479 }
1480 if let Some(e) = self.ext.as_mut() {
1481 if matches!(tag, "td" | "th" | "tr" | "table") {
1482 e.cell = false;
1483 }
1484 }
1485 match tag {
1486 "ul" | "ol" => {
1487 self.flush(false);
1488 self.lists.pop();
1489 if self.lists.is_empty() {
1490 self.item = None;
1491 }
1492 if let Some(e) = self.ext.as_mut() {
1493 e.nums.pop();
1494 }
1495 return;
1496 }
1497 "thead" => {
1498 if let Some(e) = self.ext.as_mut() {
1499 e.thead = false;
1500 }
1501 }
1502 _ => {}
1503 }
1504 if BLOCKS.contains(&tag) {
1505 self.flush(false);
1506 if let Some(i) = self.blocks.iter().rposition(|b| b.tag == tag) {
1507 self.blocks.truncate(i);
1508 }
1509 if tag == "li" {
1510 self.item = None;
1511 }
1512 }
1513 }
1514
1515 fn run(&mut self, s: &str) {
1516 let mut rest = s;
1517 while !rest.is_empty() {
1518 if let Some(tag) = &self.skip {
1519 let close = format!("</{}", tag);
1520 match find_ci(rest, &close) {
1521 Some(i) => {
1522 self.raw_text(&rest[..i]);
1523 rest = &rest[i..];
1524 self.skip = None;
1525 }
1526 None => {
1527 self.raw_text(rest);
1528 return;
1529 }
1530 }
1531 }
1532 let Some(lt) = rest.find('<') else {
1533 self.text(rest);
1534 return;
1535 };
1536 if lt > 0 {
1537 self.text(&rest[..lt]);
1538 }
1539 rest = &rest[lt..];
1540 if rest.starts_with("<!--") {
1541 rest = rest.find("-->").map_or("", |j| &rest[j + 3..]);
1542 continue;
1543 }
1544 if rest.starts_with("<!") || rest.starts_with("<?") {
1545 rest = rest.find('>').map_or("", |j| &rest[j + 1..]);
1546 continue;
1547 }
1548 let Some(gt) = tag_end(rest) else {
1549 self.text(rest);
1550 return;
1551 };
1552 let inner = &rest[1..gt];
1553 rest = &rest[gt + 1..];
1554 if let Some(name) = inner.strip_prefix('/') {
1555 let name = name.trim().split_whitespace().next().unwrap_or("").to_ascii_lowercase();
1556 self.close(&name);
1557 continue;
1558 }
1559 let name_end = inner.find(|c: char| c.is_whitespace() || c == '/').unwrap_or(inner.len());
1560 let name = inner[..name_end].to_ascii_lowercase();
1561 if name.is_empty() || !name.as_bytes()[0].is_ascii_alphabetic() {
1562 self.text("<");
1563 self.text(inner);
1564 self.text(">");
1565 continue;
1566 }
1567 let selfclose = inner.trim_end().ends_with('/');
1568 let at = attrs(&inner[name_end..]);
1569 self.open(&name, &at, selfclose);
1570 }
1571 }
1572
1573 fn finish(mut self) -> Vec<Para> {
1574 self.flush(false);
1575 while self.out.len() > 1 && self.out.last().is_some_and(|p| p.is_empty()) {
1577 self.out.pop();
1578 }
1579 self.out
1580 }
1581}
1582
1583const RULES_MAX: usize = 4096;
1587const DECLS_MAX: usize = 256;
1588
1589type Decl = (String, String, bool);
1591
1592#[derive(Debug)]
1594struct Rule {
1595 sel: Sel,
1596 spec: u32,
1597 order: u32,
1598 decls: u32,
1599}
1600
1601#[derive(Debug, Default)]
1605pub(crate) struct Sheet {
1606 rules: Vec<Rule>,
1607 decls: Vec<Vec<Decl>>,
1608 ids: BTreeMap<String, Vec<u32>>,
1609 classes: BTreeMap<String, Vec<u32>>,
1610 tags: BTreeMap<String, Vec<u32>>,
1611 any: Vec<u32>,
1612 size: usize,
1614 links: Vec<String>,
1616}
1617
1618fn sheet_link<'a>(d: &'a Dom, x: u32) -> Option<&'a str> {
1620 let rel = d.attr(x, "rel")?;
1621 if !rel.split_ascii_whitespace().any(|w| w.eq_ignore_ascii_case("stylesheet")) {
1622 return None;
1623 }
1624 d.attr(x, "href").map(str::trim).filter(|h| !h.is_empty())
1625}
1626
1627impl Sheet {
1628 pub(crate) fn of(d: &Dom, linked: &BTreeMap<String, String>) -> Sheet {
1630 let mut s = Sheet::default();
1631 if !d.is_html() {
1632 return s;
1633 }
1634 for x in d.subtree(0) {
1635 let n = &d.nodes[x as usize];
1636 if n.kind != K_ELEM || (n.name != "style" && n.name != "link") {
1637 continue;
1638 }
1639 let media = d.attr(x, "media").unwrap_or("").to_ascii_lowercase();
1640 let ty = d.attr(x, "type").unwrap_or("text/css").trim().to_ascii_lowercase();
1641 if (!media.trim().is_empty() && !media.contains("screen") && !media.contains("all")) || (!ty.is_empty() && ty != "text/css") {
1642 continue;
1643 }
1644 if n.name == "style" {
1645 s.add(&d.text_of(x));
1646 continue;
1647 }
1648 let Some(href) = sheet_link(d, x) else { continue };
1649 if !s.links.iter().any(|l| l == href) {
1650 s.links.push(String::from(href));
1651 }
1652 if let Some(css) = linked.get(href) {
1653 s.add(css);
1654 }
1655 }
1656 s
1657 }
1658
1659 pub(crate) fn links(&self) -> &[String] {
1661 &self.links
1662 }
1663
1664 pub(crate) fn style_of(&self, d: &Dom, i: u32) -> String {
1667 let mut budget = self.budget(64);
1668 match self.attrs_of(d, i, &mut budget) {
1669 Some(at) => at.into_iter().find(|a| a.0 == "style").map_or(String::new(), |a| a.1),
1670 None => String::from(d.attr(i, "style").unwrap_or("")),
1671 }
1672 }
1673
1674 pub(crate) fn is_empty(&self) -> bool {
1676 self.rules.is_empty()
1677 }
1678
1679 pub(crate) fn budget(&self, n: usize) -> usize {
1681 n.saturating_add(self.size).saturating_mul(64).saturating_add(1 << 20)
1682 }
1683
1684 fn add(&mut self, css: &str) {
1687 self.size = self.size.saturating_add(css.len());
1688 let s = strip_comments(css);
1689 let b = s.as_bytes();
1690 let mut i = 0;
1691 while i < b.len() && self.rules.len() < RULES_MAX {
1692 if b[i].is_ascii_whitespace() || b[i] == b';' || b[i] == b'}' {
1693 i += 1;
1694 continue;
1695 }
1696 if b[i] == b'@' {
1697 i = skip_rule(b, i, true);
1699 continue;
1700 }
1701 let Some(open) = s[i..].find('{').map(|k| i + k) else { break };
1702 let end = skip_rule(b, open, false);
1703 let sel = s[i..open].trim();
1704 let body = &s[open + 1..end.saturating_sub(1).max(open + 1)];
1705 i = end;
1706 let Some(sels) = parse_sels(sel) else { continue };
1707 let decls = parse_decls(body);
1708 if decls.is_empty() {
1709 continue;
1710 }
1711 let di = self.decls.len() as u32;
1712 self.decls.push(decls);
1713 for sel in sels {
1714 if self.rules.len() >= RULES_MAX {
1715 break;
1716 }
1717 let r = self.rules.len() as u32;
1718 match sel.key() {
1719 SelKey::Id(k) => self.ids.entry(String::from(k)).or_default().push(r),
1720 SelKey::Class(k) => self.classes.entry(String::from(k)).or_default().push(r),
1721 SelKey::Tag(k) => self.tags.entry(String::from(k)).or_default().push(r),
1722 SelKey::Any => self.any.push(r),
1723 }
1724 self.rules.push(Rule { spec: sel.spec(), sel, order: r, decls: di });
1725 }
1726 }
1727 }
1728
1729 pub(crate) fn attrs_of(&self, d: &Dom, i: u32, budget: &mut usize) -> Option<Vec<(String, String)>> {
1733 if self.rules.is_empty() || *budget == 0 {
1734 return None;
1735 }
1736 let mut hits: Vec<(u32, u32, u32)> = Vec::new();
1737 let test = |list: &[u32], hits: &mut Vec<(u32, u32, u32)>, budget: &mut usize| {
1738 for &r in list {
1739 if *budget == 0 {
1740 break;
1741 }
1742 let rule = &self.rules[r as usize];
1743 if sel_match(d, i, &rule.sel, budget) {
1744 hits.push((rule.spec, rule.order, rule.decls));
1745 }
1746 }
1747 };
1748 test(&self.any, &mut hits, budget);
1749 if let Some(v) = d.attr(i, "id").and_then(|id| self.ids.get(id)) {
1750 test(v, &mut hits, budget);
1751 }
1752 if let Some(c) = d.attr(i, "class") {
1753 for w in c.split_ascii_whitespace() {
1754 if let Some(v) = self.classes.get(w) {
1755 test(v, &mut hits, budget);
1756 }
1757 }
1758 }
1759 if let Some(v) = self.tags.get(&d.nodes[i as usize].name) {
1760 test(v, &mut hits, budget);
1761 }
1762 if hits.is_empty() {
1763 return None;
1764 }
1765 hits.sort_unstable();
1766 hits.dedup();
1767 let own = d.attr(i, "style").map(parse_decls).unwrap_or_default();
1768 let mut seq: Vec<(&str, &str)> = Vec::new();
1771 for imp in [false, true] {
1772 let rules = hits.iter().flat_map(|h| self.decls[h.2 as usize].iter());
1773 for (p, v, im) in rules.chain(own.iter()) {
1774 if *im == imp {
1775 seq.push((p, v));
1776 }
1777 }
1778 }
1779 let mut last: BTreeMap<&str, usize> = BTreeMap::new();
1780 for (k, (p, _)) in seq.iter().enumerate() {
1781 last.insert(p, k);
1782 }
1783 let mut css = String::new();
1784 for (k, (p, v)) in seq.iter().enumerate() {
1785 if last.get(p) == Some(&k) {
1786 css.push_str(p);
1787 css.push(':');
1788 css.push_str(v);
1789 css.push(';');
1790 }
1791 }
1792 let mut at = d.nodes[i as usize].attrs.clone();
1793 match at.iter_mut().find(|a| a.0 == "style") {
1794 Some(a) => a.1 = css,
1795 None => at.push((String::from("style"), css)),
1796 }
1797 Some(at)
1798 }
1799}
1800
1801fn strip_comments(s: &str) -> String {
1803 let mut out = String::with_capacity(s.len());
1804 let mut rest = s;
1805 while let Some(i) = rest.find("/*") {
1806 out.push_str(&rest[..i]);
1807 out.push(' ');
1808 rest = rest[i + 2..].find("*/").map_or("", |j| &rest[i + 2 + j + 2..]);
1809 }
1810 out.push_str(rest);
1811 out
1812}
1813
1814fn skip_rule(b: &[u8], mut i: usize, at: bool) -> usize {
1817 let (mut depth, mut q) = (0usize, 0u8);
1818 while i < b.len() {
1819 let c = b[i];
1820 i += 1;
1821 if q != 0 {
1822 if c == q {
1823 q = 0;
1824 }
1825 continue;
1826 }
1827 match c {
1828 b'"' | b'\'' => q = c,
1829 b'{' => depth += 1,
1830 b'}' => {
1831 depth = depth.saturating_sub(1);
1832 if depth == 0 {
1833 return i;
1834 }
1835 }
1836 b';' if at && depth == 0 => return i,
1837 _ => {}
1838 }
1839 }
1840 i
1841}
1842
1843fn parse_decls(body: &str) -> Vec<Decl> {
1845 let mut out = Vec::new();
1846 let b = body.as_bytes();
1847 let (mut start, mut q, mut paren) = (0, 0u8, 0usize);
1848 let push = |d: &str, out: &mut Vec<Decl>| {
1849 let Some((p, v)) = d.split_once(':') else { return };
1850 let p = p.trim().to_ascii_lowercase();
1851 if p.is_empty() || !p.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'-') || out.len() >= DECLS_MAX {
1852 return;
1853 }
1854 let mut v = v.trim();
1855 let mut imp = false;
1856 if let Some(k) = v.rfind('!') {
1857 if v[k + 1..].trim().eq_ignore_ascii_case("important") {
1858 imp = true;
1859 v = v[..k].trim();
1860 }
1861 }
1862 if !v.is_empty() {
1863 out.push((p, String::from(v), imp));
1864 }
1865 };
1866 for (i, &c) in b.iter().enumerate() {
1867 if q != 0 {
1868 if c == q {
1869 q = 0;
1870 }
1871 continue;
1872 }
1873 match c {
1874 b'"' | b'\'' => q = c,
1875 b'(' => paren += 1,
1876 b')' => paren = paren.saturating_sub(1),
1877 b';' if paren == 0 => {
1878 push(&body[start..i], &mut out);
1879 start = i + 1;
1880 }
1881 _ => {}
1882 }
1883 }
1884 push(&body[start..], &mut out);
1885 out
1886}
1887
1888pub(crate) fn box_tag(t: &str) -> bool {
1890 BLOCKS.contains(&t)
1891 || matches!(t, "ul" | "ol" | "dl" | "nav" | "main" | "aside" | "form" | "fieldset" | "details" | "summary" | "body" | "figcaption" | "hgroup" | "dialog" | "menu")
1892}
1893
1894fn css_len(v: &str) -> Option<i32> {
1896 let v = v.trim().to_ascii_lowercase();
1897 let num = |s: &str| s.trim().parse::<f32>().ok();
1898 let px = if v == "0" || v == "auto" {
1899 0.0
1900 } else if let Some(n) = v.strip_suffix("px") {
1901 num(n)?
1902 } else if let Some(n) = v.strip_suffix("pt") {
1903 num(n)? * 4.0 / 3.0
1904 } else if let Some(n) = v.strip_suffix("rem").or_else(|| v.strip_suffix("em")) {
1905 num(n)? * 14.0
1906 } else {
1907 match v.as_str() {
1908 "thin" => 1.0,
1909 "medium" => 3.0,
1910 "thick" => 5.0,
1911 _ => return None,
1912 }
1913 };
1914 Some((px.clamp(-2000.0, 2000.0) + if px < 0.0 { -0.5 } else { 0.5 }) as i32)
1915}
1916
1917fn sides<T: Copy>(v: &[T]) -> Option<[T; 4]> {
1919 match *v {
1920 [a] => Some([a, a, a, a]),
1921 [a, b] => Some([a, b, a, b]),
1922 [a, b, c] => Some([a, b, c, b]),
1923 [a, b, c, d] => Some([a, b, c, d]),
1924 _ => None,
1925 }
1926}
1927
1928fn border_style(v: &str) -> Option<bool> {
1930 match v.trim().to_ascii_lowercase().as_str() {
1931 "solid" | "dashed" | "dotted" | "double" | "groove" | "ridge" | "inset" | "outset" => Some(true),
1932 "none" | "hidden" => Some(false),
1933 _ => None,
1934 }
1935}
1936
1937fn words(v: &str) -> Vec<&str> {
1939 let mut out = Vec::new();
1940 let (mut start, mut paren) = (None, 0usize);
1941 for (i, c) in v.char_indices() {
1942 match c {
1943 '(' => paren += 1,
1944 ')' => paren = paren.saturating_sub(1),
1945 _ => {}
1946 }
1947 if c.is_whitespace() && paren == 0 {
1948 if let Some(s) = start.take() {
1949 out.push(&v[s..i]);
1950 }
1951 } else if start.is_none() {
1952 start = Some(i);
1953 }
1954 }
1955 if let Some(s) = start {
1956 out.push(&v[s..]);
1957 }
1958 out
1959}
1960
1961pub(crate) fn box_of(style: &str) -> Option<MdBox> {
1964 let mut b = MdBox::default();
1965 let mut bw: [Option<i32>; 4] = [None; 4];
1967 let mut bs: [Option<bool>; 4] = [None; 4];
1968 let mut bc = [0u32; 4];
1969 let pick = |p: &str| match p {
1970 "top" => Some(0),
1971 "right" => Some(1),
1972 "bottom" => Some(2),
1973 "left" => Some(3),
1974 _ => None,
1975 };
1976 for decl in style.split(';') {
1977 let Some((p, v)) = decl.split_once(':') else { continue };
1978 let p = p.trim().to_ascii_lowercase();
1979 let v = v.trim();
1980 let lens = || -> Option<[i32; 4]> { sides(&words(v).iter().map(|w| css_len(w)).collect::<Option<Vec<i32>>>()?) };
1981 match p.as_str() {
1982 "margin" => {
1983 if let Some(s) = lens() {
1984 b.margin = s.map(|x| x.clamp(0, 1000) as i16);
1985 b.mset = 15;
1986 }
1987 }
1988 "padding" => {
1989 if let Some(s) = lens() {
1990 b.padding = s.map(|x| x.clamp(0, 1000) as i16);
1991 }
1992 }
1993 "border-width" => {
1994 if let Some(s) = lens() {
1995 bw = s.map(Some);
1996 }
1997 }
1998 "border-style" => {
1999 if let Some(s) = sides(&words(v).iter().map(|w| border_style(w)).collect::<Option<Vec<bool>>>().unwrap_or_default()) {
2000 bs = s.map(Some);
2001 }
2002 }
2003 "border-color" => {
2004 if let Some(s) = sides(&words(v).iter().map(|w| css_color(w)).collect::<Option<Vec<u32>>>().unwrap_or_default()) {
2005 bc = s;
2006 }
2007 }
2008 "background" | "background-color" => {
2009 if let Some(c) = words(v).into_iter().find_map(css_color) {
2010 b.bg = c;
2011 } else if v.eq_ignore_ascii_case("none") || v.eq_ignore_ascii_case("transparent") {
2012 b.bg = 0;
2013 }
2014 }
2015 _ => {
2016 let rest = p.strip_prefix("margin-").or_else(|| p.strip_prefix("padding-"));
2017 if let (Some(side), Some(n)) = (rest.and_then(pick), css_len(v)) {
2018 if p.starts_with('m') {
2019 b.margin[side] = n.clamp(0, 1000) as i16;
2020 b.mset |= 1 << side;
2021 } else {
2022 b.padding[side] = n.clamp(0, 1000) as i16;
2023 }
2024 continue;
2025 }
2026 let Some(rest) = p.strip_prefix("border") else { continue };
2028 let (which, part) = match rest.strip_prefix('-') {
2029 None if rest.is_empty() => ([true; 4], ""),
2030 Some(r) => match r.split_once('-') {
2031 Some((s, part)) => match pick(s) {
2032 Some(k) => (core::array::from_fn(|i| i == k), part),
2033 None => continue,
2034 },
2035 None => match pick(r) {
2036 Some(k) => (core::array::from_fn(|i| i == k), ""),
2037 None => continue,
2038 },
2039 },
2040 _ => continue,
2041 };
2042 for (k, on) in which.iter().enumerate() {
2043 if !on {
2044 continue;
2045 }
2046 match part {
2047 "width" => bw[k] = css_len(v).or(bw[k]),
2048 "style" => bs[k] = border_style(v).or(bs[k]),
2049 "color" => bc[k] = css_color(v).unwrap_or(bc[k]),
2050 "" => {
2051 let (mut w, mut s, mut c) = (None, None, 0);
2053 for t in words(v) {
2054 if let Some(x) = border_style(t) {
2055 s = Some(x);
2056 } else if let Some(x) = css_len(t) {
2057 w = Some(x);
2058 } else if let Some(x) = css_color(t) {
2059 c = x;
2060 }
2061 }
2062 bw[k] = w;
2063 bs[k] = Some(s.unwrap_or(false));
2064 bc[k] = c;
2065 }
2066 _ => {}
2067 }
2068 }
2069 }
2070 }
2071 }
2072 for k in 0..4 {
2073 b.border[k] = if bs[k] == Some(true) { bw[k].unwrap_or(3).clamp(0, 100) as u8 } else { 0 };
2075 b.border_color[k] = bc[k];
2076 }
2077 let any = b.margin.iter().any(|&x| x != 0) || b.padding.iter().any(|&x| x != 0) || b.border.iter().any(|&x| x != 0) || b.bg != 0 || b.mset != 0;
2078 any.then_some(b)
2079}
2080
2081fn fmt_with(mut f: Fmt, tag: &str, at: &[(String, String)], view: bool) -> Fmt {
2084 if view && tag == "a" && at.iter().any(|a| a.0 == "href") {
2085 f.color = 0;
2086 }
2087 let boxed = view && box_tag(tag);
2088 match tag {
2089 "b" | "strong" => f.flags |= RF_BOLD,
2090 "i" | "em" | "cite" | "var" | "dfn" => f.flags |= RF_ITALIC,
2091 "u" | "ins" => f.flags |= RF_UNDER,
2092 "s" | "strike" | "del" => f.flags |= RF_STRIKE,
2093 "code" | "tt" | "kbd" | "samp" => f.font = String::from("monospace"),
2094 "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "th" => {}
2095 _ => {}
2096 }
2097 for (k, v) in at {
2098 match k.as_str() {
2099 "href" if tag == "a" => f.link = v.clone(),
2100 "color" if tag == "font" => f.color = css_color(v).unwrap_or(f.color),
2101 "face" if tag == "font" => f.font = family_of(v),
2102 "size" if tag == "font" => {
2103 f.size = match v.trim() {
2104 "1" => 75,
2105 "2" => 100,
2106 "3" => 120,
2107 "4" => 135,
2108 "5" => 180,
2109 "6" => 240,
2110 "7" => 360,
2111 _ => f.size,
2112 }
2113 }
2114 "style" => {
2115 for decl in v.split(';') {
2116 let Some((p, val)) = decl.split_once(':') else { continue };
2117 let val = val.trim().trim_end_matches("!important").trim();
2118 match p.trim().to_ascii_lowercase().as_str() {
2119 "color" => f.color = css_color(val).unwrap_or(f.color),
2120 "background-color" | "background" if !boxed => {
2121 if let Some(c) = val.split_whitespace().find_map(css_color) {
2122 if c & 0xFF_FFFF != 0xFF_FFFF {
2123 f.bg = c;
2124 }
2125 }
2126 }
2127 "font-weight" => {
2128 let bold = matches!(val, "bold" | "bolder") || val.parse::<u32>().is_ok_and(|w| w >= 600);
2129 if bold {
2130 f.flags |= RF_BOLD;
2131 } else if matches!(val, "normal" | "lighter") || val.parse::<u32>().is_ok_and(|w| w < 600) {
2132 f.flags &= !RF_BOLD;
2133 }
2134 }
2135 "font-style" => {
2136 if val.starts_with("italic") || val.starts_with("oblique") {
2137 f.flags |= RF_ITALIC;
2138 } else if val == "normal" {
2139 f.flags &= !RF_ITALIC;
2140 }
2141 }
2142 "text-decoration" | "text-decoration-line" => {
2143 if val.contains("underline") {
2144 f.flags |= RF_UNDER;
2145 }
2146 if val.contains("line-through") {
2147 f.flags |= RF_STRIKE;
2148 }
2149 if val == "none" {
2150 f.flags &= !(RF_UNDER | RF_STRIKE);
2151 }
2152 }
2153 "font-size" => f.size = css_size(val, f.size).unwrap_or(f.size),
2154 "font-family" => f.font = family_of(val),
2155 _ => {}
2156 }
2157 }
2158 }
2159 _ => {}
2160 }
2161 }
2162 f
2163}
2164
2165pub(crate) fn tag_end(s: &str) -> Option<usize> {
2167 let mut q = 0u8;
2168 for (i, b) in s.bytes().enumerate().skip(1) {
2169 if q != 0 {
2170 if b == q {
2171 q = 0;
2172 }
2173 } else if b == b'"' || b == b'\'' {
2174 q = b;
2175 } else if b == b'>' {
2176 return Some(i);
2177 }
2178 }
2179 None
2180}
2181
2182pub(crate) fn find_ci(hay: &str, needle: &str) -> Option<usize> {
2183 let h = hay.as_bytes();
2184 let n = needle.as_bytes();
2185 if n.len() > h.len() {
2186 return None;
2187 }
2188 (0..=h.len() - n.len()).find(|&i| h[i..i + n.len()].eq_ignore_ascii_case(n))
2189}
2190
2191fn family_of(v: &str) -> String {
2193 let f = v.split(',').next().unwrap_or("").trim().trim_matches(['\'', '"']).trim();
2194 String::from(f)
2195}