Skip to main content

vize_musea/
parse.rs

1//! Parser for Art files (*.art.vue), on the Davinci L0/L1 stages (P4-13).
2//!
3//! An Art file is an SFC whose gallery definition is the `<art>` custom
4//! block:
5//!
6//! 1. **L0 block splitting** ([`blocks`]) — the shared SFC container scan
7//!    yields `<script>` / `<style>` blocks and the `<art>` custom block as
8//!    L0 [`SourceBlock`](vize_l0::SourceBlock) frames over the authored file.
9//! 2. **L1 tree** ([`art_block`]) — the `<art>` block parses into one
10//!    lossless L1 surface tree; metadata comes from its open tag's attribute
11//!    tokens and variants are its `<variant>` elements ([`variant`]).
12//!
13//! Every string in the descriptor is still a slice of the source (or an
14//! arena string); offsets are file-absolute through the L0 frame.
15
16mod art_block;
17mod attrs;
18mod blocks;
19mod status;
20mod variant;
21
22#[cfg(test)]
23mod divergence_tests;
24#[cfg(test)]
25mod golden_tests;
26
27use crate::types::{ArtDescriptor, ArtParseError, ArtParseOptions, ArtParseResult, SourceLocation};
28use vize_l0::Allocator;
29
30/// Parse an Art file (*.art.vue) into an ArtDescriptor.
31///
32/// Uses arena allocation for all internal collections.
33/// All string data is borrowed from the source - zero allocations for strings.
34///
35/// # Example
36///
37/// ```
38/// use vize_l0::Allocator;
39/// use vize_musea::parse::parse_art;
40/// use vize_musea::types::ArtParseOptions;
41///
42/// let allocator = Allocator::new();
43/// let source = r#"
44/// <art title="Button" component="./Button.vue">
45///   <variant name="Primary" default>
46///     <Button>Click me</Button>
47///   </variant>
48/// </art>
49/// "#;
50///
51/// let result = parse_art(&allocator, source, ArtParseOptions::default());
52/// assert!(result.is_ok());
53/// ```
54pub fn parse_art<'a>(
55    allocator: &'a Allocator,
56    source: &'a str,
57    options: ArtParseOptions,
58) -> ArtParseResult<'a> {
59    // Allocate filename in arena if provided, otherwise use empty str
60    let filename: &'a str = if options.filename.is_empty() {
61        ""
62    } else {
63        allocator.alloc_str(&options.filename)
64    };
65
66    // L0: split the SFC container.
67    let blocks = blocks::split_blocks(allocator, source)?;
68    let frame = blocks.art.ok_or(ArtParseError::NoArtBlock)?;
69
70    // L1: the `<art>` block as a lossless surface tree.
71    let (tree, _surface_errors) = vize_l1::markup::parse(allocator, frame.source());
72    let art = art_block::art_element(&tree).ok_or(ArtParseError::NoArtBlock)?;
73
74    let define_art = blocks
75        .script_setup
76        .as_ref()
77        .and_then(|script| parse_define_art_metadata(allocator, script.content));
78
79    // Metadata from `<art>` attributes, with the defineArt() fallback.
80    let (metadata, _warnings) =
81        art_block::parse_metadata(allocator, &art.open, define_art.as_ref(), filename)?;
82
83    let variants = variant::parse_variants(allocator, &art.children, frame)?;
84
85    Ok(ArtDescriptor {
86        filename,
87        source,
88        metadata,
89        variants,
90        script_setup: blocks.script_setup,
91        script: blocks.script,
92        styles: blocks.styles,
93    })
94}
95
96/// Unknown-`status` warnings of the `<art>` attributes, or an empty list if
97/// the file has no `<art>` block or its metadata cannot be parsed.
98pub(crate) fn art_status_warnings<'a>(
99    allocator: &'a Allocator,
100    source: &'a str,
101    filename: &str,
102) -> vize_l0::Vec<'a, &'a str> {
103    let empty = || vize_l0::Vec::new_in(&allocator);
104    let Some(frame) = blocks::split_blocks(allocator, source)
105        .ok()
106        .and_then(|blocks| blocks.art)
107    else {
108        return empty();
109    };
110    let (tree, _surface_errors) = vize_l1::markup::parse(allocator, frame.source());
111    let Some(art) = art_block::art_element(&tree) else {
112        return empty();
113    };
114    match art_block::parse_metadata(allocator, &art.open, None, filename) {
115        Ok((_, warnings)) => warnings,
116        Err(_) => empty(),
117    }
118}
119
120#[derive(Debug)]
121pub(crate) struct DefineArtMetadata<'a> {
122    pub component_name: Option<&'a str>,
123    pub component: Option<&'a str>,
124    pub title: Option<&'a str>,
125    pub description: Option<&'a str>,
126    pub category: Option<&'a str>,
127    pub tags: vize_l0::Vec<'a, &'a str>,
128    pub status: Option<&'a str>,
129    pub order: Option<u32>,
130}
131
132impl<'a> DefineArtMetadata<'a> {
133    fn new(allocator: &'a Allocator) -> Self {
134        Self {
135            component_name: None,
136            component: None,
137            title: None,
138            description: None,
139            category: None,
140            tags: vize_l0::Vec::new_in(&allocator),
141            status: None,
142            order: None,
143        }
144    }
145}
146
147fn parse_define_art_metadata<'a>(
148    allocator: &'a Allocator,
149    script: &'a str,
150) -> Option<DefineArtMetadata<'a>> {
151    // Cheap pre-check: defineArt() can only return Some if the literal token
152    // "defineArt" appears in the source (it is a compiler macro recognized by
153    // name and cannot be aliased), so skip the OXC parse otherwise.
154    if !script.contains("defineArt") {
155        return None;
156    }
157
158    // The defineArt-only reader: one parse, none of the binding/scope
159    // analysis the full `parse_script_setup` runs for other consumers, and
160    // pinned equal to it by croquis's own differential battery.
161    let art = vize_croquis::script_parser::parse_define_art(script)?;
162    Some(define_art_metadata(allocator, &art))
163}
164
165/// The descriptor-side copy of a `defineArt()` call's metadata.
166pub(crate) fn define_art_metadata<'a>(
167    allocator: &'a Allocator,
168    art: &vize_croquis::macros::ArtDefinition,
169) -> DefineArtMetadata<'a> {
170    let mut meta = DefineArtMetadata::new(allocator);
171
172    meta.component_name = Some(allocator.alloc_str(art.component_name.as_str()));
173    meta.component = art
174        .component_source
175        .as_ref()
176        .map(|source| allocator.alloc_str(source.as_str()));
177    meta.title = art
178        .title
179        .as_ref()
180        .map(|value| allocator.alloc_str(value.as_str()));
181    meta.description = art
182        .description
183        .as_ref()
184        .map(|value| allocator.alloc_str(value.as_str()));
185    meta.category = art
186        .category
187        .as_ref()
188        .map(|value| allocator.alloc_str(value.as_str()));
189    meta.status = art
190        .status
191        .as_ref()
192        .map(|value| allocator.alloc_str(value.as_str()));
193    meta.order = art.order;
194    for tag in &art.tags {
195        meta.tags.push(allocator.alloc_str(tag.as_str()));
196    }
197    meta
198}
199
200/// Source location of `[start, end)`; the line and column are computed from
201/// the file-absolute start offset (1-based line, 0-based byte column).
202pub(crate) fn calculate_location_fast(source: &str, start: u32, end: u32) -> SourceLocation {
203    calculate_location_with_line(source, start, end, line_of(source, start))
204}
205
206/// Build a location when the caller already counted its starting line.
207pub(crate) fn calculate_location_with_line(
208    source: &str,
209    start: u32,
210    end: u32,
211    line: u32,
212) -> SourceLocation {
213    let before = prefix(source, start);
214    let line_start = before
215        .iter()
216        .rposition(|&byte| byte == b'\n')
217        .map_or(0, |at| at + 1);
218    let column = (before.len() - line_start) as u32;
219    SourceLocation::new(start, end, line, column)
220}
221
222/// 1-based line of a file-absolute offset (`\n`-delimited, as the
223/// descriptor has always counted).
224pub(crate) fn line_of(source: &str, offset: u32) -> u32 {
225    prefix(source, offset)
226        .iter()
227        .filter(|&&byte| byte == b'\n')
228        .count() as u32
229        + 1
230}
231
232fn prefix(source: &str, offset: u32) -> &[u8] {
233    let bytes = source.as_bytes();
234    bytes.get(..offset as usize).unwrap_or(bytes)
235}
236
237#[cfg(test)]
238mod tests;