1mod 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
30pub fn parse_art<'a>(
55 allocator: &'a Allocator,
56 source: &'a str,
57 options: ArtParseOptions,
58) -> ArtParseResult<'a> {
59 let filename: &'a str = if options.filename.is_empty() {
61 ""
62 } else {
63 allocator.alloc_str(&options.filename)
64 };
65
66 let blocks = blocks::split_blocks(allocator, source)?;
68 let frame = blocks.art.ok_or(ArtParseError::NoArtBlock)?;
69
70 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 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
96pub(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 if !script.contains("defineArt") {
155 return None;
156 }
157
158 let art = vize_croquis::script_parser::parse_define_art(script)?;
162 Some(define_art_metadata(allocator, &art))
163}
164
165pub(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
200pub(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
206pub(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
222pub(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;