Skip to content

Support block-specific style overrides #13611

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f541c9f
Working protype!
hmedina May 10, 2025
95ef08b
Media queries for light or dark style
hmedina May 13, 2025
39cc441
Merge branch 'sphinx-doc:master' into master
hmedina May 13, 2025
e2a6e04
Merge branch 'sphinx-doc:master' into master
hmedina May 29, 2025
b5c34d2
Unify pygments.css and pygments_dark.css
hmedina May 29, 2025
33a2183
Revert "Unify pygments.css and pygments_dark.css"
hmedina May 29, 2025
cac1808
Use separate style sheets; leave media queries out
hmedina May 29, 2025
a96870f
Fix `singlehtml` builder
hmedina May 30, 2025
3b2d3c9
Remove unnecessary parenthesis
hmedina May 30, 2025
6eebcf9
Add LaTeX builder
hmedina May 30, 2025
6aa37f4
Merge branch 'sphinx-doc:master' into master
hmedina Jun 2, 2025
5c2d244
Lint & style with Ruff
hmedina Jun 2, 2025
37a018f
Provide usage examples in docs for new directive options
hmedina Jun 2, 2025
a83b6af
Merge branch 'sphinx-doc:master' into master
hmedina Jun 2, 2025
a26d133
Add name to contributor list
hmedina Jun 2, 2025
957a5ee
Merge branch 'master' into master
hmedina Jun 3, 2025
349e051
Sphinx-lint; remove trailing whitespaces
hmedina Jun 3, 2025
e1a7929
Bugfix: fetcher of PygmentsBridge objects from string had wrong param…
hmedina Jun 3, 2025
5bc8dbf
Properly catch Nones; pass `mypy`
hmedina Jun 3, 2025
ae62908
Ruff, round 2
hmedina Jun 3, 2025
a8ac128
Ruff, formatting
hmedina Jun 3, 2025
0c3d5a7
Fix MyPy ?
hmedina Jun 3, 2025
b2b9c34
Merge remote-tracking branch 'upstream/master'
hmedina Jun 12, 2025
fa2a798
Fix docs-lint; line too long after adding PR number
hmedina Jun 12, 2025
35f782c
"sanitize" LaTeX command prefix from user-specified style
hmedina Jun 14, 2025
7fab81e
Fix ruff format?
hmedina Jun 14, 2025
4b09baa
LaTeX: fix style arg lacking, refactor to achieve full LaTeX support
jfbu Jun 15, 2025
3a7d4a6
Merge branch 'master' into multistyle
jfbu Jun 17, 2025
27b5239
Remove left-over curly brace pairs in LaTeX code in highlighting.py
jfbu Jun 17, 2025
901a5de
Merge branch 'sphinx-doc:master' into master
hmedina Jun 19, 2025
83932f7
Simplify LaTeX code
hmedina Jun 19, 2025
f5d5178
Simplify HTML code
hmedina Jun 19, 2025
37fabbe
LaTeX: implement support for custom background color
jfbu Jun 20, 2025
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
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Contributors
* Filip Vavera -- napoleon todo directive
* Glenn Matthews -- python domain signature improvements
* Gregory Szorc -- performance improvements
* Héctor Medina Abarca -- per-code-block highlighting style overrides
* Henrique Bastos -- SVG support for graphviz extension
* Hernan Grecco -- search improvements
* Hong Xu -- svg support in imgmath extension and various bug fixes
Expand Down
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ Features added
Patch by Adam Turner.
* #13647: LaTeX: allow more cases of table nesting.
Patch by Jean-François B.
* #13611: Allow `Pygments style <https://pygments.org/styles/>`_ overriding on a
per-block basis via new options (:rst:dir:`code-block:style-light` and
:rst:dir:`code-block:style-dark`) for the :rst:dir:`code-block`,
:rst:dir:`sourcecode`, :rst:dir:`literalinclude` and :rst:dir:`code`.
Patch by Héctor Medina Abarca.

