use std::collections::HashMap;
use std::sync::OnceLock;
#[derive(Debug)]
pub enum Owner {
Universal,
Root,
Link,
Type(&'static str),
Layout(&'static str),
Role(&'static str),
}
#[derive(Debug)]
pub enum Shape {
One(Kind),
List(Kind),
Pen,
Pattern,
}
#[derive(Debug, PartialEq)]
pub enum Kind {
Number,
Ident,
Str,
Colour,
Paint,
Marker,
Track,
Any,
}
#[derive(Debug)]
pub enum DefaultRef {
None,
Bundles,
Engine,
}
#[derive(Debug, PartialEq)]
pub enum Inherit {
No,
Text,
ScopeLink,
Engine,
}
#[derive(Debug)]
pub enum Gate {
Lenient,
Hard,
}
#[allow(dead_code)]
#[derive(Debug)]
pub struct Property {
pub name: &'static str,
pub owners: &'static [Owner],
pub shape: Shape,
pub default: DefaultRef,
pub inherit: Inherit,
pub text: bool,
pub baked: bool,
pub gate: Gate,
}
const fn row(
name: &'static str,
owners: &'static [Owner],
shape: Shape,
default: DefaultRef,
inherit: Inherit,
) -> Property {
Property {
name,
owners,
shape,
default,
inherit,
text: false,
baked: false,
gate: Gate::Lenient,
}
}
impl Property {
const fn text(mut self) -> Self {
self.text = true;
self
}
const fn baked(mut self) -> Self {
self.baked = true;
self
}
const fn hard(mut self) -> Self {
self.gate = Gate::Hard;
self
}
}
use DefaultRef::{Bundles, Engine};
use Inherit::{No, ScopeLink, Text};
use Owner::{Layout, Link, Role, Root, Type, Universal};
use Shape::{List, One};
const UNIVERSAL: &[Owner] = &[Universal];
pub static PROPERTIES: &[Property] = &[
row("fill", UNIVERSAL, One(Kind::Paint), Bundles, No).text(),
row("opacity", UNIVERSAL, One(Kind::Number), Engine, No).text(),
row("stroke", UNIVERSAL, One(Kind::Paint), Bundles, No),
row("stroke-width", UNIVERSAL, One(Kind::Number), Bundles, No),
row("stroke-style", UNIVERSAL, One(Kind::Ident), Bundles, No),
row("radius", UNIVERSAL, One(Kind::Number), Bundles, No),
row("shadow", UNIVERSAL, One(Kind::Any), DefaultRef::None, No),
row(
"gap-fill",
&[Layout("flow"), Layout("grid"), Layout("sequence")],
One(Kind::Paint),
Bundles,
No,
),
row("font-family", UNIVERSAL, One(Kind::Any), Engine, Text).text(),
row("font-size", UNIVERSAL, One(Kind::Number), Bundles, Text)
.text()
.baked(),
row("font-weight", UNIVERSAL, One(Kind::Any), Bundles, Text).text(),
row("font-style", UNIVERSAL, One(Kind::Ident), Engine, Text).text(),
row("text-transform", UNIVERSAL, One(Kind::Ident), Engine, Text).text(),
row("text-decoration", UNIVERSAL, One(Kind::Ident), Engine, Text).text(),
row(
"text-shadow",
UNIVERSAL,
One(Kind::Any),
DefaultRef::None,
Text,
)
.text(),
row("letter-spacing", UNIVERSAL, One(Kind::Number), Engine, Text)
.text()
.baked(),
row("line-spacing", UNIVERSAL, One(Kind::Number), Engine, Text)
.text()
.baked(),
row("color", UNIVERSAL, One(Kind::Colour), Engine, Text).text(),
row("width", UNIVERSAL, One(Kind::Any), Engine, No),
row("height", UNIVERSAL, One(Kind::Any), Engine, No),
row("padding", UNIVERSAL, One(Kind::Number), Bundles, No),
row(
"max-width",
UNIVERSAL,
One(Kind::Number),
DefaultRef::None,
No,
),
row(
"text-wrap",
UNIVERSAL,
One(Kind::Ident),
DefaultRef::None,
No,
),
row("pin", UNIVERSAL, One(Kind::Ident), Engine, No),
row(
"translate",
UNIVERSAL,
One(Kind::Number),
DefaultRef::None,
No,
)
.text(),
row("rotate", UNIVERSAL, One(Kind::Number), Engine, No).text(),
row("layer", UNIVERSAL, One(Kind::Number), Engine, No).text(),
row(
"scale",
&[Universal, Type("axis")],
One(Kind::Any),
Bundles,
Inherit::Engine,
),
row("pattern", UNIVERSAL, Shape::Pattern, DefaultRef::None, No),
row(
"href",
&[Universal, Link],
One(Kind::Str),
DefaultRef::None,
No,
),
row("hint", UNIVERSAL, One(Kind::Str), DefaultRef::None, No),
row(
"points",
&[Type("line"), Type("poly")],
List(Kind::Number),
DefaultRef::None,
No,
),
row(
"samples",
&[Type("line"), Type("poly"), Type("chart")],
One(Kind::Number),
Engine,
No,
),
row(
"path",
&[Type("path")],
One(Kind::Str),
DefaultRef::None,
No,
),
row(
"src",
&[Type("image")],
One(Kind::Str),
DefaultRef::None,
No,
),
row(
"symbol",
&[Type("icon")],
One(Kind::Ident),
DefaultRef::None,
No,
),
row(
"fit",
&[Type("icon"), Type("image")],
One(Kind::Ident),
Bundles,
No,
),
row("skew", &[Type("slant")], One(Kind::Number), Bundles, No),
row(
"stack",
&[Role("closed")],
One(Kind::Number),
DefaultRef::None,
No,
),
row(
"marker",
&[Type("line"), Type("mark"), Role("series"), Link],
One(Kind::Marker),
Engine,
No,
),
row(
"marker-start",
&[Type("line"), Type("mark"), Role("series"), Link],
One(Kind::Marker),
Engine,
No,
),
row(
"marker-end",
&[Type("line"), Type("mark"), Role("series"), Link],
One(Kind::Marker),
Engine,
No,
),
row("draw", &[Type("sketch")], Shape::Pen, DefaultRef::None, No),
row(
"mirror",
&[Type("sketch")],
One(Kind::Any),
DefaultRef::None,
No,
),
row(
"revolve",
&[Type("sketch")],
One(Kind::Ident),
DefaultRef::None,
No,
),
row(
"thread",
&[Type("sketch"), Type("hole")],
List(Kind::Any),
DefaultRef::None,
No,
),
row("sheet", &[Type("page")], One(Kind::Any), Engine, No),
row(
"break",
&[Type("sketch")],
List(Kind::Any),
DefaultRef::None,
No,
),
row("layout", UNIVERSAL, One(Kind::Ident), Bundles, No),
row(
"direction",
&[Layout("flow"), Layout("chart")],
One(Kind::Ident),
Engine,
No,
),
row(
"gap",
&[
Layout("flow"),
Layout("grid"),
Layout("sequence"),
Layout("chart"),
Layout("pie"),
Role("dimension"),
Role("mate"),
],
One(Kind::Number),
Bundles,
No,
),
row(
"align",
&[Layout("flow"), Layout("grid")],
List(Kind::Ident),
Bundles,
No,
),
row(
"justify",
&[Layout("flow"), Layout("grid")],
List(Kind::Ident),
Bundles,
No,
),
row("columns", &[Layout("grid")], List(Kind::Track), Bundles, No),
row(
"rows",
&[Layout("grid")],
List(Kind::Track),
DefaultRef::None,
No,
),
row(
"cell",
&[Layout("grid")],
One(Kind::Number),
DefaultRef::None,
No,
),
row(
"span",
&[Layout("grid"), Type("band")],
One(Kind::Number),
DefaultRef::None,
No,
),
row(
"data",
&[Role("series")],
List(Kind::Number),
DefaultRef::None,
No,
),
row(
"fn",
&[Role("series")],
List(Kind::Any),
DefaultRef::None,
No,
),
row(
"labels",
&[Role("series")],
List(Kind::Str),
DefaultRef::None,
No,
),
row(
"curve",
&[Type("line"), Type("area")],
One(Kind::Ident),
Engine,
No,
),
row("baseline", &[Type("area")], One(Kind::Number), Engine, No),
row(
"axis",
&[Role("series"), Type("mark"), Type("band")],
One(Kind::Ident),
DefaultRef::None,
No,
),
row("bars", &[Type("chart")], One(Kind::Ident), Engine, No),
row("categories", &[Type("chart")], List(Kind::Str), Engine, No),
row("hole", &[Type("pie")], One(Kind::Number), Engine, No),
row(
"legend",
&[Type("chart"), Type("pie")],
One(Kind::Any),
Engine,
No,
),
row(
"tooltip",
&[Type("chart"), Type("pie"), Role("series")],
One(Kind::Any),
Engine,
No,
),
row(
"value",
&[Type("slice"), Type("bubble")],
One(Kind::Number),
DefaultRef::None,
No,
),
row(
"at",
&[Type("mark"), Type("bubble"), Type("plane")],
One(Kind::Any),
DefaultRef::None,
No,
),
row(
"side",
&[Type("axis"), Role("dimension")],
One(Kind::Ident),
Engine,
No,
),
row(
"range",
&[Type("axis")],
One(Kind::Number),
DefaultRef::None,
No,
),
row(
"step",
&[Type("axis")],
One(Kind::Number),
DefaultRef::None,
No,
),
row(
"ticks",
&[Type("axis")],
List(Kind::Number),
DefaultRef::None,
No,
),
row(
"unit",
&[Type("drawing"), Type("axis")],
One(Kind::Any),
DefaultRef::None,
No,
),
row("gridlines", &[Type("axis")], One(Kind::Any), Engine, No),
row(
"place",
&[Type("note")],
One(Kind::Any),
DefaultRef::None,
No,
)
.hard(),
row(
"activation",
&[Layout("sequence")],
One(Kind::Ident),
Engine,
No,
)
.hard(),
row(
"tol",
&[Role("dimension")],
One(Kind::Any),
DefaultRef::None,
No,
)
.hard(),
row("facing", &[Type("plane")], One(Kind::Ident), Engine, No),
row(
"of",
&[Type("drawing")],
One(Kind::Ident),
DefaultRef::None,
No,
),
row(
"title",
&[Role("title-block")],
One(Kind::Str),
DefaultRef::None,
No,
),
row(
"drawing-number",
&[Role("title-block")],
One(Kind::Str),
DefaultRef::None,
No,
),
row(
"revision",
&[Role("title-block")],
One(Kind::Str),
DefaultRef::None,
No,
),
row(
"sheet-number",
&[Role("title-block")],
One(Kind::Str),
DefaultRef::None,
No,
),
row(
"date",
&[Role("title-block")],
One(Kind::Str),
DefaultRef::None,
No,
),
row(
"author",
&[Role("title-block")],
One(Kind::Str),
DefaultRef::None,
No,
),
row(
"approved",
&[Role("title-block")],
One(Kind::Str),
DefaultRef::None,
No,
),
row(
"department",
&[Role("title-block")],
One(Kind::Str),
DefaultRef::None,
No,
),
row(
"reference",
&[Role("title-block")],
One(Kind::Str),
DefaultRef::None,
No,
),
row(
"document-type",
&[Role("title-block")],
One(Kind::Str),
DefaultRef::None,
No,
),
row(
"status",
&[Role("title-block")],
One(Kind::Str),
DefaultRef::None,
No,
),
row("density", &[Root], One(Kind::Number), Engine, No),
row(
"clearance",
&[Link, Root],
One(Kind::Number),
Bundles,
ScopeLink,
),
row(
"routing",
&[Link, Root],
One(Kind::Ident),
Engine,
ScopeLink,
),
row("along", &[Link], List(Kind::Number), Engine, No),
];
pub const BUILDER_CALLS: &[&str] = &[
"oklch",
"gradient",
"linear-gradient",
"radial-gradient",
"rgb",
"rgba",
"hsl",
"hsla",
"repeat",
"hatch",
];
fn index() -> &'static HashMap<&'static str, &'static Property> {
static INDEX: OnceLock<HashMap<&'static str, &'static Property>> = OnceLock::new();
INDEX.get_or_init(|| PROPERTIES.iter().map(|p| (p.name, p)).collect())
}
pub fn get(name: &str) -> Option<&'static Property> {
index().get(name).copied()
}
pub fn is_builder_call(name: &str) -> bool {
BUILDER_CALLS.contains(&name)
}
pub fn is_string_valued(name: &str) -> bool {
get(name).is_some_and(|p| matches!(p.shape, One(Kind::Str) | List(Kind::Str)))
}
pub fn is_marker(name: &str) -> bool {
get(name).is_some_and(|p| matches!(p.shape, One(Kind::Marker)))
}
pub fn is_text_valid(name: &str) -> bool {
get(name).is_some_and(|p| p.text)
}
pub fn is_baked_text(name: &str) -> bool {
get(name).is_some_and(|p| p.baked)
}
pub fn inherited_text() -> impl Iterator<Item = &'static str> {
PROPERTIES
.iter()
.filter(|p| p.inherit == Inherit::Text)
.map(|p| p.name)
}
pub fn scope_link_props() -> impl Iterator<Item = &'static str> {
PROPERTIES
.iter()
.filter(|p| p.inherit == Inherit::ScopeLink)
.map(|p| p.name)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resolve::NodeKind;
#[test]
fn every_name_is_unique() {
let mut seen = std::collections::HashSet::new();
for p in PROPERTIES {
assert!(seen.insert(p.name), "duplicate ledger row: {}", p.name);
}
}
#[test]
fn every_bundled_default_has_a_row() {
use super::super::defaults::*;
let mut decls = Vec::new();
for kind in [
NodeKind::Block,
NodeKind::Oval,
NodeKind::Hex,
NodeKind::Cyl,
NodeKind::Diamond,
NodeKind::Slant,
NodeKind::Poly,
NodeKind::Path,
NodeKind::Sketch,
NodeKind::Line,
NodeKind::Icon,
NodeKind::Text,
NodeKind::Image,
] {
decls.extend(primitive_bundle(kind));
}
for (name, _) in crate::desugar::types::TEMPLATES {
decls.extend(template_bundle(name));
}
decls.extend(root_defaults());
decls.extend(link_defaults());
for layout in [Some("sequence"), Some("drawing"), None] {
decls.extend(root_layout_defaults(layout));
}
for d in decls {
assert!(
get(&d.name).is_some(),
"bundled default '{}' has no ledger row",
d.name
);
}
}
#[test]
fn classifier_sets_match_the_legacy_lists() {
assert_eq!(
inherited_text().collect::<Vec<_>>(),
[
"font-family",
"font-size",
"font-weight",
"font-style",
"text-transform",
"text-decoration",
"text-shadow",
"letter-spacing",
"line-spacing",
"color",
]
);
assert_eq!(
PROPERTIES
.iter()
.filter(|p| p.baked)
.map(|p| p.name)
.collect::<Vec<_>>(),
["font-size", "letter-spacing", "line-spacing"]
);
let legacy_text = [
"color",
"fill",
"opacity",
"font-family",
"font-size",
"font-weight",
"font-style",
"text-transform",
"text-decoration",
"text-shadow",
"letter-spacing",
"line-spacing",
"translate",
"rotate",
"layer",
];
for name in legacy_text {
assert!(is_text_valid(name), "'{name}' lost text validity");
}
assert_eq!(
PROPERTIES.iter().filter(|p| p.text).count(),
legacy_text.len(),
"a property gained text validity the legacy classifier did not have"
);
for name in ["marker", "marker-start", "marker-end"] {
assert!(is_marker(name));
}
assert!(!is_marker("stroke"));
assert_eq!(
scope_link_props().collect::<Vec<_>>(),
["clearance", "routing"]
);
for name in [
"title",
"hint",
"href",
"src",
"path",
"categories",
"labels",
] {
assert!(is_string_valued(name), "'{name}' lost string-valuedness");
}
assert!(!is_string_valued("symbol"));
assert!(!is_string_valued("font-family"));
for name in ["oklch", "rgb", "repeat", "hatch", "linear-gradient"] {
assert!(is_builder_call(name));
}
assert!(!is_builder_call("min"));
assert!(matches!(get("draw").map(|p| &p.shape), Some(Shape::Pen)));
assert!(matches!(
get("pattern").map(|p| &p.shape),
Some(Shape::Pattern)
));
}
#[test]
fn defaultless_names_are_covered() {
for name in [
"points", "symbol", "data", "cell", "of", "at", "tol", "draw",
] {
let p = get(name).unwrap_or_else(|| panic!("'{name}' has no ledger row"));
assert!(
matches!(p.default, DefaultRef::None),
"'{name}' should be defaultless"
);
}
}
#[test]
fn labels_is_the_series_per_datum_text() {
let labels = get("labels").unwrap();
assert!(matches!(labels.shape, List(Kind::Str)));
assert!(
labels
.owners
.iter()
.any(|o| matches!(o, Owner::Role("series")))
);
assert!(get("tags").is_none(), "'tags' was renamed to 'labels'");
assert!(get("over").is_none(), "'over' was replaced by 'place'");
}
}