tessl install tessl/npm-marked@17.0.0A markdown parser built for speed
Complete examples for common use cases.
import { marked } from "marked";
import fs from "fs";
const markdown = fs.readFileSync('README.md', 'utf-8');
const html = marked.parse(markdown, { gfm: true });
fs.writeFileSync('README.html', html);import { marked } from "marked";
import DOMPurify from "dompurify";
function renderComment(userComment) {
marked.setOptions({ silent: true, gfm: true, breaks: true });
const html = marked.parse(userComment);
return DOMPurify.sanitize(html);
}import { marked } from "marked";
marked.setOptions({
gfm: true,
breaks: false
});
const postHtml = marked.parse(blogPost);import { marked } from "marked";
const tooltipHtml = marked.parseInline('This is **important**');
// Use parseInline for short content without <p> wrapperimport { marked } from "marked";
marked.use({
extensions: [{
name: 'emoji',
level: 'inline',
start(src) { return src.indexOf(':'); },
tokenizer(src) {
const match = src.match(/^:([a-z_]+):/);
if (match) {
return { type: 'emoji', raw: match[0], name: match[1] };
}
},
renderer(token) {
return `<span class="emoji emoji-${token.name}" role="img" aria-label="${token.name}"></span>`;
}
}]
});
const html = marked.parse('Hello :wave: world!');import { marked } from "marked";
marked.use({
extensions: [{
name: 'wikilink',
level: 'inline',
start(src) { return src.indexOf('[['); },
tokenizer(src) {
const match = src.match(/^\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/);
if (match) {
const page = match[1].trim();
const display = match[2] ? match[2].trim() : page;
const slug = page.toLowerCase().replace(/\s+/g, '-');
return { type: 'wikilink', raw: match[0], page, display, slug };
}
},
renderer(token) {
return `<a href="/wiki/${encodeURIComponent(token.slug)}" class="wikilink">${escapeHtml(token.display)}</a>`;
}
}]
});
function escapeHtml(html) {
return String(html).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}import { marked } from "marked";
marked.use({
extensions: [{
name: 'alert',
level: 'block',
start(src) { return src.match(/^:::/)?.index; },
tokenizer(src) {
const match = src.match(/^:::(\w+)\n([\s\S]*?)\n:::/);
if (match) {
return {
type: 'alert',
raw: match[0],
alertType: match[1],
text: match[2],
tokens: this.lexer.blockTokens(match[2])
};
}
},
renderer(token) {
const body = this.parser.parse(token.tokens);
return `<div class="alert alert-${token.alertType}" role="alert">${body}</div>\n`;
}
}],
childTokens: ['tokens']
});import { marked } from "marked";
marked.use({
extensions: [
{
name: 'inlineMath',
level: 'inline',
start(src) { return src.indexOf('$'); },
tokenizer(src) {
const match = src.match(/^\$([^$\n]+?)\$/);
if (match) {
return { type: 'inlineMath', raw: match[0], text: match[1] };
}
},
renderer(token) {
return `<span class="math-inline">\\(${escapeHtml(token.text)}\\)</span>`;
}
},
{
name: 'blockMath',
level: 'block',
start(src) { return src.match(/^\$\$/)?.index; },
tokenizer(src) {
const match = src.match(/^\$\$\n([\s\S]+?)\n\$\$/);
if (match) {
return { type: 'blockMath', raw: match[0], text: match[1] };
}
},
renderer(token) {
return `<div class="math-block">\\[${escapeHtml(token.text)}\\]</div>\n`;
}
}
]
});
function escapeHtml(html) {
return String(html).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}import { marked } from "marked";
import hljs from "highlight.js";
marked.use({
renderer: {
code({ text, lang, escaped }) {
if (lang && hljs.getLanguage(lang)) {
try {
const highlighted = hljs.highlight(text, { language: lang }).value;
return `<pre><code class="hljs language-${escapeHtml(lang)}">${highlighted}</code></pre>\n`;
} catch (err) {
console.error('Highlight error:', err);
}
}
const code = escaped ? text : escapeHtml(text);
const langClass = lang ? ` class="language-${escapeHtml(lang)}"` : '';
return `<pre><code${langClass}>${code}</code></pre>\n`;
}
}
});
function escapeHtml(html) {
return String(html).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}import { marked, Renderer } from "marked";
const renderer = new Renderer();
renderer.heading = ({ tokens, depth }) => {
const text = renderer.parser.parseInline(tokens);
const id = text.toLowerCase().replace(/[^\w]+/g, '-');
return `<h${depth} id="${id}"><a href="#${id}" class="anchor">#</a> ${text}</h${depth}>\n`;
};
marked.setOptions({ renderer });import { marked } from "marked";
marked.use({
renderer: {
link({ href, title, tokens }) {
const text = this.parser.parseInline(tokens);
const titleAttr = title ? ` title="${escapeHtml(title)}"` : '';
const isExternal = href.startsWith('http://') || href.startsWith('https://');
const external = isExternal ? ' target="_blank" rel="noopener noreferrer"' : '';
return `<a href="${escapeHtml(href)}"${titleAttr}${external}>${text}</a>`;
}
}
});
function escapeHtml(html) {
return String(html).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}import { marked } from "marked";
marked.use({
renderer: {
image({ href, title, text }) {
const alt = text ? ` alt="${escapeHtml(text)}"` : '';
const titleAttr = title ? ` title="${escapeHtml(title)}"` : '';
if (title) {
return `<figure><img src="${escapeHtml(href)}"${alt}${titleAttr} loading="lazy"><figcaption>${escapeHtml(title)}</figcaption></figure>`;
}
return `<img src="${escapeHtml(href)}"${alt} loading="lazy">`;
}
}
});
function escapeHtml(html) {
return String(html).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}import { marked } from "marked";
const toc = [];
marked.use({
walkTokens(token) {
if (token.type === 'heading' && token.depth <= 3) {
const id = token.text.toLowerCase().replace(/[^\w]+/g, '-');
toc.push({ level: token.depth, text: token.text, id });
token.headingId = id;
}
},
renderer: {
heading({ tokens, depth, headingId }) {
const text = this.parser.parseInline(tokens);
const id = headingId || text.toLowerCase().replace(/[^\w]+/g, '-');
return `<h${depth} id="${id}">${text}</h${depth}>\n`;
}
}
});
const html = marked.parse(markdown);
const tocHtml = '<nav class="toc"><h2>Contents</h2><ul>' +
toc.map(item => `<li><a href="#${item.id}">${escapeHtml(item.text)}</a></li>`).join('') +
'</ul></nav>';
const fullHtml = tocHtml + html;
toc.length = 0;
function escapeHtml(html) {
return String(html).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}import { marked } from "marked";
import yaml from "js-yaml";
let frontMatter = {};
marked.use({
hooks: {
preprocess(markdown) {
const match = markdown.match(/^---\n([\s\S]+?)\n---\n/);
if (match) {
try {
frontMatter = yaml.load(match[1]);
} catch (err) {
console.error('Invalid YAML:', err);
frontMatter = {};
}
return markdown.slice(match[0].length);
}
frontMatter = {};
return markdown;
}
}
});
const html = marked.parse(markdownWithFrontMatter);
console.log('Front matter:', frontMatter);import { marked } from "marked";
const linkValidation = new Map();
async function validateUrl(url) {
if (linkValidation.has(url)) {
return linkValidation.get(url);
}
try {
const response = await fetch(url, { method: 'HEAD', timeout: 5000 });
const isValid = response.ok;
linkValidation.set(url, isValid);
return isValid;
} catch (err) {
linkValidation.set(url, false);
return false;
}
}
marked.use({
async: true,
async walkTokens(token) {
if (token.type === 'link') {
token.validated = await validateUrl(token.href);
}
},
renderer: {
link(token) {
const text = this.parser.parseInline(token.tokens);
if (token.validated === false) {
return `<span class="broken-link">${text}</span>`;
}
return `<a href="${escapeHtml(token.href)}">${text}</a>`;
}
}
});
const html = await marked.parse(markdown, { async: true });
function escapeHtml(html) {
return String(html).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}import { marked } from "marked";
const footnotes = {};
let footnoteCounter = 0;
marked.use({
extensions: [
{
name: 'footnoteRef',
level: 'inline',
start(src) { return src.indexOf('[^'); },
tokenizer(src) {
const match = src.match(/^\[\^(\w+)\]/);
if (match) {
return { type: 'footnoteRef', raw: match[0], id: match[1] };
}
},
renderer(token) {
if (!footnotes[token.id]) {
footnotes[token.id] = { num: ++footnoteCounter };
}
const num = footnotes[token.id].num;
return `<sup><a href="#fn-${token.id}" id="fnref-${token.id}">${num}</a></sup>`;
}
},
{
name: 'footnoteDef',
level: 'block',
start(src) { return src.match(/^\[\^/)?.index; },
tokenizer(src) {
const match = src.match(/^\[\^(\w+)\]:\s+(.+)/);
if (match) {
return { type: 'footnoteDef', raw: match[0], id: match[1], text: match[2] };
}
},
renderer(token) {
if (!footnotes[token.id]) {
footnotes[token.id] = { num: ++footnoteCounter };
}
footnotes[token.id].text = token.text;
return '';
}
}
],
hooks: {
postprocess(html) {
if (Object.keys(footnotes).length === 0) return html;
let footnotesHtml = '<section class="footnotes"><hr><ol>';
for (const [id, data] of Object.entries(footnotes)) {
if (data.text) {
footnotesHtml += `<li id="fn-${id}">${escapeHtml(data.text)} <a href="#fnref-${id}">↩</a></li>`;
}
}
footnotesHtml += '</ol></section>';
return html + footnotesHtml;
}
}
});
function escapeHtml(html) {
return String(html).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}import { marked } from "marked";
marked.use({
extensions: [{
name: 'youtube',
level: 'block',
start(src) { return src.match(/^@\[youtube\]/)?.index; },
tokenizer(src) {
const match = src.match(/^@\[youtube\]\(([\w-]+)\)/);
if (match) {
return { type: 'youtube', raw: match[0], videoId: match[1] };
}
},
renderer(token) {
return `<iframe width="560" height="315" src="https://www.youtube.com/embed/${token.videoId}" frameborder="0" allowfullscreen loading="lazy"></iframe>\n`;
}
}]
});import { marked } from "marked";
marked.use({
renderer: {
blockquote(token) {
const body = this.parser.parse(token.tokens);
const firstLine = body.match(/<p>(.+?)<\/p>/)?.[1] || '';
let type = 'default';
let content = body;
if (firstLine.startsWith('Note:')) {
type = 'note';
content = body.replace(/<p>Note:\s*/, '<p>');
} else if (firstLine.startsWith('Warning:')) {
type = 'warning';
content = body.replace(/<p>Warning:\s*/, '<p>');
}
return `<blockquote class="callout callout-${type}">${content}</blockquote>\n`;
}
}
});import { marked, Renderer } from "marked";
const renderer = new Renderer();
const headingNumbers = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
renderer.heading = ({ tokens, depth }) => {
for (let i = depth + 1; i <= 6; i++) {
headingNumbers[`h${i}`] = 0;
}
headingNumbers[`h${depth}`]++;
const numbers = [];
for (let i = 1; i <= depth; i++) {
numbers.push(headingNumbers[`h${i}`]);
}
const numberPrefix = numbers.join('.') + '. ';
const text = renderer.parser.parseInline(tokens);
return `<h${depth}>${numberPrefix}${text}</h${depth}>\n`;
};
marked.setOptions({ renderer });import { marked } from "marked";
function analyzeTokens(markdown) {
const tokens = marked.lexer(markdown);
const stats = {
headings: { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 },
paragraphs: 0,
codeBlocks: 0,
links: 0,
images: 0
};
marked.walkTokens(tokens, token => {
if (token.type === 'heading') stats.headings[`h${token.depth}`]++;
if (token.type === 'paragraph') stats.paragraphs++;
if (token.type === 'code') stats.codeBlocks++;
if (token.type === 'link') stats.links++;
if (token.type === 'image') stats.images++;
});
return stats;
}
const stats = analyzeTokens(markdown);
console.log('Document statistics:', stats);import { marked } from "marked";
let readingTime = 0;
const WORDS_PER_MINUTE = 200;
marked.use({
hooks: {
processAllTokens(tokens) {
let wordCount = 0;
function countWords(tokens) {
tokens.forEach(token => {
if (token.type === 'text' && token.text) {
wordCount += token.text.split(/\s+/).length;
}
if (token.type === 'code' && token.text) {
wordCount += token.text.split(/\s+/).length * 0.5;
}
if (token.tokens) countWords(token.tokens);
});
}
countWords(tokens);
readingTime = Math.ceil(wordCount / WORDS_PER_MINUTE);
return tokens;
},
postprocess(html) {
return `<div class="reading-time">${readingTime} min read</div>\n${html}`;
}
}
});import { Parser, TextRenderer, Lexer } from "marked";
function extractText(markdown) {
const tokens = Lexer.lex(markdown);
const textRenderer = new TextRenderer();
const parser = new Parser();
let result = '';
function processTokens(tokens) {
tokens.forEach(token => {
if (token.type === 'text') {
result += token.text + ' ';
} else if (token.tokens) {
processTokens(token.tokens);
}
});
}
processTokens(tokens);
return result.trim();
}
const plainText = extractText('# Heading\n\nThis is **bold**.');
// Output: "Heading This is bold."import { marked } from "marked";
const parseCache = new Map();
function cachedParse(markdown) {
if (parseCache.has(markdown)) {
return parseCache.get(markdown);
}
const html = marked.parse(markdown);
if (parseCache.size >= 1000) {
const firstKey = parseCache.keys().next().value;
parseCache.delete(firstKey);
}
parseCache.set(markdown, html);
return html;
}import { Marked } from "marked";
// Instance for GitHub Flavored Markdown
const gfmMarked = new Marked({ gfm: true, breaks: true });
// Instance for strict CommonMark
const strictMarked = new Marked({ gfm: false });
// Instance for blog posts
const blogMarked = new Marked({
gfm: true,
breaks: false,
renderer: {
link({ href, title, tokens }) {
const text = this.parser.parseInline(tokens);
const titleAttr = title ? ` title="${title}"` : '';
const target = href.startsWith('http') ? ' target="_blank" rel="noopener"' : '';
return `<a href="${href}"${titleAttr}${target}>${text}</a>`;
}
}
});
// Each instance has independent configuration
const html1 = gfmMarked.parse(markdown);
const html2 = strictMarked.parse(markdown);
const html3 = blogMarked.parse(markdown);