mrkeyoor.com_
Thu 06 Aug 00:58 UTC
npmUtilsupdated 05 Aug 2026

marked

marked turns a Markdown string into an HTML string. Call marked.parse('# hi') and you get '<h1>hi</h1>'. It is a single-pass lexer and parser with no dependencies, it supports CommonMark plus GitHub Flavored Markdown, and it runs the same in Node, a browser, a worker, or from a CLI. Extension points are real rather than decorative: you can override the renderer for any token type, replace or add tokenizers for new syntax, walk every token before rendering, and hook the string before and after parsing. What it deliberately does not do is sanitize. The HTML it emits contains whatever HTML the Markdown contained, so any untrusted input has to pass through DOMPurify or an equivalent before it reaches a page.

Verdict

The fastest way to get Markdown onto a page when the content is trusted and you want no dependency graph. Choose markdown-it or remark instead when the input comes from users, when you need to transform an AST, or when you cannot absorb a breaking major roughly twice a year.

API stability2/5Majors 13 through 18 landed between mid 2024 and April 2026, changing renderer signatures to token objects, reworking list and text tokens, dropping the CommonJS build, raising the Node floor to 20, and trimming trailing blank lines from block tokens; extension authors have had to ship a fix for most of them.
Docs4/5marked.js.org covers options, the parsing pipeline, renderer and tokenizer method lists, hooks, worker usage, and a searchable extension directory, and the options table honestly records what was removed and where it went; migration guidance between majors is thinner than the release notes deserve.
Maintenance5/5Pushed August 2026 with 18.0.9 released the previous day, only 7 open issues (10 counting PRs), and a steady patch cadence under the markedjs organization.
Ecosystem5/5Around 63 million weekly downloads, an extension directory of several dozen published marked-* packages covering highlighting, footnotes, KaTeX, alerts, and heading IDs, and first-party packages for the features removed from core.

Use it if

  • You need Markdown to HTML with no build step and no plugin graph: one import, one function call, zero dependencies, about 12 KB gzipped in a browser bundle
  • You are rendering trusted content such as your own docs, changelogs, or CMS output written by staff, where sanitizing is a single extra call rather than an architecture
  • You want to customize output without forking: overriding renderer methods per token type or adding a block-level extension is a documented, supported path
  • You need the same parser in Node and the browser, including a UMD build you can load from a CDN with a script tag
Skip it if

Setup reality

npm install marked, then `import { marked } from 'marked'`. That is genuinely it for the happy path, since there are no dependencies and types ship in the package. The friction is module format and safety. marked is ESM-only since v16, so CommonJS projects get ERR_REQUIRE_ESM and Jest needs `transformIgnorePatterns: ['/node_modules/(?!(marked)/)']`, which the v16 release notes call out explicitly. v18 requires Node 20 or newer. In the browser, load lib/marked.umd.js or the ESM build from a CDN rather than the old marked.min.js path, which was removed. Then there is state: the exported `marked` object is a global singleton, so calling marked.use() anywhere mutates it for every other module in the process, and calling marked.use() inside a component render or a loop stacks the same extension repeatedly until you get a recursion error. Use `new Marked(...)` when a library or a component owns its own configuration. Finally, several options people look for are gone rather than renamed: sanitize, mangle, headerIds, highlight, langPrefix, baseUrl, smartypants, and xhtml were all removed in v8 and live in separate marked-* packages or in a sanitizer you install yourself.

Patterns

Render Markdown to HTMLparse-markdown

import { marked } from 'marked';

const html = marked.parse('# Title\n\nSome **bold** text.');
console.log(html);
// <h1>Title</h1>\n<p>Some <strong>bold</strong> text.</p>

parse returns a string unless the async option is on, in which case it returns a promise. There is no CommonJS build since v16, so require('marked') throws ERR_REQUIRE_ESM.

Make untrusted Markdown safe to insertsanitize-output

import { marked } from 'marked';
import DOMPurify from 'dompurify';

const dirty = marked.parse(userInput);
const clean = DOMPurify.sanitize(dirty);
document.getElementById('content').innerHTML = clean;

This is not optional for user input. The sanitize option was removed in v8 and marked emits inline HTML verbatim, so skipping DOMPurify turns any comment box into stored XSS. On the server use isomorphic-dompurify or run DOMPurify against jsdom.

Configure parsing behaviourset-options

import { marked } from 'marked';

marked.use({
  gfm: true,      // default
  breaks: true,   // single newline becomes <br>, needs gfm
  pedantic: false,
  silent: false
});

marked.use mutates the shared global instance, so this affects every module in the process. Call it once at module top level, never inside a function or a component body, or extensions stack up until parsing recurses.

Keep configuration local to your moduleisolated-instance

import { Marked } from 'marked';

const md = new Marked({ gfm: true, breaks: true });
export const render = (src) => md.parse(src);

Use this in any library or shared component. The global `marked` export is a singleton and a dependency that calls marked.use() will silently change your output.

