or run

tessl search
Log in

Version

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

docs

index.md
tile.json

tessl/npm-marked

tessl install tessl/npm-marked@17.0.0

A markdown parser built for speed

hooks.mddocs/reference/

Hooks

Hooks provide interception points in the markdown processing pipeline, allowing you to transform content before parsing, after rendering, and at other key stages.

Class: Hooks

class Hooks {
  constructor(options?: MarkedOptions);

  /**
   * Hook options
   */
  options: MarkedOptions;

  /**
   * Whether processing block-level (true) or inline (false) content
   * Set automatically by parser
   */
  block?: boolean;

  /**
   * Set of hook names that pass their input to the next hook
   * Includes: preprocess, postprocess, processAllTokens, emStrongMask
   */
  static readonly passThroughHooks: Set<string>;

  /**
   * Set of pass-through hooks that respect async mode
   * Includes: preprocess, postprocess, processAllTokens
   */
  static readonly passThroughHooksRespectAsync: Set<string>;

  /**
   * Process markdown before marked parses it
   * Use cases: template substitution, custom syntax preprocessing, content normalization
   * @param markdown - Raw markdown string
   * @returns Processed markdown string (or Promise when async)
   */
  preprocess(markdown: string): string | Promise<string>;

  /**
   * Process HTML after marked generates it
   * Use cases: wrapper elements, class additions, post-processing transformations
   * @param html - Generated HTML string
   * @returns Processed HTML string (or Promise when async)
   */
  postprocess(html: string): string | Promise<string>;

  /**
   * Process all tokens after lexing but before walkTokens
   * Use cases: bulk modifications, validation, metadata extraction, token filtering
   * @param tokens - Token array or TokensList
   * @returns Processed token array (or Promise when async)
   */
  processAllTokens(tokens: Token[] | TokensList): Token[] | TokensList | Promise<Token[] | TokensList>;

  /**
   * Mask content that should not be interpreted as em/strong delimiters
   * Used to prevent emphasis parsing in certain contexts
   * Use cases: protecting custom syntax, preserving special character sequences
   * @param src - Source text to mask
   * @returns Masked source text (same length, with em/strong delimiters replaced)
   */
  emStrongMask(src: string): string;

  /**
   * Provide a custom function to tokenize markdown
   * Advanced use case: completely replace lexer
   * @returns Lexer function (either lex or lexInline based on context)
   */
  provideLexer(): typeof Lexer.lex | typeof Lexer.lexInline;

  /**
   * Provide a custom function to parse tokens
   * Advanced use case: completely replace parser
   * @returns Parser function (either parse or parseInline based on context)
   */
  provideParser(): typeof Parser.parse | typeof Parser.parseInline;
}

Hook Types

Pass-Through Hooks

Pass-through hooks receive input, process it, and pass the result to the next hook in the chain:

  • preprocess - Processes markdown before parsing
  • postprocess - Processes HTML after rendering
  • processAllTokens - Processes tokens before walkTokens
  • emStrongMask - Masks regions that shouldn't be parsed as emphasis

Chaining Behavior: When multiple extensions define the same pass-through hook, they execute in order, with each receiving the output of the previous.

Provider Hooks

Provider hooks return functions that replace default behavior:

  • provideLexer - Returns custom lexer function
  • provideParser - Returns custom parser function

Override Behavior: Only the last provider hook is used (last extension wins).

Usage

Preprocess Hook

Modify markdown before it is parsed:

import { marked } from "marked";

marked.use({
  hooks: {
    preprocess(markdown) {
      // Replace custom shortcodes
      return markdown
        .replace(/\{DATE\}/g, new Date().toLocaleDateString())
        .replace(/\{VERSION\}/g, '1.0.0')
        .replace(/\{YEAR\}/g, new Date().getFullYear().toString());
    }
  }
});

const html = marked.parse('Version: {VERSION}, Date: {DATE}');
// Expands shortcodes before parsing

Use Cases:

  • Template variable substitution
  • Custom syntax preprocessing
  • Content normalization
  • External content inclusion
  • Markdown variant conversion

Edge Cases:

  • Empty input returns empty output
  • Must return string of valid markdown
  • Regex replacements should handle edge cases (e.g., escaped characters)
  • Consider performance with large inputs

Postprocess Hook

Modify HTML after it is generated:

import { marked } from "marked";

marked.use({
  hooks: {
    postprocess(html) {
      // Add responsive classes to tables
      return html
        .replace(/<table>/g, '<table class="table table-responsive">')
        .replace(/<img /g, '<img loading="lazy" ');
    }
  }
});

const html = marked.parse('| A | B |\n|---|---|\n| 1 | 2 |');
// Tables have custom class, images have lazy loading

Use Cases:

  • Adding wrapper elements
  • Class or attribute injection
  • HTML minification
  • Link modification
  • Accessibility enhancements

Edge Cases:

  • Must return valid HTML
  • Be careful with regex replacements (may match unexpected content)
  • Consider escaped characters in HTML
  • Nested tags may require more sophisticated parsing

Process All Tokens Hook

Modify tokens after lexing but before walkTokens:

import { marked } from "marked";

marked.use({
  hooks: {
    processAllTokens(tokens) {
      // Add metadata to all tokens
      const timestamp = Date.now();
      
      function addMetadata(tokens) {
        tokens.forEach(token => {
          token._processed = timestamp;
          token._id = generateUniqueId();
          
          // Recursively process nested tokens
          if (token.tokens) {
            addMetadata(token.tokens);
          }
        });
      }
      
      addMetadata(tokens);
      return tokens;
    }
  }
});

Use Cases:

  • Bulk token modifications
  • Token validation
  • Metadata extraction
  • Token filtering or removal
  • Token tree restructuring

Important:

  • Must return token array (modified or new)
  • Can add/remove/modify tokens
  • Executes before walkTokens extension
  • Should handle nested tokens if needed

Em/Strong Mask Hook

Prevent emphasis parsing in specific contexts:

import { marked } from "marked";

marked.use({
  hooks: {
    emStrongMask(src) {
      // Mask content inside [[ ]] so it's not parsed as emphasis
      // Replace * and _ with spaces (same length preservation)
      return src.replace(/\[\[([^\]]+)\]\]/g, (match, content) => {
        // Mask * and _ characters to prevent em/strong parsing
        const masked = content.replace(/[*_]/g, ' ');
        return '[[' + masked + ']]';
      });
    }
  }
});

// Now [[text_with_underscores]] won't have underscores treated as emphasis

Important:

  • Must return string of same length as input
  • Only mask characters that trigger em/strong (* and _)
  • Don't mask other markdown syntax
  • Used internally during em/strong tokenization

Provide Lexer Hook

Replace the lexer with a custom implementation:

import { marked, Lexer } from "marked";

marked.use({
  hooks: {
    provideLexer() {
      // Return custom lexer function
      return (src, options) => {
        console.log('Custom lexer called with', src.length, 'characters');
        
        // Use default lexer or implement custom
        // this.block indicates if processing block or inline
        return this.block ? Lexer.lex(src, options) : Lexer.lexInline(src, options);
      };
    }
  }
});

Use Cases:

  • Complete lexer replacement
  • Lexer instrumentation (logging, metrics)
  • Caching lexer results
  • Custom token tree structure

Warning: Advanced use case. Incorrect implementation will break parsing.

Provide Parser Hook

Replace the parser with a custom implementation:

import { marked, Parser } from "marked";

marked.use({
  hooks: {
    provideParser() {
      // Return custom parser function
      return (tokens, options) => {
        console.log('Custom parser called with', tokens.length, 'tokens');
        
        // Use default parser or implement custom
        return this.block ? Parser.parse(tokens, options) : Parser.parseInline(tokens, options);
      };
    }
  }
});

Use Cases:

  • Complete parser replacement
  • Parser instrumentation
  • Non-HTML output formats
  • Custom rendering logic

Warning: Advanced use case. Incorrect implementation will break rendering.

Async Hooks

Hooks support async operations when async: true is set:

import { marked } from "marked";
import fetch from "node-fetch";

marked.use({
  async: true,
  hooks: {
    async preprocess(markdown) {
      // Async preprocessing (e.g., fetch external content)
      const includeRegex = /@include\((.*?)\)/g;
      let result = markdown;
      
      const matches = [...markdown.matchAll(includeRegex)];
      for (const match of matches) {
        const url = match[1];
        try {
          const response = await fetch(url);
          const content = await response.text();
          result = result.replace(match[0], content);
        } catch (err) {
          console.error(`Failed to include ${url}:`, err);
          result = result.replace(match[0], `<!-- Error including ${url} -->`);
        }
      }
      
      return result;
    },

    async postprocess(html) {
      // Async postprocessing
      const processed = await processWithExternalService(html);
      return processed;
    }
  }
});

// Must await when async is true
const html = await marked.parse('Content: @include(https://example.com/snippet.md)', { async: true });

Async Notes:

  • Set async: true in extension or options
  • All async hooks must be awaited
  • Mix of sync and async hooks is supported
  • Performance impact: async operations block rendering

Hook Chaining

Multiple hooks can be chained together:

import { marked } from "marked";

// First extension
marked.use({
  hooks: {
    preprocess(markdown) {
      console.log('First preprocess');
      return markdown.replace(/foo/g, 'bar');
    },
    postprocess(html) {
      console.log('First postprocess');
      return html.replace(/<p>/g, '<p class="first">');
    }
  }
});

// Second extension
marked.use({
  hooks: {
    preprocess(markdown) {
      console.log('Second preprocess');
      return markdown.replace(/hello/g, 'world');
    },
    postprocess(html) {
      console.log('Second postprocess');
      return html.replace(/<\/p>/g, '</p><!-- end -->');
    }
  }
});

// Execution order: 
// 1. First preprocess
// 2. Second preprocess
// 3. Parse
// 4. First postprocess
// 5. Second postprocess

Chaining Rules:

  • Hooks execute in registration order
  • Each hook receives output of previous hook
  • Error in one hook stops the chain (unless caught)
  • Last hook's output is final result

Real-World Examples

Add Front Matter Support

import { marked } from "marked";
import yaml from "js-yaml";

let frontMatter = {};

marked.use({
  hooks: {
    preprocess(markdown) {
      // Extract YAML front matter
      const match = markdown.match(/^---\n([\s\S]+?)\n---\n/);
      if (match) {
        try {
          frontMatter = yaml.load(match[1]);
        } catch (err) {
          console.error('Invalid front matter:', err);
          frontMatter = {};
        }
        // Remove front matter from markdown
        return markdown.slice(match[0].length);
      }
      frontMatter = {};
      return markdown;
    },
    postprocess(html) {
      // Add front matter as meta tags
      const metaTags = Object.entries(frontMatter)
        .map(([key, value]) => `<meta name="${key}" content="${escapeHtml(value)}">`)
        .join('\n');

      return metaTags ? metaTags + '\n' + html : html;
    }
  }
});

function escapeHtml(text) {
  return String(text)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

Include External Files

import { marked } from "marked";
import fs from "fs";
import path from "path";

marked.use({
  hooks: {
    preprocess(markdown) {
      // Replace @include directives
      return markdown.replace(/@include\(([^)]+)\)/g, (match, filename) => {
        try {
          const fullPath = path.resolve(filename);
          return fs.readFileSync(fullPath, 'utf-8');
        } catch (err) {
          console.error(`Error including ${filename}:`, err);
          return `<!-- Error: Could not include ${filename} -->`;
        }
      });
    }
  }
});

