Added syntax highlighting to code blocks

Added syntax highlighting for code blocks using the pygments library.

Implementation was pretty straight forward after getting the regex right to treat ‘=html’ blocks as raw.

Changes located in the run_jotdown function and the new highlightcode_ function, as well as fiddling with css.

Click here to see changes to the build script
def highlight_code(code: str, language: str = None) -> str:
    """
    Highlight code using Pygments with specified or guessed language.
    """
    try:
        if language:
            lexer = get_lexer_by_name(language.lower())
        else:
            lexer = guess_lexer(code)

        formatter = HtmlFormatter(
            style=config["syntax_highlighting"]["style"],
            linenos="table"
            if config["syntax_highlighting"].get("line_numbers", False)
            else False,
            cssclass="highlight",
        )
        return highlight(code, lexer, formatter)
    except ClassNotFound:
        # If language isn't found, return code wrapped in pre tags
        return f"<pre><code>{code}</code></pre>"


def run_jotdown(plaintext: str) -> str:
    """
    Modified to handle code blocks with syntax highlighting.
    Fixed to properly handle both raw HTML and HTML code blocks.
    """
    CODE_BLOCK_RE = re.compile(r"\`\`\`\`*(=html|\s*(?:(\w+)\n))?(.*?)\`\`\`\`*", re.DOTALL)
    code_blocks = []
    marker_template = "§CODE_BLOCK_{}§"

    def save_code_block(match):
        raw_html_marker = match.group(1)
        language = match.group(2)
        code = match.group(3).strip()

        # Check if this is a raw HTML block
        if raw_html_marker == "=html":
            return f"\`\`\`=html\n{code}\n\`\`\`"

        # For all other cases, including 'html' language, highlight the code
        highlighted = highlight_code(code, language)
        marker = marker_template.format(len(code_blocks))
        code_blocks.append(highlighted)
        return f"\`\`\`=html\n{marker}\n\`\`\`"

    # First, replace all code blocks with markers
    processed_text = CODE_BLOCK_RE.sub(save_code_block, plaintext)

    # Run through jotdown
    html = run("jotdown", input=processed_text, text=True, capture_output=True).stdout

    # Replace markers with actual highlighted code
    for i, code in enumerate(code_blocks):
        marker = marker_template.format(i)
        html = html.replace(marker, code)

    return html

