Improving Pygments Code Block Display

Improving Pygments Code Block Display

Using Pygments, CSS and Ruff to improve how code blocks are displayed on my daily notebook blog.

from execnb.nbio import read_nb
from fasthtml.common import *
from fasthtml.jupyter import JupyUvi
from fastcore.utils import L
import pygments
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import HtmlFormatter

Listing My Notebooks' File Paths

My new favorite way of grabbing all my notebooks:

  • Set root based on if I'm in a notebook or not. (IN_NOTEBOOK comes from fasthtml.common)
  • Get a fastcore L list of my notebook paths sorted from newest to oldest
root = Path() if IN_NOTEBOOK else Path("nbs/")
nb_paths = L(root.glob("*.ipynb")).sorted(reverse=True)
nb_paths
(#50) [Path('2025-01-26-Improving-Pygments-Code-Block-Display.ipynb'),Path('2025-01-25-This-Site-Is-Now-Powered-by-This-Notebook-Part-2.ipynb'),Path('2025-01-24-Creating-In-Notebook-Images-for-Social-Media-With-PIL-Pillow.ipynb'),Path('2025-01-23-Troubleshooting-MonsterUI-on-This-Site.ipynb'),Path('2025-01-23-This-Site-Is-Now-Powered-by-This-Notebook.ipynb'),Path('2025-01-22-MonsterUI-Buttons-and-Links.ipynb'),Path('2025-01-22-Customizing-FastHTML-Headers-From-Notebook-Contents.ipynb'),Path('2025-01-21-SVG-Animation-in-FastHTML.ipynb'),Path('2025-01-20-Dark-and-Light-Mode-in-FastHTML.ipynb'),Path('2025-01-19-Genanki-and-fastcore.ipynb'),Path('2025-01-18-Alarm-Sounds-App.ipynb'),Path('2025-01-17-Alarm-Clock-Sounds.ipynb'),Path('2025-01-16-Cosine-Similarity-Breakdown-in-LaTeX.ipynb'),Path('2025-01-14-Constructing-SQLite-Tables-for-Notebooks-and-Search.ipynb'),Path('2025-01-13-SQLite-FTS5-Tokenizers-unicode61-and-ascii.ipynb'),Path('2025-01-12-A-Better-Notebook-Index-Page.ipynb'),Path('2025-01-11-NBClassic-Keyboard-Shortcuts-in-Command-and-Dual-Mode.ipynb'),Path('2025-01-10-Understanding-FastHTML-Routes-Requests-and-Redirects.ipynb'),Path('2025-01-09-Reading-and-Writing-Jupyter-Notebooks-With-Python.ipynb'),Path('2025-01-08-HTML-Title-Tag-in-FastHTML.ipynb')...]

Get Sample Cells to Work With

Here I grab a specific cell of a notebook that looks good to play with.

nb = read_nb(nb_paths[10])
def is_code(c): return c.cell_type=='code'
c = L(nb.cells).filter(is_code)[3]
c
{ 'cell_type': 'code',
  'execution_count': 16,
  'id': '00540ad9',
  'idx_': 10,
  'metadata': {},
  'outputs': [ { 'data': { 'text/plain': [ "(#3) [['Magandang hapon', 'Good "
                                           "afternoon'],['Magandang gabi', "
                                           "'Good evening'],['Paalam', "
                                           "'Goodbye']]"]},
                 'execution_count': 16,
                 'metadata': {},
                 'output_type': 'execute_result'}],
  'source': "notes = L(['Magandang hapon', 'Good afternoon'],\n"
            "    ['Magandang gabi', 'Good evening'],\n"
            "    ['Paalam', 'Goodbye'])\n"
            'notes'}

Style the Cell

My FT function for styling cells that I built up to in How I Fixed CSS Scope Leakage in Pygments Syntax Highlighting:

def StyledCode(c, style='monokai'):
    "A notebook cell styled as code, with style name as its css class for scope limiting"
    fm = HtmlFormatter(style=style, cssclass=style)
    h = highlight(c, PythonLexer(), fm)
    sd = fm.get_style_defs(f".{style}")
    return Div(Style(sd), NotStr(h), id=style)
show(StyledCode(c.source))
notes = L(['Magandang hapon', 'Good afternoon'],
    ['Magandang gabi', 'Good evening'],
    ['Paalam', 'Goodbye'])
notes

Pad the Cell

I just want to pad the top and bottom with 10 pixels here.

def StyledCode(c, style='monokai'):
    "A notebook cell styled as code, with style name as its css class for scope limiting"
    fm = HtmlFormatter(style=style, cssclass=f"my-{style}", prestyles="padding:10px 0;")
    h = highlight(c, PythonLexer(), fm)
    sd = fm.get_style_defs(f".my-{style}")
    return Style(sd), NotStr(h)

Pygments lets you add inline styles to <pre>. That's nice.

st, sc = StyledCode(c.source)
Div(sc)
<div><div class="my-monokai"><pre style="padding:10px 0;"><span></span><span class="n">notes</span> <span class="o">=</span> <span class="n">L</span><span class="p">([</span><span class="s1">&#39;Magandang hapon&#39;</span><span class="p">,</span> <span class="s1">&#39;Good afternoon&#39;</span><span class="p">],</span>
    <span class="p">[</span><span class="s1">&#39;Magandang gabi&#39;</span><span class="p">,</span> <span class="s1">&#39;Good evening&#39;</span><span class="p">],</span>
    <span class="p">[</span><span class="s1">&#39;Paalam&#39;</span><span class="p">,</span> <span class="s1">&#39;Goodbye&#39;</span><span class="p">])</span>
<span class="n">notes</span>
</pre></div>
</div>

show(StyledCode(c.source))
notes = L(['Magandang hapon', 'Good afternoon'],
    ['Magandang gabi', 'Good evening'],
    ['Paalam', 'Goodbye'])
notes

Success: the code is padded on top and bottom with 10px now.

Limiting Line Length on Mobile Devices

On my phone I counted that 52 characters is the maximum I can read on a line of code, before having to scroll. Let's see if Ruff's textwrap breaks lines in a way that keeps Python code valid.

def wrap_text(text, width=52):
    wrapper = textwrap.TextWrapper(width=width)
    wrapped_text = wrapper.fill(text)
    return wrapped_text
wrapped_text = wrap_text(c.source)
print(wrapped_text)
notes = L(['Magandang hapon', 'Good afternoon'],
['Magandang gabi', 'Good evening'],     ['Paalam',
'Goodbye']) notes
wrapped_text = wrap_text(c.source, width=43)
print(wrapped_text)
notes = L(['Magandang hapon', 'Good
afternoon'],     ['Magandang gabi', 'Good
evening'],     ['Paalam', 'Goodbye']) notes

Next Steps

If I continue this approach, the next steps will be:

  • Find another way to wrap lines while ensuring they're still valid Python after wrapping
  • Choose line length to return based on User-Agent header
  • Put it all together

Or I may just implement the padding and move on. We'll see.