Override the HTML for one token typecustom-renderer

import { marked } from 'marked';

marked.use({
  renderer: {
    heading({ tokens, depth }) {
      const text = this.parser.parseInline(tokens);
      const id = text.toLowerCase().replace(/[^\w]+/g, '-');
      return `<h${depth} id="${id}">${text}</h${depth}>`;
    }
  }
});

Since v13 renderer methods take a single token object, not positional arguments, and you render children yourself via this.parser.parseInline or this.parser.parse. Returning false falls through to the previous override or the default.

Add syntax marked does not knowcustom-extension

import { marked } from 'marked';

const callout = {
  name: 'callout',
  level: 'block',
  start(src) { return src.match(/^:::/)?.index; },
  tokenizer(src) {
    const rule = /^:::(\w+)\n([\s\S]*?)\n:::\n?/;
    const match = rule.exec(src);
    if (!match) return;
    return { type: 'callout', raw: match[0], kind: match[1], tokens: this.lexer.blockTokens(match[2]) };
  },
  renderer(token) {
    return `<aside class="callout ${token.kind}">${this.parser.parse(token.tokens)}</aside>`;
  }
};

marked.use({ extensions: [callout] });

start() tells the lexer where your syntax might begin so it does not swallow the block as a paragraph; leaving it out is the usual reason a custom extension never fires. Register extensions once, in module scope.

Rewrite every token of a kind before renderingwalk-tokens

import { marked } from 'marked';

marked.use({
  walkTokens(token) {
    if (token.type === 'link' && /^https?:/.test(token.href)) {
      token.href += (token.href.includes('?') ? '&' : '?') + 'ref=docs';
    }
  }
});

Tokens are passed by reference so mutations stick. Child tokens are visited before siblings. Multiple walkTokens functions run in reverse registration order, with the last registered one first.

Await work inside the parse pipelineasync-rendering

import { marked } from 'marked';

marked.use({
  async: true,
  async walkTokens(token) {
    if (token.type === 'code' && token.lang === 'mermaid') {
      token.text = await renderMermaid(token.text);
    }
  }
});

const html = await marked.parse(source);

With async: true, marked.parse returns a promise and forgetting to await it gives you '[object Promise]' in the page. An extension that sets async: true will throw if you explicitly pass async: false.

Highlight fenced code blockssyntax-highlighting

import { marked } from 'marked';
import { markedHighlight } from 'marked-highlight';
import hljs from 'highlight.js';

marked.use(markedHighlight({
  emptyLangClass: 'hljs',
  langPrefix: 'hljs language-',
  highlight(code, lang) {
    const language = hljs.getLanguage(lang) ? lang : 'plaintext';
    return hljs.highlight(code, { language }).value;
  }
}));

The highlight and langPrefix options were removed from core in v8; marked-highlight is the official replacement. Highlighted output contains HTML, so if you also sanitize, allow the class attribute or your colours vanish.

Render a fragment without a wrapping paragraphparse-inline

import { marked } from 'marked';

console.log(marked.parse('**strong** _em_'));
// <p><strong>strong</strong> <em>em</em></p>

console.log(marked.parseInline('**strong** _em_'));
// <strong>strong</strong> <em>em</em>

parseInline is what you want for titles, table cells, button labels, and anywhere a stray <p> would break layout. It also skips block syntax entirely, so a heading marker stays literal text.

Look at the token tree instead of HTMLinspect-tokens

import { marked } from 'marked';

const tokens = marked.lexer('# Title\n\n- one\n- two');
console.dir(tokens, { depth: null });

const html = marked.parser(tokens);

Handy for extracting the first heading, building a table of contents, or debugging why an extension did not match. Treat the token shape as version-specific; v13, v17, and v18 each changed how list and text tokens are structured.

Bound parse time for untrusted documentsworker-timeout

// markedWorker.js
import { marked } from 'marked';
import { parentPort } from 'node:worker_threads';
parentPort.on('message', (src) => parentPort.postMessage(marked.parse(src)));

// caller
import { Worker } from 'node:worker_threads';
const worker = new Worker('./markedWorker.js');
const timer = setTimeout(() => { worker.terminate(); throw new Error('marked took too long'); }, 2000);
worker.on('message', (html) => { clearTimeout(timer); worker.terminate(); use(html); });
worker.postMessage(userMarkdown);

The docs recommend this specifically to limit the blast radius of a regular-expression denial of service. Parsing is synchronous and CPU-bound, so a pathological document on the main thread blocks the whole event loop.

Alternatives

PackageRegistryPick it when
markdown-itnpmYou want a stable plugin API, a bigger plugin catalogue, and an optional built-in HTML-escaping mode rather than relying on an external sanitizer.
remarknpmYou need a real syntax tree to inspect, lint, or transform, or you are already in the unified ecosystem with MDX and rehype.
micromarknpmYou want strict CommonMark and GFM compliance in a small, streaming-friendly core and are happy to build your own layer on top.