designtime-jsx 1.0.0

Lightweight Rust parser for JSX-style HTML and custom components - built for the DesignTime language.
Documentation
use tl::{Node, Parser, ParserOptions};

#[derive(Debug)]
pub enum RenderNode {
    Element {
        tag_name: String,
        attrs: Vec<(String, String)>,
        children: Vec<RenderNode>,
    },
    Text(String),
    Expr(String),
}

fn parse_text_expr(text: &str) -> Vec<RenderNode> {
    let mut nodes = Vec::new();
    let mut buffer = String::new();
    let mut in_expr = false;

    for c in text.chars() {
        match (c, in_expr) {
            ('{', false) => {
                if !buffer.is_empty() {
                    nodes.push(RenderNode::Text(buffer));
                    buffer = String::new();
                }
                in_expr = true;
            }
            ('}', true) => {
                nodes.push(RenderNode::Expr(buffer));
                buffer = String::new();
                in_expr = false;
            }
            _ => buffer.push(c),
        }
    }

    if !buffer.is_empty() {
        nodes.push(if in_expr {
            RenderNode::Expr(buffer)
        } else {
            RenderNode::Text(buffer)
        });
    }

    nodes
}

fn build_ast(node: &Node, parser: &Parser) -> Option<RenderNode> {
    match node {
        Node::Tag(tag) => {
            let tag_name = tag.name().as_utf8_str().to_string();
            let attrs = tag
                .attributes()
                .iter()
                .filter_map(|(k, v)| v.map(|v| (k.as_ref().to_string(), v.as_ref().to_string())))
                .collect();

            let children = tag
                .children()
                .all(parser)
                .iter()
                .filter_map(|node| build_ast(node, parser))
                .collect();

            Some(RenderNode::Element {
                tag_name,
                attrs,
                children,
            })
        }
        Node::Raw(bytes) => {
            // Convert tl::Bytes to &[u8] using as_bytes()
            let text = String::from_utf8_lossy(bytes.as_bytes());
            let trimmed = text.trim();

            if trimmed.is_empty() {
                None
            } else {
                let nodes = parse_text_expr(trimmed);
                if nodes.is_empty() {
                    None
                } else if nodes.len() == 1 {
                    nodes.into_iter().next()
                } else {
                    Some(RenderNode::Element {
                        tag_name: "text-group".to_string(),
                        attrs: Vec::new(),
                        children: nodes,
                    })
                }
            }
        }
        Node::Comment(_) => None,
    }
}

pub fn parse_render_block(html: &str) -> Result<RenderNode, Box<dyn std::error::Error>> {
    let dom = tl::parse(html, ParserOptions::default())?;
    let parser = dom.parser();

    let children = dom
        .nodes()
        .iter()
        .filter_map(|node| build_ast(node, parser))
        .collect();

    Ok(RenderNode::Element {
        tag_name: "root".to_string(),
        attrs: Vec::new(),
        children,
    })
}

// fn main() -> Result<(), Box<dyn std::error::Error>> {
//     let html = r#"
//         <design-box foo="bar">
//             Hello {user.first} {user.last}!
//         </design-box>
//         <design-button>Click me</design-button>
//     "#;

//     let ast = parse_render_block(html)?;
//     println!("{:#?}", ast);
//     Ok(())
// }