Skip to content

Commit dd37dbd

Browse files
authored
fix: Color parsing supports hex strings without hash [135] (#138)
1 parent e8a24bf commit dd37dbd

2 files changed

Lines changed: 135 additions & 11 deletions

File tree

packages/core/src/colors.ts

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,39 @@
11
import type { vec3, vec4 } from '@alleninstitute/vis-geometry';
22
import { logger } from './logger';
33

4-
const RGB_COLOR_REGEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
5-
const RGBA_COLOR_REGEX = /^#([0-9a-fA-F]{4}|[0-9a-fA-F]{8})$/;
4+
// Tests for optional #, then 3 or 6 hex digits
5+
const RGB_COLOR_REGEX = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
6+
// Tests for optional #, then 4 or 8 hex digits
7+
const RGBA_COLOR_REGEX = /^#?([0-9a-fA-F]{4}|[0-9a-fA-F]{8})$/;
68

9+
/**
10+
* Converts a color hash string to a vec3 RGB color vector.
11+
*
12+
* @param colorHashStr A string representing a color in hex format, e.g., '#f00' or 'ff0000'.
13+
* @param normalized A boolean indicating whether to normalize the color values to the range [0, 1]. Defaults to true.
14+
* @returns A vec3 array representing the RGB color vector. If the input is invalid, returns [0, 0, 0].
15+
*/
716
export function makeRGBColorVector(colorHashStr: string, normalized = true): vec3 {
817
if (!colorHashStr || !RGB_COLOR_REGEX.test(colorHashStr)) {
918
logger.warn('invalid color hash string; returning black color vector (0, 0, 0)');
1019
return [0, 0, 0];
1120
}
21+
const hasHash = colorHashStr.charAt(0) === '#';
22+
const sanitizedColorHashStr = hasHash ? colorHashStr : `#${colorHashStr}`;
23+
1224
const redCode =
13-
colorHashStr.length === 4 ? colorHashStr.charAt(1) + colorHashStr.charAt(1) : colorHashStr.slice(1, 3);
25+
sanitizedColorHashStr.length === 4
26+
? sanitizedColorHashStr.charAt(1) + sanitizedColorHashStr.charAt(1)
27+
: sanitizedColorHashStr.slice(1, 3);
1428
const greenCode =
15-
colorHashStr.length === 4 ? colorHashStr.charAt(2) + colorHashStr.charAt(2) : colorHashStr.slice(3, 5);
29+
sanitizedColorHashStr.length === 4
30+
? sanitizedColorHashStr.charAt(2) + sanitizedColorHashStr.charAt(2)
31+
: sanitizedColorHashStr.slice(3, 5);
1632
const blueCode =
17-
colorHashStr.length === 4 ? colorHashStr.charAt(3) + colorHashStr.charAt(3) : colorHashStr.slice(5, 7);
33+
sanitizedColorHashStr.length === 4
34+
? sanitizedColorHashStr.charAt(3) + sanitizedColorHashStr.charAt(3)
35+
: sanitizedColorHashStr.slice(5, 7);
36+
1837
const divisor = normalized ? 255 : 1;
1938
return [
2039
Number.parseInt(redCode, 16) / divisor,
@@ -23,20 +42,39 @@ export function makeRGBColorVector(colorHashStr: string, normalized = true): vec
2342
];
2443
}
2544

45+
/**
46+
* Converts a color hash string to a vec4 RGBA color vector.
47+
*
48+
* @param colorHashStr A string representing a color in hex format, e.g., '#f00f' or 'ff0000ff'.
49+
* @param normalized A boolean indicating whether to normalize the color values to the range [0, 1]. Defaults to true.
50+
* @returns A vec3 array representing the RGB color vector. If the input is invalid, returns [0, 0, 0, 0].
51+
*/
2652
export function makeRGBAColorVector(colorHashStr: string, normalized = true): vec4 {
2753
if (!colorHashStr) {
2854
logger.warn('invalid color hash string; returning transparent black color vector (0, 0, 0, 0)');
2955
return [0, 0, 0, 0];
3056
}
57+
3158
if (RGBA_COLOR_REGEX.test(colorHashStr)) {
59+
const hashHash = colorHashStr.charAt(0) === '#';
60+
const sanitizedColorHashStr = hashHash ? colorHashStr : `#${colorHashStr}`;
61+
3262
const redCode =
33-
colorHashStr.length === 5 ? colorHashStr.charAt(1) + colorHashStr.charAt(1) : colorHashStr.slice(1, 3);
63+
sanitizedColorHashStr.length === 5
64+
? sanitizedColorHashStr.charAt(1) + sanitizedColorHashStr.charAt(1)
65+
: sanitizedColorHashStr.slice(1, 3);
3466
const greenCode =
35-
colorHashStr.length === 5 ? colorHashStr.charAt(2) + colorHashStr.charAt(2) : colorHashStr.slice(3, 5);
67+
sanitizedColorHashStr.length === 5
68+
? sanitizedColorHashStr.charAt(2) + sanitizedColorHashStr.charAt(2)
69+
: sanitizedColorHashStr.slice(3, 5);
3670
const blueCode =
37-
colorHashStr.length === 5 ? colorHashStr.charAt(3) + colorHashStr.charAt(3) : colorHashStr.slice(5, 7);
71+
sanitizedColorHashStr.length === 5
72+
? sanitizedColorHashStr.charAt(3) + sanitizedColorHashStr.charAt(3)
73+
: sanitizedColorHashStr.slice(5, 7);
3874
const alphaCode =
39-
colorHashStr.length === 5 ? colorHashStr.charAt(4) + colorHashStr.charAt(4) : colorHashStr.slice(7, 9);
75+
sanitizedColorHashStr.length === 5
76+
? sanitizedColorHashStr.charAt(4) + sanitizedColorHashStr.charAt(4)
77+
: sanitizedColorHashStr.slice(7, 9);
4078
const divisor = normalized ? 255 : 1;
4179
return [
4280
Number.parseInt(redCode, 16) / divisor,
@@ -46,8 +84,8 @@ export function makeRGBAColorVector(colorHashStr: string, normalized = true): ve
4684
];
4785
}
4886
if (RGB_COLOR_REGEX.test(colorHashStr)) {
49-
const rgb = makeRGBColorVector(colorHashStr);
50-
return [rgb[0], rgb[1], rgb[2], normalized ? 1.0 : 255.0];
87+
const rgb = makeRGBColorVector(colorHashStr, normalized);
88+
return [...rgb, normalized ? 1.0 : 255.0];
5189
}
5290
logger.warn('invalid color hash string; returning transparent black color vector (0, 0, 0, 0)');
5391
return [0, 0, 0, 0];
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { makeRGBColorVector, makeRGBAColorVector } from '../colors';
3+
import { logger } from '../logger';
4+
5+
// Mock the logger to verify warning logs are emitted
6+
vi.mock('../logger', () => ({
7+
logger: {
8+
warn: vi.fn(),
9+
},
10+
}));
11+
12+
describe('makeRGBColorVector', () => {
13+
it('should return a black for invalid input and log warning', () => {
14+
const result = makeRGBColorVector('this is not a color');
15+
expect(result).toEqual([0, 0, 0]);
16+
expect(logger.warn).toHaveBeenCalled();
17+
});
18+
19+
it('should handle 3-character RGB without a hash', () => {
20+
const result = makeRGBColorVector('f00');
21+
expect(result).toEqual([1, 0, 0]);
22+
});
23+
24+
it('should handle 3-character RGB with a hash', () => {
25+
const result = makeRGBColorVector('#f00');
26+
expect(result).toEqual([1, 0, 0]);
27+
});
28+
29+
it('should handle 6-character RGB without a hash', () => {
30+
const result = makeRGBColorVector('ff0000');
31+
expect(result).toEqual([1, 0, 0]);
32+
});
33+
34+
it('should handle 6-character RGB with a hash', () => {
35+
const result = makeRGBColorVector('#ff0000');
36+
expect(result).toEqual([1, 0, 0]);
37+
});
38+
39+
it('should return non-normalized values when normalize is false', () => {
40+
const result = makeRGBColorVector('#ff0000', false);
41+
expect(result).toEqual([255, 0, 0]);
42+
});
43+
});
44+
45+
describe('makeRGBAColorVector', () => {
46+
it('should return a transparent black for invalid input and log warning', () => {
47+
const result = makeRGBAColorVector('this is not a color');
48+
expect(result).toEqual([0, 0, 0, 0]);
49+
expect(logger.warn).toHaveBeenCalled();
50+
});
51+
52+
it('should handle 4-character RGBA without a hash', () => {
53+
const result = makeRGBAColorVector('f00f');
54+
expect(result).toEqual([1, 0, 0, 1]);
55+
});
56+
57+
it('should handle 4-character RGBA with a hash', () => {
58+
const result = makeRGBAColorVector('#f00f');
59+
expect(result).toEqual([1, 0, 0, 1]);
60+
});
61+
62+
it('should handle 8-character RGBA without a hash', () => {
63+
const result = makeRGBAColorVector('ff0000ff');
64+
expect(result).toEqual([1, 0, 0, 1]);
65+
});
66+
67+
it('should handle 8-character RGBA with a hash', () => {
68+
const result = makeRGBAColorVector('#ff0000ff');
69+
expect(result).toEqual([1, 0, 0, 1]);
70+
});
71+
72+
it('should handle RGB input and add alpha channel', () => {
73+
const result = makeRGBAColorVector('#ff0000');
74+
expect(result).toEqual([1, 0, 0, 1]);
75+
});
76+
77+
it('should return non-normalized values when normalize is false', () => {
78+
const result = makeRGBAColorVector('#ff0000ff', false);
79+
expect(result).toEqual([255, 0, 0, 255]);
80+
});
81+
82+
it('should handle RGB input and add non-normalized alpha channel when normalize is false', () => {
83+
const result = makeRGBAColorVector('#ff0000', false);
84+
expect(result).toEqual([255, 0, 0, 255]);
85+
});
86+
});

0 commit comments

Comments
 (0)