Skip to content

Commit

Permalink
feat: Enhance HSLA color parsing and validation
Browse files Browse the repository at this point in the history
This commit improves HSLA color parsing by returning a detailed object including hue, saturation, lightness, alpha, and their respective units. It also enhances the HSLA validation to support different units for hue and allows alpha as both a percentage and a floating number. The test suite has been expanded accordingly.
  • Loading branch information
mallikcheripally committed Jun 1, 2024
1 parent b0339c6 commit 7648724
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 87 deletions.
50 changes: 39 additions & 11 deletions src/parser/parseHsla.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,34 @@ import { hslaRegex } from '@/utils/regex';
* This function extracts the hue, saturation, lightness, and alpha values from an HSLA color string.
*
* @param {string} color - The HSLA color string to parse.
* @returns {[number, number, number, number]} An array containing the hue, saturation, lightness, and alpha values.
* @returns {{
* h: number;
* hUnit?: string | undefined;
* hDeg: number;
* s: number;
* sUnit?: string | undefined;
* l: number;
* lUnit?: string | undefined;
* a: number;
* aUnit?: string | undefined;
* }} Returns an object value
* @throws {Error} Throws an error if the color string is not a valid HSLA color.
*/
export function parseHsla(color: string): [number, number, number, number] {
export function parseHsla(color: string): {
h: number;
hUnit?: string | undefined;
hDeg: number;
s: number;
sUnit?: string | undefined;
l: number;
lUnit?: string | undefined;
a: number;
aUnit?: string | undefined;
} {
const match = color.match(hslaRegex);
if (!match || !isValidHsla(color)) throw new Error('Invalid HSLA color format');
if (!match || !isValidHsla(color)) {
throw new Error('Invalid HSLA color format');
}

const parseHue = (hue: string, unit: string | undefined): number => {
let hueValue = parseFloat(hue);
Expand All @@ -30,14 +52,20 @@ export function parseHsla(color: string): [number, number, number, number] {
}
};

const h = Math.round(parseHue(match[1], match[2]));
const s = parseFloat(match[3]);
const l = parseFloat(match[4]);
let a = match[5] === 'none' ? 1 : parseFloat(match[5]);
const roundTo = (num: number, precision: number): number => {
const factor = Math.pow(10, precision);
return Math.round(num * factor) / factor;
};

if (match[5].endsWith('%')) {
a = parseFloat(match[5]) / 100;
}
const h = parseFloat(match[1]);
const hDeg = roundTo(parseHue(match[1], match[3]), 2);
const hUnit = match[3];
const s = parseFloat(match[4]);
const sUnit = match[6];
const l = parseFloat(match[7]);
const lUnit = match[9];
const a = match[10].includes('%') ? parseFloat(match[10]) / 100 : parseFloat(match[10]);
const aUnit = match[10].includes('%') ? '%' : undefined;

return [h, s, l, a];
return { h, hUnit, hDeg, s, sUnit, l, lUnit, a, aUnit };
}
5 changes: 3 additions & 2 deletions src/utils/regex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ export const hexRegex: RegExp = /^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$/;

export const hexAlphaRegex: RegExp = /^#([a-fA-F0-9]{4}|[a-fA-F0-9]{8})$/;

export const hslRegex: RegExp = /^hsl\(\s*([\d.]+)(deg|rad|grad|turn)?\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%\s*\)$/;
export const hslRegex: RegExp =
/^hsl\(\s*(\d+(\.\d+)?)(deg|rad|grad|turn)?\s*,\s*(\d+(\.\d+)?)(%)?\s*,\s*(\d+(\.\d+)?)(%)?\s*\)$/i;

export const hslaRegex: RegExp =
/^hsla\(\s*([\d.]+)(deg|rad|grad|turn)?\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%\s*,\s*(0|1|0?\.\d+|0?\.?\d+%|none)\s*\)$/;
/^hsla\(\s*(\d+(\.\d+)?)(deg|rad|grad|turn)?\s*,\s*(\d+(\.\d+)?)(%)?\s*,\s*(\d+(\.\d+)?)(%)?\s*,\s*(0|1|0?\.\d+|\d{1,3}%?)\s*\)$/i;

export const labRegex: RegExp =
/^lab\(\s*(\d+(\.\d+)?%?|none)\s+(-?\d+(\.\d+)?%?|none)\s+(-?\d+(\.\d+)?%?|none)(?:\s*\/\s*(none|0|1|0?\.\d+|[0-9]{1,3}%))?\s*\)$/;
Expand Down
33 changes: 16 additions & 17 deletions src/validations/isValidHsla.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,29 @@ export function isValidHsla(color: string): boolean {
const match = color.match(hslaRegex);
if (!match) return false;

const parseHue = (hue: string, unit: string | undefined): number => {
let hueValue = parseFloat(hue);
const h = parseFloat(match[1]);
const s = parseFloat(match[4]);
const l = parseFloat(match[7]);
const a = match[10].includes('%') ? parseFloat(match[10]) / 100 : parseFloat(match[10]);

const isValidHue = (value: number, unit: string | undefined): boolean => {
switch (unit) {
case 'deg':
return hueValue;
return value >= 0 && value <= 360;
case 'rad':
return hueValue * (180 / Math.PI);
return value >= 0 && value <= 2 * Math.PI;
case 'grad':
return hueValue * (9 / 10);
return value >= 0 && value <= 400;
case 'turn':
return hueValue * 360;
return value >= 0 && value <= 1;
default:
return hueValue;
return value >= 0 && value <= 360;
}
};

const h = parseHue(match[1], match[2]);
const s = parseFloat(match[3]);
const l = parseFloat(match[4]);
let a = match[5] === 'none' ? 1 : parseFloat(match[5]);

if (match[5].endsWith('%')) {
a = parseFloat(match[5]) / 100;
}

return h >= 0 && h <= 360 && s >= 0 && s <= 100 && l >= 0 && l <= 100 && a >= 0 && a <= 1;
return isValidHue(h, match[3]) &&
s >= 0 && s <= 100 &&
l >= 0 && l <= 100 &&
a >= 0 && a <= 1;
}

171 changes: 139 additions & 32 deletions tests/parser/parseHsla.test.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,145 @@
import { parseHsla } from "@/parser/parseHsla";

describe('parseHsla', () => {
test('parses valid HSLA colors correctly', () => {
expect(parseHsla('hsla(120, 100%, 50%, 0.5)')).toEqual([120, 100, 50, 0.5]);
expect(parseHsla('hsla(240, 50%, 50%, 1)')).toEqual([240, 50, 50, 1]);
expect(parseHsla('hsla(360, 100%, 100%, 0)')).toEqual([360, 100, 100, 0]);
expect(parseHsla('hsla(0, 0%, 0%, 0.25)')).toEqual([0, 0, 0, 0.25]);
});

test('parses HSLA colors with different units for hue correctly', () => {
expect(parseHsla('hsla(180deg, 100%, 50%, 0.5)')).toEqual([180, 100, 50, 0.5]);
expect(parseHsla('hsla(3.14rad, 100%, 50%, 0.5)')).toEqual([180, 100, 50, 0.5]);
expect(parseHsla('hsla(200grad, 100%, 50%, 0.5)')).toEqual([180, 100, 50, 0.5]);
expect(parseHsla('hsla(0.5turn, 100%, 50%, 0.5)')).toEqual([180, 100, 50, 0.5]);
});

test('parses HSLA colors with different alpha values correctly', () => {
expect(parseHsla('hsla(120, 100%, 50%, 0.75)')).toEqual([120, 100, 50, 0.75]);
expect(parseHsla('hsla(240, 50%, 50%, 50%)')).toEqual([240, 50, 50, 0.5]);
expect(parseHsla('hsla(360, 100%, 100%, none)')).toEqual([360, 100, 100, 1]);
expect(parseHsla('hsla(0, 0%, 0%, 0%)')).toEqual([0, 0, 0, 0]);
});

test('throws error for invalid HSLA colors', () => {
expect(() => parseHsla('hsla(120, 100, 50, 0.5)')).toThrow('Invalid HSLA color format');
expect(() => parseHsla('hsla(120, 100%, 50)')).toThrow('Invalid HSLA color format');
expect(() => parseHsla('hsla(120, 100, 50%, 0.5)')).toThrow('Invalid HSLA color format');
expect(() => parseHsla('hsla(120deg, 100%, 50, 0.5)')).toThrow('Invalid HSLA color format');
expect(() => parseHsla('hsla(120, 100%, 50%, 1.5)')).toThrow('Invalid HSLA color format');
expect(() => parseHsla('hsla(-10, 100%, 50%, 0.5)')).toThrow('Invalid HSLA color format');
test('parses HSLA color with degrees', () => {
const result = parseHsla('hsla(180deg, 100%, 50%, 0.5)');
expect(result).toEqual({
h: 180,
hUnit: 'deg',
hDeg: 180,
s: 100,
sUnit: '%',
l: 50,
lUnit: '%',
a: 0.5,
aUnit: undefined
});
});

test('parses HSLA color with radians', () => {
const result = parseHsla('hsla(3.14rad, 100%, 50%, 0.5)');
expect(result).toEqual({
h: 3.14,
hUnit: 'rad',
hDeg: 179.91,
s: 100,
sUnit: '%',
l: 50,
lUnit: '%',
a: 0.5,
aUnit: undefined
});
});

test('parses HSLA color with gradians', () => {
const result = parseHsla('hsla(200grad, 100%, 50%, 0.5)');
expect(result).toEqual({
h: 200,
hUnit: 'grad',
hDeg: 180,
s: 100,
sUnit: '%',
l: 50,
lUnit: '%',
a: 0.5,
aUnit: undefined
});
});

test('parses HSLA color with turns', () => {
const result = parseHsla('hsla(0.5turn, 100%, 50%, 0.5)');
expect(result).toEqual({
h: 0.5,
hUnit: 'turn',
hDeg: 180,
s: 100,
sUnit: '%',
l: 50,
lUnit: '%',
a: 0.5,
aUnit: undefined
});
});

test('parses HSLA color without unit (default to degrees)', () => {
const result = parseHsla('hsla(180, 100%, 50%, 0.5)');
expect(result).toEqual({
h: 180,
hUnit: undefined,
hDeg: 180,
s: 100,
sUnit: '%',
l: 50,
lUnit: '%',
a: 0.5,
aUnit: undefined
});
});

test('parses HSLA color with alpha percentage', () => {
const result = parseHsla('hsla(180, 100%, 50%, 50%)');
expect(result).toEqual({
h: 180,
hUnit: undefined,
hDeg: 180,
s: 100,
sUnit: '%',
l: 50,
lUnit: '%',
a: 0.5,
aUnit: '%'
});
});

test('parses HSLA color with mixed case units', () => {
const result = parseHsla('hsla(180DeG, 100%, 50%, 0.5)');
expect(result).toEqual({
h: 180,
hUnit: 'DeG',
hDeg: 180,
s: 100,
sUnit: '%',
l: 50,
lUnit: '%',
a: 0.5,
aUnit: undefined
});
});

test('throws error for invalid HSLA color', () => {
expect(() => parseHsla('hsla(370, 100%, 50%, 0.5)')).toThrow('Invalid HSLA color format');
expect(() => parseHsla('hsla(120, -10%, 50%, 0.5)')).toThrow('Invalid HSLA color format');
expect(() => parseHsla('hsla(120, 110%, 50%, 0.5)')).toThrow('Invalid HSLA color format');
expect(() => parseHsla('hsla(120, 100%, -10%, 0.5)')).toThrow('Invalid HSLA color format');
expect(() => parseHsla('hsla(120, 100%, 110%, 0.5)')).toThrow('Invalid HSLA color format');
expect(() => parseHsla('hsla(180, 110%, 50%, 0.5)')).toThrow('Invalid HSLA color format');
expect(() => parseHsla('hsla(180, 100%, 150%, 0.5)')).toThrow('Invalid HSLA color format');
expect(() => parseHsla('hsla(180, 100%, 50%, 1.5)')).toThrow('Invalid HSLA color format');
});

test('parses HSLA color with integer alpha', () => {
const result = parseHsla('hsla(180, 100%, 50%, 1)');
expect(result).toEqual({
h: 180,
hUnit: undefined,
hDeg: 180,
s: 100,
sUnit: '%',
l: 50,
lUnit: '%',
a: 1,
aUnit: undefined
});
});

test('parses HSLA color with alpha as 0%', () => {
const result = parseHsla('hsla(180, 100%, 50%, 0%)');
expect(result).toEqual({
h: 180,
hUnit: undefined,
hDeg: 180,
s: 100,
sUnit: '%',
l: 50,
lUnit: '%',
a: 0,
aUnit: '%'
});
});
});
52 changes: 27 additions & 25 deletions tests/validations/isValidHsla.test.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
import { isValidHsla } from '@/validations/isValidHsla';

describe('isValidHsla', () => {
test('validates HSLA colors correctly', () => {
expect(isValidHsla('hsla(120, 100%, 50%, 0.5)')).toBe(true);
expect(isValidHsla('hsla(240, 50%, 50%, 1)')).toBe(true);
expect(isValidHsla('hsla(360, 100%, 100%, 0)')).toBe(true);
expect(isValidHsla('hsla(0, 0%, 0%, 0.25)')).toBe(true);
});

test('validates HSLA colors with different units for hue correctly', () => {
test('valid HSLA colors', () => {
expect(isValidHsla('hsla(180deg, 100%, 50%, 0.5)')).toBe(true);
expect(isValidHsla('hsla(3.14rad, 100%, 50%, 0.5)')).toBe(true);
expect(isValidHsla('hsla(200grad, 100%, 50%, 0.5)')).toBe(true);
expect(isValidHsla('hsla(0.5turn, 100%, 50%, 0.5)')).toBe(true);
expect(isValidHsla('hsla(180, 100%, 50%, 0.5)')).toBe(true);
expect(isValidHsla('hsla(180, 100%, 50%, 50%)')).toBe(true);
expect(isValidHsla('hsla(180deg, 100%, 50%, 100%)')).toBe(true);
expect(isValidHsla('hsla(180deg, 100%, 50%, 0%)')).toBe(true);
});

test('invalid HSLA colors', () => {
expect(isValidHsla('hsla(370deg, 100%, 50%, 0.5)')).toBe(false);
expect(isValidHsla('hsla(180, 110%, 50%, 0.5)')).toBe(false);
expect(isValidHsla('hsla(180, 100%, 150%, 0.5)')).toBe(false);
expect(isValidHsla('hsla(180, 100%, 50%, 1.5)')).toBe(false);
expect(isValidHsla('hsla(180, 100%, 50%, -0.5)')).toBe(false);
});

test('valid HSLA colors with mixed case units', () => {
expect(isValidHsla('hsla(180DeG, 100%, 50%, 0.5)')).toBe(true);
expect(isValidHsla('hsla(3.14Rad, 100%, 50%, 0.5)')).toBe(true);
expect(isValidHsla('hsla(200GraD, 100%, 50%, 0.5)')).toBe(true);
expect(isValidHsla('hsla(0.5TuRn, 100%, 50%, 0.5)')).toBe(true);
});

test('validates HSLA colors with different alpha values correctly', () => {
expect(isValidHsla('hsla(120, 100%, 50%, 0.75)')).toBe(true);
expect(isValidHsla('hsla(240, 50%, 50%, 50%)')).toBe(true);
expect(isValidHsla('hsla(360, 100%, 100%, none)')).toBe(true);
expect(isValidHsla('hsla(0, 0%, 0%, 0%)')).toBe(true);
test('valid HSLA colors with integer alpha', () => {
expect(isValidHsla('hsla(180, 100%, 50%, 1)')).toBe(true);
expect(isValidHsla('hsla(180, 100%, 50%, 0)')).toBe(true);
});

test('invalidates incorrect HSLA colors', () => {
expect(isValidHsla('hsla(120, 100, 50, 0.5)')).toBe(false);
expect(isValidHsla('hsla(120, 100%, 50)')).toBe(false);
expect(isValidHsla('hsla(120, 100, 50%, 0.5)')).toBe(false);
expect(isValidHsla('hsla(120deg, 100%, 50, 0.5)')).toBe(false);
expect(isValidHsla('hsla(120, 100%, 50%, 1.5)')).toBe(false);
expect(isValidHsla('hsla(-10, 100%, 50%, 0.5)')).toBe(false);
expect(isValidHsla('hsla(370, 100%, 50%, 0.5)')).toBe(false);
expect(isValidHsla('hsla(120, -10%, 50%, 0.5)')).toBe(false);
expect(isValidHsla('hsla(120, 110%, 50%, 0.5)')).toBe(false);
expect(isValidHsla('hsla(120, 100%, -10%, 0.5)')).toBe(false);
expect(isValidHsla('hsla(120, 100%, 110%, 0.5)')).toBe(false);
test('valid HSLA colors with alpha as percentage', () => {
expect(isValidHsla('hsla(180, 100%, 50%, 50%)')).toBe(true);
expect(isValidHsla('hsla(180, 100%, 50%, 100%)')).toBe(true);
expect(isValidHsla('hsla(180, 100%, 50%, 0%)')).toBe(true);
});
});

0 comments on commit 7648724

Please sign in to comment.