Markdown Is a Programming Language. Fight Me.

The dev community has spent 20 years calling Markdown 'just a markup language.' They're wrong. Here's the full technical case — parse trees, AST internals, the CommonMark spec, Pandoc's extended grammar — plus why Markdown is the fastest way to write long documents that has ever existed.

I write everything in Markdown. Articles, documentation, internal notes, RFC drafts, design specs, README files. If it is longer than a paragraph, it goes in a .md file. I have been doing this for years and I am not going back.

This is not a quirk. It is a conclusion I reached after spending too much time fighting word processors — fighting fonts that drift, fighting spacing that resets when you paste something, fighting Google Docs deciding to autocorrect a code snippet into sentence case, fighting the seventeen-click export process that produces HTML with inline styles that need to be stripped. Markdown does not fight you. You type. It renders. The friction is nearly zero.

Part of why I wanted to write this piece is that Markdown gets dismissed constantly by people who do not use it as their primary writing tool. They call it "just markup." They say it is not a real language. And while the language classification argument is worth making on its own technical merits, I also want to put on record that Markdown is the best long-document writing tool available right now, and the people who disagree have either not tried it seriously or gave up before the workflow clicked.

Two arguments in one article. Markdown is a programming language, technically. And Markdown is genuinely good for writing, practically. Both are true. The technical case first.

What a Programming Language Actually Is

The debate collapses because nobody agrees on definitions before starting.

The strict definition requires Turing completeness: a system that can simulate any Turing machine. By this definition, SQL92 is not a programming language. CSS is not a programming language. HTML is not a programming language. Regex is not a programming language. Most developers who work with these tools daily would laugh at that conclusion, and they should.

The practical definition is more useful: a programming language is a formal system with defined syntax that a processor reads, parses according to a grammar, and transforms into output or behavior. Under this definition, SQL is a language. CSS is a language. HTML is a language.

Markdown passes this test cleanly. It has formal syntax. CommonMark defines a complete parsing algorithm with 652 specification examples and an automated test suite. Processors read Markdown source, parse it into a structured intermediate representation, and emit output in a target format. That is compilation. Markdown is a language.

The more precise classification: Markdown is a declarative domain-specific language for document authoring. You declare structure and emphasis. A processor decides how to render them. You do not specify how a heading becomes an <h2> tag. You specify that it is a second-level heading, and the renderer handles the rest. This is the same model SQL uses for queries, CSS uses for styling, and HTML uses for document structure.

SQL92 is not Turing complete, and nobody argues SQL is not a programming language. Markdown's non-Turing-completeness kills the argument the same way it kills that argument for SQL: it does not.

The Compiler Is Not a Metaphor

Markdown started as a Perl script John Gruber published in 2004. Markdown.pl took plain text input, ran it through a defined transformation, and produced valid XHTML. That script was a compiler. Not "like a compiler." A compiler. Input in a formal syntax, transformation by a runtime, output in a target format.

Modern Markdown processors are more sophisticated. To see why calling Markdown a language is technically precise, you need to understand what a parser does with a .md file.

The Parsing Pipeline

A Markdown processor does not scan text and swap ** for <strong>. The full pipeline runs several stages:

  1. Block structure parsing. The processor reads the document line by line and identifies block-level elements: paragraphs, headings, code fences, blockquotes, lists, thematic breaks, HTML blocks. CommonMark calls this the first pass. Block structure defines the document's skeleton.
  2. Inline parsing. Within each block, a second pass identifies inline elements: emphasis spans, strong spans, links, images, inline code, autolinks, raw HTML. Inline elements nest. A strong span can contain an emphasized span. A link can contain an image.
  3. AST construction. The processor builds an Abstract Syntax Tree — a structured data representation of the document. Every node in the tree is a typed element with children and attributes.
  4. Rendering. The renderer walks the AST and produces output. The AST is format-agnostic. The same AST can be walked by an HTML renderer, a PDF renderer, a LaTeX renderer, or anything else.

This is a compiler architecture. Lexing, parsing, tree construction, code generation. The same four stages every language compiler runs.

