yew-template 0.10.0

A crate for using separate HTML files as Yew objects, with support for seamless localization.
Documentation
use html5ever::{tokenizer::{TokenSink, Token as HtmlToken, TokenSinkResult, TagKind, Tokenizer, TokenizerOpts, BufferQueue}};
use crate::*;

pub(crate) struct HtmlSink<'a> {
    pub(crate) html_parts: &'a mut Vec<HtmlPartWithLine>,
    pub(crate) opened_elements: Vec<Element>,
    pub(crate) args: &'a Args,
}

impl<'a> TokenSink for HtmlSink<'a> {
    type Handle = ();

    fn process_token(&mut self, token: HtmlToken, line_number: u64) -> TokenSinkResult<()> {
        match token {
            HtmlToken::TagToken(tag) => match tag.kind {
                TagKind::StartTag => {
                    let mut element = Element {
                        name: tag.name.to_string(),
                        self_closing: tag.self_closing,
                        open_attrs: tag.attrs.into_iter().map(|a| (a.name.local.to_string(), a.value.to_string())).collect(),
                        close_attrs: Vec::new(),
                        children: Vec::new(),
                    };
                    if element.name == "comp" || element.name == "component" {
                        let Some(real_name) = element.open_attrs.iter().find(|(k, _)| k == "name").map(|(_, v)| v) else {
                            abort!(self.args.path_span, "Missing name attribute on component tag at line {line_number}");
                        };
                        element.name = real_name.to_owned();
                        element.open_attrs.retain(|(k, _)| k != "name");
                    }
                    match element.self_closing {
                        true => match self.opened_elements.last_mut() {
                            Some(container) => container.children.push(HtmlPartWithLine { part: HtmlPart::Element(element), line: line_number as usize }),
                            None => self.html_parts.push(HtmlPartWithLine { part: HtmlPart::Element(element), line: line_number as usize }),
                        },
                        false => self.opened_elements.push(element)
                    }
                },
                TagKind::EndTag => {
                    let mut element = self.opened_elements.pop().unwrap_or_else(|| abort!(self.args.path_span, "Unexpected closing tag {} at line {line_number}", tag.name));
                    if tag.name != element.name {
                        abort!(self.args.path_span, "Unexpected closing tag {} at line {line_number}", tag.name);
                    }
                    element.close_attrs = tag.attrs.into_iter().map(|a| (a.name.local.to_string(), a.value.to_string())).collect();
                    match self.opened_elements.last_mut() {
                        Some(container) => container.children.push(HtmlPartWithLine { part: HtmlPart::Element(element), line: line_number as usize }),
                        None => self.html_parts.push(HtmlPartWithLine { part: HtmlPart::Element(element), line: line_number as usize }),
                    }
                },
            },
            HtmlToken::CharacterTokens(text) => match self.opened_elements.last_mut() {
                Some(container) => container.children.push(HtmlPartWithLine { part: HtmlPart::Text(text.to_string()), line: line_number as usize }),
                None => self.html_parts.push(HtmlPartWithLine { part: HtmlPart::Text(text.to_string()), line: line_number as usize }),
            },
            HtmlToken::NullCharacterToken | HtmlToken::CommentToken(_) | HtmlToken::EOFToken | HtmlToken::DoctypeToken(_) => (),
            HtmlToken::ParseError(e) => abort!(self.args.path_span, format!("Failed to parse template: {e} at line {line_number}")),
        }
        TokenSinkResult::Continue
    }
}

pub(crate) fn read_template(args: &Args) -> Element {
    let template = match std::fs::read_to_string(&args.path) {
        Ok(template) => template,
        Err(e) => abort!(args.path_span, "Failed to read template file at {}: {}", args.path, e),
    };
    let mut html_parts = Vec::new();
    let html_sink = HtmlSink { html_parts: &mut html_parts, opened_elements: Vec::new(), args };
    let mut html_tokenizer = Tokenizer::new(html_sink, TokenizerOpts::default());
    let mut buffer_queue = BufferQueue::new();
    buffer_queue.push_back(template.into());
    let _  = html_tokenizer.feed(&mut buffer_queue);
    html_tokenizer.end();
    let mut root = Element {
        name: "".to_string(),
        open_attrs: Vec::new(),
        close_attrs: Vec::new(),
        self_closing: false,
        children: html_parts,
    };
    root.clean_text();
    root
}