Library / SDK
yogthos/markdown-clj avatar
yogthos/markdown-clj

markdown-clj, a Markdown parser that compiles to Clojure and ClojureScript

Markdown parser in Clojure

577 stars120 forksClojureEPL-1.0

At a glance

What is it?
markdown-clj is a Markdown parser written in Clojure that compiles to both Clojure and ClojureScript, offering reference-style links, footnotes, MultiMarkdown metadata, heading anchors and an opt-in escape hatch for text the parser should not touch. Its release history is old, and its source is small enough to read in an afternoon.
Who is it for?
markdown-clj suits a Clojure or ClojureScript project that wants Markdown parsing as a library call rather than a subprocess, and that needs the same parser on the JVM and in the browser. It does not suit a project that has moved past CommonMark, because the parser is built around the classic Markdown plus MultiMarkdown extensions rather than the current specification, and the last recorded release is from 2015.
Can I use it commercially?
Yes, with conditions. EPL-1.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
Is it still maintained?
Yes. The repository last received commits 56 days ago.
What is it written in?
Mainly Clojure, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A parser as a function call, on both Clojure targets

markdown-clj is described as a Markdown parser written in Clojure and ClojureScript, and the installation note is simply that it compiles to both. It is published on Clojars as `markdown-clj` and on npm under the same name, so a JavaScript project consumes it from the npm registry and a Clojure project from Clojars.

The API is two functions in the common case. `md-to-html` takes an input containing Markdown markup and an output where the resulting HTML will be written, with the input and output parameters passed to a reader and a writer respectively. `md-to-html-string` takes a string of Markdown and returns a string of HTML.

clojure
(ns foo
  (:use markdown.core))

(md-to-html "input.md" "output.html")

(md-to-html (input-stream "input.md") (output-stream "test.txt"))

That reader and writer signature is the interesting design decision. The parser is not tied to a file, so the same call handles a file on disk, a string in a database column, or a network stream, and a Clojure project that wants to pipe Markdown through several transformations does not have to write out a temporary file.

The project targets Clojure 1.2 and later for versions before 0.9.68, and Clojure 1.7 for 0.9.68 and later. The recorded release is 0.9.62, and the last push to the repository was on 2026-08-28, so the branch has seen work well after the last tag.

For the browser build, the documentation describes running `lein with-profile js cljsbuild once`, which creates a standalone `js/markdown.js` artifact. There is a live demo at a rawgit URL for the parser, and a `demo/` directory in the repository with the JavaScript and the demo page.

The audience is a Clojure developer who wants a parser they can call from a REPL, or a ClojureScript developer who wants the same parser in the browser without shelling out to a service.

The extensions that separate it from a plain Markdown parser

The feature list in the documentation is a list of deviations from classic Markdown, and they are worth reading one at a time because each one changes what you can write in a document.

Reference-style links are off by default. The parser defaults to using inline reference for performance reasons, and to enable reference style links you pass the `:reference-links? true` option. That is a performance decision stated plainly: building the reference table for every document costs time that most documents do not need, so it is opt-in.

Footnotes are also opt-in, through `:footnotes? true`.

Metadata is the MultiMarkdown syntax, and there are three ways to get at it. `md-to-html` attempts to parse the metadata when passed `:parse-meta? true` and returns it as its output. `md-to-html-string-with-meta` parses string input and returns a map with two keys, `:html` containing the parsed HTML and `:metadata` containing a map with the metadata included at the top of the document. And `md-to-meta` parses the metadata only, without parsing the Markdown or returning HTML, which the documentation says can run more quickly and can be useful in scenarios where the metadata is useful by itself.

The metadata rules are specific in a way that will bite you if you do not know them. The value of each key in the metadata map is a list of either zero, one or many strings. If a metadata value ends in two spaces then the string will end in a newline. If a line does not contain a header and has at least four spaces in front of it then it is considered a member of the last key that was found.

That last rule is the one to remember, because it is how you write a multi-line metadata value, and it is not obvious from the syntax alone.

Heading anchors come from `:heading-anchors`, and the code block formatting is controlled by `:code-style` and `:pre-style`, both of which take a function that produces the class attribute. The default code block format is the highlight.js compatible one, with a `pre` and a `code` element carrying the language as a class, and both overrides let you emit something else entirely.

The inhibit separator, and the parser as a transformer pipeline

The most distinctive feature of the library is the ability to tell the parser to leave part of the document alone.

If you pass `:inhibit-separator "some-string"`, then any text within occurrences of `some-string` is output verbatim. The documentation gives a LaTeX example, where the dollar signs are the separator and the mathematics inside is passed through untouched while the surrounding Markdown is still processed.

clojure
(md-to-html-string "This is one of those 20%% vs 80%% cases."
                   :inhibit-separator "%")

The output for that case is the text with the doubled percent signs reduced to single ones, so the separator itself is expressed by entering it twice with no text inside.

The stated motivation is using markdown-clj alongside other parsers of languages with conflicting syntax, and it names asciimath2jax as the case in mind. This is the feature that makes the library more than a Markdown implementation. It becomes a way to run Markdown and another lightweight markup language through one document without one mangling the other.

There are three caveats, and the documentation lists them. Like other tags, this only works within a single line. If you remove the default transformers with `:replacement-transformers`, inhibiting stops working, because the separators are found by the same mechanism that does the replacing. And dashes, both `--` and `---`, cannot be suppressed this way, which is a real limitation given that dashes are what a horizontal rule is made of.

