Just added: Algorithms you can run and practice
Updated Jul 16, 2026 Edit on GitHub

The Plan: Lines to HTML

Before we write a single regex, let's agree on what we are actually doing. A converter is a translator. Markdown goes in, HTML comes out, and our job is to write down the translation rules.

What translates to what

Markdown is a tiny language. Most of it is a handful of one-line patterns. Here is the slice we care about:

You write You get
# Title <h1>Title</h1>
## Subtitle <h2>Subtitle</h2>
- item a list item in a <ul>
Some text. <p>Some text.</p>
**bold** <strong>bold</strong>
*italic* <em>italic</em>
`code` <code>code</code>
[text](url) <a href="url">text</a>

Notice the split in that table. The top four rules look at a whole line - a # at the start makes the entire line a heading. The bottom four work inside a line - **bold** could appear in the middle of a paragraph or a heading.

That split is the most important idea in this guide. We have two kinds of rules:

  • Block rules decide what each line becomes (a heading, a list item, a paragraph).
  • Inline rules decorate the text within a block (bold, italic, code, links).

We will handle blocks first (Phase 2), inline second (Phase 3). Keeping them separate is what keeps the code readable. Try to do both at once and you get a tangle.

Why lines?

Markdown is line-oriented. The thing at the start of a line decides what that line is. # at the start means heading. - at the start means list item. Nothing special at the start means paragraph.

So the first move of almost every Markdown parser is the same: chop the input into lines and look at each one. JavaScript hands us that for free with split.

Splitting the input

Let's prove this works. We will take a small Markdown document, split it into an array of lines, and print each one with its index so we can see exactly what the loop will be chewing on.

Before you run this, guess how many lines you'll get - remember, split keeps blank lines too.

const markdown = `# My Notes

Here is a paragraph.

- first item
- second item`;

// The whole game starts here: text becomes an array of lines.
const lines = markdown.split("\n");

console.log("Got", lines.length, "lines:\n");

lines.forEach((line, i) => {
  // Show the index and the raw content, quoted so blank lines are visible.
  console.log(`${i}: "${line}"`);
});

Run that. You should see six lines, including the two blank ones (index 1 and 4). Those blank lines matter later - they are how Markdown separates one paragraph from the next - so it is good that split keeps them.

Framing the loop

Now the skeleton everything else hangs on. We loop over the lines and, for each one, decide what it is. Right now we only know how to classify a line, not convert it - but classifying is the first half of the job, and it shows you the shape of the code we will fill in over the next phases.

Your turn. Write classify yourself - fill it in and hit Run, the checks underneath tell you whether it works. My version is in the next block whenever you want it.

function classify(line) {
  // Return one of these strings, based on `line`:
  //   "heading"    - line starts with "# "
  //   "list-item"  - line starts with "- "
  //   "blank"      - line is empty or only whitespace
  //   "paragraph"  - anything else
}

// --- checks: fix your function until this prints "All good." ---
if (classify("# My Notes") !== "heading") throw new Error(`classify("# My Notes") should be "heading", got: ${classify("# My Notes")}`);
if (classify("- first item") !== "list-item") throw new Error(`classify("- first item") should be "list-item", got: ${classify("- first item")}`);
if (classify("") !== "blank") throw new Error(`classify("") should be "blank", got: ${classify("")}`);
if (classify("   ") !== "blank") throw new Error(`classify("   ") should be "blank" (whitespace counts too), got: ${classify("   ")}`);
if (classify("Just some text.") !== "paragraph") throw new Error(`classify("Just some text.") should be "paragraph", got: ${classify("Just some text.")}`);
console.log("All good.");

Stuck on blank lines? A string has a .trim() method - what does it return for " "?

One way to write it

const markdown = `# My Notes

Here is a paragraph.

- first item`;

const lines = markdown.split("\n");

function classify(line) {
  if (line.startsWith("# ")) return "heading";
  if (line.startsWith("- ")) return "list-item";
  if (line.trim() === "") return "blank";
  return "paragraph";
}

const output = [];
for (const line of lines) {
  const kind = classify(line);
  output.push(`${kind.padEnd(10)} <- "${line}"`);
}

console.log(output.join("\n"));

Run it. Each line now has a label. # My Notes is a heading, the bullet is a list-item, the blank line is blank, and the rest is a paragraph.

This is the real architecture in miniature. A Markdown parser is fundamentally a thing that walks lines, classifies each one, and emits the matching HTML. Everything from here is filling in the "emit the matching HTML" part - making classify smarter and turning those labels into real tags.

Where we are

You have a converter's skeleton: input split into lines, and a function that knows what each line is. You also have the mental model - blocks versus inline - that the rest of the build rests on.

Next phase we replace those labels with actual HTML tags. Bring the classify idea with you; it is about to grow up.