Skip to content

Instantly share code, notes, and snippets.

@kphrx
Last active December 29, 2023 20:01
Show Gist options
  • Save kphrx/04f0600394b406e4ad46c9046d068025 to your computer and use it in GitHub Desktop.
Save kphrx/04f0600394b406e4ad46c9046d068025 to your computer and use it in GitHub Desktop.
shields badge modify a11y color contrast ratio https://a11y-shields-badge.kphrx.workers.dev/
const RED = 2126;
const GREEN = 7152;
const BLUE = 722;
/**
* @typedef {Object} hueValue
* @property {'hue'} type
* @property {number} value
*/
/**
* @typedef {Object} rgbValue
* @property {'rgb'} type
* @property {[number, number, number]} value
*/
/**
* @param {hueValue|rgbValue} value
*/
const relativeLumination = ({type, value}) => {
switch (type) {
case 'hue': return Math.floor(relativeLuminationForHue(value) / 6000);
case 'rgb': return Math.floor(relativeLuminationForRGB(value) / 25500);
}
};
/**
* @param {number} hue
*/
const relativeLuminationForHue = (hue) => {
while (hue >= 360) {
hue - 360;
}
const hue60 = hue % 60;
switch ((hue - hue60) / 60) {
case 0: // up green
return 60 * RED + hue60 * GREEN + 0 * BLUE;
case 1: // down red
return (60 - hue60) * RED + 60 * GREEN + 0 * BLUE;
case 2: // up blue
return 0 * RED + 60 * GREEN + hue60 * BLUE;
case 3: // down green
return 0 * RED + (60 - hue60) * GREEN + 60 * BLUE;
case 4: // up red
return hue60 * RED + 0 * GREEN + 60 * BLUE;
case 5: // down blue
return 60 * RED + 0 * GREEN + (60 - hue60) * BLUE;
}
};
/**
* @param {[number, number, number]} rgb
*/
const relativeLuminationForRGB = ([red, green, blue]) => {
return red * RED + green * GREEN + blue * BLUE;
};
/**
* @param {number} relLum
* @returns {[number | undefined, boolean]}
*/
const wcagRatioLightness = (relLum, def = 50) => {
const lig = Math.floor(relLum * def * 2 / 100);
if (18 <= lig && lig <= 25) { // 4.5:1
return [18 / lig * def, false];
}
if (lig <= 18) {
return [def, false];
}
if (30 <= lig && lig <= 42) { // 3:1
return [30 / lig * def, true];
}
if (lig <= 30) {
return [def, true];
}
return [, false];
};
/**
* @param {string} hueValue
* @param {string} satValue
* @param {string} ligValue
* @returns {[string, boolean, boolean]}
*/
const hslFill = (hueValue, satValue, ligValue) => {
const defLig = Number(ligValue);
const relLum = relativeLumination({type: 'hue', value: Number(hueValue)});
let [lig, isBold] = wcagRatioLightness(relLum, defLig);
let isBlack = false;
if (lig == null) {
isBlack = true;
lig = defLig;
}
return [`hsl(${hueValue} ${satValue}% ${lig}%)`, isBold, isBlack];
};
/**
* @param {number} relLum
* @returns {[boolean, boolean]}
*/
const wcagRatio = (relLum) => {
if (relLum <= 18) { // 4.5:1
return [false, false];
}
if (relLum <= 30) { // 3:1
return [true, false];
}
return [false, true];
};
/**
* @param {string} rValue
* @param {string} gValue
* @param {string} bValue
* @returns {[boolean, boolean]}
*/
const rgbFill = (rValue, gValue, bValue) => {
const relLum = relativeLumination({type: 'rgb', value: [parseInt(rValue, 16), parseInt(gValue, 16), parseInt(bValue, 16)]});
let [isBold, isBlack] = wcagRatio(relLum);
return [isBold, isBlack];
};
export default {
/**
* @typedef {Object} ExecutionContext
* @property {(promise: Promise) => void} waitUntil
* @property {() => void} passThroughOnException
*/
/**
* @param {Request} request
* @param {{[key: string]: string}} env
* @param {ExecutionContext | undefined} ctx
*/
async fetch(request, env, ctx) {
let reqUrl = new URL(request.url);
reqUrl.host = 'img.shields.io';
let res = await fetch(reqUrl);
if (res.redirected) {
return Response.redirect(res.url, 302);
}
let boldText = false;
let blackText = false;
let nth = 0;
return new HTMLRewriter()
.on('rect:nth-of-type(2)', {
element(element) {
const fill = element.getAttribute('fill');
const hslMatched = fill.match(/hsl\(([0-9]+),? ([0-9]+)%,? ([0-9]+)%\)/);
if (hslMatched != null) {
let [color, hueValue, satValue, ligValue] = hslMatched;
[color, boldText, blackText] = hslFill(hueValue, satValue, ligValue);
element.setAttribute('fill', color);
return;
}
const rgbMatched = fill.match(/#(([0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F])|([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F]))/);
if (rgbMatched != null) {
const [_hex, _value, rValue, gValue, bValue] = rgbMatched;
[boldText, blackText] = rgbFill(rValue, gValue, bValue);
return;
}
},
})
.on('text:not([aria-hidden="true"])', {
element(element) {
nth++;
if (nth < 2) {
return;
}
if (boldText) {
element.setAttribute('font-weight', 'bold');
}
if (blackText) {
element.setAttribute('fill', '#000');
}
},
})
.transform(res);
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment