comark: a Markdown parser built for output that arrives half finished
A high-performance Markdown parser and renderer for Angular, React, Svelte, Vue, HTML and ANSI.
At a glance
- What is it?
- This is a Markdown parser and multi-framework renderer whose distinguishing feature is not speed but a completion pass: when text is still streaming in, it closes the unterminated bold, the open code fence and the half-written component, so generated output renders correctly at every frame instead of leaking raw syntax. Everything else in the design follows from that one problem.
- Who is it for?
- Comark is the right choice if you are rendering text a language model is still writing, because a completion pass for unterminated syntax is the one problem that every other Markdown renderer gets wrong and that you cannot fix in your own component. It is also a reasonable choice if you want to parse on a server and render in the browser from the same document, since the parse output is designed to cross that boundary.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Closing unfinished syntax is the feature, and the rest follows from it
The second entry in the list of reasons to use this library is the one that explains the project.
The problem is that Markdown streamed from a language model is not valid Markdown. At any given frame, the text on screen ends wherever the model stopped, and that might be the middle of a bold run, the inside of an unclosed code fence, or between the opening and closing markers of a custom component. Every renderer has one of two bad answers. It renders what it has, and you see two asterisks on screen. Or it refuses to render incomplete nodes, and your paragraph vanishes.
This library takes a third option. There is a completion pass that closes unterminated syntax before rendering, so a half-written bold run renders as bold, an open fence renders as a code block with what is inside it, and a half-open component renders as a component. The stated consequence is that AI output renders correctly at every frame.
That is a narrow, well-defined problem with a clean solution, and it explains the project's existence more convincingly than a general claim about performance would. A Markdown parser that is thirty percent faster is not interesting, because parsing is not usually the bottleneck and because a thirty percent speedup is invisible to a user. A completion pass is visible, it is a bug report you get once and never again, and it is the kind of thing that has to be built into the parser rather than bolted onto a component, because only the parser knows which constructs are open and where they end.
It also explains a design choice in the feature list that would otherwise look like an odd emphasis. The list leads with runtime parsing, with the claim that a parse function is a pure function returning a document, so content from a database, a content management system or a model is live the moment it is saved, with no rebuild and no redeploy. A document that is produced on demand can be produced per frame, and a document produced per frame has to be renderable while incomplete. Streaming and runtime parsing are not two features. They are the same feature seen from two ends.
The third entry, that one parser serves every renderer, is a consequence of the same thinking. If the parse output is a document rather than a string of markup, then rendering is a separate concern and can be implemented five times, once per framework, plus once for a plain HTML string and once for a terminal. The content outlives the framework, which is what the readme says it means.
The document is a nested array, which makes it a wire format
Installation is one command:
npm install comarkThe quick start shows the shape of the parse output, and it is worth looking at closely, because the shape is a decision rather than an implementation detail.
A document is an object with three keys. The first holds the nodes, and a node is a nested array: a tag name, then a props object, then the children. The second holds frontmatter. The third holds metadata. Inside the nodes, a bold run is itself an array with a tag, an empty props object, and its text child. A heading is an array with a tag, a props object carrying a generated identifier, and two children, one of them a text string and one of them a nested bold array.
So the entire tree is arrays and strings, with objects appearing only for props. No node has a type field, no node has a children field, no node has a tag field. The position carries the meaning.
That choice has three consequences and they pull in different directions.
The first is size. An object-based tree repeats three keys per node, and a document with a few hundred nodes repeats them a few hundred times. Positional arrays remove that entirely. For a library whose selling point includes sending a parsed document over a network to render on the client, that is not a micro-optimisation, it is the difference between a payload that compresses well and one that does not.
The second is that the structure is trivially serialisable and trivially traversable in any language, because arrays and strings are the two things every serialisation format handles natively. A server that parses in one runtime and a client that renders in another can agree on this format without either side knowing anything about the other's conventions.
The third is the cost, and it is real. The document is not self-describing. If you log it, you get nested brackets. If you try to hand-edit content in a content management system that stores this shape, you are editing positional arrays, and inserting a node in the wrong position silently corrupts the tree rather than failing. There is no schema validation to catch it, because the type is just whatever the array's length happens to be.
So this is a format optimised for machine transport at the cost of human legibility, chosen by a project whose primary use case is moving a document between a parse step and a render step. For that use case it is the right trade. If you are storing documents for a human to edit, the same content as an object tree would be more forgiving.
Parse on the server, render in the browser, and never parse twice
One entry in the feature list is about architecture rather than features, and it is the one that changes how you would structure an application.
Parse and render are decoupled. You parse once on the server, send the serialisable document to the client, and render it there without parsing again.
Almost every Markdown library in the JavaScript ecosystem does both in one call. The typical renderer takes a string and returns markup, and if you want to render in the browser you send the string and let the browser parse it. That is simple and it is correct for most content, and it is wasteful in a specific case: content that is the same for every visitor.
A documentation site, a product page, a help centre article: the parsed document is identical for every reader, so parsing it once at build time or on the server is strictly better than parsing it a thousand times in a thousand browsers. Splitting parse from render makes that possible, and it also makes the payload smaller because the repeated keys are gone.
The React package's description mentions server components, which is the clearest sign that this is a deliberate goal rather than an accident. A server component can receive a parsed document as data and render it with no client-side parser at all, which removes the parser from the client bundle entirely for content that was already parsed upstream.
There is a second benefit that is less obvious. Splitting the two steps means the renderer is replaceable without touching the parser, which is the whole point of having five renderers. And it means you can parse once and render the same document in several places, for example a web page and a terminal, from a single parse.
The cost is that the boundary becomes an interface you have to respect. Anything that needs the source text rather than the document cannot be done at render time, and any syntax the parser does not support cannot be added by a renderer. That constraint is exactly what makes the five renderers thin, and it is also what limits the plugin system, which has to work at parse time rather than at render time. The readme's plugin list is consistent with that: syntax highlighting, mathematics, diagrams, tables of contents, alerts and footnotes are all things the parser has to know about, because they all change the document rather than the appearance.
Still just Markdown, argued against a named competitor
One entry in the feature list is a positioning statement rather than a capability, and it is worth reading as an argument rather than a slogan.
The claim is that it is still just Markdown. Full support for the two major Markdown specifications, frontmatter, and a mechanism for attaching classes to native elements. Components, the readme says, are opt-in syntax rather than a new language.
There is a comparison page linked from the same bullet, and it names a specific competitor. The argument is one that anybody who has worked with content authored by non-programmers will recognise. The competitor format embeds a component language inside Markdown, which means the content file is no longer something a writer can be trusted with, because a stray angle bracket or a missing closing tag is a build failure rather than a rendering oddity. Everything that was true about Markdown, which was that it was safe to hand to a writer, stops being true.
This project's answer is a container syntax. You write a double colon, a component name, attributes in braces, the content, and a closing double colon. Inside it is ordinary Markdown, and the example in the readme shows bold text inside a custom component working exactly as it would anywhere else.
The other half of the answer is attributes on native elements, written as a brace-prefixed class in the element's own syntax. That means a designer can add a class to a heading without introducing a component, and a developer can style it in their own stylesheet, and neither of them has to learn a component language first.
The one place the claim is doing real work is in the phrase opt-in. A document with no double colons and no brace attributes is a plain Markdown document that any renderer will handle. You can adopt the parser without adopting the extension, migrate content file by file, and if you later decide the extension was a mistake, delete the components without touching the prose. That is a genuinely different risk profile from the competitor's, and it is the strongest argument in the readme even though it is not presented as one.
The bullet is also careful to say the syntax is an addition to native elements rather than a replacement for them, which is a small detail that determines whether your existing content keeps working.
Eight packages, twenty integration targets, and a directory of dependency patches
The promise is one parser for every renderer. The price, visible in the build scripts, is a maintenance surface considerably larger than the package count suggests.
There are eight published packages. One is the core parser with its utilities and plugins. Seven are renderers: one each for four frontend frameworks, one for a framework meta-module with auto-imports, one that produces an HTML string, and one that produces terminal escape sequences.
Then there are the development scripts, and there are around twenty of them. Seven are the obvious per-renderer development servers. The rest are integration surfaces: a static site generator, a documentation site generator, a full-stack framework, another full-stack framework, a meta-framework, and a long tail of small variants that each test one plugin in one framework. There are separate targets for diagrams, for mathematics, for syntax highlighting, for background colours, for a TypeScript-in-Markdown tool, for a JSON rendering plugin, and for a binding.
That long tail is the real cost of the promise. Each of those is a combination of a plugin and a framework that a user could plausibly deploy, and each one is a place where the abstraction can leak. A plugin that works in one renderer and not another is a bug report waiting to happen, and the only way to catch it is to have a development target for it. Twenty targets is what thorough looks like, and it is also twenty things to keep working as the frameworks move.
Two other entries in the root directory tell you about the project's relationship with its dependencies. There is a directory of committed patches, which means some dependencies ship code that does not quite do what is needed and the fix is applied locally rather than upstream. That is a pragmatic technique and it is also a maintenance liability, because a patch has to be reapplied every time the dependency updates, and a dependency update bot is configured to apply them automatically, which is the right automation and the wrong time to discover a patch stopped applying.
And there is a script whose job is to synchronise plugins across the packages, run after every build and after every packaging step. That is the mechanism behind the ecosystem claim: one plugin definition, propagated to every renderer, so a plugin cannot behave differently in two frameworks. It is also a build step that every contributor has to have working, which is a small coupling cost for a large consistency gain.
The linter and formatter are configuration files for tools written in a systems language rather than in JavaScript, which is a consistent choice for a project whose pitch includes speed, and a slightly risky one because those tools are newer than the ones they replace.
Editorial conclusion
Comark is the right choice if you are rendering text a language model is still writing, because a completion pass for unterminated syntax is the one problem that every other Markdown renderer gets wrong and that you cannot fix in your own component. It is also a reasonable choice if you want to parse on a server and render in the browser from the same document, since the parse output is designed to cross that boundary. It is a poor fit if you need a very small dependency, since there are eight published packages and an optional plugin for most things, and a poor fit if your content is authored in MDX today and you want compatibility, because the project is positioned against that syntax rather than beside it.
Frequently asked questions
What problem does comark solve that other Markdown parsers do not?
Streaming. When text is still arriving, such as output from a language model, the text on screen ends mid-construct. Comark completes unterminated syntax before rendering, so a half-written bold run, an open code fence or a half-open component still renders correctly at each frame rather than leaking raw markers or disappearing.
What does the comark parse output look like?
A compact object with nodes, frontmatter and metadata, where each node is a nested array of a tag name, a props object and its children. Nothing repeats a key per node, which makes the document small, trivially serialisable and easy to move between a server and a browser, at the cost of being unreadable in a debugger.
Why can comark render the same content in five different targets?
Because parsing and rendering are separate steps. The parser produces a serialisable document and the renderers turn that document into framework components, an HTML string or terminal escape sequences. A document parsed once on a server can be rendered in a browser, in a terminal, or in several frameworks, and the content outlives whichever framework is in use.
Is comark compatible with MDX or JSX-style Markdown?
The project positions itself against that approach rather than beside it. It supports the CommonMark and GFM specifications plus frontmatter, and adds components as opt-in container syntax with a double-colon marker, plus a brace-prefixed class on native elements. Content with no extensions is plain Markdown that any other renderer will handle.
Where does comark's component syntax come from?
From MDC, a content module in the Nuxt content ecosystem, which the readme credits with five years of production use. So the extension is a borrowed convention with a track record rather than a new syntax, which is the strongest argument available for choosing it.
Official sources
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.
[](https://hysenlabs.com/projects/comarkdown-comark)