Skip to content

Commit 03c7e89

Browse files
fix: convert colors to hex
1 parent 92834e8 commit 03c7e89

3 files changed

Lines changed: 266 additions & 15 deletions

File tree

.playwright/tests/customStyleColors.spec.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,56 +51,56 @@ const ROUND_TRIP_CASES: { name: string; input: string; expected: string }[] = [
5151
name: 'foreground color only',
5252
input: '<html><p><span style="color: #FF0000">Red text</span></p></html>',
5353
expected:
54-
'<html><p><span style="color: rgb(255, 0, 0);">Red text</span></p></html>',
54+
'<html><p><span style="color: #FF0000;">Red text</span></p></html>',
5555
},
5656
{
5757
name: 'background color only',
5858
input:
5959
'<html><p><span style="background-color: #FFFF00">Yellow bg</span></p></html>',
6060
expected:
61-
'<html><p><span style="background-color: rgb(255, 255, 0);">Yellow bg</span></p></html>',
61+
'<html><p><span style="background-color: #FFFF00;">Yellow bg</span></p></html>',
6262
},
6363
{
6464
name: 'foreground and background color',
6565
input:
6666
'<html><p><span style="color: #FF0000; background-color: #FFFF00">Both</span></p></html>',
6767
expected:
68-
'<html><p><span style="color: rgb(255, 0, 0); background-color: rgb(255, 255, 0);">Both</span></p></html>',
68+
'<html><p><span style="color: #FF0000; background-color: #FFFF00;">Both</span></p></html>',
6969
},
7070
{
7171
name: '8-digit hex background (transparent)',
7272
input:
7373
'<html><p><span style="background-color: #00FF0040">25% green bg</span></p></html>',
7474
expected:
75-
'<html><p><span style="background-color: rgba(0, 255, 0, 0.25);">25% green bg</span></p></html>',
75+
'<html><p><span style="background-color: #00FF0040;">25% green bg</span></p></html>',
7676
},
7777
{
7878
name: '8-digit hex foreground (transparent)',
7979
input:
8080
'<html><p><span style="color: #0000FF80">50% blue text</span></p></html>',
8181
expected:
82-
'<html><p><span style="color: rgba(0, 0, 255, 0.5);">50% blue text</span></p></html>',
82+
'<html><p><span style="color: #0000FF80;">50% blue text</span></p></html>',
8383
},
8484
{
8585
name: 'color inside heading',
8686
input:
8787
'<html><h6><span style="color: #000000; background-color: #00FF00">Black on green</span></h6></html>',
8888
expected:
89-
'<html><h6><span style="color: rgb(0, 0, 0); background-color: rgb(0, 255, 0);">Black on green</span></h6></html>',
89+
'<html><h6><span style="color: #000000; background-color: #00FF00;">Black on green</span></h6></html>',
9090
},
9191
{
9292
name: 'color wraps bold mark',
9393
input:
9494
'<html><p><span style="color: #FF0000"><b>Bold red</b></span></p></html>',
9595
expected:
96-
'<html><p><span style="color: rgb(255, 0, 0);"><b>Bold red</b></span></p></html>',
96+
'<html><p><span style="color: #FF0000;"><b>Bold red</b></span></p></html>',
9797
},
9898
{
9999
name: 'multiple colored spans in one paragraph',
100100
input:
101101
'<html><p><span style="color: #FF0000">Red</span> plain <span style="color: #0000FF">Blue</span></p></html>',
102102
expected:
103-
'<html><p><span style="color: rgb(255, 0, 0);">Red</span> plain <span style="color: rgb(0, 0, 255);">Blue</span></p></html>',
103+
'<html><p><span style="color: #FF0000;">Red</span> plain <span style="color: #0000FF;">Blue</span></p></html>',
104104
},
105105
];
106106

@@ -152,7 +152,7 @@ test.describe('custom style colors - toolbar interaction', () => {
152152
await expect
153153
.poll(async () => getSerializedHtml(page))
154154
.toBe(
155-
'<html><p><span style="color: rgb(255, 0, 0);">Red text</span></p></html>'
155+
'<html><p><span style="color: #FF0000;">Red text</span></p></html>'
156156
);
157157
});
158158

@@ -168,7 +168,7 @@ test.describe('custom style colors - toolbar interaction', () => {
168168
await expect
169169
.poll(async () => getSerializedHtml(page))
170170
.toBe(
171-
'<html><p><span style="color: rgb(255, 0, 0);">Red</span> plain</p></html>'
171+
'<html><p><span style="color: #FF0000;">Red</span> plain</p></html>'
172172
);
173173
});
174174

