# intellij-markdown: JetBrains' multiplatform Markdown processor

> intellij-markdown is the Markdown parser JetBrains uses in its IDEs, written in Kotlin and published to Maven Central for the JVM, JavaScript and Native targets, with a staged parsing model that lets an editor highlight a block without re-parsing the document and a strict rule that the input must have LF line endings. It is a parser for people who need the same Markdown behaviour in an editor and on a server, not a general purpose rendering library.

**JetBrains/markdown** — Markdown parser written in kotlin

- Repository: https://github.com/JetBrains/markdown
- Stars: 968 · Forks: 106
- Language: Kotlin
- License: Apache-2.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/jetbrains-markdown

## A parser with an editor in mind

intellij-markdown is a Markdown processor written in Kotlin and published under the org.jetbrains group on Maven Central. The README describes it as an extensible Markdown processor, and lists four aims: use one code base for both client and server side processing, produce consistent output on different platforms, support different Markdown flavours, and be easily extensible.

Those four aims explain the design more clearly than the feature list would. A parser that has to run in an IDE and in a server has to behave identically in both, which rules out a binding to a platform's native text handling, and it has to let the caller choose the flavour, which rules out a single hardcoded dialect. The processor is written in pure Kotlin, with a little flex, so it compiles for the JVM target, for JS and for Native, and the Kotlin/JS IR badge in the README header records that the JavaScript backend is supported rather than merely hoped for.

The multiplatform part is not a port of a JVM library, it is the same source compiled three ways, which is why the README says one code base for both client and server side processing. For a product that renders Markdown in a browser and in a backend, that is a strong argument: the same flavour descriptor and the same HTML generator on both sides means the preview and the published page cannot drift apart.

The extensibility claim is about flavours and about the pipeline rather than about the parser core. The parsing model is described in the README as holding the document structure and the inline structure in two separate logical parts, which is what allows a caller to do the work in the order that suits the interaction it is supporting.

## Adding the dependency, and the artifact you actually get

The library is on Maven Central, so the repository has to be declared, which in a Kotlin or Gradle build is:

```kts
repositories {
  mavenCentral()
}
```

and the dependency itself, for Gradle 5.4 or newer, is:

```kts
dependencies {
  implementation("org.jetbrains:markdown:<version>")
}
```

The README notes that Gradle should resolve the target platform and decide which artifact, the JVM one or the JS one, to download, which is the main convenience of publishing as a multiplatform library with Gradle module metadata. For a multiplatform project the same single dependency goes into the commonMain class path:

```groovy
commonMain {
  dependencies {
    implementation("org.jetbrains:markdown:<version>")
  }
}
```

If you are on Maven, or on a Gradle older than 5.4, the resolution is not automatic and you have to name the artifact for your platform yourself. The two names given are org.jetbrains:markdown-jvm for the JVM version and org.jetbrains:markdown-js for the JS version. That difference is worth knowing before you spend an afternoon on a resolution failure.

Intermediate releases, the ones with an A.B.C.x version, are not on Maven Central. They are published to the JetBrains intellij-dependencies repository, which you add as:

```groovy
repositories {
    maven {
        url "https://packages.jetbrains.team/maven/p/ij/intellij-dependencies"
    }
}
```

So there are two channels with different stability guarantees, a released version and a development build, and a project that needs a fix before the next release has a documented way to get it without waiting.

## Parsing in stages, which is the reason to use this parser

The most useful part of the README for an integrator is the section on using the library for parsing and generating HTML, because it explains that the tasks can be done independently and in stages.

The example given is the Markdown plugin for JetBrains IDEs, and the sequence is this. First, parse the block structure without parsing the inlines, which gives lazy parsable blocks for the editor. Second, quickly parse the inlines of a given block, which gives faster syntax highlighting updates. Third, generate the HTML for the preview.

Each of those three is a separate cost with a different performance profile, and a conventional parser gives you all three at once, every time. For an editor, that is the difference between re-parsing a document when a keystroke lands somewhere and re-parsing a paragraph. The lazy block stage means the editor can know where every block is without paying for the inline structure of blocks the user is not looking at, and the inline stage can then be run for the one block that changed.

The API for the simple case is short. In Kotlin:

```kotlin
val src = "Some *Markdown*"
val flavour = CommonMarkFlavourDescriptor()
val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(src)
val html = HtmlGenerator(src, parsedTree, flavour).generateHtml()
```

