use serde::{Deserialize, Serialize};
use vize_carton::{Bump, FxHashMap, Vec as BumpVec};
#[derive(Debug)]
pub struct ArtDescriptor<'a> {
pub filename: &'a str,
pub source: &'a str,
pub metadata: ArtMetadata<'a>,
pub variants: BumpVec<'a, ArtVariant<'a>>,
pub script_setup: Option<ArtScriptBlock<'a>>,
pub script: Option<ArtScriptBlock<'a>>,
pub styles: BumpVec<'a, ArtStyleBlock<'a>>,
}
#[derive(Debug)]
pub struct ArtMetadata<'a> {
pub title: &'a str,
pub description: Option<&'a str>,
pub component: Option<&'a str>,
pub category: Option<&'a str>,
pub tags: BumpVec<'a, &'a str>,
pub status: ArtStatus,
pub order: Option<u32>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ArtStatus {
Draft,
#[default]
Ready,
Deprecated,
}
#[derive(Debug)]
pub struct ArtVariant<'a> {
pub name: &'a str,
pub template: &'a str,
pub is_default: bool,
pub args: FxHashMap<&'a str, serde_json::Value>,
pub viewport: Option<ViewportConfig>,
pub skip_vrt: bool,
pub loc: Option<SourceLocation>,
}
#[derive(Debug)]
pub struct ArtScriptBlock<'a> {
pub content: &'a str,
pub lang: Option<&'a str>,
pub setup: bool,
pub loc: Option<SourceLocation>,
}
#[derive(Debug)]
pub struct ArtStyleBlock<'a> {
pub content: &'a str,
pub lang: Option<&'a str>,
pub scoped: bool,
pub loc: Option<SourceLocation>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ViewportConfig {
pub width: u32,
pub height: u32,
pub device_scale_factor: Option<f32>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct SourceLocation {
pub start: u32,
pub end: u32,
pub start_line: u32,
pub start_column: u32,
}
#[derive(Debug, Clone, Default)]
pub struct ArtParseOptions {
pub filename: String,
}
pub type ArtParseResult<'a> = Result<ArtDescriptor<'a>, ArtParseError>;
#[derive(Debug, Clone, thiserror::Error)]
pub enum ArtParseError {
#[error("Missing required 'title' attribute in <art> block")]
MissingTitle,
#[error("Missing required 'name' attribute in <variant> block at line {line}")]
MissingVariantName { line: u32 },
#[error("No <art> block found in file")]
NoArtBlock,
#[error("Invalid attribute value for '{attr}': {message}")]
InvalidAttribute { attr: String, message: String },
#[error("Parse error at line {line}: {message}")]
ParseError { line: u32, message: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CsfOutput {
pub code: String,
pub filename: String,
}
impl<'a> ArtDescriptor<'a> {
#[inline]
pub fn new(allocator: &'a Bump, filename: &'a str, source: &'a str) -> Self {
Self {
filename,
source,
metadata: ArtMetadata::new(allocator),
variants: BumpVec::new_in(allocator),
script_setup: None,
script: None,
styles: BumpVec::new_in(allocator),
}
}
#[inline]
pub fn default_variant(&self) -> Option<&ArtVariant<'a>> {
self.variants
.iter()
.find(|v| v.is_default)
.or_else(|| self.variants.first())
}
}
impl<'a> ArtMetadata<'a> {
#[inline]
pub fn new(allocator: &'a Bump) -> Self {
Self {
title: "",
description: None,
component: None,
category: None,
tags: BumpVec::new_in(allocator),
status: ArtStatus::default(),
order: None,
}
}
}
impl<'a> ArtVariant<'a> {
#[inline]
pub fn new(name: &'a str, template: &'a str) -> Self {
Self {
name,
template,
is_default: false,
args: FxHashMap::default(),
viewport: None,
skip_vrt: false,
loc: None,
}
}
}
impl Default for ViewportConfig {
#[inline]
fn default() -> Self {
Self {
width: 1280,
height: 720,
device_scale_factor: Some(1.0),
}
}
}
impl SourceLocation {
#[inline]
pub const fn new(start: u32, end: u32, start_line: u32, start_column: u32) -> Self {
Self {
start,
end,
start_line,
start_column,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtDescriptorOwned {
pub filename: String,
pub source: String,
pub metadata: ArtMetadataOwned,
pub variants: Vec<ArtVariantOwned>,
pub script_setup: Option<ArtScriptBlockOwned>,
pub script: Option<ArtScriptBlockOwned>,
pub styles: Vec<ArtStyleBlockOwned>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtMetadataOwned {
pub title: String,
pub description: Option<String>,
pub component: Option<String>,
pub category: Option<String>,
pub tags: Vec<String>,
pub status: ArtStatus,
pub order: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtVariantOwned {
pub name: String,
pub template: String,
pub is_default: bool,
pub args: FxHashMap<String, serde_json::Value>,
pub viewport: Option<ViewportConfig>,
pub skip_vrt: bool,
pub loc: Option<SourceLocation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtScriptBlockOwned {
pub content: String,
pub lang: Option<String>,
pub setup: bool,
pub loc: Option<SourceLocation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtStyleBlockOwned {
pub content: String,
pub lang: Option<String>,
pub scoped: bool,
pub loc: Option<SourceLocation>,
}
impl<'a> ArtDescriptor<'a> {
pub fn into_owned(self) -> ArtDescriptorOwned {
ArtDescriptorOwned {
filename: self.filename.to_string(),
source: self.source.to_string(),
metadata: self.metadata.into_owned(),
variants: self.variants.into_iter().map(|v| v.into_owned()).collect(),
script_setup: self.script_setup.map(|s| s.into_owned()),
script: self.script.map(|s| s.into_owned()),
styles: self.styles.into_iter().map(|s| s.into_owned()).collect(),
}
}
}
impl<'a> ArtMetadata<'a> {
pub fn into_owned(self) -> ArtMetadataOwned {
ArtMetadataOwned {
title: self.title.to_string(),
description: self.description.map(|s| s.to_string()),
component: self.component.map(|s| s.to_string()),
category: self.category.map(|s| s.to_string()),
tags: self.tags.into_iter().map(|s| s.to_string()).collect(),
status: self.status,
order: self.order,
}
}
}
impl<'a> ArtVariant<'a> {
pub fn into_owned(self) -> ArtVariantOwned {
ArtVariantOwned {
name: self.name.to_string(),
template: self.template.to_string(),
is_default: self.is_default,
args: self
.args
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect(),
viewport: self.viewport,
skip_vrt: self.skip_vrt,
loc: self.loc,
}
}
}
impl<'a> ArtScriptBlock<'a> {
pub fn into_owned(self) -> ArtScriptBlockOwned {
ArtScriptBlockOwned {
content: self.content.to_string(),
lang: self.lang.map(|s| s.to_string()),
setup: self.setup,
loc: self.loc,
}
}
}
impl<'a> ArtStyleBlock<'a> {
pub fn into_owned(self) -> ArtStyleBlockOwned {
ArtStyleBlockOwned {
content: self.content.to_string(),
lang: self.lang.map(|s| s.to_string()),
scoped: self.scoped,
loc: self.loc,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_art_descriptor_new() {
let allocator = Bump::new();
let desc = ArtDescriptor::new(&allocator, "test.art.vue", "<art></art>");
assert_eq!(desc.filename, "test.art.vue");
assert!(desc.variants.is_empty());
}
#[test]
fn test_art_status_default() {
assert_eq!(ArtStatus::default(), ArtStatus::Ready);
}
#[test]
fn test_viewport_default() {
let vp = ViewportConfig::default();
assert_eq!(vp.width, 1280);
assert_eq!(vp.height, 720);
}
}