Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse RGB(A) Strings Too #90

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
@@ -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;
18 changes: 14 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isRgb, rgbToRgba } from './rgb-parser';

const removeHash = hex => (hex.charAt(0) === '#' ? hex.slice(1) : hex);

const parseHex = (nakedHex) => {
Expand Down Expand Up @@ -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);

Expand Down
34 changes: 34 additions & 0 deletions src/rgb-parser.js
Original file line number Diff line number Diff line change
@@ -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 };
181 changes: 181 additions & 0 deletions test/hex.test.js
Original file line number Diff line number Diff line change
@@ -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));
});
});
});
});
});
104 changes: 104 additions & 0 deletions test/rgba.test.js
Original file line number Diff line number Diff line change
@@ -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
});
});
});
});
});
});
Loading