Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ src/quantecon_book_theme/
- **Alternative**: Try `pip install --timeout=120` for longer timeout

### GitHub CLI (gh) Issues
- **Shell Escaping**: zsh has many issues with shell escaping, heredocs, and multiline strings. **ALWAYS** use the `create_file` tool to write content to a temporary file first, then pass it to `gh` with the `--body-file` flag. **NEVER** pass multiline body content directly via `--body` in the terminal.
- Example workflow for updating a PR description:
1. Use `create_file` to write the body to `/tmp/pr_body.md`
2. Run: `gh pr edit 123 --body-file /tmp/pr_body.md`
- Example workflow for creating a release:
1. Use `create_file` to write release notes to `/tmp/release_notes.md`
2. Run: `gh release create vX.Y.Z --title "Title" --notes-file /tmp/release_notes.md`
- **Output Capture**: Always write gh output to `/tmp` file for reliable capture: `gh pr view 123 2>&1 | tee /tmp/gh_output.txt`

### Missing Python 3.13
Expand Down Expand Up @@ -240,12 +247,14 @@ git push && git push origin vX.Y.Z

### 5. Create GitHub Release
```bash
# Write release notes to a temp file first (use create_file tool)
# Then create the release using --notes-file
gh release create vX.Y.Z \
--title "vX.Y.Z - Release Title" \
--notes "Release notes from CHANGELOG.md"
--notes-file /tmp/release_notes.md
```

**IMPORTANT**: Creating the GitHub release triggers the PyPI publish workflow automatically.
**IMPORTANT**: Always use `--notes-file` with a temp file instead of `--notes` to avoid zsh shell escaping issues. Creating the GitHub release triggers the PyPI publish workflow automatically.

### 6. Verify PyPI Publication
- Check GitHub Actions for successful PyPI publish workflow
Expand Down
57 changes: 57 additions & 0 deletions docs/configure.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,60 @@ This is a collapsible note that will show "Show" when collapsed and "Hide" when
```{toggle}
This is a toggle directive that will use the configured button text.
```

## Customizing Emphasis and Definition Colors

The theme applies custom colors to emphasis (italic), bold/strong, and definition list terms. By default:

- **Emphasis** (`em`): Green (`#2d9f42`) in light mode, lighter green (`#66bb6a`) in dark mode
- **Bold/Strong** (`strong`, `b`): Brown (`#8b4513`) in light mode, lighter brown (`#cd853f`) in dark mode
- **Definitions** (`dl dt`): Inherits from bold/strong color by default

You can override these colors using `html_theme_options`:

```python
html_theme_options = {
...
"emphasis_color": "#1a73e8",
"emphasis_color_dark": "#8ab4f8",
"strong_color": "#d93025",
"strong_color_dark": "#f28b82",
"definition_color": "#6a1b9a",
"definition_color_dark": "#ce93d8",
...
}
```

For Jupyter Book projects, add to your `_config.yml`:

```yaml
sphinx:
config:
html_theme_options:
emphasis_color: "#1a73e8"
emphasis_color_dark: "#8ab4f8"
strong_color: "#d93025"
strong_color_dark: "#f28b82"
definition_color: "#6a1b9a"
definition_color_dark: "#ce93d8"
```

| Option | Description | Default |
|---|---|---|
| `emphasis_color` | Color for `em` tags in light mode | `#2d9f42` (green) |
| `emphasis_color_dark` | Color for `em` tags in dark mode | `#66bb6a` (light green) |
| `strong_color` | Color for `strong`/`b` tags in light mode | `#8b4513` (brown) |
| `strong_color_dark` | Color for `strong`/`b` tags in dark mode | `#cd853f` (peru) |
| `definition_color` | Color for definition list terms (`dl dt`) in light mode | Inherits from `strong_color` |
| `definition_color_dark` | Color for definition list terms (`dl dt`) in dark mode | Inherits from `strong_color_dark` |

Any option left empty will use the theme's built-in default color. The `definition_color`
options target Sphinx definition lists, glossary terms, and field lists specifically,
while `strong_color` applies to all inline bold/strong text.

```{note}
Color values are validated at build time against safe CSS color patterns
(hex codes, named colors, `rgb()`/`hsl()` functions). Invalid values are
ignored and a warning is logged. Only use trusted color values from your
own configuration files.
```
44 changes: 44 additions & 0 deletions src/quantecon_book_theme/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pathlib import Path
import os
import hashlib
import re
from functools import lru_cache
import subprocess
from datetime import datetime, timezone
Expand Down Expand Up @@ -594,6 +595,48 @@ def _string_or_bool(var):
return var is None