Bugs fixed
----------
Expand Down
55 changes: 51 additions & 4 deletions doc/usage/restructuredtext/directives.rst
Original file line number Diff line number Diff line change
Expand Up @@ -728,13 +728,15 @@ values are supported:
* ... and any other `lexer alias that Pygments supports`__

If highlighting with the selected language fails (i.e. Pygments emits an
"Error" token), the block is not highlighted in any way.
"Error" token), the block is not highlighted in any way. Per-block highlighting
styles can be specified for directives :rst:dir:`code-block`,
:rst:dir:`sourcecode`, :rst:dir:`literalinclude`, and :rst:dir:`code`.

.. important::

The list of lexer aliases supported is tied to the Pygment version. If you
want to ensure consistent highlighting, you should fix your version of
Pygments.
The list of lexer and style aliases supported is tied to the Pygment
version. If you want to ensure consistent highlighting, you should fix your
version of Pygments.

__ https://pygments.org/docs/lexers

Expand Down Expand Up @@ -903,6 +905,51 @@ __ https://pygments.org/docs/lexers
.. versionchanged:: 3.5
Support automatic dedent.

.. rst:directive:option:: style-light: style name
style-dark: style name
:type: the name of a style to use

Pygments includes `various highlighting styles
<https://pygments.org/styles/>`_, and supports `custom ones
<https://pygments.org/docs/styledevelopment/>`_ installed as
plugins. This option accepts any valid style name and will apply it to
this code block, overriding any default in :confval:`pygments_style`
config value. Some builder and theme configurations (e.g.
:ref:`HTML <builders>` & `Python Docs Theme <https://pypi.org/project/python-docs-theme/>`_) will
accept both `light` and `dark` options, and switch appropriately; others
may support only one style (e.g. PDF), in which case `style-light` takes
precedence. For example::

.. code-block:: python

print('Code with default styling')


Renders as:

.. code-block:: python

print('Code with default styling')


While this code::

.. code-block:: python
:style-light: tango

print('Code with a style override')


Renders as:

.. code-block:: python
:style-light: tango

print('Code with a style override')

.. versionadded:: 8.3


.. rst:directive:: .. literalinclude:: filename

Longer displays of verbatim text may be included by storing the example text
Expand Down
81 changes: 73 additions & 8 deletions sphinx/builders/html/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@

if TYPE_CHECKING:
from collections.abc import Iterator, Set
from typing import Any, TypeAlias
from typing import Any, TypeAlias, TypedDict

from docutils.nodes import Node
from docutils.readers import Reader
Expand Down Expand Up @@ -262,6 +262,19 @@ def init_highlighter(self) -> None:
else:
self.dark_highlighter = None

# Maps a code block's identifier to requested light and dark styles.
# This is populated by the writer / translator as it invokes
# the visit_literal_block method.
# The information is also used in the selectors of the CSS file(s).
if TYPE_CHECKING:

class spec_highlighter(TypedDict):
bridge: PygmentsBridge
ids: list[int]

self.specialized_dark_lighters: dict[str, spec_highlighter] = {}
self.specialized_light_lighters: dict[str, spec_highlighter] = {}

@property
def css_files(self) -> list[_CascadingStyleSheet]:
_deprecation_warning(
Expand Down Expand Up @@ -691,6 +704,7 @@ def write_doc_serialized(self, docname: str, doctree: nodes.document) -> None:
self.index_page(docname, doctree, title)

def finish(self) -> None:
self.finish_tasks.add_task(self.create_pygments_style_file)
self.finish_tasks.add_task(self.gen_indices)
self.finish_tasks.add_task(self.gen_pages_from_extensions)
self.finish_tasks.add_task(self.gen_additional_pages)
Expand Down Expand Up @@ -827,16 +841,68 @@ def to_relpath(f: str) -> str:
err,
)

def update_override_styles_dark(self, style: str, id: int) -> PygmentsBridge:
"""Update the tracker of highlighting styles with a possibly new dark-mode style;
return the PygmentsBridge object associated with said style.
"""
if style in self.specialized_dark_lighters:
self.specialized_dark_lighters[style]['ids'].append(id)
else:
pb = PygmentsBridge(dest='html', stylename=style)
self.specialized_dark_lighters[style] = {'bridge': pb, 'ids': [id]}
return self.specialized_dark_lighters[style]['bridge']

