1mod imports;
21mod params;
22mod type_aliases;
23mod validation;
24
25use alloc::{
26 string::{String, ToString},
27 vec::Vec,
28};
29#[cfg(feature = "std")]
30use std::path::PathBuf;
31
32pub use imports::*;
33pub use params::parse_type_annotation;
34pub(crate) use params::*;
35pub(crate) use type_aliases::*;
36pub(crate) use validation::*;
37
38use crate::{
39 compat::HashMap,
40 consts::{
41 FM_ALLOW_UNUSED_PREFIX, FM_CONSTS_PREFIX, FM_DELIMITER, FM_DELIMITER_NEWLINE,
42 FM_DESC_PREFIX, FM_IMPORTS_PREFIX, FM_NAME_PREFIX, FM_PARAMS_PREFIX, FM_TYPES_PREFIX,
43 },
44 error::TemplateError,
45 frontmatter::params::parse_declarations,
46 types::{VarDecl, VarType},
47};
48
49#[derive(Debug, Clone)]
51pub struct Import {
52 pub stem: String,
54 #[cfg(feature = "std")]
56 pub path: PathBuf,
57 #[cfg(not(feature = "std"))]
59 pub path: alloc::string::String,
60}
61
62#[derive(Debug, Clone, Default)]
64pub struct ImportedNamespace {
65 pub type_aliases: HashMap<String, VarType>,
67 pub param_types: HashMap<String, VarType>,
69 pub consts: HashMap<String, crate::value::Value>,
71}
72
73#[derive(Debug, Clone, Default)]
75pub struct Frontmatter {
76 pub name: Option<String>,
78 pub description: Option<String>,
80 pub declarations: Vec<VarDecl>,
82 pub params: Vec<String>,
84 pub has_params: bool,
86 pub allow_unused: bool,
91 pub type_aliases: HashMap<String, VarType>,
95 pub imports: Vec<Import>,
97 pub consts: Vec<VarDecl>,
99 pub imported_consts: HashMap<String, crate::value::Value>,
101 pub imported_enum_type_keys: Vec<String>,
105}
106
107pub fn strip_frontmatter(source: &str) -> Result<&str, TemplateError> {
113 parse_frontmatter(source).map(|(_, body)| body)
114}
115
116pub fn parse_frontmatter(source: &str) -> Result<(Frontmatter, &str), TemplateError> {
126 parse_frontmatter_impl(
127 source,
128 #[cfg(feature = "std")]
129 None,
130 None,
131 false,
132 )
133}
134
135#[cfg(feature = "std")]
146pub fn parse_frontmatter_with_base_dir<'a>(
147 source: &'a str,
148 base_dir: &std::path::Path,
149) -> Result<(Frontmatter, &'a str), TemplateError> {
150 parse_frontmatter_impl(source, Some(base_dir), None, false)
151}
152
153pub fn parse_frontmatter_with_parent_scope<'a>(
158 source: &'a str,
159 parent_type_aliases: &HashMap<String, VarType>,
160) -> Result<(Frontmatter, &'a str), TemplateError> {
161 parse_frontmatter_impl(
162 source,
163 #[cfg(feature = "std")]
164 None,
165 Some(parent_type_aliases),
166 true,
167 )
168}
169
170fn extract_yaml_logical_lines(
171 source: &str,
172 allow_missing_fm: bool,
173) -> Result<(Vec<String>, &str), TemplateError> {
174 let trimmed = source.trim_start();
175 if !trimmed.starts_with(FM_DELIMITER) {
176 if allow_missing_fm {
177 return Ok((Vec::new(), source));
178 }
179 return Err(TemplateError::syntax(
180 crate::consts::ERR_MISSING_FM.to_string(),
181 ));
182 }
183
184 let after_first = trimmed[FM_DELIMITER.len()..].trim_start_matches(['\r', '\n']);
185 let Some(end) = after_first.find(FM_DELIMITER_NEWLINE) else {
186 return Err(TemplateError::syntax(
187 crate::consts::ERR_UNCLOSED_FM.to_string(),
188 ));
189 };
190
191 let yaml_block = &after_first[..end];
192 let after_close = end + FM_DELIMITER_NEWLINE.len();
193 let body_start = if after_first[after_close..].starts_with('\n') {
194 after_close + 1
195 } else if after_first[after_close..].starts_with("\r\n") {
196 after_close + 2
197 } else {
198 after_close
199 };
200 let body = &after_first[body_start..];
201
202 let mut in_block_list = false;
203 let mut had_blank_line = true;
204 for line in yaml_block.lines() {
205 let trimmed = line.trim();
206 if trimmed.is_empty() {
207 had_blank_line = true;
208 continue;
209 }
210 let starts_with_section = line.starts_with(FM_NAME_PREFIX)
211 || line.starts_with(FM_DESC_PREFIX)
212 || line.starts_with(FM_TYPES_PREFIX)
213 || line.starts_with(FM_IMPORTS_PREFIX)
214 || line.starts_with(FM_PARAMS_PREFIX)
215 || line.starts_with(FM_CONSTS_PREFIX)
216 || line.starts_with(FM_ALLOW_UNUSED_PREFIX);
217
218 if starts_with_section {
219 if in_block_list && !had_blank_line {
220 return Err(TemplateError::syntax(format!(
221 "A blank line is required after a block list before '{trimmed}' so raw markdown renders correctly"
222 )));
223 }
224 in_block_list = false;
225 } else if trimmed.starts_with('-') {
226 in_block_list = true;
227 }
228 had_blank_line = false;
229 }
230
231 Ok((join_continuation_lines(yaml_block), body))
232}
233
234type FmResolutionResult = Result<
235 (
236 HashMap<String, VarType>,
237 HashMap<String, ImportedNamespace>,
238 HashMap<String, crate::value::Value>,
239 ),
240 TemplateError,
241>;
242
243fn resolve_fm_consts_and_imports(
244 fm: &mut Frontmatter,
245 consts_raw: Option<&str>,
246 parent_type_aliases: Option<&HashMap<String, VarType>>,
247 #[cfg(feature = "std")] base_dir: Option<&std::path::Path>,
248) -> FmResolutionResult {
249 let mut merged_aliases = if let Some(parent_aliases) = parent_type_aliases {
250 parent_aliases.clone()
251 } else {
252 HashMap::new()
253 };
254 for (k, v) in &fm.type_aliases {
255 merged_aliases.insert(k.clone(), v.clone());
256 }
257
258 let mut prelim_consts = HashMap::new();
259 let empty_imports = HashMap::new();
260 let empty_consts = HashMap::new();
261 if let Some(raw) = consts_raw {
262 if let Ok(decls) =
263 parse_declarations(raw, &merged_aliases, &empty_imports, true, &empty_consts)
264 {
265 prelim_consts = build_available_consts(&decls, &HashMap::new());
266 }
267 }
268
269 #[cfg(feature = "std")]
270 let resolved_imports = if let Some(dir) = base_dir {
271 if fm.imports.is_empty() {
272 HashMap::new()
273 } else {
274 let mut visited = std::collections::HashSet::new();
275 resolve_imports_with_consts(&mut fm.imports, dir, &mut visited, &prelim_consts)?
276 }
277 } else {
278 if !fm.imports.is_empty() {
279 interpolate_imports(&mut fm.imports, &prelim_consts)?;
280 }
281 HashMap::new()
282 };
283
284 #[cfg(not(feature = "std"))]
285 let resolved_imports = {
286 if !fm.imports.is_empty() {
287 interpolate_imports(&mut fm.imports, &prelim_consts)?;
288 }
289 HashMap::new()
290 };
291
292 #[cfg(feature = "std")]
293 inject_imported_consts(fm, &resolved_imports);
294
295 if let Some(raw) = consts_raw {
296 fm.consts =
297 parse_declarations(raw, &merged_aliases, &resolved_imports, true, &empty_consts)?;
298 }
299
300 let available_consts = build_available_consts(&fm.consts, &fm.imported_consts);
301 Ok((merged_aliases, resolved_imports, available_consts))
302}
303
304fn parse_frontmatter_impl<'a>(
305 source: &'a str,
306 #[cfg(feature = "std")] base_dir: Option<&std::path::Path>,
307 parent_type_aliases: Option<&HashMap<String, VarType>>,
308 allow_missing_fm: bool,
309) -> Result<(Frontmatter, &'a str), TemplateError> {
310 let (logical_lines, body) = extract_yaml_logical_lines(source, allow_missing_fm)?;
311 if logical_lines.is_empty()
312 && allow_missing_fm
313 && !source.trim_start().starts_with(FM_DELIMITER)
314 {
315 return Ok((Frontmatter::default(), body));
316 }
317
318 let mut fm = Frontmatter::default();
319 let mut params_raw: Option<String> = None;
320 let mut consts_raw: Option<String> = None;
321
322 for line in &logical_lines {
323 let line = line.trim();
324 if let Some(rest) = line.strip_prefix(FM_NAME_PREFIX) {
325 fm.name = Some(rest.trim().to_string());
326 } else if let Some(rest) = line.strip_prefix(FM_DESC_PREFIX) {
327 fm.description = Some(rest.trim().to_string());
328 } else if let Some(rest) = line.strip_prefix(FM_TYPES_PREFIX) {
329 fm.type_aliases = parse_types_value(rest)?;
330 } else if let Some(rest) = line.strip_prefix(FM_IMPORTS_PREFIX) {
331 fm.imports = parse_imports_value(rest)?;
332 } else if let Some(rest) = line.strip_prefix(FM_PARAMS_PREFIX) {
333 params_raw = Some(rest.to_string());
334 } else if let Some(rest) = line.strip_prefix(FM_CONSTS_PREFIX) {
335 consts_raw = Some(rest.to_string());
336 } else if let Some(rest) = line.strip_prefix(FM_ALLOW_UNUSED_PREFIX) {
337 fm.allow_unused = rest.trim() == crate::consts::LIT_TRUE;
338 }
339 }
340
341 let (merged_aliases, resolved_imports, available_consts) = resolve_fm_consts_and_imports(
342 &mut fm,
343 consts_raw.as_deref(),
344 parent_type_aliases,
345 #[cfg(feature = "std")]
346 base_dir,
347 )?;
348
349 if let Some(raw) = params_raw {
350 let decls = parse_declarations(
351 &raw,
352 &merged_aliases,
353 &resolved_imports,
354 false,
355 &available_consts,
356 )?;
357 fm.params = decls.iter().map(|d| d.name.clone()).collect();
358 fm.declarations = decls;
359 fm.has_params = true;
360 }
361
362 validate_collision_rules(&fm)?;
363 add_implicit_param_types(&mut fm);
364
365 Ok((fm, body))
366}
367
368#[cfg(feature = "std")]
374fn inject_imported_consts(
375 fm: &mut Frontmatter,
376 resolved_imports: &HashMap<String, ImportedNamespace>,
377) {
378 for (stem, ns) in resolved_imports {
379 for (name, val) in &ns.consts {
380 fm.imported_consts
381 .insert(format!("{stem}.{name}"), val.clone());
382 }
383 for (type_name, var_type) in &ns.type_aliases {
386 let VarType::Enum(variants) = var_type else {
387 continue;
388 };
389 let key = format!("{stem}.{type_name}");
390 if fm.imported_consts.contains_key(&key) {
392 continue;
393 }
394 let mut variant_map = HashMap::new();
395 let mut variant_names = Vec::with_capacity(variants.len());
396 for variant in variants {
397 variant_names.push(crate::value::Value::Str(variant.name.clone()));
398 if variant.fields.is_empty() {
399 variant_map.insert(
400 variant.name.clone(),
401 crate::value::Value::Str(variant.name.clone()),
402 );
403 } else {
404 let mut partial = HashMap::new();
405 partial.insert(
406 crate::consts::ENUM_TAG_KEY.into(),
407 crate::value::Value::Str(variant.name.clone()),
408 );
409 variant_map.insert(
410 variant.name.clone(),
411 crate::value::Value::Struct(alloc::sync::Arc::new(partial)),
412 );
413 }
414 }
415 variant_map.insert(
416 crate::consts::ENUM_VARIANTS_KEY.into(),
417 crate::value::Value::List(alloc::sync::Arc::new(variant_names)),
418 );
419 fm.imported_consts.insert(
420 key.clone(),
421 crate::value::Value::Struct(alloc::sync::Arc::new(variant_map)),
422 );
423 fm.imported_enum_type_keys.push(key);
424 }
425 }
426}
427
428fn build_available_consts(
435 consts: &[crate::types::VarDecl],
436 imported_consts: &HashMap<String, crate::value::Value>,
437) -> HashMap<String, crate::value::Value> {
438 let mut available = HashMap::with_capacity(consts.len() + imported_consts.len());
439 for d in consts {
441 if let Some(ref v) = d.default_value {
442 available.insert(d.name.clone(), v.clone());
443 }
444 }
445 for (k, v) in imported_consts {
447 available.insert(k.clone(), v.clone());
448 }
449 available
450}
451
452#[cfg(test)]
453mod tests;