use kdl::{KdlDocument, KdlNode, KdlValue};
use crate::ast::{
action::ActionDef,
asset::{AssetBlock, AssetDecl, AssetKind},
block_style::BlockStyle,
brand::BrandContract,
document::{ComponentDef, Document, DocumentBody, MasterDef, Page, Project, SectionDef},
library::LibraryDef,
node::Node,
policy::{DiagnosticPolicy, PolicyEntry, PolicyVerb},
provenance::ProvenanceDef,
recipe::{RecipeDef, RecipeParam},
style::StyleBlock,
token::TokenBlock,
variant::{VariantDef, VariantOverride},
};
use crate::error::{ParseError, ParseErrorCode};
use super::block_style::transform_block_style;
use super::helpers::{
collect_unknown_props, entry_to_dimension, entry_to_property_value, node_span,
optional_bool_prop, optional_dimension_prop, optional_i64_prop, optional_string_prop,
optional_string_prop_aliased, optional_u32_prop, required_string_prop,
required_string_prop_aliased, required_u32_prop,
};
use super::node::transform_node;
use super::page::transform_page;
use super::tokens::{transform_styles, transform_tokens};
pub(crate) const DOCUMENT_KNOWN_PROPS: &[&str] = &[
"version",
"colorspace",
"doc-id",
"doc_id",
"mirror-margins",
"mirror_margins",
"page-progression",
"page_progression",
"page-parity-start",
"page_parity_start",
"facing-pages",
"facing_pages",
"spread-gutter",
"spread_gutter",
"margin-inner",
"margin_inner",
"margin-outer",
"margin_outer",
"margin-top",
"margin_top",
"margin-bottom",
"margin_bottom",
"id",
"title",
];
pub fn transform(doc: &KdlDocument) -> Result<Document, ParseError> {
let zenith_node = doc
.nodes()
.iter()
.find(|n| n.name().value() == "zenith")
.ok_or_else(|| {
ParseError::spanless(
ParseErrorCode::MissingZenithRoot,
"no top-level `zenith` node found",
)
})?;
let version = required_u32_prop(zenith_node, "version")?;
let colorspace = optional_string_prop(zenith_node, "colorspace").map(str::to_owned);
let doc_id = optional_string_prop(zenith_node, "doc-id")
.or_else(|| optional_string_prop(zenith_node, "doc_id"))
.map(str::to_owned);
let mirror_margins = optional_bool_prop(zenith_node, "mirror-margins")
.or_else(|| optional_bool_prop(zenith_node, "mirror_margins"));
let page_progression = optional_string_prop(zenith_node, "page-progression")
.or_else(|| optional_string_prop(zenith_node, "page_progression"))
.map(str::to_owned);
let page_parity_start = optional_string_prop(zenith_node, "page-parity-start")
.or_else(|| optional_string_prop(zenith_node, "page_parity_start"))
.map(str::to_owned);
let facing_pages = optional_bool_prop(zenith_node, "facing-pages")
.or_else(|| optional_bool_prop(zenith_node, "facing_pages"));
let spread_gutter = optional_dimension_prop(zenith_node, "spread-gutter")
.or_else(|| optional_dimension_prop(zenith_node, "spread_gutter"));
let margin_inner = optional_dimension_prop(zenith_node, "margin-inner")
.or_else(|| optional_dimension_prop(zenith_node, "margin_inner"));
let margin_outer = optional_dimension_prop(zenith_node, "margin-outer")
.or_else(|| optional_dimension_prop(zenith_node, "margin_outer"));
let margin_top = optional_dimension_prop(zenith_node, "margin-top")
.or_else(|| optional_dimension_prop(zenith_node, "margin_top"));
let margin_bottom = optional_dimension_prop(zenith_node, "margin-bottom")
.or_else(|| optional_dimension_prop(zenith_node, "margin_bottom"));
let children_doc = zenith_node.children().ok_or_else(|| {
ParseError::spanless(
ParseErrorCode::MissingZenithRoot,
"`zenith` node has no children block",
)
})?;
let mut project: Option<Project> = None;
let mut assets = AssetBlock::default();
let mut libraries: Vec<LibraryDef> = Vec::new();
let mut actions: Vec<ActionDef> = Vec::new();
let mut tokens = TokenBlock::default();
let mut styles = StyleBlock::default();
let mut components: Vec<ComponentDef> = Vec::new();
let mut masters: Vec<MasterDef> = Vec::new();
let mut sections: Vec<SectionDef> = Vec::new();
let mut provenance: Vec<ProvenanceDef> = Vec::new();
let mut variants: Vec<VariantDef> = Vec::new();
let mut recipes: Vec<RecipeDef> = Vec::new();
let mut diagnostic_policy = DiagnosticPolicy::default();
let mut brand_contract = BrandContract::default();
let mut body: Option<DocumentBody> = None;
for child in children_doc.nodes() {
match child.name().value() {
"project" => {
project = Some(transform_project(child)?);
}
"assets" => {
assets = transform_assets(child)?;
}
"libraries" => {
libraries = transform_libraries(child)?;
}
"actions" => {
actions = transform_actions(child)?;
}
"tokens" => {
tokens = transform_tokens(child)?;
}
"styles" => {
styles = transform_styles(child)?;
}
"components" => {
components = transform_components(child)?;
}
"masters" => {
masters = transform_masters(child)?;
}
"sections" => {
sections = transform_sections(child)?;
}
"provenance" => {
provenance = transform_provenance(child)?;
}
"variants" => {
variants = transform_variants(child)?;
}
"recipes" => {
recipes = transform_recipes(child)?;
}
"diagnostics" => {
diagnostic_policy = transform_diagnostic_policy(child)?;
}
"brand" => {
brand_contract = transform_brand_contract(child)?;
}
"document" => {
body = Some(transform_document_body(child)?);
}
_ => {}
}
}
let body = body.ok_or_else(|| {
ParseError::spanless(
ParseErrorCode::MissingZenithRoot,
"`zenith` node is missing a `document` child",
)
})?;
Ok(Document {
version,
colorspace,
doc_id,
mirror_margins,
facing_pages,
spread_gutter,
page_progression,
page_parity_start,
margin_inner,
margin_outer,
margin_top,
margin_bottom,
project,
assets,
libraries,
actions,
tokens,
styles,
components,
masters,
sections,
provenance,
variants,
recipes,
diagnostic_policy,
brand_contract,
body,
})
}
pub(crate) fn transform_diagnostic_policy(node: &KdlNode) -> Result<DiagnosticPolicy, ParseError> {
let mut entries: Vec<PolicyEntry> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
let (verb, verb_name) = match child.name().value() {
"allow" => (PolicyVerb::Allow, "allow"),
"deny" => (PolicyVerb::Deny, "deny"),
"warn" => (PolicyVerb::Warn, "warn"),
_ => continue,
};
let code = match child.get(0) {
Some(KdlValue::String(s)) => s.clone(),
_ => {
return Err(ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!(
"diagnostics `{verb_name}` entry requires a quoted diagnostic-code \
string as its first argument, e.g. `{verb_name} \"layout.off_canvas\"`"
),
));
}
};
entries.push(PolicyEntry {
verb,
code,
source_span: node_span(child),
});
}
}
Ok(DiagnosticPolicy { entries })
}
pub(crate) fn transform_brand_contract(node: &KdlNode) -> Result<BrandContract, ParseError> {
let source_span = node_span(node);
let mut allowed_colors: Option<Vec<String>> = None;
let mut allowed_fonts: Option<Vec<String>> = None;
let mut allowed_weights: Option<Vec<u32>> = None;
if let Some(children) = node.children() {
for child in children.nodes() {
match child.name().value() {
"colors" => {
let mut colors: Vec<String> = Vec::new();
let positional: Vec<_> = child
.entries()
.iter()
.filter(|e| e.name().is_none())
.collect();
for (idx, entry) in positional.iter().enumerate() {
match entry.value() {
KdlValue::String(s) => {
colors.push(s.to_lowercase());
}
_ => {
return Err(ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!(
"brand `colors` argument {idx} must be a quoted string \
(hex color), e.g. `colors \"#0b1f33\" \"#ffffff\"`"
),
));
}
}
}
allowed_colors = Some(colors);
}
"fonts" => {
let mut fonts: Vec<String> = Vec::new();
let positional: Vec<_> = child
.entries()
.iter()
.filter(|e| e.name().is_none())
.collect();
for (idx, entry) in positional.iter().enumerate() {
match entry.value() {
KdlValue::String(s) => {
fonts.push(s.clone());
}
_ => {
return Err(ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!(
"brand `fonts` argument {idx} must be a quoted string \
(font-family name), e.g. `fonts \"Noto Sans\"`"
),
));
}
}
}
allowed_fonts = Some(fonts);
}
"weights" => {
let mut weights: Vec<u32> = Vec::new();
let positional: Vec<_> = child
.entries()
.iter()
.filter(|e| e.name().is_none())
.collect();
for (idx, entry) in positional.iter().enumerate() {
match entry.value() {
KdlValue::Integer(n) => {
let n_val = *n;
if !(100..=900).contains(&n_val) {
return Err(ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!(
"brand `weights` argument {idx} must be an integer \
in the range 100-900 (got {n_val})"
),
));
}
let w = u32::try_from(n_val).map_err(|_| {
ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!(
"brand `weights` argument {idx} is out of range \
for a u32 weight (got {n_val})"
),
)
})?;
weights.push(w);
}
_ => {
return Err(ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!(
"brand `weights` argument {idx} must be an integer \
(font weight), e.g. `weights 400 700`"
),
));
}
}
}
allowed_weights = Some(weights);
}
_ => {}
}
}
}
Ok(BrandContract {
allowed_colors,
allowed_fonts,
allowed_weights,
source_span,
})
}
fn transform_masters(node: &KdlNode) -> Result<Vec<MasterDef>, ParseError> {
let mut defs: Vec<MasterDef> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
if child.name().value() == "master" {
defs.push(transform_master_def(child)?);
}
}
}
Ok(defs)
}
fn transform_master_def(node: &KdlNode) -> Result<MasterDef, ParseError> {
let id = required_string_prop(node, "id")?.to_owned();
let children = transform_children(node)?;
Ok(MasterDef {
id,
children,
source_span: node_span(node),
})
}
fn transform_sections(node: &KdlNode) -> Result<Vec<SectionDef>, ParseError> {
let mut defs: Vec<SectionDef> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
if child.name().value() == "section" {
defs.push(transform_section_def(child)?);
}
}
}
Ok(defs)
}
fn transform_section_def(node: &KdlNode) -> Result<SectionDef, ParseError> {
let id = required_string_prop(node, "id")?.to_owned();
let name = required_string_prop(node, "name")?.to_owned();
let start_page = required_string_prop_aliased(node, "start-page", "start_page")?.to_owned();
let folio_start = optional_u32_prop(node, "folio-start")
.or_else(|| optional_u32_prop(node, "folio_start"))
.map(|n| n as usize);
let folio_style =
optional_string_prop_aliased(node, "folio-style", "folio_style").map(str::to_owned);
Ok(SectionDef {
id,
name,
folio_start,
folio_style,
start_page,
source_span: node_span(node),
})
}
const LIBRARY_KNOWN_PROPS: &[&str] = &["id", "version", "hash"];
fn transform_libraries(node: &KdlNode) -> Result<Vec<LibraryDef>, ParseError> {
let mut defs: Vec<LibraryDef> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
if child.name().value() == "library" {
defs.push(transform_library_def(child)?);
}
}
}
Ok(defs)
}
fn transform_library_def(node: &KdlNode) -> Result<LibraryDef, ParseError> {
let id = required_string_prop(node, "id")?.to_owned();
let version = optional_string_prop(node, "version").map(str::to_owned);
let hash = optional_string_prop(node, "hash").map(str::to_owned);
let unknown_props = collect_unknown_props(node, LIBRARY_KNOWN_PROPS);
let source_span = node_span(node);
Ok(LibraryDef {
id,
version,
hash,
source_span,
unknown_props,
})
}
const ACTION_KNOWN_PROPS: &[&str] = &["id", "label", "version"];
fn transform_actions(node: &KdlNode) -> Result<Vec<ActionDef>, ParseError> {
let mut defs: Vec<ActionDef> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
if child.name().value() == "action" {
defs.push(transform_action_def(child)?);
}
}
}
Ok(defs)
}
fn transform_action_def(node: &KdlNode) -> Result<ActionDef, ParseError> {
let id = required_string_prop(node, "id")?.to_owned();
let label = optional_string_prop(node, "label").map(str::to_owned);
let version = optional_string_prop(node, "version").map(str::to_owned);
let unknown_props = collect_unknown_props(node, ACTION_KNOWN_PROPS);
let source_span = node_span(node);
let tx_json = node
.children()
.and_then(|doc| {
doc.nodes().iter().find_map(|child| {
if child.name().value() != "tx" {
return None;
}
child.get(0).and_then(|v| match v {
KdlValue::String(s) => Some(s.clone()),
_ => None,
})
})
})
.ok_or_else(|| {
ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!("node `action` id=\"{id}\" is missing required `tx` child node"),
)
})?;
Ok(ActionDef {
id,
label,
version,
tx_json,
source_span,
unknown_props,
})
}
const PROVENANCE_KNOWN_PROPS: &[&str] = &["id", "node", "library", "item", "linked"];
fn transform_provenance(node: &KdlNode) -> Result<Vec<ProvenanceDef>, ParseError> {
let mut defs: Vec<ProvenanceDef> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
if child.name().value() == "origin" {
defs.push(transform_provenance_def(child)?);
}
}
}
Ok(defs)
}
fn transform_provenance_def(node: &KdlNode) -> Result<ProvenanceDef, ParseError> {
let id = required_string_prop(node, "id")?.to_owned();
let document_node = required_string_prop(node, "node")?.to_owned();
let library = required_string_prop(node, "library")?.to_owned();
let item = optional_string_prop(node, "item").map(str::to_owned);
let linked = optional_bool_prop(node, "linked");
let unknown_props = collect_unknown_props(node, PROVENANCE_KNOWN_PROPS);
let source_span = node_span(node);
Ok(ProvenanceDef {
id,
node: document_node,
library,
item,
linked,
source_span,
unknown_props,
})
}
const VARIANT_KNOWN_PROPS: &[&str] = &["id", "source", "w", "h"];
const VARIANT_OVERRIDE_KNOWN_PROPS: &[&str] =
&["node", "visible", "x", "y", "w", "h", "fill", "text"];
fn transform_variants(node: &KdlNode) -> Result<Vec<VariantDef>, ParseError> {
let mut defs: Vec<VariantDef> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
if child.name().value() == "variant" {
defs.push(transform_variant_def(child)?);
}
}
}
Ok(defs)
}
fn transform_variant_def(node: &KdlNode) -> Result<VariantDef, ParseError> {
let id = required_string_prop(node, "id")?.to_owned();
let source = required_string_prop(node, "source")?.to_owned();
let w = node
.entry("w")
.ok_or_else(|| {
ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!("variant `{id}` is missing required property `w`"),
)
})
.and_then(|e| entry_to_dimension(e, "w"))?;
let h = node
.entry("h")
.ok_or_else(|| {
ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!("variant `{id}` is missing required property `h`"),
)
})
.and_then(|e| entry_to_dimension(e, "h"))?;
let unknown_props = collect_unknown_props(node, VARIANT_KNOWN_PROPS);
let source_span = node_span(node);
let mut overrides: Vec<VariantOverride> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
if child.name().value() == "override" {
overrides.push(transform_variant_override(child)?);
}
}
}
Ok(VariantDef {
id,
source,
w,
h,
overrides,
source_span,
unknown_props,
})
}
fn transform_variant_override(node: &KdlNode) -> Result<VariantOverride, ParseError> {
let target_node = required_string_prop(node, "node")?.to_owned();
let visible = optional_bool_prop(node, "visible");
let x = optional_dimension_prop(node, "x");
let y = optional_dimension_prop(node, "y");
let w = optional_dimension_prop(node, "w");
let h = optional_dimension_prop(node, "h");
let fill = node
.entry("fill")
.and_then(|e| entry_to_property_value(e).ok());
let text = optional_string_prop(node, "text").map(str::to_owned);
let unknown_props = collect_unknown_props(node, VARIANT_OVERRIDE_KNOWN_PROPS);
let source_span = node_span(node);
Ok(VariantOverride {
node: target_node,
visible,
x,
y,
w,
h,
fill,
text,
source_span,
unknown_props,
})
}
const RECIPE_KNOWN_PROPS: &[&str] = &["id", "kind", "seed", "generator", "bounds", "detached"];
const RECIPE_PARAM_KNOWN_PROPS: &[&str] = &["name", "value"];
fn transform_recipes(node: &KdlNode) -> Result<Vec<RecipeDef>, ParseError> {
let mut defs: Vec<RecipeDef> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
if child.name().value() == "recipe" {
defs.push(transform_recipe_def(child)?);
}
}
}
Ok(defs)
}
fn transform_recipe_def(node: &KdlNode) -> Result<RecipeDef, ParseError> {
let id = required_string_prop(node, "id")?.to_owned();
let kind = required_string_prop(node, "kind")?.to_owned();
let seed = optional_i64_prop(node, "seed");
let generator = optional_string_prop(node, "generator").map(str::to_owned);
let bounds = optional_string_prop(node, "bounds").map(str::to_owned);
let detached = optional_bool_prop(node, "detached");
let unknown_props = collect_unknown_props(node, RECIPE_KNOWN_PROPS);
let source_span = node_span(node);
let mut params: Vec<RecipeParam> = Vec::new();
let mut palette: Vec<String> = Vec::new();
let mut expanded: Vec<String> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
match child.name().value() {
"param" => {
params.push(transform_recipe_param(child)?);
}
"palette" => {
palette.push(required_string_prop(child, "token")?.to_owned());
}
"expanded" => {
expanded.push(required_string_prop(child, "node")?.to_owned());
}
_ => {}
}
}
}
Ok(RecipeDef {
id,
kind,
seed,
generator,
bounds,
detached,
params,
palette,
expanded,
source_span,
unknown_props,
})
}
fn transform_recipe_param(node: &KdlNode) -> Result<RecipeParam, ParseError> {
let name = required_string_prop(node, "name")?.to_owned();
let value = node
.entry("value")
.ok_or_else(|| {
ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!("recipe `param` `{name}` is missing required property `value`"),
)
})
.and_then(entry_to_property_value)?;
let unknown_props = collect_unknown_props(node, RECIPE_PARAM_KNOWN_PROPS);
let source_span = node_span(node);
Ok(RecipeParam {
name,
value,
source_span,
unknown_props,
})
}
fn transform_components(node: &KdlNode) -> Result<Vec<ComponentDef>, ParseError> {
let mut defs: Vec<ComponentDef> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
if child.name().value() == "component" {
defs.push(transform_component_def(child)?);
}
}
}
Ok(defs)
}
fn transform_component_def(node: &KdlNode) -> Result<ComponentDef, ParseError> {
let id = required_string_prop(node, "id")?.to_owned();
let children = transform_children(node)?;
Ok(ComponentDef {
id,
children,
source_span: node_span(node),
})
}
fn transform_project(node: &KdlNode) -> Result<Project, ParseError> {
let id = required_string_prop(node, "id")?.to_owned();
let name = required_string_prop(node, "name")?.to_owned();
let author = node.children().and_then(|doc| {
doc.nodes()
.iter()
.find(|n| n.name().value() == "author")
.and_then(|n| n.get(0))
.and_then(|v| {
if let KdlValue::String(s) = v {
Some(s.clone())
} else {
None
}
})
});
Ok(Project { id, name, author })
}
pub(crate) const ASSET_KNOWN_PROPS: &[&str] = &[
"id",
"kind",
"src",
"sha256",
"ai-prompt",
"ai-model",
"ai-provider",
"ai-seed",
"ai-generation-date",
"ai-license",
"ai-source-rights",
"ai-safety-status",
"ai-reuse-policy",
];
fn transform_assets(node: &KdlNode) -> Result<AssetBlock, ParseError> {
let source_span = node_span(node);
let mut asset_list: Vec<AssetDecl> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
if child.name().value() == "asset" {
asset_list.push(transform_asset_decl(child)?);
}
}
}
Ok(AssetBlock {
assets: asset_list,
source_span,
})
}
fn transform_asset_decl(node: &KdlNode) -> Result<AssetDecl, ParseError> {
let id = required_string_prop(node, "id")?.to_owned();
let kind_str = required_string_prop(node, "kind")?;
let kind = AssetKind::from_kind_str(kind_str);
let src = required_string_prop(node, "src")?.to_owned();
let sha256 = optional_string_prop(node, "sha256").map(str::to_owned);
let ai_prompt = optional_string_prop(node, "ai-prompt").map(str::to_owned);
let ai_model = optional_string_prop(node, "ai-model").map(str::to_owned);
let ai_provider = optional_string_prop(node, "ai-provider").map(str::to_owned);
let ai_seed = optional_i64_prop(node, "ai-seed");
let ai_generation_date = optional_string_prop(node, "ai-generation-date").map(str::to_owned);
let ai_license = optional_string_prop(node, "ai-license").map(str::to_owned);
let ai_source_rights = optional_string_prop(node, "ai-source-rights").map(str::to_owned);
let ai_safety_status = optional_string_prop(node, "ai-safety-status").map(str::to_owned);
let ai_reuse_policy = optional_string_prop(node, "ai-reuse-policy").map(str::to_owned);
let unknown_props = collect_unknown_props(node, ASSET_KNOWN_PROPS);
let source_span = node_span(node);
Ok(AssetDecl {
id,
kind,
src,
sha256,
ai_prompt,
ai_model,
ai_provider,
ai_seed,
ai_generation_date,
ai_license,
ai_source_rights,
ai_safety_status,
ai_reuse_policy,
source_span,
unknown_props,
})
}
fn transform_document_body(node: &KdlNode) -> Result<DocumentBody, ParseError> {
let id = required_string_prop(node, "id")?.to_owned();
let title = optional_string_prop(node, "title").map(str::to_owned);
let mut block_styles: Vec<BlockStyle> = Vec::new();
let mut pages: Vec<Page> = Vec::new();
if let Some(children) = node.children() {
for child in children.nodes() {
match child.name().value() {
"block" => block_styles.push(transform_block_style(child)?),
"page" => pages.push(transform_page(child)?),
_ => {}
}
}
}
Ok(DocumentBody {
id,
title,
block_styles,
pages,
})
}
pub(super) fn transform_children(node: &KdlNode) -> Result<Vec<Node>, ParseError> {
let mut children: Vec<Node> = Vec::new();
if let Some(doc) = node.children() {
for child in doc.nodes() {
children.push(transform_node(child)?);
}
}
Ok(children)
}