///|
pub fn BrandBook::audit(self : BrandBook) -> AuditReport {
let findings : Array[Finding] = []
if self.name.trim() == "" {
findings.push(Finding::{
severity: Error,
path: "name",
message: "brand book name is required",
})
}
if self.palette.colors.length() < 3 {
findings.push(Finding::{
severity: Warning,
path: "palette.colors",
message: "a usable brand system normally needs at least three color tokens",
})
}
for i, color in self.palette.colors {
if !is_hex_color(color.value) {
findings.push(Finding::{
severity: Error,
path: "palette.colors[\{i}].value",
message: "expected six-digit hex color, got \{color.value}",
})
}
if color.role.trim() == "" {
findings.push(Finding::{
severity: Warning,
path: "palette.colors[\{i}].role",
message: "color token has no usage role",
})
}
for j in (i + 1)..= 360 {
findings.push(Finding::{
severity: Error,
path: "palette.gradients[" + i.to_string() + "].angle",
message: "gradient angle must be between 0 and 359 degrees",
})
}
for stop_index, stop in gradient.stops {
if !is_hex_color(stop) {
findings.push(Finding::{
severity: Error,
path: "palette.gradients[" +
i.to_string() +
"].stops[" +
stop_index.to_string() +
"]",
message: "gradient stop must be a six-digit hex color",
})
}
}
}
if self.assets.primary_logo.min_width_px < 16 {
findings.push(Finding::{
severity: Error,
path: "assets.primary_logo.min_width_px",
message: "logo minimum width is too small to be legible",
})
}
if self.assets.primary_logo.path.trim().to_owned() == "" {
findings.push(Finding::{
severity: Error,
path: "assets.primary_logo.path",
message: "primary logo asset path is required",
})
}
match self.assets.primary_logo.clear_space {
Pixels(px) if px <= 0 =>
findings.push(Finding::{
severity: Error,
path: "assets.primary_logo.clear_space",
message: "logo clear space must be positive",
})
Multiplier(value) if value <= 0.0 =>
findings.push(Finding::{
severity: Error,
path: "assets.primary_logo.clear_space",
message: "logo clear-space multiplier must be positive",
})
_ => ()
}
for i, step in self.typography.scale {
if step.size_px <= 0 {
findings.push(Finding::{
severity: Error,
path: "typography.scale[" + i.to_string() + "].size_px",
message: "font size must be positive",
})
}
if step.line_height_px < step.size_px {
findings.push(Finding::{
severity: Error,
path: "typography.scale[\{i}]",
message: "line height must be greater than or equal to font size",
})
}
}
for i, family in self.typography.families {
if family.name.trim().to_owned() == "" ||
family.fallback.trim().to_owned() == "" {
findings.push(Finding::{
severity: Error,
path: "typography.families[" + i.to_string() + "]",
message: "font family and fallback are required",
})
}
}
for i, component in self.components {
if component.tokens.length() == 0 {
findings.push(Finding::{
severity: Warning,
path: "components[\{i}].tokens",
message: "component has no token references",
})
}
if component.states.length() == 0 {
findings.push(Finding::{
severity: Warning,
path: "components[\{i}].states",
message: "component has no interaction states",
})
}
for token_index, token_name in component.tokens {
if !has_brand_token(self, token_name) {
findings.push(Finding::{
severity: Error,
path: "components[" +
i.to_string() +
"].tokens[" +
token_index.to_string() +
"]",
message: "component references an unknown token: " + token_name,
})
}
}
}
AuditReport::{ findings, }
}
///|
fn has_brand_token(book : BrandBook, name : String) -> Bool {
if book.palette.colors.any(fn(color) { color.name == name }) {
true
} else {
book.typography.scale.any(fn(step) { step.name == name }) ||
book.typography.families.any(fn(family) { family.name == name })
}
}
///|
fn is_hex_color(text : String) -> Bool {
if text.length() != 7 || text[0] != '#' {
false
} else {
for i in 1..<7 {
let ch = text[i]
if !((ch >= '0' && ch <= '9') ||
(ch >= 'A' && ch <= 'F') ||
(ch >= 'a' && ch <= 'f')) {
return false
}
}
true
}
}