Underneath, the parser is a sequence of transformers, and that is how you extend it. Additional transformers are specified with the `:custom-transformers` key, and a transformer function must accept two arguments, the string representing the current line and the map representing the current state. The default state keys are `:code` for inside a code section, `:codeblock` for inside a code block, `:eof` for end of file, `:heading` for in a heading, `:hr` for in a horizontal line, `:lists` for inside a list, `:blockquote` for inside a blockquote, `:paragraph` for in a paragraph, and `:last-line-empty?`.

So a custom transformer is a function from a line and a context map to a result, and the context map is the parser's whole notion of where it is in the document. That is a small, legible extension model, and it is the part of the library most worth understanding if you intend to change its behaviour.

The repository is small, and that is a feature

The top-level entries of the repository describe a project with a very small surface: a `src/` directory, a `test/` directory, a `demo/` directory with the JavaScript and the demo page, `project.clj`, `deps.edn`, `bb.edn`, `package.json` and `example.html`.

The build tooling is worth a look because it shows a project that has moved between Clojure's tooling over time. `project.clj` is the Leiningen build, `deps.edn` is the tools.deps build, `bb.edn` is Babashka, and `package.json` is present for the JavaScript side, whose only dev dependency is `nbb`, the Node-based ClojureScript compiler. The documented way to produce a standalone browser artefact is `lein with-profile js cljsbuild once`, which writes a `js/markdown.js` file.

The demo is a single HTML page, and the documentation links to a hosted version of it so you can type Markdown into a box and see the HTML come out. For a parser library, that is the correct amount of front end, and it is worth remembering that the demo is the fastest way to check whether a document you care about renders the way you expect.

The `test/` directory is what a small parser project should have, and the presence of `bb.edn` alongside the two Clojure build files suggests the author used Babashka to run something quickly.

What you do not find in the tree is a `docs/` directory of prose documentation, and the README is where the documentation lives. The wiki-style structure of the README, with a section per option and an example for each, is the whole manual. For a library this old, that is a real constraint on adoption, because there is no versioned documentation and no separate site that can be updated independently of the code.

The licence is EPL-1.0, the Eclipse Public License, which is the Clojure community's usual choice and is a file-level copyleft, so a modified file stays under the licence while the rest of your project does not. The last push to the repository was on 2026-08-28, and the repository is not archived.

What this parser is not

Two things are worth saying plainly, and the second one is the practical risk.

First, this is a Classic Markdown parser with MultiMarkdown extensions, not a CommonMark implementation. The feature set is the giveaway. Reference-style links, footnotes and MultiMarkdown metadata are the three extensions that matter, and none of the CommonMark work is mentioned: no GFM tables, no task list items, no autolink literals, no strikethrough, and none of the HTML block-level parsing rules that CommonMark settled on in 2014. A document that leans on GFM extensions will not render as you expect, and the parser will not complain, it will just produce different output.

Second, the release history is old. The only recorded release is 0.9.62 from 2015-01-09, and the README refers to versions 0.9.68 and later as the current line, so the published versions on Clojars extend past the tag the project records. The last push was on 2026-08-28, so the branch is alive, but the tagging practice has been loose for a decade.

That combination is the real adoption risk. The code is small, the extension model is clear, and the library does what it says. What it does not do is track a moving specification, and a parser that quietly diverges from the spec your users' editors follow is a source of rendering differences that are tedious to diagnose.

For a project that controls its own Markdown, generates its own documentation from a small set of templates, and does not accept user-supplied Markdown, none of that matters. For a platform where users write Markdown, it matters a great deal, and the Clojure ecosystem's alternatives that track CommonMark are a better fit.

Editorial conclusion

markdown-clj suits a Clojure or ClojureScript project that wants Markdown parsing as a library call rather than a subprocess, and that needs the same parser on the JVM and in the browser. It does not suit a project that has moved past CommonMark, because the parser is built around the classic Markdown plus MultiMarkdown extensions rather than the current specification, and the last recorded release is from 2015. Verify first that the Markdown features your documents use are supported, particularly the CommonMark edge cases, that the Clojure version on your classpath meets the requirement for the version you pick, and that the extension points you need are still present in the version you take.

Frequently asked questions

How do I add markdown-clj to a Clojure project?

The library is published on Clojars as markdown-clj, and it is also on npm under the same name for ClojureScript projects. Versions before 0.9.68 require Clojure 1.2 or later, and 0.9.68 and later require Clojure 1.7.

How do I enable reference-style links in markdown-clj?

The parser uses inline references by default for performance reasons, so reference style links are opt-in through the :reference-links? true option.

Can markdown-clj extract MultiMarkdown metadata?

Yes. The md-to-html function parses and returns the metadata when passed :parse-meta? true, md-to-html-string-with-meta returns a map with :html and :metadata keys, and md-to-meta parses only the metadata without producing HTML, which the documentation says can run more quickly.

What is the inhibit-separator option in markdown-clj?

Passing :inhibit-separator makes any text between occurrences of that string output verbatim, so another markup language can share a document with Markdown. The documentation notes it only works within a single line, that it stops working if the default transformers are removed with :replacement-transformers, and that dashes cannot be suppressed this way.

How do I extend the markdown-clj parser?

Add a function to the :custom-transformers key. A transformer takes two arguments, the current line as a string and the current state as a map, and the default state keys include :code, :codeblock, :eof, :heading, :hr, :lists, :blockquote and :paragraph.

Official sources

  1. Issues
  2. License: EPL-1.0
  3. README
  4. Releases
  5. yogthos/markdown-clj on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/yogthos-markdown-clj.svg)](https://hysenlabs.com/projects/yogthos-markdown-clj)

Was this useful?