And in Java, where the flavour is the GitHub flavoured one and the generator takes an extra flag:

```java
final String src = "Some *Markdown*";
final MarkdownFlavourDescriptor flavour = new GFMFlavourDescriptor();
final ASTNode parsedTree = new MarkdownParser(flavour).buildMarkdownTreeFromString(src);
final String html = new HtmlGenerator(src, parsedTree, flavour, false).generateHtml();
```

The shape is the same in both languages, which is what a multiplatform library with a JVM heritage tends to look like, and the flavour descriptor is the object that carries the dialect. Choosing CommonMarkFlavourDescriptor or GFMFlavourDescriptor is how the same parser serves two different definitions of Markdown.

## Line endings, and the offset rules that follow from them

The input requirements are the section of the README that will bite an integrator who skips it, and they are about line endings.

The parser requires LF line endings, and the callers must normalise CRLF and bare CR to LF before parsing:

```kotlin
val src = originalText.replace("\r\n", "\n").replace("\r", "\n")
```

The streaming case is called out separately, and the rule is more subtle than the one-liner suggests. You should not normalise each chunk independently with the example above, because a CRLF pair split across two chunks would become two line breaks. Instead you buffer a trailing CR until the next character or the end of the stream, emit a single LF if the next character is LF, and otherwise emit an LF for the buffered CR before processing the next character, and at the end of the stream emit an LF for any remaining buffered CR.

That is a specific, correct piece of advice, and it is the kind of thing that is only written down because somebody got it wrong first.

The second half of the requirement is about positions. The same normalised text has to be used for parsing and for HTML generation, and the AST offsets refer to the text you supplied to the parser, with baseOffset added when you specify one. Normalising CRLF to LF changes character offsets, so a caller that needs positions in the original text has to maintain its own offset mapping.

That is a real cost with real consequences. A markdown editor that highlights a range in the document cannot simply take the offset from the tree and apply it to the buffer the user is typing in, because the buffer and the parsed text differ by the number of characters removed in normalisation. The library is telling you to solve that problem yourself, and it is right to, because only the caller knows where the original text lives.

## The parsing algorithm: markers, and no rollback

The parsing algorithm section describes the internals, and the description is short enough to be useful rather than exhausting.

The process has two logical parts. The document is split into blocks of logical structure, that is lists, list items, blockquotes, paragraphs and so on, and then the inline structure of the resulting blocks is parsed. The README notes that this is the same approach as the one proposed in the CommonMark specification's appendix on a parsing strategy, so this is a design that follows the reference rather than inventing its own.

The block phase is described as a rollback-free algorithm. Every token in the file is processed one by one, tokens are passed to the marker blocks that are currently open, and each block chooses one of three actions: do nothing, drop itself, or complete itself. The MarkerProcessor holds the blocks, executes the actions they chose, and may add new ones.

The rollback-free part is the interesting property for a parser that has to run on every keystroke in an editor. A parser that can backtrack has to re-do work when it discovers it took a wrong turn, and in an editor that work happens constantly. A parser that never rolls back can process a document once and know the structure, which is the property that makes the staged model in the previous section cheap.

The inline phase goes through a MarkdownLexer first, for speed and convenience, and then the resulting tokens are processed in a specific way. The README starts to explain the priority handling for inline constructs, where two different constructs can overlap, and the visible text stops there.

The vocabulary is worth noting: a MarkerBlock, a MarkerProcessor, a MarkdownLexer, a flavour descriptor. This is a parser with a small number of concepts and a pipeline, and it is written by people who build editors, so the names describe the machinery rather than the markdown.

## The .flex to Kotlin pipeline nobody enjoys

The development section documents a build quirk that is worth reading before you contribute, because it explains why the repository looks the way it does.

The only non-Kotlin files in the project are the .flex lexer definitions, and they are used to generate the lexers, which are the first stage of inline element parsing. The problem is that the native java to kt conversion crashes for these files because of bugs, so converting a .flex file into its Kotlin equivalent takes seven manual steps.

