mod classify;
mod dat;
mod tree;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use classify::Class;
use pagesift_html::{Html, Namespace, ParseOptions};
fn data_dir() -> Option<PathBuf> {
let dir = match std::env::var_os("PAGESIFT_HTML5LIB_DIR") {
Some(d) => PathBuf::from(d),
None => PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../benchmarks/data/wpt-parsing/html/syntax/parsing/resources"),
};
dir.is_dir().then_some(dir)
}
fn known() -> BTreeMap<String, String> {
let text = include_str!("../html5lib_known.txt");
text.lines()
.filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
.filter_map(|l| {
let (id, why) = l.split_once('\t')?;
Some((id.trim().to_string(), why.trim().to_string()))
})
.collect()
}
#[test]
fn html5lib_tree_construction() {
let Some(dir) = data_dir() else {
eprintln!("html5lib-tests 数据不在,跳过(见文件开头的说明)");
return;
};
let mut files: Vec<PathBuf> = std::fs::read_dir(&dir)
.unwrap()
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|x| x == "dat"))
.collect();
files.sort();
let known = known();
let mut counts: BTreeMap<Class, usize> = BTreeMap::new();
let mut fragment = (0, 0);
let mut script_off = (0, 0);
let mut panics = Vec::new();
let mut slow = Vec::new();
let mut unexplained = Vec::new();
let mut seen_content = BTreeSet::new();
let mut report = String::new();
let mut per_file: BTreeMap<String, (usize, usize)> = BTreeMap::new();
for path in &files {
let fname = path.file_name().unwrap().to_string_lossy().to_string();
let text = std::fs::read_to_string(path).unwrap();
for case in dat::parse(&text) {
let id = format!("{fname}#{}", case.index);
let expected = tree::parse_expected(&case.document);
let context = case.fragment.as_deref().map(|c| match c.split_once(' ') {
Some(("svg", n)) => (Namespace::Svg, n),
Some(("math", n)) => (Namespace::MathMl, n),
_ => (Namespace::Html, c),
});
let opts = ParseOptions {
scripting: case.scripting != Some(false),
context,
};
let start = Instant::now();
let actual = std::panic::catch_unwind(|| {
let doc = Html::parse_with(&case.data, &opts);
if context.is_some() {
tree::from_fragment(&doc)
} else {
tree::from_html(&doc)
}
});
let elapsed = start.elapsed();
if elapsed > Duration::from_millis(500) {
slow.push(format!("{id} {elapsed:?}"));
}
let Ok(actual) = actual else {
panics.push(id);
continue;
};
let class = classify::classify(&expected, &actual);
*counts.entry(class).or_default() += 1;
for (on, c) in [
(context.is_some(), &mut fragment),
(!opts.scripting, &mut script_off),
] {
if on {
c.1 += 1;
c.0 += usize::from(class == Class::Pass);
}
}
let e = per_file.entry(fname.clone()).or_default();
e.1 += 1;
if class == Class::Pass {
e.0 += 1;
continue;
}
if class == Class::Content {
seen_content.insert(id.clone());
if !known.contains_key(&id) {
unexplained.push(id.clone());
}
}
let _ = write!(
report,
"## {id} [{}]{}{}{}\n### data\n{}\n### expected\n{}### actual\n{}\n",
class.label(),
case.fragment
.as_deref()
.map(|c| format!(" 片段:{c}"))
.unwrap_or_default(),
if opts.scripting { "" } else { " script-off" },
known
.get(&id)
.map(|w| format!(" 已知:{w}"))
.unwrap_or_default(),
case.data,
tree::dump(&expected),
tree::dump(&actual),
);
}
}
let total: usize = counts.values().sum();
let pass = counts.get(&Class::Pass).copied().unwrap_or(0);
let mut summary = format!(
"html5lib 树构建:跑了 {total} 条,通过 {pass}({:.1}%;其中片段 {}/{}、script-off {}/{})",
100.0 * pass as f64 / total.max(1) as f64,
fragment.0,
fragment.1,
script_off.0,
script_off.1,
);
for (c, n) in &counts {
if *c != Class::Pass {
let _ = write!(summary, ",{} {n}", c.label());
}
}
let _ = write!(
summary,
",崩溃 {},超过 500 ms {}",
panics.len(),
slow.len()
);
println!("{summary}");
for (f, (p, t)) in &per_file {
println!(" {f}: {p}/{t}");
}
if let Some(out) = std::env::var_os("PAGESIFT_HTML5LIB_REPORT") {
std::fs::write(out, format!("# {summary}\n\n{report}")).unwrap();
}
let stale: Vec<&String> = known
.keys()
.filter(|k| !seen_content.contains(*k))
.collect();
if !stale.is_empty() {
println!("已知清单里这些用例已经不再影响正文,可以删掉:{stale:?}");
}
assert!(panics.is_empty(), "解析时崩溃:{panics:?}");
assert!(slow.is_empty(), "解析太慢:{slow:?}");
assert!(
unexplained.is_empty(),
"{} 条影响正文的差异没有写进 tests/html5lib_known.txt,前 20 条:{:?}",
unexplained.len(),
&unexplained[..unexplained.len().min(20)]
);
}