Markdown
Escape what a tag produced, so a value someone else supplied cannot plant formatting in a template you wrote.
An admin writes a message. Your application fills in a name.
Thanks **{name}**, see you soon.Someone submits # Ada, a horizontal rule and a link as their name. The rendered page gets all
three, and the admin never wrote any of it.
Sanitising HTML afterwards does not help. By the time the body reaches a markdown renderer this is all legitimate markdown, and no renderer can tell which characters the author typed and which came from a form field. Only the render knows that, which is why this lives here rather than in your rendering step.
Usage
npm install @tagscript/markdownRender with spans: true, then escape:
import { Flavour, markdownSafe } from '@tagscript/markdown';
const response = await ts.run('Thanks **{name}**, see you soon.', { seedVariables, spans: true });
const body = markdownSafe(response, Flavour.GFM);The author's ** still works. Everything the tag produced is now literal text.
Leave spans: true off and this throws. A response without ranges looks exactly like a response
where nothing was generated, and returning the body unescaped would be the wrong way to be wrong.
It gives you markdown, not HTML
Whatever you already render with keeps rendering, whether that is react-markdown, an email
template, or a Discord message. Pick the flavour your reader uses.
| Flavour | Adds |
|---|---|
Flavour.CommonMark | the base |
Flavour.GFM | tables and strikethrough, so | and ~ |
Flavour.Discord | spoilers and strikethrough, and no raw HTML |
This is not a substitute for sanitising HTML. If your renderer passes raw HTML through, keep doing whatever you already do about that.
What counts as the author's text
Every built-in parser passes its own payload through, and the author typed that payload, so
{if(x):**yes**} keeps its emphasis. A variable is not a built-in, so whatever your application
seeded is escaped.
markdownSafe(response, Flavour.GFM, { trust: [...builtinTags, ...myTemplateTags] });Add your own tags to trust when they return template text. A tag that returns anything fetched,
submitted or configured elsewhere belongs nowhere near that list. untrust goes the other way, for
a variable an author defined with {=(name):value} that you nevertheless do not want to be able to
format.
Nesting follows where the text came from. A tag nested in a payload is carried, and one nested in a parameter is not, because a parameter is read rather than written:
| Template | Result |
|---|---|
{upper:{user}} | escaped, since the user value is in the output |
{if({user}==yes):**sure**} | not escaped, since the branch is the author's own text |
That is a rule about provenance, not proof of what a parser did. A parser that writes its own
parameter into its output is the case it gets wrong, so name such a parser in untrust.
Escaping a value on its own
When you have a value rather than a render:
import { escapeMarkdown } from '@tagscript/markdown';
escapeMarkdown(displayName, Flavour.CommonMark);Characters that only mean something at the start of a line are escaped only there, so a phone number keeps its hyphens and a sentence keeps its full stops.
Discord mentions
Escaping stops formatting, not mentions. <@123> still renders as a mention, because that is not
markdown. Use allowed_mentions on the message payload, which is what decides whether it pings.
Last updated on