# Valid CSS color pattern: hex (#RGB, #RRGGBB, #RRGGBBAA), named colors,
# rgb/rgba/hsl/hsla functions, or CSS keywords
_CSS_COLOR_RE = re.compile(
r"^("
r"#[0-9a-fA-F]{3,8}"
r"|[a-zA-Z]+"
r"|rgba?\([^)]+\)"
r"|hsla?\([^)]+\)"
r"|var\(--[a-zA-Z0-9-]+\)"
r")$"
)

_COLOR_OPTIONS = [
"emphasis_color",
"emphasis_color_dark",
"strong_color",
"strong_color_dark",
"definition_color",
"definition_color_dark",
]


def validate_color_options(app):
"""Validate that color theme options contain safe CSS color values.

Prevents CSS injection by ensuring color values match a known-safe pattern
(hex colors, named colors, rgb/hsl functions). Invalid values are replaced
with empty strings and a warning is logged.
"""
theme_options = app.config.html_theme_options
for option in _COLOR_OPTIONS:
value = theme_options.get(option, "")
if value and not _CSS_COLOR_RE.match(value.strip()):
SPHINX_LOGGER.warning(
"Ignoring invalid CSS color value for %s: %r. "
"Use hex (#RRGGBB), named colors, or rgb()/hsl() functions.",
option,
value,
)
theme_options[option] = ""


def setup(app):
# Configuration for Juypter Book
app.setup_extension("sphinx_book_theme")
Expand All @@ -603,6 +646,7 @@ def setup(app):

app.connect("html-page-context", add_hub_urls)
app.connect("builder-inited", add_plugins_list)
app.connect("builder-inited", validate_color_options)
app.connect("builder-inited", setup_pygments_css)
app.connect("html-page-context", hash_html_assets)
app.connect("html-page-context", add_pygments_style_class)
Expand Down
13 changes: 10 additions & 3 deletions src/quantecon_book_theme/assets/styles/_base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,21 @@ h4 {
em {
font-style: normal;
font-weight: 550;
color: colors.$emphasis;
color: var(--qe-emphasis-color, colors.$emphasis);
}

// Strong/bold styling - semi-bold weight with color for definitions
// Strong/bold styling - semi-bold weight with color
strong,
b {
font-weight: 550;
color: colors.$definition;
color: var(--qe-strong-color, colors.$definition);
}

// Definition list term styling - inherits from strong color by default
dl.simple dt,
dl.glossary dt,
dl.field-list dt {
color: var(--qe-definition-color, var(--qe-strong-color, colors.$definition));
}

li {
Expand Down
10 changes: 8 additions & 2 deletions src/quantecon_book_theme/assets/styles/_dark-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,18 @@ body.dark-theme {

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

strong,
b {
color: #cd853f; // Lighter brown for dark mode
color: var(--qe-strong-color, #cd853f); // Lighter brown for dark mode
}

dl.simple dt,
dl.glossary dt,
dl.field-list dt {
color: var(--qe-definition-color, var(--qe-strong-color, #cd853f));
}

.qe-toolbar__inner > ul > li > a,
Expand Down
30 changes: 30 additions & 0 deletions src/quantecon_book_theme/theme/quantecon_book_theme/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,36 @@
}
}
</style>
{%- endif %}

<!-- Custom emphasis, strong/bold, and definition colors -->
{%- if theme_emphasis_color or theme_strong_color or theme_definition_color %}
<style>
:root {
{%- if theme_emphasis_color %}
--qe-emphasis-color: {{ theme_emphasis_color }};
{%- endif %}
{%- if theme_strong_color %}
--qe-strong-color: {{ theme_strong_color }};
{%- endif %}
{%- if theme_definition_color %}
--qe-definition-color: {{ theme_definition_color }};
{%- endif %}
}
{%- if theme_emphasis_color_dark or theme_strong_color_dark or theme_definition_color_dark %}
body.dark-theme {
{%- if theme_emphasis_color_dark %}
--qe-emphasis-color: {{ theme_emphasis_color_dark }};
{%- endif %}
{%- if theme_strong_color_dark %}
--qe-strong-color: {{ theme_strong_color_dark }};
{%- endif %}
{%- if theme_definition_color_dark %}
--qe-definition-color: {{ theme_definition_color_dark }};
{%- endif %}
}
{%- endif %}
</style>
{%- endif %}

<span id="top"></span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,10 @@ twitter =
twitter_logo_url =
use_issues_button = False
use_repository_button = False

emphasis_color =
emphasis_color_dark =
strong_color =
strong_color_dark =
definition_color =
definition_color_dark =
Loading
Loading