@transtyle/core 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@transtyle/core",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Transtyle core: compilation pipeline (load, normalize, derive, resolve, emit, report).",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "dependencies": {
11
+ "@transtyle/ir": "0.1.0-alpha.0"
12
+ },
13
+ "files": [
14
+ "src"
15
+ ],
16
+ "publishConfig": { "access": "public" },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/transtyle/transtyle.git",
20
+ "directory": "packages/core"
21
+ },
22
+ "homepage": "https://github.com/transtyle/transtyle#readme",
23
+ "bugs": "https://github.com/transtyle/transtyle/issues",
24
+ "license": "MIT"
25
+ }
package/src/checks.js ADDED
@@ -0,0 +1,74 @@
1
+ /** Built-in accessibility/consistency checks (docs/specs/validation-and-coverage.md). */
2
+
3
+ import { contrastRatio } from './color.js';
4
+
5
+ const S = 'semantic.color.';
6
+
7
+ /**
8
+ * The foreground/background pairs the compiler contrast-checks. Shared with
9
+ * `transtyle diff`'s contrast-regression flag (diff.js) so "which pairs count"
10
+ * has exactly one definition — a pair added here is checked in both places.
11
+ */
12
+ export const CONTRAST_PAIRS = [
13
+ ['text.base', 'elevation.0.surface'],
14
+ ['text.base', 'elevation.1.surface'],
15
+ ['text.muted', 'elevation.0.surface'],
16
+ ['text.muted', 'elevation.1.surface'],
17
+ ];
18
+
19
+ /** Minimum ratio for the configured standard (normal text). */
20
+ export function contrastThreshold(config) {
21
+ return config?.check?.contrast?.standard === 'wcag21-aaa' ? 7 : 4.5;
22
+ }
23
+
24
+ /** The pair's ratio in one resolved mode map, or null if either slot is absent. */
25
+ export function pairRatio(map, fg, bg) {
26
+ const f = map.get(S + fg)?.value;
27
+ const b = map.get(S + bg)?.value;
28
+ if (!f || !b) return null;
29
+ return contrastRatio(f, b);
30
+ }
31
+
32
+ export function runChecks(normalized, config, diagnostics) {
33
+ const min = contrastThreshold(config);
34
+ const standard = config?.check?.contrast?.standard ?? 'wcag21-aa';
35
+ for (const mode of normalized.modeValues) {
36
+ const map = normalized.modes[mode];
37
+ for (const [fg, bg] of CONTRAST_PAIRS) {
38
+ const ratio = pairRatio(map, fg, bg);
39
+ if (ratio === null) continue;
40
+ if (ratio < min) {
41
+ // AL5: on a design system with no dark-mode values authored, the
42
+ // light values simply carry over — so a light-on-light pair is
43
+ // flagged in dark mode and the warning looks like a mystery about
44
+ // colors the user never wrote. When this mode's pair is byte-identical
45
+ // to the default mode's, that carry-over IS the reason, and saying so
46
+ // is the difference between an actionable warning and noise the user
47
+ // learns to ignore.
48
+ //
49
+ // `derivation.autoDark` is NOT consulted here (or anywhere in the
50
+ // pipeline): it's specced (derivation.md) but not implemented, so the
51
+ // hint below must not suggest it as a working remedy — "opt into
52
+ // autoDark" used to be here and was accurate-sounding but false.
53
+ const defaultMap = normalized.modes[normalized.defaultMode];
54
+ const carried =
55
+ mode === normalized.defaultMode
56
+ ? []
57
+ : [fg, bg].filter(
58
+ (p) =>
59
+ JSON.stringify(map.get(`${S}${p}`)?.value) ===
60
+ JSON.stringify(defaultMap?.get(`${S}${p}`)?.value),
61
+ );
62
+ diagnostics.warn(
63
+ 'TST2101',
64
+ `${fg} vs ${bg} is ${ratio.toFixed(1)}:1 in ${mode} mode (< ${min}:1 ${standard})`,
65
+ carried.length
66
+ ? {
67
+ hint: `${carried.join(' and ')} ${carried.length > 1 ? 'are' : 'is'} unchanged from ${normalized.defaultMode} mode — nothing authors a ${mode} value, so ${carried.length > 1 ? 'they' : 'it'} carried over. Author the ${mode} value.`,
68
+ }
69
+ : {},
70
+ );
71
+ }
72
+ }
73
+ }
74
+ }
package/src/color.js ADDED
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Minimal OKLCH color engine (zero deps).
3
+ * Internal canonical form: { l, c, h, alpha } — docs/architecture/ir.md#values.
4
+ */
5
+
6
+ import { NAMED_COLORS } from './css-colors.js';
7
+
8
+ const OKLCH_RE = /^oklch\(\s*([\d.]+%?)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+%?))?\s*\)$/i;
9
+ const FUNC_RE = /^(rgba?|hsla?)\(\s*([^)]*)\s*\)$/i;
10
+
11
+ /**
12
+ * Parse any color syntax a real stylesheet is likely to contain
13
+ * (docs/architecture/ir.md#values): `oklch()`, `#hex` (3/4/6/8 digits),
14
+ * `rgb()`/`rgba()`, `hsl()`/`hsla()`, the CSS named colors, and `transparent`.
15
+ * Both the modern space-separated (`rgb(255 0 0 / 50%)`) and legacy comma
16
+ * (`rgba(255, 0, 0, .5)`) forms are accepted. Everything canonicalizes to OKLCH.
17
+ */
18
+ export function parseColor(str) {
19
+ if (typeof str !== 'string') throw new Error(`Not a color string: ${JSON.stringify(str)}`);
20
+ const s = str.trim();
21
+
22
+ const m = OKLCH_RE.exec(s);
23
+ if (m) {
24
+ const num = (v) => (v.endsWith('%') ? parseFloat(v) / 100 : parseFloat(v));
25
+ return { l: num(m[1]), c: parseFloat(m[2]), h: parseFloat(m[3]), alpha: m[4] ? num(m[4]) : 1 };
26
+ }
27
+
28
+ if (/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(s)) {
29
+ const { alpha, ...rgb } = hexToSrgb(s);
30
+ return srgbToOklch(rgb, alpha);
31
+ }
32
+
33
+ const fn = FUNC_RE.exec(s);
34
+ if (fn) {
35
+ const kind = fn[1].toLowerCase();
36
+ const { parts, alpha } = splitComponents(fn[2]);
37
+ if (parts.length < 3) throw new Error(`Malformed ${kind}() color: ${s}`);
38
+ const a = parseAlpha(alpha);
39
+ if (kind.startsWith('rgb')) {
40
+ const ch = (v) => (v === 'none' ? 0 : v.endsWith('%') ? parseFloat(v) / 100 : parseFloat(v) / 255);
41
+ const rgb = { r: ch(parts[0]), g: ch(parts[1]), b: ch(parts[2]) };
42
+ if (Object.values(rgb).some(Number.isNaN)) throw new Error(`Malformed ${kind}() color: ${s}`);
43
+ return srgbToOklch(rgb, a);
44
+ }
45
+ const h = parseHue(parts[0]);
46
+ const pct = (v) => (v === 'none' ? 0 : parseFloat(v) / 100);
47
+ const sat = pct(parts[1]), lig = pct(parts[2]);
48
+ if ([h, sat, lig].some(Number.isNaN)) throw new Error(`Malformed ${kind}() color: ${s}`);
49
+ return srgbToOklch(hslToSrgb(h, sat, lig), a);
50
+ }
51
+
52
+ const lower = s.toLowerCase();
53
+ if (lower === 'transparent') return { l: 0, c: 0, h: 0, alpha: 0 };
54
+ if (NAMED_COLORS[lower]) {
55
+ const { alpha, ...rgb } = hexToSrgb(NAMED_COLORS[lower]);
56
+ return srgbToOklch(rgb, alpha);
57
+ }
58
+
59
+ throw new Error(`Unsupported color syntax: ${s} (expected oklch(), #hex, rgb(), hsl(), or a CSS named color)`);
60
+ }
61
+
62
+ /** Split a function body into 3 components + optional alpha, modern or legacy form. */
63
+ function splitComponents(inner) {
64
+ const body = inner.trim();
65
+ const slash = body.indexOf('/');
66
+ if (slash !== -1) {
67
+ return { parts: body.slice(0, slash).trim().split(/\s+/), alpha: body.slice(slash + 1).trim() };
68
+ }
69
+ if (body.includes(',')) {
70
+ const all = body.split(',').map((p) => p.trim());
71
+ return { parts: all.slice(0, 3), alpha: all[3] };
72
+ }
73
+ return { parts: body.split(/\s+/), alpha: undefined };
74
+ }
75
+
76
+ const parseAlpha = (v) => {
77
+ if (v === undefined || v === '' || v === 'none') return 1;
78
+ const n = v.endsWith('%') ? parseFloat(v) / 100 : parseFloat(v);
79
+ return Number.isNaN(n) ? 1 : Math.min(1, Math.max(0, n));
80
+ };
81
+
82
+ /** CSS <angle> → degrees (deg/rad/grad/turn, or unitless = deg). */
83
+ function parseHue(v) {
84
+ const m = /^(-?[\d.]+)(deg|rad|grad|turn)?$/i.exec(v.trim());
85
+ if (!m) return NaN;
86
+ const n = parseFloat(m[1]);
87
+ switch ((m[2] ?? 'deg').toLowerCase()) {
88
+ case 'rad': return (n * 180) / Math.PI;
89
+ case 'grad': return n * 0.9;
90
+ case 'turn': return n * 360;
91
+ default: return n;
92
+ }
93
+ }
94
+
95
+ function hslToSrgb(hDeg, s, l) {
96
+ const h = ((hDeg % 360) + 360) % 360;
97
+ const c = (1 - Math.abs(2 * l - 1)) * s;
98
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
99
+ const m = l - c / 2;
100
+ const [r, g, b] =
101
+ h < 60 ? [c, x, 0] : h < 120 ? [x, c, 0] : h < 180 ? [0, c, x]
102
+ : h < 240 ? [0, x, c] : h < 300 ? [x, 0, c] : [c, 0, x];
103
+ return { r: r + m, g: g + m, b: b + m };
104
+ }
105
+
106
+ export function formatColor({ l, c, h, alpha = 1 }) {
107
+ const r3 = (n) => Math.round(n * 1000) / 1000;
108
+ const L = Math.min(1, Math.max(0, r3(l)));
109
+ let C = Math.max(0, r3(c));
110
+ let H = Math.round(h * 10) / 10;
111
+ if (C < 0.002) { C = 0; H = 0; } // achromatic: drop meaningless hue
112
+ const a = alpha < 1 ? ` / ${r3(alpha)}` : '';
113
+ return `oklch(${L} ${C} ${H}${a})`;
114
+ }
115
+
116
+ /** Mix a toward b by t (0 = a, 1 = b), in OKLCH with shortest-path hue. */
117
+ export function mix(a, b, t) {
118
+ // Cartesian OKLab interpolation (derivation.md, pinned by exercise F21).
119
+ // Polar hue lerp passes through unrelated hues at moderate ratios (an amber
120
+ // border tint on a blue-cast surface must not travel through cyan).
121
+ const lerp = (x, y) => x + (y - x) * t;
122
+ const rad = Math.PI / 180;
123
+ const A = lerp(a.c * Math.cos(a.h * rad), b.c * Math.cos(b.h * rad));
124
+ const B = lerp(a.c * Math.sin(a.h * rad), b.c * Math.sin(b.h * rad));
125
+ const c = Math.sqrt(A * A + B * B);
126
+ const h = c < 1e-9 ? 0 : (Math.atan2(B, A) / rad + 360) % 360;
127
+ return { l: lerp(a.l, b.l), c, h, alpha: lerp(a.alpha ?? 1, b.alpha ?? 1) };
128
+ }
129
+
130
+ // ---- OKLab <-> linear sRGB (Björn Ottosson's matrices) ----
131
+
132
+ function oklchToLinearSrgb({ l, c, h }) {
133
+ const hr = (h * Math.PI) / 180;
134
+ const a = c * Math.cos(hr);
135
+ const b = c * Math.sin(hr);
136
+ const l_ = (l + 0.3963377774 * a + 0.2158037573 * b) ** 3;
137
+ const m_ = (l - 0.1055613458 * a - 0.0638541728 * b) ** 3;
138
+ const s_ = (l - 0.0894841775 * a - 1.291485548 * b) ** 3;
139
+ return {
140
+ r: 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_,
141
+ g: -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_,
142
+ b: -0.0041960863 * l_ - 0.7034186147 * m_ + 1.707614701 * s_,
143
+ };
144
+ }
145
+
146
+ /** #rgb / #rgba / #rrggbb / #rrggbbaa → sRGB (+ alpha). */
147
+ function hexToSrgb(hex) {
148
+ let s = hex.slice(1);
149
+ if (s.length === 3 || s.length === 4) s = s.split('').map((ch) => ch + ch).join('');
150
+ const n = parseInt(s.slice(0, 6), 16);
151
+ const alpha = s.length === 8 ? parseInt(s.slice(6, 8), 16) / 255 : 1;
152
+ return { r: ((n >> 16) & 255) / 255, g: ((n >> 8) & 255) / 255, b: (n & 255) / 255, alpha };
153
+ }
154
+
155
+ function srgbToLinear(v) {
156
+ return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
157
+ }
158
+
159
+ function srgbToOklch({ r, g, b }, alpha = 1) {
160
+ const lr = srgbToLinear(r), lg = srgbToLinear(g), lb = srgbToLinear(b);
161
+ const l_ = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
162
+ const m_ = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
163
+ const s_ = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
164
+ const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
165
+ const A = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
166
+ const B = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;
167
+ const c = Math.hypot(A, B);
168
+ const h = ((Math.atan2(B, A) * 180) / Math.PI + 360) % 360;
169
+ return { l: L, c, h, alpha };
170
+ }
171
+
172
+ const linearToSrgb = (v) => (v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055);
173
+
174
+ const inSrgbGamut = ({ r, g, b }) => r >= -0.0005 && r <= 1.0005 && g >= -0.0005 && g <= 1.0005 && b >= -0.0005 && b <= 1.0005;
175
+
176
+ /**
177
+ * Reduce chroma at a fixed lightness/hue until the color lands inside the
178
+ * sRGB gamut (binary search; ~20 steps is well under float precision).
179
+ * Keeps `l` and `h` exactly as given — only `c` moves — because rules like
180
+ * F20 (contrast-anchor) pick `l`/`h` for a reason (contrast, brand hue) and
181
+ * should not have those silently perturbed by channel-clipping downstream.
182
+ * A no-op when the color is already in gamut.
183
+ */
184
+ export function clampChromaToGamut({ l, c, h, alpha = 1 }) {
185
+ if (c <= 0 || inSrgbGamut(oklchToLinearSrgb({ l, c, h }))) return { l, c, h, alpha };
186
+ let lo = 0, hi = c;
187
+ for (let i = 0; i < 20; i++) {
188
+ const mid = (lo + hi) / 2;
189
+ if (inSrgbGamut(oklchToLinearSrgb({ l, c: mid, h }))) lo = mid;
190
+ else hi = mid;
191
+ }
192
+ return { l, c: lo, h, alpha };
193
+ }
194
+
195
+ /**
196
+ * OKLCH → gamma-encoded sRGB. `clamped` is true when the color was outside the
197
+ * sRGB gamut and had to be clamped (coverage class: approximated).
198
+ */
199
+ export function oklchToSrgb(color) {
200
+ const lin = oklchToLinearSrgb(color);
201
+ let clamped = false;
202
+ const enc = (v) => {
203
+ if (v < -0.005 || v > 1.005) clamped = true;
204
+ return linearToSrgb(Math.min(1, Math.max(0, v)));
205
+ };
206
+ return { r: enc(lin.r), g: enc(lin.g), b: enc(lin.b), clamped };
207
+ }
208
+
209
+ /**
210
+ * Format as an HSL channel triplet ("221 83% 53%") — the shadcn tailwind-v3
211
+ * convention (wrapped by components as hsl(var(--x))).
212
+ */
213
+ export function formatHslTriplet(color) {
214
+ // Near-achromatic: drop the meaningless hue/saturation before conversion,
215
+ // otherwise rounding noise yields absurd triplets like "180 100% 99.9%".
216
+ const input = color.c < 0.002 ? { ...color, c: 0 } : color;
217
+ const { r, g, b, clamped } = oklchToSrgb(input);
218
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
219
+ const l = (max + min) / 2;
220
+ const d = max - min;
221
+ let h = 0, s = 0;
222
+ if (d > 1e-6) {
223
+ s = d / (1 - Math.abs(2 * l - 1));
224
+ if (max === r) h = ((g - b) / d) % 6;
225
+ else if (max === g) h = (b - r) / d + 2;
226
+ else h = (r - g) / d + 4;
227
+ h = (h * 60 + 360) % 360;
228
+ }
229
+ const r1 = (n) => Math.round(n * 10) / 10;
230
+ return { text: `${r1(h)} ${r1(s * 100)}% ${r1(l * 100)}%`, clamped };
231
+ }
232
+
233
+ /**
234
+ * Format as #rrggbb hex (canvas-friendly: ECharts). `clamped` mirrors oklchToSrgb.
235
+ *
236
+ * Round-trip note (measured exhaustively, guarded by scripts/check-color.mjs):
237
+ * `formatHex(parseColor(hex))` returns the input for all but 1580 of the
238
+ * 16,777,216 sRGB values (0.0094%), which come back ±1/255. Those all sit in the
239
+ * near-black range, where sRGB's transfer curve is steepest relative to an 8-bit
240
+ * step; it is inherent to canonicalizing through OKLCH in float64. Determinism is
241
+ * unaffected — the same input always yields the same output.
242
+ */
243
+ export function formatHex(color) {
244
+ const input = color.c < 0.002 ? { ...color, c: 0 } : color;
245
+ const { r, g, b, clamped } = oklchToSrgb(input);
246
+ const h2 = (v) => Math.round(v * 255).toString(16).padStart(2, '0');
247
+ return { text: `#${h2(r)}${h2(g)}${h2(b)}`, clamped };
248
+ }
249
+
250
+ /** WCAG 2.1 relative luminance (via linear sRGB, gamut-clamped). */
251
+ export function relativeLuminance(color) {
252
+ const { r, g, b } = oklchToLinearSrgb(color);
253
+ const cl = (v) => Math.min(1, Math.max(0, v));
254
+ return 0.2126 * cl(r) + 0.7152 * cl(g) + 0.0722 * cl(b);
255
+ }
256
+
257
+ /** WCAG 2.1 contrast ratio between two colors (order-independent). */
258
+ export function contrastRatio(a, b) {
259
+ const ya = relativeLuminance(a), yb = relativeLuminance(b);
260
+ const [hi, lo] = ya >= yb ? [ya, yb] : [yb, ya];
261
+ return (hi + 0.05) / (lo + 0.05);
262
+ }
263
+
264
+ /** Pick the candidate with the highest contrast against bg. Returns { color, ratio, index }. */
265
+ export function contrastPick(bg, candidates) {
266
+ let best = null;
267
+ candidates.forEach((cand, index) => {
268
+ const ratio = contrastRatio(bg, cand);
269
+ if (!best || ratio > best.ratio) best = { color: cand, ratio, index };
270
+ });
271
+ return best;
272
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The CSS Color Module Level 4 named colors (plus `transparent`), as sRGB hex.
3
+ *
4
+ * Kept as data in its own module so color.js stays readable. Real products use
5
+ * these verbatim — the P4 hostile-adoption experiment hit a hard stop on
6
+ * Miniflux's literal `red` and `purple` (docs/findings/hostile-adoption.md).
7
+ */
8
+ export const NAMED_COLORS = {
9
+ aliceblue: '#f0f8ff', antiquewhite: '#faebd7', aqua: '#00ffff', aquamarine: '#7fffd4',
10
+ azure: '#f0ffff', beige: '#f5f5dc', bisque: '#ffe4c4', black: '#000000',
11
+ blanchedalmond: '#ffebcd', blue: '#0000ff', blueviolet: '#8a2be2', brown: '#a52a2a',
12
+ burlywood: '#deb887', cadetblue: '#5f9ea0', chartreuse: '#7fff00', chocolate: '#d2691e',
13
+ coral: '#ff7f50', cornflowerblue: '#6495ed', cornsilk: '#fff8dc', crimson: '#dc143c',
14
+ cyan: '#00ffff', darkblue: '#00008b', darkcyan: '#008b8b', darkgoldenrod: '#b8860b',
15
+ darkgray: '#a9a9a9', darkgreen: '#006400', darkgrey: '#a9a9a9', darkkhaki: '#bdb76b',
16
+ darkmagenta: '#8b008b', darkolivegreen: '#556b2f', darkorange: '#ff8c00', darkorchid: '#9932cc',
17
+ darkred: '#8b0000', darksalmon: '#e9967a', darkseagreen: '#8fbc8f', darkslateblue: '#483d8b',
18
+ darkslategray: '#2f4f4f', darkslategrey: '#2f4f4f', darkturquoise: '#00ced1', darkviolet: '#9400d3',
19
+ deeppink: '#ff1493', deepskyblue: '#00bfff', dimgray: '#696969', dimgrey: '#696969',
20
+ dodgerblue: '#1e90ff', firebrick: '#b22222', floralwhite: '#fffaf0', forestgreen: '#228b22',
21
+ fuchsia: '#ff00ff', gainsboro: '#dcdcdc', ghostwhite: '#f8f8ff', gold: '#ffd700',
22
+ goldenrod: '#daa520', gray: '#808080', green: '#008000', greenyellow: '#adff2f',
23
+ grey: '#808080', honeydew: '#f0fff0', hotpink: '#ff69b4', indianred: '#cd5c5c',
24
+ indigo: '#4b0082', ivory: '#fffff0', khaki: '#f0e68c', lavender: '#e6e6fa',
25
+ lavenderblush: '#fff0f5', lawngreen: '#7cfc00', lemonchiffon: '#fffacd', lightblue: '#add8e6',
26
+ lightcoral: '#f08080', lightcyan: '#e0ffff', lightgoldenrodyellow: '#fafad2', lightgray: '#d3d3d3',
27
+ lightgreen: '#90ee90', lightgrey: '#d3d3d3', lightpink: '#ffb6c1', lightsalmon: '#ffa07a',
28
+ lightseagreen: '#20b2aa', lightskyblue: '#87cefa', lightslategray: '#778899', lightslategrey: '#778899',
29
+ lightsteelblue: '#b0c4de', lightyellow: '#ffffe0', lime: '#00ff00', limegreen: '#32cd32',
30
+ linen: '#faf0e6', magenta: '#ff00ff', maroon: '#800000', mediumaquamarine: '#66cdaa',
31
+ mediumblue: '#0000cd', mediumorchid: '#ba55d3', mediumpurple: '#9370db', mediumseagreen: '#3cb371',
32
+ mediumslateblue: '#7b68ee', mediumspringgreen: '#00fa9a', mediumturquoise: '#48d1cc', mediumvioletred: '#c71585',
33
+ midnightblue: '#191970', mintcream: '#f5fffa', mistyrose: '#ffe4e1', moccasin: '#ffe4b5',
34
+ navajowhite: '#ffdead', navy: '#000080', oldlace: '#fdf5e6', olive: '#808000',
35
+ olivedrab: '#6b8e23', orange: '#ffa500', orangered: '#ff4500', orchid: '#da70d6',
36
+ palegoldenrod: '#eee8aa', palegreen: '#98fb98', paleturquoise: '#afeeee', palevioletred: '#db7093',
37
+ papayawhip: '#ffefd5', peachpuff: '#ffdab9', peru: '#cd853f', pink: '#ffc0cb',
38
+ plum: '#dda0dd', powderblue: '#b0e0e6', purple: '#800080', rebeccapurple: '#663399',
39
+ red: '#ff0000', rosybrown: '#bc8f8f', royalblue: '#4169e1', saddlebrown: '#8b4513',
40
+ salmon: '#fa8072', sandybrown: '#f4a460', seagreen: '#2e8b57', seashell: '#fff5ee',
41
+ sienna: '#a0522d', silver: '#c0c0c0', skyblue: '#87ceeb', slateblue: '#6a5acd',
42
+ slategray: '#708090', slategrey: '#708090', snow: '#fffafa', springgreen: '#00ff7f',
43
+ steelblue: '#4682b4', tan: '#d2b48c', teal: '#008080', thistle: '#d8bfd8',
44
+ tomato: '#ff6347', turquoise: '#40e0d0', violet: '#ee82ee', wheat: '#f5deb3',
45
+ white: '#ffffff', whitesmoke: '#f5f5f5', yellow: '#ffff00', yellowgreen: '#9acd32',
46
+ };