# 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

```typescript { .api }
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:

```javascript { .api }
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:

```javascript { .api }
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:

```javascript { .api }
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:

```javascript { .api }
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:

```javascript { .api }
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:

```javascript { .api }
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:

```javascript { .api }
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:

```javascript { .api }
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

```javascript { .api }
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

```javascript { .api }
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

```javascript { .api }
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

```javascript { .api }
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

```javascript { .api }
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

```javascript { .api }
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

```javascript { .api }
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

```javascript { .api }
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`:

```javascript { .api }
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

```javascript { .api }
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

```javascript { .api }
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

```javascript { .api }
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; }
```
