mirror of
https://github.com/lexmount/moli.git
synced 2026-11-13 16:10:01 +00:00
Retain valid delivered meta referrer policies on their native owner Document. Preserve delivery ordering and independent cloned Document state. Validation (32 build jobs / test threads, zero retries): - `cargo fmt --all` - `cargo clippy --workspace --all-targets --all-features -- -D warnings` - `cargo nextest run --no-fail-fast`: Summary [ 133.798s] 21996 tests run: 21996 passed (9 slow), 19 skipped
443 lines
13 KiB
Rust
443 lines
13 KiB
Rust
use html5ever::tree_builder::QuirksMode as HtmlQuirksMode;
|
|
use selectors::matching::QuirksMode;
|
|
use url::Url;
|
|
|
|
use super::{DomStringValue, NativeDom, NativeNodeId};
|
|
|
|
mod base_url;
|
|
mod referrer_policy;
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
|
|
use base_url::DocumentBaseUrlState;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum DocumentTitleSetterTarget {
|
|
ExistingTitle(NativeNodeId),
|
|
AppendToHtmlHead(NativeNodeId),
|
|
PrependToSvgRoot(NativeNodeId),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum DocumentKind {
|
|
Html,
|
|
Xml,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
enum DocumentInterface {
|
|
Document,
|
|
Html,
|
|
Xml,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum DocumentReadyState {
|
|
Loading,
|
|
Interactive,
|
|
Complete,
|
|
}
|
|
|
|
impl DocumentReadyState {
|
|
pub const fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Loading => "loading",
|
|
Self::Interactive => "interactive",
|
|
Self::Complete => "complete",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for DocumentReadyState {
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
formatter.write_str(self.as_str())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Document {
|
|
metadata: DocumentMetadata,
|
|
arena_state: DocumentArenaState,
|
|
}
|
|
|
|
// Portable document data contains no references into a NativeDom arena.
|
|
#[derive(Debug, Clone)]
|
|
struct DocumentMetadata {
|
|
url: Url,
|
|
content_type: Box<str>,
|
|
character_set: Box<str>,
|
|
ready_state: DocumentReadyState,
|
|
quirks_mode: QuirksMode,
|
|
kind: DocumentKind,
|
|
interface: DocumentInterface,
|
|
design_mode_enabled: bool,
|
|
default_language: Option<Box<str>>,
|
|
source_last_modified_ms: Option<f64>,
|
|
base_url_state: DocumentBaseUrlState,
|
|
// Delivered meta policy survives element removal but belongs to this Document.
|
|
meta_referrer_policy: Option<&'static str>,
|
|
}
|
|
|
|
// Node relationships and the associated template-owner role belong to one
|
|
// Document identity. Only a whole-arena snapshot may copy them unchanged.
|
|
#[derive(Debug, Clone, Default)]
|
|
struct DocumentArenaState {
|
|
is_inert_template_document: bool,
|
|
associated_inert_template_document: Option<NativeNodeId>,
|
|
css_target: Option<NativeNodeId>,
|
|
}
|
|
|
|
impl Document {
|
|
pub fn new(url: Url) -> Self {
|
|
Self::new_html(url)
|
|
}
|
|
|
|
pub fn new_html(url: Url) -> Self {
|
|
Self::new_with_kind(url, DocumentKind::Html)
|
|
}
|
|
|
|
pub fn new_xml(url: Url) -> Self {
|
|
Self::new_with_kind(url, DocumentKind::Xml)
|
|
}
|
|
|
|
fn new_with_kind(url: Url, kind: DocumentKind) -> Self {
|
|
let (content_type, interface) = match kind {
|
|
DocumentKind::Html => ("text/html", DocumentInterface::Html),
|
|
DocumentKind::Xml => ("application/xml", DocumentInterface::Xml),
|
|
};
|
|
Self {
|
|
metadata: DocumentMetadata {
|
|
base_url_state: DocumentBaseUrlState::new(&url),
|
|
url,
|
|
content_type: content_type.into(),
|
|
character_set: "UTF-8".into(),
|
|
ready_state: DocumentReadyState::Complete,
|
|
quirks_mode: QuirksMode::NoQuirks,
|
|
kind,
|
|
interface,
|
|
design_mode_enabled: false,
|
|
default_language: None,
|
|
source_last_modified_ms: None,
|
|
meta_referrer_policy: None,
|
|
},
|
|
arena_state: DocumentArenaState::default(),
|
|
}
|
|
}
|
|
|
|
pub(super) fn new_inert_template(kind: DocumentKind) -> Self {
|
|
let url = Url::parse("about:blank").expect("about:blank is valid");
|
|
let mut document = Self::new_with_kind(url, kind);
|
|
document.metadata.interface = DocumentInterface::Document;
|
|
document.arena_state.is_inert_template_document = true;
|
|
document
|
|
}
|
|
|
|
/// Copy an independent Document's portable data without tree-derived caches,
|
|
/// even across DOM arenas.
|
|
/// `Clone` instead preserves relationships for whole-arena snapshots and
|
|
/// copy-on-write node storage; it must not create a new Document identity.
|
|
pub(super) fn clone_metadata_for_new_arena(&self) -> Self {
|
|
Self {
|
|
metadata: DocumentMetadata {
|
|
base_url_state: self
|
|
.metadata
|
|
.base_url_state
|
|
.clone_without_tree_caches(self.url()),
|
|
meta_referrer_policy: None,
|
|
..self.metadata.clone()
|
|
},
|
|
arena_state: DocumentArenaState::default(),
|
|
}
|
|
}
|
|
|
|
pub(super) fn clone_for_node(&self) -> Self {
|
|
let mut document = self.clone_metadata_for_new_arena();
|
|
document.set_design_mode_enabled(false);
|
|
// DOM additionally freezes the computed base URL; HTML separately
|
|
// retains the about base URL used by a future relative <base href>.
|
|
// https://dom.spec.whatwg.org/#concept-node-clone
|
|
// https://html.spec.whatwg.org/multipage/dom.html#the-document-object
|
|
document.set_base_url_override(Some(self.base_url().clone()));
|
|
document
|
|
}
|
|
|
|
pub fn url(&self) -> &Url {
|
|
&self.metadata.url
|
|
}
|
|
|
|
pub fn content_type(&self) -> &str {
|
|
&self.metadata.content_type
|
|
}
|
|
|
|
pub fn character_set(&self) -> &str {
|
|
&self.metadata.character_set
|
|
}
|
|
|
|
pub fn set_character_set(&mut self, character_set: impl Into<String>) {
|
|
self.metadata.character_set = character_set.into().into_boxed_str();
|
|
}
|
|
|
|
pub fn css_target(&self) -> Option<NativeNodeId> {
|
|
self.arena_state.css_target
|
|
}
|
|
|
|
pub fn ready_state(&self) -> DocumentReadyState {
|
|
self.metadata.ready_state
|
|
}
|
|
|
|
pub fn meta_referrer_policy(&self) -> Option<&str> {
|
|
self.metadata.meta_referrer_policy
|
|
}
|
|
|
|
pub fn default_language(&self) -> Option<&str> {
|
|
self.metadata.default_language.as_deref()
|
|
}
|
|
|
|
pub fn source_last_modified_ms(&self) -> Option<f64> {
|
|
self.metadata.source_last_modified_ms
|
|
}
|
|
|
|
pub fn quirks_mode(&self) -> QuirksMode {
|
|
self.metadata.quirks_mode
|
|
}
|
|
|
|
pub fn is_quirks_mode(&self) -> bool {
|
|
self.metadata.quirks_mode == QuirksMode::Quirks
|
|
}
|
|
|
|
pub fn kind(&self) -> DocumentKind {
|
|
self.metadata.kind
|
|
}
|
|
|
|
pub fn is_html_document(&self) -> bool {
|
|
self.metadata.kind == DocumentKind::Html
|
|
}
|
|
|
|
pub fn interface_name(&self) -> &'static str {
|
|
match self.metadata.interface {
|
|
DocumentInterface::Document => "Document",
|
|
DocumentInterface::Html => "HTMLDocument",
|
|
DocumentInterface::Xml => "XMLDocument",
|
|
}
|
|
}
|
|
|
|
pub fn is_inert_template_document(&self) -> bool {
|
|
self.arena_state.is_inert_template_document
|
|
}
|
|
|
|
pub(super) fn associated_inert_template_document(&self) -> Option<NativeNodeId> {
|
|
self.arena_state.associated_inert_template_document
|
|
}
|
|
|
|
pub(super) fn set_associated_inert_template_document(&mut self, document: NativeNodeId) {
|
|
self.arena_state.associated_inert_template_document = Some(document);
|
|
}
|
|
|
|
pub fn design_mode_enabled(&self) -> bool {
|
|
self.metadata.design_mode_enabled
|
|
}
|
|
|
|
pub fn fallback_base_url(&self) -> &Url {
|
|
self.metadata.base_url_state.fallback_base_url()
|
|
}
|
|
|
|
pub fn base_element_url(&self) -> Option<&Url> {
|
|
self.metadata.base_url_state.base_element_url()
|
|
}
|
|
|
|
pub fn base_url(&self) -> &Url {
|
|
self.metadata.base_url_state.base_url()
|
|
}
|
|
|
|
pub fn base_target(&self) -> Option<&str> {
|
|
self.metadata.base_url_state.base_target()
|
|
}
|
|
|
|
pub fn set_url(&mut self, url: Url) {
|
|
self.metadata.base_url_state.set_document_url(&url);
|
|
self.metadata.url = url;
|
|
}
|
|
|
|
pub fn set_base_url_override(&mut self, url: Option<Url>) {
|
|
self.metadata.base_url_state.set_base_url_override(url);
|
|
}
|
|
|
|
pub fn set_fallback_base_url(&mut self, fallback_base_url: Option<Url>) -> bool {
|
|
self.metadata
|
|
.base_url_state
|
|
.set_fallback_base_url(&self.metadata.url, fallback_base_url)
|
|
}
|
|
|
|
pub fn set_content_type(&mut self, content_type: impl Into<String>) {
|
|
self.metadata.content_type = content_type.into().into_boxed_str();
|
|
}
|
|
|
|
pub fn set_design_mode_enabled(&mut self, design_mode_enabled: bool) {
|
|
self.metadata.design_mode_enabled = design_mode_enabled;
|
|
}
|
|
|
|
pub fn set_css_target(&mut self, target: Option<NativeNodeId>) -> bool {
|
|
if self.arena_state.css_target == target {
|
|
return false;
|
|
}
|
|
self.arena_state.css_target = target;
|
|
true
|
|
}
|
|
|
|
pub fn set_ready_state(&mut self, ready_state: DocumentReadyState) {
|
|
self.metadata.ready_state = ready_state;
|
|
}
|
|
|
|
pub fn set_default_language(&mut self, language: Option<String>) {
|
|
self.metadata.default_language = language.map(String::into_boxed_str);
|
|
}
|
|
|
|
pub fn set_source_last_modified_ms(&mut self, timestamp_ms: Option<f64>) {
|
|
self.metadata.source_last_modified_ms = timestamp_ms;
|
|
}
|
|
|
|
pub fn set_quirks_mode(&mut self, quirks_mode: QuirksMode) {
|
|
self.metadata.quirks_mode = quirks_mode;
|
|
}
|
|
|
|
pub fn set_html_quirks_mode(&mut self, quirks_mode: HtmlQuirksMode) {
|
|
self.metadata.quirks_mode = match quirks_mode {
|
|
HtmlQuirksMode::NoQuirks => QuirksMode::NoQuirks,
|
|
HtmlQuirksMode::Quirks => QuirksMode::Quirks,
|
|
HtmlQuirksMode::LimitedQuirks => QuirksMode::LimitedQuirks,
|
|
};
|
|
}
|
|
|
|
pub fn document_element_handle(
|
|
&self,
|
|
dom: &NativeDom,
|
|
document_node_id: NativeNodeId,
|
|
) -> Option<NativeNodeId> {
|
|
dom.find_child(document_node_id, |handle| {
|
|
dom.node(handle).is_some_and(|node| node.is_element())
|
|
})
|
|
}
|
|
|
|
fn html_document_element_handle(
|
|
&self,
|
|
dom: &NativeDom,
|
|
document_node_id: NativeNodeId,
|
|
) -> Option<NativeNodeId> {
|
|
self.document_element_handle(dom, document_node_id)
|
|
.filter(|handle| {
|
|
dom.node(*handle)
|
|
.is_some_and(|node| node.is_html_element_named("html"))
|
|
})
|
|
}
|
|
|
|
pub fn head_handle(
|
|
&self,
|
|
dom: &NativeDom,
|
|
document_node_id: NativeNodeId,
|
|
) -> Option<NativeNodeId> {
|
|
let document_element = self.html_document_element_handle(dom, document_node_id)?;
|
|
dom.find_child(document_element, |handle| {
|
|
dom.node(handle)
|
|
.is_some_and(|node| node.is_html_element_named("head"))
|
|
})
|
|
}
|
|
|
|
pub fn body_handle(
|
|
&self,
|
|
dom: &NativeDom,
|
|
document_node_id: NativeNodeId,
|
|
) -> Option<NativeNodeId> {
|
|
let document_element = self.html_document_element_handle(dom, document_node_id)?;
|
|
dom.find_child(document_element, |handle| {
|
|
dom.node(handle)
|
|
.is_some_and(|node| node.is_html_element_named("body"))
|
|
})
|
|
}
|
|
|
|
pub fn body_or_frameset_handle(
|
|
&self,
|
|
dom: &NativeDom,
|
|
document_node_id: NativeNodeId,
|
|
) -> Option<NativeNodeId> {
|
|
let document_element = self.html_document_element_handle(dom, document_node_id)?;
|
|
dom.find_child(document_element, |handle| {
|
|
dom.node(handle).is_some_and(|node| {
|
|
node.is_html_element_named("body") || node.is_html_element_named("frameset")
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct DocumentType {
|
|
// Doctypes are rare; their lossless strings must not enlarge common node records.
|
|
strings: Box<DocumentTypeStrings>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct DocumentTypeStrings {
|
|
name: DomStringValue,
|
|
public_id: DomStringValue,
|
|
system_id: DomStringValue,
|
|
}
|
|
|
|
impl DocumentType {
|
|
pub fn new(
|
|
name: impl Into<DomStringValue>,
|
|
public_id: impl Into<DomStringValue>,
|
|
system_id: impl Into<DomStringValue>,
|
|
) -> Self {
|
|
Self {
|
|
strings: Box::new(DocumentTypeStrings {
|
|
name: name.into(),
|
|
public_id: public_id.into(),
|
|
system_id: system_id.into(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn name(&self) -> &str {
|
|
self.strings.name.as_str_lossy()
|
|
}
|
|
|
|
pub fn public_id(&self) -> &str {
|
|
self.strings.public_id.as_str_lossy()
|
|
}
|
|
|
|
pub fn system_id(&self) -> &str {
|
|
self.strings.system_id.as_str_lossy()
|
|
}
|
|
|
|
pub fn name_value(&self) -> &DomStringValue {
|
|
&self.strings.name
|
|
}
|
|
|
|
pub fn public_id_value(&self) -> &DomStringValue {
|
|
&self.strings.public_id
|
|
}
|
|
|
|
pub fn system_id_value(&self) -> &DomStringValue {
|
|
&self.strings.system_id
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct DocumentFragment {
|
|
host: Option<NativeNodeId>,
|
|
}
|
|
|
|
impl DocumentFragment {
|
|
pub fn new(host: Option<NativeNodeId>) -> Self {
|
|
Self { host }
|
|
}
|
|
|
|
pub fn host(&self) -> Option<NativeNodeId> {
|
|
self.host
|
|
}
|
|
|
|
pub(crate) fn set_host(&mut self, host: NativeNodeId) {
|
|
self.host = Some(host);
|
|
}
|
|
}
|