-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
`.hex()` return hex string\n`.rgb()` return rgb string\n`.rgbObj()` return rgb object
- Loading branch information
Showing
4 changed files
with
371 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import type { RGBObject } from './types' | ||
|
||
const validHEXSymbols = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'] | ||
|
||
function hexToInt(symbol: string): number { | ||
return Number.parseInt(symbol, 16) | ||
} | ||
|
||
export function intToHex(number: number): string { | ||
const str = number.toString(16) | ||
return str.padEnd(2, str) | ||
} | ||
|
||
function normalizePct(number: number): number { | ||
return Math.max(0, Math.min(100, number)) | ||
} | ||
|
||
function normalizeRGBValue(number: number): number { | ||
return Math.max(0, Math.min(255, number)) | ||
} | ||
|
||
function pctToInt(pct: string): number { | ||
return normalizePct(Number.parseFloat(pct)) / 100 | ||
} | ||
|
||
function rgbValueToInt(value: string): number { | ||
return normalizeRGBValue(Number.parseFloat(value)) | ||
} | ||
|
||
function parseHEX(color: string): RGBObject { | ||
if (color.toLowerCase().split('').slice(1).some(l => !validHEXSymbols.includes(l))) | ||
throw new SyntaxError('For HEX format expected only HEX symbols') | ||
|
||
const hex = color.slice(1) | ||
let r | ||
let g | ||
let b | ||
let a | ||
|
||
switch (hex.length) { | ||
case 3: | ||
case 4: | ||
r = hexToInt(hex[0] + hex[0]) | ||
g = hexToInt(hex[1] + hex[1]) | ||
b = hexToInt(hex[2] + hex[2]) | ||
hex[3] && (a = hexToInt(hex[3] + hex[3])) | ||
break | ||
case 6: | ||
case 8: | ||
r = hexToInt(hex[0] + hex[1]) | ||
g = hexToInt(hex[2] + hex[3]) | ||
b = hexToInt(hex[4] + hex[5]) | ||
hex[6] && (a = hexToInt(hex[6] + hex[7])) | ||
break | ||
default: | ||
throw new SyntaxError('For HEX format expected 3, 4, 6, or 8 symbols') | ||
} | ||
|
||
return { r, g, b, a } | ||
} | ||
|
||
function parseRGB(color: string): RGBObject { | ||
const rgbLegacyPct = color.match(/^rgba?\(\s*(\d*\.?\d+%)\s*,\s*(\d*\.?\d+%)\s*,\s*(\d*\.?\d+%)\s*(,\s*(\d*\.?\d+%?)\s*)?\)$/) | ||
const rgbLegacyInt = color.match(/^rgba?\(\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*(,\s*(\d*\.?\d+%?)\s*)?\)$/) | ||
const rgbModernPct = color.match(/^rgba?\(\s*(\d*\.?\d+%|none)\s+(\d*\.?\d+%|none)\s+(\d*\.?\d+%|none)\s*(?:\/\s*(\d*\.?\d+%?|none)\s*)?\)$/) | ||
const rgbModernInt = color.match(/^rgba?\(\s*(\d*\.?\d+|none)\s+(\d*\.?\d+|none)\s+(\d*\.?\d+|none)\s*(?:\/\s*(\d*\.?\d+%?|none)\s*)?\)$/) | ||
const rgb = rgbLegacyPct || rgbLegacyInt || rgbModernPct || rgbModernInt | ||
|
||
if (rgb === null) | ||
throw new SyntaxError('Invalid RGB format, expected legacy or modern syntax with 3 or 4 values') | ||
|
||
const [_, R, G, B, A = '1'] = rgb | ||
const r = R === 'none' ? 0 : R.endsWith('%') ? pctToInt(R) * 255 : rgbValueToInt(R) | ||
const g = G === 'none' ? 0 : G.endsWith('%') ? pctToInt(G) * 255 : rgbValueToInt(G) | ||
const b = B === 'none' ? 0 : B.endsWith('%') ? pctToInt(B) * 255 : rgbValueToInt(B) | ||
const a = A === 'none' ? 0 : A.endsWith('%') ? pctToInt(A) : Math.max(0, Math.min(1, Number.parseFloat(A))) | ||
|
||
return { r, g, b, a } | ||
} | ||
|
||
export function parse(color: any): RGBObject { | ||
if (typeof color !== 'string') | ||
throw new TypeError('Expected a string') | ||
|
||
if (!(color.startsWith('#') || color.startsWith('rgb'))) | ||
throw new SyntaxError('Expected a HEX or RGB format') | ||
|
||
let rgbObject: RGBObject | ||
= { r: 0, g: 0, b: 0, a: 1 } | ||
|
||
if (color.startsWith('#')) | ||
rgbObject = parseHEX(color) | ||
else if (color.startsWith('rgb')) | ||
rgbObject = parseRGB(color) | ||
|
||
return rgbObject | ||
} | ||
|
||
export function safe(color: RGBObject): RGBObject { | ||
const { r, g, b } = color | ||
const nR = Math.round(r / 51) * 51 | ||
const nG = Math.round(g / 51) * 51 | ||
const nB = Math.round(b / 51) * 51 | ||
|
||
return { r: nR, g: nG, b: nB } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,161 @@ | ||
import { describe, expect, it } from 'vitest' | ||
import { describe, expect, it, vi } from 'vitest' | ||
import { safolor } from './safolor' | ||
|
||
describe('safolor', () => { | ||
it.each([ | ||
['#000000', '#000000'], | ||
['#123456', '#003366'], | ||
['#abcdef', '#99ccff'], | ||
['#01234567', '#003333'], | ||
['#fff', '#ffffff'], | ||
['#1af', '#0099ff'], | ||
['#cdef', '#ccccff'], | ||
])('%s => %s', (o_color, s_color) => { | ||
expect(safolor(o_color as any)).toBe(s_color) | ||
describe('errors', () => { | ||
const typeErrorMessage = 'Expected a string' | ||
const formatErrorMessage = 'Expected a HEX or RGB format' | ||
const hexLengthErrorMessage = 'For HEX format expected 3, 4, 6, or 8 symbols' | ||
const hexSymbolsErrorMessage = 'For HEX format expected only HEX symbols' | ||
const rgbErrorMessage = 'Invalid RGB format, expected legacy or modern syntax with 3 or 4 values' | ||
|
||
it.each([ | ||
[true, new TypeError(typeErrorMessage)], | ||
['some color', new SyntaxError(formatErrorMessage)], | ||
['#', new SyntaxError(hexLengthErrorMessage)], | ||
['#1', new SyntaxError(hexLengthErrorMessage)], | ||
['#12', new SyntaxError(hexLengthErrorMessage)], | ||
['#12345', new SyntaxError(hexLengthErrorMessage)], | ||
['#1234567', new SyntaxError(hexLengthErrorMessage)], | ||
['#123456789', new SyntaxError(hexLengthErrorMessage)], | ||
['#qwerty', new SyntaxError(hexSymbolsErrorMessage)], | ||
['rgb(123, 50%, 255)', new SyntaxError(rgbErrorMessage)], | ||
['rgba(123 50 255 1)', new SyntaxError(rgbErrorMessage)], | ||
['rgb(123 50 255, 1)', new SyntaxError(rgbErrorMessage)], | ||
['rgba(123 50% 255 / 1)', new SyntaxError(rgbErrorMessage)], | ||
['rgb(123, 50, 255 / 1)', new SyntaxError(rgbErrorMessage)], | ||
['rgba(123, 50, 255 1)', new SyntaxError(rgbErrorMessage)], | ||
['rgb(123, 50 255, 1)', new SyntaxError(rgbErrorMessage)], | ||
])('%s => %s', (o_color, s_color) => { | ||
expect(() => safolor(o_color as any)).toThrowError(s_color) | ||
}) | ||
}) | ||
|
||
describe('() direct call', () => { | ||
it('() calls .hex() method', () => { | ||
const color = '#123456' | ||
const spy = vi.spyOn(safolor, 'hex').mockImplementationOnce(() => color) | ||
|
||
safolor(color) | ||
expect(spy).toHaveBeenCalledWith(color) | ||
|
||
spy.mockRestore() | ||
}) | ||
}) | ||
|
||
describe('.hex() method', () => { | ||
describe('from hex', () => { | ||
it.each([ | ||
['#000000', '#000000'], | ||
['#123456', '#003366'], | ||
['#abcdef', '#99ccff'], | ||
['#01234567', '#003333'], | ||
['#fff', '#ffffff'], | ||
['#1af', '#0099ff'], | ||
['#cdef', '#ccccff'], | ||
])('%s => %s', (o_color, s_color) => { | ||
expect(safolor.hex(o_color as any)).toBe(s_color) | ||
}) | ||
}) | ||
|
||
describe('from rgb', () => { | ||
it.each([ | ||
['rgb(0, 0, 0)', '#000000'], | ||
['rgba(18, 52, 86, 1)', '#003366'], | ||
['rgb(100, 136, 170, 50%)', '#669999'], | ||
['rgba(200%, 100%, 100%)', '#ffffff'], | ||
['rgb(100%, 36%, 70%, 0.404)', '#ff66cc'], | ||
['rgba(18%, 52%, 86%, 50%)', '#3399cc'], | ||
['rgb(171 205 239)', '#99ccff'], | ||
['rgba(255 255 255 / 2)', '#ffffff'], | ||
['rgb(17 170 255 / 50%)', '#0099ff'], | ||
['rgba(40.4% 42.1% 83.8%)', '#6666cc'], | ||
['rgb(57.1% 20.5% 73.9% / .5)', '#9933cc'], | ||
['rgba(37.1% 60.5% 23.9% / 60%)', '#669933'], | ||
])('%s => %s', (o_color, s_color) => { | ||
expect(safolor.hex(o_color as any)).toBe(s_color) | ||
}) | ||
}) | ||
}) | ||
|
||
it.each([ | ||
[true, new TypeError('Expected a string')], | ||
['rgb(255 255 255)', new SyntaxError('Expected a HEX format')], | ||
])('%s => %s', (o_color, s_color) => { | ||
expect(() => safolor(o_color as any)).toThrowError(s_color) | ||
describe('.rgb() method', () => { | ||
it('.rgb() calls .rgbObj() method', () => { | ||
const color = '#123456' | ||
const returned = { r: 18, g: 52, b: 86, a: 1 } | ||
const spy = vi.spyOn(safolor, 'rgbObj').mockImplementationOnce(() => returned) | ||
|
||
safolor.rgb(color) | ||
expect(spy).toHaveBeenCalledWith(color) | ||
|
||
spy.mockRestore() | ||
}) | ||
|
||
describe('from hex', () => { | ||
it.each([ | ||
['#000000', 'rgb(0, 0, 0)'], | ||
['#123456', 'rgb(0, 51, 102)'], | ||
['#abcdef', 'rgb(153, 204, 255)'], | ||
['#01234567', 'rgb(0, 51, 51)'], | ||
['#fff', 'rgb(255, 255, 255)'], | ||
['#1af', 'rgb(0, 153, 255)'], | ||
['#cdef', 'rgb(204, 204, 255)'], | ||
])('%s => %s', (o_color, s_color) => { | ||
expect(safolor.rgb(o_color as any)).toBe(s_color) | ||
}) | ||
}) | ||
|
||
describe('from rgb', () => { | ||
it.each([ | ||
['rgb(0, 0, 0)', 'rgb(0, 0, 0)'], | ||
['rgba(18, 52, 86, 1)', 'rgb(0, 51, 102)'], | ||
['rgb(100, 136, 170, 50%)', 'rgb(102, 153, 153)'], | ||
['rgba(200%, 100%, 100%)', 'rgb(255, 255, 255)'], | ||
['rgb(100%, 36%, 70%, 0.404)', 'rgb(255, 102, 204)'], | ||
['rgba(18%, 52%, 86%, 50%)', 'rgb(51, 153, 204)'], | ||
['rgb(171 205 239)', 'rgb(153, 204, 255)'], | ||
['rgba(255 255 255 / 2)', 'rgb(255, 255, 255)'], | ||
['rgb(17 170 255 / 50%)', 'rgb(0, 153, 255)'], | ||
['rgba(40.4% 42.1% 83.8%)', 'rgb(102, 102, 204)'], | ||
['rgb(57.1% 20.5% 73.9% / .5)', 'rgb(153, 51, 204)'], | ||
['rgba(37.1% 60.5% 23.9% / 60%)', 'rgb(102, 153, 51)'], | ||
])('%s => %s', (o_color, s_color) => { | ||
expect(safolor.rgb(o_color as any)).toBe(s_color) | ||
}) | ||
}) | ||
}) | ||
|
||
describe('.rgbObj() method', () => { | ||
describe('from hex', () => { | ||
it.each([ | ||
['#000000', { r: 0, g: 0, b: 0 }], | ||
['#123456', { r: 0, g: 51, b: 102 }], | ||
['#abcdef', { r: 153, g: 204, b: 255 }], | ||
['#01234567', { r: 0, g: 51, b: 51 }], | ||
['#fff', { r: 255, g: 255, b: 255 }], | ||
['#1af', { r: 0, g: 153, b: 255 }], | ||
['#cdef', { r: 204, g: 204, b: 255 }], | ||
])('%s => %s', (o_color, s_color) => { | ||
expect(safolor.rgbObj(o_color as any)).toStrictEqual(s_color) | ||
}) | ||
}) | ||
|
||
describe('from rgb', () => { | ||
it.each([ | ||
['rgb(0, 0, 0)', { r: 0, g: 0, b: 0 }], | ||
['rgba(18, 52, 86, 1)', { r: 0, g: 51, b: 102 }], | ||
['rgb(100, 136, 170, 50%)', { r: 102, g: 153, b: 153 }], | ||
['rgba(200%, 100%, 100%)', { r: 255, g: 255, b: 255 }], | ||
['rgb(100%, 36%, 70%, 0.404)', { r: 255, g: 102, b: 204 }], | ||
['rgba(18%, 52%, 86%, 50%)', { r: 51, g: 153, b: 204 }], | ||
['rgb(171 205 239)', { r: 153, g: 204, b: 255 }], | ||
['rgba(255 255 255 / 2)', { r: 255, g: 255, b: 255 }], | ||
['rgb(17 170 255 / 50%)', { r: 0, g: 153, b: 255 }], | ||
['rgba(40.4% 42.1% 83.8%)', { r: 102, g: 102, b: 204 }], | ||
['rgb(57.1% 20.5% 73.9% / .5)', { r: 153, g: 51, b: 204 }], | ||
['rgba(37.1% 60.5% 23.9% / 60%)', { r: 102, g: 153, b: 51 }], | ||
])('%s => %s', (o_color, s_color) => { | ||
expect(safolor.rgbObj(o_color as any)).toStrictEqual(s_color) | ||
}) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,46 @@ | ||
export type HEX = `#${string}` | ||
import { intToHex, parse, safe } from './helpers' | ||
import type { HEX, RGB, RGBA, RGBObject, Safolor } from './types' | ||
|
||
export function safolor(color: HEX): HEX { | ||
if (typeof color !== 'string') | ||
throw new TypeError('Expected a string') | ||
export const safolor: Safolor = (() => { | ||
let color: RGBObject | ||
let safeColor: RGBObject | ||
|
||
const hex = color.match( | ||
/^#(?:([\da-f]{2})([\da-f]{2})([\da-f]{2})([\da-f]{2})?|([\da-f]{1})([\da-f]{1})([\da-f]{1})([\da-f]{1})?)$/i, | ||
) | ||
function exec(original: string) { | ||
color = parse(original) | ||
safeColor = safe(color) | ||
} | ||
|
||
if (hex === null) | ||
throw new SyntaxError('Expected a HEX format') | ||
function hex(color: HEX): HEX | ||
function hex(color: RGB | RGBA): HEX | ||
function hex(color: string): HEX { | ||
exec(color) | ||
const { r, g, b } = safeColor | ||
return `#${intToHex(r)}${intToHex(g)}${intToHex(b)}` | ||
} | ||
|
||
const [_, rr, gg, bb, __, r, g, b, ___] = hex | ||
function rgb(color: HEX): RGB | ||
function rgb(color: RGB | RGBA): RGB | ||
function rgb(color: string): RGB { | ||
const { r, g, b } = call.rgbObj(color as any) | ||
return `rgb(${r}, ${g}, ${b})` | ||
} | ||
|
||
const R = rr || r.padEnd(2, r) | ||
const G = gg || g.padEnd(2, g) | ||
const B = bb || b.padEnd(2, b) | ||
function rgbObj(color: HEX): RGBObject | ||
function rgbObj(color: RGB | RGBA): RGBObject | ||
function rgbObj(color: string): RGBObject { | ||
exec(color) | ||
return safeColor | ||
} | ||
|
||
const nR = (Math.round(Number.parseInt(R, 16) / 51) * 51).toString(16) | ||
const nG = (Math.round(Number.parseInt(G, 16) / 51) * 51).toString(16) | ||
const nB = (Math.round(Number.parseInt(B, 16) / 51) * 51).toString(16) | ||
function call(color: HEX): HEX | ||
function call(color: RGB | RGBA): HEX | ||
function call(color: string): HEX { | ||
return call.hex(color as any) | ||
} | ||
|
||
return `#${nR.padEnd(2, nR)}${nG.padEnd(2, nG)}${nB.padEnd(2, nB)}` | ||
} | ||
call.hex = hex | ||
call.rgb = rgb | ||
call.rgbObj = rgbObj | ||
|
||
return call | ||
})() |
Oops, something went wrong.