beuvy 0.1.0

Facade crate for beuvy-runtime plus optional declarative UI authoring.
Documentation
use crate::ast::{
    DeclarativeButtonType, DeclarativeEventBinding, DeclarativeEventKind, DeclarativeOnClick,
    DeclarativeTypographyStyle, DeclarativeUiNode,
};
use crate::error::DeclarativeUiAssetLoadError;
use crate::parser::{
    DeclarativeStateSpec, attr, attr_error, parse_bool_or_condition_attr, parse_class_bindings,
    parse_conditional, parse_event_bindings, parse_node_style, parse_node_style_binding,
    parse_onclick, parse_ref_binding, parse_show_attr, parse_state_visual_styles,
    parse_text_content, parse_utility_class_patch, parse_visual_style, reject_legacy_attrs,
    reject_legacy_bind_attrs, reject_legacy_event_attrs, reject_style_attrs_except,
};
use roxmltree::Node as XmlNode;
use std::collections::BTreeMap;

pub(crate) fn parse_declarative_button_node(
    node: XmlNode<'_, '_>,
    state_specs: &BTreeMap<String, DeclarativeStateSpec>,
) -> Result<DeclarativeUiNode, DeclarativeUiAssetLoadError> {
    reject_legacy_attrs(
        node,
        &["text", "selected", "visible", "active", "v-bind-active"],
    )?;
    reject_legacy_bind_attrs(node)?;
    reject_legacy_event_attrs(node)?;
    reject_style_attrs_except(node, &["style"])?;
    let class_patch = parse_utility_class_patch(node)?;
    let onclick = attr(node, "v-on-click")
        .map(|value| parse_onclick(node, "@click", value, state_specs))
        .transpose()?;
    let mut event_bindings = parse_event_bindings(node)?;
    if let Some(DeclarativeOnClick::DispatchCall { action_id, params }) = &onclick {
        event_bindings.push(DeclarativeEventBinding {
            kind: DeclarativeEventKind::Activate,
            action_id: action_id.clone(),
            params: params.clone(),
        });
    }
    let (disabled, disabled_expr) = parse_bool_or_condition_attr(node, "disabled", state_specs)?;
    let show_expr = parse_show_attr(node, state_specs)?;
    Ok(DeclarativeUiNode::Button {
        node_id: String::new(),
        name: attr(node, "name").unwrap_or_default().to_string(),
        button_type: parse_button_type(node)?,
        class: attr(node, "class").unwrap_or_default().to_string(),
        class_bindings: parse_class_bindings(node, state_specs)?,
        content: parse_text_content(node)?,
        conditional: parse_conditional(node, state_specs)?,
        onclick,
        event_bindings,
        ref_binding: parse_ref_binding(node)?,
        node_override: Some(parse_node_style(node)?),
        style_binding: parse_node_style_binding(node)?,
        visual_style: parse_visual_style(node)?,
        state_visual_styles: parse_state_visual_styles(node)?,
        disabled,
        disabled_expr,
        show_expr,
        label_typography_override: DeclarativeTypographyStyle {
            family_role: class_patch
                .font_family_role
                .map(crate::parser::font_family_role_from_utility),
            size: class_patch.font_size,
            weight: class_patch.font_weight,
            font_style: class_patch.font_style.map(crate::parser::font_style_from_utility),
            line_height: class_patch.line_height,
            letter_spacing_em: class_patch.letter_spacing_em,
            text_transform: class_patch
                .text_transform
                .map(crate::parser::text_transform_from_utility),
        },
    })
}

fn parse_button_type(
    node: XmlNode<'_, '_>,
) -> Result<DeclarativeButtonType, DeclarativeUiAssetLoadError> {
    match attr(node, "type").unwrap_or("button") {
        "" | "button" => Ok(DeclarativeButtonType::Button),
        "submit" => Ok(DeclarativeButtonType::Submit),
        "reset" => Ok(DeclarativeButtonType::Reset),
        raw => Err(attr_error(
            node,
            "type",
            raw,
            "expected button, submit, or reset",
        )),
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        DeclarativeButtonType, DeclarativeClassBinding, DeclarativeOnClick, DeclarativeUiNode,
        parse_declarative_ui_asset,
    };

    #[test]
    fn click_shorthand_parses_button_action() {
        let asset = parse_declarative_ui_asset(
            r#"
<template><button @click="tab = 'inventory'">Open</button></template>
<script>let tab = 'overview';</script>
"#,
        )
        .expect("button should parse");
        let DeclarativeUiNode::Button { onclick, .. } = asset.root else {
            panic!("expected button node");
        };
        assert!(matches!(
            onclick,
            Some(DeclarativeOnClick::Assign { ref name, .. }) if name == "tab"
        ));
    }

    #[test]
    fn class_object_shorthand_parses_button_binding() {
        let asset = parse_declarative_ui_asset(
            r#"
<template><button class="button-root" :class="{ 'btn-selected': tab === 'save' }">Save</button></template>
<script>let tab: "save" | "load" = "save";</script>
"#,
        )
        .expect("button should parse");
        let DeclarativeUiNode::Button { class_bindings, .. } = asset.root else {
            panic!("expected button node");
        };
        assert_eq!(class_bindings.len(), 1);
        assert!(matches!(
            &class_bindings[0],
            DeclarativeClassBinding::RuntimeExpr { .. }
        ));
    }

    #[test]
    fn class_binding_accepts_ternary_string_syntax() {
        let asset = parse_declarative_ui_asset(
            r#"
<template><button :class="tab === 'save' ? 'btn-selected' : ''">Save</button></template>
<script>let tab: "save" | "load" = "save";</script>
"#,
        )
        .expect("ternary class binding should parse");
        let DeclarativeUiNode::Button { class_bindings, .. } = asset.root else {
            panic!("expected button node");
        };
        assert!(matches!(
            &class_bindings[0],
            DeclarativeClassBinding::RuntimeExpr { .. }
        ));
    }

    #[test]
    fn class_binding_accepts_array_syntax() {
        let asset = parse_declarative_ui_asset(
            r#"
<template><button :class="['btn', tab === 'save' ? 'btn-selected' : '']">Save</button></template>
<script>let tab: "save" | "load" = "save";</script>
"#,
        )
        .expect("array class binding should parse");
        let DeclarativeUiNode::Button { class_bindings, .. } = asset.root else {
            panic!("expected button node");
        };
        assert!(matches!(
            &class_bindings[0],
            DeclarativeClassBinding::RuntimeExpr { .. }
        ));
    }

    #[test]
    fn button_active_binding_is_not_supported() {
        let error = parse_declarative_ui_asset(
            r#"
<template><button :active="tab === 'save'">Save</button></template>
<script>let tab: "save" | "load" = "save";</script>
"#,
        )
        .expect_err("active binding should be rejected");
        assert!(error.to_string().contains("active"));
    }

    #[test]
    fn button_type_parses() {
        let asset = parse_declarative_ui_asset(
            r#"<template><button type="submit">Save</button></template>"#,
        )
        .expect("button should parse");
        let DeclarativeUiNode::Button { button_type, .. } = asset.root else {
            panic!("expected button node");
        };
        assert_eq!(button_type, DeclarativeButtonType::Submit);
    }
}