-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Start implementation of own color engine
- Loading branch information
1 parent
4c6d9a0
commit 0bac8a8
Showing
6 changed files
with
364 additions
and
0 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,83 @@ | ||
import { isHslString, matchHslString } from '../hslString' | ||
|
||
describe('isHslString', () => { | ||
const valid = [ | ||
'hsl(240, 100%, 50%)', // comma separated | ||
'hsl(240, 100%, 50%, 0.1)', // comma separated with opacity | ||
'hsl(240, 100%, 50%, 10%)', // comma separated with % opacity | ||
'hsl(240,100%,50%,0.1)', // comma separated without spaces | ||
'hsl(180deg, 100%, 50%, 0.1)', // hue with 'deg' | ||
'hsl(3.14rad, 100%, 50%, 0.1)', // hue with 'rad' | ||
'hsl(200grad, 100%, 50%, 0.1)', // hue with 'grad' | ||
'hsl(0.5turn, 100%, 50%, 0.1)', // hue with 'turn' | ||
'hsl(-240, -100%, -50%, -0.1)', // negative values | ||
'hsl(+240, +100%, +50%, +0.1)', // explicit positive sign | ||
'hsl(240.5, 99.99%, 49.999%, 0.9999)', // non-integer values | ||
'hsl(.9, .99%, .999%, .9999)', // fraction w/o leading zero | ||
'hsl(0240, 0100%, 0050%, 01)', // leading zeros | ||
'hsl(240.0, 100.00%, 50.000%, 1.0000)', // trailing decimal zeros | ||
'hsl(2400, 1000%, 1000%, 10)', // out of range values | ||
'hsl(-2400.01deg, -1000.5%, -1000.05%, -100)', // combination of above | ||
'hsl(2.40e+2, 1.00e+2%, 5.00e+1%, 1E-3)', // scientific notation | ||
'hsl(240 100% 50%)', // space separated (CSS Color Level 4) | ||
'hsl(240 100% 50% / 0.1)', // space separated with opacity | ||
'hsla(240, 100%, 50%)', // hsla() alias | ||
'hsla(240, 100%, 50%, 0.1)', // hsla() with opacity | ||
'HSL(240Deg, 100%, 50%)', // case insensitive | ||
] | ||
valid.forEach(str => { | ||
it(`returns true for valid hsl string: ${str}`, () => { | ||
expect(isHslString(str)).toBe(true) | ||
}) | ||
}) | ||
|
||
const invalid = [ | ||
'rgb(127, 255, 255)', // different color model | ||
'#88FFFF', // hex string | ||
'blue', // color name | ||
'hsl(240, 1, 0.5, 0.1)', // missing % sign | ||
' hsl(240, 100%, 50%, 0.1) ', // untrimmed spaces | ||
'hsl(240 100% 50% 0.1)', // missing slash in space separated alpha | ||
'rainbow', // invalid color string | ||
] | ||
invalid.forEach(str => { | ||
it(`returns false for invalid hsl string: ${str}`, () => { | ||
expect(isHslString(str)).toBe(false) | ||
}) | ||
}) | ||
}) | ||
|
||
describe('matchHslString', () => { | ||
it('returns HSL and alpha (opacity) values in an array', () => { | ||
const str = 'hsl(240, 100%, 50%, 0.1)' | ||
expect(matchHslString(str)).toEqual(['240', '100', '50', '0.1']) | ||
}) | ||
|
||
it('captures percentage opacity', () => { | ||
const str = 'hsl(240, 100%, 50%, 10%)' | ||
expect(matchHslString(str)).toEqual(['240', '100', '50', '10%']) | ||
}) | ||
|
||
const hueWithUnit = ['180deg', '3.14rad', '200grad', '0.5turn'] | ||
hueWithUnit.forEach(hue => { | ||
it(`captures hue angle with unit: ${hue}`, () => { | ||
const str = `hsl(${hue} 100% 50% / 0.1)` | ||
expect(matchHslString(str)).toEqual([hue, '100', '50', '0.1']) | ||
}) | ||
}) | ||
|
||
it('captures negative values', () => { | ||
const str = 'hsl(-240, -100%, -50%, -0.1)' | ||
expect(matchHslString(str)).toEqual(['-240', '-100', '-50', '-0.1']) | ||
}) | ||
|
||
it('returns alpha value as undefined if not specified in string', () => { | ||
const str = 'hsl(240, 100%, 50%)' | ||
expect(matchHslString(str)).toEqual(['240', '100', '50', undefined]) | ||
}) | ||
|
||
it('returns null if string is not a valid hsl string', () => { | ||
const str = 'rgb(127, 255, 64)' | ||
expect(matchHslString(str)).toBeNull() | ||
}) | ||
}) |
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,72 @@ | ||
import { isRgbString, matchRgbString } from '../rgbString' | ||
|
||
describe('isRgbString', () => { | ||
const valid = [ | ||
'rgb(127, 255, 64)', // comma separated | ||
'rgb(127, 255, 64, 0.1)', // comma separated with opacity | ||
'rgb(127, 255, 64, 10%)', // % opacity | ||
'rgb(50%, 100%, 25%, 0.1)', // % values | ||
'rgb(240,255,64,0.1)', // comma separated without spaces | ||
'rgb(320, 255.5, -64, 10)', // out of range values | ||
'rgb(127 255 64)', // space separated (CSS Color Level 4) | ||
'rgb(127 255 64 / 0.1)', // space separated with opacity | ||
'rgb(127 255 64 / 10%)', // space separated % opacity | ||
'rgb(50% 100% 25% / 0.1)', // space separated % values | ||
'rgb(127 255 64/0.1)', // no spaces around slash | ||
'rgba(127, 255, 64, 0.1)', // rgba() alias | ||
'rgba(127, 255, 64)', // rgba() without opacity | ||
'rgba(127 255 64 / 0.1)', // rgba() space separated | ||
'RGB(127, 255, 64)', // case insensitive | ||
] | ||
valid.forEach(str => { | ||
it(`returns true for valid rgb string: ${str}`, () => { | ||
expect(isRgbString(str)).toBe(true) | ||
}) | ||
}) | ||
|
||
const invalid = [ | ||
'hsl(240, 100%, 50%)', // different color model | ||
'#88FFFF', // hex string | ||
'blue', // color name | ||
' rgb(127, 255, 64, 0.1) ', // untrimmed spaces | ||
'rgb(127 255 64 0.1)', // missing slash in space separated alpha | ||
'rainbow', // invalid color string | ||
] | ||
invalid.forEach(str => { | ||
it(`returns false for invalid rgb string: ${str}`, () => { | ||
expect(isRgbString(str)).toBe(false) | ||
}) | ||
}) | ||
}) | ||
|
||
describe('matchRgbString', () => { | ||
it('returns RGB and alpha (opacity) values in an array', () => { | ||
const str = 'rgb(127, 255, 64, 0.1)' | ||
expect(matchRgbString(str)).toEqual(['127', '255', '64', '0.1']) | ||
}) | ||
|
||
it('captures percentage opacity', () => { | ||
const str = 'rgb(127, 255, 64, 100%)' | ||
expect(matchRgbString(str)).toEqual(['127', '255', '64', '100%']) | ||
}) | ||
|
||
it('captures percentage values', () => { | ||
const str = 'rgb(50%, 100%, 25%, 0.1)' | ||
expect(matchRgbString(str)).toEqual(['50%', '100%', '25%', '0.1']) | ||
}) | ||
|
||
it('captures negative values', () => { | ||
const str = 'rgb(-127, -255, -64, -0.1)' | ||
expect(matchRgbString(str)).toEqual(['-127', '-255', '-64', '-0.1']) | ||
}) | ||
|
||
it('returns alpha value as undefined if not specified in string', () => { | ||
const str = 'rgb(127, 255, 64)' | ||
expect(matchRgbString(str)).toEqual(['127', '255', '64', undefined]) | ||
}) | ||
|
||
it('returns null if string is not a valid rgb string', () => { | ||
const str = 'hsl(240 100% 50%)' | ||
expect(matchRgbString(str)).toBeNull() | ||
}) | ||
}) |
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,94 @@ | ||
import { | ||
alphaSeparatorMatcher, | ||
cssNumberMatcher, | ||
exact, | ||
separatorMatcher, | ||
} from '../utils' | ||
|
||
describe('exact', () => { | ||
it('creates a new regex that is restricted to exact matches only', () => { | ||
const regex = /[0-9]+/ | ||
const exactRegex = exact(regex) | ||
expect(regex.test(' 123 ')).toBe(true) | ||
expect(exactRegex.test(' 123 ')).toBe(false) | ||
expect(exactRegex.test('123')).toBe(true) | ||
}) | ||
|
||
it('retains the original flags (if any)', () => { | ||
const regex = /[a-z]+/i | ||
expect(exact(regex).test('Abc')).toBe(true) | ||
}) | ||
}) | ||
|
||
describe('cssNumberMatcher', () => { | ||
const matcher = exact(cssNumberMatcher) | ||
|
||
const valid = [ | ||
'255', // integer | ||
'4.5', // non-integer | ||
'0.1', // fraction | ||
'.1', // fraction with no leading zero | ||
'007', // leading zeros | ||
'1.000', // trailing decimal zeros | ||
'-255', // negative | ||
'+255', // explicit positive sign | ||
'1.28e+2', // scientific notation | ||
'1.28E-2', // uppercase scientific notation | ||
'-01.2800e+02', // combination of above | ||
] | ||
valid.forEach(str => { | ||
it(`tests true for exact match with valid CSS number string: ${str}`, () => { | ||
expect(matcher.test(str)).toBe(true) | ||
}) | ||
}) | ||
|
||
const invalid = [ | ||
'1,000', // comma | ||
'1.', // missing digit following decimal point | ||
'1.0.0', // excess decimal points | ||
'1_000', // numeric separator | ||
'FF', // hexadecimal | ||
'foo', // totally not a number | ||
] | ||
invalid.forEach(str => { | ||
it(`tests false for exact match with invalid CSS number string: ${str}`, () => { | ||
expect(matcher.test(str)).toBe(false) | ||
}) | ||
}) | ||
}) | ||
|
||
describe('separatorMatcher', () => { | ||
const matcher = exact(separatorMatcher) | ||
|
||
const valid = [',', ' ,', ', ', ' , ', ' '] | ||
valid.forEach(str => { | ||
it(`tests true for exact match with valid separator: '${str}'`, () => { | ||
expect(matcher.test(str)).toBe(true) | ||
}) | ||
}) | ||
|
||
const invalid = [',,', ', ,', ', , ', '/'] | ||
invalid.forEach(str => { | ||
it(`tests false for exact match with invalid separator: '${str}'`, () => { | ||
expect(matcher.test(str)).toBe(false) | ||
}) | ||
}) | ||
}) | ||
|
||
describe('alphaSeparatorMatcher', () => { | ||
const matcher = exact(alphaSeparatorMatcher) | ||
|
||
const valid = [',', ', ', ' ,', ' , ', '/', '/ ', ' /', ' / '] | ||
valid.forEach(str => { | ||
it(`tests true for exact match with valid separator: '${str}'`, () => { | ||
expect(matcher.test(str)).toBe(true) | ||
}) | ||
}) | ||
|
||
const invalid = [',,', ', ,', ', , ', ',/', ', /', ',/ ', ', / '] | ||
invalid.forEach(str => { | ||
it(`tests false for exact match with invalid separator: '${str}'`, () => { | ||
expect(matcher.test(str)).toBe(false) | ||
}) | ||
}) | ||
}) |
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,49 @@ | ||
import { | ||
alphaSeparatorMatcher, | ||
cssNumberMatcher, | ||
exact, | ||
separatorMatcher, | ||
} from './utils' | ||
|
||
export type HslString = `hsl(${string})` | `hsla(${string})` | ||
|
||
const num = cssNumberMatcher.source | ||
const sep = separatorMatcher.source | ||
const asep = alphaSeparatorMatcher.source | ||
|
||
/** | ||
* Regular expression for HSL color string | ||
* | ||
* The pattern is less strict than actual CSS, mainly for | ||
* performance reasons. Notably, it does NOT impose | ||
* consistent separator (comma vs. space). | ||
*/ | ||
export const hslMatcher = new RegExp( | ||
`hsla?\\(\\s*(${num}(?:deg|rad|grad|turn)?)${sep}(${num})%${sep}(${num})%(?:${asep}(${num}%?))?\\s*\\)`, | ||
'i', | ||
) | ||
|
||
/** | ||
* Checks if a given string is a valid HSL color string | ||
* | ||
* @param colorString | ||
* @returns true/false (type predicate for `HslString` in TS) | ||
*/ | ||
export function isHslString(colorString: string): colorString is HslString { | ||
return exact(hslMatcher).test(colorString) | ||
} | ||
|
||
/** | ||
* Attempts to match the given color string with the HSL | ||
* string pattern, and extracts the color components | ||
* | ||
* Since the standard unit for S and L values is percent, | ||
* the % sign is not included in the captured values. | ||
* | ||
* @param colorString | ||
* @returns an array containing the matched HSL values, or `null` | ||
*/ | ||
export function matchHslString(colorString: string): string[] | null { | ||
const match = exact(hslMatcher).exec(colorString) | ||
return match?.slice(1) ?? null | ||
} |
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,47 @@ | ||
import { | ||
alphaSeparatorMatcher, | ||
cssNumberMatcher, | ||
exact, | ||
separatorMatcher, | ||
} from './utils' | ||
|
||
export type RgbString = `rgb(${string})` | `rgba(${string})` | ||
|
||
const num = cssNumberMatcher.source | ||
const sep = separatorMatcher.source | ||
const asep = alphaSeparatorMatcher.source | ||
|
||
/** | ||
* Regular expression for RGB color string | ||
* | ||
* The pattern is less strict than actual CSS, mainly for | ||
* performance reasons. Notably, it does NOT impose: | ||
* - consistent separator (comma vs. space) | ||
* - consistent unit of color components (number value vs. percentage) | ||
*/ | ||
export const rgbMatcher = new RegExp( | ||
`rgba?\\(\\s*(${num}%?)${sep}(${num}%?)${sep}(${num}%?)(?:${asep}(${num}%?))?\\s*\\)`, | ||
'i', | ||
) | ||
|
||
/** | ||
* Checks if a given string is a valid RGB color string | ||
* | ||
* @param colorString | ||
* @returns true/false (type predicate for `RgbString` in TS) | ||
*/ | ||
export function isRgbString(colorString: string): colorString is RgbString { | ||
return exact(rgbMatcher).test(colorString) | ||
} | ||
|
||
/** | ||
* Attempts to match the given color string with the RGB | ||
* string pattern, and extracts the color components | ||
* | ||
* @param colorString | ||
* @returns an array containing the matched RGB values, or `null` | ||
*/ | ||
export function matchRgbString(colorString: string): string[] | null { | ||
const match = exact(rgbMatcher).exec(colorString) | ||
return match?.slice(1) ?? null | ||
} |
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,19 @@ | ||
/** | ||
* Creates a modified version of the regular expression | ||
* that is restricted to exact matches only | ||
* | ||
* @param regex | ||
* @returns modified regex including original flags (if any) | ||
*/ | ||
export function exact(regex: RegExp): RegExp { | ||
return new RegExp(`^${regex.source}$`, regex.flags) | ||
} | ||
|
||
/** Regular expression for valid CSS number */ | ||
export const cssNumberMatcher = /[+-]?(?=\.\d|\d)\d*(?:\.\d+)?(?:[eE][+-]?\d+)?/ | ||
|
||
/** Regular expression for color component separator */ | ||
export const separatorMatcher = /(?=[,\s])\s*(?:,\s*)?/ | ||
|
||
/** Regular expression for alpha separator */ | ||
export const alphaSeparatorMatcher = /\s*[,\/]\s*/ |