fix(reader): replace light callout backgrounds in dark mode (#4392)

Why:
- Dark mode sets theme foreground on html/body and rewrites black text,
  but EPUB callout boxes often keep white/light backgrounds from inline
  styles or publisher CSS unless override book color is enabled.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesus Eduardo Medina Gallo
2026-06-01 05:16:52 -04:00
committed by GitHub
parent 458ad7510c
commit 176b950c92
3 changed files with 102 additions and 0 deletions
@@ -571,6 +571,22 @@ describe('getColorStyles branches (via getStyles)', () => {
expect(css).not.toMatch(/body\.pbg\s*\{[^}]*background-color:[^}]*!important/);
});
it('overrides inline white backgrounds in dark mode when overrideColor is false', () => {
const vs = makeViewSettings({ overrideColor: false });
const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
const css = getStyles(vs, theme);
expect(css).toContain('background-color: #1a1a1a !important');
expect(css).toContain('background-color: #fff"]');
expect(css).toContain('body.theme-dark');
});
it('does not add inline white background overrides in light mode', () => {
const vs = makeViewSettings({ overrideColor: false });
const theme = makeThemeCode({ isDarkMode: false });
const css = getStyles(vs, theme);
expect(css).not.toContain('background-color: #fff"]');
});
it('applies dark mode link color lightblue when overrideColor is false', () => {
const vs = makeViewSettings({ overrideColor: false });
const theme = makeThemeCode({ isDarkMode: true, bg: '#1a1a1a', fg: '#e0e0e0' });
@@ -313,6 +313,27 @@ describe('transformStylesheet', () => {
expect(result).toBe(css);
});
});
describe('dark mode light backgrounds', () => {
it('rewrites white backgrounds in EPUB CSS to theme bg', () => {
localStorage.setItem('themeMode', 'dark');
localStorage.setItem('themeColor', 'default');
localStorage.setItem('systemIsDarkMode', 'false');
const css = '.callout { background-color: #ffffff; padding: 1em; }';
const result = transformStylesheet(css, VW, VH, VERTICAL);
expect(result).toMatch(/background-color:\s*#[0-9a-f]{6}/i);
expect(result).not.toContain('background-color: #ffffff');
localStorage.removeItem('themeMode');
});
it('leaves dark backgrounds unchanged in dark mode', () => {
localStorage.setItem('themeMode', 'dark');
const css = '.panel { background-color: #222; }';
const result = transformStylesheet(css, VW, VH, VERTICAL);
expect(result).toContain('background-color: #222');
localStorage.removeItem('themeMode');
});
});
});
describe('getFootnoteStyles', () => {
+65
View File
@@ -108,6 +108,54 @@ const getFontStyles = (
return fontStyles;
};
/** True for #fff, #f5f5f5, rgb(255,…), etc. Used when rewriting EPUB CSS in dark mode. */
const isLightCssColor = (value: string): boolean => {
const v = value.trim().toLowerCase();
if (v === 'white') return true;
const hex = v.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
if (hex) {
const h = hex[1]!;
const expand =
h.length === 3
? h
.split('')
.map((c) => c + c)
.join('')
: h;
const r = parseInt(expand.slice(0, 2), 16);
const g = parseInt(expand.slice(2, 4), 16);
const b = parseInt(expand.slice(4, 6), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.85;
}
const rgb = v.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
if (rgb?.[1] != null && rgb[2] != null && rgb[3] != null) {
const r = parseInt(rgb[1], 10);
const g = parseInt(rgb[2], 10);
const b = parseInt(rgb[3], 10);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.85;
}
return false;
};
const getDarkModeLightBackgroundOverrides = (bg: string) => `
/* Callout boxes often use inline white/light backgrounds while html/body set dark fg. */
*[style*="background-color: #fff"], *[style*="background-color:#fff"],
*[style*="background-color: #ffffff"], *[style*="background-color:#ffffff"],
*[style*="background-color: white"], *[style*="background-color:white"],
*[style*="background: #fff"], *[style*="background:#fff"],
*[style*="background: #ffffff"], *[style*="background:#ffffff"],
*[style*="background: white"], *[style*="background:white"],
*[style*="background-color: rgb(255"], *[style*="background-color:rgb(255"],
*[style*="background: rgb(255"], *[style*="background:rgb(255"] {
background-color: ${bg} !important;
}
body.theme-dark {
background-color: ${bg} !important;
}
`;
const getEinkSelectionStyles = () => {
return `
::selection {
@@ -222,6 +270,7 @@ const getColorStyles = (
*[style*="color:#000"], *[style*="color:#000000"], *[style*="color:black"] {
color: ${fg} !important;
}
${isDarkMode && !overrideColor ? getDarkModeLightBackgroundOverrides(bg) : ''}
/* for the Gutenberg eBooks */
#pg-header * {
color: inherit !important;
@@ -975,6 +1024,22 @@ export const transformStylesheet = (css: string, vw: number, vh: number, vertica
.replace(/([\s;])color\s*:\s*#000000/gi, '$1color: var(--theme-fg-color)')
.replace(/([\s;])color\s*:\s*#000/gi, '$1color: var(--theme-fg-color)')
.replace(/([\s;])color\s*:\s*rgb\(0,\s*0,\s*0\)/gi, '$1color: var(--theme-fg-color)');
const { isDarkMode, bg } = getThemeCode();
if (isDarkMode) {
css = css.replace(ruleRegex, (match, selector, block) => {
const rewritten = block.replace(
/background(-color)?\s*:\s*([^;!}]+)(\s*!important)?(?=\s*[;!}])/gi,
(decl: string, _prop: string, value: string, important?: string) => {
const raw = value.trim().split(/\s+/)[0] ?? '';
if (!isLightCssColor(raw)) return decl;
return `background-color: ${bg}${important ?? ''}`;
},
);
return rewritten === block ? match : selector + rewritten;
});
}
return css;
};