use core::ffi::c_void;
use core::ptr;
use core::slice;
use std::os::raw::{c_char, c_int};
use crate::abi::allocator::{xmlFree, xmlMalloc, xmlMallocZero, xmlRealloc};
use crate::abi::constants::XML_DEFAULT_VERSION;
use crate::abi::structs::*;
use crate::abi::types::xmlCharEncoding::XML_CHAR_ENCODING_UTF8;
use crate::abi::types::xmlDocProperties::XML_DOC_WELLFORMED;
use crate::abi::types::xmlElementType::*;
use crate::abi::types::*;
use crate::xml::io;
use crate::xml::string::*;
use crate::xml::tree;
const HTML_INLINE: u32 = 0x1;
const HTML_BLOCK: u32 = 0x2;
const HTML_EMPTY: u32 = 0x4;
#[allow(dead_code)]
const HTML_DEPRECATED: u32 = 0x8;
const HTML_OL: u32 = 0x10;
const HTML_DL: u32 = 0x20;
#[allow(dead_code)]
const HTML_COMPACT: u32 = 0x40;
const HTML_HEAD: u32 = 0x80;
const HTML_BODY: u32 = 0x100;
#[allow(dead_code)]
const HTML_HEADSTRUCK: u32 = 0x200;
const HTML_VALID: u32 = 0x400;
const HTML_NO_END: u32 = 0x800; const HTML_IMPLIED: u32 = 0x1000;
#[derive(Clone, Copy)]
struct HtmlElementInfo {
name: &'static str,
flags: u32,
}
const HTML_ELEMENTS: &[HtmlElementInfo] = &[
HtmlElementInfo {
name: "br",
flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "hr",
flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "img",
flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "input",
flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "meta",
flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "link",
flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "base",
flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "area",
flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "col",
flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "embed",
flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "param",
flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "source",
flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "track",
flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "wbr",
flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "html",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "head",
flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "body",
flags: HTML_BODY | HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "div",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "p",
flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
},
HtmlElementInfo {
name: "h1",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "h2",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "h3",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "h4",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "h5",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "h6",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "ul",
flags: HTML_BLOCK | HTML_VALID | HTML_OL,
},
HtmlElementInfo {
name: "ol",
flags: HTML_BLOCK | HTML_VALID | HTML_OL,
},
HtmlElementInfo {
name: "li",
flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
},
HtmlElementInfo {
name: "dl",
flags: HTML_BLOCK | HTML_VALID | HTML_DL,
},
HtmlElementInfo {
name: "dt",
flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
},
HtmlElementInfo {
name: "dd",
flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
},
HtmlElementInfo {
name: "table",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "tr",
flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
},
HtmlElementInfo {
name: "td",
flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
},
HtmlElementInfo {
name: "th",
flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
},
HtmlElementInfo {
name: "thead",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "tbody",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "tfoot",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "colgroup",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "caption",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "form",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "fieldset",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "legend",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "pre",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "blockquote",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "address",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "center",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "dir",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "menu",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "noscript",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "frameset",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "frame",
flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "iframe",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "noframes",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "a",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "abbr",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "acronym",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "b",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "basefont",
flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
},
HtmlElementInfo {
name: "bdo",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "big",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "cite",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "code",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "dfn",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "em",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "font",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "i",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "kbd",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "label",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "map",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "nobr",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "object",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "q",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "rb",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "rbc",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "rp",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "rt",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "rtc",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "ruby",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "s",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "samp",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "select",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "small",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "span",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "strike",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "strong",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "sub",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "sup",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "textarea",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "tt",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "u",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "var",
flags: HTML_INLINE | HTML_VALID,
},
HtmlElementInfo {
name: "header",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "footer",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "nav",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "article",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "section",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "aside",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "main",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "figure",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "figcaption",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "details",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "summary",
flags: HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "script",
flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "style",
flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
},
HtmlElementInfo {
name: "title",
flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
},
];
fn html_tag_lookup(name: &str) -> Option<&'static HtmlElementInfo> {
let lower: Vec<u8> = name.bytes().map(|b| b.to_ascii_lowercase()).collect();
let lower_str = match core::str::from_utf8(&lower) {
Ok(s) => s,
Err(_) => return None,
};
HTML_ELEMENTS.iter().find(|info| info.name == lower_str)
}
const HTML_ENTITIES: &[(&str, &str)] = &[
("nbsp", "\u{00a0}"),
("lt", "<"),
("gt", ">"),
("amp", "&"),
("quot", "\""),
("apos", "'"),
("copy", "\u{00a9}"),
("reg", "\u{00ae}"),
("amp", "&"),
("iexcl", "\u{00a1}"),
("cent", "\u{00a2}"),
("pound", "\u{00a3}"),
("curren", "\u{00a4}"),
("yen", "\u{00a5}"),
("brvbar", "\u{00a6}"),
("sect", "\u{00a7}"),
("uml", "\u{00a8}"),
("ordf", "\u{00aa}"),
("laquo", "\u{00ab}"),
("not", "\u{00ac}"),
("shy", "\u{00ad}"),
("macr", "\u{00ae}"),
("deg", "\u{00b0}"),
("plusmn", "\u{00b1}"),
("sup2", "\u{00b2}"),
("sup3", "\u{00b3}"),
("acute", "\u{00b4}"),
("micro", "\u{00b5}"),
("para", "\u{00b6}"),
("middot", "\u{00b7}"),
("cedil", "\u{00b8}"),
("sup1", "\u{00b9}"),
("ordm", "\u{00ba}"),
("raquo", "\u{00bb}"),
("frac14", "\u{00bc}"),
("frac12", "\u{00bd}"),
("frac34", "\u{00be}"),
("iquest", "\u{00bf}"),
("times", "\u{00d7}"),
("divide", "\u{00f7}"),
("ETH", "\u{00d0}"),
("eth", "\u{00f0}"),
("THORN", "\u{00de}"),
("thorn", "\u{00fe}"),
("AElig", "\u{00c6}"),
("aelig", "\u{00e6}"),
("OElig", "\u{0152}"),
("oelig", "\u{0153}"),
("Scaron", "\u{0160}"),
("scaron", "\u{0161}"),
("Yuml", "\u{0178}"),
("circ", "\u{02c6}"),
("tilde", "\u{02dc}"),
("ensp", "\u{2002}"),
("emsp", "\u{2003}"),
("thinsp", "\u{2009}"),
("zwnj", "\u{200c}"),
("zwj", "\u{200d}"),
("lrm", "\u{200e}"),
("rlm", "\u{200f}"),
("ndash", "\u{2013}"),
("mdash", "\u{2014}"),
("lsquo", "\u{2018}"),
("rsquo", "\u{2019}"),
("sbquo", "\u{201a}"),
("ldquo", "\u{201c}"),
("rdquo", "\u{201d}"),
("bdquo", "\u{201e}"),
("dagger", "\u{2020}"),
("Dagger", "\u{2021}"),
("bull", "\u{2022}"),
("hellip", "\u{2026}"),
("permil", "\u{2030}"),
("prime", "\u{2032}"),
("Prime", "\u{2033}"),
("lsaquo", "\u{2039}"),
("rsaquo", "\u{203a}"),
("oline", "\u{203e}"),
("euro", "\u{20ac}"),
("trade", "\u{2122}"),
("larr", "\u{2190}"),
("uarr", "\u{2191}"),
("rarr", "\u{2192}"),
("darr", "\u{2193}"),
("harr", "\u{2194}"),
("crarr", "\u{21b5}"),
("lceil", "\u{2308}"),
("rceil", "\u{2309}"),
("lfloor", "\u{230a}"),
("rfloor", "\u{230b}"),
("loz", "\u{25ca}"),
("spades", "\u{2660}"),
("clubs", "\u{2663}"),
("hearts", "\u{2665}"),
("diams", "\u{2666}"),
("Alpha", "\u{0391}"),
("Beta", "\u{0392}"),
("Gamma", "\u{0393}"),
("Delta", "\u{0394}"),
("Epsilon", "\u{0395}"),
("Zeta", "\u{0396}"),
("Eta", "\u{0397}"),
("Theta", "\u{0398}"),
("Iota", "\u{0399}"),
("Kappa", "\u{039a}"),
("Lambda", "\u{039b}"),
("Mu", "\u{039c}"),
("Nu", "\u{039d}"),
("Xi", "\u{039e}"),
("Omicron", "\u{039f}"),
("Pi", "\u{03a0}"),
("Rho", "\u{03a1}"),
("Sigma", "\u{03a3}"),
("Tau", "\u{03a4}"),
("Upsilon", "\u{03a5}"),
("Phi", "\u{03a6}"),
("Chi", "\u{03a7}"),
("Psi", "\u{03a8}"),
("Omega", "\u{03a9}"),
("alpha", "\u{03b1}"),
("beta", "\u{03b2}"),
("gamma", "\u{03b3}"),
("delta", "\u{03b4}"),
("epsilon", "\u{03b5}"),
("zeta", "\u{03b6}"),
("eta", "\u{03b7}"),
("theta", "\u{03b8}"),
("iota", "\u{03b9}"),
("kappa", "\u{03ba}"),
("lambda", "\u{03bb}"),
("mu", "\u{03bc}"),
("nu", "\u{03bd}"),
("xi", "\u{03be}"),
("omicron", "\u{03bf}"),
("pi", "\u{03c0}"),
("rho", "\u{03c1}"),
("sigmaf", "\u{03c2}"),
("sigma", "\u{03c3}"),
("tau", "\u{03c4}"),
("upsilon", "\u{03c5}"),
("phi", "\u{03c6}"),
("chi", "\u{03c7}"),
("psi", "\u{03c8}"),
("omega", "\u{03c9}"),
("thetasym", "\u{03d1}"),
("upsih", "\u{03d2}"),
("piv", "\u{03d6}"),
];
fn html_entity_lookup(name: &str) -> Option<&'static str> {
HTML_ENTITIES
.iter()
.find(|(n, _)| *n == name)
.map(|(_, v)| *v)
}
struct HtmlParserCtxt {
doc: *mut _xmlDoc,
current: *mut _xmlNode,
html: *mut _xmlNode,
head: *mut _xmlNode,
body: *mut _xmlNode,
in_head: bool,
in_body: bool,
html_created: bool,
head_created: bool,
body_created: bool,
seen_body_content: bool,
input: *mut u8,
input_pos: usize,
input_len: usize,
line: c_int,
err: bool,
filename: *mut c_char,
encoding: *mut c_char,
}
impl HtmlParserCtxt {
fn new() -> Self {
HtmlParserCtxt {
doc: ptr::null_mut(),
current: ptr::null_mut(),
html: ptr::null_mut(),
head: ptr::null_mut(),
body: ptr::null_mut(),
in_head: false,
in_body: false,
html_created: false,
head_created: false,
body_created: false,
seen_body_content: false,
input: ptr::null_mut(),
input_pos: 0,
input_len: 0,
line: 1,
err: false,
filename: ptr::null_mut(),
encoding: ptr::null_mut(),
}
}
fn peek(&self) -> Option<u8> {
if self.input_pos < self.input_len {
unsafe { Some(*self.input.add(self.input_pos)) }
} else {
None
}
}
fn peek_at(&self, offset: usize) -> Option<u8> {
let pos = self.input_pos + offset;
if pos < self.input_len {
unsafe { Some(*self.input.add(pos)) }
} else {
None
}
}
fn next(&mut self) -> Option<u8> {
if self.input_pos < self.input_len {
let ch = unsafe { *self.input.add(self.input_pos) };
self.input_pos += 1;
if ch == b'\n' {
self.line += 1;
}
Some(ch)
} else {
None
}
}
fn skip_while<F: Fn(u8) -> bool>(&mut self, f: F) {
while let Some(ch) = self.peek() {
if f(ch) {
self.next();
} else {
break;
}
}
}
fn skip_whitespace(&mut self) {
self.skip_while(|ch| ch == b' ' || ch == b'\t' || ch == b'\n' || ch == b'\r');
}
fn is_eof(&self) -> bool {
self.input_pos >= self.input_len
}
fn read_while<F: Fn(u8) -> bool>(&mut self, f: F) -> Vec<u8> {
let start = self.input_pos;
while let Some(ch) = self.peek() {
if f(ch) {
self.next();
} else {
break;
}
}
unsafe { slice::from_raw_parts(self.input.add(start), self.input_pos - start).to_vec() }
}
}
fn is_heading(name: &str) -> bool {
matches!(name, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
}
unsafe fn get_parent_element(node: *mut _xmlNode) -> *mut _xmlNode {
if node.is_null() {
return ptr::null_mut();
}
let mut n = node;
loop {
let parent = unsafe { (*n).parent };
if parent.is_null() {
return ptr::null_mut();
}
let ptype = unsafe { (*parent).type_ };
if ptype == XML_ELEMENT_NODE as c_int
|| ptype == XML_HTML_DOCUMENT_NODE as c_int
|| ptype == XML_DOCUMENT_NODE as c_int
{
return parent;
}
n = parent;
}
}
unsafe fn auto_close_element(ctxt: &mut HtmlParserCtxt, tag_name: &str) {
let tag_lower: Vec<u8> = tag_name.bytes().map(|b| b.to_ascii_lowercase()).collect();
let tag_lower_str = match core::str::from_utf8(&tag_lower) {
Ok(s) => s,
Err(_) => return,
};
let info = html_tag_lookup(&tag_lower_str);
let mut current = ctxt.current;
let mut open_names: Vec<Vec<u8>> = Vec::new();
let mut cur = current;
while !cur.is_null() {
let ctype = unsafe { (*cur).type_ };
if ctype == XML_ELEMENT_NODE as c_int {
if !unsafe { (*cur).name.is_null() } {
let name_bytes = unsafe { xmlstr_to_bytes((*cur).name) };
open_names.push(name_bytes.to_vec());
}
}
cur = unsafe { (*cur).parent };
}
if tag_lower_str == "p" || info.map_or(false, |i| i.flags & HTML_BLOCK != 0) {
let mut cur2 = current;
while !cur2.is_null() {
let ctype = unsafe { (*cur2).type_ };
if ctype == XML_ELEMENT_NODE as c_int {
if !unsafe { (*cur2).name.is_null() } {
let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
if name_str.eq_ignore_ascii_case("p") {
current = unsafe { (*cur2).parent };
break;
}
}
}
cur2 = unsafe { (*cur2).parent };
}
}
if is_heading(&tag_lower_str) {
let mut cur2 = current;
while !cur2.is_null() {
let ctype = unsafe { (*cur2).type_ };
if ctype == XML_ELEMENT_NODE as c_int {
if !unsafe { (*cur2).name.is_null() } {
let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
if is_heading(name_str) {
current = unsafe { (*cur2).parent };
break;
}
}
}
cur2 = unsafe { (*cur2).parent };
}
}
if tag_lower_str == "li" {
let mut cur2 = current;
while !cur2.is_null() {
let ctype = unsafe { (*cur2).type_ };
if ctype == XML_ELEMENT_NODE as c_int {
if !unsafe { (*cur2).name.is_null() } {
let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
if name_str.eq_ignore_ascii_case("li") {
current = unsafe { (*cur2).parent };
break;
}
}
}
cur2 = unsafe { (*cur2).parent };
}
}
if tag_lower_str == "dt" || tag_lower_str == "dd" {
let mut cur2 = current;
while !cur2.is_null() {
let ctype = unsafe { (*cur2).type_ };
if ctype == XML_ELEMENT_NODE as c_int {
if !unsafe { (*cur2).name.is_null() } {
let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
if name_str == "dt" || name_str == "dd" {
current = unsafe { (*cur2).parent };
break;
}
}
}
cur2 = unsafe { (*cur2).parent };
}
}
if tag_lower_str == "tr" || tag_lower_str == "td" || tag_lower_str == "th" {
let mut cur2 = current;
while !cur2.is_null() {
let ctype = unsafe { (*cur2).type_ };
if ctype == XML_ELEMENT_NODE as c_int {
if !unsafe { (*cur2).name.is_null() } {
let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
if name_str == "tr" || name_str == "td" || name_str == "th" {
current = unsafe { (*cur2).parent };
break;
}
}
}
cur2 = unsafe { (*cur2).parent };
}
}
if matches!(tag_lower_str, "thead" | "tbody" | "tfoot") {
let mut cur2 = current;
while !cur2.is_null() {
let ctype = unsafe { (*cur2).type_ };
if ctype == XML_ELEMENT_NODE as c_int {
if !unsafe { (*cur2).name.is_null() } {
let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
if name_str == "thead" || name_str == "tbody" || name_str == "tfoot" {
current = unsafe { (*cur2).parent };
break;
}
}
}
cur2 = unsafe { (*cur2).parent };
}
}
if tag_lower_str == "colgroup" {
let mut cur2 = current;
while !cur2.is_null() {
let ctype = unsafe { (*cur2).type_ };
if ctype == XML_ELEMENT_NODE as c_int {
if !unsafe { (*cur2).name.is_null() } {
let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
if name_str == "colgroup" {
current = unsafe { (*cur2).parent };
break;
}
}
}
cur2 = unsafe { (*cur2).parent };
}
}
if tag_lower_str == "caption" {
let mut cur2 = current;
while !cur2.is_null() {
let ctype = unsafe { (*cur2).type_ };
if ctype == XML_ELEMENT_NODE as c_int {
if !unsafe { (*cur2).name.is_null() } {
let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
if name_str == "caption" {
current = unsafe { (*cur2).parent };
break;
}
}
}
cur2 = unsafe { (*cur2).parent };
}
}
if tag_lower_str == "form" {
let mut cur2 = current;
while !cur2.is_null() {
let ctype = unsafe { (*cur2).type_ };
if ctype == XML_ELEMENT_NODE as c_int {
if !unsafe { (*cur2).name.is_null() } {
let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
if name_str.eq_ignore_ascii_case("form") {
current = unsafe { (*cur2).parent };
break;
}
}
}
cur2 = unsafe { (*cur2).parent };
}
}
ctxt.current = current;
}
unsafe fn ensure_html(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
if !ctxt.html.is_null() {
return ctxt.html;
}
let html_node = tree::new_node(ptr::null_mut(), b"html\0" as *const u8 as *const xmlChar);
if html_node.is_null() {
return ptr::null_mut();
}
unsafe {
}
ctxt.html = html_node;
ctxt.html_created = true;
tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
ctxt.current = html_node;
html_node
}
unsafe fn ensure_head(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
if !ctxt.head.is_null() {
return ctxt.head;
}
ensure_html(ctxt);
let head_node = tree::new_node(ptr::null_mut(), b"head\0" as *const u8 as *const xmlChar);
if head_node.is_null() {
return ptr::null_mut();
}
ctxt.head = head_node;
ctxt.head_created = true;
tree::add_child(ctxt.html, head_node);
ctxt.current = head_node;
ctxt.in_head = true;
head_node
}
unsafe fn ensure_body(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
if !ctxt.body.is_null() {
return ctxt.body;
}
ensure_html(ctxt);
let body_node = tree::new_node(ptr::null_mut(), b"body\0" as *const u8 as *const xmlChar);
if body_node.is_null() {
return ptr::null_mut();
}
ctxt.body = body_node;
ctxt.body_created = true;
tree::add_child(ctxt.html, body_node);
ctxt.current = body_node;
ctxt.in_body = true;
body_node
}
unsafe fn transition_to_body(ctxt: &mut HtmlParserCtxt) {
if ctxt.in_head && !ctxt.seen_body_content {
ctxt.seen_body_content = true;
ctxt.in_head = false;
ensure_body(ctxt);
}
}
struct HtmlAttr {
name: Vec<u8>,
value: Vec<u8>,
quoted: bool,
}
fn parse_attr_name(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
let mut name = Vec::new();
while let Some(ch) = ctxt.peek() {
if ch == b'='
|| ch == b'>'
|| ch == b'/'
|| ch == b' '
|| ch == b'\t'
|| ch == b'\n'
|| ch == b'\r'
{
break;
}
name.push(ch);
ctxt.next();
}
name
}
fn parse_attr_value(ctxt: &mut HtmlParserCtxt) -> (Vec<u8>, bool) {
ctxt.skip_whitespace();
let quote = match ctxt.peek() {
Some(b'"') => {
ctxt.next(); b'"'
}
Some(b'\'') => {
ctxt.next(); b'\''
}
_ => {
let value = ctxt.read_while(|ch| {
ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
});
return (value, false);
}
};
let mut value = Vec::new();
loop {
match ctxt.next() {
Some(ch) if ch == quote => break,
Some(ch) => value.push(ch),
None => break,
}
}
(value, true)
}
fn parse_attributes(ctxt: &mut HtmlParserCtxt) -> Vec<HtmlAttr> {
let mut attrs = Vec::new();
loop {
ctxt.skip_whitespace();
match ctxt.peek() {
Some(b'>') | None => break,
Some(b'/') => {
if ctxt.peek_at(1) == Some(b'>') {
break;
}
}
_ => {}
}
let name = parse_attr_name(ctxt);
if name.is_empty() {
break;
}
ctxt.skip_whitespace();
if ctxt.peek() == Some(b'=') {
ctxt.next(); let (value, quoted) = parse_attr_value(ctxt);
attrs.push(HtmlAttr {
name,
value,
quoted,
});
} else {
attrs.push(HtmlAttr {
name,
value: Vec::new(),
quoted: false,
});
}
}
attrs
}
fn resolve_entity(name: &str) -> Vec<u8> {
if let Some(replacement) = html_entity_lookup(name) {
replacement.as_bytes().to_vec()
} else {
let mut result = Vec::new();
result.push(b'&');
result.extend_from_slice(name.as_bytes());
result.push(b';');
result
}
}
fn resolve_numeric_entity(value: &str, is_hex: bool) -> Vec<u8> {
let codepoint = if is_hex {
u32::from_str_radix(value, 16).unwrap_or(0xFFFD)
} else {
u32::from_str_radix(value, 10).unwrap_or(0xFFFD)
};
if codepoint == 0 {
return Vec::new();
}
match char::from_u32(codepoint) {
Some(c) => {
let mut buf = [0u8; 4];
let s = c.encode_utf8(&mut buf);
s.as_bytes().to_vec()
}
None => vec![0xEF, 0xBF, 0xBD], }
}
fn parse_entity(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
match ctxt.peek() {
Some(b'&') => {
ctxt.next(); }
_ => return vec![b'&'],
}
if ctxt.peek() == Some(b'#') {
ctxt.next(); let is_hex = ctxt.peek() == Some(b'x') || ctxt.peek() == Some(b'X');
if is_hex {
ctxt.next(); }
let digits = ctxt.read_while(|ch| {
if is_hex {
ch.is_ascii_hexdigit()
} else {
ch.is_ascii_digit()
}
});
let digits_str = core::str::from_utf8(&digits).unwrap_or("");
if digits_str.is_empty() {
let mut result = vec![b'&', b'#'];
if is_hex {
result.push(b'x');
}
return result;
}
if ctxt.peek() == Some(b';') {
ctxt.next();
}
return resolve_numeric_entity(digits_str, is_hex);
}
let name = ctxt.read_while(|ch| ch.is_ascii_alphanumeric() || ch == b'_' || ch == b'-');
let name_str = core::str::from_utf8(&name).unwrap_or("");
if ctxt.peek() == Some(b';') {
ctxt.next();
}
resolve_entity(name_str)
}
unsafe fn handle_text(ctxt: &mut HtmlParserCtxt, text: &[u8]) {
if text.is_empty() {
return;
}
let parent = if ctxt.in_head {
ctxt.head
} else if ctxt.in_body || ctxt.body_created {
ctxt.body
} else if ctxt.html_created {
ctxt.html
} else {
ctxt.doc as *mut _xmlNode
};
let insertion_point = if ctxt.current.is_null() {
parent
} else {
ctxt.current
};
if insertion_point.is_null() {
let text_node = tree::new_text(ptr::null_mut());
if !text_node.is_null() {
let content = bytes_to_xmlstr(text);
if !content.is_null() {
unsafe {
(*text_node).content = content;
}
}
tree::add_child(ctxt.doc as *mut _xmlNode, text_node);
}
return;
}
let text_node = tree::new_text(ptr::null_mut());
if text_node.is_null() {
return;
}
let content = bytes_to_xmlstr(text);
if !content.is_null() {
unsafe {
(*text_node).content = content;
}
}
tree::add_child(insertion_point, text_node);
}
unsafe fn handle_start_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8], attrs: &[HtmlAttr]) {
let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
let info = html_tag_lookup(tag_str);
let is_head_tag = info.map_or(false, |i| i.flags & HTML_HEAD != 0);
let is_body_tag = info.map_or(false, |i| i.flags & HTML_BODY != 0);
let is_empty = info.map_or(false, |i| i.flags & HTML_EMPTY != 0);
let is_block = info.map_or(false, |i| i.flags & HTML_BLOCK != 0);
if tag_str == "html" {
if !ctxt.html.is_null() && !ctxt.html_created {
return;
}
if ctxt.html.is_null() {
let html_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
if !html_node.is_null() {
ctxt.html = html_node;
ctxt.html_created = false; tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
ctxt.current = html_node;
}
} else {
ctxt.current = ctxt.html;
}
return;
}
if tag_str == "head" {
if !ctxt.head.is_null() && !ctxt.head_created {
return;
}
ensure_html(ctxt);
if ctxt.head.is_null() {
let head_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
if !head_node.is_null() {
ctxt.head = head_node;
ctxt.head_created = false;
tree::add_child(ctxt.html, head_node);
ctxt.current = head_node;
ctxt.in_head = true;
}
} else {
ctxt.current = ctxt.head;
ctxt.in_head = true;
}
return;
}
if tag_str == "body" {
if !ctxt.body.is_null() && !ctxt.body_created {
return;
}
ensure_html(ctxt);
if ctxt.body.is_null() {
let body_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
if !body_node.is_null() {
ctxt.body = body_node;
ctxt.body_created = false;
tree::add_child(ctxt.html, body_node);
ctxt.current = body_node;
ctxt.in_body = true;
ctxt.in_head = false;
ctxt.seen_body_content = true;
}
} else {
ctxt.current = ctxt.body;
ctxt.in_body = true;
ctxt.in_head = false;
ctxt.seen_body_content = true;
}
return;
}
if is_head_tag && !ctxt.seen_body_content {
if ctxt.head.is_null() {
ensure_head(ctxt);
}
if is_empty {
let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
if !node.is_null() {
for attr in attrs {
let name_c = bytes_to_xmlstr(&attr.name);
let val_c = bytes_to_xmlstr(&attr.value);
if !name_c.is_null() {
tree::set_prop(node, name_c, val_c);
xmlFree(name_c as *mut c_void);
if !val_c.is_null() {
xmlFree(val_c as *mut c_void);
}
}
}
tree::add_child(ctxt.current, node);
}
return;
}
let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
if !node.is_null() {
for attr in attrs {
let name_c = bytes_to_xmlstr(&attr.name);
let val_c = bytes_to_xmlstr(&attr.value);
if !name_c.is_null() {
tree::set_prop(node, name_c, val_c);
xmlFree(name_c as *mut c_void);
if !val_c.is_null() {
xmlFree(val_c as *mut c_void);
}
}
}
tree::add_child(ctxt.current, node);
ctxt.current = node;
}
return;
}
if !is_head_tag || ctxt.seen_body_content {
if !ctxt.seen_body_content {
ctxt.seen_body_content = true;
ctxt.in_head = false;
if ctxt.body.is_null() {
ensure_body(ctxt);
} else {
ctxt.current = ctxt.body;
ctxt.in_body = true;
}
} else if ctxt.body.is_null() {
ensure_body(ctxt);
}
}
if !ctxt.current.is_null() {
auto_close_element(ctxt, tag_str);
}
if is_empty {
let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
if !node.is_null() {
for attr in attrs {
let name_c = bytes_to_xmlstr(&attr.name);
let val_c = bytes_to_xmlstr(&attr.value);
if !name_c.is_null() {
tree::set_prop(node, name_c, val_c);
xmlFree(name_c as *mut c_void);
if !val_c.is_null() {
xmlFree(val_c as *mut c_void);
}
}
}
let insertion_point = if ctxt.current.is_null() {
ctxt.body
} else {
ctxt.current
};
if !insertion_point.is_null() {
tree::add_child(insertion_point, node);
}
}
return;
}
let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
if !node.is_null() {
for attr in attrs {
let name_c = bytes_to_xmlstr(&attr.name);
let val_c = bytes_to_xmlstr(&attr.value);
if !name_c.is_null() {
tree::set_prop(node, name_c, val_c);
xmlFree(name_c as *mut c_void);
if !val_c.is_null() {
xmlFree(val_c as *mut c_void);
}
}
}
let insertion_point = if ctxt.current.is_null() {
if ctxt.in_body || ctxt.body_created {
ctxt.body
} else if ctxt.in_head || ctxt.head_created {
ctxt.head
} else if ctxt.html_created {
ctxt.html
} else {
ctxt.doc as *mut _xmlNode
}
} else {
ctxt.current
};
if !insertion_point.is_null() {
tree::add_child(insertion_point, node);
ctxt.current = node;
}
}
}
unsafe fn handle_end_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8]) {
let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
let info = html_tag_lookup(tag_str);
if info.map_or(false, |i| i.flags & HTML_EMPTY != 0) {
return;
}
if tag_str == "html" {
ctxt.current = ctxt.doc as *mut _xmlNode;
return;
}
if tag_str == "head" {
ctxt.in_head = false;
ctxt.current = ctxt.html;
return;
}
if tag_str == "body" {
ctxt.in_body = false;
ctxt.current = ctxt.html;
return;
}
let mut cur = ctxt.current;
while !cur.is_null() {
let ctype = unsafe { (*cur).type_ };
if ctype == XML_ELEMENT_NODE as c_int {
if !unsafe { (*cur).name.is_null() } {
let name_bytes = unsafe { xmlstr_to_bytes((*cur).name) };
if name_bytes.eq_ignore_ascii_case(tag_name) {
ctxt.current = unsafe { (*cur).parent };
return;
}
}
}
cur = unsafe { (*cur).parent };
}
}
unsafe fn html_parse_buffer(
ctxt: &mut HtmlParserCtxt,
buffer: *const c_char,
size: c_int,
) -> *mut _xmlDoc {
if buffer.is_null() || size <= 0 {
return ptr::null_mut();
}
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
if doc.is_null() {
return ptr::null_mut();
}
unsafe {
(*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
(*doc).properties = XML_DOC_WELLFORMED as c_int;
}
ctxt.doc = doc;
ctxt.input = buffer as *mut u8;
ctxt.input_len = size as usize;
ctxt.input_pos = 0;
ctxt.line = 1;
loop {
if ctxt.is_eof() {
break;
}
let ch = ctxt.peek().unwrap_or(0);
if ch == b'<' {
ctxt.next();
if ctxt.peek() == Some(b'/') {
ctxt.next(); let tag_name = ctxt.read_while(|ch| {
ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
});
while ctxt.peek() != Some(b'>') && !ctxt.is_eof() {
ctxt.next();
}
if ctxt.peek() == Some(b'>') {
ctxt.next(); }
if !tag_name.is_empty() {
handle_end_tag(ctxt, &tag_name);
}
continue;
}
if ctxt.peek() == Some(b'!')
&& ctxt.peek_at(1) == Some(b'-')
&& ctxt.peek_at(2) == Some(b'-')
{
ctxt.next(); ctxt.next(); ctxt.next();
let mut comment_content = Vec::new();
loop {
if ctxt.peek() == Some(b'-')
&& ctxt.peek_at(1) == Some(b'-')
&& ctxt.peek_at(2) == Some(b'>')
{
ctxt.next(); ctxt.next(); ctxt.next(); break;
}
match ctxt.next() {
Some(ch) => comment_content.push(ch),
None => break,
}
}
if !comment_content.is_empty() {
let comment_node = tree::new_comment(bytes_to_xmlstr(&comment_content));
if !comment_node.is_null() {
let insertion_point = if !ctxt.current.is_null() {
ctxt.current
} else {
ctxt.doc as *mut _xmlNode
};
tree::add_child(insertion_point, comment_node);
}
}
continue;
}
if ctxt.peek() == Some(b'!') {
ctxt.next(); let rest = ctxt.read_while(|ch| ch != b'>');
if ctxt.peek() == Some(b'>') {
ctxt.next(); }
continue;
}
if ctxt.peek() == Some(b'?') {
ctxt.next(); let mut pi_content = Vec::new();
loop {
if ctxt.peek() == Some(b'?') && ctxt.peek_at(1) == Some(b'>') {
break;
}
match ctxt.next() {
Some(ch) => pi_content.push(ch),
None => break,
}
}
if ctxt.peek() == Some(b'?') {
ctxt.next();
}
if ctxt.peek() == Some(b'>') {
ctxt.next();
}
if !pi_content.is_empty() {
let mut parts = pi_content.splitn(2, |b| *b == b' ');
let target = parts.next().unwrap_or(&pi_content);
let value = parts.next().unwrap_or(b"");
let pi_node = tree::new_pi(bytes_to_xmlstr(target), bytes_to_xmlstr(value));
if !pi_node.is_null() {
let insertion_point = if !ctxt.current.is_null() {
ctxt.current
} else {
ctxt.doc as *mut _xmlNode
};
tree::add_child(insertion_point, pi_node);
}
}
continue;
}
let tag_name = ctxt.read_while(|ch| {
ch != b'>' && ch != b'/' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
});
if tag_name.is_empty() {
handle_text(ctxt, &[b'<']);
continue;
}
let attrs = parse_attributes(ctxt);
if ctxt.peek() == Some(b'/') {
ctxt.next(); if ctxt.peek() == Some(b'>') {
ctxt.next(); }
} else if ctxt.peek() == Some(b'>') {
ctxt.next(); }
let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
if tag_str == "script" || tag_str == "style" {
let raw_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(&tag_name));
if !raw_node.is_null() {
for attr in &attrs {
let name_c = bytes_to_xmlstr(&attr.name);
let val_c = bytes_to_xmlstr(&attr.value);
if !name_c.is_null() {
tree::set_prop(raw_node, name_c, val_c);
xmlFree(name_c as *mut c_void);
if !val_c.is_null() {
xmlFree(val_c as *mut c_void);
}
}
}
let insertion_point = if ctxt.current.is_null() {
if ctxt.in_head {
ensure_head(ctxt);
ctxt.head
} else {
ensure_body(ctxt);
ctxt.body
}
} else {
ctxt.current
};
if !insertion_point.is_null() {
tree::add_child(insertion_point, raw_node);
let end_tag = format!("</{}", tag_str);
let end_bytes = end_tag.as_bytes();
let mut raw_text = Vec::new();
let mut match_idx = 0;
loop {
if ctxt.is_eof() {
break;
}
let ch = ctxt.peek().unwrap();
if ch.to_ascii_lowercase() == end_bytes[match_idx] {
match_idx += 1;
if match_idx == end_bytes.len() {
if !raw_text.is_empty() {
let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
if !text_node.is_null() {
tree::add_child(raw_node, text_node);
}
}
ctxt.next(); let _suffix = ctxt.read_while(|ch| ch != b'>');
if ctxt.peek() == Some(b'>') {
ctxt.next();
}
ctxt.current = unsafe { (*raw_node).parent };
break;
}
if match_idx == 1 {
raw_text.push(ch);
}
ctxt.next();
} else {
if match_idx > 0 {
match_idx = 0;
}
raw_text.push(ch);
ctxt.next();
}
}
if match_idx < end_bytes.len() && !raw_text.is_empty() {
let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
if !text_node.is_null() {
tree::add_child(raw_node, text_node);
}
ctxt.current = unsafe { (*raw_node).parent };
}
}
}
continue;
}
handle_start_tag(ctxt, &tag_name, &attrs);
} else {
let mut text = Vec::new();
loop {
match ctxt.peek() {
Some(b'<') => break,
Some(b'&') => {
let entity_text = parse_entity(ctxt);
text.extend_from_slice(&entity_text);
}
Some(ch) => {
text.push(ch);
ctxt.next();
}
None => break,
}
}
if !text.is_empty() {
handle_text(ctxt, &text);
}
}
}
if ctxt.html.is_null() {
ensure_html(ctxt);
}
doc
}
pub(crate) unsafe fn parse_file(filename: *const c_char, encoding: *const c_char) -> *mut _xmlDoc {
if filename.is_null() {
return ptr::null_mut();
}
let filename_str = unsafe { std::ffi::CStr::from_ptr(filename) };
let path = filename_str.to_str().unwrap_or("");
let content = match std::fs::read(path) {
Ok(data) => data,
Err(_) => return ptr::null_mut(),
};
let mut ctxt = HtmlParserCtxt::new();
if !encoding.is_null() {
let enc_cstr = unsafe { std::ffi::CStr::from_ptr(encoding) };
ctxt.encoding = unsafe { c_strdup(encoding) };
}
let doc = unsafe {
html_parse_buffer(
&mut ctxt,
content.as_ptr() as *const c_char,
content.len() as c_int,
)
};
if !doc.is_null() && !filename.is_null() {
unsafe {
(*doc).URL = c_strdup(filename) as *mut xmlChar;
}
}
doc
}
pub(crate) unsafe fn parse_memory(buffer: *const c_char, size: c_int) -> *mut _xmlDoc {
if buffer.is_null() || size <= 0 {
return ptr::null_mut();
}
let mut ctxt = HtmlParserCtxt::new();
unsafe { html_parse_buffer(&mut ctxt, buffer, size) }
}
pub(crate) unsafe fn parse_doc(cur: *const xmlChar, encoding: *const c_char) -> *mut _xmlDoc {
if cur.is_null() {
return ptr::null_mut();
}
let len = unsafe { xml_strlen(cur) };
let mut ctxt = HtmlParserCtxt::new();
if !encoding.is_null() {
ctxt.encoding = unsafe { c_strdup(encoding) };
}
unsafe { html_parse_buffer(&mut ctxt, cur as *const c_char, len as c_int) }
}
pub(crate) unsafe fn create_file_parser_ctxt(
filename: *const c_char,
encoding: *const c_char,
) -> *mut c_void {
if filename.is_null() {
return ptr::null_mut();
}
let ctxt = unsafe { xmlMallocZero(size_of::<HtmlParserCtxt>() as usize) };
if ctxt.is_null() {
return ptr::null_mut();
}
let ctxt = ctxt as *mut HtmlParserCtxt;
unsafe {
ptr::write(ctxt, HtmlParserCtxt::new());
if !encoding.is_null() {
(*ctxt).encoding = c_strdup(encoding);
}
}
ctxt as *mut c_void
}
pub(crate) unsafe fn free_parser_ctxt(ctxt: *mut c_void) {
if ctxt.is_null() {
return;
}
let ctxt = ctxt as *mut HtmlParserCtxt;
unsafe {
if !(*ctxt).filename.is_null() {
xmlFree((*ctxt).filename as *mut c_void);
}
if !(*ctxt).encoding.is_null() {
xmlFree((*ctxt).encoding as *mut c_void);
}
xmlFree(ctxt as *mut c_void);
}
}
pub(crate) fn init_parser() {
}
pub(crate) fn cleanup_parser() {
}
pub(crate) unsafe fn new_doc(version: *const xmlChar) -> *mut _xmlDoc {
let doc = tree::new_doc(version);
if doc.is_null() {
return ptr::null_mut();
}
unsafe {
(*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
(*doc).properties = XML_DOC_WELLFORMED as c_int;
}
let mut ctxt = HtmlParserCtxt::new();
ctxt.doc = doc;
unsafe {
ensure_html(&mut ctxt);
ensure_head(&mut ctxt);
ensure_body(&mut ctxt);
}
doc
}
pub(crate) unsafe fn new_doc_no_dtd(version: *const xmlChar) -> *mut _xmlDoc {
let doc = tree::new_doc(version);
if doc.is_null() {
return ptr::null_mut();
}
unsafe {
(*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
(*doc).properties = XML_DOC_WELLFORMED as c_int;
}
doc
}
const HTML_VOID_ELEMENTS: &[&str] = &[
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
"track", "wbr", "frame",
];
fn is_html_void(name: &str) -> bool {
HTML_VOID_ELEMENTS
.iter()
.any(|v| v.eq_ignore_ascii_case(name))
}
fn has_optional_end_tag(name: &str) -> bool {
matches!(
name.to_ascii_lowercase().as_str(),
"p" | "li" | "tr" | "td" | "th" | "dt" | "dd"
)
}
unsafe fn html_serialize_text(buf: *mut _xmlBuffer, content: *const xmlChar, len: c_int) {
if buf.is_null() || content.is_null() || len <= 0 {
return;
}
let mut i: c_int = 0;
while i < len {
let ch = unsafe { *content.add(i as usize) };
match ch {
b'<' => {
io::buf_add(buf, b"<" as *const u8, 4);
}
b'&' => {
io::buf_add(buf, b"&" as *const u8, 5);
}
_ => {
io::buf_add(buf, &ch as *const u8, 1);
}
}
i += 1;
}
}
unsafe fn html_serialize_attr_value(buf: *mut _xmlBuffer, value: *const xmlChar) {
if buf.is_null() || value.is_null() {
return;
}
let len = unsafe { xml_strlen(value) as c_int };
let mut i: c_int = 0;
while i < len {
let ch = unsafe { *value.add(i as usize) };
match ch {
b'&' => {
io::buf_add(buf, b"&" as *const u8, 5);
}
b'"' => {
io::buf_add(buf, b""" as *const u8, 6);
}
_ => {
io::buf_add(buf, &ch as *const u8, 1);
}
}
i += 1;
}
}
pub(crate) unsafe fn serialize_node(
node: *mut _xmlNode,
buf: *mut _xmlBuffer,
format: c_int,
level: c_int,
) {
if node.is_null() || buf.is_null() {
return;
}
let n = unsafe { &*node };
match n.type_ {
t if t == XML_ELEMENT_NODE as c_int => {
let name = if n.name.is_null() {
""
} else {
unsafe { core::str::from_utf8(xmlstr_to_bytes(n.name)).unwrap_or("") }
};
let is_void = is_html_void(name);
if format != 0 && level > 0 {
io::buf_ccat(buf, b'\n');
for _ in 0..level {
io::buf_add(buf, b" " as *const u8, 2);
}
}
io::buf_ccat(buf, b'<');
if !n.name.is_null() {
io::buf_cat(buf, n.name);
}
let mut attr = n.properties;
while !attr.is_null() {
let a = unsafe { &*attr };
io::buf_ccat(buf, b' ');
if !a.name.is_null() {
io::buf_cat(buf, a.name);
}
if !a.children.is_null() {
let child = unsafe { &*a.children };
if child.type_ == XML_TEXT_NODE as c_int && !child.content.is_null() {
io::buf_ccat(buf, b'=');
io::buf_ccat(buf, b'"');
html_serialize_attr_value(buf, child.content);
io::buf_ccat(buf, b'"');
}
}
attr = a.next;
}
if is_void {
io::buf_ccat(buf, b'>');
} else if n.children.is_null() {
io::buf_ccat(buf, b'>');
if format != 0 {
io::buf_ccat(buf, b'\n');
for _ in 0..level {
io::buf_add(buf, b" " as *const u8, 2);
}
}
io::buf_add(buf, b"</" as *const u8, 2);
if !n.name.is_null() {
io::buf_cat(buf, n.name);
}
io::buf_ccat(buf, b'>');
} else {
io::buf_ccat(buf, b'>');
let is_text_only = n.children == n.last
&& !n.children.is_null()
&& unsafe { (*(n.children)).type_ } == XML_TEXT_NODE as c_int;
if is_text_only {
let mut child = n.children;
while !child.is_null() {
serialize_node(child, buf, format, level + 1);
child = unsafe { (*child).next };
}
} else {
let mut child = n.children;
while !child.is_null() {
serialize_node(child, buf, format, level + 1);
child = unsafe { (*child).next };
}
if format != 0 {
io::buf_ccat(buf, b'\n');
for _ in 0..level {
io::buf_add(buf, b" " as *const u8, 2);
}
}
}
io::buf_add(buf, b"</" as *const u8, 2);
if !n.name.is_null() {
io::buf_cat(buf, n.name);
}
io::buf_ccat(buf, b'>');
}
}
t if t == XML_TEXT_NODE as c_int => {
html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
}
t if t == XML_CDATA_SECTION_NODE as c_int => {
io::buf_add(buf, b"<![CDATA[" as *const u8, 9);
html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
io::buf_add(buf, b"]]>" as *const u8, 3);
}
t if t == XML_COMMENT_NODE as c_int => {
if format != 0 && level > 0 {
io::buf_ccat(buf, b'\n');
for _ in 0..level {
io::buf_add(buf, b" " as *const u8, 2);
}
}
io::buf_add(buf, b"<!--" as *const u8, 4);
if !n.content.is_null() {
io::buf_cat(buf, n.content);
}
io::buf_add(buf, b"-->" as *const u8, 3);
}
t if t == XML_PI_NODE as c_int => {
if format != 0 && level > 0 {
io::buf_ccat(buf, b'\n');
for _ in 0..level {
io::buf_add(buf, b" " as *const u8, 2);
}
}
io::buf_add(buf, b"<?" as *const u8, 2);
if !n.name.is_null() {
io::buf_cat(buf, n.name);
}
if !n.content.is_null() && unsafe { *n.content != 0 } {
io::buf_ccat(buf, b' ');
io::buf_cat(buf, n.content);
}
io::buf_add(buf, b"?>" as *const u8, 2);
}
t if t == XML_DOCUMENT_NODE as c_int || t == XML_HTML_DOCUMENT_NODE as c_int => {
let mut child = n.children;
while !child.is_null() {
serialize_node(child, buf, format, 0);
child = unsafe { (*child).next };
}
if format != 0 {
io::buf_ccat(buf, b'\n');
}
}
_ => {
if !n.content.is_null() {
html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
}
}
}
}
pub(crate) unsafe fn doc_dump(buf: *mut _xmlBuffer, doc: *mut _xmlDoc) -> c_int {
if buf.is_null() || doc.is_null() {
return -1;
}
let before = io::buf_length(buf);
serialize_node(doc as *mut _xmlNode, buf, 0, 0);
let after = io::buf_length(buf);
if after < 0 || before < 0 {
return -1;
}
after - before
}
#[cfg(test)]
mod tests {
use super::*;
use crate::abi::allocator::xmlFree;
use crate::xml::io;
unsafe fn to_xmlstr(s: &[u8]) -> *mut xmlChar {
bytes_to_xmlstr(s)
}
unsafe fn html_doc_to_string(doc: *mut _xmlDoc) -> String {
let buf = io::buf_create(-1);
assert!(!buf.is_null());
doc_dump(buf, doc);
let content = io::buf_content(buf);
let s = if !content.is_null() {
let len = xml_strlen(content);
let slice = slice::from_raw_parts(content, len as usize);
String::from_utf8_lossy(slice).to_string()
} else {
String::new()
};
io::buf_free(buf);
s
}
#[test]
fn test_html_tag_lookup() {
assert!(html_tag_lookup("html").is_some());
assert!(html_tag_lookup("HTML").is_some()); assert!(html_tag_lookup("p").is_some());
assert!(html_tag_lookup("br").is_some());
assert!(html_tag_lookup("div").is_some());
assert!(html_tag_lookup("script").is_some());
assert!(html_tag_lookup("custom").is_none());
assert!(html_tag_lookup("my-element").is_none());
}
#[test]
fn test_tag_flags() {
let br = html_tag_lookup("br").unwrap();
assert!(br.flags & HTML_INLINE != 0);
assert!(br.flags & HTML_EMPTY != 0);
let div = html_tag_lookup("div").unwrap();
assert!(div.flags & HTML_BLOCK != 0);
assert!(div.flags & HTML_VALID != 0);
let p = html_tag_lookup("p").unwrap();
assert!(p.flags & HTML_NO_END != 0);
let meta = html_tag_lookup("meta").unwrap();
assert!(meta.flags & HTML_HEAD != 0);
assert!(meta.flags & HTML_EMPTY != 0);
}
#[test]
fn test_html_entity_lookup() {
assert_eq!(html_entity_lookup("amp"), Some("&"));
assert_eq!(html_entity_lookup("lt"), Some("<"));
assert_eq!(html_entity_lookup("gt"), Some(">"));
assert_eq!(html_entity_lookup("quot"), Some("\""));
assert_eq!(html_entity_lookup("nbsp"), Some("\u{00a0}"));
assert_eq!(html_entity_lookup("copy"), Some("\u{00a9}"));
assert!(html_entity_lookup("unknown_entity").is_none());
}
#[test]
fn test_parse_basic_html() {
unsafe {
let html = b"<html><head><title>Test</title></head><body><p>Hello</p></body></html>\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("<html>"));
assert!(s.contains("<head>"));
assert!(s.contains("<title>Test</title>"));
assert!(s.contains("<body>"));
assert!(s.contains("<p>Hello</p>"));
tree::free_doc(doc);
}
}
#[test]
fn test_parse_empty_document() {
unsafe {
let html = b"\0";
let doc = parse_memory(html.as_ptr() as *const c_char, 0);
assert!(doc.is_null());
}
}
#[test]
fn test_implicit_html_head_body() {
unsafe {
let html = b"<p>Hello</p>\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("<html>"));
assert!(s.contains("<body>"));
assert!(s.contains("<p>Hello</p>"));
tree::free_doc(doc);
}
}
#[test]
fn test_implicit_head_with_title() {
unsafe {
let html = b"<title>My Page</title>\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("<html>"));
assert!(s.contains("<head>"));
assert!(s.contains("<title>My Page</title>"));
tree::free_doc(doc);
}
}
#[test]
fn test_auto_close_p() {
unsafe {
let html = b"<p>First<p>Second</p>\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
let first_pos = s.find("First");
let second_pos = s.find("Second");
assert!(first_pos.is_some());
assert!(second_pos.is_some());
tree::free_doc(doc);
}
}
#[test]
fn test_auto_close_heading() {
unsafe {
let html = b"<h1>Title</h1><h2>Subtitle</h2>\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("<h1>Title</h1>"));
assert!(s.contains("<h2>Subtitle</h2>"));
tree::free_doc(doc);
}
}
#[test]
fn test_void_elements() {
unsafe {
let html = b"<br><hr><img src=\"test.jpg\"><input type=\"text\">\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("<br>"));
assert!(s.contains("<hr>"));
assert!(s.contains("<img"));
assert!(s.contains("<input"));
assert!(!s.contains("</br>"));
assert!(!s.contains("</hr>"));
assert!(!s.contains("</img>"));
tree::free_doc(doc);
}
}
#[test]
fn test_unquoted_attributes() {
unsafe {
let html = b"<div class=main id=content>Text</div>\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("class=\"main\""));
assert!(s.contains("id=\"content\""));
tree::free_doc(doc);
}
}
#[test]
fn test_minimized_attributes() {
unsafe {
let html = b"<option selected disabled>Value</option>\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("selected"));
assert!(s.contains("disabled"));
tree::free_doc(doc);
}
}
#[test]
fn test_html_entities() {
unsafe {
let html = b"<p>& < > " ©</p>\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("&")); assert!(s.contains("<")); assert!(s.contains(">")); assert!(s.contains("\u{00a0}"));
tree::free_doc(doc);
}
}
#[test]
fn test_numeric_entities() {
unsafe {
let html = b"<p>A A</p>\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains('A'));
tree::free_doc(doc);
}
}
#[test]
fn test_nested_elements() {
unsafe {
let html = b"<div><ul><li>Item 1</li><li>Item 2</li></ul></div>\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("<div>"));
assert!(s.contains("<ul>"));
assert!(s.contains("<li>Item 1</li>"));
assert!(s.contains("<li>Item 2</li>"));
tree::free_doc(doc);
}
}
#[test]
fn test_missing_end_tags() {
unsafe {
let html = b"<p>Paragraph without closing<div>Another div\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("Paragraph without closing"));
assert!(s.contains("Another div"));
tree::free_doc(doc);
}
}
#[test]
fn test_mismatched_case() {
unsafe {
let html = b"<HTML><HEAD><TITLE>Test</TITLE></HEAD><BODY><P>Hello</P></BODY></HTML>\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("<HTML>"));
assert!(s.contains("<HEAD>"));
assert!(s.contains("<BODY>"));
assert!(s.contains("<P>Hello</P>"));
tree::free_doc(doc);
}
}
#[test]
fn test_nested_malformed() {
unsafe {
let html = b"<div><p><span><b>Deep text</div></p>\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("Deep text"));
tree::free_doc(doc);
}
}
#[test]
fn test_serialization_round_trip_simple() {
unsafe {
let original = b"<p>Hello World</p>\0";
let doc = parse_memory(
original.as_ptr() as *const c_char,
(original.len() - 1) as c_int,
);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("Hello World"));
tree::free_doc(doc);
}
}
#[test]
fn test_serialize_void_elements_no_self_close() {
unsafe {
let html = b"<br><hr><img src=\"test.png\">\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(!s.contains("<br/>"));
assert!(!s.contains("<hr/>"));
tree::free_doc(doc);
}
}
#[test]
fn test_script_content() {
unsafe {
let html = b"<script>var x = 1;</script>\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("<script>"));
assert!(s.contains("var x = 1;"));
tree::free_doc(doc);
}
}
#[test]
fn test_html_comment() {
unsafe {
let html = b"<html><!-- This is a comment --><body><p>Text</p></body></html>\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("<!-- This is a comment -->"));
tree::free_doc(doc);
}
}
#[test]
fn test_new_doc_creates_html_head_body() {
unsafe {
let doc = new_doc(ptr::null());
assert!(!doc.is_null());
assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
let s = html_doc_to_string(doc);
assert!(s.contains("<html>"));
assert!(s.contains("<head>"));
assert!(s.contains("<body>"));
tree::free_doc(doc);
}
}
#[test]
fn test_new_doc_no_dtd() {
unsafe {
let doc = new_doc_no_dtd(ptr::null());
assert!(!doc.is_null());
assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
let s = html_doc_to_string(doc);
assert_eq!(s, "");
tree::free_doc(doc);
}
}
#[test]
fn test_resolve_numeric_entity() {
assert_eq!(resolve_numeric_entity("65", false), vec![b'A']);
assert_eq!(resolve_numeric_entity("41", true), vec![b'A']);
assert_eq!(resolve_numeric_entity("0", false), Vec::<u8>::new());
}
#[test]
fn test_resolve_entity_unknown() {
let result = resolve_entity("unknown");
assert_eq!(result, b"&unknown;");
}
#[test]
fn test_init_cleanup_parser() {
init_parser();
cleanup_parser();
}
#[test]
fn test_create_free_parser_ctxt() {
unsafe {
let ctxt =
create_file_parser_ctxt(b"test.html\0" as *const u8 as *const c_char, ptr::null());
assert!(!ctxt.is_null());
free_parser_ctxt(ctxt);
}
}
#[test]
fn test_complex_html_document() {
unsafe {
let html = b"<!DOCTYPE html>
<html>
<head>
<meta charset=\"utf-8\">
<title>Test Page</title>
<link rel=\"stylesheet\" href=\"style.css\">
</head>
<body>
<div id=\"main\">
<h1>Title</h1>
<p>First paragraph with <a href=\"link.html\">a link</a>.</p>
<p>Second paragraph.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
<br>
<hr>
<img src=\"image.jpg\" alt=\"An image\">
</div>
<script>alert('hello');</script>
</body>
</html>\0";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("<html>"));
assert!(s.contains("<head>"));
assert!(s.contains("<title>Test Page</title>"));
assert!(s.contains("<body>"));
assert!(s.contains("<h1>Title</h1>"));
assert!(s.contains("a link"));
assert!(s.contains("Second paragraph"));
assert!(s.contains("<br>"));
assert!(s.contains("<hr>"));
assert!(s.contains("<img"));
assert!(s.contains("<script>"));
tree::free_doc(doc);
}
}
#[test]
fn test_parse_doc() {
unsafe {
let html = b"<p>Hello from parse_doc</p>";
let doc = parse_doc(html.as_ptr() as *const xmlChar, ptr::null());
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("Hello from parse_doc"));
tree::free_doc(doc);
}
}
#[test]
fn test_table_element_auto_close() {
unsafe {
let html = b"<table><tr><td>Cell 1<td>Cell 2</td></tr></table>";
let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
assert!(!doc.is_null());
let s = html_doc_to_string(doc);
assert!(s.contains("<td>Cell 1"));
assert!(s.contains("<td>Cell 2"));
tree::free_doc(doc);
}
}
}