def update_override_styles_light(self, style: str, id: int) -> PygmentsBridge:
"""Update the tracker of highlighting styles with a possibly new light-mode style;
return the PygmentsBridge object associated with said style.
"""
if style in self.specialized_light_lighters:
self.specialized_light_lighters[style]['ids'].append(id)
else:
pb = PygmentsBridge(dest='html', stylename=style)
self.specialized_light_lighters[style] = {'bridge': pb, 'ids': [id]}
return self.specialized_light_lighters[style]['bridge']

def create_pygments_style_file(self) -> None:
"""Create a style file for pygments."""
"""Create style file(s) for Pygments."""
pyg_path = self._static_dir / 'pygments.css'
with open(pyg_path, 'w', encoding='utf-8') as f:
f.write(self.highlighter.get_stylesheet())
light_style = self.highlighter.formatter_args.get('style')
if light_style is None:
logger.warning(__('Default highlighter has no set style'))
else:
with open(pyg_path, 'w', encoding='utf-8') as f:
light_style_name = light_style.name
light_style_sheet = '/* CSS for style: {} */\n'.format(light_style_name)
light_style_sheet += self.highlighter.get_stylesheet()
if self.specialized_light_lighters:
for s_name, item in self.specialized_light_lighters.items():
light_style_sheet += '\n\n/* CSS for style: {} */\n'.format(
s_name
)
light_style_sheet += item['bridge'].get_stylesheet(item['ids'])
f.write(light_style_sheet)

if self.dark_highlighter:
dark_path = self._static_dir / 'pygments_dark.css'
with open(dark_path, 'w', encoding='utf-8') as f:
f.write(self.dark_highlighter.get_stylesheet())
dark_style = self.dark_highlighter.formatter_args.get('style')
if dark_style is None:
logger.warning(__('Default dark highlighter has no set style'))
else:
dark_path = self._static_dir / 'pygments_dark.css'
with open(dark_path, 'w', encoding='utf-8') as f:
dark_style_name = dark_style.name
dark_style_sheet = '/* CSS for style: {} */\n'.format(
dark_style_name
)
dark_style_sheet += self.dark_highlighter.get_stylesheet()
if self.specialized_dark_lighters:
for s_name, item in self.specialized_dark_lighters.items():
dark_style_sheet += '\n\n/* CSS for style: {} */\n'.format(
s_name
)
dark_style_sheet += item['bridge'].get_stylesheet(
item['ids']
)
f.write(dark_style_sheet)

def copy_translation_js(self) -> None:
"""Copy a JavaScript file for translations."""
Expand Down Expand Up @@ -930,7 +996,6 @@ def copy_static_files(self) -> None:
if self.indexer is not None:
context.update(self.indexer.context_for_searchtool())

self.create_pygments_style_file()
self.copy_translation_js()
self.copy_stemmer_js()
self.copy_theme_static_files(context)
Expand Down
26 changes: 23 additions & 3 deletions sphinx/builders/latex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ def init(self) -> None:
self.docnames: Iterable[str] = {}
self.document_data: list[tuple[str, str, str, str, str, bool]] = []
self.themes = ThemeFactory(srcdir=self.srcdir, config=self.config)
self.specialized_highlighters: dict[str, highlighting.PygmentsBridge] = {}
texescape.init()

self.init_context()
Expand Down Expand Up @@ -272,23 +273,41 @@ def init_multilingual(self) -> None:

self.context['multilingual'] = f'{self.context["polyglossia"]}\n{language}'

def update_override_styles(self, style: str) -> highlighting.PygmentsBridge:
"""Update the tracker of highlighting styles with a possibly new style;
return the PygmentsBridge object associated with said style.
"""
if style in self.specialized_highlighters:
return self.specialized_highlighters[style]
else:
pb = highlighting.PygmentsBridge(dest='latex', stylename=style)
self.specialized_highlighters[style] = pb
return pb