@@ -182,7 +182,7 @@ test.describe('custom style colors - toolbar interaction', () => {
182182
await expect
183183
.poll(async () => getSerializedHtml(page))
184184
.toBe(
185-
'<html><p><span style="background-color: rgb(255, 255, 0);">Yellow back</span></p></html>'
185+
'<html><p><span style="background-color: #FFFF00;">Yellow back</span></p></html>'
186186
);
187187
});
188188

@@ -198,7 +198,7 @@ test.describe('custom style colors - toolbar interaction', () => {
198198
await expect
199199
.poll(async () => getSerializedHtml(page))
200200
.toBe(
201-
'<html><p><span style="background-color: rgb(255, 255, 0);">Yellow</span> plain</p></html>'
201+
'<html><p><span style="background-color: #FFFF00;">Yellow</span> plain</p></html>'
202202
);
203203
});
204204

@@ -213,7 +213,7 @@ test.describe('custom style colors - toolbar interaction', () => {
213213
await expect
214214
.poll(async () => getSerializedHtml(page))
215215
.toBe(
216-
'<html><p><span style="color: rgb(255, 0, 0); background-color: rgb(255, 255, 0);">Red+Yellow</span></p></html>'
216+
'<html><p><span style="color: #FF0000; background-color: #FFFF00;">Red+Yellow</span></p></html>'
217217
);
218218
});
219219

@@ -229,7 +229,7 @@ test.describe('custom style colors - toolbar interaction', () => {
229229
await expect
230230
.poll(async () => getSerializedHtml(page))
231231
.toBe(
232-
'<html><p><span style="color: rgb(255, 0, 0);"><b>Bold red</b></span></p></html>'
232+
'<html><p><span style="color: #FF0000;"><b>Bold red</b></span></p></html>'
233233
);
234234
});
235235

