Files
browser/src/rust/html5ever/lib.rs
T
2026-08-25 16:02:51 +08:00

911 lines
32 KiB
Rust

// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
mod prescan;
mod sink;
mod types;
mod url;
use std::cell::Cell;
use std::os::raw::{c_uchar, c_void};
use types::*;
use encoding_rs::Encoding;
use html5ever::driver::parse_fragment_for_element;
use html5ever::interface::tree_builder::QuirksMode;
use html5ever::tendril::{StrTendril, TendrilSink};
use html5ever::{ns, parse_document, LocalName, ParseOpts, Parser, QualName};
#[no_mangle]
pub extern "C" fn html5ever_parse_document(
html: *mut c_uchar,
len: usize,
document: Ref,
ctx: Ref,
create_element_callback: CreateElementCallback,
get_data_callback: GetDataCallback,
append_callback: AppendCallback,
parse_error_callback: ParseErrorCallback,
pop_callback: PopCallback,
create_comment_callback: CreateCommentCallback,
create_processing_instruction: CreateProcessingInstruction,
append_doctype_to_document: AppendDoctypeToDocumentCallback,
add_attrs_if_missing_callback: AddAttrsIfMissingCallback,
get_template_contents_callback: GetTemplateContentsCallback,
remove_from_parent_callback: RemoveFromParentCallback,
reparent_children_callback: ReparentChildrenCallback,
append_before_sibling_callback: AppendBeforeSiblingCallback,
append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,
attach_declarative_shadow_callback: AttachDeclarativeShadowCallback,
allow_declarative_shadow: bool,
) -> () {
if html.is_null() || len == 0 {
return ();
}
let arena = typed_arena::Arena::new();
let sink = sink::Sink {
ctx: ctx,
arena: &arena,
document: document,
quirks_mode: Cell::new(QuirksMode::NoQuirks),
pop_callback: pop_callback,
append_callback: append_callback,
get_data_callback: get_data_callback,
parse_error_callback: parse_error_callback,
create_element_callback: create_element_callback,
create_comment_callback: create_comment_callback,
create_processing_instruction: create_processing_instruction,
append_doctype_to_document: append_doctype_to_document,
add_attrs_if_missing_callback: add_attrs_if_missing_callback,
get_template_contents_callback: get_template_contents_callback,
remove_from_parent_callback: remove_from_parent_callback,
reparent_children_callback: reparent_children_callback,
append_before_sibling_callback: append_before_sibling_callback,
append_based_on_parent_node_callback: append_based_on_parent_node_callback,
attach_declarative_shadow_callback: attach_declarative_shadow_callback,
allow_declarative_shadow: allow_declarative_shadow,
};
let bytes = unsafe { std::slice::from_raw_parts(html, len) };
parse_document(sink, Default::default())
.from_utf8()
.one(bytes);
}
/// Parse an HTML document with encoding conversion.
/// If charset is provided, converts from that encoding to UTF-8 before parsing.
/// Uses Cow<str> internally so no allocation if content is already valid UTF-8.
#[no_mangle]
pub extern "C" fn html5ever_parse_document_with_encoding(
html: *mut c_uchar,
len: usize,
charset: *const c_uchar,
charset_len: usize,
document: Ref,
ctx: Ref,
create_element_callback: CreateElementCallback,
get_data_callback: GetDataCallback,
append_callback: AppendCallback,
parse_error_callback: ParseErrorCallback,
pop_callback: PopCallback,
create_comment_callback: CreateCommentCallback,
create_processing_instruction: CreateProcessingInstruction,
append_doctype_to_document: AppendDoctypeToDocumentCallback,
add_attrs_if_missing_callback: AddAttrsIfMissingCallback,
get_template_contents_callback: GetTemplateContentsCallback,
remove_from_parent_callback: RemoveFromParentCallback,
reparent_children_callback: ReparentChildrenCallback,
append_before_sibling_callback: AppendBeforeSiblingCallback,
append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,
attach_declarative_shadow_callback: AttachDeclarativeShadowCallback,
allow_declarative_shadow: bool,
) -> () {
if html.is_null() || len == 0 {
return ();
}
let input = unsafe { std::slice::from_raw_parts(html, len) };
let charset_bytes = if charset.is_null() {
&[]
} else {
unsafe { std::slice::from_raw_parts(charset, charset_len) }
};
// Decode to UTF-8. Returns Cow<str> - no allocation if already valid UTF-8.
let encoding = Encoding::for_label(charset_bytes).unwrap_or(encoding_rs::UTF_8);
let (decoded, _, _) = encoding.decode(input);
let arena = typed_arena::Arena::new();
let sink = sink::Sink {
ctx: ctx,
arena: &arena,
document: document,
quirks_mode: Cell::new(QuirksMode::NoQuirks),
pop_callback: pop_callback,
append_callback: append_callback,
get_data_callback: get_data_callback,
parse_error_callback: parse_error_callback,
create_element_callback: create_element_callback,
create_comment_callback: create_comment_callback,
create_processing_instruction: create_processing_instruction,
append_doctype_to_document: append_doctype_to_document,
add_attrs_if_missing_callback: add_attrs_if_missing_callback,
get_template_contents_callback: get_template_contents_callback,
remove_from_parent_callback: remove_from_parent_callback,
reparent_children_callback: reparent_children_callback,
append_before_sibling_callback: append_before_sibling_callback,
append_based_on_parent_node_callback: append_based_on_parent_node_callback,
attach_declarative_shadow_callback: attach_declarative_shadow_callback,
allow_declarative_shadow: allow_declarative_shadow,
};
// Parse directly from decoded string
parse_document(sink, Default::default()).one(StrTendril::from(decoded.as_ref()));
}
// === Encoding API for TextDecoder ===
/// Result of encoding label lookup
#[repr(C)]
pub struct EncodingInfo {
/// 0 = not found, 1 = found
pub found: u8,
/// Opaque handle to the encoding (actually &'static Encoding)
pub handle: *const c_void,
/// Length of canonical name
pub name_len: usize,
/// Pointer to canonical encoding name (static, lowercase)
pub name_ptr: *const c_uchar,
}
/// Look up an encoding by its label (case-insensitive, whitespace-trimmed)
#[no_mangle]
pub extern "C" fn encoding_for_label(label: *const c_uchar, label_len: usize) -> EncodingInfo {
if label.is_null() || label_len == 0 {
return EncodingInfo {
found: 0,
name_len: 0,
handle: std::ptr::null(),
name_ptr: std::ptr::null(),
};
}
let label_bytes = unsafe { std::slice::from_raw_parts(label, label_len) };
match Encoding::for_label(label_bytes) {
Some(encoding) => {
let name = encoding.name();
EncodingInfo {
found: 1,
name_len: name.len(),
name_ptr: name.as_ptr(),
handle: encoding as *const _ as *const c_void,
}
}
None => EncodingInfo {
found: 0,
name_len: 0,
name_ptr: std::ptr::null(),
handle: std::ptr::null(),
},
}
}
/// Calculate maximum UTF-8 buffer size needed for decoding
#[no_mangle]
pub extern "C" fn encoding_max_utf8_buffer_length(
handle: *const c_void,
input_len: usize,
) -> usize {
if handle.is_null() {
return 0;
}
let encoding: &'static Encoding = unsafe { &*(handle as *const Encoding) };
let decoder = encoding.new_decoder();
decoder.max_utf8_buffer_length(input_len).unwrap_or(0)
}
/// Result of decoding operation
#[repr(C)]
pub struct DecodeResult {
/// 0 = no errors, 1 = had malformed sequences (replaced with U+FFFD)
pub had_errors: u8,
/// Number of input bytes consumed
pub bytes_read: usize,
/// Number of UTF-8 bytes written to output buffer
pub bytes_written: usize,
}
/// Decode bytes from source encoding to UTF-8
/// For streaming, set is_last=0; for final/complete decode, set is_last=1
#[no_mangle]
pub extern "C" fn encoding_decode(
handle: *const c_void,
input: *const c_uchar,
input_len: usize,
output: *mut c_uchar,
output_len: usize,
is_last: u8,
) -> DecodeResult {
if handle.is_null() || output.is_null() {
return DecodeResult {
had_errors: 1,
bytes_read: 0,
bytes_written: 0,
};
}
let encoding: &'static Encoding = unsafe { &*(handle as *const Encoding) };
let input_bytes = if input.is_null() || input_len == 0 {
&[]
} else {
unsafe { std::slice::from_raw_parts(input, input_len) }
};
let output_slice = unsafe { std::slice::from_raw_parts_mut(output, output_len) };
let mut decoder = encoding.new_decoder();
let last = is_last != 0;
let (result, bytes_read, bytes_written, had_errors) =
decoder.decode_to_utf8(input_bytes, output_slice, last);
// If output buffer was too small, we still report what we could process
let _ = result; // CoderResult::InputEmpty or CoderResult::OutputFull
DecodeResult {
had_errors: if had_errors { 1 } else { 0 },
bytes_read,
bytes_written,
}
}
// === Streaming Decoder API ===
use encoding_rs::Decoder;
/// Create a streaming decoder that maintains state across calls
#[no_mangle]
pub extern "C" fn encoding_decoder_new(handle: *const c_void) -> *mut c_void {
if handle.is_null() {
return std::ptr::null_mut();
}
let encoding: &'static Encoding = unsafe { &*(handle as *const Encoding) };
let decoder = Box::new(encoding.new_decoder());
Box::into_raw(decoder) as *mut c_void
}
/// Decode using a streaming decoder (maintains state for incomplete sequences)
#[no_mangle]
pub extern "C" fn encoding_decoder_decode(
decoder_ptr: *mut c_void,
input: *const c_uchar,
input_len: usize,
output: *mut c_uchar,
output_len: usize,
is_last: u8,
) -> DecodeResult {
if decoder_ptr.is_null() || output.is_null() {
return DecodeResult {
had_errors: 1,
bytes_read: 0,
bytes_written: 0,
};
}
let decoder: &mut Decoder = unsafe { &mut *(decoder_ptr as *mut Decoder) };
let input_bytes = if input.is_null() || input_len == 0 {
&[]
} else {
unsafe { std::slice::from_raw_parts(input, input_len) }
};
let output_slice = unsafe { std::slice::from_raw_parts_mut(output, output_len) };
let last = is_last != 0;
let (result, bytes_read, bytes_written, had_errors) =
decoder.decode_to_utf8(input_bytes, output_slice, last);
let _ = result;
DecodeResult {
had_errors: if had_errors { 1 } else { 0 },
bytes_read,
bytes_written,
}
}
/// Free a streaming decoder
#[no_mangle]
pub extern "C" fn encoding_decoder_free(decoder_ptr: *mut c_void) {
if !decoder_ptr.is_null() {
unsafe {
drop(Box::from_raw(decoder_ptr as *mut Decoder));
}
}
}
// === Encoding API (UTF-8 to legacy encoding with NCR fallback) ===
/// Result of encoding operation
#[repr(C)]
pub struct EncodeResult {
/// 0 = success, 1 = output buffer too small
pub status: u8,
/// Number of input bytes consumed
pub bytes_read: usize,
/// Number of bytes written to output buffer
pub bytes_written: usize,
}
/// Encode UTF-8 to a legacy encoding, replacing unencodable characters with
/// HTML decimal numeric character references (&#codepoint;).
///
/// This is used for URL query string encoding per WHATWG URL spec.
/// encoding_rs's encode_from_utf8 already produces NCRs for unmappable chars.
#[no_mangle]
pub extern "C" fn encoding_encode_with_ncr(
handle: *const c_void,
input: *const c_uchar,
input_len: usize,
output: *mut c_uchar,
output_capacity: usize,
) -> EncodeResult {
if handle.is_null() || output.is_null() {
return EncodeResult {
status: 1,
bytes_read: 0,
bytes_written: 0,
};
}
let encoding: &'static Encoding = unsafe { &*(handle as *const Encoding) };
let input_str = if input.is_null() || input_len == 0 {
""
} else {
let bytes = unsafe { std::slice::from_raw_parts(input, input_len) };
match std::str::from_utf8(bytes) {
Ok(s) => s,
Err(_) => {
return EncodeResult {
status: 1,
bytes_read: 0,
bytes_written: 0,
};
}
}
};
// For UTF-8 encoding, just copy directly (no NCR needed)
if encoding == encoding_rs::UTF_8 {
if input_len > output_capacity {
return EncodeResult {
bytes_read: 0,
bytes_written: 0,
status: 1,
};
}
let output_slice = unsafe { std::slice::from_raw_parts_mut(output, output_capacity) };
output_slice[..input_len].copy_from_slice(input_str.as_bytes());
return EncodeResult {
bytes_read: input_len,
bytes_written: input_len,
status: 0,
};
}
let output_slice = unsafe { std::slice::from_raw_parts_mut(output, output_capacity) };
let mut encoder = encoding.new_encoder();
// encode_from_utf8 automatically produces NCRs for unmappable characters
let (result, bytes_read, bytes_written, _had_unmappables) =
encoder.encode_from_utf8(input_str, output_slice, true);
match result {
encoding_rs::CoderResult::InputEmpty => EncodeResult {
bytes_read,
bytes_written,
status: 0,
},
encoding_rs::CoderResult::OutputFull => EncodeResult {
bytes_read,
bytes_written,
status: 1,
},
}
}
/// Calculate maximum output buffer size needed for encoding with NCR fallback.
/// Worst case: every character becomes &#codepoint; where codepoint is up to 7 digits.
#[no_mangle]
pub extern "C" fn encoding_max_encode_buffer_length(
handle: *const c_void,
input_len: usize,
) -> usize {
if handle.is_null() {
return 0;
}
let encoding: &'static Encoding = unsafe { &*(handle as *const Encoding) };
let encoder = encoding.new_encoder();
// This returns the max buffer size accounting for NCR expansion
encoder
.max_buffer_length_from_utf8_if_no_unmappables(input_len)
.map(|len| {
// Add extra space for potential NCRs (each char could become &#nnnnnn; = 10 bytes)
// But realistically, most chars are mappable, so add 2x as safety margin
len.saturating_mul(2)
})
.unwrap_or(input_len * 10)
}
#[no_mangle]
pub extern "C" fn html5ever_parse_fragment(
html: *mut c_uchar,
len: usize,
context_name: *const c_uchar,
context_name_len: usize,
document: Ref,
ctx: Ref,
create_element_callback: CreateElementCallback,
create_context_element_callback: CreateElementCallback,
get_data_callback: GetDataCallback,
append_callback: AppendCallback,
parse_error_callback: ParseErrorCallback,
pop_callback: PopCallback,
create_comment_callback: CreateCommentCallback,
create_processing_instruction: CreateProcessingInstruction,
append_doctype_to_document: AppendDoctypeToDocumentCallback,
add_attrs_if_missing_callback: AddAttrsIfMissingCallback,
get_template_contents_callback: GetTemplateContentsCallback,
remove_from_parent_callback: RemoveFromParentCallback,
reparent_children_callback: ReparentChildrenCallback,
append_before_sibling_callback: AppendBeforeSiblingCallback,
append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,
attach_declarative_shadow_callback: AttachDeclarativeShadowCallback,
allow_declarative_shadow: bool,
) -> () {
if html.is_null() || len == 0 {
return ();
}
let arena = typed_arena::Arena::new();
let sink = sink::Sink {
ctx: ctx,
arena: &arena,
document: document,
quirks_mode: Cell::new(QuirksMode::NoQuirks),
pop_callback: pop_callback,
append_callback: append_callback,
get_data_callback: get_data_callback,
parse_error_callback: parse_error_callback,
create_element_callback: create_element_callback,
create_comment_callback: create_comment_callback,
create_processing_instruction: create_processing_instruction,
append_doctype_to_document: append_doctype_to_document,
add_attrs_if_missing_callback: add_attrs_if_missing_callback,
get_template_contents_callback: get_template_contents_callback,
remove_from_parent_callback: remove_from_parent_callback,
reparent_children_callback: reparent_children_callback,
append_before_sibling_callback: append_before_sibling_callback,
append_based_on_parent_node_callback: append_based_on_parent_node_callback,
attach_declarative_shadow_callback: attach_declarative_shadow_callback,
allow_declarative_shadow: allow_declarative_shadow,
};
let bytes = unsafe { std::slice::from_raw_parts(html, len) };
// The initial state of the parser is dertmined by the context element.
// E.g., parsing the content of a script tag behaves differently than
// parsing the content of the body.
let context_local = if context_name.is_null() || context_name_len == 0 {
LocalName::from("body")
} else {
let name_bytes = unsafe { std::slice::from_raw_parts(context_name, context_name_len) };
match std::str::from_utf8(name_bytes) {
Ok(name) => LocalName::from(name),
Err(_) => LocalName::from("body"),
}
};
// Only a document's input stream may have a single leading U+FEFF BOM
// stripped; fragment parsing (innerHTML, setHTMLUnsafe, etc.) must preserve
// a leading U+FEFF as a ZWNBSP.
let opts = ParseOpts {
tokenizer: html5ever::tokenizer::TokenizerOpts {
discard_bom: false,
..Default::default()
},
..Default::default()
};
let context_qname = QualName::new(None, ns!(html), context_local);
let context_data = arena.alloc(sink::ElementData {
qname: context_qname.clone(),
mathml_annotation_xml_integration_point: false,
});
let mut context_attrs = CAttributeIterator {
vec: vec![],
pos: 0,
};
let context_elem = unsafe {
(create_context_element_callback)(
ctx,
context_data as *mut _ as *mut c_void,
CQualName::create(&context_qname),
&mut context_attrs as *mut _ as *mut c_void,
)
};
parse_fragment_for_element(
sink,
opts,
context_elem,
false, // context_element_allows_scripting
None, // form_element
)
.from_utf8()
.one(bytes);
}
#[no_mangle]
pub extern "C" fn html5ever_attribute_iterator_next(
c_iter: *const c_void,
) -> CNullable<CAttribute> {
let iter: &mut CAttributeIterator = unsafe { &mut *(c_iter as *mut CAttributeIterator) };
let pos = iter.pos;
if pos == iter.vec.len() {
return CNullable::<CAttribute>::none();
}
let attr = &iter.vec[pos];
iter.pos += 1;
CNullable::<CAttribute>::some(CAttribute {
name: CQualName::create(&attr.name),
value: StringSlice {
ptr: attr.value.as_ptr(),
len: attr.value.len(),
},
})
}
#[no_mangle]
pub extern "C" fn html5ever_attribute_iterator_count(c_iter: *const c_void) -> usize {
let iter: &mut CAttributeIterator = unsafe { &mut *(c_iter as *mut CAttributeIterator) };
return iter.vec.len();
}
// Streaming parser API
// The Parser type from html5ever implements TendrilSink and supports streaming
pub struct StreamingParser {
#[allow(dead_code)]
arena: Box<typed_arena::Arena<sink::ElementData>>,
parser: Box<dyn std::any::Any>,
}
#[no_mangle]
pub extern "C" fn html5ever_streaming_parser_create(
document: Ref,
ctx: Ref,
create_element_callback: CreateElementCallback,
get_data_callback: GetDataCallback,
append_callback: AppendCallback,
parse_error_callback: ParseErrorCallback,
pop_callback: PopCallback,
create_comment_callback: CreateCommentCallback,
create_processing_instruction: CreateProcessingInstruction,
append_doctype_to_document: AppendDoctypeToDocumentCallback,
add_attrs_if_missing_callback: AddAttrsIfMissingCallback,
get_template_contents_callback: GetTemplateContentsCallback,
remove_from_parent_callback: RemoveFromParentCallback,
reparent_children_callback: ReparentChildrenCallback,
append_before_sibling_callback: AppendBeforeSiblingCallback,
append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,
attach_declarative_shadow_callback: AttachDeclarativeShadowCallback,
allow_declarative_shadow: bool,
) -> *mut c_void {
let arena = Box::new(typed_arena::Arena::new());
// SAFETY: We're creating a self-referential structure here.
// The arena is stored in the StreamingParser and lives as long as the parser.
// The sink contains a reference to the arena that's valid for the parser's lifetime.
let arena_ref: &'static typed_arena::Arena<sink::ElementData> =
unsafe { std::mem::transmute(arena.as_ref()) };
let sink = sink::Sink {
ctx: ctx,
arena: arena_ref,
document: document,
quirks_mode: Cell::new(QuirksMode::NoQuirks),
pop_callback: pop_callback,
append_callback: append_callback,
get_data_callback: get_data_callback,
parse_error_callback: parse_error_callback,
create_element_callback: create_element_callback,
create_comment_callback: create_comment_callback,
create_processing_instruction: create_processing_instruction,
append_doctype_to_document: append_doctype_to_document,
add_attrs_if_missing_callback: add_attrs_if_missing_callback,
get_template_contents_callback: get_template_contents_callback,
remove_from_parent_callback: remove_from_parent_callback,
reparent_children_callback: reparent_children_callback,
append_before_sibling_callback: append_before_sibling_callback,
append_based_on_parent_node_callback: append_based_on_parent_node_callback,
attach_declarative_shadow_callback: attach_declarative_shadow_callback,
allow_declarative_shadow: allow_declarative_shadow,
};
// Create a parser which implements TendrilSink for streaming parsing
let parser = parse_document(sink, ParseOpts::default());
let streaming_parser = Box::new(StreamingParser {
arena,
parser: Box::new(parser),
});
return Box::into_raw(streaming_parser) as *mut c_void;
}
#[no_mangle]
pub extern "C" fn html5ever_streaming_parser_feed(
parser_ptr: *mut c_void,
html: *const c_uchar,
len: usize,
) -> i32 {
if parser_ptr.is_null() || html.is_null() || len == 0 {
return 0;
}
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let streaming_parser = unsafe { &mut *(parser_ptr as *mut StreamingParser) };
let bytes = unsafe { std::slice::from_raw_parts(html, len) };
// Convert bytes to UTF-8 string
if let Ok(s) = std::str::from_utf8(bytes) {
let tendril = StrTendril::from(s);
// Feed the chunk to the parser
// The Parser implements TendrilSink, so we can call process() on it
let parser = streaming_parser
.parser
.downcast_mut::<Parser<sink::Sink>>()
.expect("Invalid parser type");
parser.process(tendril);
}
}));
match result {
Ok(_) => 0, // Success
Err(_) => -1, // Panic occurred
}
}
#[no_mangle]
pub extern "C" fn html5ever_streaming_parser_finish(parser_ptr: *mut c_void) {
if parser_ptr.is_null() {
return;
}
let streaming_parser = unsafe { Box::from_raw(parser_ptr as *mut StreamingParser) };
// Extract and finish the parser
let parser = streaming_parser
.parser
.downcast::<Parser<sink::Sink>>()
.expect("Invalid parser type");
// Finish consumes the parser, which will call finish() on the sink
parser.finish();
// Note: The arena will be dropped here automatically
}
#[no_mangle]
pub extern "C" fn html5ever_streaming_parser_destroy(parser_ptr: *mut c_void) {
if parser_ptr.is_null() {
return;
}
// Drop the parser box without finishing
// This is for cases where you want to cancel parsing
unsafe {
drop(Box::from_raw(parser_ptr as *mut StreamingParser));
}
}
#[no_mangle]
pub extern "C" fn xml5ever_parse_document(
xml: *mut c_uchar,
len: usize,
document: Ref,
ctx: Ref,
create_element_callback: CreateElementCallback,
get_data_callback: GetDataCallback,
append_callback: AppendCallback,
parse_error_callback: ParseErrorCallback,
pop_callback: PopCallback,
create_comment_callback: CreateCommentCallback,
create_processing_instruction: CreateProcessingInstruction,
append_doctype_to_document: AppendDoctypeToDocumentCallback,
add_attrs_if_missing_callback: AddAttrsIfMissingCallback,
get_template_contents_callback: GetTemplateContentsCallback,
remove_from_parent_callback: RemoveFromParentCallback,
reparent_children_callback: ReparentChildrenCallback,
append_before_sibling_callback: AppendBeforeSiblingCallback,
append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,
attach_declarative_shadow_callback: AttachDeclarativeShadowCallback,
allow_declarative_shadow: bool,
) -> () {
if xml.is_null() || len == 0 {
return ();
}
let arena = typed_arena::Arena::new();
let sink = sink::Sink {
ctx: ctx,
arena: &arena,
document: document,
quirks_mode: Cell::new(QuirksMode::NoQuirks),
pop_callback: pop_callback,
append_callback: append_callback,
get_data_callback: get_data_callback,
parse_error_callback: parse_error_callback,
create_element_callback: create_element_callback,
create_comment_callback: create_comment_callback,
create_processing_instruction: create_processing_instruction,
append_doctype_to_document: append_doctype_to_document,
add_attrs_if_missing_callback: add_attrs_if_missing_callback,
get_template_contents_callback: get_template_contents_callback,
remove_from_parent_callback: remove_from_parent_callback,
reparent_children_callback: reparent_children_callback,
append_before_sibling_callback: append_before_sibling_callback,
append_based_on_parent_node_callback: append_based_on_parent_node_callback,
attach_declarative_shadow_callback: attach_declarative_shadow_callback,
allow_declarative_shadow: allow_declarative_shadow,
};
let bytes = unsafe { std::slice::from_raw_parts(xml, len) };
let bytes = strip_doctype_internal_subset(bytes);
let tb = xml5ever::tree_builder::XmlTreeBuilder::new(sink, Default::default());
let tokenizer = xml5ever::tokenizer::XmlTokenizer::new(
UnclosedTagSink {
tb,
depth: Cell::new(0),
},
Default::default(),
);
html5ever::tendril::stream::Utf8LossyDecoder::new(XmlDocumentParser {
tokenizer,
input_buffer: Default::default(),
})
.one(&*bytes);
}
// xml5ever's tokenizer has no notion of a DOCTYPE internal subset
// (`<!DOCTYPE svg [ <!ENTITY ns_svg "..."> ]>`, as old Illustrator exports
// emit): the `[` ends the doctype as bogus and the declarations tokenize as
// junk before the root, each step a parse error. Nothing in the subset is used
// anyway (no DTD support), so cut it out before tokenizing. Quotes are
// respected; nested `[`/`]` are not (they don't occur in the subset syntax).
fn strip_doctype_internal_subset(bytes: &[u8]) -> std::borrow::Cow<'_, [u8]> {
let Some(doctype) = bytes.windows(9).position(|w| w == b"<!DOCTYPE") else {
return std::borrow::Cow::Borrowed(bytes);
};
let mut i = doctype + 9;
let mut quote: Option<u8> = None;
let mut open = None;
while i < bytes.len() {
let c = bytes[i];
match quote {
Some(q) => {
if c == q {
quote = None
}
}
None => match c {
b'"' | b'\'' => quote = Some(c),
b'>' if open.is_none() => return std::borrow::Cow::Borrowed(bytes),
b'[' if open.is_none() => open = Some(i),
b']' if open.is_some() => {
let mut out = Vec::with_capacity(bytes.len());
out.extend_from_slice(&bytes[..open.unwrap()]);
out.extend_from_slice(&bytes[i + 1..]);
return std::borrow::Cow::Owned(out);
}
_ => {}
},
}
i += 1;
}
std::borrow::Cow::Borrowed(bytes)
}
// xml5ever's tree builder silently closes elements still open at EOF, and
// its tokenizer reports nothing either, so a truncated document
// (`<root><a>text`) parses "cleanly". Browsers reject it. This sits between
// the tokenizer and the tree builder, tracks tag nesting from the raw token
// stream, and reports an error when EOF arrives with tags still open.
struct UnclosedTagSink<'arena> {
tb: xml5ever::tree_builder::XmlTreeBuilder<Ref, sink::Sink<'arena>>,
depth: Cell<u32>,
}
impl<'arena> xml5ever::tokenizer::TokenSink for UnclosedTagSink<'arena> {
type Handle = Ref;
fn process_token(
&self,
token: xml5ever::tokenizer::Token,
) -> xml5ever::tokenizer::ProcessResult<Ref> {
use xml5ever::tokenizer::{TagKind, Token};
match &token {
Token::Tag(tag) => match tag.kind {
TagKind::StartTag => self.depth.set(self.depth.get() + 1),
TagKind::EndTag | TagKind::ShortTag => {
self.depth.set(self.depth.get().saturating_sub(1))
}
TagKind::EmptyTag => {}
},
Token::EndOfFile => {
if self.depth.get() > 0 {
use xml5ever::tree_builder::TreeSink;
self.tb
.sink
.parse_error(std::borrow::Cow::Borrowed("Unclosed element at EOF"));
}
}
_ => {}
}
self.tb.process_token(token)
}
fn end(&self) {
self.tb.end()
}
}
// xml5ever::driver::XmlParser, minus the tree-builder-typed tokenizer so the
// UnclosedTagSink can sit in between.
struct XmlDocumentParser<'arena> {
tokenizer: xml5ever::tokenizer::XmlTokenizer<UnclosedTagSink<'arena>>,
input_buffer: xml5ever::buffer_queue::BufferQueue,
}
impl<'arena> TendrilSink<html5ever::tendril::fmt::UTF8> for XmlDocumentParser<'arena> {
type Output = ();
fn process(&mut self, t: StrTendril) {
self.input_buffer.push_back(t);
while let xml5ever::TokenizerResult::Script(_) = self.tokenizer.feed(&self.input_buffer) {}
}
fn error(&mut self, desc: std::borrow::Cow<'static, str>) {
use xml5ever::tree_builder::TreeSink;
self.tokenizer.sink.tb.sink.parse_error(desc)
}
fn finish(self) -> () {
self.tokenizer.end();
}
}