from execnb.nbio import * from fastcore.all import * from fasthtml.common import * from inspect import getsource from IPython.display import display, HTML import pygments from pygments import highlight from pygments.lexers import PythonLexer from pygments.formatters import HtmlFormatter
audrey.feldroy.com
The experimental notebooks of Audrey M. Roy Greenfeld. This website and all its notebooks are open-source at github.com/audreyfeldroy/audrey.feldroy.com
# How I Fixed CSS Scope Leakage in Pygments Syntax Highlighting
by Audrey M. Roy Greenfeld | Fri, Dec 27, 2024
This notebook shows how to: * Get and use Pygments styles programmatically * Extract and display the source code from Python functions * Apply different Pygments syntax highlighting to different cells of the same notebook with proper CSS scoping * Use Pygments-highlighted code in a FastHTML FastTag
Pygments Styles
I was getting all Pygments styles the hard way in my previous notebooks. There's a method for getting the highlight style names via Python:
styles = L(pygments.styles.get_all_styles()) print(styles)
Inspect and getsource
Let's grab a function to highlight. How about read_nb
from execnb:
rn = getsource(read_nb) rn
We have to print it to see it nicely:
print(rn)
Pygments highlight
As in previous posts, we call highlight
to highlight a Python code block like this:
h = highlight(rn, PythonLexer(), HtmlFormatter(style='tango')) h
Then to display that in a notebook:
HTML(h)
Highlighting Code In-Notebook
Putting highlight
and HTML
into a function together, building up from above:
def show(c): return HTML(highlight(c, PythonLexer(), HtmlFormatter(style='tango')))
show(rn)
Function Getting Its Own Source
To get some source code to highlight without having to read a notebook:
def get_myself(): return getsource(get_myself)
get_myself()
Function Highlighting Itself
Putting together highlight
, HTML
, and getsource
:
def show(c=None): if not c: c = getsource(show) return HTML(highlight(c, PythonLexer(), HtmlFormatter(style='tango')))
show()
Adding a style
Arg
I wanted to show my code with a particular Pygments style:
def show(c=None, style='tango'): if not c: c = getsource(show) return HTML(highlight(c, PythonLexer(), HtmlFormatter(style=style)))
show(style='zenburn')
Something's not right here. That showed no colors.
Understanding Pygments Style Defs
In Pygments, style defs are CSS style definitions:
sd = HtmlFormatter(style='zenburn').get_style_defs() sd[:200]
s = L(sd.splitlines()) s
s[0]
s[6]
s[10]
Looking at Hex Colors With FastTags
In style zenburn
, comments are colored in #7f9f7f
. Let's see what this looks like with a Div
FastTag:
cdiv = Div('#7f9f7f', style="background-color:#7f9f7f;") cdiv
HTML(to_xml(cdiv))
def show_color(c): return HTML(to_xml(Div(c, style=f"background-color:{c};")))
Keywords in zenburn
are colored with #efdcbc
:
show_color("#efdcbc")
Pygments Styles in FastHTML FastTags
Putting zenburn
comment and keyword styles in a Style
FastTag:
Style(s[6], s[10])
Pygments Highlighting in FastTags
Recall Pygments highlight
from earlier generates a div
containing pre
full of span
tags:
h = highlight(rn, PythonLexer(), HtmlFormatter(style='tango')) print(h)
This is a nice string of HTML to use with FastTags. I use NotStr
to make it work well with a Div
FastTag:
Div(NotStr(h), id="container")
Adding style:
styled_container = Div(Style(s[6], s[10]), NotStr(h), id="container") styled_container
To display it in-notebook here:
HTML(to_xml(styled_container))
Pygments Background Color
The Pygments get_style_defs
docs say you can specify a CSS selector to prefix styles with:
sd = HtmlFormatter(style='zenburn').get_style_defs('.highlight') sd[:500]
I see all the zenburn style defs with background colors are early on:
Style(sd[:600])
show_color("#484848")
show_color("#353535")
styled_container = Div(Style(sd), NotStr(h), id="container") HTML(to_xml(styled_container))
Combining Everything Into show
Let's combine everything we've learned into a function:
def show(c=None, style='monokai'): if not c: c = getsource(show) fm = HtmlFormatter(style=style) h = highlight(c, PythonLexer(), fm) sd = fm.get_style_defs('.highlight') styled_container = Div(Style(sd), NotStr(h), id="container") return HTML(to_xml(styled_container))
show(style='monokai')
show(style='lightbulb')
Fixing CSS Scope Leakage
Let's see if we can customize the highlight
class
fm = HtmlFormatter(style='monokai') h = highlight("print('Hi')", PythonLexer(), fm) h
def show(c=None, style='monokai'): if not c: c = getsource(show) fm = HtmlFormatter(style=style) h = highlight(c, PythonLexer(), fm) sd = fm.get_style_defs(f'#{style}') styled_container = Div(Style(sd), NotStr(h), id=style) return HTML(to_xml(styled_container))
show(style='monokai')
show(style='lightbulb')
The above 2 appeared to work correctly, but this didn't, so something's wrong:
show(style='paraiso-light')
print(HtmlFormatter(style='paraiso-light').get_style_defs('#paraiso-light')[:1000])
The background color is supposed to be:
show_color("#a39e9b")
I think get_style_defs('#paraiso-light')
where that ID is on the parent div is too hacky here. I feel like <div class="highlight">
itself should get the ID.
print(HtmlFormatter(style='paraiso-light').get_background_style_defs('#paraiso-light')[:1000])
c = 'print("Hi")' fm = HtmlFormatter(style='paraiso-light', cssclass='audrey') h = highlight(c, PythonLexer(), fm) h
def show(c=None, style='monokai'): if not c: c = getsource(show) fm = HtmlFormatter(style=style, cssclass=style) h = highlight(c, PythonLexer(), fm) sd = fm.get_style_defs(f".{style}") styled_container = Div(Style(sd), NotStr(h), id=style) return HTML(to_xml(styled_container))
show(style='paraiso-light')
show(c="print('Hey')", style="dracula")
show(style="dracula")
show(style="gruvbox-dark")
show(style="solarized-dark")
Success! The cells above are syntax-highlighted without their CSS interfering with each other.
Summary
I've created a function for displaying Pygments syntax-highlighted code in Jupyter notebooks with properly-scoped CSS. To do this, I discovered I could:
- Use Pygments'
HtmlFormatter
'scssclass
parameter to change the name of the outerhighlight
div to the Pygments style name. - Use
get_style_defs
to scope style definitions to that name, to prevent CSS conflicts - Combine it into a tiny
show
function for use in future notebooks
def show(c=None, style='monokai'): if not c: c = getsource(show) fm = HtmlFormatter(style=style, cssclass=style) h = highlight(c, PythonLexer(), fm) sd = fm.get_style_defs(f".{style}") styled_container = Div(Style(sd), NotStr(h), id=style) return HTML(to_xml(styled_container))
You can use this to show code blocks in Jupyter notebooks, allowing different Pygments syntax highlighting themes in the same notebook. All without CSS leaking between Pygments styles.
© 2024-2025 Audrey M. Roy Greenfeld