@@ -245,7 +245,7 @@ test.describe('custom style colors - toolbar interaction', () => {
245245
await expect
246246
.poll(async () => getSerializedHtml(page))
247247
.toBe(
248-
'<html><p><span style="background-color: rgb(255, 255, 0);"><i>Italic yellow back</i></span></p></html>'
248+
'<html><p><span style="background-color: #FFFF00;"><i>Italic yellow back</i></span></p></html>'
249249
);
250250
});
251251

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
const CSS_NAMED_COLORS: Record<string, string> = {
2+
aliceblue: '#F0F8FFFF',
3+
antiquewhite: '#FAEBD7FF',
4+
aqua: '#00FFFFFF',
5+
aquamarine: '#7FFFD4FF',
6+
azure: '#F0FFFFFF',
7+
beige: '#F5F5DCFF',
8+
bisque: '#FFE4C4FF',
9+
black: '#000000FF',
10+
blanchedalmond: '#FFEBCDFF',
11+
blue: '#0000FFFF',
12+
blueviolet: '#8A2BE2FF',
13+
brown: '#A52A2AFF',
14+
burlywood: '#DEB887FF',
15+
cadetblue: '#5F9EA0FF',
16+
chartreuse: '#7FFF00FF',
17+
chocolate: '#D2691EFF',
18+
coral: '#FF7F50FF',
19+
cornflowerblue: '#6495EDFF',
20+
cornsilk: '#FFF8DCFF',
21+
crimson: '#DC143CFF',
22+
cyan: '#00FFFFFF',
23+
darkblue: '#00008BFF',
24+
darkcyan: '#008B8BFF',
25+
darkgoldenrod: '#B8860BFF',
26+
darkgray: '#A9A9A9FF',
27+
darkgrey: '#A9A9A9FF',
28+
darkgreen: '#006400FF',
29+
darkkhaki: '#BDB76BFF',
30+
darkmagenta: '#8B008BFF',
31+
darkolivegreen: '#556B2FFF',
32+
darkorange: '#FF8C00FF',
33+
darkorchid: '#9932CCFF',
34+
darkred: '#8B0000FF',
35+
darksalmon: '#E9967AFF',
36+
darkseagreen: '#8FBC8FFF',
37+
darkslateblue: '#483D8BFF',
38+
darkslategray: '#2F4F4FFF',
39+
darkslategrey: '#2F4F4FFF',
40+
darkturquoise: '#00CED1FF',
41+
darkviolet: '#9400D3FF',
42+
deeppink: '#FF1493FF',
43+
deepskyblue: '#00BFFFFF',
44+
dimgray: '#696969FF',
45+
dimgrey: '#696969FF',
46+
dodgerblue: '#1E90FFFF',
47+
firebrick: '#B22222FF',
48+
floralwhite: '#FFFAF0FF',
49+
forestgreen: '#228B22FF',
50+
fuchsia: '#FF00FFFF',
51+
gainsboro: '#DCDCDCFF',
52+
ghostwhite: '#F8F8FFFF',
53+
gold: '#FFD700FF',
54+
goldenrod: '#DAA520FF',
55+
gray: '#808080FF',
56+
grey: '#808080FF',
57+
green: '#008000FF',
58+
greenyellow: '#ADFF2FFF',
59+
honeydew: '#F0FFF0FF',
60+
hotpink: '#FF69B4FF',
61+
indianred: '#CD5C5CFF',
62+
indigo: '#4B0082FF',
63+
ivory: '#FFFFF0FF',
64+
khaki: '#F0E68CFF',
65+
lavender: '#E6E6FAFF',
66+
lavenderblush: '#FFF0F5FF',
67+
lawngreen: '#7CFC00FF',
68+
lemonchiffon: '#FFFACDFF',
69+
lightblue: '#ADD8E6FF',
70+
lightcoral: '#F08080FF',
71+
lightcyan: '#E0FFFFFF',
72+
lightgoldenrodyellow: '#FAFAD2FF',
73+
lightgray: '#D3D3D3FF',
74+
lightgrey: '#D3D3D3FF',
75+
lightgreen: '#90EE90FF',
76+
lightpink: '#FFB6C1FF',
77+
lightsalmon: '#FFA07AFF',
78+
lightseagreen: '#20B2AAFF',
79+
lightskyblue: '#87CEFAFF',
80+
lightslategray: '#778899FF',
81+
lightslategrey: '#778899FF',
82+
lightsteelblue: '#B0C4DEFF',
83+
lightyellow: '#FFFFE0FF',
84+
lime: '#00FF00FF',
85+
limegreen: '#32CD32FF',
86+
linen: '#FAF0E6FF',
87+
magenta: '#FF00FFFF',
88+
maroon: '#800000FF',
89+
mediumaquamarine: '#66CDAAFF',
90+
mediumblue: '#0000CDFF',
91+
mediumorchid: '#BA55D3FF',
92+
mediumpurple: '#9370D8FF',
93+
mediumseagreen: '#3CB371FF',
94+
mediumslateblue: '#7B68EEFF',
95+
mediumspringgreen: '#00FA9AFF',
96+
mediumturquoise: '#48D1CCFF',
97+
mediumvioletred: '#C71585FF',
98+
midnightblue: '#191970FF',
99+
mintcream: '#F5FFFAFF',
100+
mistyrose: '#FFE4E1FF',
101+
moccasin: '#FFE4B5FF',
102+
navajowhite: '#FFDEADFF',
103+
navy: '#000080FF',
104+
oldlace: '#FDF5E6FF',
105+
olive: '#808000FF',
106+
olivedrab: '#6B8E23FF',
107+
orange: '#FFA500FF',
108+
orangered: '#FF4500FF',
109+
orchid: '#DA70D6FF',
110+
palegoldenrod: '#EEE8AAFF',
111+
palegreen: '#98FB98FF',
112+
paleturquoise: '#AFEEEEFF',
113+
palevioletred: '#D87093FF',
114+
papayawhip: '#FFEFD5FF',
115+
peachpuff: '#FFDAB9FF',
116+
peru: '#CD853FFF',
117+
pink: '#FFC0CBFF',
118+
plum: '#DDA0DDFF',
119+
powderblue: '#B0E0E6FF',
120+
purple: '#800080FF',
121+
rebeccapurple: '#663399FF',
122+
red: '#FF0000FF',
123+
rosybrown: '#BC8F8FFF',
124+
royalblue: '#4169E1FF',
125+
saddlebrown: '#8B4513FF',
126+
salmon: '#FA8072FF',
127+
sandybrown: '#F4A460FF',
128+
seagreen: '#2E8B57FF',
129+
seashell: '#FFF5EEFF',
130+
sienna: '#A0522DFF',
131+
silver: '#C0C0C0FF',
132+
skyblue: '#87CEEBFF',
133+
slateblue: '#6A5ACDFF',
134+
slategray: '#708090FF',
135+
slategrey: '#708090FF',
136+
snow: '#FFFAFAFF',
137+
springgreen: '#00FF7FFF',
138+
steelblue: '#4682B4FF',
139+
tan: '#D2B48CFF',
140+
teal: '#008080FF',
141+
thistle: '#D8BFD8FF',
142+
tomato: '#FF6347FF',
143+
turquoise: '#40E0D0FF',
144+
violet: '#EE82EEFF',
145+
wheat: '#F5DEB3FF',
146+
white: '#FFFFFFFF',
147+
whitesmoke: '#F5F5F5FF',
148+
yellow: '#FFFF00FF',
149+
yellowgreen: '#9ACD32FF',
150+
};
151+
152+
export function normalizeColorToHex(
153+
value: string | null | undefined
154+
): string | null {
155+
if (!value) return null;
156+
let str = value.trim().toLowerCase();
157+
158+
// Handle Named Colors
159+
if (CSS_NAMED_COLORS[str] !== undefined) {
160+
str = CSS_NAMED_COLORS[str]!.toLowerCase();
161+
}
162+
163+
// Handle Hex (#FFF, #RGBA, #RRGGBB, #RRGGBBAA)
164+
if (str.startsWith('#')) {
165+
const hex = str.substring(1);
166+
let r,
167+
g,
168+
b,
169+
a = 'ff';
170+
171+
if (hex.length === 3) {
172+
r = hex.charAt(0) + hex.charAt(0);
173+
g = hex.charAt(1) + hex.charAt(1);
174+
b = hex.charAt(2) + hex.charAt(2);
175+
} else if (hex.length === 4) {
176+
r = hex.charAt(0) + hex.charAt(0);
177+
g = hex.charAt(1) + hex.charAt(1);
178+
b = hex.charAt(2) + hex.charAt(2);
179+
a = hex.charAt(3) + hex.charAt(3);
180+
} else if (hex.length === 6) {
181+
r = hex.substring(0, 2);
182+
g = hex.substring(2, 4);
183+
b = hex.substring(4, 6);
184+
} else if (hex.length === 8) {
185+
r = hex.substring(0, 2);
186+
g = hex.substring(2, 4);
187+
b = hex.substring(4, 6);
188+
a = hex.substring(6, 8);
189+
} else {
190+
return null;
191+
}
192+
193+
// drop alpha if it's 255 (FF)
194+
if (a === 'ff') {
195+
return `#${r}${g}${b}`.toUpperCase();
196+
}
197+
return `#${r}${g}${b}${a}`.toUpperCase();
198+
}
199+
200+
// Handle rgb() and rgba()
201+
if (str.startsWith('rgb')) {
202+
const match = str.match(
203+
/rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)/
204+
);
205+
if (match && match[1] && match[2] && match[3]) {
206+
const r = Math.round(parseFloat(match[1])).toString(16).padStart(2, '0');
207+
const g = Math.round(parseFloat(match[2])).toString(16).padStart(2, '0');
208+
const b = Math.round(parseFloat(match[3])).toString(16).padStart(2, '0');
209+
210+
let a = 'ff';
211+
if (match[4] !== undefined) {
212+
a = Math.round(parseFloat(match[4]) * 255)
213+
.toString(16)
214+
.padStart(2, '0');
215+
}
216+
217+
if (a === 'ff') {
218+
return `#${r}${g}${b}`.toUpperCase();
219+
}
220+
return `#${r}${g}${b}${a}`.toUpperCase();
221+
}
222+
}
223+
224+
return null;
225+
}

