Note: This site is no longer built with the technique described below.
An important part of any tech blog is sharing code samples, terminal sessions, and configuration files. These should use a monospaced font
and have syntax highlighting for the best clarity and readability.
While the Wagtail ecosystem offers many 3rd-party packages and even two dedicated to highlighted code blocks, I found nothing that had exactly the flexibility I needed here. Normally I lean towards using off-the-shelf solutions, but this was a small enough feature to write and will play an important role in the blogging experience, so I decided to roll my own Wagtail code block.
Writing your own Wagtail Code Block
My Wagtail project has one app called blog
. In its models.py
I use the StreamField for freeform content:
# blog/models.py
from wagtail.core import blocks
from wagtail.core.models import Page
from wagtail.core.fields import StreamField
from wagtail.admin.edit_handlers import StreamFieldPanel
# ...
class BlogEntryPage(Page):
body = StreamField(
[
# We'll look at how to define these custom blocks later
("heading", HeadingBlock()),
("paragraph", blocks.RichTextBlock()),
("figure", FigureBlock()),
("quote", QuoteBlock()),
("code", CodeBlock()),
],
)
content_panels = Page.content_panels + [
StreamFieldPanel("body"),
]
The StreamField
uses blocks, most of which are custom types. Here's my CodeBlock
:
# blog/models.py
from django.utils.safestring import mark_safe
from wagtail.core import blocks
import pygments
import pygments.lexers
import pygments.formatters
# A custom block should normally inherit from StructBlock
class CodeBlock(blocks.StructBlock):
# Get all "lexers" from pygments to populate a language
# choices in the content editing interface
language = blocks.ChoiceBlock(
choices=[
(lexer[1][0], lexer[0])
for lexer in pygments.lexers.get_all_lexers()
if lexer[1]
],
)
filename = blocks.CharBlock(required=False, max_length=250)
# form_classname allows using custom CSS in the editing interface
code = blocks.TextBlock(form_classname="monospace")
def get_context(self, value, parent_context=None):
context = super().get_context(value, parent_context=parent_context)
# Get the user-selected lexer
lexer = pygments.lexers.get_lexer_by_name(value["language"])
formatter = pygments.formatters.get_formatter_by_name(
"html",
cssclass="pygments-highlight",
filename=value["filename"],
)
highlighted_code = pygments.highlight(value["code"], lexer, formatter)
# highlighted_code is an HTML string that we want to
# render without escaping, so use mark_safe
context["highlighted_code"] = mark_safe(highlighted_code)
return context
class Meta:
# Define the presentation in a separate
# file that consumes the context from above
template = "blog/blocks/code.html"
and the template:
<!-- blog/blocks/code.html -->
{{ highlighted_code }}
In this case Pygments' HTML formatter does the heavy lifting by rendering the HTML with appropriate class
attributes, so the template is trivial.
Styling
The highlighted_code
value contains HTML with class
attributes, but there is no CSS to style those elements unless you make it.
Pygments comes with built-in styles, but getting a static CSS file out of them takes a little bit of Python-fu. The trick is mentioned here:
The get_style_defs(arg='') method of a HtmlFormatter returns a string containing CSS rules for the CSS classes used by the formatter.
What this means is if you open a Python REPL session and get a reference to a Pygments style object, you can get the CSS out like this:
>>> import pygments.styles
>>> for s in styles:
... print(s)
...
default
emacs
friendly
colorful
autumn
murphy
manni
monokai
perldoc
pastie
# etc.
>>> style = pygments.styles.get_style_by_name("monokai")
>>> import pygments.formatters
>>> formatter = pygments.formatters.get_formatter_by_name("html", cssclass="pygments-highlight", style=style)
>>> print(formatter.get_style_defs())
pre { line-height: 125%; }
td.linenos pre { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; }
span.linenos { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; }
td.linenos pre.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.pygments-highlight .hll { background-color: #49483e }
.pygments-highlight .c { color: #75715e } /* Comment */
.pygments-highlight .err { color: #960050; background-color: #1e0010 } /* Error */
.pygments-highlight .k { color: #66d9ef } /* Keyword */
.pygments-highlight .l { color: #ae81ff } /* Literal */
.pygments-highlight .n { color: #f8f8f2 } /* Name */
.pygments-highlight .o { color: #f92672 } /* Operator */
.pygments-highlight .p { color: #f8f8f2 } /* Punctuation */
.pygments-highlight .ch { color: #75715e } /* Comment.Hashbang */
.pygments-highlight .cm { color: #75715e } /* Comment.Multiline */
.pygments-highlight .cp { color: #75715e } /* Comment.Preproc */
.pygments-highlight .cpf { color: #75715e } /* Comment.PreprocFile */
.pygments-highlight .c1 { color: #75715e } /* Comment.Single */
.pygments-highlight .cs { color: #75715e } /* Comment.Special */
.pygments-highlight .gd { color: #f92672 } /* Generic.Deleted */
.pygments-highlight .ge { font-style: italic } /* Generic.Emph */
.pygments-highlight .gi { color: #a6e22e } /* Generic.Inserted */
.pygments-highlight .go { color: #66d9ef } /* Generic.Output */
.pygments-highlight .gp { color: #f92672; font-weight: bold } /* Generic.Prompt */
.pygments-highlight .gs { font-weight: bold } /* Generic.Strong */
.pygments-highlight .gu { color: #75715e } /* Generic.Subheading */
.pygments-highlight .kc { color: #66d9ef } /* Keyword.Constant */
.pygments-highlight .kd { color: #66d9ef } /* Keyword.Declaration */
.pygments-highlight .kn { color: #f92672 } /* Keyword.Namespace */
.pygments-highlight .kp { color: #66d9ef } /* Keyword.Pseudo */
.pygments-highlight .kr { color: #66d9ef } /* Keyword.Reserved */
.pygments-highlight .kt { color: #66d9ef } /* Keyword.Type */
.pygments-highlight .ld { color: #e6db74 } /* Literal.Date */
.pygments-highlight .m { color: #ae81ff } /* Literal.Number */
.pygments-highlight .s { color: #e6db74 } /* Literal.String */
.pygments-highlight .na { color: #a6e22e } /* Name.Attribute */
.pygments-highlight .nb { color: #f8f8f2 } /* Name.Builtin */
.pygments-highlight .nc { color: #a6e22e } /* Name.Class */
.pygments-highlight .no { color: #66d9ef } /* Name.Constant */
.pygments-highlight .nd { color: #a6e22e } /* Name.Decorator */
.pygments-highlight .ni { color: #f8f8f2 } /* Name.Entity */
.pygments-highlight .ne { color: #a6e22e } /* Name.Exception */
.pygments-highlight .nf { color: #a6e22e } /* Name.Function */
.pygments-highlight .nl { color: #f8f8f2 } /* Name.Label */
.pygments-highlight .nn { color: #f8f8f2 } /* Name.Namespace */
.pygments-highlight .nx { color: #a6e22e } /* Name.Other */
.pygments-highlight .py { color: #f8f8f2 } /* Name.Property */
.pygments-highlight .nt { color: #f92672 } /* Name.Tag */
.pygments-highlight .nv { color: #f8f8f2 } /* Name.Variable */
.pygments-highlight .ow { color: #f92672 } /* Operator.Word */
.pygments-highlight .w { color: #f8f8f2 } /* Text.Whitespace */
.pygments-highlight .mb { color: #ae81ff } /* Literal.Number.Bin */
.pygments-highlight .mf { color: #ae81ff } /* Literal.Number.Float */
.pygments-highlight .mh { color: #ae81ff } /* Literal.Number.Hex */
.pygments-highlight .mi { color: #ae81ff } /* Literal.Number.Integer */
.pygments-highlight .mo { color: #ae81ff } /* Literal.Number.Oct */
.pygments-highlight .sa { color: #e6db74 } /* Literal.String.Affix */
.pygments-highlight .sb { color: #e6db74 } /* Literal.String.Backtick */
.pygments-highlight .sc { color: #e6db74 } /* Literal.String.Char */
.pygments-highlight .dl { color: #e6db74 } /* Literal.String.Delimiter */
.pygments-highlight .sd { color: #e6db74 } /* Literal.String.Doc */
.pygments-highlight .s2 { color: #e6db74 } /* Literal.String.Double */
.pygments-highlight .se { color: #ae81ff } /* Literal.String.Escape */
.pygments-highlight .sh { color: #e6db74 } /* Literal.String.Heredoc */
.pygments-highlight .si { color: #e6db74 } /* Literal.String.Interpol */
.pygments-highlight .sx { color: #e6db74 } /* Literal.String.Other */
.pygments-highlight .sr { color: #e6db74 } /* Literal.String.Regex */
.pygments-highlight .s1 { color: #e6db74 } /* Literal.String.Single */
.pygments-highlight .ss { color: #e6db74 } /* Literal.String.Symbol */
.pygments-highlight .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */
.pygments-highlight .fm { color: #a6e22e } /* Name.Function.Magic */
.pygments-highlight .vc { color: #f8f8f2 } /* Name.Variable.Class */
.pygments-highlight .vg { color: #f8f8f2 } /* Name.Variable.Global */
.pygments-highlight .vi { color: #f8f8f2 } /* Name.Variable.Instance */
.pygments-highlight .vm { color: #f8f8f2 } /* Name.Variable.Magic */
.pygments-highlight .il { color: #ae81ff } /* Literal.Number.Integer.Long */
You may have to massage the output manually to get the exact style you want.
Styling the Editing Interface
If we're writing code into the Wagtail interface, we want it to be monospaced, too.
Wagtail looks for a wagtail_hooks.py
file in the root of every app, and we'll use that to inject CSS into the admin interface:
# blog/wagtail_hooks.py
from django.templatetags.static import static
from django.utils.html import format_html
from wagtail.core import hooks
@hooks.register("insert_editor_css")
def editor_css():
return format_html(
'<link rel="stylesheet" href="{}">',
static("blog/css/admin/monospace.css"),
)
This adds a link to a CSS file we manually place here:
/* blog/css/admin/monospace.css */
.monospace textarea {
font-family: monospace;
}
The End Product
You've seen the output of this work as code blocks all throughout this blog post, but here's what the editing experience looks like: