use std::borrow::Cow;
use std::cell::OnceCell;
use std::collections::BTreeMap;
pub static EMPTY_ATTRS: BTreeMap<String, String> = BTreeMap::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum NodeType {
Text,
Element,
Heading,
Paragraph,
Div,
Blockquote,
Pre,
Hr,
List,
ListItem,
DefinitionList,
DefinitionTerm,
DefinitionDescription,
Table,
TableRow,
TableCell,
TableHeader,
TableBody,
TableHead,
TableFoot,
Link,
Image,
Strong,
Em,
Code,
Strikethrough,
Underline,
Subscript,
Superscript,
Mark,
Small,
Br,
Span,
Article,
Section,
Nav,
Aside,
Header,
Footer,
Main,
Figure,
Figcaption,
Time,
Details,
Summary,
Form,
Input,
Select,
Option,
Button,
Textarea,
Label,
Fieldset,
Legend,
Audio,
Video,
Picture,
Source,
Iframe,
Svg,
Canvas,
Ruby,
Rt,
Rp,
Abbr,
Kbd,
Samp,
Var,
Cite,
Q,
Del,
Ins,
Data,
Meter,
Progress,
Output,
Template,
Slot,
Html,
Head,
Body,
Title,
Meta,
LinkTag,
Style,
Script,
Base,
Custom,
}
pub struct AttributesSource<'a> {
inner: AttributesInner<'a>,
}
enum AttributesInner<'a> {
Borrowed(&'a BTreeMap<String, String>),
Owned(BTreeMap<String, String>),
Lazy {
tag: &'a tl::HTMLTag<'a>,
cell: OnceCell<BTreeMap<String, String>>,
},
}
impl<'a> AttributesSource<'a> {
pub(crate) const fn borrowed(map: &'a BTreeMap<String, String>) -> Self {
Self {
inner: AttributesInner::Borrowed(map),
}
}
pub(crate) const fn owned(map: BTreeMap<String, String>) -> Self {
Self {
inner: AttributesInner::Owned(map),
}
}
pub(crate) const fn lazy(tag: &'a tl::HTMLTag<'a>) -> Self {
Self {
inner: AttributesInner::Lazy {
tag,
cell: OnceCell::new(),
},
}
}
pub fn get(&self) -> &BTreeMap<String, String> {
match &self.inner {
AttributesInner::Borrowed(map) => map,
AttributesInner::Owned(map) => map,
AttributesInner::Lazy { tag, cell } => cell.get_or_init(|| {
tag.attributes()
.iter()
.filter_map(|(k, v)| v.as_ref().map(|val| (k.to_string(), val.to_string())))
.collect()
}),
}
}
}
impl Clone for AttributesSource<'_> {
fn clone(&self) -> Self {
Self {
inner: AttributesInner::Owned(self.get().clone()),
}
}
}
impl std::fmt::Debug for AttributesSource<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_map().entries(self.get()).finish()
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for AttributesSource<'_> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.get().serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for AttributesSource<'static> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let map = BTreeMap::<String, String>::deserialize(deserializer)?;
Ok(Self::owned(map))
}
}
#[derive(Debug, Clone)]
pub struct NodeContext<'a> {
pub node_type: NodeType,
pub tag_name: Cow<'a, str>,
attributes: AttributesSource<'a>,
pub depth: usize,
pub index_in_parent: usize,
pub parent_tag: Option<Cow<'a, str>>,
pub is_inline: bool,
}
#[cfg(feature = "serde")]
impl serde::Serialize for NodeContext<'_> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("NodeContext", 7)?;
s.serialize_field("node_type", &self.node_type)?;
s.serialize_field("tag_name", &self.tag_name)?;
s.serialize_field("attributes", &self.attributes)?;
s.serialize_field("depth", &self.depth)?;
s.serialize_field("index_in_parent", &self.index_in_parent)?;
s.serialize_field("parent_tag", &self.parent_tag)?;
s.serialize_field("is_inline", &self.is_inline)?;
s.end()
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for NodeContext<'static> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::{self, MapAccess, Visitor};
#[derive(serde::Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
enum Field {
NodeType,
TagName,
Attributes,
Depth,
IndexInParent,
ParentTag,
IsInline,
}
struct NodeContextVisitor;
impl<'de> Visitor<'de> for NodeContextVisitor {
type Value = NodeContext<'static>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("struct NodeContext")
}
fn visit_map<V: MapAccess<'de>>(self, mut map: V) -> Result<NodeContext<'static>, V::Error> {
let mut node_type = None;
let mut tag_name: Option<String> = None;
let mut attributes: Option<BTreeMap<String, String>> = None;
let mut depth = None;
let mut index_in_parent = None;
let mut parent_tag: Option<Option<String>> = None;
let mut is_inline = None;
while let Some(key) = map.next_key()? {
match key {
Field::NodeType => node_type = Some(map.next_value()?),
Field::TagName => tag_name = Some(map.next_value()?),
Field::Attributes => attributes = Some(map.next_value()?),
Field::Depth => depth = Some(map.next_value()?),
Field::IndexInParent => index_in_parent = Some(map.next_value()?),
Field::ParentTag => parent_tag = Some(map.next_value()?),
Field::IsInline => is_inline = Some(map.next_value()?),
}
}
Ok(NodeContext {
node_type: node_type.ok_or_else(|| de::Error::missing_field("node_type"))?,
tag_name: Cow::Owned(tag_name.ok_or_else(|| de::Error::missing_field("tag_name"))?),
attributes: AttributesSource::owned(
attributes.ok_or_else(|| de::Error::missing_field("attributes"))?,
),
depth: depth.ok_or_else(|| de::Error::missing_field("depth"))?,
index_in_parent: index_in_parent.ok_or_else(|| de::Error::missing_field("index_in_parent"))?,
parent_tag: parent_tag
.ok_or_else(|| de::Error::missing_field("parent_tag"))?
.map(Cow::Owned),
is_inline: is_inline.ok_or_else(|| de::Error::missing_field("is_inline"))?,
})
}
}
const FIELDS: &[&str] = &[
"node_type",
"tag_name",
"attributes",
"depth",
"index_in_parent",
"parent_tag",
"is_inline",
];
deserializer.deserialize_struct("NodeContext", FIELDS, NodeContextVisitor)
}
}
impl<'a> NodeContext<'a> {
#[inline]
pub fn attributes(&self) -> &BTreeMap<String, String> {
self.attributes.get()
}
#[must_use]
pub const fn with_borrowed_attributes(
node_type: NodeType,
tag_name: Cow<'a, str>,
attributes: &'a BTreeMap<String, String>,
depth: usize,
index_in_parent: usize,
parent_tag: Option<Cow<'a, str>>,
is_inline: bool,
) -> Self {
Self {
node_type,
tag_name,
attributes: AttributesSource::borrowed(attributes),
depth,
index_in_parent,
parent_tag,
is_inline,
}
}
#[must_use]
pub const fn with_owned_attributes(
node_type: NodeType,
tag_name: Cow<'a, str>,
attributes: BTreeMap<String, String>,
depth: usize,
index_in_parent: usize,
parent_tag: Option<Cow<'a, str>>,
is_inline: bool,
) -> Self {
Self {
node_type,
tag_name,
attributes: AttributesSource::owned(attributes),
depth,
index_in_parent,
parent_tag,
is_inline,
}
}
#[cfg(feature = "visitor")]
pub(crate) const fn with_lazy_attributes(
node_type: NodeType,
tag_name: Cow<'a, str>,
tag: &'a tl::HTMLTag<'a>,
depth: usize,
index_in_parent: usize,
parent_tag: Option<Cow<'a, str>>,
is_inline: bool,
) -> Self {
Self {
node_type,
tag_name,
attributes: AttributesSource::lazy(tag),
depth,
index_in_parent,
parent_tag,
is_inline,
}
}
#[must_use]
pub fn into_owned(self) -> NodeContext<'static> {
NodeContext {
node_type: self.node_type,
tag_name: Cow::Owned(self.tag_name.into_owned()),
attributes: AttributesSource::owned(self.attributes.get().clone()),
depth: self.depth,
index_in_parent: self.index_in_parent,
parent_tag: self.parent_tag.map(|p| Cow::Owned(p.into_owned())),
is_inline: self.is_inline,
}
}
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum VisitResult {
#[default]
Continue,
Custom(String),
Skip,
PreserveHtml,
Error(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_type_equality() {
assert_eq!(NodeType::Text, NodeType::Text);
assert_ne!(NodeType::Text, NodeType::Element);
assert_eq!(NodeType::Heading, NodeType::Heading);
}
#[test]
fn test_node_context_with_borrowed_attributes() {
let attrs = BTreeMap::new();
let ctx = NodeContext::with_borrowed_attributes(
NodeType::Heading,
Cow::Borrowed("h1"),
&attrs,
1,
0,
Some(Cow::Borrowed("body")),
false,
);
assert_eq!(ctx.node_type, NodeType::Heading);
assert_eq!(ctx.tag_name, "h1");
assert_eq!(ctx.depth, 1);
assert!(!ctx.is_inline);
assert!(ctx.attributes().is_empty());
}
#[test]
fn test_node_context_with_owned_attributes() {
let mut attrs = BTreeMap::new();
attrs.insert("class".to_string(), "hero".to_string());
let ctx = NodeContext::with_owned_attributes(NodeType::Div, Cow::Borrowed("div"), attrs, 0, 0, None, false);
assert_eq!(ctx.attributes().get("class").map(String::as_str), Some("hero"));
}
#[test]
fn test_node_context_attributes_empty() {
let ctx =
NodeContext::with_borrowed_attributes(NodeType::Text, Cow::Borrowed(""), &EMPTY_ATTRS, 0, 0, None, true);
assert!(ctx.attributes().is_empty());
}
#[test]
fn test_node_context_into_owned() {
let mut attrs = BTreeMap::new();
attrs.insert("id".to_string(), "main".to_string());
let ctx = NodeContext::with_owned_attributes(
NodeType::Div,
Cow::Borrowed("div"),
attrs,
1,
0,
Some(Cow::Borrowed("body")),
false,
);
let owned = ctx.into_owned();
assert_eq!(owned.tag_name, "div");
assert_eq!(owned.attributes().get("id").map(String::as_str), Some("main"));
assert_eq!(owned.parent_tag, Some(Cow::Borrowed("body")));
}
#[test]
fn test_node_context_clone() {
let mut attrs = BTreeMap::new();
attrs.insert("href".to_string(), "https://example.com".to_string());
let ctx = NodeContext::with_owned_attributes(
NodeType::Link,
Cow::Borrowed("a"),
attrs,
2,
1,
Some(Cow::Borrowed("p")),
true,
);
let cloned = ctx.clone();
assert_eq!(
cloned.attributes().get("href").map(String::as_str),
Some("https://example.com")
);
}
#[test]
fn test_visit_result_variants() {
let continue_result = VisitResult::Continue;
matches!(continue_result, VisitResult::Continue);
let custom_result = VisitResult::Custom("# Custom Output".to_string());
if let VisitResult::Custom(output) = custom_result {
assert_eq!(output, "# Custom Output");
}
let skip_result = VisitResult::Skip;
matches!(skip_result, VisitResult::Skip);
let preserve_result = VisitResult::PreserveHtml;
matches!(preserve_result, VisitResult::PreserveHtml);
let error_result = VisitResult::Error("Test error".to_string());
if let VisitResult::Error(msg) = error_result {
assert_eq!(msg, "Test error");
}
}
}