From 9d31e9b38dc3659aa27ea3e19e47d9e8cb763d9f Mon Sep 17 00:00:00 2001 From: LitoMore Date: Thu, 13 Feb 2025 07:54:57 +0800 Subject: [PATCH 1/4] move to TypeScript --- .gitignore | 5 +- README.md | 101 ++++++++----- index.d.ts | 23 --- index.js | 230 ---------------------------- index.test-d.ts | 44 ------ package.json | 21 ++- source/index.ts | 249 +++++++++++++++++++++++++++++++ test.js | 389 ++++++++++++++++++++++++------------------------ tsconfig.json | 7 + 9 files changed, 534 insertions(+), 535 deletions(-) delete mode 100644 index.d.ts delete mode 100644 index.js delete mode 100644 index.test-d.ts create mode 100644 source/index.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index c2658d7..96895a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -node_modules/ +node_modules +distribution +coverage +.vscode diff --git a/README.md b/README.md index 9651e4e..7e21d27 100644 --- a/README.md +++ b/README.md @@ -13,45 +13,78 @@ npm install color-string ### Parsing ```js -colorString.get('#FFF') // {model: 'rgb', value: [255, 255, 255, 1]} -colorString.get('#FFFA') // {model: 'rgb', value: [255, 255, 255, 0.67]} -colorString.get('#FFFFFFAA') // {model: 'rgb', value: [255, 255, 255, 0.67]} -colorString.get('hsl(360, 100%, 50%)') // {model: 'hsl', value: [0, 100, 50, 1]} -colorString.get('hsl(360 100% 50%)') // {model: 'hsl', value: [0, 100, 50, 1]} -colorString.get('hwb(60, 3%, 60%)') // {model: 'hwb', value: [60, 3, 60, 1]} - -colorString.get.rgb('#FFF') // [255, 255, 255, 1] -colorString.get.rgb('blue') // [0, 0, 255, 1] -colorString.get.rgb('rgba(200, 60, 60, 0.3)') // [200, 60, 60, 0.3] -colorString.get.rgb('rgba(200 60 60 / 0.3)') // [200, 60, 60, 0.3] -colorString.get.rgb('rgba(200 60 60 / 30%)') // [200, 60, 60, 0.3] -colorString.get.rgb('rgb(200, 200, 200)') // [200, 200, 200, 1] -colorString.get.rgb('rgb(200 200 200)') // [200, 200, 200, 1] - -colorString.get.hsl('hsl(360, 100%, 50%)') // [0, 100, 50, 1] -colorString.get.hsl('hsl(360 100% 50%)') // [0, 100, 50, 1] -colorString.get.hsl('hsla(360, 60%, 50%, 0.4)') // [0, 60, 50, 0.4] -colorString.get.hsl('hsl(360 60% 50% / 0.4)') // [0, 60, 50, 0.4] - -colorString.get.hwb('hwb(60, 3%, 60%)') // [60, 3, 60, 1] -colorString.get.hwb('hwb(60, 3%, 60%, 0.6)') // [60, 3, 60, 0.6] - -colorString.get.rgb('invalid color string') // null +import {getColor, getRgb, getHsl, getHwb} from 'color-string'; + +getColor('#FFF') // {model: 'rgb', value: [255, 255, 255, 1]} +getColor('#FFFA') // {model: 'rgb', value: [255, 255, 255, 0.67]} +getColor('#FFFFFFAA') // {model: 'rgb', value: [255, 255, 255, 0.67]} +getColor('rgb(244 233 100)') // {model: 'rgb' value: [244, 233, 100, 1]}); +getColor('rgb(100%, 30%, 90%)') // {model: 'rgb', value: [255, 77, 229, 1]}); +getColor('rgb(100% 30% 90%)') // {model: 'rgb', value: [255, 77, 229, 1]}); +getColor('hsl(360, 100%, 50%)') // {model: 'hsl', value: [0, 100, 50, 1]} +getColor('hsl(360 100% 50%)') // {model: 'hsl', value: [0, 100, 50, 1]} +getColor('hwb(60, 3%, 60%)') // {model: 'hwb', value: [60, 3, 60, 1]} + +getRgb('#FFF') // [255, 255, 255, 1] +getRgb('blue') // [0, 0, 255, 1] +getRgb('rgba(200, 60, 60, 0.3)') // [200, 60, 60, 0.3] +getRgb('rgba(200 60 60 / 0.3)') // [200, 60, 60, 0.3] +getRgb('rgba(200 60 60 / 30%)') // [200, 60, 60, 0.3] +getRgb('rgb(200, 200, 200)') // [200, 200, 200, 1] +getRgb('rgb(200 200 200)') // [200, 200, 200, 1] + +getHel('hsl(360, 100%, 50%)') // [0, 100, 50, 1] +getHel('hsl(360 100% 50%)') // [0, 100, 50, 1] +getHel('hsla(360, 60%, 50%, 0.4)') // [0, 60, 50, 0.4] +getHel('hsl(360 60% 50% / 0.4)') // [0, 60, 50, 0.4] + +getHwb('hwb(60, 3%, 60%)') // [60, 3, 60, 1] +getHwb('hwb(60, 3%, 60%, 0.6)') // [60, 3, 60, 0.6] + +getRgb('invalid color string') // undefined ``` ### Generation ```js -colorString.to.hex(255, 255, 255) // "#FFFFFF" -colorString.to.hex(0, 0, 255, 0.4) // "#0000FF66" -colorString.to.hex(0, 0, 255, 0.4) // "#0000FF66" -colorString.to.rgb(255, 255, 255) // "rgb(255, 255, 255)" -colorString.to.rgb(0, 0, 255, 0.4) // "rgba(0, 0, 255, 0.4)" -colorString.to.rgb(0, 0, 255, 0.4) // "rgba(0, 0, 255, 0.4)" -colorString.to.rgb.percent(0, 0, 255) // "rgb(0%, 0%, 100%)" -colorString.to.keyword(255, 255, 0) // "yellow" -colorString.to.hsl(360, 100, 100) // "hsl(360, 100%, 100%)" -colorString.to.hwb(50, 3, 15) // "hwb(50, 3%, 15%)" +import {toHex, toRgb, toRgbPercent, toKeyword, toHsl, toHwb} from 'color-string'; + +toHex(255, 255, 255) // "#FFFFFF" +toHex(0, 0, 255, 0.4) // "#0000FF66" +toHex(0, 0, 255, 0.4) // "#0000FF66" +toRgb(255, 255, 255) // "rgb(255, 255, 255)" +toRgb(0, 0, 255, 0.4) // "rgba(0, 0, 255, 0.4)" +toRgb(0, 0, 255, 0.4) // "rgba(0, 0, 255, 0.4)" +toRgbPercent(0, 0, 255) // "rgb(0%, 0%, 100%)" +toKeyword(255, 255, 0) // "yellow" +toHsl(360, 100, 100) // "hsl(360, 100%, 100%)" +toHwb(50, 3, 15) // "hwb(50, 3%, 15%)" +``` + +### Utilities + +```js +import {alphaClamp, rgbClamp, hueClamp, percentClamp} from 'color-string'; + +alphaClamp(-1) // 0 +alphaClamp(0.5) // 0.5 +alphaClamp(1) // 1 +alphaClamp(2) // 1 + +rgbClamp(-255) // 0 +rgbClamp(128) // 128 +rgbClamp(255) // 255 +rgbClamp(256) // 255 + +hueClamp(-40) // 320 +hueClamp(40) // 40 +hueClamp(360) // 0 +hueClamp(400) // 40 + +percentClamp(-1) // 0 +percentClamp(10) // 10 +percentClamp(100) // 100 +percentClamp(101) // 100 ``` ## License diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index d7490a8..0000000 --- a/index.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -export type Model = 'rgb' | 'hsl' | 'hwb'; - -export type ColorString = { - get: { - (color: string): {model: Model; value: number[]} | undefined; - rgb: (color: string) => number[] | undefined; - hsl: (color: string) => number[] | undefined; - hwb: (color: string) => number[] | undefined; - }; - to: { - hex: (r: number, g: number, b: number, a?: number) => string | undefined; - rgb: { - (r: number, g: number, b: number, a?: number): string | undefined; - percent: (r: number, g: number, b: number, a?: number) => string | undefined; - }; - keyword: (r: number, g: number, b: number, a?: number) => string | undefined; - hsl: (h: number, s: number, l: number, a?: number) => string | undefined; - hwb: (h: number, w: number, b: number, a?: number) => string | undefined; - }; -}; - -declare const colorString: ColorString; -export default colorString; diff --git a/index.js b/index.js deleted file mode 100644 index f175c23..0000000 --- a/index.js +++ /dev/null @@ -1,230 +0,0 @@ -import colorNames from 'color-name'; - -const reverseNames = Object.create(null); - -// Create a list of reverse color names -for (const name in colorNames) { - if (Object.hasOwn(colorNames, name)) { - reverseNames[colorNames[name]] = name; - } -} - -const cs = { - to: {}, - get: {}, -}; - -cs.get = function (string) { - const prefix = string.slice(0, 3).toLowerCase(); - let value; - let model; - switch (prefix) { - case 'hsl': { - value = cs.get.hsl(string); - model = 'hsl'; - break; - } - - case 'hwb': { - value = cs.get.hwb(string); - model = 'hwb'; - break; - } - - default: { - value = cs.get.rgb(string); - model = 'rgb'; - break; - } - } - - if (!value) { - return null; - } - - return {model, value}; -}; - -cs.get.rgb = function (string) { - if (!string) { - return null; - } - - const abbr = /^#([a-f\d]{3,4})$/i; - const hex = /^#([a-f\d]{6})([a-f\d]{2})?$/i; - const rgba = /^rgba?\(\s*([+-]?\d+)(?=[\s,])\s*(?:,\s*)?([+-]?\d+)(?=[\s,])\s*(?:,\s*)?([+-]?\d+)\s*(?:[,|/]\s*([+-]?[\d.]+)(%?)\s*)?\)$/; - const per = /^rgba?\(\s*([+-]?[\d.]+)%\s*,?\s*([+-]?[\d.]+)%\s*,?\s*([+-]?[\d.]+)%\s*(?:[,|/]\s*([+-]?[\d.]+)(%?)\s*)?\)$/; - const keyword = /^(\w+)$/; - - let rgb = [0, 0, 0, 1]; - let match; - let i; - let hexAlpha; - - if (match = string.match(hex)) { - hexAlpha = match[2]; - match = match[1]; - - for (i = 0; i < 3; i++) { - // https://jsperf.com/slice-vs-substr-vs-substring-methods-long-string/19 - const i2 = i * 2; - rgb[i] = Number.parseInt(match.slice(i2, i2 + 2), 16); - } - - if (hexAlpha) { - rgb[3] = Number.parseInt(hexAlpha, 16) / 255; - } - } else if (match = string.match(abbr)) { - match = match[1]; - hexAlpha = match[3]; - - for (i = 0; i < 3; i++) { - rgb[i] = Number.parseInt(match[i] + match[i], 16); - } - - if (hexAlpha) { - rgb[3] = Number.parseInt(hexAlpha + hexAlpha, 16) / 255; - } - } else if (match = string.match(rgba)) { - for (i = 0; i < 3; i++) { - rgb[i] = Number.parseInt(match[i + 1], 10); - } - - if (match[4]) { - rgb[3] = match[5] ? Number.parseFloat(match[4]) * 0.01 : Number.parseFloat(match[4]); - } - } else if (match = string.match(per)) { - for (i = 0; i < 3; i++) { - rgb[i] = Math.round(Number.parseFloat(match[i + 1]) * 2.55); - } - - if (match[4]) { - rgb[3] = match[5] ? Number.parseFloat(match[4]) * 0.01 : Number.parseFloat(match[4]); - } - } else if (match = string.match(keyword)) { - if (match[1] === 'transparent') { - return [0, 0, 0, 0]; - } - - if (!Object.hasOwn(colorNames, match[1])) { - return null; - } - - rgb = colorNames[match[1]]; - rgb[3] = 1; - - return rgb; - } else { - return null; - } - - for (i = 0; i < 3; i++) { - rgb[i] = clamp(rgb[i], 0, 255); - } - - rgb[3] = clamp(rgb[3], 0, 1); - - return rgb; -}; - -cs.get.hsl = function (string) { - if (!string) { - return null; - } - - const hsl = /^hsla?\(\s*([+-]?(?:\d{0,3}\.)?\d+)(?:deg)?\s*,?\s*([+-]?[\d.]+)%\s*,?\s*([+-]?[\d.]+)%\s*(?:[,|/]\s*([+-]?(?=\.\d|\d)(?:0|[1-9]\d*)?(?:\.\d*)?(?:[eE][+-]?\d+)?)\s*)?\)$/; - const match = string.match(hsl); - - if (match) { - const alpha = Number.parseFloat(match[4]); - const h = ((Number.parseFloat(match[1]) % 360) + 360) % 360; - const s = clamp(Number.parseFloat(match[2]), 0, 100); - const l = clamp(Number.parseFloat(match[3]), 0, 100); - const a = clamp(Number.isNaN(alpha) ? 1 : alpha, 0, 1); - - return [h, s, l, a]; - } - - return null; -}; - -cs.get.hwb = function (string) { - if (!string) { - return null; - } - - const hwb = /^hwb\(\s*([+-]?\d{0,3}(?:\.\d+)?)(?:deg)?\s*,\s*([+-]?[\d.]+)%\s*,\s*([+-]?[\d.]+)%\s*(?:,\s*([+-]?(?=\.\d|\d)(?:0|[1-9]\d*)?(?:\.\d*)?(?:[eE][+-]?\d+)?)\s*)?\)$/; - const match = string.match(hwb); - - if (match) { - const alpha = Number.parseFloat(match[4]); - const h = ((Number.parseFloat(match[1]) % 360) + 360) % 360; - const w = clamp(Number.parseFloat(match[2]), 0, 100); - const b = clamp(Number.parseFloat(match[3]), 0, 100); - const a = clamp(Number.isNaN(alpha) ? 1 : alpha, 0, 1); - return [h, w, b, a]; - } - - return null; -}; - -cs.to.hex = function (...rgba) { - return ( - '#' + - hexDouble(rgba[0]) + - hexDouble(rgba[1]) + - hexDouble(rgba[2]) + - (rgba[3] < 1 - ? (hexDouble(Math.round(rgba[3] * 255))) - : '') - ); -}; - -cs.to.rgb = function (...rgba) { - return rgba.length < 4 || rgba[3] === 1 - ? 'rgb(' + Math.round(rgba[0]) + ', ' + Math.round(rgba[1]) + ', ' + Math.round(rgba[2]) + ')' - : 'rgba(' + Math.round(rgba[0]) + ', ' + Math.round(rgba[1]) + ', ' + Math.round(rgba[2]) + ', ' + rgba[3] + ')'; -}; - -cs.to.rgb.percent = function (...rgba) { - const r = Math.round(rgba[0] / 255 * 100); - const g = Math.round(rgba[1] / 255 * 100); - const b = Math.round(rgba[2] / 255 * 100); - - return rgba.length < 4 || rgba[3] === 1 - ? 'rgb(' + r + '%, ' + g + '%, ' + b + '%)' - : 'rgba(' + r + '%, ' + g + '%, ' + b + '%, ' + rgba[3] + ')'; -}; - -cs.to.hsl = function (...hsla) { - return hsla.length < 4 || hsla[3] === 1 - ? 'hsl(' + hsla[0] + ', ' + hsla[1] + '%, ' + hsla[2] + '%)' - : 'hsla(' + hsla[0] + ', ' + hsla[1] + '%, ' + hsla[2] + '%, ' + hsla[3] + ')'; -}; - -// Hwb is a bit different than rgb(a) & hsl(a) since there is no alpha specific syntax -// (hwb have alpha optional & 1 is default value) -cs.to.hwb = function (...hwba) { - let a = ''; - if (hwba.length >= 4 && hwba[3] !== 1) { - a = ', ' + hwba[3]; - } - - return 'hwb(' + hwba[0] + ', ' + hwba[1] + '%, ' + hwba[2] + '%' + a + ')'; -}; - -cs.to.keyword = function (...rgb) { - return reverseNames[rgb.slice(0, 3)]; -}; - -// Helpers -function clamp(number_, min, max) { - return Math.min(Math.max(min, number_), max); -} - -function hexDouble(number_) { - const string_ = Math.round(number_).toString(16).toUpperCase(); - return (string_.length < 2) ? '0' + string_ : string_; -} - -export default cs; diff --git a/index.test-d.ts b/index.test-d.ts deleted file mode 100644 index 7347147..0000000 --- a/index.test-d.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {expectType} from 'tsd'; -import colorString, {type Model} from './index.js'; - -type GetColorResult = {model: Model; value: number[]} | undefined; -type GetSpecificTypeResult = number[] | undefined; - -type ToColorResult = string | undefined; - -expectType(colorString.get('#FFF')); -expectType(colorString.get('#FFFA')); -expectType(colorString.get('hsl(360, 100%, 50%)')); -expectType(colorString.get('hsl(360 100% 50%)')); -expectType(colorString.get('hwb(60, 3%, 60%)')); - -expectType(colorString.get.rgb('#FFF')); -expectType(colorString.get.rgb('blue')); -expectType(colorString.get.rgb('#FFF')); -expectType(colorString.get.rgb('blue')); -expectType(colorString.get.rgb('rgba(200, 60, 60, 0.3)')); -expectType(colorString.get.rgb('rgba(200 60 60 / 0.3)')); -expectType(colorString.get.rgb('rgba(200 60 60 / 30%)')); -expectType(colorString.get.rgb('rgb(200, 200, 200)')); -expectType(colorString.get.rgb('rgb(200 200 200)')); - -expectType(colorString.get.hsl('hsl(360, 100%, 50%)')); -expectType(colorString.get.hsl('hsl(360 100% 50%)')); -expectType(colorString.get.hsl('hsla(360, 60%, 50%, 0.4)')); -expectType(colorString.get.hsl('hsl(360 60% 50% / 0.4)')); - -expectType(colorString.get.hwb('hwb(60, 3%, 60%)')); -expectType(colorString.get.hwb('hwb(60, 3%, 60%, 0.6)')); - -expectType(colorString.get.rgb('invalid color string')); - -expectType(colorString.to.hex(255, 255, 255)); -expectType(colorString.to.hex(0, 0, 255, 0.4)); -expectType(colorString.to.hex(0, 0, 255, 0.4)); -expectType(colorString.to.rgb(255, 255, 255)); -expectType(colorString.to.rgb(0, 0, 255, 0.4)); -expectType(colorString.to.rgb(0, 0, 255, 0.4)); -expectType(colorString.to.rgb.percent(0, 0, 255)); -expectType(colorString.to.keyword(255, 255, 0)); -expectType(colorString.to.hsl(360, 100, 100)); -expectType(colorString.to.hwb(50, 3, 15)); diff --git a/package.json b/package.json index 5d7164c..dc3e4fb 100644 --- a/package.json +++ b/package.json @@ -10,18 +10,21 @@ ], "repository": "Qix-/color-string", "type": "module", - "exports": "./index.js", - "types": "./index.d.ts", + "exports": "./distribution/index.js", + "types": "distribution", "engines": { "node": ">=18" }, "scripts": { - "test": "xo && tsd && node test.js" + "prepublishOnly": "npm run build", + "build": "del-cli distribution && tsc", + "dev": "del-cli distribution && tsc --watch", + "pretest": "npm run build", + "test": "xo && c8 ava" }, "license": "MIT", "files": [ - "index.js", - "index.d.ts" + "distribution" ], "xo": { "rules": { @@ -33,7 +36,13 @@ "color-name": "^2.0.0" }, "devDependencies": { - "tsd": "^0.31.2", + "@sindresorhus/tsconfig": "^7.0.0", + "@types/color-name": "^2.0.0", + "@types/node": "^22.13.1", + "ava": "^6.2.0", + "c8": "^10.1.3", + "del-cli": "^6.0.0", + "typescript": "^5.7.3", "xo": "^0.60.0" }, "keywords": [ diff --git a/source/index.ts b/source/index.ts new file mode 100644 index 0000000..9d5b05e --- /dev/null +++ b/source/index.ts @@ -0,0 +1,249 @@ +import colorNames from 'color-name'; + +const reversedColorNames = Object.fromEntries(Object.entries(colorNames).map(([k, v]) => [v.join(','), k])); + +const clamp = (number_: number, min: number, max: number) => Math.min(Math.max(min, number_), max); + +export const rgbClamp = (number_: number) => clamp(number_, 0, 255); + +export const alphaClamp = (number_: number) => clamp(number_, 0, 1); + +export const hueClamp = (number_: number) => ((number_ % 360) + 360) % 360; + +export const percentClamp = (number_: number) => clamp(number_, 0, 100); + +const hexDouble = (number_: number) => { + const string_ = Math.round(Math.max(0, number_)).toString(16).toUpperCase(); + return (string_.length < 2) ? '0' + string_ : string_; +}; + +const normalizeRgba = (rgba: number[]) => rgba.map((x, i) => i < 3 ? rgbClamp(x) : alphaClamp(x)); + +// eslint-disable-next-line complexity +export const getRgb = (rgbString: string) => { + if (!rgbString) { + return undefined; + } + + const abbrPattern = /^#(?[a-f\d]{3,4})$/i; + const hexPattern = /^#(?[a-f\d]{6})(?[a-f\d]{2})?$/i; + const rgbaPattern = /^rgba?\(\s*(?[+-]?\d+)(?=[\s,])\s*(?:,\s*)?(?[+-]?\d+)(?=[\s,])\s*(?:,\s*)?(?[+-]?\d+)\s*(?:[,|/]\s*(?[+-]?[\d.]+)(?

