MOOSSin
Menu

All posts · Open Source Tools

Should I use markdown-it-py, or write my own renderer that only recognizes a whitelist?

markdown-it-py is a mature CommonMark compatible library that can be plugged in. However, if your CSP does not allow inline scripts and the content comes from a pipeline you don't fully control, a 200-line renderer that only recognizes whitelisted syntax and skips each value only once might be more suitable. This article discusses the conditions of each path, starting with a debate about whether an asterisk will become bold.

I once had an argument with a friend about Markdown. She said that enclosing two asterisks in a text box always makes it bold, and I said it depends on which renderer you're using. She said Markdown is Markdown, there's no such thing as a sub-class. I said Markdown does have sub-classes, and they're very detailed, so detailed that there's a specification called CommonMark, hundreds of pages long, specifically explaining "when two asterisks will become bold." She said I was nitpicking.

I'm not nitpicking. Converting Markdown to HTML seems simple, but it's actually a world full of unconventional examples. The easiest Python solution is markdown-it-py: a port of markdown-it from the JavaScript community, licensed by MIT, and tested according to the CommonMark specification. Add-ons are provided via mdit-py-plugins, and tables, footnotes, and mathematical expressions are readily available. One of my projects uses it, while another project wrote its own renderer. This article discusses which approach to take under what conditions.

Usage of markdown-it-py and a note

MarkdownIt().render(text)That's all. It has several preset configurations to choose from.commonmark The strictest,zero It lets you enable everything one by one without opening anything. Tables need to be opened separately because they're not in the CommonMark specification; they were added by GitHub. This is one of the reasons my friend and I argued: she thought Markdown was GitHub's Markdown, while I thought it was the standard Markdown. Both are correct, and both are incorrect.

It's important to note HTML passthrough. The Markdown specification allows you to write HTML directly in the file. If the renderer outputs it as is, users can insert a script tag, and your website becomes someone else's. When the content source isn't yours, turn off the HTML option, or run it through a sterilizer after output; choose one, but ideally both. Tutorials on this topic are usually placed in the last paragraph, in small print, but it's the most important paragraph in the entire article.

Under what conditions would I write it myself?

I have a website whose CSP isscript-src 'self' ,No'unsafe-inline' Any embedded scripts will be blocked by the browser, even the JSON-LD blocks commonly used for structured data cannot be used; instead, the microdata attribute is used. The content passes through AI and messaging software before reaching the backend; it's not something I wrote verbatim.

Given this premise, my requirements for the renderer became very specific: the output could only contain tags I explicitly allowed, each paragraph of text could only be skipped once, links had to be on a whitelist, and images could only be from within the site. A fully functional library could do this, but I would have to understand all its options and add-ons, and trust that the next version wouldn't change the default values. I could read and test two hundred lines of code that I wrote myself.

It's a bit like the difference between cooking at home and eating out. Eating out is convenient, offers more choices, and usually doesn't cause any problems. But if you're allergic to something, and have a severe allergic reaction, you'll want to know what's in the pot. Cooking at home isn't about how well you cook; it's about needing to know what's in the pot.

What should a strict subset renderer do?

The syntax you recognize should be minimal and explicit: headings, paragraphs, lists, quotations, tables, fence code, horizontal lines, inline bold, italics, code, and links. That's it. All other syntax should be treated as visible text output, neither explained nor omitted; the author will know which syntax is not supported just by seeing it. "Seeing" is crucial; the author will never notice things that are silently omitted.

List the rejected elements in a table: Any content containing the tags script, iframe, object, embed, style, link, meta, svg, form, input, textarea, or button will be rejected.on Event attributes at the beginning are also rejected. Links are only allowed to http, https, mailto, tel, and relative paths.javascript: anddata: Rejection, backslash, and control character rejection. These checks run during write, returning unqualified drafts to the author with details of which check was rejected, rather than being silently removed during rendering.

The skip operation is performed only once. First, the original text is parsed, then each value is processed.html.escape Never run Markdown replacement again for tags or attributes that have already been generated. Double-checking is one of the most common sources of XSS, and it's the kind of source that makes you think, "This makes perfect sense."

Mini-drama: The aftermath of the Asterisk dispute

"So, in the one you wrote yourself, will the two asterisks become bold?"

"meeting."

"What about an asterisk?"

"Italicized text".

"Three?"

"If we don't support it, we'll print it as is with three asterisks."

