Skip to content

Commit 59d2d5e

Browse files
committed
feat: Allow customizing emphasis and bold text colors via html_theme_options
Add four new theme options to customize emphasis (em) and definition (strong/b) text colors for both light and dark modes: - emphasis_color: Color for em tags in light mode (default: #2d9f42) - emphasis_color_dark: Color for em tags in dark mode (default: #66bb6a) - definition_color: Color for strong/b tags in light mode (default: #8b4513) - definition_color_dark: Color for strong/b tags in dark mode (default: #cd853f) Implementation approach: - Replace hardcoded SCSS colors with CSS custom properties (var()) with fallback values matching the original defaults - Register new options in theme.conf with empty defaults - Inject custom property overrides via inline style in layout.html when theme options are set This is fully backward-compatible: existing sites see no visual change. When options are left empty, CSS var() fallbacks provide the original colors. Closes #355
1 parent 0358a7c commit 59d2d5e

File tree

6 files changed

+263
-4
lines changed

6 files changed

+263
-4
lines changed

docs/configure.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,3 +323,44 @@ This is a collapsible note that will show "Show" when collapsed and "Hide" when
323323
```{toggle}
324324
This is a toggle directive that will use the configured button text.
325325
```
326+
327+
## Customizing Emphasis and Definition Colors
328+
329+
The theme applies custom colors to emphasis (italic) and bold/strong text. By default:
330+
331+
- **Emphasis** (`em`): Green (`#2d9f42`) in light mode, lighter green (`#66bb6a`) in dark mode
332+
- **Bold/Strong** (`strong`, `b`): Brown (`#8b4513`) in light mode, lighter brown (`#cd853f`) in dark mode
333+
334+
You can override these colors using `html_theme_options`:
335+
336+
```python
337+
html_theme_options = {
338+
...
339+
"emphasis_color": "#1a73e8",
340+
"emphasis_color_dark": "#8ab4f8",
341+
"definition_color": "#d93025",
342+
"definition_color_dark": "#f28b82",
343+
...
344+
}
345+
```
346+
347+
For Jupyter Book projects, add to your `_config.yml`:
348+
349+
```yaml
350+
sphinx:
351+
config:
352+
html_theme_options:
353+
emphasis_color: "#1a73e8"
354+
emphasis_color_dark: "#8ab4f8"
355+
definition_color: "#d93025"
356+
definition_color_dark: "#f28b82"
357+
```
358+
359+
| Option | Description | Default |
360+
|---|---|---|
361+
| `emphasis_color` | Color for `em` tags in light mode | `#2d9f42` (green) |
362+
| `emphasis_color_dark` | Color for `em` tags in dark mode | `#66bb6a` (light green) |
363+
| `definition_color` | Color for `strong`/`b` tags in light mode | `#8b4513` (brown) |
364+
| `definition_color_dark` | Color for `strong`/`b` tags in dark mode | `#cd853f` (peru) |
365+
366+
Any option left empty will use the theme's built-in default color.

src/quantecon_book_theme/assets/styles/_base.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,14 @@ h4 {
151151
em {
152152
font-style: normal;
153153
font-weight: 550;
154-
color: colors.$emphasis;
154+
color: var(--qe-emphasis-color, colors.$emphasis);
155155
}
156156

157157
// Strong/bold styling - semi-bold weight with color for definitions
158158
strong,
159159
b {
160160
font-weight: 550;
161-
color: colors.$definition;
161+
color: var(--qe-definition-color, colors.$definition);
162162
}
163163

164164
li {

src/quantecon_book_theme/assets/styles/_dark-theme.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ body.dark-theme {
2323

2424
// Lighter colors for emphasis and strong in dark mode
2525
em {
26-
color: #66bb6a; // Lighter green for dark mode
26+
color: var(--qe-emphasis-color, #66bb6a); // Lighter green for dark mode
2727
}
2828

2929
strong,
3030
b {
31-
color: #cd853f; // Lighter brown for dark mode
31+
color: var(--qe-definition-color, #cd853f); // Lighter brown for dark mode
3232
}
3333

3434
.qe-toolbar__inner > ul > li > a,

src/quantecon_book_theme/theme/quantecon_book_theme/layout.html

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,30 @@
119119
}
120120
}
121121
</style>
122+
{%- endif %}
123+
124+
<!-- Custom emphasis and definition colors -->
125+
{%- if theme_emphasis_color or theme_definition_color %}
126+
<style>
127+
:root {
128+
{%- if theme_emphasis_color %}
129+
--qe-emphasis-color: {{ theme_emphasis_color }};
130+
{%- endif %}
131+
{%- if theme_definition_color %}
132+
--qe-definition-color: {{ theme_definition_color }};
133+
{%- endif %}
134+
}
135+
{%- if theme_emphasis_color_dark or theme_definition_color_dark %}
136+
body.dark-theme {
137+
{%- if theme_emphasis_color_dark %}
138+
--qe-emphasis-color: {{ theme_emphasis_color_dark }};
139+
{%- endif %}
140+
{%- if theme_definition_color_dark %}
141+
--qe-definition-color: {{ theme_definition_color_dark }};
142+
{%- endif %}
143+
}
144+
{%- endif %}
145+
</style>
122146
{%- endif %}
123147

124148
<span id="top"></span>

src/quantecon_book_theme/theme/quantecon_book_theme/theme.conf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,8 @@ twitter =
3636
twitter_logo_url =
3737
use_issues_button = False
3838
use_repository_button = False
39+
40+
emphasis_color =
41+
emphasis_color_dark =
42+
definition_color =
43+
definition_color_dark =

tests/test_custom_colors.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
"""
2+
Tests for customizable emphasis and definition text colors.
3+
4+
Verifies that:
5+
- Theme options are registered in theme.conf
6+
- CSS custom properties are used in SCSS source files
7+
- CSS custom properties appear in compiled CSS output
8+
- Layout template injects custom color styles when options are set
9+
"""
10+
11+
from pathlib import Path
12+
13+
14+
# Paths
15+
THEME_DIR = Path("src/quantecon_book_theme/theme/quantecon_book_theme")
16+
ASSETS_DIR = Path("src/quantecon_book_theme/assets")
17+
18+
19+
class TestCustomColorThemeOptions:
20+
"""Test that color customization options are registered in theme.conf."""
21+
22+
def test_emphasis_color_option_exists(self):
23+
"""theme.conf should define emphasis_color option."""
24+
content = (THEME_DIR / "theme.conf").read_text()
25+
assert "emphasis_color =" in content
26+
27+
def test_emphasis_color_dark_option_exists(self):
28+
"""theme.conf should define emphasis_color_dark option."""
29+
content = (THEME_DIR / "theme.conf").read_text()
30+
assert "emphasis_color_dark =" in content
31+
32+
def test_definition_color_option_exists(self):
33+
"""theme.conf should define definition_color option."""
34+
content = (THEME_DIR / "theme.conf").read_text()
35+
assert "definition_color =" in content
36+
37+
def test_definition_color_dark_option_exists(self):
38+
"""theme.conf should define definition_color_dark option."""
39+
content = (THEME_DIR / "theme.conf").read_text()
40+
assert "definition_color_dark =" in content
41+
42+
def test_options_default_to_empty(self):
43+
"""All color options should default to empty (use CSS fallback)."""
44+
content = (THEME_DIR / "theme.conf").read_text()
45+
for option in [
46+
"emphasis_color",
47+
"emphasis_color_dark",
48+
"definition_color",
49+
"definition_color_dark",
50+
]:
51+
# Each option should appear as "option_name =" with no value
52+
# Use a targeted check to avoid matching substrings
53+
lines = content.splitlines()
54+
matching = [
55+
line for line in lines if line.strip().startswith(f"{option} =")
56+
]
57+
assert len(matching) == 1, f"Expected exactly one '{option}' option"
58+
assert matching[0].strip() == f"{option} =", (
59+
f"'{option}' should default to empty string"
60+
)
61+
62+
63+
class TestCustomColorCSSVariables:
64+
"""Test that SCSS uses CSS custom properties for emphasis/definition."""
65+
66+
def test_base_scss_uses_emphasis_variable(self):
67+
"""_base.scss should use --qe-emphasis-color CSS variable for em."""
68+
content = (ASSETS_DIR / "styles" / "_base.scss").read_text()
69+
assert "var(--qe-emphasis-color" in content
70+
71+
def test_base_scss_uses_definition_variable(self):
72+
"""_base.scss should use --qe-definition-color CSS variable for strong."""
73+
content = (ASSETS_DIR / "styles" / "_base.scss").read_text()
74+
assert "var(--qe-definition-color" in content
75+
76+
def test_base_scss_has_emphasis_fallback(self):
77+
"""_base.scss should have SCSS fallback value for emphasis color."""
78+
content = (ASSETS_DIR / "styles" / "_base.scss").read_text()
79+
assert "var(--qe-emphasis-color, colors.$emphasis)" in content
80+
81+
def test_base_scss_has_definition_fallback(self):
82+
"""_base.scss should have SCSS fallback value for definition color."""
83+
content = (ASSETS_DIR / "styles" / "_base.scss").read_text()
84+
assert "var(--qe-definition-color, colors.$definition)" in content
85+
86+
def test_dark_theme_uses_emphasis_variable(self):
87+
"""_dark-theme.scss should use --qe-emphasis-color CSS variable."""
88+
content = (ASSETS_DIR / "styles" / "_dark-theme.scss").read_text()
89+
assert "var(--qe-emphasis-color" in content
90+
91+
def test_dark_theme_uses_definition_variable(self):
92+
"""_dark-theme.scss should use --qe-definition-color CSS variable."""
93+
content = (ASSETS_DIR / "styles" / "_dark-theme.scss").read_text()
94+
assert "var(--qe-definition-color" in content
95+
96+
def test_dark_theme_has_emphasis_fallback(self):
97+
"""_dark-theme.scss should have fallback for emphasis in dark mode."""
98+
content = (ASSETS_DIR / "styles" / "_dark-theme.scss").read_text()
99+
assert "var(--qe-emphasis-color, #66bb6a)" in content
100+
101+
def test_dark_theme_has_definition_fallback(self):
102+
"""_dark-theme.scss should have fallback for definition in dark mode."""
103+
content = (ASSETS_DIR / "styles" / "_dark-theme.scss").read_text()
104+
assert "var(--qe-definition-color, #cd853f)" in content
105+
106+
107+
class TestCustomColorCompiledCSS:
108+
"""Test that compiled CSS contains custom properties."""
109+
110+
def test_compiled_css_has_emphasis_variable(self):
111+
"""Compiled CSS should contain --qe-emphasis-color variable."""
112+
css_path = THEME_DIR / "static" / "styles" / "quantecon-book-theme.css"
113+
content = css_path.read_text()
114+
assert "--qe-emphasis-color" in content
115+
116+
def test_compiled_css_has_definition_variable(self):
117+
"""Compiled CSS should contain --qe-definition-color variable."""
118+
css_path = THEME_DIR / "static" / "styles" / "quantecon-book-theme.css"
119+
content = css_path.read_text()
120+
assert "--qe-definition-color" in content
121+
122+
def test_compiled_css_em_uses_variable(self):
123+
"""Compiled CSS em rule should use var(--qe-emphasis-color)."""
124+
css_path = THEME_DIR / "static" / "styles" / "quantecon-book-theme.css"
125+
content = css_path.read_text()
126+
assert "var(--qe-emphasis-color" in content
127+
128+
def test_compiled_css_strong_uses_variable(self):
129+
"""Compiled CSS strong rule should use var(--qe-definition-color)."""
130+
css_path = THEME_DIR / "static" / "styles" / "quantecon-book-theme.css"
131+
content = css_path.read_text()
132+
assert "var(--qe-definition-color" in content
133+
134+
135+
class TestCustomColorLayoutTemplate:
136+
"""Test that layout.html injects custom color styles."""
137+
138+
def _read_layout(self):
139+
return (THEME_DIR / "layout.html").read_text()
140+
141+
def test_template_checks_emphasis_color(self):
142+
"""Layout template should conditionally check theme_emphasis_color."""
143+
content = self._read_layout()
144+
assert "theme_emphasis_color" in content
145+
146+
def test_template_checks_definition_color(self):
147+
"""Layout template should conditionally check theme_definition_color."""
148+
content = self._read_layout()
149+
assert "theme_definition_color" in content
150+
151+
def test_template_checks_emphasis_color_dark(self):
152+
"""Layout template should conditionally check theme_emphasis_color_dark."""
153+
content = self._read_layout()
154+
assert "theme_emphasis_color_dark" in content
155+
156+
def test_template_checks_definition_color_dark(self):
157+
"""Layout template should conditionally check theme_definition_color_dark."""
158+
content = self._read_layout()
159+
assert "theme_definition_color_dark" in content
160+
161+
def test_template_sets_emphasis_css_variable(self):
162+
"""Layout template should set --qe-emphasis-color CSS variable."""
163+
content = self._read_layout()
164+
assert "--qe-emphasis-color: {{ theme_emphasis_color }}" in content
165+
166+
def test_template_sets_definition_css_variable(self):
167+
"""Layout template should set --qe-definition-color CSS variable."""
168+
content = self._read_layout()
169+
assert "--qe-definition-color: {{ theme_definition_color }}" in content
170+
171+
def test_template_sets_dark_emphasis_css_variable(self):
172+
"""Layout template should set --qe-emphasis-color for dark mode."""
173+
content = self._read_layout()
174+
assert "--qe-emphasis-color: {{ theme_emphasis_color_dark }}" in content
175+
176+
def test_template_sets_dark_definition_css_variable(self):
177+
"""Layout template should set --qe-definition-color for dark mode."""
178+
content = self._read_layout()
179+
assert "--qe-definition-color: {{ theme_definition_color_dark }}" in content
180+
181+
def test_template_dark_mode_uses_body_dark_theme(self):
182+
"""Layout template should target body.dark-theme for dark colors."""
183+
content = self._read_layout()
184+
assert "body.dark-theme" in content
185+
186+
def test_template_light_mode_uses_root(self):
187+
"""Layout template should target :root for light mode colors."""
188+
content = self._read_layout()
189+
assert ":root" in content

0 commit comments

Comments
 (0)