stawege-html-plugin 0.1.2

HTML template engine plugin for Stawege.
Documentation
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(())
    }
}

// -----------------------------
// TryFrom trait implementations
// -----------------------------

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,
        })
    }
}

// --------------------------
// Debug trait implementation
// --------------------------

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(),
        }
    }
}