src/web/normalization/tiptapHtmlNormalizer.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
checkboxHtmlForTiptap,
33
checkboxHtmlFromTiptap,
44
} from './checkboxHtmlNormalizer';
5+
import { normalizeColorToHex } from './colorNormalizer';
56
import { normalizeHtml } from './htmlNormalizer';
67

78
export function prepareHtmlForTiptap(
@@ -36,5 +37,30 @@ export function normalizeHtmlFromTiptap(html: string): string {
3637
return `<img${attrs}/>`;
3738
});
3839

40+
// Find all style="..." attributes in the HTML
41+
html = html.replace(/style="([^"]*)"/gi, (_, styleString: string) => {
42+
let updatedStyle = styleString;
43+
44+
// Convert color: <value> to hex
45+
updatedStyle = updatedStyle.replace(
46+
/(?:^|;)\s*(?<!-)color\s*:\s*([^;]+)/gi,
47+
(match, colorValue) => {
48+
const hex = normalizeColorToHex(colorValue);
49+
return hex ? match.replace(colorValue, hex) : match;
50+
}
51+
);
52+
53+
// Convert background-color: <value> to hex
54+
updatedStyle = updatedStyle.replace(
55+
/(?:^|;)\s*background-color\s*:\s*([^;]+)/gi,
56+
(match, bgColorValue) => {
57+
const hex = normalizeColorToHex(bgColorValue);
58+
return hex ? match.replace(bgColorValue, hex) : match;
59+
}
60+
);
61+
62+
return `style="${updatedStyle}"`;
63+
});
64+
3965
return `<html>${html}</html>`;
4066
}

0 commit comments

Comments
 (0)