use crate::error::Error;
use crate::resolve::NodeKind;
use crate::span::Span;
use crate::syntax::ast::{Define, File, StyleItem};
use std::collections::HashMap;
const MAX_INHERITANCE_DEPTH: usize = 16;
pub const TEMPLATES: &[(&str, &str)] = &[
("box", "block"),
("rect", "box"),
("group", "block"),
("caption", "block"),
("footnote", "caption"),
("badge", "block"),
("row", "block"),
("column", "block"),
("grid", "block"),
("table", "group"),
("cell", "block"),
("header", "cell"),
("footer", "cell"),
("entity", "table"),
("sign", "icon"),
("chart", "block"),
("pie", "block"),
("area", "block"),
("bars", "block"),
("dots", "block"),
("bubble", "block"),
("slice", "block"),
("axis", "block"),
("band", "block"),
("mark", "block"),
("sequence", "block"),
("note", "block"),
("loop", "block"),
("opt", "block"),
("alt", "block"),
("else", "block"),
];
pub fn is_template(name: &str) -> bool {
TEMPLATES.iter().any(|(n, _)| *n == name)
}
pub fn template_base(name: &str) -> Option<&'static str> {
TEMPLATES.iter().find(|(n, _)| *n == name).map(|(_, b)| *b)
}
fn is_builtin_type(name: &str) -> bool {
NodeKind::parse(name).is_some()
|| is_template(name)
|| matches!(
name,
"link" | "node" | "text" | "marker" | "canvas" | "scene" | "cut"
)
}
pub struct TypeInfo {
pub kind: NodeKind,
pub chain: Vec<String>,
}
pub struct Types {
defines: HashMap<String, String>,
}
impl Types {
pub fn build(file: &File) -> Result<Self, Error> {
let mut defines = HashMap::new();
for d in file.stylesheet.iter().filter_map(as_define) {
if is_builtin_type(&d.name) {
return Err(Error::at(
d.span,
format!("'{}' shadows a built-in type", d.name),
));
}
if defines.insert(d.name.clone(), d.base.clone()).is_some() {
return Err(Error::at(d.span, format!("duplicate type '{}'", d.name)));
}
}
let types = Self { defines };
for d in file.stylesheet.iter().filter_map(as_define) {
types.walk(&d.name, d.span, &mut Vec::new(), 0)?;
}
Ok(types)
}
pub fn is_known(&self, name: &str) -> bool {
NodeKind::parse(name).is_some() || is_template(name) || self.defines.contains_key(name)
}
pub fn resolve(&self, name: &str, span: Span) -> Result<TypeInfo, Error> {
self.walk(name, span, &mut Vec::new(), 0)
}
fn walk(
&self,
name: &str,
span: Span,
visiting: &mut Vec<String>,
depth: usize,
) -> Result<TypeInfo, Error> {
if depth > MAX_INHERITANCE_DEPTH {
return Err(Error::at(
span,
format!("'{}' exceeds max inheritance depth (16)", name),
));
}
if visiting.iter().any(|n| n == name) {
return Err(Error::at(
span,
format!("cycle in '{} -> {}'", visiting.join(" -> "), name),
));
}
if let Some(kind) = NodeKind::parse(name) {
return Ok(TypeInfo {
kind,
chain: Vec::new(),
});
}
let base = template_base(name)
.map(str::to_string)
.or_else(|| self.defines.get(name).cloned())
.ok_or_else(|| Error::at(span, format!("unknown type '{}'", name)))?;
visiting.push(name.to_string());
let mut info = self.walk(&base, span, visiting, depth + 1)?;
visiting.pop();
info.chain.push(name.to_string());
Ok(info)
}
}
fn as_define(it: &StyleItem) -> Option<&Define> {
match it {
StyleItem::Define(d) => Some(d),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(src: &str) -> File {
crate::syntax::parser::parse(&crate::lexer::lex(src).expect("lex")).expect("parse")
}
fn chain(src: &str, name: &str) -> Vec<String> {
let file = parse(src);
let t = Types::build(&file).expect("build");
t.resolve(name, Span::empty()).expect("resolve").chain
}
fn build_err(src: &str) -> String {
let file = parse(src);
Types::build(&file)
.err()
.map(|e| e.message)
.unwrap_or_default()
}
#[test]
fn primitive_has_empty_chain() {
assert!(chain("", "block").is_empty());
assert_eq!(chain("", "box"), vec!["box"]);
}
#[test]
fn table_chain_is_group_then_table() {
assert_eq!(chain("", "table"), vec!["group", "table"]);
}
#[test]
fn user_define_appends_after_its_base_chain() {
assert_eq!(
chain("{ |panel::group| { } }\n", "panel"),
vec!["group", "panel"]
);
}
#[test]
fn cycle_and_shadow_error() {
assert!(build_err("{ |a::b| { }\n|b::a| { } }\n").contains("cycle"));
assert!(build_err("{ |rect::oval| { } }\n").contains("shadows a built-in"));
}
}