///|
pub struct AccessibilityPolicy {
normal_min_x100 : Int
large_min_x100 : Int
error_below_x100 : Int
require_text_surface_pairs : Bool
} derive(Debug, Eq)
///|
pub fn AccessibilityPolicy::wcag_aa() -> AccessibilityPolicy {
{
normal_min_x100: 450,
large_min_x100: 300,
error_below_x100: 300,
require_text_surface_pairs: true,
}
}
///|
pub fn AccessibilityPolicy::wcag_aaa() -> AccessibilityPolicy {
{
normal_min_x100: 700,
large_min_x100: 450,
error_below_x100: 300,
require_text_surface_pairs: true,
}
}
///|
pub fn AccessibilityPolicy::for_thresholds(
normal_min_x100 : Int,
large_min_x100 : Int,
) -> AccessibilityPolicy {
{
normal_min_x100,
large_min_x100,
error_below_x100: if large_min_x100 < 1 {
1
} else {
large_min_x100
},
require_text_surface_pairs: false,
}
}
///|
pub fn ContrastPair::passes(
self : ContrastPair,
policy : AccessibilityPolicy,
) -> Bool {
self.ratio_x100 >= policy.normal_min_x100
}
///|
pub fn ContrastPair::passes_large_text(
self : ContrastPair,
policy : AccessibilityPolicy,
) -> Bool {
self.ratio_x100 >= policy.large_min_x100
}
///|
pub fn ContrastPair::ratio_text(self : ContrastPair) -> String {
let whole = self.ratio_x100 / 100
let fraction = self.ratio_x100 % 100
let fraction_text = if fraction < 10 {
"0" + fraction.to_string()
} else {
fraction.to_string()
}
whole.to_string() + "." + fraction_text + ":1"
}
///|
pub fn Palette::audit_contrast(
self : Palette,
policy : AccessibilityPolicy,
) -> AuditReport {
let findings : Array[Finding] = []
let text_tokens = self.colors.filter(fn(color) {
color.role.contains("text") ||
color.role.contains("body") ||
color.name == "ink"
})
let surfaces = self.colors.filter(fn(color) {
color.role.contains("surface") ||
color.role.contains("background") ||
color.name == "paper"
})
if policy.require_text_surface_pairs &&
(text_tokens.length() == 0 || surfaces.length() == 0) {
findings.push(Finding::{
severity: Warning,
path: "palette.colors",
message: "accessibility policy could not find a text/surface pair",
})
}
for foreground in text_tokens {
for background in surfaces {
if foreground.name != background.name {
let pair = foreground.contrast_against(background)
if pair.ratio_x100 < policy.error_below_x100 {
findings.push(Finding::{
severity: Error,
path: "palette.contrast." + foreground.name + "." + background.name,
message: pair.ratio_text() + " is below the large-text threshold",
})
} else if pair.ratio_x100 < policy.normal_min_x100 {
findings.push(Finding::{
severity: Warning,
path: "palette.contrast." + foreground.name + "." + background.name,
message: pair.ratio_text() +
" passes large text but not normal text",
})
}
}
}
}
AuditReport::from_findings(findings)
}
///|
pub fn BrandBook::accessibility_score(self : BrandBook) -> Int {
let pairs = self.palette.contrast_pairs()
if pairs.length() == 0 {
return 0
}
let mut passed = 0
for pair in pairs {
if pair.grade != ContrastGrade::Fail {
passed += 1
}
}
passed * 100 / pairs.length()
}
///|
pub fn Palette::contrast_matrix_markdown(self : Palette) -> String {
let lines : Array[String] = [
"## Contrast matrix", "", "| Foreground | Background | Ratio | Grade |", "| --- | --- | ---: | --- |",
]
for pair in self.contrast_pairs() {
if pair.foreground.name != pair.background.name {
lines.push(
"| " +
pair.foreground.name +
" | " +
pair.background.name +
" | " +
pair.ratio_text() +
" | " +
pair.grade.label() +
" |",
)
}
}
lines.join("\n")
}
///|
pub fn ColorToken::is_light(self : ColorToken) -> Bool {
self.relative_luminance_x10000() > 5000
}
///|
pub fn ColorToken::is_dark(self : ColorToken) -> Bool {
!self.is_light()
}
///|
pub fn ColorToken::can_be_text_on(
self : ColorToken,
background : ColorToken,
) -> Bool {
self.contrast_against(background).ratio_x100 >= 450
}
///|
pub fn Palette::recommended_text_color(
self : Palette,
background : ColorToken,
) -> ColorToken? {
let mut best : ColorToken? = None
let mut best_ratio = -1
for color in self.colors {
let ratio = color.contrast_against(background).ratio_x100
if ratio > best_ratio {
best_ratio = ratio
best = Some(color)
}
}
best
}