Project · after Level 3

Markdown to HTML Converter 📝

Write a converter that turns Markdown (# headings, **bold**, - lists) into HTML. It is a genuine parser, small enough to finish and real enough to teach you why parsing is harder than it looks. This is the project that makes string handling click.

Difficulty 🐍🐍🐍🐍🐍

📋 Build this

  • Convert # / ## / ### headings to h1 / h2 / h3.
  • Convert **bold** to <strong> and *italic* to <em>.
  • Convert consecutive - lines into a <ul> with <li> items.
  • Wrap plain lines in <p>. Handle blank lines as separators.

Hints, if you want them

Try the spec cold first. Open a hint only when you are properly stuck; the struggle is where the learning is.

Hint 1: Go line by line
Split the input on newlines and process each line by what it starts with. Headings and list items are decided by the first characters (Lesson 4).
Hint 2: Inline formatting with regex
For **bold** and *italic*, re.sub with a captured group is cleanest: re.sub(r'\\*\\*(.+?)\\*\\*', r'<strong>\\1</strong>', line) (Lesson 25). Do bold before italic.
Hint 3: Lists need state
A list spans multiple lines, so you must remember whether you are 'inside a list' and open the
    when the first - appears, closing it when a non-list line arrives. This state-tracking is the heart of the exercise.

The reference solution

Yours does not need to match this. There are many good ways to build any of these. Compare only after you have your own working.

Reveal the reference solution
import re


def inline(text):
    """Bold and italic. Bold first, so ** is not eaten by the * rule."""
    text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
    text = re.sub(r"\*(.+?)\*", r"<em>\1</em>", text)
    return text


def convert(markdown):
    html = []
    in_list = False

    for line in markdown.splitlines():
        stripped = line.strip()

        if stripped.startswith("- "):
            if not in_list:
                html.append("<ul>")
                in_list = True
            html.append(f"  <li>{inline(stripped[2:])}</li>")
            continue

        if in_list:
            html.append("</ul>")
            in_list = False

        if stripped.startswith("### "):
            html.append(f"<h3>{inline(stripped[4:])}</h3>")
        elif stripped.startswith("## "):
            html.append(f"<h2>{inline(stripped[3:])}</h2>")
        elif stripped.startswith("# "):
            html.append(f"<h1>{inline(stripped[2:])}</h1>")
        elif stripped:
            html.append(f"<p>{inline(stripped)}</p>")

    if in_list:
        html.append("</ul>")

    return "\n".join(html)


sample = """# Shopping
Some **important** notes and an *idea*.
- milk
- eggs
- bread
Done."""

print(convert(sample))
<h1>Shopping</h1>
<p>Some <strong>important</strong> notes and an <em>idea</em>.</p>
<ul>
  <li>milk</li>
  <li>eggs</li>
  <li>bread</li>
</ul>
<p>Done.</p>

Stretch goals

+250 XP