mod art_block;
mod variant;
use crate::types::{
ArtDescriptor, ArtParseError, ArtParseOptions, ArtParseResult, ArtScriptBlock, ArtStyleBlock,
SourceLocation,
};
use memchr::{memchr, memmem};
use vize_carton::Bump;
type SfcBlocksParseResult<'a> = Result<
(
Option<ArtScriptBlock<'a>>,
Option<ArtScriptBlock<'a>>,
vize_carton::Vec<'a, ArtStyleBlock<'a>>,
),
ArtParseError,
>;
#[inline]
pub fn parse_art<'a>(
allocator: &'a Bump,
source: &'a str,
options: ArtParseOptions,
) -> ArtParseResult<'a> {
let bytes = source.as_bytes();
let filename: &'a str = if options.filename.is_empty() {
""
} else {
allocator.alloc_str(&options.filename)
};
let art_block = art_block::find_art_block(bytes, source)?;
let metadata = art_block::parse_metadata(allocator, &art_block)?;
let variants = variant::parse_variants(
allocator,
art_block.content,
source,
art_block.content_start,
)?;
let (script_setup, script, styles) = parse_sfc_blocks(allocator, source)?;
Ok(ArtDescriptor {
filename,
source,
metadata,
variants,
script_setup,
script,
styles,
})
}
#[derive(Debug)]
pub(crate) struct BlockInfo<'a> {
pub attrs_str: &'a str,
pub content: &'a str,
pub content_start: usize,
}
#[inline]
fn parse_sfc_blocks<'a>(allocator: &'a Bump, source: &'a str) -> SfcBlocksParseResult<'a> {
let bytes = source.as_bytes();
let mut script_setup: Option<ArtScriptBlock<'a>> = None;
let mut script: Option<ArtScriptBlock<'a>> = None;
let mut styles = vize_carton::Vec::new_in(allocator);
let script_finder = memmem::Finder::new(b"<script");
let style_finder = memmem::Finder::new(b"<style");
let mut pos = 0;
while pos < bytes.len() {
let script_pos = script_finder.find(&bytes[pos..]).map(|p| pos + p);
let style_pos = style_finder.find(&bytes[pos..]).map(|p| pos + p);
match (script_pos, style_pos) {
(Some(sp), Some(stp)) if sp < stp => {
if let Some((block, end)) = parse_script_block(source, sp)? {
if block.setup {
script_setup = Some(block);
} else {
script = Some(block);
}
pos = end;
} else {
pos = sp + 1;
}
}
(Some(sp), Some(stp)) if stp < sp => {
if let Some((block, end)) = parse_style_block(source, stp)? {
styles.push(block);
pos = end;
} else {
pos = stp + 1;
}
}
(Some(sp), None) => {
if let Some((block, end)) = parse_script_block(source, sp)? {
if block.setup {
script_setup = Some(block);
} else {
script = Some(block);
}
pos = end;
} else {
pos = sp + 1;
}
}
(None, Some(stp)) => {
if let Some((block, end)) = parse_style_block(source, stp)? {
styles.push(block);
pos = end;
} else {
pos = stp + 1;
}
}
(None, None) => break,
_ => pos += 1,
}
}
Ok((script_setup, script, styles))
}
#[inline]
fn parse_script_block<'a>(
source: &'a str,
start: usize,
) -> Result<Option<(ArtScriptBlock<'a>, usize)>, ArtParseError> {
let bytes = source.as_bytes();
let Some(tag_end) = memchr(b'>', &bytes[start..]) else {
return Ok(None);
};
let tag_end = start + tag_end;
if bytes[tag_end - 1] == b'/' {
return Ok(None);
}
let attrs_str = &source[start + 7..tag_end];
let lang = extract_attr(attrs_str, "lang");
let is_setup = has_attr_fast(attrs_str.as_bytes(), b"setup");
let content_start = tag_end + 1;
let close_finder = memmem::Finder::new(b"</script>");
let Some(close_offset) = close_finder.find(&bytes[content_start..]) else {
return Ok(None);
};
let close_pos = content_start + close_offset;
let content = &source[content_start..close_pos];
let loc = calculate_location_fast(source, start as u32, (close_pos + 9) as u32);
Ok(Some((
ArtScriptBlock {
content: content.trim(),
lang,
setup: is_setup,
loc: Some(loc),
},
close_pos + 9, )))
}
#[inline]
fn parse_style_block<'a>(
source: &'a str,
start: usize,
) -> Result<Option<(ArtStyleBlock<'a>, usize)>, ArtParseError> {
let bytes = source.as_bytes();
let Some(tag_end) = memchr(b'>', &bytes[start..]) else {
return Ok(None);
};
let tag_end = start + tag_end;
if bytes[tag_end - 1] == b'/' {
return Ok(None);
}
let attrs_str = &source[start + 6..tag_end];
let lang = extract_attr(attrs_str, "lang");
let is_scoped = has_attr_fast(attrs_str.as_bytes(), b"scoped");
let content_start = tag_end + 1;
let close_finder = memmem::Finder::new(b"</style>");
let Some(close_offset) = close_finder.find(&bytes[content_start..]) else {
return Ok(None);
};
let close_pos = content_start + close_offset;
let content = &source[content_start..close_pos];
let loc = calculate_location_fast(source, start as u32, (close_pos + 8) as u32);
Ok(Some((
ArtStyleBlock {
content: content.trim(),
lang,
scoped: is_scoped,
loc: Some(loc),
},
close_pos + 8, )))
}
#[inline]
pub(crate) fn extract_attr<'a>(attrs: &'a str, name: &str) -> Option<&'a str> {
let bytes = attrs.as_bytes();
let name_bytes = name.as_bytes();
let mut pos = 0;
while pos < bytes.len() {
if let Some(offset) = memmem::find(&bytes[pos..], name_bytes) {
let match_pos = pos + offset;
let after_name = match_pos + name_bytes.len();
if after_name < bytes.len() && bytes[after_name] == b'=' {
let before_ok = match_pos == 0 || bytes[match_pos - 1].is_ascii_whitespace();
if before_ok {
let value_start = after_name + 1;
if value_start >= bytes.len() {
return None;
}
let quote = bytes[value_start];
if quote == b'"' || quote == b'\'' {
let search_start = value_start + 1;
if let Some(end_offset) = memchr(quote, &bytes[search_start..]) {
return Some(&attrs[search_start..search_start + end_offset]);
}
} else {
let mut end = value_start;
while end < bytes.len()
&& !bytes[end].is_ascii_whitespace()
&& bytes[end] != b'>'
&& bytes[end] != b'/'
{
end += 1;
}
if end > value_start {
return Some(&attrs[value_start..end]);
}
}
}
}
pos = match_pos + 1;
} else {
break;
}
}
None
}
#[inline]
pub(crate) fn has_attr_fast(bytes: &[u8], name: &[u8]) -> bool {
let mut pos = 0;
while pos < bytes.len() {
if let Some(offset) = memmem::find(&bytes[pos..], name) {
let match_pos = pos + offset;
let after_name = match_pos + name.len();
let before_ok = match_pos == 0 || bytes[match_pos - 1].is_ascii_whitespace();
let after_ok = after_name >= bytes.len()
|| bytes[after_name].is_ascii_whitespace()
|| bytes[after_name] == b'>'
|| bytes[after_name] == b'='
|| bytes[after_name] == b'/';
if before_ok && after_ok {
return true;
}
pos = match_pos + 1;
} else {
break;
}
}
false
}
#[inline]
pub(crate) fn has_attr(attrs: &str, name: &str) -> bool {
has_attr_fast(attrs.as_bytes(), name.as_bytes())
}
#[inline]
pub(crate) fn calculate_location_fast(source: &str, start: u32, end: u32) -> SourceLocation {
let bytes = source.as_bytes();
let start_usize = start as usize;
let mut line = 1u32;
let mut last_newline = 0usize;
for pos in memchr::memchr_iter(b'\n', &bytes[..start_usize]) {
line += 1;
last_newline = pos + 1;
}
let column = (start_usize - last_newline) as u32;
SourceLocation::new(start, end, line, column)
}
#[cfg(test)]
mod tests {
use super::{extract_attr, has_attr, parse_art};
use crate::types::{ArtParseError, ArtParseOptions};
use vize_carton::Bump;
#[test]
fn test_parse_simple_art() {
let allocator = Bump::new();
let source = r#"
<art title="Button" component="./Button.vue">
<variant name="Primary" default>
<Button>Click me</Button>
</variant>
</art>
<script setup lang="ts">
import Button from './Button.vue'
</script>
"#;
let result = parse_art(&allocator, source, ArtParseOptions::default());
assert!(result.is_ok());
let desc = result.unwrap();
assert_eq!(desc.metadata.title, "Button");
assert_eq!(desc.metadata.component, Some("./Button.vue"));
assert_eq!(desc.variants.len(), 1);
assert_eq!(desc.variants[0].name, "Primary");
assert!(desc.variants[0].is_default);
assert!(desc.script_setup.is_some());
}
#[test]
fn test_parse_multiple_variants() {
let allocator = Bump::new();
let source = r#"
<art title="Button">
<variant name="Primary" default>
<Button variant="primary">Click</Button>
</variant>
<variant name="Secondary">
<Button variant="secondary">Click</Button>
</variant>
<variant name="Disabled">
<Button disabled>Click</Button>
</variant>
</art>
"#;
let result = parse_art(&allocator, source, ArtParseOptions::default());
assert!(result.is_ok());
let desc = result.unwrap();
assert_eq!(desc.variants.len(), 3);
assert_eq!(desc.variants[0].name, "Primary");
assert_eq!(desc.variants[1].name, "Secondary");
assert_eq!(desc.variants[2].name, "Disabled");
}
#[test]
fn test_extract_attr() {
assert_eq!(extract_attr(r#"title="Hello""#, "title"), Some("Hello"));
assert_eq!(extract_attr(r#"title='Hello'"#, "title"), Some("Hello"));
assert_eq!(extract_attr(r#"title=Hello"#, "title"), Some("Hello"));
assert_eq!(extract_attr(r#"foo="bar""#, "title"), None);
}
#[test]
fn test_has_attr() {
assert!(has_attr("default scoped", "default"));
assert!(has_attr("scoped default", "default"));
assert!(has_attr("default", "default"));
assert!(!has_attr("defaults", "default"));
}
#[test]
fn test_missing_title_error() {
let allocator = Bump::new();
let source = r#"<art><variant name="Test"></variant></art>"#;
let result = parse_art(&allocator, source, ArtParseOptions::default());
assert!(matches!(result, Err(ArtParseError::MissingTitle)));
}
#[test]
fn test_no_art_block_error() {
let allocator = Bump::new();
let source = r#"<template><div>Hello</div></template>"#;
let result = parse_art(&allocator, source, ArtParseOptions::default());
assert!(matches!(result, Err(ArtParseError::NoArtBlock)));
}
}