use std::iter;
use std::str::FromStr;
use strum::VariantNames;
use svugy_model::model::{Position, Shape};
use svugy_model::path::ElementPath;
use super::ast::{Scalar, Span, SvugyProperty};
use super::diagnostic::Diagnostics;
use crate::prose::ProseList;
const SUBTITLE_PATH: [&str; 1] = ["subtitle"];
pub(super) struct CarrierReader<'sink> {
diagnostics: &'sink mut Diagnostics,
}
pub(super) enum ElementAttribute {
Caption(String),
Primitive(Shape),
Height(f64),
BadgeMark { is_marked: bool, span: Span },
Position(Position),
}
impl<'sink> CarrierReader<'sink> {
pub(super) fn new(diagnostics: &'sink mut Diagnostics) -> Self {
Self { diagnostics }
}
pub(super) fn read_root_subtitle(&mut self, properties: Vec<SvugyProperty>) -> Option<String> {
let mut subtitle = None;
for property in properties {
let SvugyProperty {
attribute_path,
value: scalar,
span: property_span,
} = property;
let is_subtitle = attribute_path == SUBTITLE_PATH;
if is_subtitle {
subtitle = Some(scalar.text);
continue;
}
self.reject_root_attribute(&attribute_path, property_span);
}
subtitle
}
fn reject_root_attribute(&mut self, attribute_path: &[String], span: Span) {
let carrier_path = Self::spell_carrier_path(attribute_path);
let message = format!(
"`{carrier_path}` is not a svugy attribute of the root: expected `svugy.subtitle`, and the title is the root's own `label`"
);
self.diagnostics.record_error(message, span);
}
pub(super) fn read_element(
&mut self,
properties: Vec<SvugyProperty>,
span: Span,
) -> Vec<ElementAttribute> {
let mut attributes = Vec::new();
let mut position_x = None;
let mut position_y = None;
for property in properties {
let SvugyProperty {
attribute_path,
value,
span: property_span,
} = property;
let attribute_segments: Vec<&str> = attribute_path.iter().map(String::as_str).collect();
let attribute = match attribute_segments.as_slice() {
["caption"] => {
let caption = ElementAttribute::Caption(value.text);
Some(caption)
}
["shape"] => self.read_primitive(value).map(ElementAttribute::Primitive),
["height"] => self
.parse_decimal_or_reject(value, "`svugy.height`")
.map(ElementAttribute::Height),
["badge"] => {
let mark_span = value.span;
let is_marked = self.read_badge_mark(value);
is_marked.map(|is_marked| ElementAttribute::BadgeMark {
is_marked,
span: mark_span,
})
}
["position", "x"] => {
position_x = Some(value);
None
}
["position", "y"] => {
position_y = Some(value);
None
}
_unknown_attribute => {
self.reject_element_attribute(&attribute_path, property_span);
None
}
};
attributes.extend(attribute);
}
let position = self.read_position(position_x, position_y, span);
let position = position.map(ElementAttribute::Position);
attributes.extend(position);
attributes
}
fn read_primitive(&mut self, primitive: Scalar) -> Option<Shape> {
let lowercase_primitive = primitive.text.to_ascii_lowercase();
let Ok(shape) = Shape::from_str(&lowercase_primitive) else {
let expected = ProseList::join_quoted(Shape::VARIANTS, "or");
let message = format!(
"`{}` is not a svugy primitive: expected {expected}",
primitive.text
);
self.diagnostics.record_error(message, primitive.span);
return None;
};
Some(shape)
}
fn read_badge_mark(&mut self, mark: Scalar) -> Option<bool> {
let lowercase_mark = mark.text.to_ascii_lowercase();
match lowercase_mark.as_str() {
"true" => Some(true),
"false" => Some(false),
_not_a_boolean => {
let message = format!(
"`{}` is not a boolean: `svugy.badge` is `true` or `false`",
mark.text
);
self.diagnostics.record_error(message, mark.span);
None
}
}
}
fn read_position(
&mut self,
x: Option<Scalar>,
y: Option<Scalar>,
span: Span,
) -> Option<Position> {
let is_x_absent = x.is_none();
let is_y_absent = y.is_none();
let is_position_absent = is_x_absent && is_y_absent;
if is_position_absent {
return None;
}
let (Some(x), Some(y)) = (x, y) else {
let message = "`svugy.position` needs both `x` and `y` in the same `vars` map";
self.diagnostics.record_error(message, span);
return None;
};
let coordinate_attribute = "a `svugy.position` coordinate";
let x = self.parse_decimal_or_reject(x, coordinate_attribute)?;
let y = self.parse_decimal_or_reject(y, coordinate_attribute)?;
let position = Position { x, y };
Some(position)
}
fn reject_element_attribute(&mut self, attribute_path: &[String], span: Span) {
let carrier_path = Self::spell_carrier_path(attribute_path);
let message = format!(
"`{carrier_path}` is not a svugy attribute: expected `svugy.caption`, `svugy.shape`, `svugy.height`, `svugy.badge` or `svugy.position` with `x` and `y`"
);
self.diagnostics.record_error(message, span);
}
fn parse_decimal_or_reject(&mut self, number: Scalar, attribute: &str) -> Option<f64> {
let Ok(value) = number.text.parse() else {
let message = format!(
"`{}` is not a number: {attribute} is a plain decimal like `91.5`",
number.text
);
self.diagnostics.record_error(message, number.span);
return None;
};
Some(value)
}
fn spell_carrier_path(attribute_path: &[impl AsRef<str>]) -> String {
let attribute_segments = attribute_path.iter().map(AsRef::as_ref);
let variable_segment = iter::once(SvugyProperty::VARIABLE);
let carrier_segments = variable_segment.chain(attribute_segments);
let carrier_path = ElementPath::from_segments(carrier_segments);
carrier_path.to_string()
}
}