#![allow(clippy::disallowed_macros)]
use crate::types::ArtDescriptor;
use vize_carton::{String, append, cstr};
#[derive(Debug, Clone)]
pub struct VueOutput {
pub code: String,
pub metadata_code: String,
}
pub fn transform_to_vue(art: &ArtDescriptor<'_>) -> VueOutput {
let main_code = generate_main_component(art);
let metadata_code = generate_metadata_module(art);
VueOutput {
code: main_code,
metadata_code,
}
}
fn generate_main_component(art: &ArtDescriptor<'_>) -> String {
let mut code = String::default();
code.push_str("import { defineComponent, h, reactive, markRaw } from 'vue';\n");
if let Some(ref component_path) = art.metadata.component {
append!(code, "import TargetComponent from '{component_path}';\n");
}
if let Some(ref script) = art.script_setup {
for line in script.content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("import ") {
code.push_str(trimmed);
code.push('\n');
}
}
}
code.push('\n');
append!(
code,
"export const metadata = {};\n\n",
generate_metadata_json(art)
);
code.push_str("export const variants = [\n");
for variant in &art.variants {
let args_json = serde_json::to_string(&variant.args).unwrap_or_else(|_| "{}".into());
append!(
code,
" {{ name: '{}', isDefault: {}, args: {}, skipVrt: {} }},\n",
escape_js_string(variant.name),
variant.is_default,
args_json,
variant.skip_vrt
);
}
code.push_str("];\n\n");
for (i, variant) in art.variants.iter().enumerate() {
let component_name = to_pascal_case(variant.name);
let args_json = serde_json::to_string(&variant.args).unwrap_or_else(|_| "{}".into());
append!(
code,
r#"export const {} = defineComponent({{
name: '{}',
setup(props, {{ attrs }}) {{
const defaultArgs = {};
const args = reactive({{ ...defaultArgs, ...attrs }});
return () => h('div', {{ class: 'musea-variant', 'data-variant': '{}' }}, [
{}
]);
}}
}});
"#,
component_name,
component_name,
args_json,
escape_js_string(variant.name),
generate_render_expression(variant.template, art),
);
if variant.is_default {
append!(code, "{component_name}.isDefault = true;\n\n");
}
append!(code, "{component_name}.variantIndex = {i};\n\n");
}
code.push_str(
r#"export default defineComponent({
name: 'ArtGallery',
props: {
variant: { type: String, default: null },
interactive: { type: Boolean, default: false },
},
setup(props) {
const variantComponents = {
"#,
);
for variant in &art.variants {
let component_name = to_pascal_case(variant.name);
append!(
code,
" '{}': {},\n",
escape_js_string(variant.name),
component_name
);
}
code.push_str(
r#" };
return () => {
if (props.variant && variantComponents[props.variant]) {
return h(variantComponents[props.variant]);
}
// Render all variants
return h('div', { class: 'musea-gallery' },
variants.map(v => h(variantComponents[v.name], { key: v.name }))
);
};
}
});
"#,
);
code
}
fn generate_render_expression(template: &str, art: &ArtDescriptor<'_>) -> String {
let uses_target = art.metadata.component.is_some();
if uses_target {
cstr!(
"h(TargetComponent, args, () => `{}`)",
escape_template_literal(template)
)
} else {
cstr!(
"h('div', {{ innerHTML: `{}` }})",
escape_template_literal(template)
)
}
}
fn generate_metadata_json(art: &ArtDescriptor<'_>) -> String {
let mut json = String::default();
json.push_str("{\n");
append!(
json,
" title: '{}',\n",
escape_js_string(art.metadata.title)
);
if let Some(desc) = art.metadata.description {
append!(json, " description: '{}',\n", escape_js_string(desc));
}
if let Some(component) = art.metadata.component {
append!(json, " component: '{}',\n", escape_js_string(component));
}
if let Some(category) = art.metadata.category {
append!(json, " category: '{}',\n", escape_js_string(category));
}
if !art.metadata.tags.is_empty() {
let tags: Vec<String> = art
.metadata
.tags
.iter()
.map(|t| cstr!("'{}'", escape_js_string(t)))
.collect();
append!(json, " tags: [{}],\n", tags.join(", "));
}
append!(
json,
" status: '{}',\n",
status_to_string(art.metadata.status)
);
if let Some(order) = art.metadata.order {
append!(json, " order: {order},\n");
}
append!(json, " variantCount: {},\n", art.variants.len());
json.push('}');
json
}
fn generate_metadata_module(art: &ArtDescriptor<'_>) -> String {
let mut code = String::default();
code.push_str("// Auto-generated metadata module\n");
append!(
code,
"export const metadata = {};\n\n",
generate_metadata_json(art)
);
code.push_str("export const variants = [\n");
for variant in &art.variants {
code.push_str(" {\n");
append!(code, " name: '{}',\n", escape_js_string(variant.name));
append!(code, " isDefault: {},\n", variant.is_default);
append!(code, " skipVrt: {},\n", variant.skip_vrt);
if let Some(ref viewport) = variant.viewport {
append!(
code,
" viewport: {{ width: {}, height: {} }},\n",
viewport.width,
viewport.height
);
}
code.push_str(" },\n");
}
code.push_str("];\n");
code
}
fn status_to_string(status: crate::types::ArtStatus) -> &'static str {
match status {
crate::types::ArtStatus::Draft => "draft",
crate::types::ArtStatus::Ready => "ready",
crate::types::ArtStatus::Deprecated => "deprecated",
}
}
fn to_pascal_case(s: &str) -> String {
let mut result = String::default();
for part in s
.split(|c: char| c.is_whitespace() || c == '-' || c == '_')
.filter(|p| !p.is_empty())
{
let mut chars = part.chars();
if let Some(first) = chars.next() {
for uc in first.to_uppercase() {
result.push(uc);
}
for ch in chars {
result.push(ch);
}
}
}
result
}
fn escape_js_string(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('\'', "\\'")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
.into()
}
fn escape_template_literal(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('`', "\\`")
.replace("${", "\\${")
.into()
}
#[cfg(test)]
mod tests {
use super::{escape_template_literal, to_pascal_case, transform_to_vue};
use crate::parse::parse_art;
use crate::types::ArtParseOptions;
use vize_carton::Bump;
#[test]
fn test_transform_to_vue_basic() {
let allocator = Bump::new();
let source = r#"
<art title="Button" component="./Button.vue">
<variant name="Primary" default>
<Button>Click me</Button>
</variant>
</art>
"#;
let art = parse_art(&allocator, source, ArtParseOptions::default()).unwrap();
let output = transform_to_vue(&art);
insta::assert_debug_snapshot!(output);
}
#[test]
fn test_transform_multiple_variants() {
let allocator = Bump::new();
let source = r#"
<art title="Button" component="./Button.vue">
<variant name="Primary" default>
<Button variant="primary">Primary</Button>
</variant>
<variant name="Secondary">
<Button variant="secondary">Secondary</Button>
</variant>
</art>
"#;
let art = parse_art(&allocator, source, ArtParseOptions::default()).unwrap();
let output = transform_to_vue(&art);
insta::assert_debug_snapshot!(output);
}
#[test]
fn test_to_pascal_case() {
assert_eq!(to_pascal_case("primary"), "Primary");
assert_eq!(to_pascal_case("with icon"), "WithIcon");
assert_eq!(to_pascal_case("my-variant"), "MyVariant");
}
#[test]
fn test_escape_template_literal() {
assert_eq!(escape_template_literal("`code`"), "\\`code\\`");
assert_eq!(escape_template_literal("${var}"), "\\${var}");
}
}