or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/marked@17.0.x

docs

examples

edge-cases.mdreal-world-scenarios.md
index.md
tile.json

tessl/npm-marked

tessl install tessl/npm-marked@17.0.0

A markdown parser built for speed

real-world-scenarios.mddocs/examples/

Real-World Scenarios

Complete examples for common use cases.

Basic Parsing

Document Parsing

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);

User Comments

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);
}

Blog Posts

import { marked } from "marked";

marked.setOptions({
  gfm: true,
  breaks: false
});

const postHtml = marked.parse(blogPost);

Tooltips

import { marked } from "marked";

const tooltipHtml = marked.parseInline('This is **important**');
// Use parseInline for short content without <p> wrapper

Custom Syntax

Emoji Support

import { 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!');

Wiki-Style Links

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

Custom Alert Blocks

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']
});

Math Expressions

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

Custom Rendering

Syntax Highlighting

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

Custom Heading Anchors

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 });

External Link Handling

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

Image Lazy Loading

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

Table of Contents

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

Front Matter

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);

Async Link Validation

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

Footnotes

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

YouTube Embeds

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`;
    }
  }]
});

Custom Blockquotes

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`;
    }
  }
});

Numbered Headings

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 });

Token Statistics

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);

Reading Time Estimator

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}`;
    }
  }
});

Plain Text Extraction

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."

Caching

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;
}

Multi-Instance Configuration

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);

Related Documentation

  • Extensions Guide
  • Rendering Guide
  • Token Manipulation Guide
  • Edge Cases