// Usage: @include(./snippet.md)

Auto-Generate Table of Contents

import { marked } from "marked";

const toc = [];

marked.use({
  hooks: {
    processAllTokens(tokens) {
      // Extract headings for TOC
      toc.length = 0; // Clear previous TOC

      function extractHeadings(tokens) {
        tokens.forEach(token => {
          if (token.type === 'heading' && token.depth <= 3) {
            toc.push({
              level: token.depth,
              text: token.text,
              id: token.text.toLowerCase().replace(/[^\w]+/g, '-')
            });
          }
          if (token.tokens) {
            extractHeadings(token.tokens);
          }
        });
      }

      extractHeadings(tokens);
      return tokens;
    },
    postprocess(html) {
      // Prepend TOC to HTML
      if (toc.length === 0) {
        return html;
      }
      
      const tocHtml = '<nav class="toc">\n' +
        toc.map(item =>
          `  <a href="#${item.id}" class="toc-level-${item.level}">${item.text}</a>`
        ).join('\n') +
        '\n</nav>\n\n';

      return tocHtml + html;
    }
  }
});

Template Variable Substitution

import { marked } from "marked";

const variables = {
  author: 'John Doe',
  date: '2024-01-01',
  version: '1.0.0'
};

marked.use({
  hooks: {
    preprocess(markdown) {
      // Replace {{variable}} with values
      return markdown.replace(/\{\{(\w+)\}\}/g, (match, key) => {
        if (key in variables) {
          return variables[key];
        }
        console.warn(`Unknown variable: ${key}`);
        return match; // Leave unchanged if not found
      });
    }
  }
});

const html = marked.parse('Author: {{author}}, Version: {{version}}');

Syntax Validation

import { marked } from "marked";

const errors = [];

marked.use({
  hooks: {
    processAllTokens(tokens) {
      errors.length = 0; // Clear previous errors

      function validate(tokens) {
        tokens.forEach(token => {
          // Validate links have href
          if (token.type === 'link' && !token.href) {
            errors.push(`Invalid link at: ${token.raw}`);
          }

          // Validate images have alt text
          if (token.type === 'image' && !token.text) {
            errors.push(`Image missing alt text: ${token.href}`);
          }
          
          // Validate code blocks have language
          if (token.type === 'code' && token.codeBlockStyle !== 'indented' && !token.lang) {
            errors.push(`Code block missing language: ${token.raw.substring(0, 50)}`);
          }

          if (token.tokens) {
            validate(token.tokens);
          }
        });
      }

      validate(tokens);

      if (errors.length > 0) {
        console.warn('Markdown validation errors:', errors);
      }

      return tokens;
    }
  }
});

Content Security

import { marked } from "marked";

marked.use({
  hooks: {
    processAllTokens(tokens) {
      // Remove or sanitize dangerous tokens
      function sanitize(tokens) {
        return tokens.filter(token => {
          // Remove raw HTML tokens
          if (token.type === 'html') {
            console.warn('Removed HTML token:', token.raw);
            return false;
          }

          // Sanitize link hrefs
          if (token.type === 'link') {
            const url = token.href.toLowerCase();
            if (url.startsWith('javascript:') || url.startsWith('data:')) {
              console.warn('Neutralized dangerous link:', token.href);
              token.href = '#'; // Neutralize dangerous links
            }
          }

          // Recursively sanitize nested tokens
          if (token.tokens) {
            token.tokens = sanitize(token.tokens);
          }

          return true;
        });
      }

      // Modify tokens in place
      const sanitized = sanitize(tokens);
      tokens.length = 0;
      tokens.push(...sanitized);
      
      return tokens;
    }
  }
});

Markdown Statistics

import { marked } from "marked";

let stats = {};