def write_stylesheet(self) -> None:
highlighter = highlighting.PygmentsBridge('latex', self.config.pygments_style)
stylesheet = self.outdir / 'sphinxhighlight.sty'
with open(stylesheet, 'w', encoding='utf-8') as f:
f.write('\\NeedsTeXFormat{LaTeX2e}[1995/12/01]\n')
f.write(
'\\ProvidesPackage{sphinxhighlight}'
'[2022/06/30 stylesheet for highlighting with pygments]\n'
'[2025/06/15 stylesheet for highlighting with pygments]\n'
)
f.write(
'% Its contents depend on pygments_style configuration variable.\n\n'
'% Its contents depend on pygments_style configuration variable.\n'
'% And also on encountered code-blocks :style-light: options.\n\n'
)
f.write(highlighter.get_stylesheet())
if self.specialized_highlighters:
specialized_styles = []
for style_name, pyg_bridge in self.specialized_highlighters.items():
specialized_style = '\n% Stylesheet for style {}'.format(style_name)
specialized_style += pyg_bridge.get_stylesheet(style_name)
specialized_styles.append(specialized_style)
f.write('\n'.join(specialized_styles))

def prepare_writing(self, docnames: Set[str]) -> None:
self.init_document_data()
self.write_stylesheet()

def copy_assets(self) -> None:
self.copy_support_files()
Expand Down Expand Up @@ -413,6 +432,7 @@ def assemble_doctree(
return largetree

def finish(self) -> None:
self.write_stylesheet()
self.copy_image_files()
self.write_message_catalog()

Expand Down
1 change: 1 addition & 0 deletions sphinx/builders/singlehtml.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ def finish(self) -> None:
self.copy_image_files()
self.copy_download_files()
self.copy_static_files()
self.create_pygments_style_file()
self.copy_extra_files()
self.write_buildinfo()
self.dump_inventory()
Expand Down
8 changes: 8 additions & 0 deletions sphinx/directives/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ class CodeBlock(SphinxDirective):
'caption': directives.unchanged_required,
'class': directives.class_option,
'name': directives.unchanged,
'style-light': directives.unchanged,
'style-dark': directives.unchanged,
}

def run(self) -> list[Node]:
Expand Down Expand Up @@ -162,6 +164,8 @@ def run(self) -> list[Node]:
self.env.current_document.highlight_language
or self.config.highlight_language
)
literal['style-light'] = self.options.get('style-light')
literal['style-dark'] = self.options.get('style-dark')
extra_args = literal['highlight_args'] = {}
if hl_lines is not None:
extra_args['hl_lines'] = hl_lines
Expand Down Expand Up @@ -425,6 +429,8 @@ class LiteralInclude(SphinxDirective):
'lineno-match': directives.flag,
'tab-width': int,
'language': directives.unchanged_required,
'style-light': directives.unchanged,
'style-dark': directives.unchanged,
'force': directives.flag,
'encoding': directives.encoding,
'pyobject': directives.unchanged_required,
Expand Down Expand Up @@ -468,6 +474,8 @@ def run(self) -> list[Node]:
retnode['language'] = 'udiff'
elif 'language' in self.options:
retnode['language'] = self.options['language']
retnode['style-light'] = self.options.get('style-light')
retnode['style-dark'] = self.options.get('style-dark')
if (
'linenos' in self.options
or 'lineno-start' in self.options
Expand Down
5 changes: 5 additions & 0 deletions sphinx/directives/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ class Code(SphinxDirective):
'class': directives.class_option,
'force': directives.flag,
'name': directives.unchanged,
'style-light': directives.unchanged,
'style-dark': directives.unchanged,
'number-lines': optional_int,
}
has_content = True
Expand Down Expand Up @@ -124,6 +126,9 @@ def run(self) -> list[Node]:
or self.config.highlight_language
)

node['style-light'] = self.options.get('style-light')
node['style-dark'] = self.options.get('style-dark')

if 'number-lines' in self.options:
node['linenos'] = True

Expand Down
Loading
Loading