Skip to content

Commit

Permalink
Merge pull request #8 from caoimghgin/insert-isValid
Browse files Browse the repository at this point in the history
Insert isValid function as an extention for colorjs.io
  • Loading branch information
caoimghgin authored Feb 9, 2025
2 parents d14af31 + fdcafc2 commit eef538b
Show file tree
Hide file tree
Showing 6 changed files with 382 additions and 0 deletions.
5 changes: 5 additions & 0 deletions extensions/colorjs.io/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# colorsjs.io/isValid

Lifts portions of code from colorjs.io npm package that was private so we could expose add an isValid function. I have submitted
a [Pull Request](https://github.com/color-js/color.js/pull/633) to [colorjs](https://nextjs.org/) to add this feature. In the meantime,
this extension does the job until it's approved and merged.
91 changes: 91 additions & 0 deletions extensions/colorjs.io/isValid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* eslint-disable curly */
import KEYWORDS from './keywords.js';
import { parseFunction } from './parse.js';

/**
* Verify string can be parsed into a color object
* @param {String} str
* @returns boolean
*/
export default function isValid(str) {
if (!str) return false;
if (isColorHex(str)) return true;
if (isTransparent(str)) return true;
return validateParsedValue(str, parseFunction(str));
}

/**
* Return if parsed is valid and not a rgb function with angles
* @param {String} str
* @param {any} parsed
* @returns boolean
*/
const validateParsedValue = (str, parsed) => {
return !(!isParsedValid(parsed, str) || isParsedRGBWithAngles(parsed));
};

/**
* Return if string is "transparent"
* @param {String} str
* @returns boolean
*/
const isTransparent = (str) => {
return str === 'transparent';
};

/**
* Return if parsed is valid
* @param {any} parsed
* @param {String} str
* @returns boolean
*/
const isParsedValid = (parsed, str) => {
if (
!parsed ||
!parsed.name ||
parsed.argMeta.filter((item) => isTypeNumberPercentageOrAngle(item.type)).length < 3 ||
parsed.argMeta.filter((item) => isTypeNumberPercentageOrAngle(item.type)).length > 4
) {
if (!KEYWORDS[str.toLowerCase()]) return false;
}
return true;
};

/**
* Return if parsed is a rgb function with angles
* @param {any} parsed
* @returns boolean
*/
const isParsedRGBWithAngles = (parsed) => {
if (
parsed &&
parsed.name === 'rgb' &&
parsed.argMeta.filter((item) => item.type === '<angle>').length
) {
return true;
}
return false;
};

/**
* Return if string is a number, percentage or angle
* @param {String} type
* @returns boolean
*/
const isTypeNumberPercentageOrAngle = (type) => {
return type === '<number>' || type === '<percentage>' || type === '<angle>' || !type;
};

/**
* Verify string is valid hex
* @param {String} str
* @returns boolean
*/
function isColorHex(str) {
const isString = (color) => color && typeof color === 'string';
if (isString(str)) {
const regex = /^#([\da-f]{3}){1,2}$|^#([\da-f]{4}){1,2}$/i;
return str && regex.test(str);
}
return false;
}
160 changes: 160 additions & 0 deletions extensions/colorjs.io/keywords.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// To produce: Visit https://www.w3.org/TR/css-color-4/#named-colors
// and run in the console:
// copy($$("tr", $(".named-color-table tbody")).map(tr => `"${tr.cells[2].textContent.trim()}": [${tr.cells[4].textContent.trim().split(/\s+/).map(c => c === "0"? "0" : c === "255"? "1" : c + " / 255").join(", ")}]`).join(",\n"))

/** List of CSS color keywords
* Note that this does not include currentColor, transparent,
* or system colors
*
* @type {Record<string, [number, number, number]>}
*/
export default {
aliceblue: [240 / 255, 248 / 255, 1],
antiquewhite: [250 / 255, 235 / 255, 215 / 255],
aqua: [0, 1, 1],
aquamarine: [127 / 255, 1, 212 / 255],
azure: [240 / 255, 1, 1],
beige: [245 / 255, 245 / 255, 220 / 255],
bisque: [1, 228 / 255, 196 / 255],
black: [0, 0, 0],
blanchedalmond: [1, 235 / 255, 205 / 255],
blue: [0, 0, 1],
blueviolet: [138 / 255, 43 / 255, 226 / 255],
brown: [165 / 255, 42 / 255, 42 / 255],
burlywood: [222 / 255, 184 / 255, 135 / 255],
cadetblue: [95 / 255, 158 / 255, 160 / 255],
chartreuse: [127 / 255, 1, 0],
chocolate: [210 / 255, 105 / 255, 30 / 255],
coral: [1, 127 / 255, 80 / 255],
cornflowerblue: [100 / 255, 149 / 255, 237 / 255],
cornsilk: [1, 248 / 255, 220 / 255],
crimson: [220 / 255, 20 / 255, 60 / 255],
cyan: [0, 1, 1],
darkblue: [0, 0, 139 / 255],
darkcyan: [0, 139 / 255, 139 / 255],
darkgoldenrod: [184 / 255, 134 / 255, 11 / 255],
darkgray: [169 / 255, 169 / 255, 169 / 255],
darkgreen: [0, 100 / 255, 0],
darkgrey: [169 / 255, 169 / 255, 169 / 255],
darkkhaki: [189 / 255, 183 / 255, 107 / 255],
darkmagenta: [139 / 255, 0, 139 / 255],
darkolivegreen: [85 / 255, 107 / 255, 47 / 255],
darkorange: [1, 140 / 255, 0],
darkorchid: [153 / 255, 50 / 255, 204 / 255],
darkred: [139 / 255, 0, 0],
darksalmon: [233 / 255, 150 / 255, 122 / 255],
darkseagreen: [143 / 255, 188 / 255, 143 / 255],
darkslateblue: [72 / 255, 61 / 255, 139 / 255],
darkslategray: [47 / 255, 79 / 255, 79 / 255],
darkslategrey: [47 / 255, 79 / 255, 79 / 255],
darkturquoise: [0, 206 / 255, 209 / 255],
darkviolet: [148 / 255, 0, 211 / 255],
deeppink: [1, 20 / 255, 147 / 255],
deepskyblue: [0, 191 / 255, 1],
dimgray: [105 / 255, 105 / 255, 105 / 255],
dimgrey: [105 / 255, 105 / 255, 105 / 255],
dodgerblue: [30 / 255, 144 / 255, 1],
firebrick: [178 / 255, 34 / 255, 34 / 255],
floralwhite: [1, 250 / 255, 240 / 255],
forestgreen: [34 / 255, 139 / 255, 34 / 255],
fuchsia: [1, 0, 1],
gainsboro: [220 / 255, 220 / 255, 220 / 255],
ghostwhite: [248 / 255, 248 / 255, 1],
gold: [1, 215 / 255, 0],
goldenrod: [218 / 255, 165 / 255, 32 / 255],
gray: [128 / 255, 128 / 255, 128 / 255],
green: [0, 128 / 255, 0],
greenyellow: [173 / 255, 1, 47 / 255],
grey: [128 / 255, 128 / 255, 128 / 255],
honeydew: [240 / 255, 1, 240 / 255],
hotpink: [1, 105 / 255, 180 / 255],
indianred: [205 / 255, 92 / 255, 92 / 255],
indigo: [75 / 255, 0, 130 / 255],
ivory: [1, 1, 240 / 255],
khaki: [240 / 255, 230 / 255, 140 / 255],
lavender: [230 / 255, 230 / 255, 250 / 255],
lavenderblush: [1, 240 / 255, 245 / 255],
lawngreen: [124 / 255, 252 / 255, 0],
lemonchiffon: [1, 250 / 255, 205 / 255],
lightblue: [173 / 255, 216 / 255, 230 / 255],
lightcoral: [240 / 255, 128 / 255, 128 / 255],
lightcyan: [224 / 255, 1, 1],
lightgoldenrodyellow: [250 / 255, 250 / 255, 210 / 255],
lightgray: [211 / 255, 211 / 255, 211 / 255],
lightgreen: [144 / 255, 238 / 255, 144 / 255],
lightgrey: [211 / 255, 211 / 255, 211 / 255],
lightpink: [1, 182 / 255, 193 / 255],
lightsalmon: [1, 160 / 255, 122 / 255],
lightseagreen: [32 / 255, 178 / 255, 170 / 255],
lightskyblue: [135 / 255, 206 / 255, 250 / 255],
lightslategray: [119 / 255, 136 / 255, 153 / 255],
lightslategrey: [119 / 255, 136 / 255, 153 / 255],
lightsteelblue: [176 / 255, 196 / 255, 222 / 255],
lightyellow: [1, 1, 224 / 255],
lime: [0, 1, 0],
limegreen: [50 / 255, 205 / 255, 50 / 255],
linen: [250 / 255, 240 / 255, 230 / 255],
magenta: [1, 0, 1],
maroon: [128 / 255, 0, 0],
mediumaquamarine: [102 / 255, 205 / 255, 170 / 255],
mediumblue: [0, 0, 205 / 255],
mediumorchid: [186 / 255, 85 / 255, 211 / 255],
mediumpurple: [147 / 255, 112 / 255, 219 / 255],
mediumseagreen: [60 / 255, 179 / 255, 113 / 255],
mediumslateblue: [123 / 255, 104 / 255, 238 / 255],
mediumspringgreen: [0, 250 / 255, 154 / 255],
mediumturquoise: [72 / 255, 209 / 255, 204 / 255],
mediumvioletred: [199 / 255, 21 / 255, 133 / 255],
midnightblue: [25 / 255, 25 / 255, 112 / 255],
mintcream: [245 / 255, 1, 250 / 255],
mistyrose: [1, 228 / 255, 225 / 255],
moccasin: [1, 228 / 255, 181 / 255],
navajowhite: [1, 222 / 255, 173 / 255],
navy: [0, 0, 128 / 255],
oldlace: [253 / 255, 245 / 255, 230 / 255],
olive: [128 / 255, 128 / 255, 0],
olivedrab: [107 / 255, 142 / 255, 35 / 255],
orange: [1, 165 / 255, 0],
orangered: [1, 69 / 255, 0],
orchid: [218 / 255, 112 / 255, 214 / 255],
palegoldenrod: [238 / 255, 232 / 255, 170 / 255],
palegreen: [152 / 255, 251 / 255, 152 / 255],
paleturquoise: [175 / 255, 238 / 255, 238 / 255],
palevioletred: [219 / 255, 112 / 255, 147 / 255],
papayawhip: [1, 239 / 255, 213 / 255],
peachpuff: [1, 218 / 255, 185 / 255],
peru: [205 / 255, 133 / 255, 63 / 255],
pink: [1, 192 / 255, 203 / 255],
plum: [221 / 255, 160 / 255, 221 / 255],
powderblue: [176 / 255, 224 / 255, 230 / 255],
purple: [128 / 255, 0, 128 / 255],
rebeccapurple: [102 / 255, 51 / 255, 153 / 255],
red: [1, 0, 0],
rosybrown: [188 / 255, 143 / 255, 143 / 255],
royalblue: [65 / 255, 105 / 255, 225 / 255],
saddlebrown: [139 / 255, 69 / 255, 19 / 255],
salmon: [250 / 255, 128 / 255, 114 / 255],
sandybrown: [244 / 255, 164 / 255, 96 / 255],
seagreen: [46 / 255, 139 / 255, 87 / 255],
seashell: [1, 245 / 255, 238 / 255],
sienna: [160 / 255, 82 / 255, 45 / 255],
silver: [192 / 255, 192 / 255, 192 / 255],
skyblue: [135 / 255, 206 / 255, 235 / 255],
slateblue: [106 / 255, 90 / 255, 205 / 255],
slategray: [112 / 255, 128 / 255, 144 / 255],
slategrey: [112 / 255, 128 / 255, 144 / 255],
snow: [1, 250 / 255, 250 / 255],
springgreen: [0, 1, 127 / 255],
steelblue: [70 / 255, 130 / 255, 180 / 255],
tan: [210 / 255, 180 / 255, 140 / 255],
teal: [0, 128 / 255, 128 / 255],
thistle: [216 / 255, 191 / 255, 216 / 255],
tomato: [1, 99 / 255, 71 / 255],
turquoise: [64 / 255, 224 / 255, 208 / 255],
violet: [238 / 255, 130 / 255, 238 / 255],
wheat: [245 / 255, 222 / 255, 179 / 255],
white: [1, 1, 1],
whitesmoke: [245 / 255, 245 / 255, 245 / 255],
yellow: [1, 1, 0],
yellowgreen: [154 / 255, 205 / 255, 50 / 255],
};
105 changes: 105 additions & 0 deletions extensions/colorjs.io/parse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/* eslint-disable no-multi-assign */
/* eslint-disable object-shorthand */
/* eslint-disable prefer-const */
/* eslint-disable no-param-reassign */
/**
* Parse a CSS function, regardless of its name and arguments
* @param {string} str String to parse
* @return {ParseFunctionReturn | void}
*/
export function parseFunction(str) {
if (!str) {
return;
}

str = str.trim();

let parts = str.match(regex.function);

if (parts) {
// It is a function, parse args
let args = [];
let argMeta = [];
let lastAlpha = false;

let separators = parts[2].replace(regex.singleArgument, ($0, rawArg) => {
let { value, meta } = parseArgument(rawArg);

if ($0.startsWith('/')) {
// It's alpha
lastAlpha = true;
}

args.push(value);
argMeta.push(meta);
return '';
});

return {
name: parts[1].toLowerCase(),
args,
argMeta,
lastAlpha,
commas: separators.includes(','),
rawName: parts[1],
rawArgs: parts[2],
};
}
}

/**
* Parse a single function argument
* @param {string} rawArg
* @returns {{value: number, meta: ArgumentMeta}}
*/
export function parseArgument(rawArg) {
/** @type {Partial<ArgumentMeta>} */
let meta = {};
let unit = rawArg.match(regex.unitValue)?.[0];
/** @type {string | number} */
let value = (meta.raw = rawArg);

if (unit) {
// It’s a dimension token
meta.type = unit === '%' ? '<percentage>' : '<angle>';
meta.unit = unit;
meta.unitless = Number(value.slice(0, -unit.length)); // unitless number

value = meta.unitless * units[unit];
} else if (regex.number.test(value)) {
// It's a number
// Convert numerical args to numbers
value = Number(value);
meta.type = '<number>';
} else if (value === 'none') {
value = null;
} else if (value === 'NaN' || value === 'calc(NaN)') {
value = NaN;
meta.type = '<number>';
} else {
meta.type = '<ident>';
}

return { value: /** @type {number} */ (value), meta: /** @type {ArgumentMeta} */ (meta) };
}

/**
* Units and multiplication factors for the internally stored numbers
*/
export const units = {
'%': 0.01,
deg: 1,
grad: 0.9,
rad: 180 / Math.PI,
turn: 360,
};

export const regex = {
// Need to list calc(NaN) explicitly as otherwise its ending paren would terminate the function call
function: /^([a-z]+)\(((?:calc\(NaN\)|.)+?)\)$/i,
number: /^([-+]?(?:[0-9]*\.)?[0-9]+(e[-+]?[0-9]+)?)$/i,
unitValue: RegExp(`(${Object.keys(units).join('|')})$`),

// NOTE The -+ are not just for prefix, but also for idents, and e+N notation!
singleArgument: /\/?\s*(none|NaN|calc\(NaN\)|[-+\w.]+(?:%|deg|g?rad|turn)?)/g,
};
9 changes: 9 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import isValid from './utilities/validate-color/isValid.js';

function main() {
console.log('Hello, World!');
console.log(isValid('transparent'));
console.log('IS VALID?', isValid('#0055'));
}

main();
12 changes: 12 additions & 0 deletions test-utils/tracer-bullets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* eslint-disable no-console */
// https://wiki.c2.com/?TracerBullets

import isValid from '../extensions/colorjs.io/isValid.js';

function main() {
console.log('Hello, World!');
console.log('IS transparent VALID?', isValid('transparent'));
console.log('IS #0055 VALID?', isValid('#0055'));
}

main();

0 comments on commit eef538b

Please sign in to comment.