"Isn't this terrible?"

"This is intentional. I'd rather the author see the three asterisks and change them than have the renderer guess the author's meaning. The cost of guessing wrong is higher than printing out the asterisks."

"You really don't trust the renderer."

"I don't trust any program that I haven't finished reading, including the one I wrote myself three months ago."

The article title is already printed on the page. If the body text repeats the same first-level heading, there will be two headings on the screen.h1 When writing, if you encounter a first-level heading at the beginning of the main text, you can go back and indicate the line number; when reading old content, the line that is exactly the same as the heading is skipped and not displayed, while first-level headings with different content are not deleted, as they are the author's text. This rule is not related to security, but to "whether the article looks like an article".

My conclusion

When all content is self-generated and the output environment is flexible, markdown-it-py is the right choice; don't reinvent the wheel, spend your time writing content. When content comes from external sources and CSPs are strict, a subset renderer that you can fully understand is more reassuring than a complete library that you've only read the files for. The choice of tool depends on who will be putting things into it.

As for that argument with my friend, it ended like this: she opened three different websites, pasted the same Markdown, and got three different results. She looked at the screen and said, "Okay." That was the least satisfying debate I've ever won.

Why are there so many versions of Markdown?

Markdown was published by John Gruber in 2004 as a Perl script with a one-page explanation. That explanation was rather casual and left many things unclear, such as "how many blank lines between two lists count as the same list." As a result, each implementer had to guess, and the results were different. Ten years later, it became common for the same text to look different on different websites.

In 2014, a group of people, including the founder of Stack Overflow and a linguist, couldn't take it anymore. They wrote CommonMark, a specification several hundred pages long with over six hundred test examples, fixing every ambiguity. This caused some controversy at the time because Gruber himself was reluctant to see Markdown "standardized." But the specification was still written, and now mainstream renderers use it as a benchmark.

GitHub added tables, strikethroughs, task lists, and autolinks to CommonMark, calling it GFM. Most people use GFM for "Markdown" in their daily lives, which is why a table you write on GitHub might appear as a row of vertical lines when pasted elsewhere. markdown-it is one of the most complete CommonMark implementations in the JavaScript community, and markdown-it-py is its Python port, including its tests, so the behavior of both is almost identical.

Three common misunderstandings

The first misconception is that "Markdown is safe." Markdown itself allows embedding HTML, and if the renderer doesn't disable this feature, it's just as dangerous as allowing users to write HTML directly. Safety isn't a property of Markdown; it's a result of the renderer's settings.

The second misconception is that "a blank line makes a new paragraph, while no blank line makes it the same paragraph." In CommonMark, line breaks without blank lines are defaulted to being merged into the same paragraph, but many chat programs and note-taking apps treat each line break as a new paragraph. This is why the same text looks different on both sides.

The third misconception is that "the more forgiving the renderer, the better." A forgiving renderer can "guess" the author's intentions, which is helpful when it's right, but the author is completely unaware when it's wrong. A strict renderer prints out anything it doesn't recognize as is, which the author can see and correct. For an automated workflow, I choose the latter.

A small exercise you can try over the weekend

寫一段 Markdown,包含一個表格、一段內嵌 HTML(隨便一個 <b> 標籤)、三個星號包起來的字、一個沒有空行的換行。貼到三個地方:GitHub 的 issue 預覽、你常用的筆記 app、以及 pip install markdown-it-py 之後在 Python 裡用預設設定 render 一次。

Compare the three results side-by-side. You'll see tables become vertical lines somewhere, HTML is printed verbatim somewhere, three asterisks become bold italics somewhere, and only bold text somewhere. These twenty minutes will make you remember "Markdown has points" forever. Then, if you want to go a step further, try it in markdown-it-py.MarkdownIt("zero") Without turning on anything, see what a piece of Markdown looks like without any rules.

Postscript

After that argument with my friend, we developed an unspoken agreement: when discussing Markdown, we'd first ask, "Which one are you referring to?" This phrase later expanded to many other topics, such as, "Is your understanding of JSON strict or does it allow for annotations?" Many disputes aren't actually about differing opinions, but rather about two people talking about different things without realizing it. This is probably the most useful lesson I learned from that asterisk debate, even more useful than the CommonMark specification itself.

Project page: github.com/executablebooks/markdown-it-py

Reading preferences

Back to all postsAbout the author and corrections (Chinese)