You install the Grammar Kit plugin, which the IDE offers when you open a .flex file, and the jflexToKotlin plugin, which you have to build and install manually through the settings. Then you open the .flex file and run the Run JFlex Generator action, which on the first run offers to download JFlex, where you select the project root and then delete the excessively downloaded .skeleton file. A generated .java lexer appears somewhere, which you move next to the existing .kt lexer, and you delete the .kt one. Then you run the Convert JFlex Lexer to Kotlin action on the new .java file and fix the small problems, such as imports. The README asks you to keep the number of changes to the generated files minimal, and explains why, to keep a clean Git history.

That last instruction is the real point of the section. Generated code that is edited by hand is a source of conflicts, and the project is explicit that it wants the generated files to be reproducible so that a diff shows only the intended change.

## Licence, versions and where this parser fits

The licence is Apache-2.0, which permits use in proprietary products and includes an explicit patent grant, and the LICENSE file is at the repository root alongside a CHANGELOG.md. The repository is an official JetBrains project, as the badge in the README header records, and it is not archived with the last push on 2026-10-16. There are no GitHub releases, so the version you consume is the one on Maven Central, and the changelog is the release history.

The repository is a Gradle project with the usual structure: build.gradle.kts, settings.gradle.kts, a buildSrc directory, gradle.properties, the gradle wrapper, a docs directory and a src directory, plus a run_html_gen.sh script.

The judgement about where it fits comes down to what kind of consumer you are. If you are writing an editor, a preview pane, or anything that has to re-parse as the user types, the staged model and the rollback-free block parsing are the reason to use this library rather than a general purpose one, and the Kotlin/JS IR support means the same parser can run in a browser preview. If you are rendering Markdown to HTML on a server and nothing else, this is a heavier choice than you need, and the staged API, the flavour descriptors and the line ending contract are all extra surface to learn.

There is one more consideration for a team that is not JetBrains: the project exists to serve the IDEs, so the extension points and the flavour set will be shaped by what an editor needs. The Multiplatform and extension-friendly design are deliberate and real, but a project with a large commercial maintainer behind it will prioritise its own use cases, and a fork is easier than waiting for a change when it does not.

## Conclusion

Adopt intellij-markdown when you need the Markdown behaviour JetBrains ships in its IDEs and you want the same parser on the JVM, in JavaScript and on Native from one code base, or when you need the staged parsing model to drive an editor without re-parsing whole documents. It is the wrong tool for a simple render a Markdown string to HTML on a server, where a small library will be easier, and it is the wrong tool if you cannot normalise line endings, because the parser requires LF and silently misbehaves on CRLF. Before integrating, read the input section on line endings and the offset rules, since they determine how you have to feed text to it, and pin the version because there are no GitHub releases to track.

## FAQ

### What is intellij-markdown and who maintains it?

It is a multiplatform Markdown processor written in Kotlin, maintained by JetBrains as an official project and published on Maven Central under the org.jetbrains group. It compiles for the JVM, JavaScript and Native targets from a single code base.

### How do I add intellij-markdown to a Gradle project?

Declare mavenCentral() in the repositories block and add implementation("org.jetbrains:markdown:<version>") to the dependencies. With Gradle 5.4 or newer the target platform is resolved automatically, while Maven and older Gradle need the artifact named explicitly, such as org.jetbrains:markdown-jvm or org.jetbrains:markdown-js.

### Why does the intellij-markdown parser require LF line endings?

The parser requires LF, so callers must normalise CRLF and bare CR before parsing. For streaming input the README warns against normalising each chunk independently, because a CRLF pair split across chunks would become two line breaks, and asks for the trailing CR to be buffered instead.

### What is the benefit of the staged parsing in intellij-markdown?

The block structure, the inline structure and the HTML generation are separate tasks that can be done independently. The IDE plugin example parses blocks without inlines for lazy parsing, then parses inlines of a single block for faster syntax highlighting, and only then generates HTML for the preview.

### How does intellij-markdown parse block structure?

With a rollback-free algorithm: every token is processed one by one and passed to the currently open marker blocks, and each block decides whether to do nothing, drop itself or complete itself. The MarkerProcessor stores the blocks and executes the actions they choose, adding new ones as needed.

## Sources

- [Issues](https://github.com/JetBrains/markdown/issues)
- [JetBrains/markdown on GitHub](https://github.com/JetBrains/markdown)
- [License: Apache-2.0](https://github.com/JetBrains/markdown/blob/master/LICENSE)
- [README](https://github.com/JetBrains/markdown/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/jetbrains-markdown
