From 3113af5edfa5fa4c6bc49cdbf981319cfcc1e9bf Mon Sep 17 00:00:00 2001 From: Jordan Robinson Date: Wed, 17 Sep 2025 21:14:38 +0100 Subject: [PATCH] add pygments code block syntax highlighting --- main/templates/assets/style.css | 3 ++- main/templates/main/page_detail.html | 15 +++++++++++++++ main/templates/main/post_detail.html | 15 +++++++++++++++ main/util.py | 17 ++++++++++++++++- main/views/general.py | 14 ++++++++++++++ 5 files changed, 62 insertions(+), 2 deletions(-) diff --git a/main/templates/assets/style.css b/main/templates/assets/style.css index 51c220bd21359b26bae08c19e3ea23a68312c2c0..5a79ca29435a91fb87506f49d1fc298ee5596234 100644 --- a/main/templates/assets/style.css +++ b/main/templates/assets/style.css @@ -268,8 +268,9 @@ td { } pre { - background: var(--airy-grey-color); + background: var(--airy-grey-color) !important; overflow-x: auto; + padding: 12px; } code { diff --git a/main/templates/main/page_detail.html b/main/templates/main/page_detail.html index 5c6179e3300e3a4e9c40175a8e7d83c7ff9d9555..599bd859f7c5502f92a8a129d2498fa7b85985df 100644 --- a/main/templates/main/page_detail.html +++ b/main/templates/main/page_detail.html @@ -8,6 +8,21 @@ {% include 'partials/rsl_license_head.html' %} {% endblock head_rsl_license %} +{% block head_extra %} + +{% endblock head_extra %} + {% block content %}
{% if blog_user.blog_title %} diff --git a/main/templates/main/post_detail.html b/main/templates/main/post_detail.html index 6dca49bc0890c81052f249dbaf3e77a61f6cc124..c656a02118c59b8ed3acb601acef191d383af5be 100644 --- a/main/templates/main/post_detail.html +++ b/main/templates/main/post_detail.html @@ -8,6 +8,21 @@ {% include 'partials/rsl_license_head.html' %} {% endblock head_rsl_license %} +{% block head_extra %} + +{% endblock head_extra %} + {% block content %}
{% if blog_user.blog_title %} diff --git a/main/util.py b/main/util.py index 815e3b1926adaf44887d4a90c0544e33304fc570..c2dd7ea2e96d9e71785478901ac2a0c5830e5149 100644 --- a/main/util.py +++ b/main/util.py @@ -15,6 +15,7 @@ from django.utils.text import slugify from pygments.formatters import HtmlFormatter from pygments.lexers import ClassNotFound, get_lexer_by_name, get_lexer_for_filename from l2m4m import LaTeX2MathMLExtension +from pygments import highlight from main import denylist, models @@ -24,13 +25,15 @@ from mdit_py_plugins.tasklists import tasklists_plugin from graphviz import Source md = ( - MarkdownIt("commonmark", {"html": True}) + MarkdownIt("commonmark", {"html": True, "highlight": None}) .enable("strikethrough") .enable("table") .use(footnote_plugin) .use(tasklists_plugin) ) +formatter = HtmlFormatter(nowrap=True) + # Define allowed CSS properties and SVG attributes ALLOWED_CSS_PROPERTIES = frozenset([ "azimuth", "background-color", "border-bottom-color", "border-collapse", @@ -162,6 +165,15 @@ def syntax_highlight(text): return processed_text +def highlight_code(code: str, lang: str) -> str: + try: + lexer = get_lexer_by_name(lang, stripall=True) + except Exception: + # fallback for unknown languages + return f"
{code}
" + return f'
' + \
+           highlight(code, lexer, formatter) + \
+           "
" def clean_html(dirty_html, strip_tags=False): allowed_tags = list(bleach.sanitizer.ALLOWED_TAGS) + denylist.ALLOWED_HTML_ELEMENTS + SVG_TAGS + MATHML_TAGS @@ -191,6 +203,9 @@ def fence_override(tokens, idx, options, env): return svg_str except Exception: return f"
{code}
" + + if lang: + return highlight_code(code, lang) # fallback to default renderer if default_fence: diff --git a/main/views/general.py b/main/views/general.py index df3c767647f147e3feb273728f62459276d8c9cc..2b354ddf06993ac6f0571dc4e7a36d1a2b1d13cb 100644 --- a/main/views/general.py +++ b/main/views/general.py @@ -41,6 +41,8 @@ from main import denylist, forms, models, util from main.sitemaps import PageSitemap, PostSitemap, StaticSitemap from main.views import billing +from pygments.formatters import HtmlFormatter + logger = logging.getLogger(__name__) @@ -334,6 +336,8 @@ def post_detail_redir(request, slug): class PostDetail(DetailView): model = models.Post + light_css = HtmlFormatter(style="default").get_style_defs('.code-block') + dark_css = HtmlFormatter(style="monokai").get_style_defs('.code-block') def get_queryset(self): queryset = models.Post.objects.filter(owner__username=self.request.subdomain) @@ -367,6 +371,10 @@ class PostDetail(DetailView): # Reading time calculation context["reading_time"] = util.reading_time(self.object.body) + # Pygments CSS for code highlighting + context["light_css"] = self.light_css + context["dark_css"] = self.dark_css + # do not record analytic if post is authed user's if ( self.request.user.is_authenticated @@ -931,6 +939,8 @@ class PageCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView): class PageDetail(DetailView): model = models.Page + light_css = HtmlFormatter(style="default").get_style_defs('.code-block') + dark_css = HtmlFormatter(style="monokai").get_style_defs('.code-block') def get_queryset(self): queryset = models.Page.objects.filter(owner__username=self.request.subdomain) @@ -955,6 +965,10 @@ class PageDetail(DetailView): context["license_url"] = license_url + # Pygments CSS for code highlighting + context["light_css"] = self.light_css + context["dark_css"] = self.dark_css + # do not record analytic if post is authed user's if ( self.request.user.is_authenticated