Skip to Content
Course content

222: Building a Markdown Previewer

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably seen plenty of "Markdown Previewers" in junior portfolios. On the surface, they seem trivial: you take a string of text, swap some symbols for HTML tags, and shove it into a div. But if you're building this for a real production environment, there is a massive gap between a "demo that works" and a feature that won't get your company hacked.

The temptation of a few regexes

When I first started building these, my instinct was to handle the conversion myself. I'd think, "How hard can it be? I'll just use a regular expression to find two asterisks and wrap the text in a <strong> tag." It feels clever and lightweight. You avoid adding a dependency, and for a few basic test cases, it looks perfect.

const naiveParse = (text) => {
  return text
    .replace(/\*\*(.*?)\*\*/g, '$1')
    .replace(/\*(.*?)\*/g, '$1')
    .replace(/\n/g, '<br>');
};

// Usage
previewElement.innerHTML = naiveParse(userInput);

The problem is that Markdown is a deceptively complex specification. The moment you try to handle nested lists, blockquotes, or escaped characters, your regexes will start to collide. You'll spend three days fighting edge cases that someone else already solved a decade ago. I've seen developers descend into "regex hell" trying to support just five different Markdown flavors, only to realize they've essentially written a buggy, incomplete version of a parser.

When the browser becomes a weapon

The bigger issue here isn't the lack of features—it's the security hole. In the example above, I'm using innerHTML. This is the most dangerous way to inject content into a page because it tells the browser to execute whatever HTML it finds. If your "previewer" is purely local, maybe you don't care. But the second you allow that Markdown to be saved to a database or shared with another user, you've handed over the keys to your application.

I'll give you a specific example. If a malicious user types <img src="x" onerror="alert('Hacked!')"> into your text area, your naiveParse function won't touch it because it doesn't match your bold or italic patterns. It passes straight through to innerHTML, and the browser executes that JavaScript. This is a classic Cross-Site Scripting (XSS) attack. In a real app, that alert could be a script that steals session cookies or redirects users to a phishing site.

Delegating the heavy lifting

The professional approach is to separate parsing from sanitization. First, use a battle-tested library like marked to handle the Markdown-to-HTML conversion. These libraries handle the weird edge cases of the spec so you don't have to. But—and this is the part people forget—you never trust the output of a parser. You run that HTML through a sanitizer like DOMPurify before it ever touches the DOM.

import { marked } from 'marked';
import DOMPurify from 'dompurify';

const renderMarkdown = (text) => {
  // 1. Convert Markdown to HTML string
  const rawHtml = marked.parse(text);
  
  // 2. Strip out dangerous tags and attributes
  const cleanHtml = DOMPurify.sanitize(rawHtml);
  
  // 3. Now it is safe to inject
  previewElement.innerHTML = cleanHtml;
};

By adding that sanitization step, you're creating a whitelist of "safe" HTML. If marked produces a <script> tag or an onerror attribute, DOMPurify simply deletes it. It's a small amount of overhead for a massive increase in reliability. I always tell my team: if you're using innerHTML with user-generated content, you should be terrified unless there's a sanitizer in the pipeline.




📋 Practical Task

Build a Sanitized Live-Preview Editor

Create a Markdown previewer that implements the professional workflow discussed in the lesson. Your application must include:

  • A textarea for user input and a div for the preview.
  • Integration with the marked library to convert the input in real-time (using an input event listener).
  • Integration with DOMPurify to ensure that any HTML tags manually entered into the textarea (like <script> or <iframe>) are stripped out before being rendered to the preview pane.
  • A test case: Verify that typing **Bold Text** renders as bold, but typing <img src=x onerror=alert(1)> does not trigger an alert.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.