use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
use crate::error::{ConversionError, Result};
use crate::visitor::{HtmlVisitor, NodeContext, NodeType, VisitResult};
#[allow(dead_code)]
#[inline]
pub fn build_node_context(
node_type: NodeType,
tag_name: &str,
attributes: &BTreeMap<String, String>,
depth: usize,
index_in_parent: usize,
parent_tag: Option<&str>,
is_inline: bool,
) -> NodeContext {
NodeContext {
node_type,
tag_name: tag_name.to_string(),
attributes: attributes.clone(),
depth,
index_in_parent,
parent_tag: parent_tag.map(String::from),
is_inline,
}
}
#[allow(dead_code)]
#[inline]
pub fn dispatch_visitor<F>(visitor: &Option<Rc<RefCell<dyn HtmlVisitor>>>, callback: F) -> Result<VisitorDispatch>
where
F: FnOnce(&mut dyn HtmlVisitor) -> VisitResult,
{
let Some(visitor_rc) = visitor else {
return Ok(VisitorDispatch::Continue);
};
let mut visitor_ref = visitor_rc.borrow_mut();
let result = callback(&mut *visitor_ref);
match result {
VisitResult::Continue => Ok(VisitorDispatch::Continue),
VisitResult::Custom(output) => Ok(VisitorDispatch::Custom(output)),
VisitResult::Skip => Ok(VisitorDispatch::Skip),
VisitResult::PreserveHtml => Ok(VisitorDispatch::PreserveHtml),
VisitResult::Error(msg) => Err(ConversionError::Visitor(msg)),
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum VisitorDispatch {
Continue,
Custom(String),
Skip,
PreserveHtml,
}
impl VisitorDispatch {
#[allow(dead_code)]
#[inline]
#[must_use]
pub const fn is_continue(&self) -> bool {
matches!(self, Self::Continue)
}
#[allow(dead_code)]
#[inline]
#[must_use]
pub const fn is_custom(&self) -> bool {
matches!(self, Self::Custom(_))
}
#[allow(dead_code)]
#[inline]
#[must_use]
pub const fn is_skip(&self) -> bool {
matches!(self, Self::Skip)
}
#[allow(dead_code)]
#[inline]
#[must_use]
pub const fn is_preserve_html(&self) -> bool {
matches!(self, Self::PreserveHtml)
}
#[allow(dead_code)]
#[inline]
#[must_use]
pub fn into_custom(self) -> Option<String> {
match self {
Self::Custom(output) => Some(output),
_ => None,
}
}
#[allow(dead_code)]
#[inline]
#[must_use]
pub fn as_custom(&self) -> Option<&str> {
match self {
Self::Custom(output) => Some(output),
_ => None,
}
}
}
#[macro_export]
macro_rules! try_visitor {
($visitor:expr, $method:ident, $ctx:expr $(, $arg:expr)*) => {{
let dispatch = $crate::visitor_helpers::dispatch_visitor(
$visitor,
|v| v.$method($ctx $(, $arg)*),
)?;
match dispatch {
$crate::visitor_helpers::VisitorDispatch::Continue => {
}
$crate::visitor_helpers::VisitorDispatch::Custom(output) => {
return Ok(output);
}
$crate::visitor_helpers::VisitorDispatch::Skip => {
return Ok(String::new());
}
$crate::visitor_helpers::VisitorDispatch::PreserveHtml => {
}
}
}};
}
#[macro_export]
macro_rules! try_visitor_element_start {
($visitor:expr, $ctx:expr) => {{
$crate::try_visitor!($visitor, visit_element_start, $ctx);
}};
}
#[macro_export]
macro_rules! try_visitor_element_end {
($visitor:expr, $ctx:expr, $output:expr) => {{
$crate::try_visitor!($visitor, visit_element_end, $ctx, $output);
}};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_node_context() {
let mut attrs = BTreeMap::new();
attrs.insert("id".to_string(), "main".to_string());
attrs.insert("class".to_string(), "container".to_string());
let ctx = build_node_context(NodeType::Div, "div", &attrs, 2, 3, Some("body"), false);
assert_eq!(ctx.node_type, NodeType::Div);
assert_eq!(ctx.tag_name, "div");
assert_eq!(ctx.depth, 2);
assert_eq!(ctx.index_in_parent, 3);
assert_eq!(ctx.parent_tag, Some("body".to_string()));
assert!(!ctx.is_inline);
assert_eq!(ctx.attributes.len(), 2);
assert_eq!(ctx.attributes.get("id"), Some(&"main".to_string()));
}
#[test]
fn test_build_node_context_no_parent() {
let attrs = BTreeMap::new();
let ctx = build_node_context(NodeType::Html, "html", &attrs, 0, 0, None, false);
assert_eq!(ctx.node_type, NodeType::Html);
assert_eq!(ctx.parent_tag, None);
assert!(ctx.attributes.is_empty());
}
#[test]
fn test_dispatch_visitor_none() {
let visitor: Option<Rc<RefCell<dyn HtmlVisitor>>> = None;
let result = dispatch_visitor(&visitor, |v| {
let ctx = NodeContext {
node_type: NodeType::Text,
tag_name: String::new(),
attributes: BTreeMap::new(),
depth: 0,
index_in_parent: 0,
parent_tag: None,
is_inline: true,
};
v.visit_text(&ctx, "test")
})
.unwrap();
assert!(result.is_continue());
}
#[derive(Debug)]
struct TestVisitor {
mode: TestMode,
}
#[derive(Debug)]
enum TestMode {
Continue,
Custom,
Skip,
PreserveHtml,
Error,
}
impl HtmlVisitor for TestVisitor {
fn visit_text(&mut self, _ctx: &NodeContext, text: &str) -> VisitResult {
match self.mode {
TestMode::Continue => VisitResult::Continue,
TestMode::Custom => VisitResult::Custom(format!("CUSTOM: {}", text)),
TestMode::Skip => VisitResult::Skip,
TestMode::PreserveHtml => VisitResult::PreserveHtml,
TestMode::Error => VisitResult::Error("test error".to_string()),
}
}
}
#[test]
fn test_dispatch_visitor_continue() {
let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor {
mode: TestMode::Continue,
}));
let visitor_opt = Some(visitor);
let ctx = NodeContext {
node_type: NodeType::Text,
tag_name: String::new(),
attributes: BTreeMap::new(),
depth: 0,
index_in_parent: 0,
parent_tag: None,
is_inline: true,
};
let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, "hello")).unwrap();
assert!(result.is_continue());
}
#[test]
fn test_dispatch_visitor_custom() {
let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor { mode: TestMode::Custom }));
let visitor_opt = Some(visitor);
let ctx = NodeContext {
node_type: NodeType::Text,
tag_name: String::new(),
attributes: BTreeMap::new(),
depth: 0,
index_in_parent: 0,
parent_tag: None,
is_inline: true,
};
let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, "hello")).unwrap();
assert!(result.is_custom());
assert_eq!(result.as_custom(), Some("CUSTOM: hello"));
}
#[test]
fn test_dispatch_visitor_skip() {
let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor { mode: TestMode::Skip }));
let visitor_opt = Some(visitor);
let ctx = NodeContext {
node_type: NodeType::Text,
tag_name: String::new(),
attributes: BTreeMap::new(),
depth: 0,
index_in_parent: 0,
parent_tag: None,
is_inline: true,
};
let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, "hello")).unwrap();
assert!(result.is_skip());
}
#[test]
fn test_dispatch_visitor_preserve_html() {
let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor {
mode: TestMode::PreserveHtml,
}));
let visitor_opt = Some(visitor);
let ctx = NodeContext {
node_type: NodeType::Text,
tag_name: String::new(),
attributes: BTreeMap::new(),
depth: 0,
index_in_parent: 0,
parent_tag: None,
is_inline: true,
};
let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, "hello")).unwrap();
assert!(result.is_preserve_html());
}
#[test]
fn test_dispatch_visitor_error() {
let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor { mode: TestMode::Error }));
let visitor_opt = Some(visitor);
let ctx = NodeContext {
node_type: NodeType::Text,
tag_name: String::new(),
attributes: BTreeMap::new(),
depth: 0,
index_in_parent: 0,
parent_tag: None,
is_inline: true,
};
let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, "hello"));
assert!(result.is_err());
if let Err(ConversionError::Visitor(msg)) = result {
assert_eq!(msg, "test error");
} else {
panic!("Expected Visitor error");
}
}
#[test]
fn test_visitor_dispatch_predicates() {
let continue_dispatch = VisitorDispatch::Continue;
assert!(continue_dispatch.is_continue());
assert!(!continue_dispatch.is_custom());
assert!(!continue_dispatch.is_skip());
let custom_dispatch = VisitorDispatch::Custom("output".to_string());
assert!(!custom_dispatch.is_continue());
assert!(custom_dispatch.is_custom());
assert!(!custom_dispatch.is_skip());
let skip_dispatch = VisitorDispatch::Skip;
assert!(!skip_dispatch.is_continue());
assert!(!skip_dispatch.is_custom());
assert!(skip_dispatch.is_skip());
let preserve_dispatch = VisitorDispatch::PreserveHtml;
assert!(!preserve_dispatch.is_continue());
assert!(preserve_dispatch.is_preserve_html());
}
#[test]
fn test_visitor_dispatch_into_custom() {
let custom = VisitorDispatch::Custom("test".to_string());
assert_eq!(custom.into_custom(), Some("test".to_string()));
let continue_dispatch = VisitorDispatch::Continue;
assert_eq!(continue_dispatch.into_custom(), None);
}
#[test]
fn test_visitor_dispatch_as_custom() {
let custom = VisitorDispatch::Custom("test".to_string());
assert_eq!(custom.as_custom(), Some("test"));
let skip = VisitorDispatch::Skip;
assert_eq!(skip.as_custom(), None);
}
}