`: an empty paragraph on purpose. p.inlines.clear(); self.para_cont = false; self.container().push(FBlock::Para(p)); return; } if p.is_empty() && p.kind != Kind::Code && p.list.is_none() { // Whitespace-only text between blocks. return; } let cont = std::mem::take(&mut self.para_cont); let level = p.list.map(|l| l.level); let c = self.container(); if cont && let Some(FBlock::Para(prev)) = c.last_mut() && prev.list.is_some() && prev.list.map(|l| l.level) == level { prev.push_text("\n", &Fmt::default()); prev.inlines.extend(p.inlines); return; } c.push(FBlock::Para(p)); } fn text(&mut self, t: &str) { if self.in_head() { if let Some(title) = &mut self.title { title.push_str(t); } return; } if t.is_empty() { return; } let inside_table_only = matches!( self.stack.iter().rev().find(|e| matches!(e.kind, ElKind::Table | ElKind::Tr | ElKind::Cell)).map(|e| e.kind), Some(ElKind::Table | ElKind::Tr) ); if inside_table_only && t.trim().is_empty() { return; } let code = self.stack.iter().any(|e| e.kind == ElKind::Pre); let f = self.fmt(); if code { let mut first = true; for line in t.split('\n') { if !first { // A newline right after
is ignored.
let empty_start = self.para.as_ref().is_none_or(|p| p.inlines.is_empty()) && self.pre_fresh();
if !empty_start {
self.start_para();
self.flush();
}
}
first = false;
let line = line.trim_end_matches('\r');
if !line.is_empty() {
self.start_para().push_text(line, &Fmt { code: false, ..f.clone() });
self.mark_pre_used();
}
}
return;
}
if self.preserve() {
self.note_edge(true);
self.start_para().push_text(t, &f);
return;
}
// Collapse whitespace.
let mut s = String::with_capacity(t.len());
let mut prev_space = self.para.as_ref().is_none_or(|p| match p.inlines.last() {
Some(Inline::Text(x, _)) => x.ends_with([' ', '\n']),
Some(_) => false,
None => true,
});
for c in t.chars() {
if matches!(c, ' ' | '\t' | '\n' | '\r' | '\u{000C}') {
if !prev_space {
s.push(' ');
prev_space = true;
}
} else {
s.push(c);
prev_space = false;
}
}
if s.is_empty() {
return;
}
self.note_edge(false);
self.start_para().push_text(&s, &f);
}
/// Record whether text about to be added keeps its white space (see `pre_lead`).
fn note_edge(&mut self, preserved: bool) {
if self.para.as_ref().is_none_or(|p| p.inlines.is_empty()) {
self.pre_lead = Some(preserved);
}
self.pre_tail = preserved;
}
/// Add a `` element's base text and reading as one inline (#1681). Half-marked ruby (no
/// base, or no reading) is plain text instead: a guide that annotates nothing is no guide, and
/// the text it held is never lost.
fn push_ruby(&mut self, base: &str, ruby: &str) {
let preserve = self.preserve();
let text = |s: &str| if preserve { s.to_string() } else { collapse_ws(s) };
let (base, ruby) = (text(base), text(ruby));
let f = self.fmt();
if base.is_empty() || ruby.is_empty() {
self.start_para().push_text(if base.is_empty() { &ruby } else { &base }, &f);
return;
}
self.start_para().inlines.push(Inline::Ruby { base, ruby, fmt: f });
}
fn pre_fresh(&self) -> bool {
self.stack.iter().rev().find(|e| e.kind == ElKind::Pre).is_some_and(|e| !e.used)
}
fn mark_pre_used(&mut self) {
if let Some(e) = self.stack.iter_mut().rev().find(|e| e.kind == ElKind::Pre) {
e.used = true;
}
}
/// Pop elements down to and including index `k`.
fn pop_to(&mut self, k: usize) {
while self.stack.len() > k {
let Some(e) = self.stack.pop() else { break };
self.close(&e);
}
}
fn close(&mut self, e: &El) {
if e.page_break_after || e.page_break {
self.pending_page_break = true;
}
if e.kind != ElKind::Inline {
self.flush();
}
match e.kind {
ElKind::Cell => {
self.flush();
let blocks = if self.containers.len() > 1 { self.containers.pop().unwrap_or_default() } else { Vec::new() };
let mut cell = self.cells.pop().unwrap_or_default();
cell.blocks = blocks;
if let Some(t) = self.tables.last_mut() {
t.row.get_or_insert_with(Vec::new).push(cell);
}
}
ElKind::Tr => {
if let Some(t) = self.tables.last_mut()
&& let Some(r) = t.row.take()
{
t.rows.push(r);
}
}
ElKind::Table => {
if let Some(mut t) = self.tables.pop() {
if let Some(r) = t.row.take() {
t.rows.push(r);
}
let borderless = !t.bordered;
// Keep at most MAX_ROWS rows (the model drops the rest anyway): bounds the
// O(rows × rowspan) work of `insert_covered` on a table of millions of ``.
let rows: Vec<_> = t.rows.into_iter().filter(|r| !r.is_empty()).take(wordcraft_doc::table::MAX_ROWS).collect();
let mut ft = FTable { rows, widths: Vec::new(), borderless };
if !ft.rows.is_empty() {
ft.insert_covered();
self.container().push(FBlock::Table(ft));
}
}
}
ElKind::Head => {}
_ => {}
}
}
/// Index of the innermost open element named `name`, not looking past a table boundary
/// unless `cross_tables`.
fn find_open(&self, pred: impl Fn(&El) -> bool, stop: impl Fn(&El) -> bool) -> Option {
for (i, e) in self.stack.iter().enumerate().rev() {
if pred(e) {
return Some(i);
}
if stop(e) {
return None;
}
}
None
}
fn close_head_for_start(&mut self, name: &str) {
// A body-content start tag implicitly ends an omitted .
if !matches!(
name,
"html" | "head" | "base" | "basefont" | "bgsound" | "link" | "meta" | "noframes" | "noscript" | "script" | "style" | "template" | "title"
) && let Some(k) = self.find_open(|e| e.kind == ElKind::Head, |_| false)
{
self.pop_to(k);
}
}
fn start(&mut self, name: &str, attrs: &[(String, String)], self_close: bool) {
// Head metadata.
match name {
"title" => {
self.title = Some(String::new());
}
"meta" => {
let n = attr(attrs, "name").unwrap_or("").to_ascii_lowercase();
let c = attr(attrs, "content").unwrap_or("").to_string();
match n.as_str() {
"author" => self.meta.author = c,
"description" => self.meta.description = c,
"keywords" => self.meta.keywords = c,
"subject" => self.meta.subject = c,
_ => {}
}
return;
}
_ => {}
}
let is_table_part = matches!(name, "tr" | "td" | "th");
let in_table = self.tables.len() < model::MAX_DEPTH;
// Implicit closes.
if closes_p(name)
&& let Some(k) = self.find_open(|e| e.name == "p", |e| matches!(e.kind, ElKind::Cell | ElKind::Table | ElKind::Li | ElKind::Quote))
{
self.pop_to(k);
}
if matches!(name, "h1" | "h2" | "h3" | "h4" | "h5" | "h6") && self.stack.last().is_some_and(|e| matches!(e.kind, ElKind::Heading(_))) {
let k = self.stack.len() - 1;
self.pop_to(k);
}
if name == "li"
&& let Some(k) = self.find_open(|e| e.kind == ElKind::Li, |e| matches!(e.kind, ElKind::List(_) | ElKind::Table | ElKind::Cell))
{
self.pop_to(k);
}
if matches!(name, "dt" | "dd")
&& let Some(k) =
self.find_open(|e| e.name == "dt" || e.name == "dd", |e| matches!(e.kind, ElKind::Table | ElKind::Cell) || e.name == "dl")
{
self.pop_to(k);
}
if is_table_part && !self.tables.is_empty() {
if matches!(name, "td" | "th")
&& let Some(k) = self.find_open(|e| e.kind == ElKind::Cell, |e| matches!(e.kind, ElKind::Table | ElKind::Tr))
{
self.pop_to(k);
}
if name == "tr"
&& let Some(k) = self.find_open(|e| matches!(e.kind, ElKind::Tr | ElKind::Cell), |e| e.kind == ElKind::Table)
{
// Pop to the outermost of cell/row inside the current table.
let mut kk = k;
while kk > 0 && self.stack.get(kk - 1).is_some_and(|e| matches!(e.kind, ElKind::Tr | ElKind::Cell)) {
kk -= 1;
}
self.pop_to(kk);
}
}
if name == "a"
&& !self.in_head()
&& let Some(id) = attr(attrs, "id").or_else(|| attr(attrs, "name")).filter(|s| !s.is_empty())
{
self.start_para().inlines.push(Inline::Anchor(id.to_string()));
}
// Element-specific work.
match name {
"br" => {
if self.stack.iter().any(|e| e.kind == ElKind::Pre) {
self.start_para();
self.flush();
} else {
let f = self.fmt();
self.start_para().push_text("\n", &f);
}
return;
}
"hr" => {
self.flush();
self.container().push(FBlock::Para(Para::new(Kind::Rule)));
return;
}
"wbr" => {
let f = self.fmt();
self.start_para().push_text("\u{200B}", &f);
return;
}
"img" => {
let alt = attr(attrs, "alt").unwrap_or("").to_string();
let dim = |k: &str| attr(attrs, k).and_then(|v| v.trim().trim_end_matches("px").trim().parse::().ok()).map(|p| p * 0.75);
let mut w = dim("width");
let mut h = dim("height");
if let Some(st) = attr(attrs, "style") {
for decl in st.split(';') {
if let Some((k, v)) = decl.split_once(':') {
match k.trim().to_ascii_lowercase().as_str() {
"width" => w = css_len_pt(v).or(w),
"height" => h = css_len_pt(v).or(h),
_ => {}
}
}
}
}
let img = attr(attrs, "src")
.and_then(|src| if src.trim_start().starts_with("data:") { data_uri(src).map(Arc::new) } else { (self.images)(src.trim()) })
.and_then(|d| make_img(d, w, h, &alt));
let f = self.fmt();
match img {
Some(im) => self.start_para().inlines.push(Inline::Image(im)),
None if !alt.is_empty() => self.start_para().push_text(&alt, &f),
None => {}
}
return;
}
_ => {}
}
if is_void(name) || self_close && block_kind(name).is_none() && !matches!(name, "a" | "span") {
return;
}
if self.stack.len() >= MAX_STACK {
return;
}
let mut kind = block_kind(name).unwrap_or(ElKind::Inline);
if matches!(kind, ElKind::Table | ElKind::Tr | ElKind::Cell) && !in_table {
kind = ElKind::Block;
}
if matches!(kind, ElKind::Tr | ElKind::Cell) && self.tables.is_empty() {
kind = ElKind::Block;
}
if kind != ElKind::Inline {
self.flush();
}
let mut fmt = self.fmt();
let mut align = None;
let mut preserve = self.preserve();
let mut page_break = false;
let mut page_break_after = false;
match name {
// A header cell is bold, as in browsers and Word, unless its own CSS says otherwise.
"b" | "strong" | "th" => fmt.bold = true,
"i" | "em" | "cite" | "var" | "dfn" | "address" => fmt.italic = true,
"u" | "ins" => fmt.underline = true,
"s" | "strike" | "del" => fmt.strike = true,
"code" | "kbd" | "tt" | "samp" => fmt.code = true,
"sup" => fmt.sup = true,
"sub" => fmt.sub = true,
"small" => fmt.size = Some(fmt.size.map_or(10.0, |s| (s * 0.833).max(1.0))),
"big" => fmt.size = Some(fmt.size.map_or(14.0, |s| (s * 1.2).min(1638.0))),
"mark" => fmt.background = Some(wordcraft_doc::Rgb(255, 255, 0)),
"font" => {
if let Some(c) = attr(attrs, "color").and_then(parse_color) {
fmt.color = Some(c);
}
if let Some(face) = attr(attrs, "face") {
let fam = face.split(',').next().unwrap_or("").trim().to_string();
if model::is_mono(&fam) {
fmt.code = true;
} else if !fam.is_empty() {
fmt.font = Some(fam);
}
}
if let Some(sz) = attr(attrs, "size") {
let s = sz.trim();
let val = if let Some(rest) = s.strip_prefix('+') {
rest.parse::().ok().map(|d| 3 + d)
} else if let Some(rest) = s.strip_prefix('-') {
rest.parse::().ok().map(|d| 3 - d)
} else {
s.parse::().ok()
};
if let Some(sz) = val {
fmt.size = Some([7.5, 10.0, 12.0, 13.5, 18.0, 24.0, 36.0].get((sz.clamp(1, 7) - 1) as usize).copied().unwrap_or(12.0));
}
}
}
"a" => {
if let Some(h) = attr(attrs, "href").filter(|h| wordcraft_doc::link::is_safe_href(h)) {
fmt.link = Some(h.trim().to_string());
}
}
_ => {}
}
// `align` on a table is about where the table sits (not imported); it doesn't align the
// text in its cells.
if let Some(a) = attr(attrs, "align").filter(|_| kind != ElKind::Table) {
align = match a.to_ascii_lowercase().as_str() {
"center" | "middle" => Some(Align::Center),
"right" => Some(Align::Right),
"justify" => Some(Align::Justify),
"left" => Some(Align::Left),
_ => None,
};
}
if name == "center" {
align = Some(Align::Center);
}
if let Some(st) = attr(attrs, "style") {
apply_style(st, &mut fmt, &mut align, &mut preserve, &mut page_break, &mut page_break_after);
}
if kind == ElKind::Pre {
preserve = true;
}
match kind {
ElKind::Table => self.tables.push(TableB { rows: Vec::new(), row: None, bordered: declares_border(attrs, true) }),
ElKind::Tr => {
if let Some(t) = self.tables.last_mut()
&& let Some(r) = t.row.take()
{
t.rows.push(r);
}
}
ElKind::Cell => {
let tb = self.find_open(|e| e.kind == ElKind::Tr, |e| e.kind == ElKind::Table).is_none();
if let Some(t) = self.tables.last_mut()
&& declares_border(attrs, false)
{
t.bordered = true;
}
if tb && let Some(t) = self.tables.last_mut() {
// A cell without a row: start one.
if let Some(r) = t.row.take() {
t.rows.push(r);
}
t.row = Some(Vec::new());
}
let num = |k: &str| attr(attrs, k).and_then(|v| v.trim().parse::().ok()).map(|v| v.clamp(1, 63));
let shading = attr(attrs, "bgcolor").and_then(parse_color).or(fmt.background);
let mut valign = attr(attrs, "valign").and_then(|v| match v.trim().to_ascii_lowercase().as_str() {
"top" => Some(VAlign::Top),
"middle" | "center" => Some(VAlign::Center),
"bottom" => Some(VAlign::Bottom),
_ => None,
});
if let Some(st) = attr(attrs, "style") {
for decl in st.split(';') {
if let Some((k, v)) = decl.split_once(':')
&& k.trim().eq_ignore_ascii_case("vertical-align")
{
match v.trim().to_ascii_lowercase().as_str() {
"top" => valign = Some(VAlign::Top),
"middle" | "center" => valign = Some(VAlign::Center),
"bottom" => valign = Some(VAlign::Bottom),
_ => {}
}
}
}
}
self.cells.push(Cell {
blocks: Vec::new(),
colspan: num("colspan").unwrap_or(1),
rowspan: attr(attrs, "rowspan").and_then(|v| v.trim().parse::().ok()).map(|v| v.clamp(1, 1000)).unwrap_or(1),
covered: false,
header: name == "th",
shading,
borders: None,
valign,
});
fmt.background = None;
self.containers.push(Vec::new());
}
_ => {}
}
self.stack.push(El { name: name.to_string(), kind, fmt, align, preserve, page_break, page_break_after, used: false });
}
fn end(&mut self, name: &str) {
if name == "title" {
if let Some(t) = self.title.take() {
self.meta.title = t.split_whitespace().collect::>().join(" ");
}
return;
}
let crosses = matches!(name, "table");
let target = if matches!(name, "td" | "th") {
self.find_open(|e| e.kind == ElKind::Cell, |e| e.kind == ElKind::Table)
} else if name == "tr" {
self.find_open(|e| e.kind == ElKind::Tr, |e| e.kind == ElKind::Table)
} else {
self.find_open(|e| e.name == name, |e| !crosses && matches!(e.kind, ElKind::Table | ElKind::Cell))
};
if let Some(k) = target {
self.pop_to(k);
}
}
fn finish(mut self) -> Flow {
self.pop_to(0);
self.flush();
// Unclosed cells: fold remaining containers into the body.
while self.containers.len() > 1 {
let c = self.containers.pop().unwrap_or_default();
self.container().extend(c);
}
let blocks = self.containers.pop().unwrap_or_default();
if let Some(t) = self.title.take() {
self.meta.title = t;
}
Flow { blocks, meta: self.meta }
}
}
/// Parse HTML text into the flow model.
pub fn parse(s: &str) -> Flow {
parse_with(s, &|_| None)
}
/// [`parse`], loading pictures that aren't `data:` URIs through `images`.
pub fn parse_with(s: &str, images: ImageLoader) -> Flow {
let mut lx = Lexer { s, pos: 0 };
let mut b = Builder::new(images);
let mut guard = 0usize;
while let Some(t) = lx.next() {
guard += 1;
if guard > 50_000_000 {
break;
}
match t {
Tok::Text(t) => b.text(&t),
Tok::Start { name, attrs, self_close } => {
b.close_head_for_start(&name);
if crate::mathml::local(&name) == "math" && !self_close {
let el = read_math(&mut lx, name, attrs);
if !b.in_head()
&& let Some((linear, display)) = crate::mathml::read(&el)
{
b.start_para().inlines.push(Inline::Equation { linear, display });
}
continue;
}
if name == "ruby" && !self_close {
let r = read_ruby(&mut lx);
if !b.in_head() {
b.push_ruby(&r.base, &r.ruby);
}
continue;
}
if matches!(name.as_str(), "script" | "style" | "noscript" | "template" | "textarea") && !self_close {
lx.skip_past_end_tag(&name);
continue;
}
b.start(&name, &attrs, self_close);
}
Tok::End(name) => b.end(&name),
}
}
b.finish()
}
/// A `` that closes the element
/// being read; nested `