%?)\s*)?\)$/; + const perPattern = /^rgba?\(\s*(?[+-]?[\d.]+)%\s*,?\s*(?[+-]?[\d.]+)%\s*,?\s*(?[+-]?[\d.]+)%\s*(?:[,|/]\s*(?[+-]?[\d.]+)(?

%?)\s*)?\)$/; + const keywordPattern = /^(?\w+)$/; + + const hexMatch = hexPattern.exec(rgbString); + if (hexMatch) { + const rrggbb = hexMatch.groups?.['rrggbb']; + const hexAlpha = hexMatch.groups?.['alpha']; + /* c8 ignore start */ + if (!rrggbb) { + return undefined; + } + /* c8 ignore stop */ + + const rgb = [0, 0, 0, 1]; + + for (let i = 0; i < 3; i++) { + const i2 = i * 2; + rgb[i] = Number.parseInt(rrggbb.slice(i2, i2 + 2), 16); + } + + if (hexAlpha) { + rgb[3] = Number.parseInt(hexAlpha, 16) / 255; + } + + return rgb; + } + + const abbrMatch = abbrPattern.exec(rgbString); + if (abbrMatch) { + const rgba = abbrMatch.groups?.['rgba']; + /* c8 ignore start */ + if (!rgba) { + return undefined; + } + /* c8 ignore stop */ + + return normalizeRgba([...rgba.padEnd(4, 'F')].map((x, i) => Number.parseInt(x + x, 16) / (i < 3 ? 1 : 255))); + } + + const rgbaMatch = rgbaPattern.exec(rgbString); + if (rgbaMatch) { + const {r, g, b, a, p} = rgbaMatch.groups /* c8 ignore next */ ?? {}; + /* c8 ignore start */ + if (!r || !g || !b) { + return undefined; + } + /* c8 ignore stop */ + + const rgb = [r, g, b].map(x => Number.parseInt(x, 10)); + const alpha = a ? (p ? Number.parseFloat(a) * 0.01 : Number.parseFloat(a)) : 1; + return normalizeRgba([...rgb, alpha]); + } + + const perMatch = perPattern.exec(rgbString); + if (perMatch) { + const {r, g, b, a, p} = perMatch.groups /* c8 ignore next */ ?? {}; + /* c8 ignore start */ + if (!r || !g || !b) { + return undefined; + } + /* c8 ignore stop */ + + const rgb = [r, g, b].map(x => Math.round(Number.parseFloat(x) * 2.55)); + const alpha = a ? (p ? Number.parseFloat(a) * 0.01 : Number.parseFloat(a)) : 1; + return normalizeRgba([...rgb, alpha]); + } + + const keywordMatch = keywordPattern.exec(rgbString); + if (keywordMatch) { + const keyword = keywordMatch.groups?.['keyword']; + /* c8 ignore start */ + if (!keyword) { + return undefined; + } + /* c8 ignore stop */ + + if (keyword === 'transparent') { + return [0, 0, 0, 0]; + } + + if (!Object.hasOwn(colorNames, keyword)) { + return undefined; + } + + const rgb = colorNames[keyword as keyof typeof colorNames]; + return normalizeRgba([...rgb, 1]); + } + + return undefined; +}; + +export const getHsl = (hslString: string) => { + if (!hslString) { + return undefined; + } + + const hsl = /^hsla?\(\s*(?[+-]?(?:\d{0,3}\.)?\d+)(?:deg)?\s*,?\s*(?[+-]?[\d.]+)%\s*,?\s*(?[+-]?[\d.]+)%\s*(?:[,|/]\s*(?[+-]?(?=\.\d|\d)(?:0|[1-9]\d*)?(?:\.\d*)?(?:[eE][+-]?\d+)?)\s*)?\)$/; + + const hslMatch = hsl.exec(hslString); + if (hslMatch) { + const {h_, s_, l_, a_} = hslMatch.groups /* c8 ignore next */ ?? {}; + /* c8 ignore start */ + if (!h_ || !s_ || !l_) { + return undefined; + } + /* c8 ignore stop */ + + const alpha = Number.parseFloat(a_ ?? ''); + const h = hueClamp(Number.parseFloat(h_)); + const s = percentClamp(Number.parseFloat(s_)); + const l = percentClamp(Number.parseFloat(l_)); + const a = alphaClamp(Number.isNaN(alpha) ? 1 : alpha); + + return [h, s, l, a]; + } + + return undefined; +}; + +export const getHwb = (hwbString: string) => { + if (!hwbString) { + return undefined; + } + + const hwb = /^hwb\(\s*(?[+-]?\d{0,3}(?:\.\d+)?)(?:deg)?\s*,\s*(?[+-]?[\d.]+)%\s*,\s*(?[+-]?[\d.]+)%\s*(?:,\s*(?[+-]?(?=\.\d|\d)(?:0|[1-9]\d*)?(?:\.\d*)?(?:[eE][+-]?\d+)?)\s*)?\)$/; + + const hwbMatch = hwb.exec(hwbString); + if (hwbMatch) { + const {h_, w_, b_, a_} = hwbMatch.groups /* c8 ignore next */ ?? {}; + /* c8 ignore start */ + if (!h_ || !w_ || !b_) { + return null; + } + /* c8 ignore stop */ + + const alpha = Number.parseFloat(a_ ?? ''); + const h = hueClamp(Number.parseFloat(h_)); + const w = percentClamp(Number.parseFloat(w_)); + const b = percentClamp(Number.parseFloat(b_)); + const a = alphaClamp(Number.isNaN(alpha) ? 1 : alpha); + return [h, w, b, a]; + } + + return undefined; +}; + +export const getColor = (colorString: string) => { + const prefix = colorString.slice(0, 3).toLowerCase(); + let value; + let model; + switch (prefix) { + case 'hsl': { + value = getHsl(colorString); + model = 'hsl'; + break; + } + + case 'hwb': { + value = getHwb(colorString); + model = 'hwb'; + break; + } + + default: { + value = getRgb(colorString); + model = 'rgb'; + break; + } + } + + if (!value) { + return undefined; + } + + return {model, value}; +}; + +export const toHex = (r: number, g: number, b: number, a = 1) => { + const alpha = alphaClamp(a); + return '#' + + hexDouble(rgbClamp(r)) + + hexDouble(rgbClamp(g)) + + hexDouble(rgbClamp(b)) + + (alpha < 1 + ? (hexDouble(Math.round(alpha * 255))) + : ''); +}; + +export const toRgb = (r: number, g: number, b: number, a = 1) => { + const alpha = alphaClamp(a); + return alpha === 1 + ? 'rgb(' + Math.round(rgbClamp(r)) + ', ' + Math.round(rgbClamp(g)) + ', ' + Math.round(rgbClamp(b)) + ')' + : 'rgba(' + Math.round(rgbClamp(r)) + ', ' + Math.round(rgbClamp(g)) + ', ' + Math.round(rgbClamp(b)) + ', ' + alpha + ')'; +}; + +export const toRgbPercent = (r: number, g: number, b: number, a = 1) => { + const r_ = Math.round(rgbClamp(r) / 255 * 100); + const g_ = Math.round(rgbClamp(g) / 255 * 100); + const b_ = Math.round(rgbClamp(b) / 255 * 100); + const alpha = alphaClamp(a); + return alpha === 1 + ? 'rgb(' + r_ + '%, ' + g_ + '%, ' + b_ + '%)' + : 'rgba(' + r_ + '%, ' + g_ + '%, ' + b_ + '%, ' + alpha + ')'; +}; + +export const toHsl = (h: number, s: number, l: number, a = 1) => { + const alpha = alphaClamp(a); + return alpha === 1 + ? 'hsl(' + hueClamp(h) + ', ' + percentClamp(s) + '%, ' + percentClamp(l) + '%)' + : 'hsla(' + hueClamp(h) + ', ' + percentClamp(s) + '%, ' + percentClamp(l) + '%, ' + alpha + ')'; +}; + +export const toHwb = (h: number, w: number, b: number, a = 1) => { + const alpha = alphaClamp(a); + const a_ = alpha === 1 ? '' : (', ' + alpha); + return 'hwb(' + hueClamp(h) + ', ' + percentClamp(w) + '%, ' + percentClamp(b) + '%' + a_ + ')'; +}; + +export const toKeyword = (r: number, g: number, b: number) => reversedColorNames[[rgbClamp(r), rgbClamp(g), rgbClamp(b)].join(',')]; diff --git a/test.js b/test.js index e7f6cc6..3cfc703 100644 --- a/test.js +++ b/test.js @@ -1,5 +1,8 @@ -import assert from 'node:assert'; -import string from './index.js'; +import test from 'ava'; +import colorNames from 'color-name'; +import { + getRgb, getHsl, getHwb, getColor, toHex, toRgb, toRgbPercent, toHsl, toHwb, toKeyword, +} from './distribution/index.js'; function normalizeAlpha(result) { if (result.model === 'rgb' && result.value.length >= 4) { @@ -11,198 +14,190 @@ function normalizeAlpha(result) { return result; } -assert.deepEqual(string.get.rgb('#fef'), [255, 238, 255, 1]); -assert.deepEqual(string.get.rgb('#fffFEF'), [255, 255, 239, 1]); -assert.deepEqual(string.get.rgb('rgb(244, 233, 100)'), [244, 233, 100, 1]); -assert.deepEqual(string.get.rgb('rgb(244 233 100)'), [244, 233, 100, 1]); -assert.deepEqual(string.get.rgb('rgb(100%, 30%, 90%)'), [255, 77, 229, 1]); -assert.deepEqual(string.get.rgb('rgb(100% 30% 90%)'), [255, 77, 229, 1]); -assert.deepEqual(string.get.rgb('transparent'), [0, 0, 0, 0]); -assert.deepEqual(string.get.hsl('hsl(240, 100%, 50.5%)'), [240, 100, 50.5, 1]); -assert.deepEqual(string.get.hsl('hsl(240 100% 50.5%)'), [240, 100, 50.5, 1]); -assert.deepEqual(string.get.hsl('hsl(240deg, 100%, 50.5%)'), [240, 100, 50.5, 1]); -assert.deepEqual(string.get.hsl('hsl(240deg 100% 50.5%)'), [240, 100, 50.5, 1]); -assert.deepEqual(string.get.hwb('hwb(240, 100%, 50.5%)'), [240, 100, 50.5, 1]); -assert.deepEqual(string.get.hwb('hwb(240deg, 100%, 50.5%)'), [240, 100, 50.5, 1]); - -// Generic .get() -assert.deepEqual(string.get('invalid'), null); -assert.deepEqual(string.get('#fef'), {model: 'rgb', value: [255, 238, 255, 1]}); -assert.deepEqual(string.get('#fffFEF'), {model: 'rgb', value: [255, 255, 239, 1]}); -assert.deepEqual(string.get('#fffFEFff'), {model: 'rgb', value: [255, 255, 239, 1]}); -assert.deepEqual(string.get('#fffFEF00'), {model: 'rgb', value: [255, 255, 239, 0]}); -assert.deepEqual(normalizeAlpha(string.get('#fffFEFa9')), {model: 'rgb', value: [255, 255, 239, '0.66']}); -assert.deepEqual(string.get('rgb(244, 233, 100)'), {model: 'rgb', value: [244, 233, 100, 1]}); -assert.deepEqual(string.get('rgb(244 233 100)'), {model: 'rgb', value: [244, 233, 100, 1]}); -assert.deepEqual(string.get('rgb(100%, 30%, 90%)'), {model: 'rgb', value: [255, 77, 229, 1]}); -assert.deepEqual(string.get('rgb(100% 30% 90%)'), {model: 'rgb', value: [255, 77, 229, 1]}); -assert.deepEqual(string.get('transparent'), {model: 'rgb', value: [0, 0, 0, 0]}); -assert.deepEqual(string.get('hsl(240, 100%, 50.5%)'), {model: 'hsl', value: [240, 100, 50.5, 1]}); -assert.deepEqual(string.get('hsl(-480, 100%, 50.5%)'), {model: 'hsl', value: [240, 100, 50.5, 1]}); -assert.deepEqual(string.get('hsl(240 100% 50.5%)'), {model: 'hsl', value: [240, 100, 50.5, 1]}); -assert.deepEqual(string.get('hsl(240deg, 100%, 50.5%)'), {model: 'hsl', value: [240, 100, 50.5, 1]}); -assert.deepEqual(string.get('hsl(240deg 100% 50.5%)'), {model: 'hsl', value: [240, 100, 50.5, 1]}); -assert.deepEqual(string.get('hwb(240, 100%, 50.5%)'), {model: 'hwb', value: [240, 100, 50.5, 1]}); -assert.deepEqual(string.get('hwb(240deg, 100%, 50.5%)'), {model: 'hwb', value: [240, 100, 50.5, 1]}); - -// Invalid generic .get() calls -assert.deepEqual(string.get('hsla(250, 100%, 50%, 50%)'), null); -assert.deepEqual(string.get('hsl(250 100% 50% / 50%)'), null); -assert.deepEqual(string.get('rgba(250, 100%, 50%, 50%)'), null); -assert.deepEqual(string.get('333333'), null); -assert.strictEqual(string.get('#1'), null); -assert.strictEqual(string.get('#f'), null); -assert.strictEqual(string.get('#4f'), null); -assert.strictEqual(string.get('#45ab4'), null); -assert.strictEqual(string.get('#45ab45e'), null); -assert.strictEqual(string.get('rgb()'), null); -assert.strictEqual(string.get('rgb(10)'), null); -assert.strictEqual(string.get('rgb(10, 2)'), null); -assert.strictEqual(string.get('rgb(10, 2, 2348723dskjfs)'), null); -assert.strictEqual(string.get('rgb(10%)'), null); -assert.strictEqual(string.get('rgb(10%, 2%)'), null); -assert.strictEqual(string.get('rgb(10%, 2%, 2348723%dskjfs)'), null); -assert.strictEqual(string.get('rgb(10%, 2%, 2348723dskjfs%)'), null); -assert.strictEqual(string.get('rgb(10$,3)'), null); -assert.strictEqual(string.get('rgba(10, 3)'), null); - -// With sign -assert.deepEqual(string.get.rgb('rgb(-244, +233, -100)'), [0, 233, 0, 1]); -assert.deepEqual(string.get.rgb('rgb(-244 +233 -100)'), [0, 233, 0, 1]); -assert.deepEqual(string.get.hsl('hsl(+240, 100%, 50.5%)'), [240, 100, 50.5, 1]); -assert.deepEqual(string.get.hsl('hsl(+240 100% 50.5%)'), [240, 100, 50.5, 1]); -assert.deepEqual(string.get.rgb('rgba(200, +20, -233, -0.0)'), [200, 20, 0, 0]); -assert.deepEqual(string.get.rgb('rgba(200 +20 -233 / -0.0)'), [200, 20, 0, 0]); -assert.deepEqual(string.get.rgb('rgba(200, +20, -233, -0.0)'), [200, 20, 0, 0]); -assert.deepEqual(string.get.rgb('rgba(200 +20 -233 / -0.0)'), [200, 20, 0, 0]); -assert.deepEqual(string.get.hsl('hsla(+200, 100%, 50%, -0.2)'), [200, 100, 50, 0]); -assert.deepEqual(string.get.hsl('hsla(+200, 100%, 50%, -1e-7)'), [200, 100, 50, 0]); -assert.deepEqual(string.get.hsl('hsl(+200 100% 50% / -0.2)'), [200, 100, 50, 0]); -assert.deepEqual(string.get.hsl('hsl(+200 100% 50% / -1e-7)'), [200, 100, 50, 0]); -assert.deepEqual(string.get.hsl('hsl(+200 100% 50% / -2.e7)'), [200, 100, 50, 0]); -assert.deepEqual(string.get.hsl('hsl(+200 100% 50% / +1e7)'), [200, 100, 50, 1]); -assert.deepEqual(string.get.hsl('hsl(+200 100% 50% / 127.88e4)'), [200, 100, 50, 1]); -assert.deepEqual(string.get.hsl('hsl(+200 100% 50% / 0.2e3)'), [200, 100, 50, 1]); -assert.deepEqual(string.get.hsl('hsl(+200 100% 50% / .1e-4)'), [200, 100, 50, 1e-5]); -assert.deepEqual(string.get.hsl('hsla(-10.0, 100%, 50%, -0.2)'), [350, 100, 50, 0]); -assert.deepEqual(string.get.hsl('hsl(-10.0 100% 50% / -0.2)'), [350, 100, 50, 0]); -assert.deepEqual(string.get.hsl('hsla(.5, 100%, 50%, -0.2)'), [0.5, 100, 50, 0]); -assert.deepEqual(string.get.hsl('hsl(.5 100% 50% / -0.2)'), [0.5, 100, 50, 0]); -assert.deepEqual(string.get.hwb('hwb(+240, 100%, 50.5%)'), [240, 100, 50.5, 1]); -assert.deepEqual(string.get.hwb('hwb(-240deg, 100%, 50.5%)'), [120, 100, 50.5, 1]); -assert.deepEqual(string.get.hwb('hwb(-240deg, 100%, 50.5%, +0.6)'), [120, 100, 50.5, 0.6]); -assert.deepEqual(string.get.hwb('hwb(-240deg, 100%, 50.5%, +1e-7)'), [120, 100, 50.5, 1e-7]); -assert.deepEqual(string.get.hwb('hwb(-240deg, 100%, 50.5%, -2.e7)'), [120, 100, 50.5, 0]); -assert.deepEqual(string.get.hwb('hwb(-240deg, 100%, 50.5%, +1e7)'), [120, 100, 50.5, 1]); -assert.deepEqual(string.get.hwb('hwb(-240deg, 100%, 50.5%, +1e7)'), [120, 100, 50.5, 1]); -assert.deepEqual(string.get.hwb('hwb(-240deg, 100%, 50.5%, 127.88e4)'), [120, 100, 50.5, 1]); -assert.deepEqual(string.get.hwb('hwb(-240deg, 100%, 50.5%, 0.2e3)'), [120, 100, 50.5, 1]); -assert.deepEqual(string.get.hwb('hwb(-240deg, 100%, 50.5%, .1e-4)'), [120, 100, 50.5, 1e-5]); -assert.deepEqual(string.get.hwb('hwb(10.0deg, 100%, 50.5%)'), [10, 100, 50.5, 1]); -assert.deepEqual(string.get.hwb('hwb(-.5, 100%, 50.5%)'), [359.5, 100, 50.5, 1]); -assert.deepEqual(string.get.hwb('hwb(-10.0deg, 100%, 50.5%, +0.6)'), [350, 100, 50.5, 0.6]); - -// Subsequent return values should not change array -assert.deepEqual(string.get.rgb('blue'), [0, 0, 255, 1]); -assert.deepEqual(string.get.rgb('blue'), [0, 0, 255, 1]); - -// Alpha -assert.deepEqual(normalizeAlpha(string.get.rgb('#fffa')), [255, 255, 255, '0.67']); -assert.deepEqual(string.get.rgb('#c814e933'), [200, 20, 233, 0.2]); -assert.deepEqual(string.get.rgb('#c814e900'), [200, 20, 233, 0]); -assert.deepEqual(string.get.rgb('#c814e9ff'), [200, 20, 233, 1]); -assert.deepEqual(string.get.rgb('rgba(200, 20, 233, 0.2)'), [200, 20, 233, 0.2]); -assert.deepEqual(string.get.rgb('rgba(200 20 233 / 0.2)'), [200, 20, 233, 0.2]); -assert.deepEqual(string.get.rgb('rgba(200 20 233 / 20%)'), [200, 20, 233, 0.2]); -assert.deepEqual(string.get.rgb('rgba(200, 20, 233, 0)'), [200, 20, 233, 0]); -assert.deepEqual(string.get.rgb('rgba(200 20 233 / 0)'), [200, 20, 233, 0]); -assert.deepEqual(string.get.rgb('rgba(200 20 233 / 0%)'), [200, 20, 233, 0]); -assert.deepEqual(string.get.rgb('rgba(100%, 30%, 90%, 0.2)'), [255, 77, 229, 0.2]); -assert.deepEqual(string.get.rgb('rgba(100% 30% 90% / 0.2)'), [255, 77, 229, 0.2]); -assert.deepEqual(string.get.rgb('rgba(100% 30% 90% / 20%)'), [255, 77, 229, 0.2]); -assert.deepEqual(string.get.hsl('hsla(200, 20%, 33%, 0.2)'), [200, 20, 33, 0.2]); -assert.deepEqual(string.get.hsl('hsla(200, 20%, 33%, 1e-7)'), [200, 20, 33, 1e-7]); -assert.deepEqual(string.get.hsl('hsl(200 20% 33% / 0.2)'), [200, 20, 33, 0.2]); -assert.deepEqual(string.get.hsl('hsl(200 20% 33% / 1e-7)'), [200, 20, 33, 1e-7]); -assert.deepEqual(string.get.hwb('hwb(200, 20%, 33%, 0.2)'), [200, 20, 33, 0.2]); -assert.deepEqual(string.get.hwb('hwb(200, 20%, 33%, 1e-7)'), [200, 20, 33, 1e-7]); - -// No alpha -assert.deepEqual(string.get.rgb('#fef'), [255, 238, 255, 1]); -assert.deepEqual(string.get.rgb('rgba(200, 20, 233)'), [200, 20, 233, 1]); -assert.deepEqual(string.get.rgb('rgba(200 20 233)'), [200, 20, 233, 1]); -assert.deepEqual(string.get.hsl('hsl(240, 100%, 50.5%)'), [240, 100, 50.5, 1]); -assert.deepEqual(string.get.hsl('hsl(240 100% 50.5%)'), [240, 100, 50.5, 1]); -assert.deepEqual(string.get.rgb('rgba(0, 0, 0, 0)'), [0, 0, 0, 0]); -assert.deepEqual(string.get.rgb('rgba(0 0 0 / 0)'), [0, 0, 0, 0]); -assert.deepEqual(string.get.hsl('hsla(0, 0%, 0%, 0)'), [0, 0, 0, 0]); -assert.deepEqual(string.get.hsl('hsl(0 0% 0% / 0)'), [0, 0, 0, 0]); -assert.deepEqual(string.get.hsl('hsl(0deg 0% 0% / 0)'), [0, 0, 0, 0]); -assert.deepEqual(string.get.hwb('hwb(400, 10%, 200%, 0)'), [40, 10, 100, 0]); - -// Range -assert.deepEqual(string.get.rgb('rgba(300, 600, 100, 3)'), [255, 255, 100, 1]); -assert.deepEqual(string.get.rgb('rgba(300 600 100 / 3)'), [255, 255, 100, 1]); -assert.deepEqual(string.get.rgb('rgba(8000%, 100%, 333%, 88)'), [255, 255, 255, 1]); -assert.deepEqual(string.get.rgb('rgba(8000% 100% 333% / 88)'), [255, 255, 255, 1]); -assert.deepEqual(string.get.hsl('hsla(400, 10%, 200%, 10)'), [40, 10, 100, 1]); -assert.deepEqual(string.get.hsl('hsl(400 10% 200% / 10)'), [40, 10, 100, 1]); -assert.deepEqual(string.get.hwb('hwb(400, 10%, 200%, 10)'), [40, 10, 100, 1]); - -// Invalid -assert.strictEqual(string.get.rgb('yellowblue'), null); -assert.strictEqual(string.get.rgb('hsl(100, 10%, 10%)'), null); -assert.strictEqual(string.get.rgb('hsl(100 10% 10%)'), null); -assert.strictEqual(string.get.rgb('hwb(100, 10%, 10%)'), null); -assert.strictEqual(string.get.rgb('rgb(123, 255, 9)1234'), null); -assert.strictEqual(string.get.rgb('rgb(123 255 9)1234'), null); -assert.strictEqual(string.get.rgb('333333'), null); -assert.strictEqual(string.get.rgb('1'), null); -assert.strictEqual(string.get.rgb('1892371923879'), null); -assert.strictEqual(string.get.rgb('444'), null); -assert.strictEqual(string.get.rgb('#1'), null); -assert.strictEqual(string.get.rgb('#f'), null); -assert.strictEqual(string.get.rgb('#4f'), null); -assert.strictEqual(string.get.rgb('#45ab4'), null); -assert.strictEqual(string.get.rgb('#45ab45e'), null); -assert.strictEqual(string.get.hsl('hsl(41, 50%, 45%)1234'), null); -assert.strictEqual(string.get.hsl('hsl(41 50% 45%)1234'), null); -assert.strictEqual(string.get.hsl('hsl(41 50% 45% / 3)1234'), null); -assert.strictEqual(string.get.hsl('hsl(41 50% 45% / 1e)'), null); -assert.strictEqual(string.get.hsl('hsl(41 50% 45% / e)'), null); -assert.strictEqual(string.get.hsl('hsl(41 50% 45% / 0e-)'), null); -assert.strictEqual(string.get.hsl('hsl(41 50% 45% / 0e+)'), null); -assert.strictEqual(string.get.hsl('hsl(41 50% 45% / +000e33)'), null); -assert.strictEqual(string.get.hwb('hwb(240, 100%, 1e'), null); -assert.strictEqual(string.get.hwb('hwb(240, 100%, e'), null); -assert.strictEqual(string.get.hwb('hwb(240, 100%, 0e-'), null); -assert.strictEqual(string.get.hwb('hwb(240, 100%, 0e+'), null); -assert.strictEqual(string.get.hwb('hwb(240, 100%, +000e33'), null); - -// Generators -assert.equal(string.to.hex(255, 10, 35), '#FF0A23'); -assert.equal(string.to.hex(255, 10, 35, 1), '#FF0A23'); - -assert.equal(string.to.rgb(255, 10, 35), 'rgb(255, 10, 35)'); -assert.equal(string.to.rgb(255, 10, 35, 0.3), 'rgba(255, 10, 35, 0.3)'); - -assert.equal(string.to.rgb.percent(255, 10, 35), 'rgb(100%, 4%, 14%)'); - -assert.equal(string.to.rgb.percent(255, 10, 35), 'rgb(100%, 4%, 14%)'); -assert.equal(string.to.rgb.percent(255, 10, 35, 0.3), 'rgba(100%, 4%, 14%, 0.3)'); - -assert.equal(string.to.hsl(280, 40, 60), 'hsl(280, 40%, 60%)'); -assert.equal(string.to.hsl(280, 40, 60, 0.3), 'hsla(280, 40%, 60%, 0.3)'); - -assert.equal(string.to.hwb(280, 40, 60), 'hwb(280, 40%, 60%)'); -assert.equal(string.to.hwb(280, 40, 60, 0.3), 'hwb(280, 40%, 60%, 0.3)'); - -assert.equal(string.to.keyword(255, 255, 0), 'yellow'); -assert.equal(string.to.keyword(100, 255, 0), undefined); - -// Make sure .get() doesn't return object prototype values (regression test, #44) -Object.keys(Object.getOwnPropertyDescriptors(Object.prototype)) - .map(property => assert.deepStrictEqual([property, string.get(property)], [property, null])); - -// Make sure writing decimal values as hex doesn't cause bizarre output (regression test, #25) -assert.equal(string.to.hex(44.2, 83.8, 44), '#2C542C'); +test('getRgb()', t => { + t.is(getRgb(''), undefined); + t.is(getRgb('unicorn'), undefined); + t.is(getRgb('invalid color string'), undefined); + t.deepEqual(getRgb('transparent'), [0, 0, 0, 0]); + t.deepEqual(getRgb('#fef'), [255, 238, 255, 1]); + t.deepEqual(getRgb('#fefe'), [255, 238, 255, 0xEE / 0xFF]); + t.deepEqual(getRgb('#fffFEF'), [255, 255, 239, 1]); + t.deepEqual(getRgb('#fffFEFEF'), [255, 255, 239, 0xEF / 0xFF]); + t.deepEqual(getRgb('rgb(244, 233, 100)'), [244, 233, 100, 1]); + t.deepEqual(getRgb('rgb(244 233 100)'), [244, 233, 100, 1]); + t.deepEqual(getRgb('rgb(100%, 30%, 90%)'), [255, 77, 229, 1]); + t.deepEqual(getRgb('rgb(100% 30% 90%)'), [255, 77, 229, 1]); + t.deepEqual(getRgb('rgba(244, 233, 100)'), [244, 233, 100, 1]); + t.deepEqual(getRgb('rgba(244, 233, 100, 0.5)'), [244, 233, 100, 0.5]); + t.deepEqual(getRgb('rgba(244, 233, 100, 50%)'), [244, 233, 100, 0.5]); + t.deepEqual(getRgb('rgba(244 233 100 / 0.5)'), [244, 233, 100, 0.5]); + t.deepEqual(getRgb('rgba(244 233 100 / 50%)'), [244, 233, 100, 0.5]); + t.deepEqual(getRgb('rgba(100%, 30%, 90%, 0.5)'), [255, 77, 229, 0.5]); + t.deepEqual(getRgb('rgba(100%, 30%, 90%, 50%)'), [255, 77, 229, 0.5]); + t.deepEqual(getRgb('rgba(100% 30% 90% / 0.5)'), [255, 77, 229, 0.5]); + t.deepEqual(getRgb('rgba(100% 30% 90% / 50%)'), [255, 77, 229, 0.5]); + t.deepEqual(getRgb('rgb(-1, -255, -255, -1)'), [0, 0, 0, 0]); + t.deepEqual(getRgb('rgb(256, 256, 355)'), [255, 255, 255, 1]); + for (const [name, color] of Object.entries(colorNames)) { + t.deepEqual(getRgb(name), [...color, 1]); + } +}); + +test('getHsl()', t => { + t.is(getHsl(''), undefined); + t.is(getHsl('invalid color string'), undefined); + t.deepEqual(getHsl('hsla(+200, 100%, 50%, -0.2)'), [200, 100, 50, 0]); + t.deepEqual(getHsl('hsla(+200, 100%, 50%, -1e-7)'), [200, 100, 50, 0]); + t.deepEqual(getHsl('hsl(+200 100% 50% / -0.2)'), [200, 100, 50, 0]); + t.deepEqual(getHsl('hsl(+200 100% 50% / -1e-7)'), [200, 100, 50, 0]); + t.deepEqual(getHsl('hsl(+200 100% 50% / -2.e7)'), [200, 100, 50, 0]); + t.deepEqual(getHsl('hsl(+200 100% 50% / +1e7)'), [200, 100, 50, 1]); + t.deepEqual(getHsl('hsl(+200 100% 50% / 127.88e4)'), [200, 100, 50, 1]); + t.deepEqual(getHsl('hsl(+200 100% 50% / 0.2e3)'), [200, 100, 50, 1]); + t.deepEqual(getHsl('hsl(+200 100% 50% / .1e-4)'), [200, 100, 50, 1e-5]); + t.deepEqual(getHsl('hsla(-10.0, 100%, 50%, -0.2)'), [350, 100, 50, 0]); + t.deepEqual(getHsl('hsl(-10.0 100% 50% / -0.2)'), [350, 100, 50, 0]); + t.deepEqual(getHsl('hsla(.5, 100%, 50%, -0.2)'), [0.5, 100, 50, 0]); + t.deepEqual(getHsl('hsl(.5 100% 50% / -0.2)'), [0.5, 100, 50, 0]); + t.deepEqual(getHsl('hsla(200, 20%, 33%, 0.2)'), [200, 20, 33, 0.2]); + t.deepEqual(getHsl('hsla(200, 20%, 33%, 1e-7)'), [200, 20, 33, 1e-7]); + t.deepEqual(getHsl('hsl(200 20% 33% / 0.2)'), [200, 20, 33, 0.2]); + t.deepEqual(getHsl('hsl(200 20% 33% / 1e-7)'), [200, 20, 33, 1e-7]); + t.deepEqual(getHsl('hsl(240, 100%, 50.5%)'), [240, 100, 50.5, 1]); + t.deepEqual(getHsl('hsl(240 100% 50.5%)'), [240, 100, 50.5, 1]); + t.deepEqual(getHsl('hsla(0, 0%, 0%, 0)'), [0, 0, 0, 0]); + t.deepEqual(getHsl('hsl(0 0% 0% / 0)'), [0, 0, 0, 0]); + t.deepEqual(getHsl('hsl(0deg 0% 0% / 0)'), [0, 0, 0, 0]); + t.deepEqual(getHsl('hsla(400, 10%, 200%, 10)'), [40, 10, 100, 1]); + t.deepEqual(getHsl('hsl(400 10% 200% / 10)'), [40, 10, 100, 1]); + t.is(getHsl('hsl(41, 50%, 45%)1234'), undefined); + t.is(getHsl('hsl(41 50% 45%)1234'), undefined); + t.is(getHsl('hsl(41 50% 45% / 3)1234'), undefined); + t.is(getHsl('hsl(41 50% 45% / 1e)'), undefined); + t.is(getHsl('hsl(41 50% 45% / e)'), undefined); + t.is(getHsl('hsl(41 50% 45% / 0e-)'), undefined); + t.is(getHsl('hsl(41 50% 45% / 0e+)'), undefined); + t.is(getHsl('hsl(41 50% 45% / +000e33)'), undefined); +}); + +test('getHwb()', t => { + t.is(getHwb(''), undefined); + t.is(getHwb('invalid color string'), undefined); + t.deepEqual(getHwb('hwb(240, 100%, 50.5%)'), [240, 100, 50.5, 1]); + t.deepEqual(getHwb('hwb(240deg, 100%, 50.5%)'), [240, 100, 50.5, 1]); + t.deepEqual(getHwb('hwb(+240, 100%, 50.5%)'), [240, 100, 50.5, 1]); + t.deepEqual(getHwb('hwb(-240deg, 100%, 50.5%)'), [120, 100, 50.5, 1]); + t.deepEqual(getHwb('hwb(-240deg, 100%, 50.5%, +0.6)'), [120, 100, 50.5, 0.6]); + t.deepEqual(getHwb('hwb(-240deg, 100%, 50.5%, +1e-7)'), [120, 100, 50.5, 1e-7]); + t.deepEqual(getHwb('hwb(-240deg, 100%, 50.5%, -2.e7)'), [120, 100, 50.5, 0]); + t.deepEqual(getHwb('hwb(-240deg, 100%, 50.5%, +1e7)'), [120, 100, 50.5, 1]); + t.deepEqual(getHwb('hwb(-240deg, 100%, 50.5%, +1e7)'), [120, 100, 50.5, 1]); + t.deepEqual(getHwb('hwb(-240deg, 100%, 50.5%, 127.88e4)'), [120, 100, 50.5, 1]); + t.deepEqual(getHwb('hwb(-240deg, 100%, 50.5%, 0.2e3)'), [120, 100, 50.5, 1]); + t.deepEqual(getHwb('hwb(-240deg, 100%, 50.5%, .1e-4)'), [120, 100, 50.5, 1e-5]); + t.deepEqual(getHwb('hwb(10.0deg, 100%, 50.5%)'), [10, 100, 50.5, 1]); + t.deepEqual(getHwb('hwb(-.5, 100%, 50.5%)'), [359.5, 100, 50.5, 1]); + t.deepEqual(getHwb('hwb(-10.0deg, 100%, 50.5%, +0.6)'), [350, 100, 50.5, 0.6]); + t.deepEqual(getHwb('hwb(200, 20%, 33%, 0.2)'), [200, 20, 33, 0.2]); + t.deepEqual(getHwb('hwb(200, 20%, 33%, 1e-7)'), [200, 20, 33, 1e-7]); + t.deepEqual(getHwb('hwb(400, 10%, 200%, 0)'), [40, 10, 100, 0]); + t.deepEqual(getHwb('hwb(400, 10%, 200%, 10)'), [40, 10, 100, 1]); + t.is(getHwb('hwb(240, 100%, 1e'), undefined); + t.is(getHwb('hwb(240, 100%, e'), undefined); + t.is(getHwb('hwb(240, 100%, 0e-'), undefined); + t.is(getHwb('hwb(240, 100%, 0e+'), undefined); + t.is(getHwb('hwb(240, 100%, +000e33'), undefined); +}); + +test('getColor()', t => { + t.is(getColor(''), undefined); + t.is(getColor('invalid'), undefined); + t.is(getColor('invalid color string'), undefined); + t.deepEqual(getColor('#fef'), {model: 'rgb', value: [255, 238, 255, 1]}); + t.deepEqual(getColor('#fffFEF'), {model: 'rgb', value: [255, 255, 239, 1]}); + t.deepEqual(getColor('#fffFEFff'), {model: 'rgb', value: [255, 255, 239, 1]}); + t.deepEqual(getColor('#fffFEF00'), {model: 'rgb', value: [255, 255, 239, 0]}); + t.deepEqual(normalizeAlpha(getColor('#fffFEFa9')), {model: 'rgb', value: [255, 255, 239, '0.66']}); + t.deepEqual(getColor('rgb(244, 233, 100)'), {model: 'rgb', value: [244, 233, 100, 1]}); + t.deepEqual(getColor('rgb(244 233 100)'), {model: 'rgb', value: [244, 233, 100, 1]}); + t.deepEqual(getColor('rgb(100%, 30%, 90%)'), {model: 'rgb', value: [255, 77, 229, 1]}); + t.deepEqual(getColor('rgb(100% 30% 90%)'), {model: 'rgb', value: [255, 77, 229, 1]}); + t.deepEqual(getColor('transparent'), {model: 'rgb', value: [0, 0, 0, 0]}); + t.deepEqual(getColor('hsl(240, 100%, 50.5%)'), {model: 'hsl', value: [240, 100, 50.5, 1]}); + t.deepEqual(getColor('hsl(-480, 100%, 50.5%)'), {model: 'hsl', value: [240, 100, 50.5, 1]}); + t.deepEqual(getColor('hsl(240 100% 50.5%)'), {model: 'hsl', value: [240, 100, 50.5, 1]}); + t.deepEqual(getColor('hsl(240deg, 100%, 50.5%)'), {model: 'hsl', value: [240, 100, 50.5, 1]}); + t.deepEqual(getColor('hsl(240deg 100% 50.5%)'), {model: 'hsl', value: [240, 100, 50.5, 1]}); + t.deepEqual(getColor('hwb(240, 100%, 50.5%)'), {model: 'hwb', value: [240, 100, 50.5, 1]}); + t.deepEqual(getColor('hwb(240deg, 100%, 50.5%)'), {model: 'hwb', value: [240, 100, 50.5, 1]}); + t.is(getColor('hsla(250, 100%, 50%, 50%)'), undefined); + t.is(getColor('hsl(250 100% 50% / 50%)'), undefined); + t.is(getColor('rgba(250, 100%, 50%, 50%)'), undefined); + t.is(getColor('333333'), undefined); + t.is(getColor('#1'), undefined); + t.is(getColor('#f'), undefined); + t.is(getColor('#4f'), undefined); + t.is(getColor('#45ab4'), undefined); + t.is(getColor('#45ab45e'), undefined); + t.is(getColor('rgb()'), undefined); + t.is(getColor('rgb(10)'), undefined); + t.is(getColor('rgb(10, 2)'), undefined); + t.is(getColor('rgb(10, 2, 2348723dskjfs)'), undefined); + t.is(getColor('rgb(10%)'), undefined); + t.is(getColor('rgb(10%, 2%)'), undefined); + t.is(getColor('rgb(10%, 2%, 2348723%dskjfs)'), undefined); + t.is(getColor('rgb(10%, 2%, 2348723dskjfs%)'), undefined); + t.is(getColor('rgb(10$,3)'), undefined); + t.is(getColor('rgba(10, 3)'), undefined); +}); + +test('toHex()', t => { + t.is(toHex(255, 10, 35), '#FF0A23'); + t.is(toHex(255, 10, 35, 1), '#FF0A23'); + t.is(toHex(255, 10, 35, 2), '#FF0A23'); + t.is(toHex(255, 10, 35, 0), '#FF0A2300'); + t.is(toHex(255, 10, 35, -1), '#FF0A2300'); + t.is(toHex(44.2, 83.8, 44), '#2C542C'); + t.is(toHex(255, 10, 35, 0.5), '#FF0A2380'); + t.is(toHex(-1, -255, -255, -1), '#00000000'); + t.is(toHex(256, 256, 355, 2), '#FFFFFF'); +}); + +test('toRgb()', t => { + t.is(toRgb(255, 10, 35), 'rgb(255, 10, 35)'); + t.is(toRgb(255, 10, 35, 1), 'rgb(255, 10, 35)'); + t.is(toRgb(255, 10, 35, 0), 'rgba(255, 10, 35, 0)'); + t.is(toRgb(255, 10, 35, 0.3), 'rgba(255, 10, 35, 0.3)'); + t.is(toRgb(-255, -10, -35, -1), 'rgba(0, 0, 0, 0)'); + t.is(toRgb(256, 1000, 355, 2), 'rgb(255, 255, 255)'); +}); + +test('toRgbPercent', t => { + t.is(toRgbPercent(255, 10, 35), 'rgb(100%, 4%, 14%)'); + t.is(toRgbPercent(255, 10, 35, 1), 'rgb(100%, 4%, 14%)'); + t.is(toRgbPercent(255, 10, 35, 0), 'rgba(100%, 4%, 14%, 0)'); + t.is(toRgbPercent(255, 10, 35, 0.3), 'rgba(100%, 4%, 14%, 0.3)'); + t.is(toRgbPercent(-255, -10, -35, -1), 'rgba(0%, 0%, 0%, 0)'); + t.is(toRgbPercent(256, 1000, 355, 2), 'rgb(100%, 100%, 100%)'); +}); + +test('toHsl()', t => { + t.is(toHsl(280, 40, 60), 'hsl(280, 40%, 60%)'); + t.is(toHsl(280, 40, 60, 0.3), 'hsla(280, 40%, 60%, 0.3)'); + t.is(toHsl(-80, -40, -60, -1), 'hsla(280, 0%, 0%, 0)'); + t.is(toHsl(640, 400, 120, 2), 'hsl(280, 100%, 100%)'); +}); + +test('toHwb()', t => { + t.is(toHwb(280, 40, 60), 'hwb(280, 40%, 60%)'); + t.is(toHwb(280, 40, 60, 0.3), 'hwb(280, 40%, 60%, 0.3)'); + t.is(toHwb(-80, -40, -60, -1), 'hwb(280, 0%, 0%, 0)'); + t.is(toHwb(640, 400, 120, 2), 'hwb(280, 100%, 100%)'); +}); + +test('toKeyword()', t => { + t.is(toKeyword(255, 255, 0), 'yellow'); + t.is(toKeyword(100, 255, 0), undefined); + t.is(toKeyword(-255, -255, -1), 'black'); + t.is(toKeyword(256, 256, 355), 'white'); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d9018f5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@sindresorhus/tsconfig", + "include": ["source"], + "compilerOptions": { + "outDir": "distribution" + } +} From 13ad08e7ae49e723b192d062fff652f3c9bbf931 Mon Sep 17 00:00:00 2001 From: LitoMore Date: Thu, 13 Feb 2025 08:31:34 +0800 Subject: [PATCH 2/4] Fix typo --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7e21d27..c604000 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,10 @@ getRgb('rgba(200 60 60 / 30%)') // [200, 60, 60, 0.3] getRgb('rgb(200, 200, 200)') // [200, 200, 200, 1] getRgb('rgb(200 200 200)') // [200, 200, 200, 1] -getHel('hsl(360, 100%, 50%)') // [0, 100, 50, 1] -getHel('hsl(360 100% 50%)') // [0, 100, 50, 1] -getHel('hsla(360, 60%, 50%, 0.4)') // [0, 60, 50, 0.4] -getHel('hsl(360 60% 50% / 0.4)') // [0, 60, 50, 0.4] +getHsl('hsl(360, 100%, 50%)') // [0, 100, 50, 1] +getHsl('hsl(360 100% 50%)') // [0, 100, 50, 1] +getHsl('hsla(360, 60%, 50%, 0.4)') // [0, 60, 50, 0.4] +getHsl('hsl(360 60% 50% / 0.4)') // [0, 60, 50, 0.4] getHwb('hwb(60, 3%, 60%)') // [60, 3, 60, 1] getHwb('hwb(60, 3%, 60%, 0.6)') // [60, 3, 60, 0.6] From 7c560a79b116f70b6c7674c1c4ffd501183224a7 Mon Sep 17 00:00:00 2001 From: LitoMore Date: Thu, 13 Feb 2025 08:53:56 +0800 Subject: [PATCH 3/4] Fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c604000..9cf8032 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ import {getColor, getRgb, getHsl, getHwb} from 'color-string'; getColor('#FFF') // {model: 'rgb', value: [255, 255, 255, 1]} getColor('#FFFA') // {model: 'rgb', value: [255, 255, 255, 0.67]} getColor('#FFFFFFAA') // {model: 'rgb', value: [255, 255, 255, 0.67]} -getColor('rgb(244 233 100)') // {model: 'rgb' value: [244, 233, 100, 1]}); +getColor('rgb(244 233 100)') // {model: 'rgb', value: [244, 233, 100, 1]}); getColor('rgb(100%, 30%, 90%)') // {model: 'rgb', value: [255, 77, 229, 1]}); getColor('rgb(100% 30% 90%)') // {model: 'rgb', value: [255, 77, 229, 1]}); getColor('hsl(360, 100%, 50%)') // {model: 'hsl', value: [0, 100, 50, 1]} From 58ec4508fef17283fd7e629c8ff086ce14654cd0 Mon Sep 17 00:00:00 2001 From: LitoMore Date: Thu, 13 Feb 2025 08:58:39 +0800 Subject: [PATCH 4/4] Polish --- README.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 9cf8032..894572b 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ import {getColor, getRgb, getHsl, getHwb} from 'color-string'; getColor('#FFF') // {model: 'rgb', value: [255, 255, 255, 1]} getColor('#FFFA') // {model: 'rgb', value: [255, 255, 255, 0.67]} getColor('#FFFFFFAA') // {model: 'rgb', value: [255, 255, 255, 0.67]} -getColor('rgb(244 233 100)') // {model: 'rgb', value: [244, 233, 100, 1]}); -getColor('rgb(100%, 30%, 90%)') // {model: 'rgb', value: [255, 77, 229, 1]}); -getColor('rgb(100% 30% 90%)') // {model: 'rgb', value: [255, 77, 229, 1]}); +getColor('rgb(244 233 100)') // {model: 'rgb', value: [244, 233, 100, 1]}) +getColor('rgb(100%, 30%, 90%)') // {model: 'rgb', value: [255, 77, 229, 1]}) +getColor('rgb(100% 30% 90%)') // {model: 'rgb', value: [255, 77, 229, 1]}) getColor('hsl(360, 100%, 50%)') // {model: 'hsl', value: [0, 100, 50, 1]} getColor('hsl(360 100% 50%)') // {model: 'hsl', value: [0, 100, 50, 1]} getColor('hwb(60, 3%, 60%)') // {model: 'hwb', value: [60, 3, 60, 1]} @@ -66,25 +66,25 @@ toHwb(50, 3, 15) // "hwb(50, 3%, 15%)" ```js import {alphaClamp, rgbClamp, hueClamp, percentClamp} from 'color-string'; -alphaClamp(-1) // 0 -alphaClamp(0.5) // 0.5 -alphaClamp(1) // 1 -alphaClamp(2) // 1 - -rgbClamp(-255) // 0 -rgbClamp(128) // 128 -rgbClamp(255) // 255 -rgbClamp(256) // 255 - -hueClamp(-40) // 320 -hueClamp(40) // 40 -hueClamp(360) // 0 -hueClamp(400) // 40 - -percentClamp(-1) // 0 -percentClamp(10) // 10 -percentClamp(100) // 100 -percentClamp(101) // 100 +alphaClamp(-1) // 0 +alphaClamp(0.5) // 0.5 +alphaClamp(1) // 1 +alphaClamp(2) // 1 + +rgbClamp(-255) // 0 +rgbClamp(128) // 128 +rgbClamp(255) // 255 +rgbClamp(256) // 255 + +hueClamp(-40) // 320 +hueClamp(40) // 40 +hueClamp(360) // 0 +hueClamp(400) // 40 + +percentClamp(-1) // 0 +percentClamp(10) // 10 +percentClamp(100) // 100 +percentClamp(101) // 100 ``` ## License