diff --git a/index.d.ts b/index.d.ts index a63f779..baabed0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,3 +1,3 @@ -declare function hexToRgba(color: string, alpha?: string | number): string; +declare function hexToRgba(color: string, alpha?: string | number, parseRgb?: boolean): string; export = hexToRgba; diff --git a/src/index.js b/src/index.js index 17bdf05..ff9d5e2 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,5 @@ +import { isRgb, rgbToRgba } from './rgb-parser'; + const removeHash = hex => (hex.charAt(0) === '#' ? hex.slice(1) : hex); const parseHex = (nakedHex) => { @@ -48,12 +50,20 @@ const formatRgb = (decimalObject, parameterA) => { * * If you specify an alpha value, you'll get a rgba() value instead. * - * @param The hex value to convert. ('123456'. '#123456', ''123', '#123') - * @param An alpha value to apply. (optional) ('0.5', '0.25') + * @param colorStr: The value to convert. ('123456', '#123456', ''123', '#123', rgb(0, 0, 0), + * rgba(0, 1, 2, 1) ) + * @param a: An alpha value to apply. (optional) ('0.5', '0.25') + * @param parseRgb: enable rgb and rgba string parsing. (optional) (true, false), false by default. + * Useful in situations where the input value is unpredictable (hex or rgb), but you still need to + * return an rgba string consistently. * @return An rgb or rgba value. ('rgb(11, 22, 33)'. 'rgba(11, 22, 33, 0.5)') */ -const hexToRgba = (hex, a) => { - const hashlessHex = removeHash(hex); +const hexToRgba = (colorStr, a, parseRgb = false) => { + if (parseRgb && isRgb(colorStr)) { + return rgbToRgba(colorStr, a); + } + + const hashlessHex = removeHash(colorStr); const hexObject = parseHex(hashlessHex); const decimalObject = hexesToDecimals(hexObject); diff --git a/src/rgb-parser.js b/src/rgb-parser.js new file mode 100644 index 0000000..182909f --- /dev/null +++ b/src/rgb-parser.js @@ -0,0 +1,34 @@ +class RgbParseError extends Error { + constructor(message) { + super(message); + this.name = 'RgbParseError'; + } +} + +// The long expressions are just to catch errors +const RE_RGB = /^rgb\( *(?:(?:[01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5]) *, *){2}(?:[01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5]) *\)$/; +const RE_RGBA = /^rgba\( *(?:(?:[01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5]) *, *){3}(?:(?:0\.)?\d+) *\)$/; +const RE_ALPHA = /(?:0\.)?\d+ *(?=\))/; +const RE_NO_ALPHA = /\)/; +const RE_IS_RGB = /^rgba?/; + +const isRgb = str => RE_IS_RGB.test(str); + +const rgbToRgba = (str, a) => { + if (RE_RGB.test(str)) { + if (a !== undefined) { + return str.replace(RE_NO_ALPHA, `, ${a})`).replace(/rgb\(/, 'rgba('); + } + return str; + } + + if (RE_RGBA.test(str)) { + return a !== undefined ? str.replace(RE_ALPHA, `${a}`) : str; // replace alpha if defined, otherwise don't + } + + throw new RgbParseError( + `rgba? string is invalid, must be in the form rgba?('0-255', '0-255', '0-255', '0-1'?), not: ${str}`, + ); +}; + +module.exports = { isRgb, rgbToRgba, RgbParseError }; diff --git a/test/hex.test.js b/test/hex.test.js new file mode 100644 index 0000000..702b593 --- /dev/null +++ b/test/hex.test.js @@ -0,0 +1,181 @@ +/* global describe it */ +import assert from 'assert'; +import hexToRgba from '..'; + +[false, true].forEach((parseRgb) => { + describe('hex-to-rgba', () => { + describe(`when parseRgb is ${parseRgb}`, () => { + describe('6-digit hex values, no a', () => { + it('should calculate correct rgb values', () => { + assert.equal('rgba(17, 34, 51, 1)', hexToRgba('112233', undefined, parseRgb)); + }); + + it('should ignore a leading hash sign', () => { + assert.equal('rgba(17, 34, 51, 1)', hexToRgba('#112233', undefined, parseRgb)); + }); + + it('should correctly calculate uppercase hex', () => { + assert.equal('rgba(127, 127, 127, 1)', hexToRgba('#7F7F7F', undefined, parseRgb)); + }); + + it('should correctly calculate lowercase hex', () => { + assert.equal('rgba(127, 127, 127, 1)', hexToRgba('#7f7f7f', undefined, parseRgb)); + }); + }); + + describe('6-digit hex values, a as parameter', () => { + it('should calculate rgba values from hex and string alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('112233', '0.5', parseRgb)); + }); + + it('should calculate rgba values from hex and numerical alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('112233', 0.75, parseRgb)); + }); + + it('should handle the edge case where alpha value is 1', () => { + assert.equal('rgba(17, 34, 51, 1)', hexToRgba('112233', 1, parseRgb)); + }); + + it('should handle the edge case where alpha value is 0', () => { + assert.equal('rgba(17, 34, 51, 0)', hexToRgba('112233', 0, parseRgb)); + }); + + it('should calculate rgba values from hex with leading hash and string alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('#112233', '0.5', parseRgb)); + }); + + it('should calculate rgba values from hex with leading hash and numerical alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('#112233', 0.75, parseRgb)); + }); + }); + + describe('3-digit hex values, no a', () => { + it('should calculate correct rgb values', () => { + assert.equal('rgba(17, 34, 51, 1)', hexToRgba('123', undefined, parseRgb)); + }); + + it('should ignore a leading hash sign', () => { + assert.equal('rgba(17, 34, 51, 1)', hexToRgba('#123', undefined, parseRgb)); + }); + }); + + describe('3-digit hex values, a as parameter', () => { + it('should calculate rgba values from hex and string alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('123', '0.5', parseRgb)); + }); + + it('should calculate rgba values from hex and numerical alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('123', 0.75, parseRgb)); + }); + + it('should handle the edge case where alpha value is 1', () => { + assert.equal('rgba(17, 34, 51, 1)', hexToRgba('123', 1, parseRgb)); + }); + + it('should handle the edge case where alpha value is 0', () => { + assert.equal('rgba(17, 34, 51, 0)', hexToRgba('123', 0, parseRgb)); + }); + + it('should calculate rgba values from hex with leading hash and string alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('#123', '0.5', parseRgb)); + }); + + it('should calculate rgba values from hex with leading hash and numerical alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('#123', 0.75, parseRgb)); + }); + }); + + describe('8-digit hex values, no a', () => { + it('should calculate correct rgb values', () => { + assert.equal('rgba(17, 34, 51, 0.27)', hexToRgba('11223344', undefined, parseRgb)); + }); + + it('should ignore a leading hash sign', () => { + assert.equal('rgba(17, 34, 51, 0.27)', hexToRgba('#11223344', undefined, parseRgb)); + }); + + it('should remove trailing zeros', () => { + assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('1122337f', undefined, parseRgb)); + }); + + it('should handle the edge case where alpha value is 1', () => { + assert.equal('rgba(17, 34, 51, 1)', hexToRgba('112233ff', undefined, parseRgb)); + }); + + it('should handle the edge case where alpha value is 0', () => { + assert.equal('rgba(17, 34, 51, 0)', hexToRgba('112233', 0, parseRgb)); + }); + }); + + describe('8-digit hex values, a as parameter (separate parameter should override alpha in hex)', () => { + it('should calculate rgba values from hex and string alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('11223344', '0.5', parseRgb)); + }); + + it('should calculate rgba values from hex and numerical alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('11223344', 0.75, parseRgb)); + }); + + it('should handle the edge case where alpha value is 1', () => { + assert.equal('rgba(17, 34, 51, 1)', hexToRgba('11223344', 1, parseRgb)); + }); + + it('should handle the edge case where alpha value is 0', () => { + assert.equal('rgba(17, 34, 51, 0)', hexToRgba('11223344', 0, parseRgb)); + }); + + it('should calculate rgba values from hex with leading hash and string alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('#11223344', '0.5', parseRgb)); + }); + + it('should calculate rgba values from hex with leading hash and numerical alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('#11223344', 0.75, parseRgb)); + }); + }); + + describe('4-digit hex values, no a', () => { + it('should calculate correct rgb values', () => { + assert.equal('rgba(17, 34, 51, 0.27)', hexToRgba('1234', undefined, parseRgb)); + }); + + it('should ignore a leading hash sign', () => { + assert.equal('rgba(17, 34, 51, 0.27)', hexToRgba('#1234', undefined, parseRgb)); + }); + + it('should handle the edge case where alpha value is 1', () => { + assert.equal('rgba(17, 34, 51, 1)', hexToRgba('123f', undefined, parseRgb)); + }); + + it('should handle the edge case where alpha value is 0', () => { + assert.equal('rgba(17, 34, 51, 0)', hexToRgba('1230', undefined, parseRgb)); + }); + }); + + describe('4-digit hex values, a as parameter (separate parameter should override alpha in hex)', () => { + it('should calculate rgba values from hex and string alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('1234', '0.5', parseRgb)); + }); + + it('should calculate rgba values from hex and numerical alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('1234', 0.75, parseRgb)); + }); + + it('should handle the edge case where alpha value is 1', () => { + assert.equal('rgba(17, 34, 51, 1)', hexToRgba('1234', 1, parseRgb)); + }); + + it('should handle the edge case where alpha value is 0', () => { + assert.equal('rgba(17, 34, 51, 0)', hexToRgba('1234', 0, parseRgb)); + }); + + it('should calculate rgba values from hex with leading hash and string alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('#1234', '0.5', parseRgb)); + }); + + it('should calculate rgba values from hex with leading hash and numerical alpha value', () => { + assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('#1234', 0.75, parseRgb)); + }); + }); + }); + }); +}); diff --git a/test/rgba.test.js b/test/rgba.test.js new file mode 100644 index 0000000..f1d0bec --- /dev/null +++ b/test/rgba.test.js @@ -0,0 +1,104 @@ +/* global describe it */ +import assert from 'assert'; +import hexToRgba from '..'; + +describe('rgba?-to-rgba', () => { + describe('parseRgb option', () => { + it('should parse rgba strings when set to true', () => { + assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('rgba(17, 34, 51, 1)', '0.5', true)); + }); + + it('should parse rgb strings when set to true', () => { + assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('rgb(17, 34, 51)', '0.5', true)); + }); + }); + + describe('rgb parser', () => { + it('should throw when given an alpha channel', () => { + const callback = () => hexToRgba('rgb(17, 34, 51, 1)', undefined, true); + assert.throws(callback, Error); // RgbParseError doesn't pass, even though it throws + }); + + it('should return the rgb string if "a" is undefined', () => { + assert.equal('rgb(17, 34, 51)', hexToRgba('rgb(17, 34, 51)', undefined, true)); + }); + + it('should accept all valid rgb values', () => { + for (let i = 0; i <= 255; i++) { // eslint-disable-line no-plusplus + const result = hexToRgba(`rgb(${i}, ${i}, ${i})`, '1', true); + assert.equal(result, `rgba(${i}, ${i}, ${i}, 1)`); + } + }); + + ['-1', '-2', '-100', '256', '257', '1000'].forEach((val) => { + describe(`given the invalid rgb value: ${val}`, () => { + [ + `rgb(${val}, 0, 0)`, + `rgb(0, ${val}, 0)`, + `rgb(0, 0, ${val})`, + `rgb(0, ${val}, ${val})`, + `rgb(${val}, ${val}, 0)`, + `rgb(${val}, 0, ${val})`, + `rgb(${val}, ${val}, ${val})`, + ].forEach((rgbStr) => { + it(`should throw for: ${rgbStr}`, () => { + const callback = () => hexToRgba(rgbStr, undefined, true); + assert.throws(callback, Error); // RgbParseError doesn't pass, even though it throws + }); + }); + }); + }); + }); + + describe('rgba parser', () => { + it('should throw when not given an alpha channel', () => { + const callback = () => hexToRgba('rgba(17, 34, 51)', undefined, true); + assert.throws(callback, Error); // RgbParseError doesn't pass, even though it throws + }); + + it('should leave the alpha channel untouched if no alpha is given', () => { + assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('rgba(17, 34, 51, 0.5)', undefined, true)); + }); + + it('should accept all valid rgb values', () => { + for (let i = 0; i <= 255; i++) { // eslint-disable-line no-plusplus + const result = hexToRgba(`rgba(${i}, ${i}, ${i}, 1)`, '1', true); + assert.equal(result, `rgba(${i}, ${i}, ${i}, 1)`); + } + }); + + it('should accept at least all rgb-alpha values from 0.001 to 1, when a is undefined', () => { + for (let i = 0.001; i <= 1; i += 0.001) { + const result = hexToRgba(`rgba(0, 0, 0, ${i})`, undefined, true); + assert.equal(result, `rgba(0, 0, 0, ${i})`); + } + }); + + it('should accept a mix of valid rgb and alpha values, when a is undefined', () => { + for (let i = 0; i <= 255; i++) { // eslint-disable-line no-plusplus + // Magic number: 1/255 = 0.00392156862745098 - allows alpha to scale with RGB values, <= 1 + const result = hexToRgba(`rgba(${i}, ${i}, ${i}, ${i * 0.00392156862745098})`, undefined, true); + assert.equal(result, `rgba(${i}, ${i}, ${i}, ${i * 0.00392156862745098})`); + } + }); + + ['-1', '-2', '-100', '256', '257', '1000'].forEach((val) => { + describe(`given the invalid rgb value: ${val}`, () => { + [ + `rgba(${val}, 0, 0, 1)`, + `rgba(0, ${val}, 0, 1)`, + `rgba(0, 0, ${val}, 1)`, + `rgba(0, ${val}, ${val}, 1)`, + `rgba(${val}, ${val}, 0, 1)`, + `rgba(${val}, 0, ${val}, 1)`, + `rgba(${val}, ${val}, ${val}, 1)`, + ].forEach((rgbaStr) => { + it(`should throw for: ${rgbaStr}`, () => { + const callback = () => hexToRgba(rgbaStr, undefined, true); + assert.throws(callback, Error); // RgbParseError doesn't pass, even though it throws + }); + }); + }); + }); + }); +}); diff --git a/test/test.js b/test/test.js deleted file mode 100644 index 15a9668..0000000 --- a/test/test.js +++ /dev/null @@ -1,177 +0,0 @@ -/* global describe it */ -import assert from 'assert'; -import hexToRgba from '..'; - -describe('hex-to-rgba', () => { - describe('6-digit hex values, no a', () => { - it('should calculate correct rgb values', () => { - assert.equal('rgba(17, 34, 51, 1)', hexToRgba('112233')); - }); - - it('should ignore a leading hash sign', () => { - assert.equal('rgba(17, 34, 51, 1)', hexToRgba('#112233')); - }); - - it('should correctly calculate uppercase hex', () => { - assert.equal('rgba(127, 127, 127, 1)', hexToRgba('#7F7F7F')); - }); - - it('should correctly calculate lowercase hex', () => { - assert.equal('rgba(127, 127, 127, 1)', hexToRgba('#7f7f7f')); - }); - }); - - describe('6-digit hex values, a as parameter', () => { - it('should calculate rgba values from hex and string alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('112233', '0.5')); - }); - - it('should calculate rgba values from hex and numerical alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('112233', 0.75)); - }); - - it('should handle the edge case where alpha value is 1', () => { - assert.equal('rgba(17, 34, 51, 1)', hexToRgba('112233', 1)); - }); - - it('should handle the edge case where alpha value is 0', () => { - assert.equal('rgba(17, 34, 51, 0)', hexToRgba('112233', 0)); - }); - - it('should calculate rgba values from hex with leading hash and string alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('#112233', '0.5')); - }); - - it('should calculate rgba values from hex with leading hash and numerical alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('#112233', 0.75)); - }); - }); - - describe('3-digit hex values, no a', () => { - it('should calculate correct rgb values', () => { - assert.equal('rgba(17, 34, 51, 1)', hexToRgba('123')); - }); - - it('should ignore a leading hash sign', () => { - assert.equal('rgba(17, 34, 51, 1)', hexToRgba('#123')); - }); - }); - - describe('3-digit hex values, a as parameter', () => { - it('should calculate rgba values from hex and string alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('123', '0.5')); - }); - - it('should calculate rgba values from hex and numerical alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('123', 0.75)); - }); - - it('should handle the edge case where alpha value is 1', () => { - assert.equal('rgba(17, 34, 51, 1)', hexToRgba('123', 1)); - }); - - it('should handle the edge case where alpha value is 0', () => { - assert.equal('rgba(17, 34, 51, 0)', hexToRgba('123', 0)); - }); - - it('should calculate rgba values from hex with leading hash and string alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('#123', '0.5')); - }); - - it('should calculate rgba values from hex with leading hash and numerical alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('#123', 0.75)); - }); - }); - - describe('8-digit hex values, no a', () => { - it('should calculate correct rgb values', () => { - assert.equal('rgba(17, 34, 51, 0.27)', hexToRgba('11223344')); - }); - - it('should ignore a leading hash sign', () => { - assert.equal('rgba(17, 34, 51, 0.27)', hexToRgba('#11223344')); - }); - - it('should remove trailing zeros', () => { - assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('1122337f')); - }); - - it('should handle the edge case where alpha value is 1', () => { - assert.equal('rgba(17, 34, 51, 1)', hexToRgba('112233ff')); - }); - - it('should handle the edge case where alpha value is 0', () => { - assert.equal('rgba(17, 34, 51, 0)', hexToRgba('112233', 0)); - }); - }); - - describe('8-digit hex values, a as parameter (separate parameter should override alpha in hex)', () => { - it('should calculate rgba values from hex and string alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('11223344', '0.5')); - }); - - it('should calculate rgba values from hex and numerical alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('11223344', 0.75)); - }); - - it('should handle the edge case where alpha value is 1', () => { - assert.equal('rgba(17, 34, 51, 1)', hexToRgba('11223344', 1)); - }); - - it('should handle the edge case where alpha value is 0', () => { - assert.equal('rgba(17, 34, 51, 0)', hexToRgba('11223344', 0)); - }); - - it('should calculate rgba values from hex with leading hash and string alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('#11223344', '0.5')); - }); - - it('should calculate rgba values from hex with leading hash and numerical alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('#11223344', 0.75)); - }); - }); - - describe('4-digit hex values, no a', () => { - it('should calculate correct rgb values', () => { - assert.equal('rgba(17, 34, 51, 0.27)', hexToRgba('1234')); - }); - - it('should ignore a leading hash sign', () => { - assert.equal('rgba(17, 34, 51, 0.27)', hexToRgba('#1234')); - }); - - it('should handle the edge case where alpha value is 1', () => { - assert.equal('rgba(17, 34, 51, 1)', hexToRgba('123f')); - }); - - it('should handle the edge case where alpha value is 0', () => { - assert.equal('rgba(17, 34, 51, 0)', hexToRgba('1230')); - }); - }); - - describe('4-digit hex values, a as parameter (separate parameter should override alpha in hex)', () => { - it('should calculate rgba values from hex and string alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('1234', '0.5')); - }); - - it('should calculate rgba values from hex and numerical alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('1234', 0.75)); - }); - - it('should handle the edge case where alpha value is 1', () => { - assert.equal('rgba(17, 34, 51, 1)', hexToRgba('1234', 1)); - }); - - it('should handle the edge case where alpha value is 0', () => { - assert.equal('rgba(17, 34, 51, 0)', hexToRgba('1234', 0)); - }); - - it('should calculate rgba values from hex with leading hash and string alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.5)', hexToRgba('#1234', '0.5')); - }); - - it('should calculate rgba values from hex with leading hash and numerical alpha value', () => { - assert.equal('rgba(17, 34, 51, 0.75)', hexToRgba('#1234', 0.75)); - }); - }); -});