1use kdl::{KdlDocument, KdlNode, KdlValue};
6
7use crate::ast::{
8 action::ActionDef,
9 asset::{AssetBlock, AssetDecl, AssetKind},
10 block_style::BlockStyle,
11 brand::BrandContract,
12 document::{ComponentDef, Document, DocumentBody, MasterDef, Page, Project, SectionDef},
13 library::LibraryDef,
14 node::Node,
15 policy::{DiagnosticPolicy, PolicyEntry, PolicyVerb},
16 provenance::ProvenanceDef,
17 recipe::{RecipeDef, RecipeParam},
18 style::StyleBlock,
19 token::TokenBlock,
20 variant::{VariantDef, VariantOverride},
21};
22use crate::error::{ParseError, ParseErrorCode};
23
24use super::block_style::transform_block_style;
25use super::helpers::{
26 collect_unknown_props, entry_to_dimension, entry_to_property_value, node_span,
27 optional_bool_prop, optional_dimension_prop, optional_i64_prop, optional_string_prop,
28 optional_string_prop_aliased, optional_u32_prop, required_string_prop,
29 required_string_prop_aliased, required_u32_prop,
30};
31use super::node::transform_node;
32use super::page::transform_page;
33use super::tokens::{transform_styles, transform_tokens};
34
35pub(crate) const DOCUMENT_KNOWN_PROPS: &[&str] = &[
47 "version",
49 "colorspace",
50 "doc-id",
51 "doc_id",
52 "mirror-margins",
53 "mirror_margins",
54 "page-progression",
55 "page_progression",
56 "page-parity-start",
57 "page_parity_start",
58 "facing-pages",
59 "facing_pages",
60 "spread-gutter",
61 "spread_gutter",
62 "margin-inner",
63 "margin_inner",
64 "margin-outer",
65 "margin_outer",
66 "margin-top",
67 "margin_top",
68 "margin-bottom",
69 "margin_bottom",
70 "id",
72 "title",
73];
74
75pub fn transform(doc: &KdlDocument) -> Result<Document, ParseError> {
77 let zenith_node = doc
79 .nodes()
80 .iter()
81 .find(|n| n.name().value() == "zenith")
82 .ok_or_else(|| {
83 ParseError::spanless(
84 ParseErrorCode::MissingZenithRoot,
85 "no top-level `zenith` node found",
86 )
87 })?;
88
89 let version = required_u32_prop(zenith_node, "version")?;
90 let colorspace = optional_string_prop(zenith_node, "colorspace").map(str::to_owned);
94
95 let doc_id = optional_string_prop(zenith_node, "doc-id")
101 .or_else(|| optional_string_prop(zenith_node, "doc_id"))
102 .map(str::to_owned);
103
104 let mirror_margins = optional_bool_prop(zenith_node, "mirror-margins")
107 .or_else(|| optional_bool_prop(zenith_node, "mirror_margins"));
108
109 let page_progression = optional_string_prop(zenith_node, "page-progression")
113 .or_else(|| optional_string_prop(zenith_node, "page_progression"))
114 .map(str::to_owned);
115
116 let page_parity_start = optional_string_prop(zenith_node, "page-parity-start")
121 .or_else(|| optional_string_prop(zenith_node, "page_parity_start"))
122 .map(str::to_owned);
123
124 let facing_pages = optional_bool_prop(zenith_node, "facing-pages")
128 .or_else(|| optional_bool_prop(zenith_node, "facing_pages"));
129
130 let spread_gutter = optional_dimension_prop(zenith_node, "spread-gutter")
134 .or_else(|| optional_dimension_prop(zenith_node, "spread_gutter"));
135
136 let margin_inner = optional_dimension_prop(zenith_node, "margin-inner")
141 .or_else(|| optional_dimension_prop(zenith_node, "margin_inner"));
142 let margin_outer = optional_dimension_prop(zenith_node, "margin-outer")
143 .or_else(|| optional_dimension_prop(zenith_node, "margin_outer"));
144 let margin_top = optional_dimension_prop(zenith_node, "margin-top")
145 .or_else(|| optional_dimension_prop(zenith_node, "margin_top"));
146 let margin_bottom = optional_dimension_prop(zenith_node, "margin-bottom")
147 .or_else(|| optional_dimension_prop(zenith_node, "margin_bottom"));
148
149 let children_doc = zenith_node.children().ok_or_else(|| {
150 ParseError::spanless(
151 ParseErrorCode::MissingZenithRoot,
152 "`zenith` node has no children block",
153 )
154 })?;
155
156 let mut project: Option<Project> = None;
157 let mut assets = AssetBlock::default();
158 let mut libraries: Vec<LibraryDef> = Vec::new();
159 let mut actions: Vec<ActionDef> = Vec::new();
160 let mut tokens = TokenBlock::default();
161 let mut styles = StyleBlock::default();
162 let mut components: Vec<ComponentDef> = Vec::new();
163 let mut masters: Vec<MasterDef> = Vec::new();
164 let mut sections: Vec<SectionDef> = Vec::new();
165 let mut provenance: Vec<ProvenanceDef> = Vec::new();
166 let mut variants: Vec<VariantDef> = Vec::new();
167 let mut recipes: Vec<RecipeDef> = Vec::new();
168 let mut diagnostic_policy = DiagnosticPolicy::default();
169 let mut brand_contract = BrandContract::default();
170 let mut body: Option<DocumentBody> = None;
171
172 for child in children_doc.nodes() {
173 match child.name().value() {
174 "project" => {
175 project = Some(transform_project(child)?);
176 }
177 "assets" => {
178 assets = transform_assets(child)?;
179 }
180 "libraries" => {
181 libraries = transform_libraries(child)?;
182 }
183 "actions" => {
184 actions = transform_actions(child)?;
185 }
186 "tokens" => {
187 tokens = transform_tokens(child)?;
188 }
189 "styles" => {
190 styles = transform_styles(child)?;
191 }
192 "components" => {
193 components = transform_components(child)?;
194 }
195 "masters" => {
196 masters = transform_masters(child)?;
197 }
198 "sections" => {
199 sections = transform_sections(child)?;
200 }
201 "provenance" => {
202 provenance = transform_provenance(child)?;
203 }
204 "variants" => {
205 variants = transform_variants(child)?;
206 }
207 "recipes" => {
208 recipes = transform_recipes(child)?;
209 }
210 "diagnostics" => {
211 diagnostic_policy = transform_diagnostic_policy(child)?;
212 }
213 "brand" => {
214 brand_contract = transform_brand_contract(child)?;
215 }
216 "document" => {
217 body = Some(transform_document_body(child)?);
218 }
219 _ => {}
222 }
223 }
224
225 let body = body.ok_or_else(|| {
226 ParseError::spanless(
227 ParseErrorCode::MissingZenithRoot,
228 "`zenith` node is missing a `document` child",
229 )
230 })?;
231
232 Ok(Document {
233 version,
234 colorspace,
235 doc_id,
236 mirror_margins,
237 facing_pages,
238 spread_gutter,
239 page_progression,
240 page_parity_start,
241 margin_inner,
242 margin_outer,
243 margin_top,
244 margin_bottom,
245 project,
246 assets,
247 libraries,
248 actions,
249 tokens,
250 styles,
251 components,
252 masters,
253 sections,
254 provenance,
255 variants,
256 recipes,
257 diagnostic_policy,
258 brand_contract,
259 body,
260 })
261}
262
263pub(crate) fn transform_diagnostic_policy(node: &KdlNode) -> Result<DiagnosticPolicy, ParseError> {
288 let mut entries: Vec<PolicyEntry> = Vec::new();
289 if let Some(children) = node.children() {
290 for child in children.nodes() {
291 let (verb, verb_name) = match child.name().value() {
292 "allow" => (PolicyVerb::Allow, "allow"),
293 "deny" => (PolicyVerb::Deny, "deny"),
294 "warn" => (PolicyVerb::Warn, "warn"),
295 _ => continue,
297 };
298 let mut positional = child
299 .entries()
300 .iter()
301 .filter(|entry| entry.name().is_none());
302 let code = match positional.next().map(|entry| entry.value()) {
303 Some(KdlValue::String(s)) => s.clone(),
304 _ => {
305 return Err(ParseError::spanless(
306 ParseErrorCode::InvalidPropertyValue,
307 format!(
308 "diagnostics `{verb_name}` entry requires a quoted diagnostic-code \
309 string as its first argument, e.g. `{verb_name} \"layout.off_canvas\"`"
310 ),
311 ));
312 }
313 };
314 let mut subjects: Vec<String> = Vec::new();
315 for (idx, entry) in positional.enumerate() {
316 match entry.value() {
317 KdlValue::String(s) => subjects.push(s.clone()),
318 _ => {
319 let subject_index = idx + 1;
320 return Err(ParseError::spanless(
321 ParseErrorCode::InvalidPropertyValue,
322 format!(
323 "diagnostics `{verb_name}` subject argument {subject_index} must \
324 be a quoted subject-id string, e.g. `{verb_name} \
325 \"layout.off_canvas\" \"bg.glow\"`"
326 ),
327 ));
328 }
329 }
330 }
331 if child.entries().iter().any(|entry| {
332 entry
333 .name()
334 .map(|name| name.value() == "subject" || name.value() == "subjects")
335 .unwrap_or(false)
336 }) {
337 return Err(ParseError::spanless(
338 ParseErrorCode::InvalidPropertyValue,
339 "diagnostics scoped subjects must be positional strings after the \
340 diagnostic code, e.g. `allow \"layout.off_canvas\" \"bg.glow\"`",
341 ));
342 }
343 entries.push(PolicyEntry {
344 verb,
345 code,
346 subjects,
347 source_span: node_span(child),
348 });
349 }
350 }
351 Ok(DiagnosticPolicy { entries })
352}
353
354pub(crate) fn transform_brand_contract(node: &KdlNode) -> Result<BrandContract, ParseError> {
374 let source_span = node_span(node);
375 let mut allowed_colors: Option<Vec<String>> = None;
376 let mut allowed_fonts: Option<Vec<String>> = None;
377 let mut allowed_weights: Option<Vec<u32>> = None;
378
379 if let Some(children) = node.children() {
380 for child in children.nodes() {
381 match child.name().value() {
382 "colors" => {
383 let mut colors: Vec<String> = Vec::new();
384 let positional: Vec<_> = child
386 .entries()
387 .iter()
388 .filter(|e| e.name().is_none())
389 .collect();
390 for (idx, entry) in positional.iter().enumerate() {
391 match entry.value() {
392 KdlValue::String(s) => {
393 colors.push(s.to_lowercase());
396 }
397 _ => {
398 return Err(ParseError::spanless(
399 ParseErrorCode::InvalidPropertyValue,
400 format!(
401 "brand `colors` argument {idx} must be a quoted string \
402 (hex color), e.g. `colors \"#0b1f33\" \"#ffffff\"`"
403 ),
404 ));
405 }
406 }
407 }
408 allowed_colors = Some(colors);
409 }
410 "fonts" => {
411 let mut fonts: Vec<String> = Vec::new();
412 let positional: Vec<_> = child
413 .entries()
414 .iter()
415 .filter(|e| e.name().is_none())
416 .collect();
417 for (idx, entry) in positional.iter().enumerate() {
418 match entry.value() {
419 KdlValue::String(s) => {
420 fonts.push(s.clone());
421 }
422 _ => {
423 return Err(ParseError::spanless(
424 ParseErrorCode::InvalidPropertyValue,
425 format!(
426 "brand `fonts` argument {idx} must be a quoted string \
427 (font-family name), e.g. `fonts \"Noto Sans\"`"
428 ),
429 ));
430 }
431 }
432 }
433 allowed_fonts = Some(fonts);
434 }
435 "weights" => {
436 let mut weights: Vec<u32> = Vec::new();
437 let positional: Vec<_> = child
438 .entries()
439 .iter()
440 .filter(|e| e.name().is_none())
441 .collect();
442 for (idx, entry) in positional.iter().enumerate() {
443 match entry.value() {
444 KdlValue::Integer(n) => {
445 let n_val = *n;
447 if !(100..=900).contains(&n_val) {
448 return Err(ParseError::spanless(
449 ParseErrorCode::InvalidPropertyValue,
450 format!(
451 "brand `weights` argument {idx} must be an integer \
452 in the range 100-900 (got {n_val})"
453 ),
454 ));
455 }
456 let w = u32::try_from(n_val).map_err(|_| {
458 ParseError::spanless(
459 ParseErrorCode::InvalidPropertyValue,
460 format!(
461 "brand `weights` argument {idx} is out of range \
462 for a u32 weight (got {n_val})"
463 ),
464 )
465 })?;
466 weights.push(w);
467 }
468 _ => {
469 return Err(ParseError::spanless(
470 ParseErrorCode::InvalidPropertyValue,
471 format!(
472 "brand `weights` argument {idx} must be an integer \
473 (font weight), e.g. `weights 400 700`"
474 ),
475 ));
476 }
477 }
478 }
479 allowed_weights = Some(weights);
480 }
481 _ => {}
483 }
484 }
485 }
486
487 Ok(BrandContract {
488 allowed_colors,
489 allowed_fonts,
490 allowed_weights,
491 source_span,
492 })
493}
494
495fn transform_masters(node: &KdlNode) -> Result<Vec<MasterDef>, ParseError> {
505 let mut defs: Vec<MasterDef> = Vec::new();
506 if let Some(children) = node.children() {
507 for child in children.nodes() {
508 if child.name().value() == "master" {
509 defs.push(transform_master_def(child)?);
510 }
511 }
512 }
513 Ok(defs)
514}
515
516fn transform_master_def(node: &KdlNode) -> Result<MasterDef, ParseError> {
517 let id = required_string_prop(node, "id")?.to_owned();
518 let children = transform_children(node)?;
519 Ok(MasterDef {
520 id,
521 children,
522 source_span: node_span(node),
523 })
524}
525
526fn transform_sections(node: &KdlNode) -> Result<Vec<SectionDef>, ParseError> {
535 let mut defs: Vec<SectionDef> = Vec::new();
536 if let Some(children) = node.children() {
537 for child in children.nodes() {
538 if child.name().value() == "section" {
539 defs.push(transform_section_def(child)?);
540 }
541 }
542 }
543 Ok(defs)
544}
545
546fn transform_section_def(node: &KdlNode) -> Result<SectionDef, ParseError> {
547 let id = required_string_prop(node, "id")?.to_owned();
548 let name = required_string_prop(node, "name")?.to_owned();
549 let start_page = required_string_prop_aliased(node, "start-page", "start_page")?.to_owned();
550
551 let folio_start = optional_u32_prop(node, "folio-start")
556 .or_else(|| optional_u32_prop(node, "folio_start"))
557 .map(|n| n as usize);
558
559 let folio_style =
561 optional_string_prop_aliased(node, "folio-style", "folio_style").map(str::to_owned);
562
563 Ok(SectionDef {
564 id,
565 name,
566 folio_start,
567 folio_style,
568 start_page,
569 source_span: node_span(node),
570 })
571}
572
573const LIBRARY_KNOWN_PROPS: &[&str] = &["id", "version", "hash"];
578
579fn transform_libraries(node: &KdlNode) -> Result<Vec<LibraryDef>, ParseError> {
584 let mut defs: Vec<LibraryDef> = Vec::new();
585 if let Some(children) = node.children() {
586 for child in children.nodes() {
587 if child.name().value() == "library" {
588 defs.push(transform_library_def(child)?);
589 }
590 }
591 }
592 Ok(defs)
593}
594
595fn transform_library_def(node: &KdlNode) -> Result<LibraryDef, ParseError> {
596 let id = required_string_prop(node, "id")?.to_owned();
597 let version = optional_string_prop(node, "version").map(str::to_owned);
598 let hash = optional_string_prop(node, "hash").map(str::to_owned);
599 let unknown_props = collect_unknown_props(node, LIBRARY_KNOWN_PROPS);
600 let source_span = node_span(node);
601
602 Ok(LibraryDef {
603 id,
604 version,
605 hash,
606 source_span,
607 unknown_props,
608 })
609}
610
611const ACTION_KNOWN_PROPS: &[&str] = &["id", "label", "version"];
616
617fn transform_actions(node: &KdlNode) -> Result<Vec<ActionDef>, ParseError> {
623 let mut defs: Vec<ActionDef> = Vec::new();
624 if let Some(children) = node.children() {
625 for child in children.nodes() {
626 if child.name().value() == "action" {
627 defs.push(transform_action_def(child)?);
628 }
629 }
630 }
631 Ok(defs)
632}
633
634fn transform_action_def(node: &KdlNode) -> Result<ActionDef, ParseError> {
635 let id = required_string_prop(node, "id")?.to_owned();
636 let label = optional_string_prop(node, "label").map(str::to_owned);
637 let version = optional_string_prop(node, "version").map(str::to_owned);
638 let unknown_props = collect_unknown_props(node, ACTION_KNOWN_PROPS);
639 let source_span = node_span(node);
640
641 let tx_json = node
645 .children()
646 .and_then(|doc| {
647 doc.nodes().iter().find_map(|child| {
648 if child.name().value() != "tx" {
649 return None;
650 }
651 child.get(0).and_then(|v| match v {
652 KdlValue::String(s) => Some(s.clone()),
653 _ => None,
654 })
655 })
656 })
657 .ok_or_else(|| {
658 ParseError::spanless(
659 ParseErrorCode::InvalidPropertyValue,
660 format!("node `action` id=\"{id}\" is missing required `tx` child node"),
661 )
662 })?;
663
664 Ok(ActionDef {
665 id,
666 label,
667 version,
668 tx_json,
669 source_span,
670 unknown_props,
671 })
672}
673
674const PROVENANCE_KNOWN_PROPS: &[&str] = &["id", "node", "library", "item", "linked"];
679
680fn transform_provenance(node: &KdlNode) -> Result<Vec<ProvenanceDef>, ParseError> {
685 let mut defs: Vec<ProvenanceDef> = Vec::new();
686 if let Some(children) = node.children() {
687 for child in children.nodes() {
688 if child.name().value() == "origin" {
689 defs.push(transform_provenance_def(child)?);
690 }
691 }
692 }
693 Ok(defs)
694}
695
696fn transform_provenance_def(node: &KdlNode) -> Result<ProvenanceDef, ParseError> {
697 let id = required_string_prop(node, "id")?.to_owned();
698 let document_node = required_string_prop(node, "node")?.to_owned();
699 let library = required_string_prop(node, "library")?.to_owned();
700 let item = optional_string_prop(node, "item").map(str::to_owned);
701 let linked = optional_bool_prop(node, "linked");
702 let unknown_props = collect_unknown_props(node, PROVENANCE_KNOWN_PROPS);
703 let source_span = node_span(node);
704
705 Ok(ProvenanceDef {
706 id,
707 node: document_node,
708 library,
709 item,
710 linked,
711 source_span,
712 unknown_props,
713 })
714}
715
716const VARIANT_KNOWN_PROPS: &[&str] = &["id", "source", "w", "h"];
721const VARIANT_OVERRIDE_KNOWN_PROPS: &[&str] =
722 &["node", "visible", "x", "y", "w", "h", "fill", "text"];
723
724fn transform_variants(node: &KdlNode) -> Result<Vec<VariantDef>, ParseError> {
729 let mut defs: Vec<VariantDef> = Vec::new();
730 if let Some(children) = node.children() {
731 for child in children.nodes() {
732 if child.name().value() == "variant" {
733 defs.push(transform_variant_def(child)?);
734 }
735 }
736 }
737 Ok(defs)
738}
739
740fn transform_variant_def(node: &KdlNode) -> Result<VariantDef, ParseError> {
741 let id = required_string_prop(node, "id")?.to_owned();
742 let source = required_string_prop(node, "source")?.to_owned();
743
744 let w = node
745 .entry("w")
746 .ok_or_else(|| {
747 ParseError::spanless(
748 ParseErrorCode::InvalidPropertyValue,
749 format!("variant `{id}` is missing required property `w`"),
750 )
751 })
752 .and_then(|e| entry_to_dimension(e, "w"))?;
753
754 let h = node
755 .entry("h")
756 .ok_or_else(|| {
757 ParseError::spanless(
758 ParseErrorCode::InvalidPropertyValue,
759 format!("variant `{id}` is missing required property `h`"),
760 )
761 })
762 .and_then(|e| entry_to_dimension(e, "h"))?;
763
764 let unknown_props = collect_unknown_props(node, VARIANT_KNOWN_PROPS);
765 let source_span = node_span(node);
766
767 let mut overrides: Vec<VariantOverride> = Vec::new();
768 if let Some(children) = node.children() {
769 for child in children.nodes() {
770 if child.name().value() == "override" {
771 overrides.push(transform_variant_override(child)?);
772 }
773 }
774 }
775
776 Ok(VariantDef {
777 id,
778 source,
779 w,
780 h,
781 overrides,
782 source_span,
783 unknown_props,
784 })
785}
786
787fn transform_variant_override(node: &KdlNode) -> Result<VariantOverride, ParseError> {
788 let target_node = required_string_prop(node, "node")?.to_owned();
789 let visible = optional_bool_prop(node, "visible");
790 let x = optional_dimension_prop(node, "x");
791 let y = optional_dimension_prop(node, "y");
792 let w = optional_dimension_prop(node, "w");
793 let h = optional_dimension_prop(node, "h");
794 let fill = node
795 .entry("fill")
796 .and_then(|e| entry_to_property_value(e).ok());
797 let text = optional_string_prop(node, "text").map(str::to_owned);
798 let unknown_props = collect_unknown_props(node, VARIANT_OVERRIDE_KNOWN_PROPS);
799 let source_span = node_span(node);
800
801 Ok(VariantOverride {
802 node: target_node,
803 visible,
804 x,
805 y,
806 w,
807 h,
808 fill,
809 text,
810 source_span,
811 unknown_props,
812 })
813}
814
815const RECIPE_KNOWN_PROPS: &[&str] = &["id", "kind", "seed", "generator", "bounds", "detached"];
820const RECIPE_PARAM_KNOWN_PROPS: &[&str] = &["name", "value"];
821
822fn transform_recipes(node: &KdlNode) -> Result<Vec<RecipeDef>, ParseError> {
827 let mut defs: Vec<RecipeDef> = Vec::new();
828 if let Some(children) = node.children() {
829 for child in children.nodes() {
830 if child.name().value() == "recipe" {
831 defs.push(transform_recipe_def(child)?);
832 }
833 }
834 }
835 Ok(defs)
836}
837
838fn transform_recipe_def(node: &KdlNode) -> Result<RecipeDef, ParseError> {
839 let id = required_string_prop(node, "id")?.to_owned();
840 let kind = required_string_prop(node, "kind")?.to_owned();
841
842 let seed = optional_i64_prop(node, "seed");
844
845 let generator = optional_string_prop(node, "generator").map(str::to_owned);
846 let bounds = optional_string_prop(node, "bounds").map(str::to_owned);
847 let detached = optional_bool_prop(node, "detached");
848
849 let unknown_props = collect_unknown_props(node, RECIPE_KNOWN_PROPS);
850 let source_span = node_span(node);
851
852 let mut params: Vec<RecipeParam> = Vec::new();
853 let mut palette: Vec<String> = Vec::new();
854 let mut expanded: Vec<String> = Vec::new();
855
856 if let Some(children) = node.children() {
857 for child in children.nodes() {
858 match child.name().value() {
859 "param" => {
860 params.push(transform_recipe_param(child)?);
861 }
862 "palette" => {
863 palette.push(required_string_prop(child, "token")?.to_owned());
864 }
865 "expanded" => {
866 expanded.push(required_string_prop(child, "node")?.to_owned());
867 }
868 _ => {}
869 }
870 }
871 }
872
873 Ok(RecipeDef {
874 id,
875 kind,
876 seed,
877 generator,
878 bounds,
879 detached,
880 params,
881 palette,
882 expanded,
883 source_span,
884 unknown_props,
885 })
886}
887
888fn transform_recipe_param(node: &KdlNode) -> Result<RecipeParam, ParseError> {
889 let name = required_string_prop(node, "name")?.to_owned();
890 let value = node
891 .entry("value")
892 .ok_or_else(|| {
893 ParseError::spanless(
894 ParseErrorCode::InvalidPropertyValue,
895 format!("recipe `param` `{name}` is missing required property `value`"),
896 )
897 })
898 .and_then(entry_to_property_value)?;
899 let unknown_props = collect_unknown_props(node, RECIPE_PARAM_KNOWN_PROPS);
900 let source_span = node_span(node);
901
902 Ok(RecipeParam {
903 name,
904 value,
905 source_span,
906 unknown_props,
907 })
908}
909
910fn transform_components(node: &KdlNode) -> Result<Vec<ComponentDef>, ParseError> {
920 let mut defs: Vec<ComponentDef> = Vec::new();
921 if let Some(children) = node.children() {
922 for child in children.nodes() {
923 if child.name().value() == "component" {
924 defs.push(transform_component_def(child)?);
925 }
926 }
927 }
928 Ok(defs)
929}
930
931fn transform_component_def(node: &KdlNode) -> Result<ComponentDef, ParseError> {
932 let id = required_string_prop(node, "id")?.to_owned();
933 let children = transform_children(node)?;
934 Ok(ComponentDef {
935 id,
936 children,
937 source_span: node_span(node),
938 })
939}
940
941fn transform_project(node: &KdlNode) -> Result<Project, ParseError> {
946 let id = required_string_prop(node, "id")?.to_owned();
947 let name = required_string_prop(node, "name")?.to_owned();
948 let author = node.children().and_then(|doc| {
949 doc.nodes()
950 .iter()
951 .find(|n| n.name().value() == "author")
952 .and_then(|n| n.get(0))
953 .and_then(|v| {
954 if let KdlValue::String(s) = v {
955 Some(s.clone())
956 } else {
957 None
958 }
959 })
960 });
961 Ok(Project { id, name, author })
962}
963
964pub(crate) const ASSET_KNOWN_PROPS: &[&str] = &[
973 "id",
974 "kind",
975 "src",
976 "sha256",
977 "ai-prompt",
978 "ai-model",
979 "ai-provider",
980 "ai-seed",
981 "ai-generation-date",
982 "ai-license",
983 "ai-source-rights",
984 "ai-safety-status",
985 "ai-reuse-policy",
986];
987
988fn transform_assets(node: &KdlNode) -> Result<AssetBlock, ParseError> {
989 let source_span = node_span(node);
990 let mut asset_list: Vec<AssetDecl> = Vec::new();
991
992 if let Some(children) = node.children() {
993 for child in children.nodes() {
994 if child.name().value() == "asset" {
995 asset_list.push(transform_asset_decl(child)?);
996 }
997 }
1000 }
1001
1002 Ok(AssetBlock {
1003 assets: asset_list,
1004 source_span,
1005 })
1006}
1007
1008fn transform_asset_decl(node: &KdlNode) -> Result<AssetDecl, ParseError> {
1009 let id = required_string_prop(node, "id")?.to_owned();
1010 let kind_str = required_string_prop(node, "kind")?;
1011 let kind = AssetKind::from_kind_str(kind_str);
1012 let src = required_string_prop(node, "src")?.to_owned();
1013 let sha256 = optional_string_prop(node, "sha256").map(str::to_owned);
1014 let ai_prompt = optional_string_prop(node, "ai-prompt").map(str::to_owned);
1015 let ai_model = optional_string_prop(node, "ai-model").map(str::to_owned);
1016 let ai_provider = optional_string_prop(node, "ai-provider").map(str::to_owned);
1017 let ai_seed = optional_i64_prop(node, "ai-seed");
1018 let ai_generation_date = optional_string_prop(node, "ai-generation-date").map(str::to_owned);
1019 let ai_license = optional_string_prop(node, "ai-license").map(str::to_owned);
1020 let ai_source_rights = optional_string_prop(node, "ai-source-rights").map(str::to_owned);
1021 let ai_safety_status = optional_string_prop(node, "ai-safety-status").map(str::to_owned);
1022 let ai_reuse_policy = optional_string_prop(node, "ai-reuse-policy").map(str::to_owned);
1023 let unknown_props = collect_unknown_props(node, ASSET_KNOWN_PROPS);
1024 let source_span = node_span(node);
1025
1026 Ok(AssetDecl {
1027 id,
1028 kind,
1029 src,
1030 sha256,
1031 ai_prompt,
1032 ai_model,
1033 ai_provider,
1034 ai_seed,
1035 ai_generation_date,
1036 ai_license,
1037 ai_source_rights,
1038 ai_safety_status,
1039 ai_reuse_policy,
1040 source_span,
1041 unknown_props,
1042 })
1043}
1044
1045fn transform_document_body(node: &KdlNode) -> Result<DocumentBody, ParseError> {
1050 let id = required_string_prop(node, "id")?.to_owned();
1051 let title = optional_string_prop(node, "title").map(str::to_owned);
1052
1053 let mut block_styles: Vec<BlockStyle> = Vec::new();
1054 let mut pages: Vec<Page> = Vec::new();
1055 if let Some(children) = node.children() {
1056 for child in children.nodes() {
1057 match child.name().value() {
1058 "block" => block_styles.push(transform_block_style(child)?),
1059 "page" => pages.push(transform_page(child)?),
1060 _ => {}
1061 }
1062 }
1063 }
1064
1065 Ok(DocumentBody {
1066 id,
1067 title,
1068 block_styles,
1069 pages,
1070 })
1071}
1072
1073pub(super) fn transform_children(node: &KdlNode) -> Result<Vec<Node>, ParseError> {
1084 let mut children: Vec<Node> = Vec::new();
1085 if let Some(doc) = node.children() {
1086 for child in doc.nodes() {
1087 children.push(transform_node(child)?);
1088 }
1089 }
1090 Ok(children)
1091}