Skip to main content

ikwukao

Back to Journal

Engineering Journal

Designing Text Nodes for Markdown Processing in Python

A practical look at representing plain text, emphasis, links, images, and inline Markdown as structured nodes inside a static site generator.

4 min read Python, Markdown, Text Processing, HTML, Parsing, Static Site Generator

Designing Text Nodes for Markdown Processing in Python

Markdown looks like plain text, but it contains structure.

A sentence can contain bold text, emphasis, links, images, and code without looking like a formal programming language.

That makes Markdown pleasant to write.

It also makes Markdown surprisingly interesting to parse.

While building a Static Site Generator in Python, one of the important design problems was deciding how inline content should be represented before it became HTML.

The solution was to treat pieces of text as structured nodes.

Why Text Needs Structure

Consider:

Build **reliable** systems with *clear* abstractions.

A human immediately understands the structure.

The parser needs to understand it too.

Instead of storing the paragraph as one string, it can represent it as:

Text("Build ")
Bold("reliable")
Text(" systems with ")
Italic("clear")
Text(" abstractions.")

That structure can then be converted into HTML.

The Text Node

The simplest node is plain text.

Conceptually:

TextNode
    "Build reliable systems."

A text node does not need children.

It simply represents a piece of literal content.

That makes it a leaf in the document tree.

Styled Text

Formatting introduces parent nodes.

For example:

**reliable systems**

can become:

Bold
└── Text

which renders to:

<strong>reliable systems</strong>

The important distinction is that the text itself remains separate from the formatting element.

Nested Formatting

The real value of this representation becomes obvious with nesting.

Consider:

**highly *reliable* systems**

The structure becomes:

Bold
└── Text("highly ")
└── Italic
    └── Text("reliable")
└── Text(" systems")

The renderer can recursively process this structure without knowing anything about the original Markdown delimiters.

Delimiter Splitting

A common parsing strategy is to split text around Markdown delimiters.

For example:

This is **important**.

can be separated into:

This is
**
important
**
.

The parser can then determine which pieces represent formatted content.

The implementation needs to be careful about:

  • missing closing delimiters
  • empty content
  • nested formatting
  • multiple formatted regions
  • delimiters appearing inside unrelated content

These edge cases are where a seemingly simple parser becomes an engineering problem.

Links introduce attributes as well as content.

For example:

[Go](https://go.dev)

contains:

text = Go
url = https://go.dev

The resulting HTML node can represent:

<a href="https://go.dev">Go</a>

The visible text and destination are therefore stored separately.

Images

Images follow a similar pattern:

![Go logo](go.png)

contains:

alt = Go logo
src = go.png

which becomes:

<img src="go.png" alt="Go logo">

This demonstrates why structured nodes are useful.

Different Markdown constructs contain different data, but they can still participate in one rendering system.

Escaping Content

One important responsibility of a text-processing pipeline is distinguishing content from markup.

If a user writes:

5 < 10

the generator should not accidentally interpret the < character as HTML.

The renderer therefore needs to treat literal text safely.

This becomes particularly important when generated pages contain user-controlled or externally sourced content.

Testing Inline Parsing

Inline parsing is an ideal candidate for focused tests.

Examples include:

plain text
**bold**
*italic*
[text](url)
![alt](image.png)

and combinations such as:

**bold *nested italic***

Testing these independently makes failures much easier to diagnose.

The Value of Small Functions

One lesson from the project was the importance of keeping parsing functions focused.

A function responsible for splitting Markdown delimiters should not also:

  • create files
  • render HTML
  • manage templates
  • copy assets

Those are different responsibilities.

Small parsing functions are easier to test and compose.

From Nodes to HTML

Once inline nodes exist, rendering becomes straightforward.

For example:

Paragraph
├── Text("Build ")
├── Strong
│   └── Text("reliable")
└── Text(" systems.")

becomes:

<p>Build <strong>reliable</strong> systems.</p>

The renderer does not need to understand the original Markdown.

It only needs to understand the node types.

Why This Matters Beyond Markdown

The same design principle appears throughout software engineering.

Compilers use intermediate representations.

Browsers construct document trees.

Programming languages use syntax trees.

Data pipelines use normalized intermediate formats.

The common idea is to transform messy input into a structured representation before performing later operations.

That is exactly what the text-node layer provides here.

Final Thoughts

Markdown parsing looks like a text-processing problem until formatting becomes nested and contextual.

At that point, strings alone become difficult to reason about.

Representing inline content as nodes creates a much cleaner abstraction.

The parser can focus on understanding Markdown.

The renderer can focus on producing HTML.

And the filesystem layer can focus on generating the final website.

That separation is one of the most useful architectural lessons from building the project.

ikwukao

Backend • Distributed Systems • Platform Engineering

I design and build reliable backend systems, distributed platforms, cloud-native infrastructure, and developer tooling with Go, Python, Linux, Kubernetes, and modern DevOps practices.

Building reliable systems from Nigeria · GMT+1

Explore

Connect

© 2026 Ikwuka Okoye. All rights reserved.

Built with Hugo, Tailwind CSS & Go-inspired simplicity.