extern crate alloc;
use alloc::rc::Rc;
use core::{error::Error, fmt::Debug, mem::discriminant};
use super::tag_logics::EmptyTagLogic;
use crate::{Attribute, Attributes, TagLogic, TemplateContext};
#[derive(Clone)]
pub enum Item {
Comment {
content: String,
},
Text {
content: String,
},
FilteredTag {
content: String,
},
Tag {
name: String,
attributes: Attributes,
items: Vec<Item>,
tag_logic: Rc<dyn TagLogic>,
},
}
impl Item {
pub fn comment(content: &[char]) -> Item {
let mut string_content = String::with_capacity(content.len());
for char in content {
string_content.push(*char);
}
Item::Comment {
content: string_content,
}
}
pub fn text(content: &[char]) -> Item {
let mut string_content = String::with_capacity(content.len());
for char in content {
string_content.push(*char);
}
Item::Text {
content: string_content,
}
}
pub fn tag(name: &[char]) -> Item {
let mut string_name = String::with_capacity(name.len());
for char in name {
string_name.push(*char);
}
Item::Tag {
name: string_name,
attributes: Attributes::new(),
items: Vec::new(),
tag_logic: Rc::new(EmptyTagLogic::new()),
}
}
pub fn filtered_tag() -> Item {
Item::FilteredTag {
content: String::new(),
}
}
pub fn name(&self) -> String {
match self {
Item::Comment { .. } => "Comment",
Item::Text { .. } => "Text",
Item::Tag { name, .. } => name,
Item::FilteredTag { .. } => "Filtered Tag",
}
.to_string()
}
pub fn clone_empty(&self) -> Item {
match self {
Item::Comment { .. } => Item::comment(&[]),
Item::Text { .. } => Item::text(&[]),
Item::Tag { name, .. } => Item::tag(&name.chars().collect::<Vec<_>>()),
Item::FilteredTag { .. } => Item::filtered_tag(),
}
}
pub fn matches(&self, other: &Item) -> bool {
if let (
Item::Tag {
name: self_name, ..
},
Item::Tag {
name: other_name, ..
},
) = (self, other)
{
return self_name == other_name;
}
discriminant(self) == discriminant(other)
}
pub fn push_item(&mut self, item: Item) {
if let Item::Tag { items, .. } = self {
items.push(item)
}
}
pub fn push_attributes(&mut self, attributes: Attributes) {
if let Item::Tag {
attributes: self_attributes,
..
} = self
{
self_attributes.extend(attributes);
}
}
pub fn replace_block(&mut self, block: &Item) {
if let Item::Tag {
name: self_name,
attributes: self_attributes,
items: self_items,
..
} = self
{
if self_name == "BLOCK" {
if let Item::Tag {
attributes: block_attributes,
items: block_items,
..
} = block
{
if let (
Some(Attribute { value: self_id, .. }),
Some(Attribute {
value: block_id, ..
}),
) = (
self_attributes.get_by_key("id"),
block_attributes.get_by_key("id"),
) {
if self_id == block_id {
self_items.clear();
self_items.extend(block_items.clone());
self_attributes.clear();
self_attributes.extend(block_attributes.clone());
return;
}
}
}
}
for item in self_items {
item.replace_block(block);
}
}
}
pub fn run(&self, context: &mut TemplateContext) -> Result<(), Box<dyn Error>> {
match self {
Item::Comment { content } => {
if context.should_remove_comments() {
return Ok(());
}
context.write_all(b"<!--")?;
context.write_all(content.as_bytes())?;
context.write_all(b"-->")?;
}
Item::Text { content } => {
let whitespace = !context.should_remove_whitespaces();
let is_debug = context.is_debug();
const WHITESPACE_CHARS: &[char] = &[' ', '\t', '\n', '\r'];
const QUOTE_CHARS: &[char] = &['"', '\''];
let content_chars = match whitespace {
true => content.chars().collect::<Vec<_>>(),
false => content.trim().chars().collect::<Vec<_>>(),
};
let mut buffer = String::new();
let mut is_in_expression = false;
let mut expression = String::new();
let mut quote = None;
let mut prev_c = 0 as char;
let mut i = 0;
while i < content_chars.len() {
let Some(c1) = content_chars.get(i) else {
break;
};
if let Some(c2) = content_chars.get(i + 1) {
if !is_in_expression && *c1 == '{' && *c2 == '{' {
i += 2;
is_in_expression = true;
continue;
} else if is_in_expression && *c1 == '}' && *c2 == '}' {
if let Some(value) = context.variables.get(expression.trim()) {
buffer.push_str(value);
} else if is_debug {
buffer
.push_str(format!("{{{{ {} }}}}", expression.trim()).as_str());
}
expression.clear();
is_in_expression = false;
i += 2;
prev_c = 0 as char;
continue;
}
}
if is_in_expression {
expression.push(*c1);
prev_c = *c1;
i += 1;
continue;
}
if let Some(quote_char) = quote {
if c1 == quote_char {
quote = None;
}
buffer.push(*c1);
} else {
if QUOTE_CHARS.contains(c1) {
quote = Some(c1);
}
if !whitespace {
if WHITESPACE_CHARS.contains(c1) {
if !WHITESPACE_CHARS.contains(&prev_c) {
buffer.push(' ');
}
} else {
buffer.push(*c1);
}
} else {
buffer.push(*c1);
}
}
context.write_all(buffer.drain(..).as_str().as_bytes())?;
prev_c = *c1;
i += 1;
}
context.write_all(buffer.drain(..).as_str().as_bytes())?;
}
Item::FilteredTag { content } => {
context.write_all(content.as_bytes())?;
}
Item::Tag {
name,
attributes,
items,
tag_logic,
} => {
tag_logic.run(name, attributes, items, context);
}
}
Ok(())
}
}
impl TryFrom<(&str, Rc<dyn TagLogic>)> for Item {
type Error = ();
fn try_from((name, tag_logic): (&str, Rc<dyn TagLogic>)) -> Result<Self, Self::Error> {
Ok(Item::Tag {
name: name.to_string(),
attributes: Attributes::new(),
items: Vec::new(),
tag_logic,
})
}
}
impl Debug for Item {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Item::Comment { content } => f
.debug_struct("Item::Comment")
.field("content", content)
.finish(),
Item::Text { content } => f
.debug_struct("Item::Text")
.field("content", content)
.finish(),
Item::FilteredTag { content } => f
.debug_struct("Item::FilteredTag")
.field("content", content)
.finish(),
Item::Tag {
name,
attributes,
items,
tag_logic,
} => f
.debug_struct("Item::Text")
.field("name", name)
.field("attributes", attributes)
.field("items", items)
.field("tag_logic", &tag_logic.names())
.finish(),
}
}
}