marked.use({
  hooks: {
    processAllTokens(tokens) {
      stats = {
        headings: 0,
        paragraphs: 0,
        codeBlocks: 0,
        links: 0,
        images: 0,
        lists: 0,
        tables: 0
      };

      function countTokens(tokens) {
        tokens.forEach(token => {
          switch (token.type) {
            case 'heading': stats.headings++; break;
            case 'paragraph': stats.paragraphs++; break;
            case 'code': stats.codeBlocks++; break;
            case 'link': stats.links++; break;
            case 'image': stats.images++; break;
            case 'list': stats.lists++; break;
            case 'table': stats.tables++; break;
          }
          
          if (token.tokens) {
            countTokens(token.tokens);
          }
        });
      }

      countTokens(tokens);
      return tokens;
    }
  }
});

const html = marked.parse(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) {
            // Count code words at half speed
            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}`;
    }
  }
});

Hook Context

Hooks have access to this.options and this.block:

import { marked } from "marked";

marked.use({
  hooks: {
    preprocess(markdown) {
      // Access options
      const isGfm = this.options.gfm;
      const isAsync = this.options.async;

      // Access context
      const isBlockLevel = this.block;

      console.log(`Processing: GFM=${isGfm}, Async=${isAsync}, Block=${isBlockLevel}`);

      if (isGfm) {
        // GFM-specific preprocessing
        return markdown.replace(/~~(\w+)~~/g, '<del>$1</del>');
      }

      return markdown;
    }
  }
});

Available Context:

  • this.options: Current marked options
  • this.block: Boolean indicating block vs inline context

Performance Considerations

  • Hooks are called for every parse operation
  • Keep hook logic efficient
  • Avoid expensive operations in hooks
  • Consider caching when possible
  • Profile hooks if experiencing performance issues
import { marked } from "marked";

const cache = new Map();

marked.use({
  hooks: {
    preprocess(markdown) {
      // Cache expensive preprocessing
      if (cache.has(markdown)) {
        return cache.get(markdown);
      }

      const result = expensiveTransform(markdown);
      
      // Implement cache size limit
      if (cache.size > 1000) {
        const firstKey = cache.keys().next().value;
        cache.delete(firstKey);
      }
      
      cache.set(markdown, result);
      return result;
    }
  }
});

function expensiveTransform(markdown) {
  // Simulate expensive operation
  return markdown.replace(/complex-pattern/g, 'replacement');
}

Debugging Hooks

import { marked } from "marked";

marked.use({
  hooks: {
    preprocess(markdown) {
      console.log('Preprocess input length:', markdown.length);
      console.log('First 100 chars:', markdown.substring(0, 100));
      const result = processMarkdown(markdown);
      console.log('Preprocess output length:', result.length);
      return result;
    },
    postprocess(html) {
      console.log('Postprocess input length:', html.length);
      console.log('First 100 chars:', html.substring(0, 100));
      const result = processHtml(html);
      console.log('Postprocess output length:', result.length);
      return result;
    },
    processAllTokens(tokens) {
      console.log('Token count:', tokens.length);
      console.log('Token types:', tokens.map(t => t.type));
      return tokens;
    }
  }
});

function processMarkdown(md) { return md; }
function processHtml(html) { return html; }

Error Handling

import { marked } from "marked";

marked.use({
  hooks: {
    preprocess(markdown) {
      try {
        return dangerousTransform(markdown);
      } catch (err) {
        console.error('Preprocess error:', err);
        // Return original on error
        return markdown;
      }
    },
    postprocess(html) {
      try {
        return transformHtml(html);
      } catch (err) {
        console.error('Postprocess error:', err);
        // Return original on error
        return html;
      }
    },
    processAllTokens(tokens) {
      try {
        return transformTokens(tokens);
      } catch (err) {
        console.error('Token processing error:', err);
        // Return original on error
        return tokens;
      }
    }
  }
});

function dangerousTransform(md) { return md; }
function transformHtml(html) { return html; }
function transformTokens(tokens) { return tokens; }