///|
pub struct ThemePolicy {
require_descriptions : Bool
require_unique_paths : Bool
allowed_prefix : String
min_color_tokens : Int
reject_empty_values : Bool
} derive(Debug, Eq)
///|
pub fn ThemePolicy::default() -> ThemePolicy {
{
require_descriptions: false,
require_unique_paths: true,
allowed_prefix: "",
min_color_tokens: 1,
reject_empty_values: true,
}
}
///|
pub fn ThemePolicy::strict() -> ThemePolicy {
{
require_descriptions: true,
require_unique_paths: true,
allowed_prefix: "",
min_color_tokens: 1,
reject_empty_values: true,
}
}
///|
pub fn ThemePolicy::with_prefix(
self : ThemePolicy,
prefix : String,
) -> ThemePolicy {
{ ..self, allowed_prefix: TokenPath::from_string(prefix).canonical() }
}
///|
pub fn ThemeToken::is_empty(self : ThemeToken) -> Bool {
match self.value {
ColorValue(value) => value.trim().to_owned() == ""
NumberValue(_) => false
TextValue(value) => value.trim().to_owned() == ""
BooleanValue(_) => false
}
}
///|
pub fn ThemeToken::is_valid(self : ThemeToken) -> Bool {
match self.value {
ColorValue(value) => is_hex_color(value)
NumberValue(_) => true
TextValue(_) => true
BooleanValue(_) => true
}
}
///|
pub fn ThemeDocument::audit_theme(
self : ThemeDocument,
policy : ThemePolicy,
) -> AuditReport {
let findings : Array[Finding] = []
let seen : Array[String] = []
let mut colors = 0
for token in self.tokens {
let path = token.path.canonical()
if policy.require_unique_paths && seen.any(fn(item) { item == path }) {
findings.push(Finding::{
severity: Error,
path,
message: "duplicate theme path",
})
}
seen.push(path)
if policy.allowed_prefix != "" &&
!token.path.canonical().has_prefix(policy.allowed_prefix) {
findings.push(Finding::{
severity: Error,
path: token.path.canonical(),
message: "token is outside the allowed prefix",
})
}
if policy.require_descriptions && token.description.trim().to_owned() == "" {
findings.push(Finding::{
severity: Warning,
path: token.path.canonical(),
message: "token description is required by policy",
})
}
if policy.reject_empty_values && token.is_empty() {
findings.push(Finding::{
severity: Error,
path: token.path.canonical(),
message: "token value must not be empty",
})
}
if !token.is_valid() {
findings.push(Finding::{
severity: Error,
path: token.path.canonical(),
message: "token value does not match its declared kind",
})
}
if token.kind == Color {
colors += 1
}
}
if colors < policy.min_color_tokens {
findings.push(Finding::{
severity: Warning,
path: "tokens",
message: "theme has fewer color tokens than policy requires",
})
}
for link in self.aliases {
if self.find(link.target.canonical()) is None {
findings.push(Finding::{
severity: Error,
path: link.name.canonical(),
message: "alias target is missing: " + link.target.canonical(),
})
}
}
AuditReport::from_findings(findings)
}
///|
pub fn ThemeDocument::color_tokens(self : ThemeDocument) -> Array[ThemeToken] {
let result : Array[ThemeToken] = []
for token in self.tokens {
if token.kind == Color {
result.push(token)
}
}
result
}
///|
pub fn ThemeDocument::number_tokens(self : ThemeDocument) -> Array[ThemeToken] {
let result : Array[ThemeToken] = []
for token in self.tokens {
if token.kind == Number {
result.push(token)
}
}
result
}
///|
pub fn ThemeDocument::paths(self : ThemeDocument) -> Array[String] {
let result : Array[String] = []
for token in self.tokens {
result.push(token.path.canonical())
}
result
}
///|
pub fn ThemeDocument::description_rate(self : ThemeDocument) -> Int {
if self.tokens.length() == 0 {
0
} else {
let mut documented = 0
for token in self.tokens {
if token.description.trim().to_owned() != "" {
documented += 1
}
}
documented * 100 / self.tokens.length()
}
}