Repository: tradingview/lightweight-charts (ts)

Issue:
Add support for 'transparent' color
**Lightweight Charts Version:** 3.0.1

**Steps/code to reproduce:**
```
var volumeSeries = chart.addHistogramSeries({
		color: 'rgba(76, 175, 80, 0.5)',
		priceFormat: {
			type: 'volume',
		},
		priceLineVisible: false,

	overlay: true,
        color: 'transparent',
	scaleMargins: {
		top: 0.85,
		bottom: 0,
	},
});
```

It will throw error in console that color 'transparent' is not valid. Is it possible to support this in new release?



Pull request description:
Added support for transparent color name
**Type of PR:** bugfix

**PR checklist:**

- [x] Addresses an existing issue: fixes #491
- [x] Includes tests
- [ ] Documentation update


Patch:
diff --git a/src/helpers/color.ts b/src/helpers/color.ts
index 5517ed21b..c913d37f0 100644
--- a/src/helpers/color.ts
+++ b/src/helpers/color.ts
@@ -5,30 +5,23 @@ import { isNaN } from './strict-type-checks';
  * Red component of the RGB color value
  * The valid values are integers in range [0, 255]
  */
-export type RedComponent = Nominal<number, 'RedComponent'>;
+type RedComponent = Nominal<number, 'RedComponent'>;
 
 /**
  * Green component of the RGB color value
  * The valid values are integers in range [0, 255]
  */
-export type GreenComponent = Nominal<number, 'GreenComponent'>;
+type GreenComponent = Nominal<number, 'GreenComponent'>;
 
 /**
  * Blue component of the RGB color value
  * The valid values are integers in range [0, 255]
  */
-export type BlueComponent = Nominal<number, 'BlueComponent'>;
+type BlueComponent = Nominal<number, 'BlueComponent'>;
 
-/**
- * Alpha component of the RGBA color value
- * The valid values are integers in range [0, 1]
- */
-type AlphaComponent = Nominal<number, 'AlphaComponent'>;
+type Rgb = [RedComponent, GreenComponent, BlueComponent];
 
-export type Rgb = [RedComponent, GreenComponent, BlueComponent];
-type Rgba = [RedComponent, GreenComponent, BlueComponent, AlphaComponent];
-
-/** @public */
+/** @public see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value */
 const namedColorRgbHexStrings: Record<string, string> = {
 	aliceblue: '#f0f8ff',
 	antiquewhite: '#faebd7',
@@ -184,32 +177,20 @@ function normalizeInteger(min: number, n: number, max: number): number {
 	);
 }
 
-function normalizeNumber(min: number, n: number, max: number): number {
-	return (
-		isNaN(n) ? min :
-			n < min ? min :
-			n > max ? max :
-			// limit the precision of all numbers to at most 4 digits in fractional part
-			Math.round(n * 10000) / 10000
-	);
-}
-
 function normalizeRgbComponent<T extends RedComponent | GreenComponent | BlueComponent>(component: number): T {
 	return normalizeInteger(0, component, 255) as T;
 }
 
