Markdown and code

Table of contents

Introduction

Every feature that stores written material renders it through one function, render_markdown. Cutting a stored body down to an allowlist is a security decision, and one copy of it is easier to keep right than several.

from splent_framework.markdown import render_markdown

@bp.route("/page/<slug>")
def page(slug):
    page = service.get(slug)
    return render_template("page.html", body_html=render_markdown(page.body_md))

The result is Markup, so your template renders it with {{ body_html }} and no filter.

Never write | safe on a raw stored body, which runs whatever it contains. A body stored as HTML, from a rich text editor, goes through the rich_text filter instead.


What it does to a body

The render_markdown pipeline body_md what was stored 1 Parse GitHub-flavoured, raw HTML allowed 2 Highlight code blocks, by Pygments, into spans 3 Sanitise all of it, by nh3, against one allowlist Markup rendered as is external_links_new_tab adds target="_blank", then sanitises again Highlighting runs before sanitising, so the highlighter's spans go through the allowlist too.
Nothing reaches the page without passing the allowlist, the framework's own markup included.

It reads GitHub-flavoured markdown, with tables, strikethrough and bare URLs. Raw HTML is allowed and cleaned afterwards, since imported bodies contain it. Heading anchors, definition lists, footnotes and task lists are on.

Then it sanitises everything with nh3, against nh3’s own allowlist plus a little more.

  • id, class, lang and title on any element, for anchors, highlighting and imported styles. None of them can execute anything.
  • <input> only as a disabled checkbox, which is what a task list is.
  • URLs only with http, https or mailto.

render_markdown(page.body_md, external_links_new_tab=True)

Links with an absolute http or https address get target="_blank". It is off by default, since the feature that owns the material decides. Every link already carries rel="noopener noreferrer", and an author cannot choose a target, because the allowlist removes any target a body writes.


Highlighting code

Code is coloured on the server, at render time, by Pygments. No JavaScript is loaded, so code is never on screen uncoloured, and a page works printed or with JavaScript off.

<pre><code class="language-bash">vagrant init ubuntu/trusty32 <span class="tok-c1">#comentario</span>
</code></pre>

A language without a lexer renders plain, and never raises. Blocks tagged text, txt or plaintext are left alone, since colouring pasted output invents structure. Whitespace is never wrapped in markup.

Styling the colours

Token classes carry a tok- prefix, so they never clash with classes an imported body brings. Your theme maps them to a few custom properties, and a skin with a dark panel overrides them in one block.

.site {
    --code-comment: #7c8797;
    --code-keyword: #ff7bbf;
    --code-string:  #8ddb8d;
    --code-number:  #ffb86c;
    --code-name:    #c9a6ff;
    --code-builtin: #79c0ff;
    --code-added:   #8ddb8d;
    --code-removed: #ff8080;
}

Check the contrast against the panel, 4.5:1 at least. Colour only what carries meaning, and let punctuation and plain names inherit the text colour.


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es