use super::tree::{Kind, Node};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum Class {
Pass,
Content,
Inline,
Inert,
Minor,
}
impl Class {
pub fn label(self) -> &'static str {
match self {
Class::Pass => "通过",
Class::Content => "影响正文",
Class::Inline => "只差行内结构",
Class::Inert => "只差不产出正文的元素内部",
Class::Minor => "其他(不影响正文)",
}
}
}
pub fn classify(expected: &[Node], actual: &[Node]) -> Class {
if expected == actual {
return Class::Pass;
}
if projection(expected) != projection(actual) {
return Class::Content;
}
if shape(expected, true) != shape(actual, true) {
return Class::Inline;
}
if shape(expected, false) != shape(actual, false) {
return Class::Inert;
}
Class::Minor
}
fn block(name: &str) -> bool {
matches!(
name,
"html"
| "head"
| "body"
| "address"
| "article"
| "aside"
| "blockquote"
| "caption"
| "center"
| "dd"
| "details"
| "dialog"
| "dir"
| "div"
| "dl"
| "dt"
| "fieldset"
| "figcaption"
| "figure"
| "footer"
| "form"
| "frameset"
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
| "header"
| "hgroup"
| "hr"
| "li"
| "listing"
| "main"
| "menu"
| "nav"
| "ol"
| "optgroup"
| "option"
| "p"
| "plaintext"
| "pre"
| "search"
| "section"
| "select"
| "summary"
| "table"
| "tbody"
| "td"
| "textarea"
| "tfoot"
| "th"
| "thead"
| "tr"
| "ul"
| "xmp"
| "title"
| "script"
| "style"
| "noscript"
| "noframes"
| "iframe"
| "noembed"
| "template"
| "object"
| "button"
| "colgroup"
)
}
fn skipped(ns: &str, name: &str) -> bool {
match ns {
"" => matches!(
name,
"script"
| "style"
| "noscript"
| "template"
| "iframe"
| "canvas"
| "object"
| "embed"
| "select"
| "option"
| "input"
| "textarea"
| "button"
| "dialog"
),
"svg" => name == "svg",
_ => false,
}
}
pub fn projection(nodes: &[Node]) -> Vec<String> {
let mut out = Vec::new();
let mut text = String::new();
for n in nodes {
project(n, &mut out, &mut text);
}
flush(&mut out, &mut text);
out
}
fn project(n: &Node, out: &mut Vec<String>, text: &mut String) {
match &n.kind {
Kind::Text(t) => text.push_str(t),
Kind::Element { ns, name, .. } if skipped(ns, name) => {
flush(out, text);
out.push(format!("<{name}/>"));
}
Kind::Element { ns, name, .. } if ns.is_empty() && block(name) => {
flush(out, text);
out.push(format!("<{name}>"));
for c in &n.children {
project(c, out, text);
}
flush(out, text);
out.push(format!("</{name}>"));
}
Kind::Element { ns, name, .. } => {
if ns.is_empty() && name == "br" {
text.push(' ');
}
for c in &n.children {
project(c, out, text);
}
}
Kind::Comment(_) | Kind::Doctype => {}
}
}
fn flush(out: &mut Vec<String>, text: &mut String) {
let collapsed = text.split_whitespace().collect::<Vec<_>>().join(" ");
if !collapsed.is_empty() {
out.push(collapsed);
}
text.clear();
}
fn shape(nodes: &[Node], skip_inert: bool) -> Vec<String> {
let mut out = Vec::new();
let mut text = String::new();
for n in nodes {
shape_node(n, skip_inert, &mut out, &mut text);
}
flush(&mut out, &mut text);
out
}
fn shape_node(n: &Node, skip_inert: bool, out: &mut Vec<String>, text: &mut String) {
match &n.kind {
Kind::Text(t) => text.push_str(t),
Kind::Element { ns, name, .. } if skip_inert && skipped(ns, name) => {
flush(out, text);
out.push(format!("<{ns}:{name}/>"));
}
Kind::Element { ns, name, .. } => {
flush(out, text);
out.push(format!("<{ns}:{}>", name.to_ascii_lowercase()));
for c in &n.children {
shape_node(c, skip_inert, out, text);
}
flush(out, text);
out.push("</>".to_string());
}
Kind::Comment(_) | Kind::Doctype => {}
}
}