-function normalizeAlphaComponent(alpha: number): AlphaComponent {
-	return normalizeNumber(0, alpha, 1) as AlphaComponent;
-}
-
 namespace RgbShortHexRepresentation {
 	/**
 	 * @example
 	 * #fb0
 	 * @example
 	 * #f0f
+	 * @example
+	 * #f0fa
 	 */
-	export const re = /^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/;
+	export const re = /^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])?$/;
 	export function parse(matches: RegExpExecArray): Rgb {
 		return [
 			normalizeRgbComponent<RedComponent>(parseInt(matches[1] + matches[1], 16)),
@@ -219,19 +200,16 @@ namespace RgbShortHexRepresentation {
 	}
 }
 
-function tryParseRgbShortHexString(rgbShortHexString: string): Rgb | null {
-	const matches = RgbShortHexRepresentation.re.exec(rgbShortHexString);
-	return matches !== null ? RgbShortHexRepresentation.parse(matches) : null;
-}
-
 namespace RgbHexRepresentation {
 	/**
 	 * @example
 	 * #00ff00
 	 * @example
 	 * #336699
+	 * @example
+	 * #336699FA
 	 */
-	export const re = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/;
+	export const re = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})?$/;
 	export function parse(matches: RegExpExecArray): Rgb {
 		return [
 			normalizeRgbComponent<RedComponent>(parseInt(matches[1], 16)),
@@ -248,52 +226,46 @@ namespace RgbRepresentation {
 	 * @example
 	 * rgb(255,234,245)
 	 */
-	export const re = /^rgb\(\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*\)$/;
-	export function parse(matches: RegExpExecArray): Rgb {
-		return [
-			normalizeRgbComponent<RedComponent>(parseInt(matches[1], 10)),
-			normalizeRgbComponent<GreenComponent>(parseInt(matches[2], 10)),
-			normalizeRgbComponent<BlueComponent>(parseInt(matches[3], 10)),
-		];
-	}
-}
+	export const rgbRe = /^rgb\(\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*\)$/;
 
-namespace RgbaRepresentation {
 	/**
 	 * @example
 	 * rgba(123, 234, 45, 1)
 	 * @example
 	 * rgba(255,234,245,0.1)
 	 */
-	export const re = /^rgba\(\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*,\s*(-?[\d]{0,10}(?:\.\d+)?)\s*\)$/;
-	export function parse(matches: RegExpExecArray): Rgba {
+	export const rgbaRe = /^rgba\(\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*,\s*(-?[\d]{0,10}(?:\.\d+)?)\s*\)$/;
+	export function parse(matches: RegExpExecArray): Rgb {
 		return [
 			normalizeRgbComponent<RedComponent>(parseInt(matches[1], 10)),
 			normalizeRgbComponent<GreenComponent>(parseInt(matches[2], 10)),
 			normalizeRgbComponent<BlueComponent>(parseInt(matches[3], 10)),
-			normalizeAlphaComponent(parseFloat(matches[4])),
 		];
 	}
 }
 
+function tryParseRgbShortHexString(rgbShortHexString: string): Rgb | null {
+	const matches = RgbShortHexRepresentation.re.exec(rgbShortHexString);
+	return matches !== null ? RgbShortHexRepresentation.parse(matches) : null;
+}
+
 function tryParseRgbHexString(rgbHexString: string): Rgb | null {
 	const matches = RgbHexRepresentation.re.exec(rgbHexString);
 	return matches !== null ? RgbHexRepresentation.parse(matches) : null;
 }
 
-function tryParseRgbString(rgbString: string): Rgb | null {
-	const matches = RgbRepresentation.re.exec(rgbString);
+function tryParseRgbString(colorString: string): Rgb | null {
+	const matches = RgbRepresentation.rgbaRe.exec(colorString) || RgbRepresentation.rgbRe.exec(colorString);
 	return matches !== null ? RgbRepresentation.parse(matches) : null;
 }
 
-function tryParseRgbaString(rgbaString: string): Rgba | null {
-	const matches = RgbaRepresentation.re.exec(rgbaString);
-	return matches !== null ? RgbaRepresentation.parse(matches) : null;
-}
-
-function tryParseRgb(colorString: string): Rgb | null {
+function colorStringToRgb(colorString: string): Rgb {
 	colorString = colorString.toLowerCase();
 
+	if (colorString === 'transparent') {
+		return [0 as RedComponent, 0 as GreenComponent, 0 as BlueComponent];
+	}
+
 	if (colorString in namedColorRgbHexStrings) {
 		colorString = namedColorRgbHexStrings[colorString];
 	}
@@ -313,22 +285,7 @@ function tryParseRgb(colorString: string): Rgb | null {
 		return rgbShortHexParseResult;
 	}
 
-	const rgbaParseResult = tryParseRgbaString(colorString);
-	if (rgbaParseResult !== null) {
-		return [rgbaParseResult[0], rgbaParseResult[1], rgbaParseResult[2]];
-	}
-
-	return null;
-}
-
-export function parseRgb(colorString: string): Rgb {
-	const parseResult = tryParseRgb(colorString);
-
-	if (parseResult !== null) {
-		return parseResult;
-	} else {
-		throw new Error(`Passed color string ${colorString} does not match any of the known color representations`);
-	}
+	throw new Error(`Cannot parse color: ${colorString}`);
 }
 
 function rgbToGrayscale(rgbValue: Rgb): number {
@@ -345,44 +302,16 @@ function rgbToGrayscale(rgbValue: Rgb): number {
 	);
 }
 
-export function rgbToBlackWhiteString(rgbValue: Rgb, threshold: number): 'black' | 'white' {
-	if (threshold < 0 || threshold > 255) {
-		throw new Error('invalid threshold value, valid values are [0, 255]');
-	}
-
-	return rgbToGrayscale(rgbValue) >= threshold ? 'white' : 'black';
-}
-
-function rgba(rgb: Rgb, alpha: number): Rgba {
-	return [
-		rgb[0],
-		rgb[1],
-		rgb[2],
-		normalizeAlphaComponent(alpha),
-	];
-}
-
-function rgbaToString(rgbaValue: Rgba): string {
-	return `rgba(${rgbaValue[0]}, ${rgbaValue[1]}, ${rgbaValue[2]}, ${rgbaValue[3]})`;
+export interface ContrastColors {
+	foreground: string;
+	background: string;
 }
 
-export function resetTransparency(color: string): string {
-	if (isHexColor(color)) {
-		return color;
-	}
-
-	return rgbaToString(rgba(parseRgb(color), 1));
-}
-
-export function colorWithTransparency(color: string, transparency: number): string {
-	return rgbaToString(rgba(parseRgb(color), transparency));
-}
-
-function isHexColor(color: string): boolean {
-	return color.indexOf('#') === 0;
-}
+export function generateContrastColors(backgroundColor: string): ContrastColors {
+	const rgb = colorStringToRgb(backgroundColor);
 
-export function generateTextColor(color: string): string {
-	const backColorBW = rgbToBlackWhiteString(parseRgb(color), 160);
-	return backColorBW === 'black' ? 'white' : 'black';
+	return {
+		background: `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`,
+		foreground: rgbToGrayscale(rgb) > 160 ? 'black' : 'white',
+	};
 }
diff --git a/src/renderers/price-axis-view-renderer.ts b/src/renderers/price-axis-view-renderer.ts
index 83689c819..115dbc627 100644
--- a/src/renderers/price-axis-view-renderer.ts
+++ b/src/renderers/price-axis-view-renderer.ts
@@ -1,5 +1,4 @@
 import { drawScaled } from '../helpers/canvas-helpers';
-import { resetTransparency } from '../helpers/color';
 
 import { TextWidthCache } from '../model/text-width-cache';
 
@@ -69,7 +68,7 @@ export class PriceAxisViewRenderer implements IPriceAxisViewRenderer {
 		let xTick: number;
 		let xText: number;
 
-		ctx.fillStyle = resetTransparency(this._commonData.background);
+		ctx.fillStyle = this._commonData.background;
 		ctx.lineWidth = 1;
 		ctx.lineCap = 'butt';
 
diff --git a/src/views/price-axis/crosshair-price-axis-view.ts b/src/views/price-axis/crosshair-price-axis-view.ts
index 0da4b3ac2..30514aa8e 100644
--- a/src/views/price-axis/crosshair-price-axis-view.ts
+++ b/src/views/price-axis/crosshair-price-axis-view.ts
@@ -1,4 +1,4 @@
-import { generateTextColor } from '../../helpers/color';
+import { generateContrastColors } from '../../helpers/color';
 
 import { Crosshair, CrosshairPriceAndCoordinate } from '../../model/crosshair';
 import { PriceScale } from '../../model/price-scale';
@@ -36,8 +36,9 @@ export class CrosshairPriceAxisView extends PriceAxisView {
 			return;
 		}
 
-		commonRendererData.background = options.labelBackgroundColor;
-		commonRendererData.color = generateTextColor(options.labelBackgroundColor);
+		const colors = generateContrastColors(options.labelBackgroundColor);
+		commonRendererData.background = colors.background;
+		commonRendererData.color = colors.foreground;
 
 		const value = this._valueProvider(this._priceScale);
 		commonRendererData.coordinate = value.coordinate;
diff --git a/src/views/price-axis/custom-price-line-price-axis-view.ts b/src/views/price-axis/custom-price-line-price-axis-view.ts
index 6e60bd579..a58530676 100644
--- a/src/views/price-axis/custom-price-line-price-axis-view.ts
+++ b/src/views/price-axis/custom-price-line-price-axis-view.ts
@@ -1,4 +1,4 @@
-import { generateTextColor } from '../../helpers/color';
+import { generateContrastColors } from '../../helpers/color';
 
 import { CustomPriceLine } from '../../model/custom-price-line';
 import { Series } from '../../model/series';
@@ -42,8 +42,9 @@ export class CustomPriceLinePriceAxisView extends PriceAxisView {
 		axisRendererData.text = this._series.priceScale().formatPriceAbsolute(options.price);
 		axisRendererData.visible = true;
 
-		commonData.background = options.color;
-		commonData.color = generateTextColor(options.color);
+		const colors = generateContrastColors(options.color);
+		commonData.background = colors.background;
+		commonData.color = colors.foreground;
 		commonData.coordinate = y;
 	}
 }
diff --git a/src/views/price-axis/price-axis-view.ts b/src/views/price-axis/price-axis-view.ts
index 39a2a338a..804ec5482 100644
--- a/src/views/price-axis/price-axis-view.ts
+++ b/src/views/price-axis/price-axis-view.ts
@@ -1,5 +1,3 @@
-import { generateTextColor } from '../../helpers/color';
-
 import { PriceScale } from '../../model/price-scale';
 import {
 	IPriceAxisViewRenderer,
@@ -48,14 +46,6 @@ export abstract class PriceAxisView implements IPriceAxisView {
 		return this._axisRendererData.text;
 	}
 
-	public background(): string {
-		return this._commonRendererData.background;
-	}
-
-	public color(): string {
-		return generateTextColor(this.background());
-	}
-
 	public coordinate(): number {
 		this._updateRendererDataIfNeeded();
 		return this._commonRendererData.coordinate;
diff --git a/src/views/price-axis/series-price-axis-view.ts b/src/views/price-axis/series-price-axis-view.ts
index d6b1356b5..52fd0f732 100644
--- a/src/views/price-axis/series-price-axis-view.ts
+++ b/src/views/price-axis/series-price-axis-view.ts
@@ -1,4 +1,4 @@
-import { generateTextColor } from '../../helpers/color';
+import { generateContrastColors } from '../../helpers/color';
 
 import { LastValueDataResultWithData, Series } from '../../model/series';
 import { PriceAxisLastValueMode } from '../../model/series-options';
@@ -44,11 +44,14 @@ export class SeriesPriceAxisView extends PriceAxisView {
 			paneRendererData.visible = paneRendererData.text.length > 0;
 		}
 
-		commonRendererData.background = this._source.priceLineColor(lastValueData.color);
-		commonRendererData.color = generateTextColor(commonRendererData.background);
+		const lastValueColor = this._source.priceLineColor(lastValueData.color);
+		const colors = generateContrastColors(lastValueColor);
+
+		commonRendererData.background = colors.background;
+		commonRendererData.color = colors.foreground;
 		commonRendererData.coordinate = lastValueData.coordinate;
 		paneRendererData.borderColor = this._source.model().options().layout.backgroundColor;
-		axisRendererData.borderColor = commonRendererData.background;
+		axisRendererData.borderColor = lastValueColor;
 	}
 
 	protected _paneText(
diff --git a/src/views/time-axis/crosshair-time-axis-view.ts b/src/views/time-axis/crosshair-time-axis-view.ts
index cc0cbfbf3..a9b202314 100644
--- a/src/views/time-axis/crosshair-time-axis-view.ts
+++ b/src/views/time-axis/crosshair-time-axis-view.ts
@@ -1,5 +1,5 @@
 import { ensureNotNull } from '../../helpers/assertions';
-import { generateTextColor } from '../../helpers/color';
+import { generateContrastColors } from '../../helpers/color';
 
 import { ChartModel } from '../../model/chart-model';
 import { Crosshair, TimeAndCoordinateProvider } from '../../model/crosshair';
@@ -69,7 +69,9 @@ export class CrosshairTimeAxisView implements ITimeAxisView {
 		data.coordinate = value.coordinate;
 		data.text = timeScale.formatDateTime(ensureNotNull(currentTime));
 		data.visible = true;
-		data.background = options.labelBackgroundColor;
-		data.color = generateTextColor(options.labelBackgroundColor);
+
+		const colors = generateContrastColors(options.labelBackgroundColor);
+		data.background = colors.background;
+		data.color = colors.foreground;
 	}
 }


Test patch:
diff --git a/tests/e2e/graphics/test-cases/initial-options/crosshair-label-color-with-transparency.js b/tests/e2e/graphics/test-cases/initial-options/crosshair-label-color-with-transparency.js
new file mode 100644
index 000000000..4c28d50d6
--- /dev/null
+++ b/tests/e2e/graphics/test-cases/initial-options/crosshair-label-color-with-transparency.js
@@ -0,0 +1,30 @@
+function generateData() {
+	const res = [];
+	const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
+	for (let i = 0; i < 500; ++i) {
+		res.push({
+			time: time.getTime() / 1000,
+			value: i,
+		});
+
+		time.setUTCDate(time.getUTCDate() + 1);
+	}
+	return res;
+}
+
+function runTestCase(container) {
+	const chart = LightweightCharts.createChart(container, {
+		crosshair: {
+			vertLine: {
+				labelBackgroundColor: 'rgba(123, 123, 123, 0.5)',
+			},
+			horzLine: {
+				labelBackgroundColor: 'rgba(120, 0, 19, 0.5)',
+			},
+		},
+	});
+
+	const mainSeries = chart.addAreaSeries();
+
+	mainSeries.setData(generateData());
+}
diff --git a/tests/e2e/graphics/test-cases/transparent-color.js b/tests/e2e/graphics/test-cases/transparent-color.js
new file mode 100644
index 000000000..6e16199f8
--- /dev/null
+++ b/tests/e2e/graphics/test-cases/transparent-color.js
@@ -0,0 +1,23 @@
+function generateData() {
+	const res = [];
+	const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
+	for (let i = 0; i < 500; ++i) {
+		res.push({
+			time: time.getTime() / 1000,
+			value: i,
+		});
+
+		time.setUTCDate(time.getUTCDate() + 1);
+	}
+	return res;
+}
+
+function runTestCase(container) {
+	const chart = LightweightCharts.createChart(container);
+
+	const mainSeries = chart.addHistogramSeries({
+		color: 'transparent',
+	});
+
+	mainSeries.setData(generateData());
+}
diff --git a/tests/unittests/color.spec.ts b/tests/unittests/color.spec.ts
index c489ed15d..744929d83 100644
--- a/tests/unittests/color.spec.ts
+++ b/tests/unittests/color.spec.ts
@@ -1,170 +1,122 @@
 import { expect } from 'chai';
 import { describe, it } from 'mocha';
 
-import {
-	colorWithTransparency,
-	parseRgb,
-	resetTransparency,
-	Rgb,
-	rgbToBlackWhiteString,
-} from '../../src/helpers/color';
-
-describe('rgbToBlackWhiteString', () => {
-	it('should return \'black\' for black color and every non-zero threshold', () => {
-		expect(rgbToBlackWhiteString([0, 0, 0] as Rgb, 0)).to.equal('white');
-		expect(rgbToBlackWhiteString([0, 0, 0] as Rgb, 1)).to.equal('black');
-		expect(rgbToBlackWhiteString([0, 0, 0] as Rgb, 255)).to.equal('black');
+import { generateContrastColors } from '../../src/helpers/color';
+
+describe('generateContrastColors', () => {
+	it('should work', () => {
+		expect(generateContrastColors('rgb(255, 255, 255)')).to.be.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
+		expect(generateContrastColors('rgb(255, 255, 255)')).to.be.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
+		expect(generateContrastColors('rgba(255, 255, 255, 0)')).to.be.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
+		expect(generateContrastColors('rgba(255, 255, 255, 1)')).to.be.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
+
+		expect(generateContrastColors('rgb(0, 0, 0)')).to.be.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
+		expect(generateContrastColors('rgb(0, 0, 0)')).to.be.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
+		expect(generateContrastColors('rgba(0, 0, 0, 0)')).to.be.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
+		expect(generateContrastColors('rgba(0, 0, 0, 1)')).to.be.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
 	});
 
-	it('should return \'white\' for white color and any threshold', () => {
-		expect(rgbToBlackWhiteString([255, 255, 255] as Rgb, 0)).to.equal('white');
-		expect(rgbToBlackWhiteString([255, 255, 255] as Rgb, 1)).to.equal('white');
-		expect(rgbToBlackWhiteString([255, 255, 255] as Rgb, 255)).to.equal('white');
+	it('should correctly parse known named colors', () => {
+		expect(generateContrastColors('aliceblue')).to.deep.equal({ foreground: 'black', background: 'rgb(240, 248, 255)' });
+		expect(generateContrastColors('coral')).to.deep.equal({ foreground: 'white', background: 'rgb(255, 127, 80)' });
+		expect(generateContrastColors('darkmagenta')).to.deep.equal({ foreground: 'white', background: 'rgb(139, 0, 139)' });
+		expect(generateContrastColors('linen')).to.deep.equal({ foreground: 'black', background: 'rgb(250, 240, 230)' });
+		expect(generateContrastColors('whitesmoke')).to.deep.equal({ foreground: 'black', background: 'rgb(245, 245, 245)' });
 	});
 
-	it('should respect the threshold value', () => {
-		expect(rgbToBlackWhiteString([0xAA, 0xAA, 0xAA] as Rgb, 0x00)).to.equal('white');
-		expect(rgbToBlackWhiteString([0xAA, 0xAA, 0xAA] as Rgb, 0x11)).to.equal('white');
-		expect(rgbToBlackWhiteString([0xAA, 0xAA, 0xAA] as Rgb, 0x33)).to.equal('white');
-		expect(rgbToBlackWhiteString([0xAA, 0xAA, 0xAA] as Rgb, 0x55)).to.equal('white');
-		expect(rgbToBlackWhiteString([0xAA, 0xAA, 0xAA] as Rgb, 0x77)).to.equal('white');
-		expect(rgbToBlackWhiteString([0xAA, 0xAA, 0xAA] as Rgb, 0x99)).to.equal('white');
-		// ------------------------------------------------------------------- //
-		expect(rgbToBlackWhiteString([0xAA, 0xAA, 0xAA] as Rgb, 0xBB)).to.equal('black');
-		expect(rgbToBlackWhiteString([0xAA, 0xAA, 0xAA] as Rgb, 0xDD)).to.equal('black');
-		expect(rgbToBlackWhiteString([0xAA, 0xAA, 0xAA] as Rgb, 0xFF)).to.equal('black');
+	it('should correctly parse short hex colors', () => {
+		expect(generateContrastColors('#fff')).to.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
+		expect(generateContrastColors('#000')).to.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
+		expect(generateContrastColors('#fffa')).to.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
 	});
-});
 
-// parsers
-describe('parseRgb', () => {
-	it('should correctly parse known named colors', () => {
-		expect(parseRgb('aliceblue')).to.deep.equal([240, 248, 255]);
-		expect(parseRgb('coral')).to.deep.equal([255, 127, 80]);
-		expect(parseRgb('darkmagenta')).to.deep.equal([139, 0, 139]);
-		expect(parseRgb('linen')).to.deep.equal([250, 240, 230]);
-		expect(parseRgb('whitesmoke')).to.deep.equal([245, 245, 245]);
+	it('should correctly parse hex colors', () => {
+		expect(generateContrastColors('#ffffff')).to.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
+		expect(generateContrastColors('#ff0110')).to.deep.equal({ foreground: 'white', background: 'rgb(255, 1, 16)' });
+		expect(generateContrastColors('#f0f0f0aa')).to.deep.equal({ foreground: 'black', background: 'rgb(240, 240, 240)' });
 	});
 
 	it('should correctly parse RGB tuple string', () => {
-		expect(parseRgb('rgb(10, 20, 30)')).to.deep.equal([10, 20, 30]);
-		expect(parseRgb('rgb(0,0,0)')).to.deep.equal([0, 0, 0]);
-		expect(parseRgb('rgb(	10	 , 	20 	, 	30  	)')).to.deep.equal([10, 20, 30]);
+		expect(generateContrastColors('rgb(10, 20, 30)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
+		expect(generateContrastColors('rgb(0,0,0)')).to.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
+		expect(generateContrastColors('rgb(	10	 , 	20 	, 	30  	)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
 
 		// RGB tuple may contain values exceeding 255, that should be clamped to 255 after parsing
-		expect(parseRgb('rgb(256, 256, 256)')).to.deep.equal([255, 255, 255]);
-		expect(parseRgb('rgb(100500, 100500, 100500)')).to.deep.equal([255, 255, 255]);
-		expect(parseRgb('rgb(0, 100500, 0)')).to.deep.equal([0, 255, 0]);
+		expect(generateContrastColors('rgb(256, 256, 256)')).to.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
+		expect(generateContrastColors('rgb(100500, 100500, 100500)')).to.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
+		expect(generateContrastColors('rgb(0, 100500, 0)')).to.deep.equal({ foreground: 'black', background: 'rgb(0, 255, 0)' });
 
 		// RGB tuple may contain negative values, that should be clamped to zero after parsing
-		expect(parseRgb('rgb(-10, -20, -30)')).to.deep.equal([0, 0, 0]);
-		expect(parseRgb('rgb(10, -20, 30)')).to.deep.equal([10, 0, 30]);
+		expect(generateContrastColors('rgb(-10, -20, -30)')).to.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
+		expect(generateContrastColors('rgb(10, -20, 30)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 0, 30)' });
 
 		// whitespace characters before 'rgb', after 'rgb' and after the closing parenthesis are prohibited
-		expect(parseRgb.bind(null, '   	rgb(	10, 	20, 	30 		 )')).to.throw();
-		expect(parseRgb.bind(null, 'rgb	  (	10, 	20, 	30 		 )')).to.throw();
-		expect(parseRgb.bind(null, 'rgb(	10, 	20, 	30 		 ) 	  ')).to.throw();
+		expect(generateContrastColors.bind(null, '   	rgb(	10, 	20, 	30 		 )')).to.throw();
+		expect(generateContrastColors.bind(null, 'rgb	  (	10, 	20, 	30 		 )')).to.throw();
+		expect(generateContrastColors.bind(null, 'rgb(	10, 	20, 	30 		 ) 	  ')).to.throw();
 
 		// RGB tuple should not contain non-integer values
-		expect(parseRgb.bind(null, 'rgb(10.0, 20, 30)')).to.throw();
-		expect(parseRgb.bind(null, 'rgb(10, 20.0, 30)')).to.throw();
-		expect(parseRgb.bind(null, 'rgb(10, 20, 30.0)')).to.throw();
+		expect(generateContrastColors.bind(null, 'rgb(10.0, 20, 30)')).to.throw();
+		expect(generateContrastColors.bind(null, 'rgb(10, 20.0, 30)')).to.throw();
+		expect(generateContrastColors.bind(null, 'rgb(10, 20, 30.0)')).to.throw();
 
 		// not enough values in the tuple
-		expect(parseRgb.bind(null, 'rgb(10, 20)')).to.throw();
+		expect(generateContrastColors.bind(null, 'rgb(10, 20)')).to.throw();
 
 		// too much values in the tuple
-		expect(parseRgb.bind(null, 'rgb(10, 20, 30, 40)')).to.throw();
+		expect(generateContrastColors.bind(null, 'rgb(10, 20, 30, 40)')).to.throw();
 	});
-});
 
-describe('parseRgb with rgba', () => {
 	it('should correctly parse RGBA tuple string', () => {
-		expect(parseRgb('rgba(10, 20, 30, 0.40)')).to.deep.equal([10, 20, 30]);
-		expect(parseRgb('rgba(0,0,0,1)')).to.deep.equal([0, 0, 0]);
-		expect(parseRgb('rgba(	10 	, 	20 	, 	30	, 	0.40   	)')).to.deep.equal([10, 20, 30]);
-		expect(parseRgb('rgba(10, 20, 30, 0.1)')).to.deep.equal([10, 20, 30]);
-		expect(parseRgb('rgba(10, 20, 30, .1)')).to.deep.equal([10, 20, 30]);
-		expect(parseRgb('rgba(10, 20, 30, .001)')).to.deep.equal([10, 20, 30]);
-		expect(parseRgb('rgba(10, 20, 30, .000000000001)')).to.deep.equal([10, 20, 30]);
-		expect(parseRgb('rgba(10, 20, 30, .10001)')).to.deep.equal([10, 20, 30]);
-		expect(parseRgb('rgba(10, 20, 30, .10005)')).to.deep.equal([10, 20, 30]);
-		expect(parseRgb('rgba(10, 20, 30, .100000000005)')).to.deep.equal([10, 20, 30]);
+		expect(generateContrastColors('rgba(10, 20, 30, 0.40)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
+		expect(generateContrastColors('rgba(0,0,0,1)')).to.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
+		expect(generateContrastColors('rgba(	10 	, 	20 	, 	30	, 	0.40   	)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
+		expect(generateContrastColors('rgba(10, 20, 30, 0.1)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
+		expect(generateContrastColors('rgba(10, 20, 30, .1)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
+		expect(generateContrastColors('rgba(10, 20, 30, .001)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
+		expect(generateContrastColors('rgba(10, 20, 30, .000000000001)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
+		expect(generateContrastColors('rgba(10, 20, 30, .10001)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
+		expect(generateContrastColors('rgba(10, 20, 30, .10005)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
+		expect(generateContrastColors('rgba(10, 20, 30, .100000000005)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
 
 		// RGB components of a tuple may contain values exceeding 255, that should be clamped to 255 after parsing
-		expect(parseRgb('rgba(256, 256, 256, 1.0)')).to.deep.equal([255, 255, 255]);
-		expect(parseRgb('rgba(100500, 100500, 100500, 1.0)')).to.deep.equal([255, 255, 255]);
-		expect(parseRgb('rgba(0, 100500, 0, 1.0)')).to.deep.equal([0, 255, 0]);
+		expect(generateContrastColors('rgba(256, 256, 256, 1.0)')).to.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
+		expect(generateContrastColors('rgba(100500, 100500, 100500, 1.0)')).to.deep.equal({ foreground: 'black', background: 'rgb(255, 255, 255)' });
+		expect(generateContrastColors('rgba(0, 100500, 0, 1.0)')).to.deep.equal({ foreground: 'black', background: 'rgb(0, 255, 0)' });
 
 		// RGB components of a tuple may contain negative values, that should be clamped to zero after parsing
-		expect(parseRgb('rgba(-10, -20, -30, 1.0)')).to.deep.equal([0, 0, 0]);
-		expect(parseRgb('rgba(10, -20, 30, 1.0)')).to.deep.equal([10, 0, 30]);
+		expect(generateContrastColors('rgba(-10, -20, -30, 1.0)')).to.deep.equal({ foreground: 'white', background: 'rgb(0, 0, 0)' });
+		expect(generateContrastColors('rgba(10, -20, 30, 1.0)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 0, 30)' });
 
 		// Alpha component of a tuple may be a value exceeding 1.0, that should be clamped to 1.0 after parsing
-		expect(parseRgb('rgba(10, 20, 30, 1.1)')).to.deep.equal([10, 20, 30]);
-		expect(parseRgb('rgba(10, 20, 30, 1000.0)')).to.deep.equal([10, 20, 30]);
-		expect(parseRgb('rgba(10, 20, 30, 1000000)')).to.deep.equal([10, 20, 30]);
+		expect(generateContrastColors('rgba(10, 20, 30, 1.1)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
+		expect(generateContrastColors('rgba(10, 20, 30, 1000.0)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
+		expect(generateContrastColors('rgba(10, 20, 30, 1000000)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
 
 		// Alpha component of a tuple may be a negative value, that should be clamped to zero after parsing
-		expect(parseRgb('rgba(10, 20, 30, -0.1)')).to.deep.equal([10, 20, 30]);
-		expect(parseRgb('rgba(10, 20, 30, -1.1)')).to.deep.equal([10, 20, 30]);
-		expect(parseRgb('rgba(10, 20, 30, -1000.0)')).to.deep.equal([10, 20, 30]);
-		expect(parseRgb('rgba(10, 20, 30, -1000000)')).to.deep.equal([10, 20, 30]);
-		expect(parseRgb('rgba(10, 20, 30, -1000000.100000000005)')).to.deep.equal([10, 20, 30]);
+		expect(generateContrastColors('rgba(10, 20, 30, -0.1)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
+		expect(generateContrastColors('rgba(10, 20, 30, -1.1)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
+		expect(generateContrastColors('rgba(10, 20, 30, -1000.0)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
+		expect(generateContrastColors('rgba(10, 20, 30, -1000000)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
+		expect(generateContrastColors('rgba(10, 20, 30, -1000000.100000000005)')).to.deep.equal({ foreground: 'white', background: 'rgb(10, 20, 30)' });
 
 		// dangling dot is prohibited
-		expect(parseRgb.bind(null, 'rgba(10, 20, 30, 1.)')).to.throw();
+		expect(generateContrastColors.bind(null, 'rgba(10, 20, 30, 1.)')).to.throw();
 
 		// whitespace characters before 'rgba', after 'rgba' and after the closing parenthesis are prohibited
-		expect(parseRgb.bind(null, '   	rgba(	10, 	20, 	30	, 	0.40   	)')).to.throw();
-		expect(parseRgb.bind(null, 'rgba	  (	10, 	20, 	30	, 	0.40   	)')).to.throw();
-		expect(parseRgb.bind(null, 'rgba(	10, 	20, 	30	, 	0.40   	) 	  ')).to.throw();
+		expect(generateContrastColors.bind(null, '   	rgba(	10, 	20, 	30	, 	0.40   	)')).to.throw();
+		expect(generateContrastColors.bind(null, 'rgba	  (	10, 	20, 	30	, 	0.40   	)')).to.throw();
+		expect(generateContrastColors.bind(null, 'rgba(	10, 	20, 	30	, 	0.40   	) 	  ')).to.throw();
 
 		// RGB components of tuple should not contain non-integer values
-		expect(parseRgb.bind(null, 'rgba(10.0, 20, 30, 0)')).to.throw();
-		expect(parseRgb.bind(null, 'rgba(10, 20.0, 30, 0)')).to.throw();
-		expect(parseRgb.bind(null, 'rgba(10, 20, 30.0, 0)')).to.throw();
+		expect(generateContrastColors.bind(null, 'rgba(10.0, 20, 30, 0)')).to.throw();
+		expect(generateContrastColors.bind(null, 'rgba(10, 20.0, 30, 0)')).to.throw();
+		expect(generateContrastColors.bind(null, 'rgba(10, 20, 30.0, 0)')).to.throw();
 
 		// not enough values in the tuple
-		expect(parseRgb.bind(null, 'rgba(10, 20, 30)')).to.throw();
+		expect(generateContrastColors.bind(null, 'rgba(10, 20, 30)')).to.throw();
 
 		// too much values in the tuple
-		expect(parseRgb.bind(null, 'rgba(10, 20, 30, 1.0, 1.0)')).to.throw();
-	});
-});
-
-describe('resetTransparency', () => {
-	it('should work', () => {
-		expect(resetTransparency('red')).to.equal('rgba(255, 0, 0, 1)');
-		expect(resetTransparency('rgba(255, 0, 0, .1)')).to.equal('rgba(255, 0, 0, 1)');
-	});
-
-	it('should keep hex colors as is', () => {
-		expect(resetTransparency('#0f0')).to.equal('#0f0');
-		expect(resetTransparency('#0000ff')).to.equal('#0000ff');
-	});
-});
-
-describe('colorWithTransparency', () => {
-	it('should work', () => {
-		expect(colorWithTransparency('red', 1)).to.equal('rgba(255, 0, 0, 1)');
-		expect(colorWithTransparency('red', 0.5)).to.equal('rgba(255, 0, 0, 0.5)');
-		expect(colorWithTransparency('red', 0)).to.equal('rgba(255, 0, 0, 0)');
-
-		expect(colorWithTransparency('#0f0', 0.2)).to.equal('rgba(0, 255, 0, 0.2)');
-		expect(colorWithTransparency('#00ff00', 0.7)).to.equal('rgba(0, 255, 0, 0.7)');
-
-		// That how we roll!
-		expect(colorWithTransparency('rgba(0, 0, 255, .1)', 0.6)).to.equal('rgba(0, 0, 255, 0.6)');
-		expect(colorWithTransparency('rgba(-1, -2, 123, 9001)', 0.65)).to.equal('rgba(0, 0, 123, 0.65)');
-	});
-
-	it('should normalize alpha channel', () => {
-		expect(colorWithTransparency('red', -1)).to.equal('rgba(255, 0, 0, 0)');
-		expect(colorWithTransparency('red', 2)).to.equal('rgba(255, 0, 0, 1)');
-		expect(colorWithTransparency('red', -0)).to.equal('rgba(255, 0, 0, 0)');
-		expect(colorWithTransparency('red', NaN)).to.equal('rgba(255, 0, 0, 0)');
-		expect(colorWithTransparency('red', 0.30004)).to.equal('rgba(255, 0, 0, 0.3)');
-		expect(colorWithTransparency('red', 0.30006)).to.equal('rgba(255, 0, 0, 0.3001)');
+		expect(generateContrastColors.bind(null, 'rgba(10, 20, 30, 1.0, 1.0)')).to.throw();
 	});
 });