use std::collections::BTreeMap;
use kdl::{KdlEntry, KdlNode, KdlValue};
use crate::ast::{
Span,
node::{ObjectPosition, UnknownProperty, UnknownValue},
value::{Dimension, PropertyValue, Unit},
};
use crate::error::{ParseError, ParseErrorCode};
pub(super) fn node_span(node: &KdlNode) -> Option<Span> {
let span = node.span();
let start = span.offset();
Some(Span {
start,
end: start + span.len(),
})
}
pub(super) fn entry_annotation(entry: &KdlEntry) -> Option<&str> {
entry.ty().map(|id| id.value())
}
pub(super) fn entry_to_property_value(entry: &KdlEntry) -> Result<PropertyValue, ParseError> {
let annotation = entry_annotation(entry);
match annotation {
Some("token") => match entry.value() {
KdlValue::String(s) => Ok(PropertyValue::TokenRef(s.clone())),
other => Err(ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!("(token) annotation requires a string value, got: {other:?}"),
)),
},
Some("data") => match entry.value() {
KdlValue::String(s) => Ok(PropertyValue::DataRef(s.clone())),
other => Err(ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!("(data) annotation requires a string value, got: {other:?}"),
)),
},
Some(ann) => match kdl_value_to_f64(entry.value()) {
Some(value) => Ok(PropertyValue::Dimension(Dimension {
value,
unit: Unit::from_annotation(ann),
})),
None => Ok(PropertyValue::Literal(kdl_value_to_literal_string(
entry.value(),
))),
},
None => {
let literal = kdl_value_to_literal_string(entry.value());
Ok(PropertyValue::Literal(literal))
}
}
}
pub(super) fn kdl_value_to_f64(v: &KdlValue) -> Option<f64> {
match v {
KdlValue::Integer(n) => Some(*n as f64),
KdlValue::Float(f) => Some(*f),
_ => None,
}
}
pub(super) fn kdl_value_to_literal_string(v: &KdlValue) -> String {
match v {
KdlValue::String(s) => s.clone(),
KdlValue::Integer(n) => n.to_string(),
KdlValue::Float(f) => f.to_string(),
KdlValue::Bool(b) => b.to_string(),
KdlValue::Null => "null".to_owned(),
}
}
pub(super) fn entry_to_dimension(entry: &KdlEntry, prop: &str) -> Result<Dimension, ParseError> {
let unit_str = entry_annotation(entry).ok_or_else(|| {
ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!("property `{prop}` requires a unit annotation such as (px) or (pt)"),
)
})?;
let unit = Unit::from_annotation(unit_str);
let value = kdl_value_to_f64(entry.value()).ok_or_else(|| {
ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!(
"property `{prop}` must be numeric, got: {:?}",
entry.value()
),
)
})?;
Ok(Dimension { value, unit })
}
pub(super) fn required_string_prop<'a>(
node: &'a KdlNode,
key: &str,
) -> Result<&'a str, ParseError> {
node.get(key)
.and_then(|v| {
if let KdlValue::String(s) = v {
Some(s.as_str())
} else {
None
}
})
.ok_or_else(|| {
ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!(
"node `{}` is missing required string property `{key}`",
node.name().value()
),
)
})
}
pub(super) fn required_u32_prop(node: &KdlNode, key: &str) -> Result<u32, ParseError> {
node.get(key)
.and_then(|v| {
if let KdlValue::Integer(n) = v {
u32::try_from(*n).ok()
} else {
None
}
})
.ok_or_else(|| {
ParseError::spanless(
ParseErrorCode::InvalidPropertyValue,
format!(
"node `{}` is missing required integer property `{key}`",
node.name().value()
),
)
})
}
pub(super) fn optional_u32_prop(node: &KdlNode, key: &str) -> Option<u32> {
node.get(key).and_then(|v| {
if let KdlValue::Integer(n) = v {
u32::try_from(*n).ok()
} else {
None
}
})
}
pub(super) fn optional_bool_prop(node: &KdlNode, key: &str) -> Option<bool> {
node.get(key).and_then(|v| {
if let KdlValue::Bool(b) = v {
Some(*b)
} else {
None
}
})
}
pub(super) fn optional_i64_prop(node: &KdlNode, key: &str) -> Option<i64> {
node.get(key).and_then(|v| {
if let KdlValue::Integer(n) = v {
i64::try_from(*n).ok()
} else {
None
}
})
}
pub(super) fn optional_f64_prop(node: &KdlNode, key: &str) -> Option<f64> {
node.get(key).and_then(|v| match v {
KdlValue::Float(f) => Some(*f),
KdlValue::Integer(n) => Some(*n as f64),
_ => None,
})
}
pub(super) fn optional_string_prop<'a>(node: &'a KdlNode, key: &str) -> Option<&'a str> {
node.get(key).and_then(|v| {
if let KdlValue::String(s) = v {
Some(s.as_str())
} else {
None
}
})
}
pub(super) fn optional_dimension_prop(node: &KdlNode, key: &str) -> Option<Dimension> {
let entry = node.entry(key)?;
entry_to_dimension(entry, key).ok()
}
pub(super) fn optional_object_position_prop(node: &KdlNode, key: &str) -> Option<ObjectPosition> {
let entry = node.entry(key)?;
if entry_annotation(entry) == Some("pct") {
let value = match entry.value() {
KdlValue::Integer(n) => *n as f64,
KdlValue::Float(f) => *f,
_ => return None,
};
return Some(ObjectPosition::Pct(value));
}
match entry.value() {
KdlValue::String(s) => match s.as_str() {
"start" => Some(ObjectPosition::Start),
"center" => Some(ObjectPosition::Center),
"end" => Some(ObjectPosition::End),
_ => None,
},
_ => None,
}
}
pub(super) fn optional_property_value(node: &KdlNode, key: &str) -> Option<PropertyValue> {
let entry = node.entry(key)?;
entry_to_property_value(entry).ok()
}
pub(super) fn optional_property_value_aliased(
node: &KdlNode,
primary_key: &str,
alias_key: &str,
) -> Option<PropertyValue> {
optional_property_value(node, primary_key).or_else(|| optional_property_value(node, alias_key))
}
pub(super) fn optional_string_prop_aliased<'a>(
node: &'a KdlNode,
primary_key: &str,
alias_key: &str,
) -> Option<&'a str> {
optional_string_prop(node, primary_key).or_else(|| optional_string_prop(node, alias_key))
}
pub(super) fn required_string_prop_aliased<'a>(
node: &'a KdlNode,
primary_key: &str,
alias_key: &str,
) -> Result<&'a str, ParseError> {
if let Some(v) = optional_string_prop(node, primary_key) {
return Ok(v);
}
required_string_prop(node, alias_key)
}
pub(super) fn optional_token_ref_prop(node: &KdlNode, key: &str) -> Option<String> {
let entry = node.entry(key)?;
match (entry_annotation(entry), entry.value()) {
(Some("token"), KdlValue::String(s)) => Some(s.clone()),
_ => None,
}
}
pub(super) fn kdl_value_to_unknown_value(v: &KdlValue) -> UnknownValue {
match v {
KdlValue::String(s) => UnknownValue::String(s.clone()),
KdlValue::Integer(n) => UnknownValue::Integer(*n),
KdlValue::Float(f) => UnknownValue::Float(*f),
KdlValue::Bool(b) => UnknownValue::Bool(*b),
KdlValue::Null => UnknownValue::Null,
}
}
pub(crate) fn known_props_for_kind(kind: &str) -> &'static [&'static str] {
use super::chart::CHART_KNOWN_PROPS;
use super::container::{
CELL_KNOWN_PROPS, COLUMN_KNOWN_PROPS, FRAME_KNOWN_PROPS, GROUP_KNOWN_PROPS,
INSTANCE_KNOWN_PROPS, ROW_KNOWN_PROPS, TABLE_KNOWN_PROPS,
};
use super::leaf::{
CODE_KNOWN_PROPS, ELLIPSE_KNOWN_PROPS, IMAGE_KNOWN_PROPS, LINE_KNOWN_PROPS,
POLYGON_KNOWN_PROPS, POLYLINE_KNOWN_PROPS, RECT_KNOWN_PROPS, TEXT_KNOWN_PROPS,
};
use super::pattern::PATTERN_KNOWN_PROPS;
use super::special::{
CONNECTOR_KNOWN_PROPS, FIELD_KNOWN_PROPS, FOOTNOTE_KNOWN_PROPS, SHAPE_KNOWN_PROPS,
TOC_KNOWN_PROPS,
};
match kind {
"rect" => RECT_KNOWN_PROPS,
"ellipse" => ELLIPSE_KNOWN_PROPS,
"image" => IMAGE_KNOWN_PROPS,
"text" => TEXT_KNOWN_PROPS,
"code" => CODE_KNOWN_PROPS,
"line" => LINE_KNOWN_PROPS,
"polygon" => POLYGON_KNOWN_PROPS,
"polyline" => POLYLINE_KNOWN_PROPS,
"frame" => FRAME_KNOWN_PROPS,
"group" => GROUP_KNOWN_PROPS,
"table" => TABLE_KNOWN_PROPS,
"cell" => CELL_KNOWN_PROPS,
"row" => ROW_KNOWN_PROPS,
"column" => COLUMN_KNOWN_PROPS,
"shape" => SHAPE_KNOWN_PROPS,
"connector" => CONNECTOR_KNOWN_PROPS,
"field" => FIELD_KNOWN_PROPS,
"toc" => TOC_KNOWN_PROPS,
"footnote" => FOOTNOTE_KNOWN_PROPS,
"pattern" => PATTERN_KNOWN_PROPS,
"chart" => CHART_KNOWN_PROPS,
"instance" => INSTANCE_KNOWN_PROPS,
_ => &[],
}
}
pub(super) fn collect_unknown_props(
node: &KdlNode,
known_keys: &[&str],
) -> BTreeMap<String, UnknownProperty> {
let mut map = BTreeMap::new();
for entry in node.entries() {
if let Some(name_id) = entry.name() {
let key = name_id.value();
if !known_keys.contains(&key) {
map.insert(
key.to_owned(),
UnknownProperty {
value: kdl_value_to_unknown_value(entry.value()),
ty: entry.ty().map(|id| id.value().to_owned()),
},
);
}
}
}
map
}