#![allow(unsafe_code)]
use std::cell::{Cell, RefCell};
use std::sync::Arc;
use bumpalo::Bump;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuirksMode {
NoQuirks,
LimitedQuirks,
Quirks,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HtmlDoctype {
pub name: String,
pub public_id: String,
pub system_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HtmlMeta {
pub quirks_mode: QuirksMode,
pub doctype: Option<HtmlDoctype>,
}
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NodeKind {
Element = 1,
Attribute = 2,
Text = 3,
CData = 4,
EntityRef = 5,
Comment = 8,
Pi = 7,
Document = 9,
DocumentFragment = 11,
Dtd = 14,
DtdDecl = 15,
}
#[cfg(feature = "c-abi")]
#[repr(transparent)]
#[derive(Copy, Clone)]
pub struct ArenaCStr<'doc> {
ptr: std::ptr::NonNull<u8>,
_marker: std::marker::PhantomData<&'doc u8>,
}
#[cfg(feature = "c-abi")]
impl<'doc> ArenaCStr<'doc> {
#[inline]
pub unsafe fn from_raw(ptr: *const u8) -> Self {
debug_assert!(!ptr.is_null(), "ArenaCStr::from_raw given NULL");
Self {
ptr: unsafe { std::ptr::NonNull::new_unchecked(ptr as *mut u8) },
_marker: std::marker::PhantomData,
}
}
#[inline]
pub fn as_ptr(&self) -> *const u8 { self.ptr.as_ptr() }
pub fn as_str(&self) -> &'doc str {
unsafe {
let cstr = std::ffi::CStr::from_ptr(self.ptr.as_ptr() as *const std::os::raw::c_char);
std::str::from_utf8_unchecked(cstr.to_bytes())
}
}
pub fn empty() -> Self {
static EMPTY: u8 = 0;
Self {
ptr: std::ptr::NonNull::from(&EMPTY),
_marker: std::marker::PhantomData,
}
}
#[inline]
pub fn text_name() -> Self {
unsafe { Self::from_raw(xml_string_text()) }
}
#[inline]
pub fn text_noenc_name() -> Self {
unsafe { Self::from_raw(xml_string_text_noenc()) }
}
}
#[cfg(feature = "c-abi")]
#[unsafe(no_mangle)]
pub static xmlStringText: [u8; 5] = *b"text\0";
#[cfg(feature = "c-abi")]
#[unsafe(no_mangle)]
pub static xmlStringTextNoenc: [u8; 10] = *b"textnoenc\0";
#[cfg(feature = "c-abi")]
#[inline]
fn xml_string_text() -> *const u8 { (&xmlStringText) as *const _ as *const u8 }
#[cfg(feature = "c-abi")]
#[inline]
fn xml_string_text_noenc() -> *const u8 { (&xmlStringTextNoenc) as *const _ as *const u8 }
#[cfg(feature = "c-abi")]
impl std::fmt::Debug for ArenaCStr<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.as_str())
}
}
#[cfg(not(feature = "c-abi"))]
#[repr(C)]
#[derive(Debug)]
pub struct Namespace<'doc> {
pub prefix: Option<&'doc str>,
pub href: &'doc str,
}
#[cfg(feature = "c-abi")]
#[repr(C)]
pub struct Namespace<'doc> {
pub next: Cell<Option<&'doc Namespace<'doc>>>, pub kind: i32, _pad_kind: i32, pub href: ArenaCStr<'doc>, pub prefix: Option<ArenaCStr<'doc>>, pub _private: Cell<*mut std::os::raw::c_void>, pub context: Cell<*mut std::os::raw::c_void>, }
#[cfg(feature = "c-abi")]
impl std::fmt::Debug for Namespace<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Namespace")
.field("prefix", &self.prefix.map(|p| p.as_str()))
.field("href", &self.href.as_str())
.finish()
}
}
impl<'doc> Namespace<'doc> {
#[inline]
pub fn prefix(&self) -> Option<&'doc str> {
#[cfg(not(feature = "c-abi"))]
{ self.prefix }
#[cfg(feature = "c-abi")]
{ self.prefix.map(|p| p.as_str()) }
}
#[inline]
pub fn href(&self) -> &'doc str {
#[cfg(not(feature = "c-abi"))]
{ self.href }
#[cfg(feature = "c-abi")]
{ self.href.as_str() }
}
}
#[cfg(not(feature = "c-abi"))]
#[repr(C)]
pub struct Attribute<'doc> {
pub name: &'doc str,
pub namespace: Cell<Option<&'doc Namespace<'doc>>>,
pub value: &'doc str,
pub next: Cell<Option<&'doc Attribute<'doc>>>,
pub prev: Cell<Option<&'doc Attribute<'doc>>>,
pub parent: Cell<Option<&'doc Node<'doc>>>,
}
#[cfg(feature = "c-abi")]
#[repr(C)]
pub struct Attribute<'doc> {
pub _private: Cell<*mut std::os::raw::c_void>, pub kind: NodeKind, _pad_kind: u32, pub name: ArenaCStr<'doc>, pub children: Cell<Option<&'doc Node<'doc>>>, pub last: Cell<Option<&'doc Node<'doc>>>, pub parent: Cell<Option<&'doc Node<'doc>>>, pub next: Cell<Option<&'doc Attribute<'doc>>>, pub prev: Cell<Option<&'doc Attribute<'doc>>>, pub doc: Cell<*mut std::os::raw::c_void>, pub namespace: Cell<Option<&'doc Namespace<'doc>>>, pub atype: u32, _pad_atype: u32, pub psvi: Cell<*mut std::os::raw::c_void>, pub value: ArenaCStr<'doc>,
}
impl<'doc> Attribute<'doc> {
#[inline]
pub fn name(&self) -> &'doc str {
#[cfg(not(feature = "c-abi"))]
{ self.name }
#[cfg(feature = "c-abi")]
{ self.name.as_str() }
}
#[inline]
pub fn local_name(&self) -> &'doc str {
let n = self.name();
match n.rfind(':') {
Some(i) => &n[i + 1..],
None => n,
}
}
#[inline]
pub fn value(&self) -> &'doc str {
#[cfg(not(feature = "c-abi"))]
{ self.value }
#[cfg(feature = "c-abi")]
{
let v = self.value.as_str();
if !v.is_empty() { return v; }
let mut child = self.children.get();
while let Some(c) = child {
let s = c.content();
if !s.is_empty() { return s; }
child = c.next_sibling.get();
}
""
}
}
#[cfg(feature = "c-abi")]
pub fn children(&self) -> ChildIter<'doc> {
ChildIter::from_head(self.children.get())
}
}
impl std::fmt::Debug for Attribute<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Attribute")
.field("name", &self.name())
.field("value", &self.value())
.finish()
}
}
#[cfg(not(feature = "c-abi"))]
#[repr(C)]
pub struct Node<'doc> {
pub kind: NodeKind,
pub name: &'doc str,
pub namespace: Cell<Option<&'doc Namespace<'doc>>>,
pub first_attribute: Cell<Option<&'doc Attribute<'doc>>>,
pub last_attribute: Cell<Option<&'doc Attribute<'doc>>>,
pub first_child: Cell<Option<&'doc Node<'doc>>>,
pub last_child: Cell<Option<&'doc Node<'doc>>>,
pub parent: Cell<Option<&'doc Node<'doc>>>,
pub next_sibling: Cell<Option<&'doc Node<'doc>>>,
pub prev_sibling: Cell<Option<&'doc Node<'doc>>>,
pub content: Cell<Option<&'doc str>>,
pub line: u32,
pub source_offset: u32,
}
#[cfg(feature = "c-abi")]
#[repr(C)]
pub struct Node<'doc> {
pub _private: Cell<*mut std::os::raw::c_void>, pub kind: NodeKind, _pad_kind: u32, pub name: Cell<ArenaCStr<'doc>>, pub first_child: Cell<Option<&'doc Node<'doc>>>, pub last_child: Cell<Option<&'doc Node<'doc>>>, pub parent: Cell<Option<&'doc Node<'doc>>>, pub next_sibling: Cell<Option<&'doc Node<'doc>>>, pub prev_sibling: Cell<Option<&'doc Node<'doc>>>, pub doc: Cell<*mut std::os::raw::c_void>, pub namespace: Cell<Option<&'doc Namespace<'doc>>>, pub content: Cell<Option<ArenaCStr<'doc>>>, pub first_attribute: Cell<Option<&'doc Attribute<'doc>>>, pub ns_def: Cell<Option<&'doc Namespace<'doc>>>, pub psvi: Cell<*mut std::os::raw::c_void>, pub line: Cell<u16>, pub extra: u16, _pad_extra: [u8; 4], pub last_attribute: Cell<Option<&'doc Attribute<'doc>>>,
pub full_line: Cell<u32>,
pub source_offset: u32,
}
#[cfg(feature = "c-abi")]
const _: () = {
use std::mem::offset_of;
assert!(offset_of!(Node<'static>, _private) == 0, "Node::_private @ 0");
assert!(offset_of!(Node<'static>, kind) == 8, "Node::kind @ 8");
assert!(offset_of!(Node<'static>, name) == 16, "Node::name @ 16");
assert!(offset_of!(Node<'static>, first_child) == 24, "Node::first_child (children) @ 24");
assert!(offset_of!(Node<'static>, last_child) == 32, "Node::last_child (last) @ 32");
assert!(offset_of!(Node<'static>, parent) == 40, "Node::parent @ 40");
assert!(offset_of!(Node<'static>, next_sibling) == 48, "Node::next_sibling (next) @ 48");
assert!(offset_of!(Node<'static>, prev_sibling) == 56, "Node::prev_sibling (prev) @ 56");
assert!(offset_of!(Node<'static>, doc) == 64, "Node::doc @ 64");
assert!(offset_of!(Node<'static>, namespace) == 72, "Node::namespace (ns) @ 72");
assert!(offset_of!(Node<'static>, content) == 80, "Node::content @ 80");
assert!(offset_of!(Node<'static>, first_attribute) == 88, "Node::first_attribute (properties) @ 88");
assert!(offset_of!(Node<'static>, ns_def) == 96, "Node::ns_def @ 96");
assert!(offset_of!(Node<'static>, psvi) == 104, "Node::psvi @ 104");
assert!(offset_of!(Node<'static>, line) == 112, "Node::line @ 112");
assert!(offset_of!(Node<'static>, extra) == 114, "Node::extra @ 114");
assert!(offset_of!(Node<'static>, last_attribute) >= 120, "Node tail starts after ABI window");
assert!(offset_of!(Attribute<'static>, _private) == 0, "Attribute::_private @ 0");
assert!(offset_of!(Attribute<'static>, kind) == 8, "Attribute::kind @ 8");
assert!(offset_of!(Attribute<'static>, name) == 16, "Attribute::name @ 16");
assert!(offset_of!(Attribute<'static>, children) == 24, "Attribute::children @ 24");
assert!(offset_of!(Attribute<'static>, last) == 32, "Attribute::last @ 32");
assert!(offset_of!(Attribute<'static>, parent) == 40, "Attribute::parent @ 40");
assert!(offset_of!(Attribute<'static>, next) == 48, "Attribute::next @ 48");
assert!(offset_of!(Attribute<'static>, prev) == 56, "Attribute::prev @ 56");
assert!(offset_of!(Attribute<'static>, doc) == 64, "Attribute::doc @ 64");
assert!(offset_of!(Attribute<'static>, namespace) == 72, "Attribute::namespace (ns) @ 72");
assert!(offset_of!(Attribute<'static>, atype) == 80, "Attribute::atype @ 80");
assert!(offset_of!(Attribute<'static>, psvi) == 88, "Attribute::psvi @ 88");
assert!(offset_of!(Attribute<'static>, value) >= 96, "Attribute value in tail");
assert!(std::mem::size_of::<Node<'static>>() >= 120, "Node size >= 120 (ABI window)");
assert!(std::mem::size_of::<Attribute<'static>>() >= 96, "Attribute size >= ABI window");
assert!(std::mem::size_of::<ArenaCStr<'static>>() == std::mem::size_of::<*const u8>(),
"ArenaCStr must be a single thin pointer to match xmlChar*");
assert!(std::mem::size_of::<Option<ArenaCStr<'static>>>() == std::mem::size_of::<*const u8>(),
"Option<ArenaCStr> must niche-opt to a single pointer");
assert!(offset_of!(Namespace<'static>, next) == 0, "Namespace::next @ 0");
assert!(offset_of!(Namespace<'static>, kind) == 8, "Namespace::kind @ 8");
assert!(offset_of!(Namespace<'static>, href) == 16, "Namespace::href @ 16");
assert!(offset_of!(Namespace<'static>, prefix) == 24, "Namespace::prefix @ 24");
assert!(offset_of!(Namespace<'static>, _private) == 32, "Namespace::_private @ 32");
assert!(offset_of!(Namespace<'static>, context) == 40, "Namespace::context @ 40");
assert!(std::mem::size_of::<Namespace<'static>>() == 48, "sizeof(Namespace) == 48");
};
impl<'doc> Node<'doc> {
pub fn is_element(&self) -> bool { matches!(self.kind, NodeKind::Element) }
pub fn is_text(&self) -> bool { matches!(self.kind, NodeKind::Text) }
pub fn is_entity_ref(&self) -> bool { matches!(self.kind, NodeKind::EntityRef) }
#[inline]
pub fn name(&self) -> &'doc str {
#[cfg(not(feature = "c-abi"))]
{ self.name }
#[cfg(feature = "c-abi")]
{ self.name.get().as_str() }
}
#[inline]
pub fn line_no(&self) -> u32 {
#[cfg(not(feature = "c-abi"))]
{ self.line }
#[cfg(feature = "c-abi")]
{ self.line.get() as u32 }
}
#[inline]
pub fn content(&self) -> &'doc str {
self.content_opt().unwrap_or("")
}
pub fn content_opt(&self) -> Option<&'doc str> {
#[cfg(not(feature = "c-abi"))]
{ self.content.get() }
#[cfg(feature = "c-abi")]
{ self.content.get().map(|c| c.as_str()) }
}
pub fn set_content(&self, arena: &'doc DocumentBuilder, content: &'doc str) {
#[cfg(not(feature = "c-abi"))]
{
let _ = arena; self.content.set(Some(content));
}
#[cfg(feature = "c-abi")]
{
self.content.set(Some(arena.alloc_arena_cstr(content)));
}
}
pub fn children(&self) -> ChildIter<'doc> {
ChildIter { cur: self.first_child.get() }
}
pub fn attributes(&self) -> AttrIter<'doc> {
AttrIter { cur: self.first_attribute.get() }
}
#[inline]
pub fn local_name(&self) -> &'doc str {
let n = self.name();
match n.rfind(':') {
Some(i) => &n[i + 1..],
None => n,
}
}
pub fn ns_declarations(&self) -> NsDeclIter<'doc> {
#[cfg(feature = "c-abi")]
{
NsDeclIter::CAbi { cur: self.ns_def.get(), attrs: self.first_attribute.get() }
}
#[cfg(not(feature = "c-abi"))]
{
NsDeclIter::Lean { cur: self.first_attribute.get() }
}
}
pub fn find_child(&self, name: &str) -> Option<&'doc Node<'doc>> {
self.children().find(|n| n.is_element() && n.name() == name)
}
pub fn text_content(&self) -> Option<&'doc str> {
match self.kind {
NodeKind::Text | NodeKind::CData => Some(self.content()),
NodeKind::Element => self.children().find_map(|c| match c.kind {
NodeKind::Text | NodeKind::CData => Some(c.content()),
_ => None,
}),
_ => None,
}
}
}
impl std::fmt::Debug for Node<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("Node");
s.field("kind", &self.kind);
let name = self.name();
if !name.is_empty() { s.field("name", &name); }
let content = self.content();
if !content.is_empty() { s.field("content", &content); }
s.finish()
}
}
pub struct ChildIter<'doc> { cur: Option<&'doc Node<'doc>> }
impl<'doc> ChildIter<'doc> {
#[inline]
pub fn from_head(head: Option<&'doc Node<'doc>>) -> Self {
Self { cur: head }
}
}
impl<'doc> Iterator for ChildIter<'doc> {
type Item = &'doc Node<'doc>;
fn next(&mut self) -> Option<Self::Item> {
let c = self.cur?;
self.cur = c.next_sibling.get();
Some(c)
}
}
pub struct AttrIter<'doc> { cur: Option<&'doc Attribute<'doc>> }
impl<'doc> Iterator for AttrIter<'doc> {
type Item = &'doc Attribute<'doc>;
fn next(&mut self) -> Option<Self::Item> {
let a = self.cur?;
self.cur = a.next.get();
Some(a)
}
}
pub enum NsDeclIter<'doc> {
#[cfg(feature = "c-abi")]
CAbi {
cur: Option<&'doc Namespace<'doc>>,
attrs: Option<&'doc Attribute<'doc>>,
},
#[cfg(not(feature = "c-abi"))]
Lean { cur: Option<&'doc Attribute<'doc>> },
}
#[inline]
fn next_xmlns_attr<'doc>(
cur: &mut Option<&'doc Attribute<'doc>>,
) -> Option<(Option<&'doc str>, &'doc str)> {
loop {
let a = (*cur)?;
*cur = a.next.get();
let n = a.name();
if n == "xmlns" {
return Some((None, a.value()));
} else if let Some(local) = n.strip_prefix("xmlns:") {
return Some((Some(local), a.value()));
}
}
}
impl<'doc> Iterator for NsDeclIter<'doc> {
type Item = (Option<&'doc str>, &'doc str);
fn next(&mut self) -> Option<Self::Item> {
match self {
#[cfg(feature = "c-abi")]
NsDeclIter::CAbi { cur, attrs } => {
if let Some(ns) = *cur {
*cur = ns.next.get();
return Some((ns.prefix(), ns.href()));
}
next_xmlns_attr(attrs)
}
#[cfg(not(feature = "c-abi"))]
NsDeclIter::Lean { cur } => next_xmlns_attr(cur),
}
}
}
pub struct DocumentBuilder {
bump: Arc<Bump>,
#[cfg(feature = "c-abi")]
dict: *mut crate::dict::Dict,
root: Cell<*const Node<'static>>,
version: std::cell::RefCell<String>,
encoding: std::cell::RefCell<String>,
standalone: Cell<Option<bool>>,
base_url: std::cell::RefCell<Option<String>>,
html_metadata: std::cell::RefCell<Option<HtmlMeta>>,
source_ptr: Cell<*mut u8>,
source_len: Cell<usize>,
prolog_orphans: RefCell<Vec<*const Node<'static>>>,
epilogue_orphans: RefCell<Vec<*const Node<'static>>>,
}
impl DocumentBuilder {
pub fn new() -> Self {
Self {
bump: Arc::new(Bump::new()),
#[cfg(feature = "c-abi")]
dict: crate::dict::Dict::new_refcounted(),
root: Cell::new(std::ptr::null()),
version: std::cell::RefCell::new("1.0".to_string()),
encoding: std::cell::RefCell::new(String::new()),
standalone: Cell::new(None),
base_url: std::cell::RefCell::new(None),
html_metadata: std::cell::RefCell::new(None),
source_ptr: Cell::new(std::ptr::null_mut()),
source_len: Cell::new(0),
prolog_orphans: RefCell::new(Vec::new()),
epilogue_orphans: RefCell::new(Vec::new()),
}
}
#[cfg(feature = "c-abi")]
pub unsafe fn new_with_dict(dict: *mut crate::dict::Dict) -> Self {
unsafe { (*dict).add_ref(); }
Self {
bump: Arc::new(Bump::new()),
dict,
root: Cell::new(std::ptr::null()),
version: std::cell::RefCell::new("1.0".to_string()),
encoding: std::cell::RefCell::new(String::new()),
standalone: Cell::new(None),
base_url: std::cell::RefCell::new(None),
html_metadata: std::cell::RefCell::new(None),
source_ptr: Cell::new(std::ptr::null_mut()),
source_len: Cell::new(0),
prolog_orphans: RefCell::new(Vec::new()),
epilogue_orphans: RefCell::new(Vec::new()),
}
}
#[cfg(feature = "c-abi")]
pub unsafe fn new_with_dict_and_arena(
dict: *mut crate::dict::Dict,
arena: Arc<Bump>,
) -> Self {
unsafe { (*dict).add_ref(); }
Self {
bump: arena,
dict,
root: Cell::new(std::ptr::null()),
version: std::cell::RefCell::new("1.0".to_string()),
encoding: std::cell::RefCell::new(String::new()),
standalone: Cell::new(None),
base_url: std::cell::RefCell::new(None),
html_metadata: std::cell::RefCell::new(None),
source_ptr: Cell::new(std::ptr::null_mut()),
source_len: Cell::new(0),
prolog_orphans: RefCell::new(Vec::new()),
epilogue_orphans: RefCell::new(Vec::new()),
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
bump: Arc::new(Bump::with_capacity(capacity)),
#[cfg(feature = "c-abi")]
dict: crate::dict::Dict::new_refcounted(),
root: Cell::new(std::ptr::null()),
version: std::cell::RefCell::new("1.0".to_string()),
encoding: std::cell::RefCell::new(String::new()),
standalone: Cell::new(None),
base_url: std::cell::RefCell::new(None),
html_metadata: std::cell::RefCell::new(None),
source_ptr: Cell::new(std::ptr::null_mut()),
source_len: Cell::new(0),
prolog_orphans: RefCell::new(Vec::new()),
epilogue_orphans: RefCell::new(Vec::new()),
}
}
pub fn set_source(&self, bytes: Box<[u8]>) {
self.free_source();
let leaked: &'static mut [u8] = Box::leak(bytes);
self.source_ptr.set(leaked.as_mut_ptr());
self.source_len.set(leaked.len());
}
fn free_source(&self) {
let ptr = self.source_ptr.get();
let len = self.source_len.get();
if !ptr.is_null() {
unsafe {
let _: Box<[u8]> = Box::from_raw(
std::slice::from_raw_parts_mut(ptr, len) as *mut [u8]
);
}
self.source_ptr.set(std::ptr::null_mut());
self.source_len.set(0);
}
}
#[doc(hidden)]
pub fn source_ptr_for_inplace(&self) -> *mut u8 {
self.source_ptr.get()
}
#[doc(hidden)]
pub fn source_len_for_inplace(&self) -> usize {
self.source_len.get()
}
pub fn source(&self) -> Option<&[u8]> {
let ptr = self.source_ptr.get();
let len = self.source_len.get();
if ptr.is_null() {
None
} else {
Some(unsafe { std::slice::from_raw_parts(ptr, len) })
}
}
pub fn set_version(&self, v: impl Into<String>) {
*self.version.borrow_mut() = v.into();
}
pub fn set_encoding(&self, e: impl Into<String>) {
*self.encoding.borrow_mut() = e.into();
}
pub fn set_base_url(&self, uri: Option<String>) {
*self.base_url.borrow_mut() = uri;
}
pub fn set_standalone(&self, s: Option<bool>) {
self.standalone.set(s);
}
pub fn set_html_metadata(&self, m: Option<HtmlMeta>) {
*self.html_metadata.borrow_mut() = m;
}
pub fn bump(&self) -> &Bump { &self.bump }
pub fn alloc_str<'a>(&'a self, s: &str) -> &'a str {
self.bump.alloc_str(s)
}
pub unsafe fn alloc_str_borrow<'a>(&'a self, s: &'a str) -> &'a str {
unsafe { &*(s as *const str) }
}
fn new_node_impl<'a>(
&'a self,
kind: NodeKind,
name: &'a str,
content: Option<&'a str>,
) -> &'a mut Node<'a> {
#[cfg(not(feature = "c-abi"))]
{
self.bump.alloc(Node {
kind,
name,
namespace: Cell::new(None),
first_attribute: Cell::new(None),
last_attribute: Cell::new(None),
first_child: Cell::new(None),
last_child: Cell::new(None),
parent: Cell::new(None),
next_sibling: Cell::new(None),
prev_sibling: Cell::new(None),
content: Cell::new(content),
line: 0,
source_offset: 0,
})
}
#[cfg(feature = "c-abi")]
{
let name_c = match kind {
NodeKind::Text | NodeKind::CData => ArenaCStr::text_name(),
_ if name.is_empty() => ArenaCStr::empty(),
_ => self.intern_arena_cstr(name),
};
let content_c = content.map(|c| if c.is_empty() { ArenaCStr::empty() } else { self.alloc_arena_cstr(c) });
self.bump.alloc(Node {
_private: Cell::new(std::ptr::null_mut()),
kind,
_pad_kind: 0,
name: Cell::new(name_c),
first_child: Cell::new(None),
last_child: Cell::new(None),
parent: Cell::new(None),
next_sibling: Cell::new(None),
prev_sibling: Cell::new(None),
doc: Cell::new(std::ptr::null_mut()),
namespace: Cell::new(None),
content: Cell::new(content_c),
first_attribute: Cell::new(None),
ns_def: Cell::new(None),
psvi: Cell::new(std::ptr::null_mut()),
line: Cell::new(0),
extra: 0,
_pad_extra: [0u8; 4],
last_attribute: Cell::new(None),
full_line: Cell::new(0),
source_offset: 0,
})
}
}
pub fn new_element<'a>(&'a self, name: &'a str) -> &'a mut Node<'a> {
self.new_node_impl(NodeKind::Element, name, None)
}
pub fn new_text<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
self.new_node_impl(NodeKind::Text, "", Some(content))
}
pub fn new_cdata<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
self.new_node_impl(NodeKind::CData, "", Some(content))
}
pub fn new_comment<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
self.new_node_impl(NodeKind::Comment, "", Some(content))
}
pub fn new_pi<'a>(&'a self, target: &'a str, content: Option<&'a str>) -> &'a mut Node<'a> {
self.new_node_impl(NodeKind::Pi, target, content)
}
pub fn new_dtd_decl<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
self.new_node_impl(NodeKind::DtdDecl, "", Some(content))
}
pub fn new_fragment<'a>(&'a self) -> &'a mut Node<'a> {
self.new_node_impl(NodeKind::DocumentFragment, "", None)
}
pub fn new_entity_ref<'a>(&'a self, name: &'a str, content: &'a str) -> &'a mut Node<'a> {
self.new_node_impl(NodeKind::EntityRef, name, Some(content))
}
pub fn new_attribute<'a>(&'a self, name: &'a str, value: &'a str) -> &'a mut Attribute<'a> {
#[cfg(not(feature = "c-abi"))]
{
self.bump.alloc(Attribute {
name,
namespace: Cell::new(None),
value,
next: Cell::new(None),
prev: Cell::new(None),
parent: Cell::new(None),
})
}
#[cfg(feature = "c-abi")]
{
let text_child: &Node = self.new_text(value);
self.bump.alloc(Attribute {
_private: Cell::new(std::ptr::null_mut()),
kind: NodeKind::Attribute,
_pad_kind: 0,
name: self.intern_arena_cstr(name),
children: Cell::new(Some(text_child)),
last: Cell::new(Some(text_child)),
parent: Cell::new(None),
next: Cell::new(None),
prev: Cell::new(None),
doc: Cell::new(std::ptr::null_mut()),
namespace: Cell::new(None),
atype: 0,
_pad_atype: 0,
psvi: Cell::new(std::ptr::null_mut()),
value: self.alloc_arena_cstr(value),
})
}
}
#[cfg(feature = "c-abi")]
pub fn append_ns_def<'a>(&'a self, el: &'a Node<'a>, ns: &'a Namespace<'a>) {
match el.ns_def.get() {
None => el.ns_def.set(Some(ns)),
Some(head) => {
let mut cur = head;
while let Some(n) = cur.next.get() {
cur = n;
}
cur.next.set(Some(ns));
}
}
}
pub fn new_namespace<'a>(&'a self, prefix: Option<&'a str>, href: &'a str) -> &'a Namespace<'a> {
#[cfg(not(feature = "c-abi"))]
{
self.bump.alloc(Namespace { prefix, href })
}
#[cfg(feature = "c-abi")]
{
let href_c = self.intern_arena_cstr(href);
let prefix_c = prefix.map(|p| self.intern_arena_cstr(p));
self.bump.alloc(Namespace {
next: Cell::new(None),
kind: 18, _pad_kind: 0,
href: href_c,
prefix: prefix_c,
_private: Cell::new(std::ptr::null_mut()),
context: Cell::new(std::ptr::null_mut()),
})
}
}
#[cfg(feature = "c-abi")]
fn alloc_arena_cstr<'a>(&'a self, s: &str) -> ArenaCStr<'a> {
let bytes = s.as_bytes();
let dst: &mut [u8] = self.bump.alloc_slice_fill_with(bytes.len() + 1, |i| {
if i < bytes.len() { bytes[i] } else { 0 }
});
unsafe { ArenaCStr::from_raw(dst.as_ptr()) }
}
#[cfg(feature = "c-abi")]
fn intern_arena_cstr<'a>(&'a self, s: &str) -> ArenaCStr<'a> {
let ptr = unsafe { (*self.dict).intern_str(s) };
unsafe { ArenaCStr::from_raw(ptr) }
}
pub fn append_child<'a>(&'a self, parent: &'a Node<'a>, child: &'a Node<'a>) {
debug_assert!(child.parent.get().is_none(), "append_child: child is already attached");
child.parent.set(Some(parent));
match parent.last_child.get() {
None => {
parent.first_child.set(Some(child));
parent.last_child.set(Some(child));
}
Some(prev_last) => {
prev_last.next_sibling.set(Some(child));
child.prev_sibling.set(Some(prev_last));
parent.last_child.set(Some(child));
}
}
}
pub fn append_attribute<'a>(&'a self, element: &'a Node<'a>, attr: &'a Attribute<'a>) {
debug_assert!(element.is_element(), "append_attribute: not an element");
debug_assert!(attr.parent.get().is_none(), "append_attribute: attr already attached");
attr.parent.set(Some(element));
match element.last_attribute.get() {
None => {
element.first_attribute.set(Some(attr));
element.last_attribute.set(Some(attr));
}
Some(prev_last) => {
prev_last.next.set(Some(attr));
attr.prev.set(Some(prev_last));
element.last_attribute.set(Some(attr));
}
}
}
pub fn detach<'a>(&'a self, child: &'a Node<'a>) {
let Some(parent) = child.parent.get() else { return; };
let prev = child.prev_sibling.get();
let next = child.next_sibling.get();
match prev {
Some(p) => p.next_sibling.set(next),
None => parent.first_child.set(next),
}
match next {
Some(n) => n.prev_sibling.set(prev),
None => parent.last_child.set(prev),
}
child.parent.set(None);
child.prev_sibling.set(None);
child.next_sibling.set(None);
}
pub fn attach_prolog_orphan<'a>(&'a self, node: &'a Node<'a>) {
let p: *const Node<'static> = node as *const _ as *const Node<'static>;
self.prolog_orphans.borrow_mut().push(p);
}
pub fn attach_epilogue_orphan<'a>(&'a self, node: &'a Node<'a>) {
let p: *const Node<'static> = node as *const _ as *const Node<'static>;
self.epilogue_orphans.borrow_mut().push(p);
}
pub fn set_root<'a>(&'a self, root: &'a Node<'a>) {
let p: *const Node<'static> = root as *const _ as *const Node<'static>;
self.root.set(p);
}
pub fn build(self) -> Document {
let root = self.root.get();
assert!(!root.is_null(),
"DocumentBuilder::build called without set_root — call set_root(node) first");
let first_sibling: *const Node<'static> = unsafe {
link_doc_level_orphans(
root,
&self.prolog_orphans.borrow(),
&self.epilogue_orphans.borrow(),
)
};
let s = std::mem::ManuallyDrop::new(self);
let source_ptr = s.source_ptr.get();
let source_len = s.source_len.get();
let standalone = s.standalone.get();
let bump = unsafe { std::ptr::read(&s.bump) };
let version = unsafe { std::ptr::read(&s.version) }.into_inner();
let encoding = unsafe { std::ptr::read(&s.encoding) }.into_inner();
let html_metadata = unsafe { std::ptr::read(&s.html_metadata) }.into_inner();
let base_url = unsafe { std::ptr::read(&s.base_url) }.into_inner();
let _prolog_orphans: std::cell::RefCell<Vec<*const Node<'static>>>
= unsafe { std::ptr::read(&s.prolog_orphans) };
let _epilogue_orphans: std::cell::RefCell<Vec<*const Node<'static>>>
= unsafe { std::ptr::read(&s.epilogue_orphans) };
#[cfg(feature = "c-abi")]
let dict = s.dict;
Document {
bump,
#[cfg(feature = "c-abi")]
#[cfg(feature = "c-abi")]
dict,
root,
first_sibling,
version,
encoding,
standalone,
html_metadata,
source_ptr,
source_len,
unparsed_entities: std::sync::Arc::new(std::collections::HashMap::new()),
id_attributes: std::sync::Arc::new(std::collections::HashMap::new()),
idref_attributes: std::sync::Arc::new(std::collections::HashMap::new()),
base_url,
}
}
}
unsafe fn link_doc_level_orphans(
root: *const Node<'static>,
prolog: &[*const Node<'static>],
epilogue: &[*const Node<'static>],
) -> *const Node<'static> {
if prolog.is_empty() && epilogue.is_empty() {
return root;
}
let root_ref: &Node<'static> = unsafe { &*root };
let mut prev_node: Option<&Node<'static>> = None;
for &p in prolog {
let n: &Node<'static> = unsafe { &*p };
if let Some(pv) = prev_node {
n.prev_sibling.set(Some(pv));
pv.next_sibling.set(Some(n));
}
prev_node = Some(n);
}
if let Some(last_prolog) = prev_node {
last_prolog.next_sibling.set(Some(root_ref));
root_ref.prev_sibling.set(Some(last_prolog));
}
let mut prev_node = Some(root_ref);
for &p in epilogue {
let n: &Node<'static> = unsafe { &*p };
if let Some(pv) = prev_node {
pv.next_sibling.set(Some(n));
n.prev_sibling.set(Some(pv));
}
prev_node = Some(n);
}
if let Some(&first) = prolog.first() { first } else { root }
}
impl Default for DocumentBuilder {
fn default() -> Self { Self::new() }
}
impl Drop for DocumentBuilder {
fn drop(&mut self) {
self.free_source();
#[cfg(feature = "c-abi")]
unsafe {
if !self.dict.is_null() {
crate::dict::Dict::release(self.dict);
}
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct UnparsedEntity {
pub system_id: String,
pub public_id: Option<String>,
}
pub struct Document {
root: *const Node<'static>,
first_sibling: *const Node<'static>,
bump: Arc<Bump>,
#[cfg(feature = "c-abi")]
dict: *mut crate::dict::Dict,
pub version: String,
pub encoding: String,
pub standalone: Option<bool>,
pub html_metadata: Option<HtmlMeta>,
source_ptr: *mut u8,
source_len: usize,
unparsed_entities: std::sync::Arc<std::collections::HashMap<String, UnparsedEntity>>,
id_attributes: std::sync::Arc<std::collections::HashMap<String, Vec<String>>>,
idref_attributes: std::sync::Arc<std::collections::HashMap<String, Vec<String>>>,
base_url: Option<String>,
}
unsafe impl Send for Document {}
impl Drop for Document {
fn drop(&mut self) {
if !self.source_ptr.is_null() {
unsafe {
let _: Box<[u8]> = Box::from_raw(
std::slice::from_raw_parts_mut(self.source_ptr, self.source_len)
as *mut [u8]
);
}
}
#[cfg(feature = "c-abi")]
unsafe {
if !self.dict.is_null() {
crate::dict::Dict::release(self.dict);
}
}
}
}
impl Document {
pub fn source(&self) -> Option<&[u8]> {
if self.source_ptr.is_null() {
None
} else {
Some(unsafe { std::slice::from_raw_parts(self.source_ptr, self.source_len) })
}
}
pub fn root<'a>(&'a self) -> &'a Node<'a> {
unsafe { &*(self.root as *const Node<'a>) }
}
pub fn memory_bytes(&self) -> usize {
self.bump.allocated_bytes()
}
pub fn unparsed_entity_uri(&self, name: &str) -> Option<&str> {
self.unparsed_entities.get(name).map(|e| e.system_id.as_str())
}
pub fn unparsed_entity_public_id(&self, name: &str) -> Option<&str> {
self.unparsed_entities.get(name)
.and_then(|e| e.public_id.as_deref())
}
pub fn node_string_value_by_ptr(ptr: *const Node<'static>) -> String {
if ptr.is_null() { return String::new(); }
let n: &Node<'_> = unsafe { &*(ptr as *const Node<'_>) };
n.content().to_string()
}
pub fn set_node_text_content_by_ptr(&self, node_ptr: *const Node<'static>, content: &str) {
if node_ptr.is_null() { return; }
let node: &Node<'_> = unsafe { &*(node_ptr as *const Node<'_>) };
self.set_node_text_content(node, content);
}
pub fn set_node_text_content<'a>(&'a self, node: &'a Node<'a>, content: &str) {
#[cfg(not(feature = "c-abi"))]
{
let s: &'a str = self.bump.alloc_str(content);
node.content.set(Some(s));
}
#[cfg(feature = "c-abi")]
{
let dst: &mut [u8] = self.bump.alloc_slice_fill_with(content.len() + 1, |i| {
if i < content.len() { content.as_bytes()[i] } else { 0 }
});
let new_content = unsafe { ArenaCStr::from_raw(dst.as_ptr()) };
node.content.set(Some(new_content));
}
}
pub fn unparsed_entities(&self) -> &std::sync::Arc<std::collections::HashMap<String, UnparsedEntity>> {
&self.unparsed_entities
}
#[doc(hidden)]
pub fn set_unparsed_entities(
&mut self,
map: std::collections::HashMap<String, UnparsedEntity>,
) {
self.unparsed_entities = std::sync::Arc::new(map);
}
pub fn id_attributes(
&self,
) -> &std::sync::Arc<std::collections::HashMap<String, Vec<String>>> {
&self.id_attributes
}
#[doc(hidden)]
pub fn set_id_attributes(
&mut self,
map: std::collections::HashMap<String, Vec<String>>,
) {
self.id_attributes = std::sync::Arc::new(map);
}
pub fn idref_attributes(
&self,
) -> &std::sync::Arc<std::collections::HashMap<String, Vec<String>>> {
&self.idref_attributes
}
#[doc(hidden)]
pub fn set_idref_attributes(
&mut self,
map: std::collections::HashMap<String, Vec<String>>,
) {
self.idref_attributes = std::sync::Arc::new(map);
}
pub fn base_url(&self) -> Option<&str> {
self.base_url.as_deref()
}
#[doc(hidden)]
pub fn set_base_url(&mut self, uri: Option<String>) {
self.base_url = uri;
}
pub fn first_sibling<'a>(&'a self) -> &'a Node<'a> {
let p = if self.first_sibling.is_null() { self.root } else { self.first_sibling };
unsafe { &*(p as *const Node<'a>) }
}
pub fn is_html(&self) -> bool {
self.html_metadata.is_some()
}
pub unsafe fn set_root_ptr(&mut self, node: *const Node<'static>) {
self.root = node;
}
pub unsafe fn set_first_sibling_ptr(&mut self, node: *const Node<'static>) {
self.first_sibling = node;
}
pub fn bump(&self) -> &Bump {
&self.bump
}
#[cfg(feature = "c-abi")]
pub fn bump_arc(&self) -> Arc<Bump> {
Arc::clone(&self.bump)
}
pub fn new_element<'a>(&'a self, name: &'a str) -> &'a mut Node<'a> {
self.alloc_node(NodeKind::Element, name, None)
}
pub fn new_text<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
self.alloc_node(NodeKind::Text, "", Some(content))
}
pub fn new_cdata<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
self.alloc_node(NodeKind::CData, "", Some(content))
}
pub fn new_comment<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
self.alloc_node(NodeKind::Comment, "", Some(content))
}
pub fn new_pi<'a>(&'a self, target: &'a str, content: Option<&'a str>) -> &'a mut Node<'a> {
self.alloc_node(NodeKind::Pi, target, content)
}
pub fn new_dtd_decl<'a>(&'a self, content: &'a str) -> &'a mut Node<'a> {
self.alloc_node(NodeKind::DtdDecl, "", Some(content))
}
pub fn new_fragment<'a>(&'a self) -> &'a mut Node<'a> {
self.alloc_node(NodeKind::DocumentFragment, "", None)
}
pub fn new_entity_ref<'a>(&'a self, name: &'a str, content: &'a str) -> &'a mut Node<'a> {
self.alloc_node(NodeKind::EntityRef, name, Some(content))
}
pub fn adopt_subtree<'a>(&'a self, foreign: &Node<'_>) -> &'a Node<'a> {
self.adopt_node_inner(foreign)
}
fn adopt_node_inner<'a>(&'a self, src: &Node<'_>) -> &'a Node<'a> {
let copy: &Node<'a> = match src.kind {
NodeKind::Element => {
let name = self.bump().alloc_str(src.name());
let new_el = self.new_element(name);
for attr in src.attributes() {
let aname = self.bump().alloc_str(attr.name());
let aval = self.bump().alloc_str(attr.value());
let new_attr = self.new_attribute(aname, aval);
self.append_attribute(new_el, new_attr);
}
new_el
}
NodeKind::Text => {
let content = self.bump().alloc_str(src.content());
self.new_text(content)
}
NodeKind::CData => {
let content = self.bump().alloc_str(src.content());
self.new_cdata(content)
}
NodeKind::Comment => {
let content = self.bump().alloc_str(src.content());
self.new_comment(content)
}
NodeKind::Pi => {
let name = self.bump().alloc_str(src.name());
let content = src.content_opt().map(|c| &*self.bump().alloc_str(c));
self.new_pi(name, content)
}
NodeKind::EntityRef => {
let name = self.bump().alloc_str(src.name());
let content = self.bump().alloc_str(src.content());
self.new_entity_ref(name, content)
}
NodeKind::DtdDecl => {
let content = self.bump().alloc_str(src.content());
self.new_dtd_decl(content)
}
NodeKind::DocumentFragment => self.new_fragment(),
NodeKind::Attribute => unreachable!(
"adopt_subtree: NodeKind::Attribute is not a Node — use new_attribute directly"
),
NodeKind::Document => unreachable!(
"adopt_subtree: NodeKind::Document marker never appears on a Node — pass the root element instead"
),
NodeKind::Dtd => unreachable!(
"adopt_subtree: NodeKind::Dtd is a compat-shim sibling node (xmlDtd), never an arena Node reached by a subtree copy"
),
};
if matches!(src.kind, NodeKind::Element | NodeKind::DocumentFragment) {
for child in src.children() {
let child_copy = self.adopt_node_inner(child);
self.append_child(copy, child_copy);
}
}
copy
}
pub fn new_attribute<'a>(&'a self, name: &'a str, value: &'a str) -> &'a mut Attribute<'a> {
#[cfg(not(feature = "c-abi"))]
{
self.bump.alloc(Attribute {
name, value,
namespace: Cell::new(None),
next: Cell::new(None),
prev: Cell::new(None),
parent: Cell::new(None),
})
}
#[cfg(feature = "c-abi")]
{
let text_child: &Node = self.new_text(value);
let name_c = self.intern_arena_cstr(name);
let value_c = self.alloc_arena_cstr(value);
self.bump.alloc(Attribute {
_private: Cell::new(std::ptr::null_mut()),
kind: NodeKind::Attribute,
_pad_kind: 0,
name: name_c,
children: Cell::new(Some(text_child)),
last: Cell::new(Some(text_child)),
parent: Cell::new(None),
next: Cell::new(None),
prev: Cell::new(None),
doc: Cell::new(std::ptr::null_mut()),
namespace: Cell::new(None),
atype: 0,
_pad_atype: 0,
psvi: Cell::new(std::ptr::null_mut()),
value: value_c,
})
}
}
pub fn append_child<'a>(&'a self, parent: &'a Node<'a>, child: &'a Node<'a>) {
debug_assert!(child.parent.get().is_none(),
"Document::append_child: child is already attached");
child.parent.set(Some(parent));
match parent.last_child.get() {
None => {
parent.first_child.set(Some(child));
parent.last_child.set(Some(child));
}
Some(last) => {
last.next_sibling.set(Some(child));
child.prev_sibling.set(Some(last));
parent.last_child.set(Some(child));
}
}
}
#[cfg(feature = "c-abi")]
pub fn bump_new_namespace<'a>(
&'a self,
prefix: Option<&'a str>,
href: &'a str,
) -> &'a Namespace<'a> {
let href_c = self.alloc_arena_cstr(href);
let prefix_c = prefix.map(|p| self.alloc_arena_cstr(p));
self.bump.alloc(Namespace {
next: Cell::new(None),
kind: 18, _pad_kind: 0,
href: href_c,
prefix: prefix_c,
_private: Cell::new(std::ptr::null_mut()),
context: Cell::new(std::ptr::null_mut()),
})
}
#[cfg(feature = "c-abi")]
pub fn attach_ns_def<'a>(&'a self, element: &'a Node<'a>, ns: &'a Namespace<'a>) {
match element.ns_def.get() {
None => element.ns_def.set(Some(ns)),
Some(head) => {
let mut cur = head;
while let Some(n) = cur.next.get() {
cur = n;
}
cur.next.set(Some(ns));
}
}
}
pub fn append_attribute<'a>(&'a self, element: &'a Node<'a>, attr: &'a Attribute<'a>) {
debug_assert!(element.is_element(),
"Document::append_attribute: not an element");
debug_assert!(attr.parent.get().is_none(),
"Document::append_attribute: attr already attached");
attr.parent.set(Some(element));
match element.last_attribute.get() {
None => {
element.first_attribute.set(Some(attr));
element.last_attribute.set(Some(attr));
}
Some(last) => {
last.next.set(Some(attr));
attr.prev.set(Some(last));
element.last_attribute.set(Some(attr));
}
}
}
fn alloc_node<'a>(
&'a self,
kind: NodeKind,
name: &'a str,
content: Option<&'a str>,
) -> &'a mut Node<'a> {
#[cfg(not(feature = "c-abi"))]
{
self.bump.alloc(Node {
kind,
name,
namespace: Cell::new(None),
first_attribute: Cell::new(None),
last_attribute: Cell::new(None),
first_child: Cell::new(None),
last_child: Cell::new(None),
parent: Cell::new(None),
next_sibling: Cell::new(None),
prev_sibling: Cell::new(None),
content: Cell::new(content),
line: 0,
source_offset: 0,
})
}
#[cfg(feature = "c-abi")]
{
let name_c = match kind {
NodeKind::Text | NodeKind::CData => ArenaCStr::text_name(),
_ if name.is_empty() => ArenaCStr::empty(),
_ => self.intern_arena_cstr(name),
};
let content_c = content.map(|c| if c.is_empty() { ArenaCStr::empty() } else { self.alloc_arena_cstr(c) });
self.bump.alloc(Node {
_private: Cell::new(std::ptr::null_mut()),
kind,
_pad_kind: 0,
name: Cell::new(name_c),
first_child: Cell::new(None),
last_child: Cell::new(None),
parent: Cell::new(None),
next_sibling: Cell::new(None),
prev_sibling: Cell::new(None),
doc: Cell::new(std::ptr::null_mut()),
namespace: Cell::new(None),
content: Cell::new(content_c),
first_attribute: Cell::new(None),
ns_def: Cell::new(None),
psvi: Cell::new(std::ptr::null_mut()),
line: Cell::new(0),
extra: 0,
_pad_extra: [0u8; 4],
last_attribute: Cell::new(None),
full_line: Cell::new(0),
source_offset: 0,
})
}
}
#[cfg(feature = "c-abi")]
fn alloc_arena_cstr<'a>(&'a self, s: &str) -> ArenaCStr<'a> {
let bytes = s.as_bytes();
let dst: &mut [u8] = self.bump.alloc_slice_fill_with(bytes.len() + 1, |i| {
if i < bytes.len() { bytes[i] } else { 0 }
});
unsafe { ArenaCStr::from_raw(dst.as_ptr()) }
}
#[cfg(feature = "c-abi")]
fn intern_arena_cstr<'a>(&'a self, s: &str) -> ArenaCStr<'a> {
let ptr = unsafe { (*self.dict).intern_str(s) };
unsafe { ArenaCStr::from_raw(ptr) }
}
#[cfg(feature = "c-abi")]
pub fn dict_ptr(&self) -> *mut crate::dict::Dict {
self.dict
}
}
impl std::fmt::Debug for Document {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Document")
.field("root", self.root())
.field("memory_bytes", &self.memory_bytes())
.finish()
}
}
#[cfg(feature = "c-abi")]
#[repr(C)]
pub struct XmlDoc {
pub _private: Cell<*mut std::os::raw::c_void>, pub kind: NodeKind, _pad_kind: u32, pub name: *const std::os::raw::c_char, pub children: Cell<*mut Node<'static>>, pub last: Cell<*mut Node<'static>>, pub parent: Cell<*mut Node<'static>>, pub next: Cell<*mut XmlDoc>, pub prev: Cell<*mut XmlDoc>, pub doc: Cell<*mut XmlDoc>, pub compression: i32, pub standalone: i32, pub int_subset: *mut std::os::raw::c_void, pub ext_subset: *mut std::os::raw::c_void, pub old_ns: *mut std::os::raw::c_void, pub version: ArenaCStr<'static>, pub encoding: Option<ArenaCStr<'static>>, pub ids: *mut std::os::raw::c_void, pub refs: *mut std::os::raw::c_void, pub url: *const std::os::raw::c_char, pub charset: i32, _pad_charset: u32, pub dict: *mut std::os::raw::c_void, pub psvi: *mut std::os::raw::c_void, pub parse_flags: i32, pub properties: i32, pub _doc: std::mem::ManuallyDrop<Document>,
}
#[cfg(feature = "c-abi")]
const _: () = {
use std::mem::offset_of;
assert!(offset_of!(XmlDoc, _private) == 0, "XmlDoc::_private @ 0");
assert!(offset_of!(XmlDoc, kind) == 8, "XmlDoc::kind @ 8");
assert!(offset_of!(XmlDoc, name) == 16, "XmlDoc::name @ 16");
assert!(offset_of!(XmlDoc, children) == 24, "XmlDoc::children @ 24");
assert!(offset_of!(XmlDoc, last) == 32, "XmlDoc::last @ 32");
assert!(offset_of!(XmlDoc, parent) == 40, "XmlDoc::parent @ 40");
assert!(offset_of!(XmlDoc, next) == 48, "XmlDoc::next @ 48");
assert!(offset_of!(XmlDoc, prev) == 56, "XmlDoc::prev @ 56");
assert!(offset_of!(XmlDoc, doc) == 64, "XmlDoc::doc @ 64");
assert!(offset_of!(XmlDoc, compression) == 72, "XmlDoc::compression @ 72");
assert!(offset_of!(XmlDoc, standalone) == 76, "XmlDoc::standalone @ 76");
assert!(offset_of!(XmlDoc, int_subset) == 80, "XmlDoc::int_subset @ 80");
assert!(offset_of!(XmlDoc, ext_subset) == 88, "XmlDoc::ext_subset @ 88");
assert!(offset_of!(XmlDoc, old_ns) == 96, "XmlDoc::old_ns @ 96");
assert!(offset_of!(XmlDoc, version) == 104, "XmlDoc::version @ 104");
assert!(offset_of!(XmlDoc, encoding) == 112, "XmlDoc::encoding @ 112");
assert!(offset_of!(XmlDoc, ids) == 120, "XmlDoc::ids @ 120");
assert!(offset_of!(XmlDoc, refs) == 128, "XmlDoc::refs @ 128");
assert!(offset_of!(XmlDoc, url) == 136, "XmlDoc::url @ 136");
assert!(offset_of!(XmlDoc, charset) == 144, "XmlDoc::charset @ 144");
assert!(offset_of!(XmlDoc, dict) == 152, "XmlDoc::dict @ 152");
assert!(offset_of!(XmlDoc, psvi) == 160, "XmlDoc::psvi @ 160");
assert!(offset_of!(XmlDoc, parse_flags) == 168, "XmlDoc::parse_flags @ 168");
assert!(offset_of!(XmlDoc, properties) == 172, "XmlDoc::properties @ 172");
assert!(offset_of!(XmlDoc, _doc) >= 176, "XmlDoc tail starts after ABI window");
};
#[cfg(feature = "c-abi")]
impl Document {
pub fn into_xml_doc(self) -> *mut XmlDoc {
let version_str = self.version.clone();
let encoding_str = self.encoding.clone();
let standalone_i32 = match self.standalone {
None => -1,
Some(true) => 1,
Some(false) => 0,
};
let root_ptr: *mut Node<'static> =
self.root as *const Node<'static> as *mut Node<'static>;
let first_child_ptr: *mut Node<'static> = if self.first_sibling.is_null() {
root_ptr
} else {
self.first_sibling as *const Node<'static> as *mut Node<'static>
};
let last_child_ptr: *mut Node<'static> = {
let mut cur: &Node<'static> = unsafe { &*first_child_ptr };
while let Some(next) = cur.next_sibling.get() {
let next_ptr = next as *const Node<'_> as *const Node<'static>;
cur = unsafe { &*next_ptr };
}
cur as *const Node<'_> as *mut Node<'static>
};
let dict_for_slot: *mut std::os::raw::c_void = {
let d = self.dict;
if !d.is_null() {
unsafe { (*d).add_ref(); }
}
d as *mut std::os::raw::c_void
};
let (version_c, encoding_c) = {
let bytes_v = version_str.as_bytes();
let dst_v: &mut [u8] = self.bump.alloc_slice_fill_with(bytes_v.len() + 1, |i| {
if i < bytes_v.len() { bytes_v[i] } else { 0 }
});
let encoding_c = if encoding_str.is_empty() {
None
} else {
let bytes_e = encoding_str.as_bytes();
let dst_e: &mut [u8] = self.bump.alloc_slice_fill_with(bytes_e.len() + 1, |i| {
if i < bytes_e.len() { bytes_e[i] } else { 0 }
});
Some(unsafe { ArenaCStr::from_raw(dst_e.as_ptr()) })
};
(unsafe { ArenaCStr::from_raw(dst_v.as_ptr()) }, encoding_c)
};
let boxed = Box::new(XmlDoc {
_private: Cell::new(std::ptr::null_mut()),
kind: NodeKind::Document,
_pad_kind: 0,
name: std::ptr::null(),
children: Cell::new(first_child_ptr),
last: Cell::new(last_child_ptr),
parent: Cell::new(std::ptr::null_mut()),
next: Cell::new(std::ptr::null_mut()),
prev: Cell::new(std::ptr::null_mut()),
doc: Cell::new(std::ptr::null_mut()),
compression: 0,
standalone: standalone_i32,
int_subset: std::ptr::null_mut(),
ext_subset: std::ptr::null_mut(),
old_ns: std::ptr::null_mut(),
version: version_c,
encoding: encoding_c,
ids: std::ptr::null_mut(),
refs: std::ptr::null_mut(),
url: std::ptr::null(),
charset: 1, _pad_charset: 0,
dict: dict_for_slot,
psvi: std::ptr::null_mut(),
parse_flags: 0,
properties: 0,
_doc: std::mem::ManuallyDrop::new(self),
});
let raw = Box::into_raw(boxed);
unsafe { (*raw).doc.set(raw); }
unsafe {
let owned = &*raw;
let raw_void = raw as *mut std::os::raw::c_void;
let mut stack: Vec<*const Node<'static>> = Vec::new();
stack.push(owned.children.get() as *const _);
let doc_as_node: &Node<'static> = &*(raw as *const Node<'static>);
let mut cur_sib: *const Node<'static> = owned.children.get() as *const _;
while !cur_sib.is_null() {
stack.push(cur_sib);
let s_ref = &*cur_sib;
s_ref.parent.set(Some(doc_as_node));
cur_sib = s_ref.next_sibling.get()
.map(|n| n as *const _)
.unwrap_or(std::ptr::null());
}
while let Some(np) = stack.pop() {
if np.is_null() { continue; }
let n = &*np;
n.doc.set(raw_void);
let mut a = n.first_attribute.get();
while let Some(attr) = a {
attr.doc.set(raw_void);
a = attr.next.get();
}
let mut c = n.first_child.get();
while let Some(ch) = c {
stack.push(ch as *const _);
c = ch.next_sibling.get();
}
}
}
raw
}
}
#[cfg(feature = "c-abi")]
impl XmlDoc {
pub unsafe fn free(ptr: *mut XmlDoc) {
if ptr.is_null() { return; }
let mut boxed = unsafe { Box::from_raw(ptr) };
if !boxed.url.is_null() {
unsafe { let _ = std::ffi::CString::from_raw(boxed.url as *mut std::os::raw::c_char); }
}
let dict_at_free = boxed.dict;
if !dict_at_free.is_null() {
unsafe { crate::dict::Dict::release(dict_at_free as *mut crate::dict::Dict); }
}
unsafe { std::mem::ManuallyDrop::drop(&mut boxed._doc); }
drop(boxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn node_kind_libxml2_values_match() {
assert_eq!(NodeKind::Element as u32, 1);
assert_eq!(NodeKind::Attribute as u32, 2);
assert_eq!(NodeKind::Text as u32, 3);
assert_eq!(NodeKind::CData as u32, 4);
assert_eq!(NodeKind::EntityRef as u32, 5);
assert_eq!(NodeKind::Pi as u32, 7);
assert_eq!(NodeKind::Comment as u32, 8);
assert_eq!(NodeKind::Document as u32, 9);
}
fn build_simple_tree() -> Document {
let b = DocumentBuilder::new();
let root = b.new_element(b.alloc_str("catalog"));
let book = b.new_element(b.alloc_str("book"));
let id = b.new_attribute(b.alloc_str("id"), b.alloc_str("1"));
b.append_attribute(book, id);
let title = b.new_element(b.alloc_str("title"));
let title_text = b.new_text(b.alloc_str("Dune"));
b.append_child(title, title_text);
b.append_child(book, title);
b.append_child(root, book);
b.set_root(root);
b.build()
}
#[test]
fn basic_tree_navigation() {
let doc = build_simple_tree();
let root = doc.root();
assert_eq!(root.name(), "catalog");
assert!(root.is_element());
let book = root.children().next().unwrap();
assert_eq!(book.name(), "book");
assert!(book.parent.get().is_some());
let title = book.find_child("title").unwrap();
assert_eq!(title.name(), "title");
assert_eq!(title.text_content(), Some("Dune"));
}
#[test]
fn attribute_iteration() {
let b = DocumentBuilder::new();
let el = b.new_element(b.alloc_str("el"));
for (n, v) in [("a", "1"), ("b", "2"), ("c", "3")] {
let attr = b.new_attribute(b.alloc_str(n), b.alloc_str(v));
b.append_attribute(el, attr);
}
b.set_root(el); let doc = b.build();
let pairs: Vec<(String, String)> = doc.root().attributes()
.map(|a| (a.name().to_owned(), a.value().to_owned()))
.collect();
assert_eq!(pairs, vec![
("a".into(), "1".into()),
("b".into(), "2".into()),
("c".into(), "3".into()),
]);
}
#[test]
fn sibling_links_are_doubly_threaded() {
let b = DocumentBuilder::new();
let root = b.new_element(b.alloc_str("r"));
for n in &["a", "b", "c"] {
let child = b.new_element(b.alloc_str(*n));
b.append_child(root, child);
}
b.set_root(root); let doc = b.build();
let r = doc.root();
let names_fwd: Vec<&str> = r.children().map(|c| c.name()).collect();
assert_eq!(names_fwd, vec!["a", "b", "c"]);
let mut names_back: Vec<&str> = Vec::new();
let mut cur = r.last_child.get();
while let Some(n) = cur {
names_back.push(n.name());
cur = n.prev_sibling.get();
}
assert_eq!(names_back, vec!["c", "b", "a"]);
for c in r.children() {
assert!(std::ptr::eq(c.parent.get().unwrap(), r));
}
}
#[test]
fn detach_middle_child_repairs_links() {
let b = DocumentBuilder::new();
let root = b.new_element(b.alloc_str("r"));
let a = b.new_element(b.alloc_str("a"));
let b_node = b.new_element(b.alloc_str("b"));
let c = b.new_element(b.alloc_str("c"));
b.append_child(root, a);
b.append_child(root, b_node);
b.append_child(root, c);
b.detach(b_node);
let names: Vec<&str> = root.children().map(|n| n.name()).collect();
assert_eq!(names, vec!["a", "c"]);
assert!(b_node.parent.get().is_none());
assert!(b_node.prev_sibling.get().is_none());
assert!(b_node.next_sibling.get().is_none());
assert!(std::ptr::eq(a.next_sibling.get().unwrap(), c));
assert!(std::ptr::eq(c.prev_sibling.get().unwrap(), a));
}
#[test]
fn detach_first_and_last() {
let b = DocumentBuilder::new();
let root = b.new_element(b.alloc_str("r"));
let a = b.new_element(b.alloc_str("a"));
let c = b.new_element(b.alloc_str("c"));
b.append_child(root, a);
b.append_child(root, c);
b.detach(a); assert!(std::ptr::eq(root.first_child.get().unwrap(), c));
assert!(root.last_child.get().is_some());
assert!(c.prev_sibling.get().is_none());
b.detach(c); assert!(root.first_child.get().is_none());
assert!(root.last_child.get().is_none());
}
#[test]
fn document_is_send() {
fn assert_send<T: Send>() {}
assert_send::<Document>();
}
#[test]
fn root_lifetime_bounded_by_document() {
let doc = build_simple_tree();
let root = doc.root();
assert_eq!(root.name(), "catalog");
drop(doc);
}
#[test]
fn mixed_content_children() {
let b = DocumentBuilder::new();
let root = b.new_element(b.alloc_str("r"));
b.append_child(root, b.new_text(b.alloc_str("before ")));
let em = b.new_element(b.alloc_str("em"));
b.append_child(em, b.new_text(b.alloc_str("middle")));
b.append_child(root, em);
b.append_child(root, b.new_text(b.alloc_str(" after")));
b.append_child(root, b.new_comment(b.alloc_str(" note ")));
b.append_child(root, b.new_cdata(b.alloc_str("<raw>")));
b.set_root(root); let doc = b.build();
let r = doc.root();
let kinds: Vec<NodeKind> = r.children().map(|c| c.kind).collect();
assert_eq!(kinds, vec![NodeKind::Text, NodeKind::Element, NodeKind::Text,
NodeKind::Comment, NodeKind::CData]);
assert_eq!(r.text_content(), Some("before "));
}
#[test]
fn namespace_attached_to_element() {
let b = DocumentBuilder::new();
let ns = b.new_namespace(Some(b.alloc_str("dc")),
b.alloc_str("http://purl.org/dc/elements/1.1/"));
let el = b.new_element(b.alloc_str("dc:title"));
el.namespace.set(Some(ns));
b.set_root(el); let doc = b.build();
let ns_got = doc.root().namespace.get().unwrap();
assert_eq!(ns_got.prefix(), Some("dc"));
assert_eq!(ns_got.href(), "http://purl.org/dc/elements/1.1/");
}
#[test]
fn memory_bytes_grows_with_alloc() {
let b = DocumentBuilder::new();
let root = b.new_element(b.alloc_str("r"));
for i in 0..100 {
let child = b.new_element(b.alloc_str(&format!("c{i}")));
b.append_child(root, child);
}
b.set_root(root); let doc = b.build();
assert!(doc.memory_bytes() > 0);
assert_eq!(doc.root().children().count(), 100);
}
#[test]
fn pi_node_holds_target_and_content() {
let b = DocumentBuilder::new();
let root = b.new_element(b.alloc_str("r"));
let pi = b.new_pi(b.alloc_str("xml-stylesheet"),
Some(b.alloc_str(r#"type="text/xsl" href="s.xsl""#)));
b.append_child(root, pi);
b.set_root(root); let doc = b.build();
let pi = doc.root().children().next().unwrap();
assert_eq!(pi.kind, NodeKind::Pi);
assert_eq!(pi.name(), "xml-stylesheet");
assert_eq!(pi.content(), r#"type="text/xsl" href="s.xsl""#);
}
#[test]
fn builder_default_is_same_as_new() {
let b = DocumentBuilder::default();
let root = b.new_element(b.alloc_str("r"));
b.set_root(root);
let doc = b.build();
assert_eq!(doc.root().name(), "r");
}
#[test]
fn builder_with_capacity_reserves_arena() {
let b = DocumentBuilder::with_capacity(8 * 1024);
let root = b.new_element(b.alloc_str("r"));
b.set_root(root);
let doc = b.build();
assert!(doc.memory_bytes() > 0);
assert_eq!(doc.root().name(), "r");
}
#[test]
fn builder_bump_accessor() {
let b = DocumentBuilder::new();
let _: &bumpalo::Bump = b.bump();
}
#[test]
fn builder_source_returns_none_when_no_inplace_buffer() {
let b = DocumentBuilder::new();
assert!(b.source().is_none());
}
#[test]
fn entity_ref_node_via_builder() {
let b = DocumentBuilder::new();
let root = b.new_element(b.alloc_str("r"));
let er = b.new_entity_ref(b.alloc_str("foo"), b.alloc_str("&foo;"));
b.append_child(root, er);
b.set_root(root); let doc = b.build();
let e = doc.root().children().next().unwrap();
assert!(e.is_entity_ref());
assert!(!e.is_element());
assert!(!e.is_text());
assert_eq!(e.name(), "foo");
assert_eq!(e.content(), "&foo;");
}
#[test]
fn text_content_on_element_without_text_returns_none() {
let b = DocumentBuilder::new();
let root = b.new_element(b.alloc_str("r"));
let child = b.new_element(b.alloc_str("c"));
b.append_child(root, child);
b.set_root(root); let doc = b.build();
assert_eq!(doc.root().text_content(), None);
}
#[test]
fn text_content_on_comment_returns_none() {
let b = DocumentBuilder::new();
let root = b.new_element(b.alloc_str("r"));
let c = b.new_comment(b.alloc_str(" hi "));
b.append_child(root, c);
b.set_root(root); let doc = b.build();
let comment = doc.root().children().next().unwrap();
assert_eq!(comment.text_content(), None);
}
#[test]
fn node_debug_shows_kind_and_relevant_fields() {
let b = DocumentBuilder::new();
let root = b.new_element(b.alloc_str("foo"));
b.append_child(root, b.new_text(b.alloc_str("hello")));
b.set_root(root); let doc = b.build();
let s = format!("{:?}", doc.root());
assert!(s.contains("Element"), "got {s}");
assert!(s.contains("foo"), "got {s}");
let t = doc.root().children().next().unwrap();
let s = format!("{t:?}");
assert!(s.contains("Text"), "got {s}");
assert!(s.contains("hello"), "got {s}");
}
#[test]
fn attribute_debug_shows_name_and_value() {
let b = DocumentBuilder::new();
let el = b.new_element(b.alloc_str("el"));
let attr = b.new_attribute(b.alloc_str("id"), b.alloc_str("123"));
b.append_attribute(el, attr);
b.set_root(el); let doc = b.build();
let attr = doc.root().attributes().next().unwrap();
let s = format!("{attr:?}");
assert!(s.contains("Attribute"), "got {s}");
assert!(s.contains("id"), "got {s}");
assert!(s.contains("123"), "got {s}");
}
#[test]
fn document_debug_shows_root_and_memory() {
let b = DocumentBuilder::new();
b.set_root(b.new_element(b.alloc_str("rootname")));
let doc = b.build();
let s = format!("{doc:?}");
assert!(s.contains("Document"), "got {s}");
assert!(s.contains("memory_bytes"), "got {s}");
assert!(s.contains("rootname"), "got {s}");
}
#[test]
fn child_iter_from_head_walks_chain() {
let b = DocumentBuilder::new();
let root = b.new_element(b.alloc_str("r"));
for n in &["a", "b", "c"] {
b.append_child(root, b.new_element(b.alloc_str(n)));
}
b.set_root(root); let doc = b.build();
let head = doc.root().first_child.get();
let names: Vec<&str> = ChildIter::from_head(head).map(|c| c.name()).collect();
assert_eq!(names, vec!["a", "b", "c"]);
let empty: Vec<&str> = ChildIter::from_head(None).map(|c| c.name()).collect();
assert!(empty.is_empty());
}
#[test]
fn document_first_sibling_equals_root_by_default() {
let b = DocumentBuilder::new();
b.set_root(b.new_element(b.alloc_str("r")));
let doc = b.build();
let first = doc.first_sibling();
assert_eq!(first.name(), "r");
}
#[test]
fn document_is_html_false_without_metadata() {
let b = DocumentBuilder::new();
b.set_root(b.new_element(b.alloc_str("r")));
let doc = b.build();
assert!(!doc.is_html());
}
#[test]
fn document_post_build_new_element_and_append_child() {
let b = DocumentBuilder::new();
b.set_root(b.new_element(b.alloc_str("r")));
let doc = b.build();
let root = doc.root();
let added = doc.new_element(doc.bump().alloc_str("late"));
doc.append_child(root, added);
let names: Vec<&str> = root.children().map(|c| c.name()).collect();
assert_eq!(names, vec!["late"]);
}
#[test]
fn document_post_build_append_two_children_threads_siblings() {
let b = DocumentBuilder::new();
b.set_root(b.new_element(b.alloc_str("r")));
let doc = b.build();
let root = doc.root();
let a = doc.new_element(doc.bump().alloc_str("a"));
let bn = doc.new_element(doc.bump().alloc_str("b"));
doc.append_child(root, a);
doc.append_child(root, bn);
let names: Vec<&str> = root.children().map(|c| c.name()).collect();
assert_eq!(names, vec!["a", "b"]);
assert!(std::ptr::eq(a.next_sibling.get().unwrap(), bn));
assert!(std::ptr::eq(bn.prev_sibling.get().unwrap(), a));
}
#[test]
fn document_post_build_append_attribute_two_threads_links() {
let b = DocumentBuilder::new();
b.set_root(b.new_element(b.alloc_str("r")));
let doc = b.build();
let root = doc.root();
let a1 = doc.new_attribute(doc.bump().alloc_str("id"), doc.bump().alloc_str("1"));
let a2 = doc.new_attribute(doc.bump().alloc_str("class"), doc.bump().alloc_str("c"));
doc.append_attribute(root, a1);
doc.append_attribute(root, a2);
let pairs: Vec<(&str, &str)> = root.attributes()
.map(|a| (a.name(), a.value())).collect();
assert_eq!(pairs, vec![("id", "1"), ("class", "c")]);
}
#[test]
fn document_post_build_allocator_variants() {
let b = DocumentBuilder::new();
b.set_root(b.new_element(b.alloc_str("r")));
let doc = b.build();
let root = doc.root();
let t = doc.new_text(doc.bump().alloc_str("hi"));
let cd = doc.new_cdata(doc.bump().alloc_str("raw"));
let cm = doc.new_comment(doc.bump().alloc_str(" c "));
let pi = doc.new_pi(doc.bump().alloc_str("php"), Some(doc.bump().alloc_str("")));
let er = doc.new_entity_ref(doc.bump().alloc_str("foo"), doc.bump().alloc_str("&foo;"));
doc.append_child(root, t);
doc.append_child(root, cd);
doc.append_child(root, cm);
doc.append_child(root, pi);
doc.append_child(root, er);
let kinds: Vec<NodeKind> = root.children().map(|c| c.kind).collect();
assert_eq!(kinds, vec![
NodeKind::Text, NodeKind::CData, NodeKind::Comment,
NodeKind::Pi, NodeKind::EntityRef,
]);
}
#[test]
fn document_bump_accessor() {
let b = DocumentBuilder::new();
b.set_root(b.new_element(b.alloc_str("r")));
let doc = b.build();
let _: &bumpalo::Bump = doc.bump();
}
}