What the AST Actually Looks Like

Take this Markdown:

## Getting Started

Install the package with [npm](https://npmjs.com):

```bash
npm install my-package
```

For **production** use, pin the version.

The AST produced by a CommonMark-compliant parser looks like this (from remark's actual output, slightly simplified):

{
  "type": "root",
  "children": [
    {
      "type": "heading",
      "depth": 2,
      "children": [{ "type": "text", "value": "Getting Started" }]
    },
    {
      "type": "paragraph",
      "children": [
        { "type": "text", "value": "Install the package with " },
        {
          "type": "link",
          "url": "https://npmjs.com",
          "children": [{ "type": "text", "value": "npm" }]
        },
        { "type": "text", "value": ":" }
      ]
    },
    {
      "type": "code",
      "lang": "bash",
      "value": "npm install my-package"
    },
    {
      "type": "paragraph",
      "children": [
        { "type": "text", "value": "For " },
        {
          "type": "strong",
          "children": [{ "type": "text", "value": "production" }]
        },
        { "type": "text", "value": " use, pin the version." }
      ]
    }
  ]
}

That is a typed, traversable data structure produced from source text. You can write transformation plugins that walk this tree and modify it before rendering — add anchor links to headings, replace image paths, validate external links, inject reading time metadata. The remark ecosystem in JavaScript has over 150 plugins built exactly on this architecture.

CommnMark is the The Spec That Proved It

For Markdown's first ten years, no formal grammar existed. Gruber published a description and a Perl implementation. Other developers wrote their own parsers from that description and got different results. The same document rendered differently on GitHub, Reddit, Stack Overflow, and Jekyll. Nested lists were a disaster. Indented code inside list items was inconsistent across every major parser. Emphasis with asterisks versus underscores behaved differently depending on who wrote the parser.

This is exactly what happens when you treat a language as a convention rather than a specification.

In 2014, a group of developers including Jeff Atwood and John MacFarlane published CommonMark — a formal, unambiguous specification for Markdown. CommonMark defines the complete parsing algorithm, not just the syntax. It specifies precedence rules for conflicting constructs. It includes 652 test cases, each pairing a Markdown input with the exact HTML output a compliant parser must produce.

The spec document reads like a language specification because it is one. It defines a two-phase parsing model (block structure, then inline structure), specifies link reference definition rules, defines how lazy continuation lines work inside blockquotes, and resolves decades of ambiguity with precise algorithmic rules.

A formal specification with an automated test suite is not documentation for a convention. It is the definition of a language. CommonMark confirmed what Markdown already was.

GitHub published GitHub Flavored Markdown (GFM) as a formal specification in 2017, built on CommonMark as a strict superset. GFM adds tables, strikethrough, task lists, and autolinks as specified extensions. The GFM spec follows the same format: formal syntax definitions and example-based tests. That is language versioning.

The CommonMark Parsing Algorithm

CommonMark's block parsing algorithm is more nuanced than most people expect. It is not a regex pass. It is a stateful line-by-line processor maintaining a stack of open block elements.

The algorithm processes each line against the current open block stack. A line can continue an existing block, close open blocks and open new ones, or be a lazy continuation line appending to a paragraph without an explicit block marker. The spec defines specific rules for each case.

A blockquote line starting with > continues or opens a blockquote. A line with fewer than four leading spaces and a specific marker opens a list item. A line with four or more leading spaces inside a list item opens an indented code block — which is why indented code blocks inside lists caused so many compatibility issues for a decade. The spec rules for that interaction span several paragraphs of careful disambiguation.

After block structure resolves, inline parsing runs on the text content of each block. CommonMark's inline parser uses a delimiter stack to handle emphasis. When the parser encounters * or _, it pushes a delimiter run onto the stack. After scanning the full inline content, it runs a matching pass to pair openers with closers according to rules about left-flanking and right-flanking delimiter runs.

This is why _foo_bar_ does not produce expected emphasis in all parsers. The flanking rules determine whether a delimiter run is an opener, a closer, both, or neither. _foo_bar_ has an underscore followed by a word character (b) at its close, making the closing underscore right-flanking but also left-flanking, which the spec treats as a non-closer in most contexts. The spec writers spent serious time on this.

That is a non-trivial parser algorithm. Not a regex substitution. A stateful machine with a delimiter stack, lazy continuation handling, and precedence rules.

The Grammar

CommonMark's syntax can be expressed as a context-free grammar. The block-level productions look like:

document     = block*
 
block        = atx_heading
             | setext_heading
             | thematic_break
             | fenced_code
             | indented_code
             | blockquote
             | list
             | html_block
             | link_definition
             | paragraph
 
atx_heading  = ("#"{1,6}) SP inline* NL
 
fenced_code  = fence_open info_string? NL
               line*
               fence_close NL
 
blockquote   = (">" SP? line)+
 
list         = list_item+
list_item    = list_marker SP block+

This is a context-free grammar — the same class of grammar used to define Python, TypeScript, and every other programming language that gets a formal specification. Markdown has a formal grammar. It belongs to the same formal category.

A Document Programming System

If CommonMark is the language standard, Pandoc is the compiler suite.

Pandoc is a universal document converter written in Haskell by John MacFarlane (who also co-authored CommonMark). Input formats include Markdown, HTML, LaTeX, DOCX, RST, Org-mode, and about twenty others. Output formats include HTML, PDF via LaTeX, DOCX, ePub, man pages, reveal.js presentations, Jupyter notebooks, and dozens more.

Pandoc's extended Markdown dialect adds constructs that standard Markdown does not have:

Footnotes. [^1] inline with a corresponding [^1]: footnote text definition anywhere in the document. Pandoc numbers and renders them correctly in every output format.

Citations. [@author2024] syntax combined with a .bib BibTeX file produces fully formatted citations and reference lists in any citation style — APA, Chicago, MLA, IEEE — via citeproc integration. Researchers write academic papers in Pandoc Markdown for exactly this feature.

Math. $x^2 + y^2 = z^2$ for inline LaTeX math, $$...$$ for display math. Pandoc passes these to MathJax for HTML output and directly to LaTeX for PDF output. No plugin configuration required.

Definition lists

CommonMark
: A formal, unambiguous specification for Markdown.
 
GFM
: GitHub Flavored Markdown, a strict superset of CommonMark.

Raw blocks. Fenced code blocks with language {=html} or {=latex} pass raw output directly to the target format. You write raw LaTeX inside a Markdown document for PDF output and it passes through untouched.

YAML front matter sets document metadata:

---
title: "Distributed Systems Architecture"
author: "Traven"
date: 2026-04-04
bibliography: references.bib
csl: ieee.csl
toc: true
---

This is not decorative. Pandoc reads this metadata and uses it throughout the output. The title goes into <title> tags in HTML and into the PDF cover page. The bibliography file and CSL style sheet control citation rendering. The toc: true flag generates a table of contents from the heading hierarchy.

That YAML block is metadata programming. You declaratively specify document properties that the processor uses to control output behavior. Ghost Blog reads the same front matter format for post metadata. Hugo, Jekyll, Astro — all of them. This pattern is consistent across the Markdown ecosystem because Markdown as a language made front matter a natural attachment point for document-level configuration.

The Pandoc pipeline for producing an academic paper from Markdown source:

pandoc paper.md \
  --from markdown+footnotes+citations+definition_lists \
  --to pdf \
  --pdf-engine=xelatex \
  --citeproc \
  --bibliography references.bib \
  --csl ieee.csl \
  --toc \
  -o paper.pdf

You wrote Markdown. You got a formatted paper with citations, a bibliography, a table of contents, and proper typographic layout. The Markdown source is version-controlled. The PDF is the build artifact.

The remark/rehype Compiler Pipeline

In JavaScript, the canonical Markdown processing ecosystem is remark (Markdown processor) and rehype (HTML processor), both built on the unified framework.

unified treats document processing as AST transformation. You compose a pipeline of plugins:

import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkGfm from 'remark-gfm'
import remarkRehype from 'remark-rehype'
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypePrism from 'rehype-prism'
import rehypeStringify from 'rehype-stringify'
 
const result = await unified()
  .use(remarkParse)             // Markdown source → mdast
  .use(remarkGfm)               // add GFM table, strikethrough, tasklist support
  .use(remarkRehype)            // mdast → hast
  .use(rehypeSlug)              // add id attributes to headings
  .use(rehypeAutolinkHeadings)  // add anchor links to each heading
  .use(rehypePrism)             // syntax highlight code blocks
  .use(rehypeStringify)         // hast → HTML string
  .process(source)

remarkParse produces an mdast (Markdown Abstract Syntax Tree). remarkRehype converts mdast to hast (Hypertext Abstract Syntax Tree). Each rehype plugin transforms the HTML tree before serialization. rehypeSlug walks all heading nodes and adds id attributes derived from the heading text. rehypePrism finds code block nodes and replaces their children with syntax-highlighted token nodes.

This is a compiler plugin architecture. It is structurally identical to how Babel plugins transform JavaScript ASTs, how PostCSS plugins transform CSS ASTs, how the Rust compiler applies optimization passes to MIR. The pattern is the same because Markdown processing is the same category of problem as every other language compilation problem.

When the Language Shows Its Full Nature

MDX extends Markdown with JSX. You write .mdx files mixing Markdown prose with React component calls:

---
title: "API Reference"
---
 
import { ParamTable } from '../components/ParamTable'
import { Warning } from '../components/Warning'
 
## Authentication
 
All requests require a Bearer token in the `Authorization` header.
 
<Warning>
  Tokens expire after 24 hours. Your client must handle 401 responses
  and re-authenticate.
</Warning>
 
<ParamTable
  endpoint="/api/v1/users"
  method="GET"
  params={[
    { name: "page", type: "number", required: false },
    { name: "limit", type: "number", required: false }
  ]}
/>
 
The response includes a `cursor` field for pagination.

MDX has imports. MDX has typed component props. MDX renders through @mdx-js/mdx, a compiler that parses the file, resolves JSX components, and outputs a JavaScript module. Node.js runs it. TypeScript type-checks it. Astro, Next.js, and Gatsby use MDX for content pages with embedded interactive components.

At some point in this chain, I want the person arguing Markdown is not a programming language to specify exactly where the language ends. The MDX compiler's input is a superset of CommonMark Markdown. The parser handles Markdown syntax and JSX syntax through the same pipeline. There is no ceremony where Markdown gets promoted to a real language when JSX gets added. The base was already a language. MDX made the compiler architecture impossible to ignore.

Why I Write Everything in Markdown

The technical argument is made. Now the personal one.

I open VS Code. I create a file. I start typing. My hands stay on the keyboard. I type ## and space for a heading. I type **word** for bold. I type backtick pairs for inline code. The document structure comes from what I type, not from menus I navigate or buttons I click. There is no formatting toolbar interrupting the writing process. No style pane to open. No font picker to accidentally click.

Contrast this with Google Docs. You type a heading and have to either use the Styles dropdown, press a shortcut you need to memorize, or accept that your heading looks like body text until you format it. Then you paste in a code snippet and Docs autocorrects the quotes, changes the font, adjusts the line spacing. You spend three minutes undoing things that should not have happened. The document fights you.

Word is worse. Word has been accumulating formatting state since 1983. A Word document has a hidden XML structure that carries formatting from wherever you pasted text, produces different output depending on which version of Word opens it, and can get corrupted in ways that are impossible to predict. I have opened the same Word file on two different computers and gotten different pagination. Markdown renders consistently because the source is plain text and the renderer is deterministic.

The speed argument is real and I want to be specific about it. When writing a long technical document in Markdown, I do not stop to format things. I write. When I reach a code example, I type three backticks and a language identifier and write the code. When I reach a heading, I type the hash symbols and continue. The context switch between writing and formatting drops to zero. For documents over 2,000 words, that adds up to meaningful time savings and — more importantly — maintained writing flow.

The constraints Markdown imposes are features. The formatting vocabulary is limited to things that reflect structure: headings, emphasis, lists, links, code. No custom fonts. No random color changes mid-paragraph. No twenty-point heading followed by a twelve-point subheading because you dragged the font size slider. You cannot produce a finely formatted mess in Markdown. You write structure, and the renderer applies consistent visual treatment. The constraints force you to think about what you are actually communicating rather than how it looks.

A Markdown document I write today opens correctly in any text editor in 2040. The .docx format requires Microsoft Office or a compatible application. The Google Doc requires a Google account and Google's servers to be running. The Markdown file opens in Notepad.

The Combination That Changes Documentation

Plain text diffs cleanly in Git. This is one of the most underrated properties of writing in Markdown, and it changes how you think about documentation.

A Word document is a binary file (or a zipped XML bundle). When you commit a .docx to a Git repository and edit three paragraphs, git diff shows the file changed. That is it. You cannot see which words changed. Code review for documentation written in Word is impossible.

A Markdown document diffs at the line level. Change a heading, the diff shows exactly which heading changed. Edit a paragraph, the diff shows exactly which sentences moved. Revert to a previous draft, Git shows every change since then. git blame on a confusing sentence shows who wrote it and when.

This makes Markdown documentation version-controllable the same way code is version-controllable. Some engineering teams run documentation through the same pull request review process as code. An engineer proposes a documentation change. Another engineer reviews the diff and approves or requests changes. This is only possible because the source is plain text.

Static site generators like Jekyll, Hugo, and Astro build entire websites from Markdown source stored in Git. The content is source-controlled. The rendered HTML is a build artifact. This site works the same way — CoderOasis runs Ghost on a self-hosted server, and Ghost's editor writes Markdown. The articles are source files. The rendered pages are output.

The No-Control-Flow Objection

The most serious technical objection to Markdown as a programming language is the absence of control flow. No conditionals. No loops. No variables in standard Markdown. A Python developer writing a loop is doing something qualitatively different from a Markdown author writing a document.

This is true. It matters less than it seems.

SQL's core declarative subset has no variables or loops either. You write a query describing what data you want. The database engine decides how to retrieve it. SQL is a language. The absence of imperative control flow does not remove it from the category.

HTML has no variables or loops. You write elements describing a document structure. The browser decides how to render them. HTML is a language.

Markdown belongs to the same category: declarative languages that describe a desired output without specifying the steps to produce it. The processor handles the steps. The absence of imperative features is a design choice, not a deficiency.

Pandoc's template system does add variables. A Pandoc template uses $title$, $author$, and $date$ placeholders filled from the document's front matter metadata. That is variable substitution. Pandoc's Lua filter API lets you write Lua scripts that walk the AST and perform conditional transformations — real control flow at the processing layer. The language is extensible at the point where control flow becomes necessary.

The Conclusion

Markdown is a declarative domain-specific programming language for document authoring. It has a formal grammar, a two-phase parsing algorithm specified by CommonMark, a typed AST produced by compliant parsers, a plugin ecosystem built on AST transformation, and a compiler toolchain in Pandoc that outputs over 40 formats. MDX extends it with JSX and makes the compiler architecture explicit. The remark/rehype ecosystem treats Markdown the same way Babel treats JavaScript: source in, AST out, plugins transform, serializer emits.

Markdown is not Turing complete. SQL92 is not Turing complete. CSS is not Turing complete. All of them are languages.

The practical case stands on its own. Markdown removes formatting friction from writing. It diffs cleanly in Git. It opens in any editor that has ever existed. Pandoc converts it to anything. The constraints it imposes force structure and prevent the formatting chaos that makes Word documents unmaintainable. For long documents that need to be version-controlled, portable, and written fast, Markdown is the right tool.

I have covered this kind of taxonomy fight in other contexts — if you want the "real language" debate in a different arena, the TypeScript 6.0 piece covers the same dynamic playing out in a community that really should know better by now.

Markdown is a language. Write it, version-control it, build toolchains on it.