Skip to Content
Course content

422: Building a Markdown-to-HTML Converter

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

When I first started writing parsers, I fell into a trap that I still see a lot of developers stumble into today: the belief that a Markdown-to-HTML converter is just a series of .replace() calls. It seems intuitive. You see **bold**, you replace it with <strong>bold</strong>, and you move on. But this approach falls apart the second your input gets real.

Thinking .replace() handles the heavy lifting

Let's look at why that's a dangerous path. Imagine you're converting a technical document that contains both a header and a Python code block. Your input looks like this:

# My Python Guide
Here is how to comment in Python:
```python
# This is a comment, not a header
```

If you're just using text.replace('# ', '<h1>'), your converter is going to happily turn that Python comment into an <h1> tag. Your output becomes a mess of broken HTML because the code didn't "know" it was inside a code block. String replacement is blind; it doesn't understand context or state.

Using Regular Expressions for Pattern Recognition

To do this right, we need to move away from literal replacements and toward pattern matching. Python's re module is our best friend here. Instead of looking for an exact string, we look for a pattern—like "a hash symbol at the start of a line followed by a space."

I usually start by handling the "block-level" elements first (headers, paragraphs) and then diving into "inline" elements (bold, italics). Here is how I'd approach a header conversion using a capturing group:

import re

def convert_headers(text):
    # ^# matches a # at the start of the line
    # (.*) captures everything after the space into Group 1
    # $ ensures we match until the end of the line
    return re.sub(r'^# (.*)$', r'<h1>\1</h1>', text, flags=re.MULTILINE)

The \1 in the replacement string tells Python to put whatever it found in the first set of parentheses back into the string. It's much cleaner and far more predictable than .replace().

Managing State to Prevent "Leaking" Styles

The real challenge is the "context" problem I mentioned earlier. You can't just run a bunch of regexes over the whole file; you have to track whether you are currently "inside" a special block, like a code fence (```). I typically handle this by iterating through the lines of the file and using a boolean flag.

I've found that a simple in_code_block variable is the most reliable way to prevent your header or bold logic from triggering inside a snippet of code. When the flag is true, you ignore all other formatting rules and just wrap the line in <pre> or <code> tags until you hit the closing fence.

Once you've handled the blocks, you can then run your inline regexes (like \*\*(.*?)\*\* for bold) on the resulting text. Because the code blocks are already wrapped in their own tags, you can choose to skip them or process them differently, keeping your document structure intact.




📋 Practical Task

Building a Markdown-to-HTML List Parser

Your task is to extend a basic converter to handle unordered lists. In Markdown, a list item starts with a * or - . However, in HTML, the <li> tags must be wrapped inside a single <ul> tag.

Write a function convert_lists(lines) that takes a list of strings (lines from a file) and returns a single string. Your logic must:

  • Detect when a sequence of lines starting with * or - begins.
  • Wrap the entire group of list items in one set of <ul>...</ul> tags.
  • Convert each individual line into an <li>...</li> tag.
  • Ensure that when the list ends (a line doesn't start with a bullet), the <ul> is closed before processing the next line.

Test Case:
Input: ["* Apple", "* Banana", "Not a fruit", "* Cherry"]
Expected Output: "<ul><li>Apple</li><li>Banana</li></ul>Not a fruit<ul><li>Cherry</li></ul>"

Rating
0 0

There are no comments for now.

to be the first to leave a comment.