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> {
287 let mut entries: Vec<PolicyEntry> = Vec::new();
288 if let Some(children) = node.children() {
289 for child in children.nodes() {
290 let (verb, verb_name) = match child.name().value() {
291 "allow" => (PolicyVerb::Allow, "allow"),
292 "deny" => (PolicyVerb::Deny, "deny"),
293 "warn" => (PolicyVerb::Warn, "warn"),
294 _ => continue,
296 };
297 let code = match child.get(0) {
298 Some(KdlValue::String(s)) => s.clone(),
299 _ => {
300 return Err(ParseError::spanless(
301 ParseErrorCode::InvalidPropertyValue,
302 format!(
303 "diagnostics `{verb_name}` entry requires a quoted diagnostic-code \
304 string as its first argument, e.g. `{verb_name} \"layout.off_canvas\"`"
305 ),
306 ));
307 }
308 };
309 entries.push(PolicyEntry {
310 verb,
311 code,
312 source_span: node_span(child),
313 });
314 }
315 }
316 Ok(DiagnosticPolicy { entries })
317}
318
319pub(crate) fn transform_brand_contract(node: &KdlNode) -> Result<BrandContract, ParseError> {
339 let source_span = node_span(node);
340 let mut allowed_colors: Option<Vec<String>> = None;
341 let mut allowed_fonts: Option<Vec<String>> = None;
342 let mut allowed_weights: Option<Vec<u32>> = None;
343
344 if let Some(children) = node.children() {
345 for child in children.nodes() {
346 match child.name().value() {
347 "colors" => {
348 let mut colors: Vec<String> = Vec::new();
349 let positional: Vec<_> = child
351 .entries()
352 .iter()
353 .filter(|e| e.name().is_none())
354 .collect();
355 for (idx, entry) in positional.iter().enumerate() {
356 match entry.value() {
357 KdlValue::String(s) => {
358 colors.push(s.to_lowercase());
361 }
362 _ => {
363 return Err(ParseError::spanless(
364 ParseErrorCode::InvalidPropertyValue,
365 format!(
366 "brand `colors` argument {idx} must be a quoted string \
367 (hex color), e.g. `colors \"#0b1f33\" \"#ffffff\"`"
368 ),
369 ));
370 }
371 }
372 }
373 allowed_colors = Some(colors);
374 }
375 "fonts" => {
376 let mut fonts: Vec<String> = Vec::new();
377 let positional: Vec<_> = child
378 .entries()
379 .iter()
380 .filter(|e| e.name().is_none())
381 .collect();
382 for (idx, entry) in positional.iter().enumerate() {
383 match entry.value() {
384 KdlValue::String(s) => {
385 fonts.push(s.clone());
386 }
387 _ => {
388 return Err(ParseError::spanless(
389 ParseErrorCode::InvalidPropertyValue,
390 format!(
391 "brand `fonts` argument {idx} must be a quoted string \
392 (font-family name), e.g. `fonts \"Noto Sans\"`"
393 ),
394 ));
395 }
396 }
397 }
398 allowed_fonts = Some(fonts);
399 }
400 "weights" => {
401 let mut weights: Vec<u32> = Vec::new();
402 let positional: Vec<_> = child
403 .entries()
404 .iter()
405 .filter(|e| e.name().is_none())
406 .collect();
407 for (idx, entry) in positional.iter().enumerate() {
408 match entry.value() {
409 KdlValue::Integer(n) => {
410 let n_val = *n;
412 if !(100..=900).contains(&n_val) {
413 return Err(ParseError::spanless(
414 ParseErrorCode::InvalidPropertyValue,
415 format!(
416 "brand `weights` argument {idx} must be an integer \
417 in the range 100-900 (got {n_val})"
418 ),
419 ));
420 }
421 let w = u32::try_from(n_val).map_err(|_| {
423 ParseError::spanless(
424 ParseErrorCode::InvalidPropertyValue,
425 format!(
426 "brand `weights` argument {idx} is out of range \
427 for a u32 weight (got {n_val})"
428 ),
429 )
430 })?;
431 weights.push(w);
432 }
433 _ => {
434 return Err(ParseError::spanless(
435 ParseErrorCode::InvalidPropertyValue,
436 format!(
437 "brand `weights` argument {idx} must be an integer \
438 (font weight), e.g. `weights 400 700`"
439 ),
440 ));
441 }
442 }
443 }
444 allowed_weights = Some(weights);
445 }
446 _ => {}
448 }
449 }
450 }
451
452 Ok(BrandContract {
453 allowed_colors,
454 allowed_fonts,
455 allowed_weights,
456 source_span,
457 })
458}
459
460fn transform_masters(node: &KdlNode) -> Result<Vec<MasterDef>, ParseError> {
470 let mut defs: Vec<MasterDef> = Vec::new();
471 if let Some(children) = node.children() {
472 for child in children.nodes() {
473 if child.name().value() == "master" {
474 defs.push(transform_master_def(child)?);
475 }
476 }
477 }
478 Ok(defs)
479}
480
481fn transform_master_def(node: &KdlNode) -> Result<MasterDef, ParseError> {
482 let id = required_string_prop(node, "id")?.to_owned();
483 let children = transform_children(node)?;
484 Ok(MasterDef {
485 id,
486 children,
487 source_span: node_span(node),
488 })
489}
490
491fn transform_sections(node: &KdlNode) -> Result<Vec<SectionDef>, ParseError> {
500 let mut defs: Vec<SectionDef> = Vec::new();
501 if let Some(children) = node.children() {
502 for child in children.nodes() {
503 if child.name().value() == "section" {
504 defs.push(transform_section_def(child)?);
505 }
506 }
507 }
508 Ok(defs)
509}
510
511fn transform_section_def(node: &KdlNode) -> Result<SectionDef, ParseError> {
512 let id = required_string_prop(node, "id")?.to_owned();
513 let name = required_string_prop(node, "name")?.to_owned();
514 let start_page = required_string_prop_aliased(node, "start-page", "start_page")?.to_owned();
515
516 let folio_start = optional_u32_prop(node, "folio-start")
521 .or_else(|| optional_u32_prop(node, "folio_start"))
522 .map(|n| n as usize);
523
524 let folio_style =
526 optional_string_prop_aliased(node, "folio-style", "folio_style").map(str::to_owned);
527
528 Ok(SectionDef {
529 id,
530 name,
531 folio_start,
532 folio_style,
533 start_page,
534 source_span: node_span(node),
535 })
536}
537
538const LIBRARY_KNOWN_PROPS: &[&str] = &["id", "version", "hash"];
543
544fn transform_libraries(node: &KdlNode) -> Result<Vec<LibraryDef>, ParseError> {
549 let mut defs: Vec<LibraryDef> = Vec::new();
550 if let Some(children) = node.children() {
551 for child in children.nodes() {
552 if child.name().value() == "library" {
553 defs.push(transform_library_def(child)?);
554 }
555 }
556 }
557 Ok(defs)
558}
559
560fn transform_library_def(node: &KdlNode) -> Result<LibraryDef, ParseError> {
561 let id = required_string_prop(node, "id")?.to_owned();
562 let version = optional_string_prop(node, "version").map(str::to_owned);
563 let hash = optional_string_prop(node, "hash").map(str::to_owned);
564 let unknown_props = collect_unknown_props(node, LIBRARY_KNOWN_PROPS);
565 let source_span = node_span(node);
566
567 Ok(LibraryDef {
568 id,
569 version,
570 hash,
571 source_span,
572 unknown_props,
573 })
574}
575
576const ACTION_KNOWN_PROPS: &[&str] = &["id", "label", "version"];
581
582fn transform_actions(node: &KdlNode) -> Result<Vec<ActionDef>, ParseError> {
588 let mut defs: Vec<ActionDef> = Vec::new();
589 if let Some(children) = node.children() {
590 for child in children.nodes() {
591 if child.name().value() == "action" {
592 defs.push(transform_action_def(child)?);
593 }
594 }
595 }
596 Ok(defs)
597}
598
599fn transform_action_def(node: &KdlNode) -> Result<ActionDef, ParseError> {
600 let id = required_string_prop(node, "id")?.to_owned();
601 let label = optional_string_prop(node, "label").map(str::to_owned);
602 let version = optional_string_prop(node, "version").map(str::to_owned);
603 let unknown_props = collect_unknown_props(node, ACTION_KNOWN_PROPS);
604 let source_span = node_span(node);
605
606 let tx_json = node
610 .children()
611 .and_then(|doc| {
612 doc.nodes().iter().find_map(|child| {
613 if child.name().value() != "tx" {
614 return None;
615 }
616 child.get(0).and_then(|v| match v {
617 KdlValue::String(s) => Some(s.clone()),
618 _ => None,
619 })
620 })
621 })
622 .ok_or_else(|| {
623 ParseError::spanless(
624 ParseErrorCode::InvalidPropertyValue,
625 format!("node `action` id=\"{id}\" is missing required `tx` child node"),
626 )
627 })?;
628
629 Ok(ActionDef {
630 id,
631 label,
632 version,
633 tx_json,
634 source_span,
635 unknown_props,
636 })
637}
638
639const PROVENANCE_KNOWN_PROPS: &[&str] = &["id", "node", "library", "item", "linked"];
644
645fn transform_provenance(node: &KdlNode) -> Result<Vec<ProvenanceDef>, ParseError> {
650 let mut defs: Vec<ProvenanceDef> = Vec::new();
651 if let Some(children) = node.children() {
652 for child in children.nodes() {
653 if child.name().value() == "origin" {
654 defs.push(transform_provenance_def(child)?);
655 }
656 }
657 }
658 Ok(defs)
659}
660
661fn transform_provenance_def(node: &KdlNode) -> Result<ProvenanceDef, ParseError> {
662 let id = required_string_prop(node, "id")?.to_owned();
663 let document_node = required_string_prop(node, "node")?.to_owned();
664 let library = required_string_prop(node, "library")?.to_owned();
665 let item = optional_string_prop(node, "item").map(str::to_owned);
666 let linked = optional_bool_prop(node, "linked");
667 let unknown_props = collect_unknown_props(node, PROVENANCE_KNOWN_PROPS);
668 let source_span = node_span(node);
669
670 Ok(ProvenanceDef {
671 id,
672 node: document_node,
673 library,
674 item,
675 linked,
676 source_span,
677 unknown_props,
678 })
679}
680
681const VARIANT_KNOWN_PROPS: &[&str] = &["id", "source", "w", "h"];
686const VARIANT_OVERRIDE_KNOWN_PROPS: &[&str] =
687 &["node", "visible", "x", "y", "w", "h", "fill", "text"];
688
689fn transform_variants(node: &KdlNode) -> Result<Vec<VariantDef>, ParseError> {
694 let mut defs: Vec<VariantDef> = Vec::new();
695 if let Some(children) = node.children() {
696 for child in children.nodes() {
697 if child.name().value() == "variant" {
698 defs.push(transform_variant_def(child)?);
699 }
700 }
701 }
702 Ok(defs)
703}
704
705fn transform_variant_def(node: &KdlNode) -> Result<VariantDef, ParseError> {
706 let id = required_string_prop(node, "id")?.to_owned();
707 let source = required_string_prop(node, "source")?.to_owned();
708
709 let w = node
710 .entry("w")
711 .ok_or_else(|| {
712 ParseError::spanless(
713 ParseErrorCode::InvalidPropertyValue,
714 format!("variant `{id}` is missing required property `w`"),
715 )
716 })
717 .and_then(|e| entry_to_dimension(e, "w"))?;
718
719 let h = node
720 .entry("h")
721 .ok_or_else(|| {
722 ParseError::spanless(
723 ParseErrorCode::InvalidPropertyValue,
724 format!("variant `{id}` is missing required property `h`"),
725 )
726 })
727 .and_then(|e| entry_to_dimension(e, "h"))?;
728
729 let unknown_props = collect_unknown_props(node, VARIANT_KNOWN_PROPS);
730 let source_span = node_span(node);
731
732 let mut overrides: Vec<VariantOverride> = Vec::new();
733 if let Some(children) = node.children() {
734 for child in children.nodes() {
735 if child.name().value() == "override" {
736 overrides.push(transform_variant_override(child)?);
737 }
738 }
739 }
740
741 Ok(VariantDef {
742 id,
743 source,
744 w,
745 h,
746 overrides,
747 source_span,
748 unknown_props,
749 })
750}
751
752fn transform_variant_override(node: &KdlNode) -> Result<VariantOverride, ParseError> {
753 let target_node = required_string_prop(node, "node")?.to_owned();
754 let visible = optional_bool_prop(node, "visible");
755 let x = optional_dimension_prop(node, "x");
756 let y = optional_dimension_prop(node, "y");
757 let w = optional_dimension_prop(node, "w");
758 let h = optional_dimension_prop(node, "h");
759 let fill = node
760 .entry("fill")
761 .and_then(|e| entry_to_property_value(e).ok());
762 let text = optional_string_prop(node, "text").map(str::to_owned);
763 let unknown_props = collect_unknown_props(node, VARIANT_OVERRIDE_KNOWN_PROPS);
764 let source_span = node_span(node);
765
766 Ok(VariantOverride {
767 node: target_node,
768 visible,
769 x,
770 y,
771 w,
772 h,
773 fill,
774 text,
775 source_span,
776 unknown_props,
777 })
778}
779
780const RECIPE_KNOWN_PROPS: &[&str] = &["id", "kind", "seed", "generator", "bounds", "detached"];
785const RECIPE_PARAM_KNOWN_PROPS: &[&str] = &["name", "value"];
786
787fn transform_recipes(node: &KdlNode) -> Result<Vec<RecipeDef>, ParseError> {
792 let mut defs: Vec<RecipeDef> = Vec::new();
793 if let Some(children) = node.children() {
794 for child in children.nodes() {
795 if child.name().value() == "recipe" {
796 defs.push(transform_recipe_def(child)?);
797 }
798 }
799 }
800 Ok(defs)
801}
802
803fn transform_recipe_def(node: &KdlNode) -> Result<RecipeDef, ParseError> {
804 let id = required_string_prop(node, "id")?.to_owned();
805 let kind = required_string_prop(node, "kind")?.to_owned();
806
807 let seed = optional_i64_prop(node, "seed");
809
810 let generator = optional_string_prop(node, "generator").map(str::to_owned);
811 let bounds = optional_string_prop(node, "bounds").map(str::to_owned);
812 let detached = optional_bool_prop(node, "detached");
813
814 let unknown_props = collect_unknown_props(node, RECIPE_KNOWN_PROPS);
815 let source_span = node_span(node);
816
817 let mut params: Vec<RecipeParam> = Vec::new();
818 let mut palette: Vec<String> = Vec::new();
819 let mut expanded: Vec<String> = Vec::new();
820
821 if let Some(children) = node.children() {
822 for child in children.nodes() {
823 match child.name().value() {
824 "param" => {
825 params.push(transform_recipe_param(child)?);
826 }
827 "palette" => {
828 palette.push(required_string_prop(child, "token")?.to_owned());
829 }
830 "expanded" => {
831 expanded.push(required_string_prop(child, "node")?.to_owned());
832 }
833 _ => {}
834 }
835 }
836 }
837
838 Ok(RecipeDef {
839 id,
840 kind,
841 seed,
842 generator,
843 bounds,
844 detached,
845 params,
846 palette,
847 expanded,
848 source_span,
849 unknown_props,
850 })
851}
852
853fn transform_recipe_param(node: &KdlNode) -> Result<RecipeParam, ParseError> {
854 let name = required_string_prop(node, "name")?.to_owned();
855 let value = node
856 .entry("value")
857 .ok_or_else(|| {
858 ParseError::spanless(
859 ParseErrorCode::InvalidPropertyValue,
860 format!("recipe `param` `{name}` is missing required property `value`"),
861 )
862 })
863 .and_then(entry_to_property_value)?;
864 let unknown_props = collect_unknown_props(node, RECIPE_PARAM_KNOWN_PROPS);
865 let source_span = node_span(node);
866
867 Ok(RecipeParam {
868 name,
869 value,
870 source_span,
871 unknown_props,
872 })
873}
874
875fn transform_components(node: &KdlNode) -> Result<Vec<ComponentDef>, ParseError> {
885 let mut defs: Vec<ComponentDef> = Vec::new();
886 if let Some(children) = node.children() {
887 for child in children.nodes() {
888 if child.name().value() == "component" {
889 defs.push(transform_component_def(child)?);
890 }
891 }
892 }
893 Ok(defs)
894}
895
896fn transform_component_def(node: &KdlNode) -> Result<ComponentDef, ParseError> {
897 let id = required_string_prop(node, "id")?.to_owned();
898 let children = transform_children(node)?;
899 Ok(ComponentDef {
900 id,
901 children,
902 source_span: node_span(node),
903 })
904}
905
906fn transform_project(node: &KdlNode) -> Result<Project, ParseError> {
911 let id = required_string_prop(node, "id")?.to_owned();
912 let name = required_string_prop(node, "name")?.to_owned();
913 let author = node.children().and_then(|doc| {
914 doc.nodes()
915 .iter()
916 .find(|n| n.name().value() == "author")
917 .and_then(|n| n.get(0))
918 .and_then(|v| {
919 if let KdlValue::String(s) = v {
920 Some(s.clone())
921 } else {
922 None
923 }
924 })
925 });
926 Ok(Project { id, name, author })
927}
928
929pub(crate) const ASSET_KNOWN_PROPS: &[&str] = &[
938 "id",
939 "kind",
940 "src",
941 "sha256",
942 "ai-prompt",
943 "ai-model",
944 "ai-provider",
945 "ai-seed",
946 "ai-generation-date",
947 "ai-license",
948 "ai-source-rights",
949 "ai-safety-status",
950 "ai-reuse-policy",
951];
952
953fn transform_assets(node: &KdlNode) -> Result<AssetBlock, ParseError> {
954 let source_span = node_span(node);
955 let mut asset_list: Vec<AssetDecl> = Vec::new();
956
957 if let Some(children) = node.children() {
958 for child in children.nodes() {
959 if child.name().value() == "asset" {
960 asset_list.push(transform_asset_decl(child)?);
961 }
962 }
965 }
966
967 Ok(AssetBlock {
968 assets: asset_list,
969 source_span,
970 })
971}
972
973fn transform_asset_decl(node: &KdlNode) -> Result<AssetDecl, ParseError> {
974 let id = required_string_prop(node, "id")?.to_owned();
975 let kind_str = required_string_prop(node, "kind")?;
976 let kind = AssetKind::from_kind_str(kind_str);
977 let src = required_string_prop(node, "src")?.to_owned();
978 let sha256 = optional_string_prop(node, "sha256").map(str::to_owned);
979 let ai_prompt = optional_string_prop(node, "ai-prompt").map(str::to_owned);
980 let ai_model = optional_string_prop(node, "ai-model").map(str::to_owned);
981 let ai_provider = optional_string_prop(node, "ai-provider").map(str::to_owned);
982 let ai_seed = optional_i64_prop(node, "ai-seed");
983 let ai_generation_date = optional_string_prop(node, "ai-generation-date").map(str::to_owned);
984 let ai_license = optional_string_prop(node, "ai-license").map(str::to_owned);
985 let ai_source_rights = optional_string_prop(node, "ai-source-rights").map(str::to_owned);
986 let ai_safety_status = optional_string_prop(node, "ai-safety-status").map(str::to_owned);
987 let ai_reuse_policy = optional_string_prop(node, "ai-reuse-policy").map(str::to_owned);
988 let unknown_props = collect_unknown_props(node, ASSET_KNOWN_PROPS);
989 let source_span = node_span(node);
990
991 Ok(AssetDecl {
992 id,
993 kind,
994 src,
995 sha256,
996 ai_prompt,
997 ai_model,
998 ai_provider,
999 ai_seed,
1000 ai_generation_date,
1001 ai_license,
1002 ai_source_rights,
1003 ai_safety_status,
1004 ai_reuse_policy,
1005 source_span,
1006 unknown_props,
1007 })
1008}
1009
1010fn transform_document_body(node: &KdlNode) -> Result<DocumentBody, ParseError> {
1015 let id = required_string_prop(node, "id")?.to_owned();
1016 let title = optional_string_prop(node, "title").map(str::to_owned);
1017
1018 let mut block_styles: Vec<BlockStyle> = Vec::new();
1019 let mut pages: Vec<Page> = Vec::new();
1020 if let Some(children) = node.children() {
1021 for child in children.nodes() {
1022 match child.name().value() {
1023 "block" => block_styles.push(transform_block_style(child)?),
1024 "page" => pages.push(transform_page(child)?),
1025 _ => {}
1026 }
1027 }
1028 }
1029
1030 Ok(DocumentBody {
1031 id,
1032 title,
1033 block_styles,
1034 pages,
1035 })
1036}
1037
1038pub(super) fn transform_children(node: &KdlNode) -> Result<Vec<Node>, ParseError> {
1049 let mut children: Vec<Node> = Vec::new();
1050 if let Some(doc) = node.children() {
1051 for child in doc.nodes() {
1052 children.push(transform_node(child)?);
1053 }
1054 }
1055 Ok(children)
1056}