Skip to content
Lesson 14 of 20

Step 1 of 5 · Reading · ~4 min

Read

Production Concerns

HTML Escaping

Markdown's output is HTML, and its input is whatever a user typed. Somewhere between those two facts sits every Markdown-powered security incident ever filed. Your program will escape a whole document for HTML output, copying well-formed entity references through unchanged.

Which characters have to change?

CharacterBecomesWhy
<&lt;otherwise it opens a tag
>&gt;symmetry, and it closes one
&&amp;otherwise it opens an entity
"&quot;it terminates an attribute value

The reference renderer escapes all four everywhere, so this course does too: a quote in a paragraph and a quote in an attribute take the same path, and the escaper never needs to know which one it is in.

Escaping is a render-time job, not a parse-time one. You escape when you write a character into the HTML output, which means the contents of code spans and code blocks get escaped too. A <script> tag inside backticks must display as text, not execute.

Why is the ampersand the hard one?

Because it is the one character whose replacement contains itself, and because authors legitimately write entity references by hand. Replace blindly and you escape your own escapes:

InputBlind replaceEntity-aware
Tom & JerryTom &amp; JerryTom &amp; Jerry
Already &amp; escapedAlready &amp;amp; escapedAlready &amp; escaped
&copy; 2026&amp;copy; 2026&copy; 2026
a < b && c > da &lt; b &amp;&amp; c &gt; da &lt; b &amp;&amp; c &gt; d

Rows two and three are the bug: an author who already wrote an entity gets it mangled into visible &amp; noise. The fix is a lookahead. Before escaping an ampersand, check whether it begins a well-formed entity reference, and if so copy the whole reference through untouched.

What counts as an entity?

CommonMark recognises three shapes:

ShapeDigits or lettersExample
decimal&# + 1 to 7 digits + ;&#65;
hexadecimal&#x or &#X + 1 to 6 hex digits + ;&#x41;
named& + a letter, then letters or digits, 2 to 32 characters in all + ;&copy;

Anything else, such as &&, & c, an unterminated &amp, &#;, &D; (too short) or an eight-digit number, is a bare ampersand and gets escaped normally. The real parser also checks a named entity against the HTML5 list of about two thousand names, so &madeup; is escaped; this exercise checks only the shape and leaves the table out.

Escape order

If you escape with a chain of replacements instead of one scan, & must go first. Escape < first and its replacement's own ampersand gets caught by the later pass, turning &lt; into &amp;lt;. A single left-to-right scan that handles each character once avoids the question entirely, and it is the only approach that can also protect entities.

Is escaping enough to be safe?

No. CommonMark deliberately permits raw HTML, so a conforming parser will happily pass <script> straight through when it appears at block level. Several widely used Markdown libraries have shipped cross-site-scripting advisories over the years for exactly this reason. For untrusted input, render with raw HTML disabled if your library supports it, and run the resulting HTML through a dedicated sanitizer regardless. The raw-HTML lesson comes back to this.

Common mistakes

  • Double-escaping a valid entity. Already &amp; escaped must come back unchanged. A plain replace("&", "&amp;") returns &amp;amp;.
  • Only recognising the named shape. &#x41; and &#65; are entities too.
  • Working line by line. The input is a whole document; copy every newline (including a CR) through like any other character so nothing is added or lost.
  • Forgetting the quote. " becomes &quot; as well.

Your exercise

Escape a document, entity-aware. The starter reads all of stdin as one string and writes your result back unchanged; you write entity_length (the three shapes above) and escape (one scan, one decision per character).

Up nextBackslash EscapesProduction Concerns

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…