Note that in the above snippet, the backticks (`) have been escaped to prevent the markup on this page from breaking, remove the backslashes if you use this code yourself.

Click here to see the CSS I settled on
:root {
  --highlight-font-size: 0.9em;
  --highlight-border-radius: 6px;
  --highlight-padding: 1rem;
  --highlight-line-number-color: #6e7681;
}

.highlight {
  font-size: var(--highlight-font-size, 0.9em);
  border-radius: var(--highlight-border-radius, 6px);
  padding: var(--highlight-padding, 1rem);
}

.highlight pre {
  margin: 0;
  overflow-x: auto;
}

.highlight table {
  border-spacing: 0;
  border: none;
  margin: 0;
}

.highlight table td {
  padding: 0;
  border: none;
}

.highlight .linenos {
  user-select: none;
  padding-right: 1rem;
  color: var(--highlight-line-number-color);
  text-align: right;
  width: 1%;
  white-space: nowrap;
}

@media (prefers-color-scheme: dark) {
  .highlight {
    background: var(--code-background, #262220) !important;
  }
  pre {
    line-height: 125%;
  }
  td.linenos .normal {
    color: #4e4e4e;
    background-color: transparent;
    padding-left: 5px;
    padding-right: 5px;
  }
  span.linenos {
    color: #4e4e4e;
    background-color: transparent;
    padding-left: 5px;
    padding-right: 5px;
  }
  td.linenos .special {
    color: #8f9494;
    background-color: #ffffc0;
    padding-left: 5px;
    padding-right: 5px;
  }
  span.linenos.special {
    color: #8f9494;
    background-color: #ffffc0;
    padding-left: 5px;
    padding-right: 5px;
  }
  .highlight .hll {
    background-color: #ddd0c0;
  }
  .highlight {
    background: #262220;
    color: #ddd0c0;
  }
  .highlight .c {
    color: #70757a;
  } /* Comment */
  .highlight .err {
    color: #af5f5f;
  } /* Error */
  .highlight .esc {
    color: #ddd0c0;
  } /* Escape */
  .highlight .g {
    color: #ddd0c0;
  } /* Generic */
  .highlight .k {
    color: #919191;
  } /* Keyword */
  .highlight .l {
    color: #af875f;
  } /* Literal */
  .highlight .n {
    color: #ddd0c0;
  } /* Name */
  .highlight .o {
    color: #878787;
  } /* Operator */
  .highlight .x {
    color: #ddd0c0;
  } /* Other */
  .highlight .p {
    color: #ddd0c0;
  } /* Punctuation */
  .highlight .ch {
    color: #8f9f9f;
  } /* Comment.Hashbang */
  .highlight .cm {
    color: #70757a;
  } /* Comment.Multiline */
  .highlight .cp {
    color: #fdd0c0;
  } /* Comment.Preproc */
  .highlight .cpf {
    color: #c9b98f;
  } /* Comment.PreprocFile */
  .highlight .c1 {
    color: #70757a;
  } /* Comment.Single */
  .highlight .cs {
    color: #af5f5f;
  } /* Comment.Special */
  .highlight .gd {
    color: #bb6868;
  } /* Generic.Deleted */
  .highlight .ge {
    color: #ddd0c0;
    font-style: italic;
  } /* Generic.Emph */
  .highlight .ges {
    color: #ddd0c0;
  } /* Generic.EmphStrong */
  .highlight .gr {
    color: #af5f5f;
  } /* Generic.Error */
  .highlight .gh {
    color: #ddd0c0;
  } /* Generic.Heading */
  .highlight .gi {
    color: #849155;
  } /* Generic.Inserted */
  .highlight .go {
    color: #ddd0c0;
  } /* Generic.Output */
  .highlight .gp {
    color: #ddd0c0;
  } /* Generic.Prompt */
  .highlight .gs {
    color: #ddd0c0;
    font-weight: bold;
  } /* Generic.Strong */
  .highlight .gu {
    color: #ddd0c0;
  } /* Generic.Subheading */
  .highlight .gt {
    color: #af5f5f;
  } /* Generic.Traceback */
  .highlight .kc {
    color: #875f5f;
  } /* Keyword.Constant */
  .highlight .kd {
    color: #875f5f;
  } /* Keyword.Declaration */
  .highlight .kn {
    color: #875f5f;
  } /* Keyword.Namespace */
  .highlight .kp {
    color: #919191;
  } /* Keyword.Pseudo */
  .highlight .kr {
    color: #b46276;
  } /* Keyword.Reserved */
  .highlight .kt {
    color: #af875f;
  } /* Keyword.Type */
  .highlight .ld {
    color: #af875f;
  } /* Literal.Date */
  .highlight .m {
    color: #87afaf;
  } /* Literal.Number */
  .highlight .s {
    color: #c9b98f;
  } /* Literal.String */
  .highlight .na {
    color: #ddd0c0;
  } /* Name.Attribute */
  .highlight .nb {
    color: #ddd0c0;
  } /* Name.Builtin */
  .highlight .nc {
    color: #875f5f;
  } /* Name.Class */
  .highlight .no {
    color: #af8787;
  } /* Name.Constant */
  .highlight .nd {
    color: #fdd0c0;
  } /* Name.Decorator */
  .highlight .ni {
    color: #ddd0c0;
  } /* Name.Entity */
  .highlight .ne {
    color: #877575;
  } /* Name.Exception */
  .highlight .nf {
    color: #fdd0c0;
  } /* Name.Function */
  .highlight .nl {
    color: #ddd0c0;
  } /* Name.Label */
  .highlight .nn {
    color: #ddd0c0;
  } /* Name.Namespace */
  .highlight .nx {
    color: #ddd0c0;
  } /* Name.Other */
  .highlight .py {
    color: #dfaf87;
  } /* Name.Property */
  .highlight .nt {
    color: #87afaf;
  } /* Name.Tag */
  .highlight .nv {
    color: #ddd0c0;
  } /* Name.Variable */
  .highlight .ow {
    color: #878787;
  } /* Operator.Word */
  .highlight .pm {
    color: #ddd0c0;
  } /* Punctuation.Marker */
  .highlight .w {
    color: #ddd0c0;
  } /* Text.Whitespace */
  .highlight .mb {
    color: #87afaf;
  } /* Literal.Number.Bin */
  .highlight .mf {
    color: #87afaf;
  } /* Literal.Number.Float */
  .highlight .mh {
    color: #87afaf;
  } /* Literal.Number.Hex */
  .highlight .mi {
    color: #87afaf;
  } /* Literal.Number.Integer */
  .highlight .mo {
    color: #87afaf;
  } /* Literal.Number.Oct */
  .highlight .sa {
    color: #dfaf87;
  } /* Literal.String.Affix */
  .highlight .sb {
    color: #c9b98f;
  } /* Literal.String.Backtick */
  .highlight .sc {
    color: #c9b98f;
  } /* Literal.String.Char */
  .highlight .dl {
    color: #c9b98f;
  } /* Literal.String.Delimiter */
  .highlight .sd {
    color: #878787;
  } /* Literal.String.Doc */
  .highlight .s2 {
    color: #c9b98f;
  } /* Literal.String.Double */
  .highlight .se {
    color: #af5f5f;
  } /* Literal.String.Escape */
  .highlight .sh {
    color: #c9b98f;
  } /* Literal.String.Heredoc */
  .highlight .si {
    color: #af5f5f;
  } /* Literal.String.Interpol */
  .highlight .sx {
    color: #fdd0c0;
  } /* Literal.String.Other */
  .highlight .sr {
    color: #af5f5f;
  } /* Literal.String.Regex */
  .highlight .s1 {
    color: #c9b98f;
  } /* Literal.String.Single */
  .highlight .ss {
    color: #af5f5f;
  } /* Literal.String.Symbol */
  .highlight .bp {
    color: #87afaf;
  } /* Name.Builtin.Pseudo */
  .highlight .fm {
    color: #fdd0c0;
  } /* Name.Function.Magic */
  .highlight .vc {
    color: #ddd0c0;
  } /* Name.Variable.Class */
  .highlight .vg {
    color: #ddd0c0;
  } /* Name.Variable.Global */
  .highlight .vi {
    color: #ddd0c0;
  } /* Name.Variable.Instance */
  .highlight .vm {
    color: #ddd0c0;
  } /* Name.Variable.Magic */
  .highlight .il {
    color: #87afaf;
  } /* Literal.Number.Integer.Long */
}

@media (prefers-color-scheme: light) {
  .highlight {
    background: var(--code-background, #e7daca) !important;
  }
  pre {
    line-height: 125%;
  }
  td.linenos .normal {
    color: inherit;
    background-color: transparent;
    padding-left: 5px;
    padding-right: 5px;
  }
  span.linenos {
    color: inherit;
    background-color: transparent;
    padding-left: 5px;
    padding-right: 5px;
  }
  td.linenos .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;
  }
  .highlight .hll {
    background-color: #ffffcc;
  }
  .highlight {
    background: #ffffff;
  }
  .highlight .c {
    color: #999988;
    font-style: italic;
  } /* Comment */
  .highlight .err {
    color: #a61717;
    background-color: #e3d2d2;
  } /* Error */
  .highlight .k {
    font-weight: bold;
  } /* Keyword */
  .highlight .o {
    font-weight: bold;
  } /* Operator */
  .highlight .ch {
    color: #999988;
    font-style: italic;
  } /* Comment.Hashbang */
  .highlight .cm {
    color: #999988;
    font-style: italic;
  } /* Comment.Multiline */
  .highlight .cp {
    color: #999999;
    font-weight: bold;
  } /* Comment.Preproc */
  .highlight .cpf {
    color: #999988;
    font-style: italic;
  } /* Comment.PreprocFile */
  .highlight .c1 {
    color: #999988;
    font-style: italic;
  } /* Comment.Single */
  .highlight .cs {
    color: #999999;
    font-weight: bold;
    font-style: italic;
  } /* Comment.Special */
  .highlight .gd {
    color: #000000;
    background-color: #ffdddd;
  } /* Generic.Deleted */
  .highlight .ge {
    font-style: italic;
  } /* Generic.Emph */
  .highlight .ges {
    font-weight: bold;
    font-style: italic;
  } /* Generic.EmphStrong */
  .highlight .gr {
    color: #aa0000;
  } /* Generic.Error */
  .highlight .gh {
    color: #999999;
  } /* Generic.Heading */
  .highlight .gi {
    color: #000000;
    background-color: #ddffdd;
  } /* Generic.Inserted */
  .highlight .go {
    color: #888888;
  } /* Generic.Output */
  .highlight .gp {
    color: #555555;
  } /* Generic.Prompt */
  .highlight .gs {
    font-weight: bold;
  } /* Generic.Strong */
  .highlight .gu {
    color: #aaaaaa;
  } /* Generic.Subheading */
  .highlight .gt {
    color: #aa0000;
  } /* Generic.Traceback */
  .highlight .kc {
    font-weight: bold;
  } /* Keyword.Constant */
  .highlight .kd {
    font-weight: bold;
  } /* Keyword.Declaration */
  .highlight .kn {
    font-weight: bold;
  } /* Keyword.Namespace */
  .highlight .kp {
    font-weight: bold;
  } /* Keyword.Pseudo */
  .highlight .kr {
    font-weight: bold;
  } /* Keyword.Reserved */
  .highlight .kt {
    color: #445588;
    font-weight: bold;
  } /* Keyword.Type */
  .highlight .m {
    color: #009999;
  } /* Literal.Number */
  .highlight .s {
    color: #bb8844;
  } /* Literal.String */
  .highlight .na {
    color: #008080;
  } /* Name.Attribute */
  .highlight .nb {
    color: #999999;
  } /* Name.Builtin */
  .highlight .nc {
    color: #445588;
    font-weight: bold;
  } /* Name.Class */
  .highlight .no {
    color: #008080;
  } /* Name.Constant */
  .highlight .ni {
    color: #800080;
  } /* Name.Entity */
  .highlight .ne {
    color: #990000;
    font-weight: bold;
  } /* Name.Exception */
  .highlight .nf {
    color: #990000;
    font-weight: bold;
  } /* Name.Function */
  .highlight .nn {
    color: #555555;
  } /* Name.Namespace */
  .highlight .nt {
    color: #000080;
  } /* Name.Tag */
  .highlight .nv {
    color: #008080;
  } /* Name.Variable */
  .highlight .ow {
    font-weight: bold;
  } /* Operator.Word */
  .highlight .w {
    color: #bbbbbb;
  } /* Text.Whitespace */
  .highlight .mb {
    color: #009999;
  } /* Literal.Number.Bin */
  .highlight .mf {
    color: #009999;
  } /* Literal.Number.Float */
  .highlight .mh {
    color: #009999;
  } /* Literal.Number.Hex */
  .highlight .mi {
    color: #009999;
  } /* Literal.Number.Integer */
  .highlight .mo {
    color: #009999;
  } /* Literal.Number.Oct */
  .highlight .sa {
    color: #bb8844;
  } /* Literal.String.Affix */
  .highlight .sb {
    color: #bb8844;
  } /* Literal.String.Backtick */
  .highlight .sc {
    color: #bb8844;
  } /* Literal.String.Char */
  .highlight .dl {
    color: #bb8844;
  } /* Literal.String.Delimiter */
  .highlight .sd {
    color: #bb8844;
  } /* Literal.String.Doc */
  .highlight .s2 {
    color: #bb8844;
  } /* Literal.String.Double */
  .highlight .se {
    color: #bb8844;
  } /* Literal.String.Escape */
  .highlight .sh {
    color: #bb8844;
  } /* Literal.String.Heredoc */
  .highlight .si {
    color: #bb8844;
  } /* Literal.String.Interpol */
  .highlight .sx {
    color: #bb8844;
  } /* Literal.String.Other */
  .highlight .sr {
    color: #808000;
  } /* Literal.String.Regex */
  .highlight .s1 {
    color: #bb8844;
  } /* Literal.String.Single */
  .highlight .ss {
    color: #bb8844;
  } /* Literal.String.Symbol */
  .highlight .bp {
    color: #999999;
  } /* Name.Builtin.Pseudo */
  .highlight .fm {
    color: #990000;
    font-weight: bold;
  } /* Name.Function.Magic */
  .highlight .vc {
    color: #008080;
  } /* Name.Variable.Class */
  .highlight .vg {
    color: #008080;
  } /* Name.Variable.Global */
  .highlight .vi {
    color: #008080;
  } /* Name.Variable.Instance */
  .highlight .vm {
    color: #008080;
  } /* Name.Variable.Magic */
  .highlight .il {
    color: #009999;
  } /* Literal.Number.Integer.Long */
}