use indexmap::IndexMap;
use std::sync::Arc;
use html5ever::driver::ParseOpts;
use html5ever::tendril::TendrilSink;
use html5ever::{LocalName, QualName, namespace_url, ns};
use log::{debug, info, warn};
use markup5ever_rcdom::{Handle, NodeData, RcDom};
use crate::resources;
use crate::source::SharedSourceText;
use crate::tdom::node::{Node, NodeArena, NodeId, NodeType, SharedNodeArena, document_base_href};
#[derive(Debug, Clone)]
pub struct ParsedDocument {
pub root_id: NodeId,
pub arena: SharedNodeArena,
pub css: SharedSourceText,
pub scripts: Arc<[String]>,
}
#[derive(Debug)]
pub struct Parser {
input: String,
}
impl Parser {
pub fn new(input: &str) -> Self {
Self {
input: input.to_string(),
}
}
pub fn parse(&mut self) -> (NodeId, NodeArena, String) {
let doc = self.parse_with_scripts();
let arena = (*doc.arena).clone();
(doc.root_id, arena, doc.css.to_string())
}
pub fn parse_with_scripts(&mut self) -> ParsedDocument {
let opts = ParseOpts::default();
let dom = html5ever::parse_document(RcDom::default(), opts).one(self.input.clone());
let mut arena = NodeArena::default();
let root_id = arena.insert(Node::from(NodeType::Document));
let mut css_sources: Vec<String> = Vec::new();
let mut scripts: Vec<String> = Vec::new();
convert_children(
&dom.document,
root_id,
&mut arena,
&mut css_sources,
&mut scripts,
);
debug!(
"parse done: root={}, {} B, CSS: {} B, JS: {} B",
root_id,
arena.len(),
css_sources.join("\n").len(),
scripts.join("\n").len()
);
let arena = Arc::new(arena);
let css = css_sources.join("\n");
ParsedDocument {
root_id,
arena,
css: Arc::from(css),
scripts: scripts.into(),
}
}
}
fn convert_attributes(html5_attrs: &[html5ever::Attribute]) -> IndexMap<String, String> {
html5_attrs
.iter()
.map(|a| (a.name.local.to_string(), a.value.to_string()))
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ConvertMode {
Document,
Template,
InnerHtml,
}
fn convert_children(
handle: &Handle,
parent_id: NodeId,
arena: &mut NodeArena,
css_sources: &mut Vec<String>,
scripts: &mut Vec<String>,
) {
convert_children_with_mode(
handle,
parent_id,
arena,
css_sources,
scripts,
ConvertMode::Document,
);
}
fn convert_children_with_mode(
handle: &Handle,
parent_id: NodeId,
arena: &mut NodeArena,
css_sources: &mut Vec<String>,
scripts: &mut Vec<String>,
mode: ConvertMode,
) {
let children = handle.children.borrow().clone();
for child_handle in &children {
convert_one_child(child_handle, parent_id, arena, css_sources, scripts, mode);
}
}
fn convert_one_child(
child_handle: &Handle,
parent_id: NodeId,
arena: &mut NodeArena,
css_sources: &mut Vec<String>,
scripts: &mut Vec<String>,
mode: ConvertMode,
) {
match &child_handle.data {
NodeData::Document => {
convert_children_with_mode(child_handle, parent_id, arena, css_sources, scripts, mode);
}
NodeData::Element {
name,
attrs,
template_contents,
..
} => {
let tag = name.local.as_ref();
if tag.eq_ignore_ascii_case("script") {
match mode {
ConvertMode::Document => {
let script_attrs = {
let ar = attrs.borrow();
convert_attributes(&ar)
};
if let Some(src) = script_attrs.get("src") {
let src = resources::resolve_url(
document_base_href(arena, parent_id).as_deref(),
src,
);
info!("外部脚本: {}", src);
match load_text(&src) {
Ok(js) if !js.trim().is_empty() => scripts.push(js),
Ok(_) => warn!("外部脚本 {} 为空", src),
Err(e) => warn!("读取 {} 失败: {}", src, e),
}
} else {
let js = extract_text_content(child_handle);
if !js.trim().is_empty() {
info!("提取 <script> JS: {} 字节", js.len());
scripts.push(js);
}
}
}
ConvertMode::Template => {
convert_children_with_mode(
child_handle,
parent_id,
arena,
css_sources,
scripts,
ConvertMode::Template,
);
}
ConvertMode::InnerHtml => {}
}
return;
}
match tag_name_to_node_type(tag) {
Some(node_type) => {
if node_type == NodeType::Style
&& matches!(mode, ConvertMode::Document | ConvertMode::InnerHtml)
{
let css = extract_text_content(child_handle);
if !css.trim().is_empty() {
info!("提取 <style> CSS: {} 字节", css.len());
css_sources.push(css);
}
return;
}
if node_type == NodeType::Link {
let link_attrs = {
let ar = attrs.borrow();
convert_attributes(&ar)
};
let loads_css =
matches!(mode, ConvertMode::Document | ConvertMode::InnerHtml);
if loads_css
&& let (Some(rel), Some(href)) =
(link_attrs.get("rel"), link_attrs.get("href"))
&& rel.to_lowercase().contains("stylesheet")
{
let href = resources::resolve_url(
document_base_href(arena, parent_id).as_deref(),
href,
);
info!("外部样式表: {}", href);
match load_text(&href) {
Ok(c) => {
css_sources.push(c);
}
Err(e) => warn!("读取 {} 失败: {}", href, e),
}
}
}
let child_id = arena.insert(Node::from(node_type));
{
let ar = attrs.borrow();
arena[child_id].attributes = convert_attributes(&ar);
}
Node::append_child(arena, parent_id, child_id);
if node_type == NodeType::Template {
if let Some(contents) = template_contents.borrow().as_ref() {
convert_children_with_mode(
contents,
child_id,
arena,
css_sources,
scripts,
ConvertMode::Template,
);
}
} else {
convert_children_with_mode(
child_handle,
child_id,
arena,
css_sources,
scripts,
mode,
);
}
}
None => {
warn!("不支持标签 <{}>,压平", tag);
convert_children_with_mode(
child_handle,
parent_id,
arena,
css_sources,
scripts,
mode,
);
}
}
}
NodeData::Text { contents } => {
let text = contents.borrow().to_string();
let text_id = arena.insert(Node::text(&text));
Node::append_child(arena, parent_id, text_id);
}
NodeData::Comment { contents } => {
let comment_id = arena.insert(Node::comment(contents));
Node::append_child(arena, parent_id, comment_id);
}
NodeData::Doctype { name, .. } => {
let doctype_id = arena.insert(Node::doctype(name));
Node::append_child(arena, parent_id, doctype_id);
}
NodeData::ProcessingInstruction { .. } => {}
}
}
pub fn parse_fragment_into(
arena: &mut NodeArena,
parent_id: NodeId,
html: &str,
css_sources: &mut Vec<String>,
) {
let context_tag = arena
.get(parent_id)
.and_then(|n| n.node_type.tag_name())
.unwrap_or("div");
let ctx = QualName::new(None, ns!(html), LocalName::from(context_tag));
let dom = html5ever::parse_fragment(RcDom::default(), ParseOpts::default(), ctx, Vec::new())
.one(html);
let old_children = arena
.get_mut(parent_id)
.map(|n| std::mem::take(&mut n.children))
.unwrap_or_default();
for child in old_children {
if let Some(c) = arena.get_mut(child) {
c.parent = None;
}
}
let document_children = dom.document.children.borrow().clone();
let mut scripts = Vec::new();
for child in &document_children {
if let NodeData::Element { name, .. } = &child.data
&& name.local.as_ref().eq_ignore_ascii_case("html")
{
convert_children_with_mode(
child,
parent_id,
arena,
css_sources,
&mut scripts,
ConvertMode::InnerHtml,
);
return;
}
}
for child in &document_children {
convert_one_child(
child,
parent_id,
arena,
css_sources,
&mut scripts,
ConvertMode::InnerHtml,
);
}
}
fn extract_text_content(handle: &Handle) -> String {
let children = handle.children.borrow();
let mut text = String::new();
for child in children.iter() {
if let NodeData::Text { contents } = &child.data {
text.push_str(&contents.borrow());
}
}
text
}
fn load_text(src: &str) -> Result<String, String> {
let bytes = resources::load(src).map_err(|e| e.to_string())?;
String::from_utf8(bytes.as_ref().to_vec()).map_err(|e| e.to_string())
}
#[doc(hidden)]
pub fn tag_name_to_node_type(tag: &str) -> Option<NodeType> {
NodeType::from_tag_name(tag)
}
pub fn serialize_inner_html(arena: &NodeArena, node_id: NodeId) -> String {
let Some(node) = arena.get(node_id) else {
return String::new();
};
let mut out = String::new();
for &child in &node.children {
serialize_node(arena, child, &mut out);
}
out
}
pub fn serialize_outer_html(arena: &NodeArena, node_id: NodeId) -> String {
let mut out = String::new();
serialize_node(arena, node_id, &mut out);
out
}
fn serialize_node(arena: &NodeArena, node_id: NodeId, out: &mut String) {
let node = &arena[node_id];
match node.node_type {
NodeType::Text => {
if let Some(data) = &node.data {
escape_text(data, out);
}
}
NodeType::Comment => {
out.push_str("<!--");
if let Some(data) = &node.data {
out.push_str(data);
}
out.push_str("-->");
}
NodeType::DocumentFragment => {
for &child in &node.children {
serialize_node(arena, child, out);
}
}
_ => {
let Some(tag) = node.node_type.tag_name() else {
return;
};
out.push('<');
out.push_str(tag);
for (k, v) in &node.attributes {
out.push(' ');
out.push_str(k);
out.push_str("=\"");
escape_attr(v, out);
out.push('"');
}
out.push('>');
if node.node_type.is_self_closing() {
return;
}
for &child in &node.children {
serialize_node(arena, child, out);
}
out.push_str("</");
out.push_str(tag);
out.push('>');
}
}
}
fn escape_text(s: &str, out: &mut String) {
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'\u{00a0}' => out.push_str(" "),
c => out.push(c),
}
}
}
fn escape_attr(s: &str, out: &mut String) {
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'"' => out.push_str("""),
'\u{00a0}' => out.push_str(" "),
c => out.push(c),
}
}
}