diff --git a/extensions/colorjs.io/README.md b/extensions/colorjs.io/README.md new file mode 100644 index 0000000..cbed777 --- /dev/null +++ b/extensions/colorjs.io/README.md @@ -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. \ No newline at end of file diff --git a/extensions/colorjs.io/isValid.js b/extensions/colorjs.io/isValid.js new file mode 100644 index 0000000..15b0bb2 --- /dev/null +++ b/extensions/colorjs.io/isValid.js @@ -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 === '').length + ) { + return true; + } + return false; +}; + +/** + * Return if string is a number, percentage or angle + * @param {String} type + * @returns boolean + */ +const isTypeNumberPercentageOrAngle = (type) => { + return type === '' || type === '' || type === '' || !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; +} diff --git a/extensions/colorjs.io/keywords.js b/extensions/colorjs.io/keywords.js new file mode 100644 index 0000000..21565b5 --- /dev/null +++ b/extensions/colorjs.io/keywords.js @@ -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} + */ +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], +}; diff --git a/extensions/colorjs.io/parse.js b/extensions/colorjs.io/parse.js new file mode 100644 index 0000000..fa9bcdc --- /dev/null +++ b/extensions/colorjs.io/parse.js @@ -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} */ + 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 === '%' ? '' : ''; + 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 = ''; + } else if (value === 'none') { + value = null; + } else if (value === 'NaN' || value === 'calc(NaN)') { + value = NaN; + meta.type = ''; + } else { + meta.type = ''; + } + + 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, +}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..f902a65 --- /dev/null +++ b/index.js @@ -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(); diff --git a/next-env.d.ts b/next-env.d.ts index 52e831b..a4a7b3f 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/test-utils/tracer-bullets.js b/test-utils/tracer-bullets.js new file mode 100644 index 0000000..aaae6e0 --- /dev/null +++ b/test-utils/tracer-bullets.js @@ -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();