# Extension System

Marked's extension system allows you to customize parsing and rendering behavior by adding custom tokenizers, renderers, and processing hooks.

## Quick Reference

| Extension Type | Purpose | Level | Returns |
|----------------|---------|-------|---------|
| Tokenizer Extension | Add new syntax | `'block'` or `'inline'` | Token object or `undefined` |
| Renderer Extension | Custom HTML output | - | HTML string or `false` |
| Tokenizer Override | Modify built-in syntax | - | Token object or `false` |
| Renderer Override | Modify built-in output | - | HTML string or `false` |
| Hooks | Intercept processing | - | Processed content |
| walkTokens | Process all tokens | - | `void` or `Promise<void>` |

## Using Extensions

```typescript { .api }
/**
 * Apply one or more extensions to marked
 * Extensions are applied in order and later extensions can override earlier ones
 * @param extensions - Extension objects to apply
 * @returns The marked function for chaining
 */
function use(...extensions: MarkedExtension[]): typeof marked;
```

Extensions are applied in the order they are passed and can be chained:

```javascript { .api }
import { marked } from "marked";

marked.use(extension1)
  .use(extension2, extension3)
  .use(extension4);
```

**Important**: Extensions are additive - calling `use()` multiple times adds to existing extensions rather than replacing them.

### Extension Application Order

Extensions and options are applied in this order (later overrides earlier):

1. Built-in defaults
2. Global `setOptions()`
3. Extensions via `use()` (in order called)
4. Parse-time options

```javascript { .api }
import { marked } from "marked";

// 1. Built-in defaults
// gfm: true, breaks: false

// 2. Global options
marked.setOptions({ breaks: false, gfm: true });

// 3. Extensions (override global options)
marked.use({ breaks: true });

// 4. Parse-time options (override everything)
const html = marked.parse(markdown, { breaks: false });
```

## Extension Types

### Tokenizer Extensions

Add new syntax patterns that marked should recognize and convert into custom tokens.

```typescript { .api }
interface TokenizerExtension {
  /**
   * Name of the extension (becomes token type)
   * Must be unique - will override existing token types with same name
   */
  name: string;

  /**
   * Whether this is a block-level or inline-level tokenizer
   * block: for elements like paragraphs, headings, lists
   * inline: for elements within text like emphasis, links
   */
  level: 'block' | 'inline';

  /**
   * Optional function to quickly find where this token might start
   * Performance optimization - return the index to begin trying tokenization
   * @param src - Remaining source text
   * @returns Index where token might start, or void if not found
   */
  start?: (src: string) => number | void;

  /**
   * Function to tokenize the custom syntax
   * Called when src is at a position where this token might begin
   * @param src - Source text starting at potential token
   * @param tokens - Current token array (for context, usually not modified)
   * @returns Custom token object or undefined if no match
   *          Token must include: type, raw, and any custom properties
   */
  tokenizer: (src: string, tokens: Token[] | TokensList) => Tokens.Generic | undefined;

  /**
   * Optional array of property names containing child tokens
   * Used by walkTokens to traverse the token tree
   * Example: ['tokens'] if your token has a 'tokens' property with nested tokens
   */
  childTokens?: string[];
}
```

**Example - Custom Emoji Syntax**:

```javascript { .api }
import { marked } from "marked";

marked.use({
  extensions: [{
    name: 'emoji',
    level: 'inline',
    start(src) {
      // Performance optimization: quickly find potential start
      return src.indexOf(':');
    },
    tokenizer(src, tokens) {
      // Match :emoji_name: pattern
      const rule = /^:([a-z_]+):/;
      const match = rule.exec(src);
      if (match) {
        return {
          type: 'emoji',
          raw: match[0],        // Required: original matched text
          name: match[1]        // Custom property
        };
      }
      // Return undefined if no match
    },
    renderer(token) {
      return `<span class="emoji emoji-${token.name}" role="img" aria-label="${token.name}"></span>`;
    }
  }]
});

const html = marked.parse('Hello :wave: world!');
// Output: <p>Hello <span class="emoji emoji-wave" role="img" aria-label="wave"></span> world!</p>
```

**Tokenizer Best Practices**:
- Always return `undefined` (not `null` or `false`) when no match
- Always include `raw` property in returned token
- Use `start()` function for performance with large documents
- Match from the beginning of `src` (use `^` anchor in regex)
- Consider nested content - use `this.lexer.inlineTokens()` or `this.lexer.blockTokens()`

**Common Tokenizer Mistakes**:

```javascript { .api }
// ❌ BAD: Returning null/false instead of undefined
tokenizer(src) {
  const match = src.match(/^pattern/);
  if (!match) {
    return false;  // WRONG! Should be undefined
  }
  return { type: 'custom', raw: match[0] };
}

// ✓ GOOD: Returning undefined
tokenizer(src) {
  const match = src.match(/^pattern/);
  if (!match) {
    return undefined;  // or just return;
  }
  return { type: 'custom', raw: match[0] };
}

// ❌ BAD: Missing raw property
tokenizer(src) {
  const match = src.match(/^pattern/);
  if (match) {
    return { type: 'custom' };  // Missing raw!
  }
}

// ✓ GOOD: Including raw property
tokenizer(src) {
  const match = src.match(/^pattern/);
  if (match) {
    return { type: 'custom', raw: match[0] };  // Required
  }
}

// ❌ BAD: Not matching from start of src
tokenizer(src) {
  const match = src.match(/pattern/);  // Missing ^ anchor
  // This can match anywhere in src, not just at start
}

// ✓ GOOD: Matching from start
tokenizer(src) {
  const match = src.match(/^pattern/);  // Matches at start only
}
```

### Nested Content in Extensions

```javascript { .api }
import { marked } from "marked";

// Example: Custom alert boxes with nested markdown
marked.use({
  extensions: [{
    name: 'alert',
    level: 'block',
    start(src) {
      return src.match(/^:::/)?.index;
    },
    tokenizer(src) {
      // Match :::type\ncontent\n::: syntax
      const rule = /^:::(\w+)\n([\s\S]*?)\n:::/;
      const match = rule.exec(src);
      if (match) {
        return {
          type: 'alert',
          raw: match[0],
          alertType: match[1],
          text: match[2],
          // Recursively tokenize nested content
          tokens: this.lexer.blockTokens(match[2])
        };
      }
    },
    renderer(token) {
      // Parse nested tokens
      const body = this.parser.parse(token.tokens);
      return `<div class="alert alert-${token.alertType}" role="alert">${body}</div>\n`;
    }
  }],
  // Tell walkTokens about nested tokens
  childTokens: ['tokens']
});

// Usage:
// :::warning
// This is a **warning** with *markdown*.
// :::
```

### Renderer Extensions

Add custom rendering for tokenizer extensions or override built-in token rendering.

```typescript { .api }
interface RendererExtension {
  /**
   * Name matching the token type to render
   * Must match a token type (built-in or from tokenizer extension)
   */
  name: string;

  /**
   * Function to render the token to output
   * @param token - The token to render
   * @returns Rendered output string, or false to use default renderer
   *          Returning false allows fallback to next renderer or default
   */
  renderer: (token: Tokens.Generic) => string | false;
}
```

**Example - Custom Blockquote Renderer**:

```javascript { .api }
import { marked } from "marked";

marked.use({
  extensions: [{
    name: 'blockquote',
    renderer(token) {
      // Access parser to render nested tokens
      const body = this.parser.parse(token.tokens);
      
      // Detect blockquote type from first line
      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>');
      } else if (firstLine.startsWith('Tip:')) {
        type = 'tip';
        content = body.replace(/<p>Tip:\s*/, '<p>');
      }
      
      return `<blockquote class="callout callout-${type}">${content}</blockquote>\n`;
    }
  }]
});

// Usage:
// > Note: This is an informational note.
// > Warning: Be careful with this.
```

**Renderer Context**:
- `this.parser`: Access to parser instance
- `this.parser.parse(tokens)`: Render block tokens
- `this.parser.parseInline(tokens)`: Render inline tokens
- Return `false` to fall back to default or next renderer

**Renderer with Error Handling**:

```javascript { .api }
import { marked } from "marked";

marked.use({
  renderer: {
    code({ text, lang, escaped }) {
      try {
        // Attempt custom highlighting
        if (lang === 'mermaid') {
          return `<div class="mermaid">${text}</div>\n`;
        }
        
        // Attempt syntax highlighting
        if (lang && highlighter.getLanguage(lang)) {
          const highlighted = highlighter.highlight(text, { language: lang }).value;
          return `<pre><code class="hljs language-${lang}">${highlighted}</code></pre>\n`;
        }
      } catch (err) {
        // Log error but don't break rendering
        console.error(`Code rendering error for language ${lang}:`, err);
      }
      
      // Fallback to default
      return false;
    }
  }
});
```

### Combined Tokenizer and Renderer Extensions

A single extension can provide both tokenizer and renderer:

```typescript { .api }
type TokenizerAndRendererExtension =
  | TokenizerExtension
  | RendererExtension
  | (TokenizerExtension & RendererExtension);
```

**Example - YouTube Embed**:

```javascript { .api }
import { marked } from "marked";

marked.use({
  extensions: [{
    name: 'youtube',
    level: 'block',
    start(src) {
      return src.match(/^@\[youtube\]/)?.index;
    },
    tokenizer(src) {
      // Match @[youtube](videoId) syntax
      const rule = /^@\[youtube\]\(([\w-]+)\)(\s*{([^}]+)})?\n?/;
      const match = rule.exec(src);
      if (match) {
        // Parse optional attributes
        const attrs = match[3] ? match[3].split(/\s+/) : [];
        const width = attrs.find(a => a.startsWith('width='))?.split('=')[1] || '560';
        const height = attrs.find(a => a.startsWith('height='))?.split('=')[1] || '315';
        
        return {
          type: 'youtube',
          raw: match[0],
          videoId: match[1],
          width,
          height
        };
      }
    },
    renderer(token) {
      return `<div class="video-wrapper">
  <iframe 
    width="${token.width}" 
    height="${token.height}" 
    src="https://www.youtube.com/embed/${token.videoId}" 
    frameborder="0" 
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" 
    allowfullscreen
    loading="lazy">
  </iframe>
</div>\n`;
    }
  }]
});

// Usage:
// @[youtube](dQw4w9WgXcQ)
// @[youtube](dQw4w9WgXcQ) {width=800 height=600}
```

## Renderer Overrides

Override built-in renderer methods without creating custom tokens.

```javascript { .api }
import { marked } from "marked";

marked.use({
  renderer: {
    // Override heading rendering with IDs and anchor links
    heading({ tokens, depth }) {
      const text = this.parser.parseInline(tokens);
      const id = text
        .toLowerCase()
        .replace(/[^\w\s-]/g, '')
        .replace(/\s+/g, '-');
      
      return `<h${depth} id="${id}">
  <a href="#${id}" class="anchor" aria-hidden="true">
    <span class="icon icon-link"></span>
  </a>
  ${text}
</h${depth}>\n`;
    },

    // Override link rendering for external links
    link({ href, title, tokens }) {
      const text = this.parser.parseInline(tokens);
      const titleAttr = title ? ` title="${escapeHtml(title)}"` : '';
      
      // Add target="_blank" for external links
      const isExternal = href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//');
      const external = isExternal ? ' target="_blank" rel="noopener noreferrer"' : '';
      
      // Add icon for external links
      const icon = isExternal ? ' <span class="external-link-icon" aria-hidden="true">↗</span>' : '';
      
      return `<a href="${escapeHtml(href)}"${titleAttr}${external}>${text}${icon}</a>`;
    },

    // Override code rendering with language badge
    code({ text, lang, escaped }) {
      const code = escaped ? text : escapeHtml(text);
      const language = lang || 'text';
      const langBadge = lang ? `<div class="code-lang">${escapeHtml(lang)}</div>` : '';
      
      return `<div class="code-block">
  ${langBadge}
  <pre><code class="language-${escapeHtml(language)}">${code}</code></pre>
</div>\n`;
    },
    
    // Override image rendering with lazy loading and figure wrapper
    image({ href, title, text }) {
      const alt = text ? ` alt="${escapeHtml(text)}"` : '';
      const titleAttr = title ? ` title="${escapeHtml(title)}"` : '';
      
      if (title) {
        // Wrap in figure with caption
        return `<figure class="image-figure">
  <img src="${escapeHtml(href)}"${alt}${titleAttr} loading="lazy">
  <figcaption>${escapeHtml(title)}</figcaption>
</figure>`;
      }
      
      return `<img src="${escapeHtml(href)}"${alt} loading="lazy">`;
    },

    // Returning false falls back to default renderer
    table(token) {
      // Only apply custom rendering to small tables
      if (token.header.length > 10) {
        return false; // Use default for large tables
      }
      
      // Custom rendering for small tables
      // ... custom logic ...
      return false; // Or return custom HTML
    }
  }
});

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

**Renderer Override Notes**:
- Methods receive token objects with all properties
- Must call `this.parser.parseInline()` or `this.parser.parse()` for nested tokens
- Return `false` to use default renderer
- Can access `this.options` for current configuration

## Tokenizer Overrides

Override built-in tokenizer methods to customize recognition of standard markdown elements.

```javascript { .api }
import { marked } from "marked";

marked.use({
  tokenizer: {
    // Override code block tokenization to support custom attributes
    fences(src) {
      // Match ```lang [filename] {attrs}
      const match = src.match(/^```(\w+)?(?:\s+\[([^\]]+)\])?(?:\s+\{([^}]+)\})?\n([\s\S]*?)\n```/);
      if (match) {
        return {
          type: 'code',
          raw: match[0],
          lang: match[1] || '',
          filename: match[2] || null,      // Custom property
          attributes: match[3] || null,    // Custom property
          text: match[4]
        };
      }
      return false; // Use default if no match
    },

    // Override heading tokenization to add custom metadata
    heading(src) {
      // Match heading with optional {#id .class} syntax
      const match = src.match(/^(#{1,6})\s+(.+?)(?:\s+\{([^}]+)\})?\s*$/);
      if (match) {
        const attrs = match[3];
        let customId = null;
        let customClasses = [];
        
        if (attrs) {
          // Parse {#id .class1 .class2}
          customId = attrs.match(/#([\w-]+)/)?.[1];
          customClasses = [...attrs.matchAll(/\.([\w-]+)/g)].map(m => m[1]);
        }
        
        return {
          type: 'heading',
          raw: match[0],
          depth: match[1].length,
          text: match[2],
          tokens: this.lexer.inline(match[2]),
          customId,           // Custom property
          customClasses       // Custom property
        };
      }
      return false;
    },
    
    // Override link tokenization to detect anchor links
    link(src) {
      // Let default tokenizer handle most of the work
      // We'll just add a flag for anchor links
      const match = src.match(/^!?\[([^\]]*)\]\(([^)]+)\)/);
      if (match && match[2].startsWith('#')) {
        return {
          type: 'link',
          raw: match[0],
          href: match[2],
          title: null,
          text: match[1],
          tokens: this.lexer.inlineTokens(match[1]),
          isAnchor: true      // Custom property
        };
      }
      return false; // Use default tokenizer
    }
  }
});

// Add corresponding renderer
marked.use({
  renderer: {
    code({ text, lang, filename, attributes }) {
      const code = escapeHtml(text);
      const langClass = lang ? ` class="language-${lang}"` : '';
      const fileLabel = filename ? `<div class="code-filename">${escapeHtml(filename)}</div>` : '';
      
      return `<div class="code-block" ${attributes || ''}>
  ${fileLabel}
  <pre><code${langClass}>${code}</code></pre>
</div>\n`;
    },
    
    heading({ tokens, depth, customId, customClasses }) {
      const text = this.parser.parseInline(tokens);
      const id = customId || text.toLowerCase().replace(/[^\w]+/g, '-');
      const classes = customClasses?.length ? ` class="${customClasses.join(' ')}"` : '';
      
      return `<h${depth} id="${id}"${classes}>${text}</h${depth}>\n`;
    },
    
    link({ href, title, tokens, isAnchor }) {
      const text = this.parser.parseInline(tokens);
      const titleAttr = title ? ` title="${escapeHtml(title)}"` : '';
      const anchorClass = isAnchor ? ' class="anchor-link"' : '';
      
      return `<a href="${escapeHtml(href)}"${titleAttr}${anchorClass}>${text}</a>`;
    }
  }
});

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

// Usage:
// ```javascript [example.js] {data-line="1-5"}
// code here
// ```
//
// # Heading {#custom-id .special}
//
// [Jump to section](#custom-id)
```

**Tokenizer Override Notes**:
- Return token object if matched, `false` or `undefined` if not
- Access `this.lexer` for recursive tokenization
- Access `this.options` for configuration
- Tokenizer context provides `this.lexer.inline()` and `this.lexer.blockTokens()`

## Token Walking

Process all tokens before they are rendered, useful for global modifications or validations.

```typescript { .api }
/**
 * Walk through all tokens including nested tokens
 * Executes callback for every token in the tree
 * @param tokens - Token array or TokensList
 * @param callback - Function called for each token (can modify token in place)
 * @returns Array of callback return values
 */
function walkTokens(
  tokens: Token[] | TokensList,
  callback: (token: Token) => void | Promise<void> | (void | Promise<void>)[]
): (void | Promise<void>)[];
```

**Example - Add IDs to Headings**:

```javascript { .api }
import { marked } from "marked";

const headingIds = new Set();

marked.use({
  walkTokens(token) {
    if (token.type === 'heading') {
      // Generate unique ID from text
      let id = token.text
        .toLowerCase()
        .replace(/[^\w\s-]/g, '')
        .replace(/\s+/g, '-');
      
      // Ensure uniqueness
      let counter = 1;
      let uniqueId = id;
      while (headingIds.has(uniqueId)) {
        uniqueId = `${id}-${counter++}`;
      }
      headingIds.add(uniqueId);
      
      token.id = uniqueId; // Add custom property
    }
  },
  renderer: {
    heading({ tokens, depth, id }) {
      const text = this.parser.parseInline(tokens);
      const idAttr = id ? ` id="${id}"` : '';
      return `<h${depth}${idAttr}>${text}</h${depth}>\n`;
    }
  }
});

// Clear IDs before each parse (if reusing marked instance)
headingIds.clear();
const html = marked.parse(markdown);
```

**Example - Async Token Processing**:

```javascript { .api }
import { marked } from "marked";

// Cache for link validation
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) {
    console.warn(`Failed to validate link: ${url}`, err.message);
    linkValidation.set(url, false);
    return false;
  }
}

marked.use({
  async: true,
  async walkTokens(token) {
    if (token.type === 'link') {
      // Async operation: validate link
      const isValid = await validateUrl(token.href);
      token.validated = isValid;
      
      if (!isValid) {
        console.warn(`Invalid link detected: ${token.href}`);
      }
    }
  },
  renderer: {
    link(token) {
      const text = this.parser.parseInline(token.tokens);
      const titleAttr = token.title ? ` title="${escapeHtml(token.title)}"` : '';
      
      if (token.validated === false) {
        return `<span class="broken-link" title="Link validation failed">${text}</span>`;
      }
      
      return `<a href="${escapeHtml(token.href)}"${titleAttr}>${text}</a>`;
    }
  }
});

// Must use await with async mode
const html = await marked.parse('[Link](https://example.com)', { async: true });

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

**WalkTokens Best Practices**:
- Modify tokens in place (direct property assignment)
- Return value is ignored (typically void)
- For async operations, set `async: true` and use `async/await`
- Called before rendering, so modifications affect output
- Traverses entire tree including deeply nested tokens

## Hook Extensions

Hooks intercept and modify content at key points in the processing pipeline.

```javascript { .api }
import { marked } from "marked";

marked.use({
  hooks: {
    // Process markdown before parsing
    preprocess(markdown) {
      // Replace custom shortcodes
      return markdown
        .replace(/\{DATE\}/g, new Date().toLocaleDateString())
        .replace(/\{VERSION\}/g, process.env.npm_package_version || '1.0.0')
        .replace(/\{YEAR\}/g, new Date().getFullYear().toString())
        .replace(/-->/g, '→')
        .replace(/<--/g, '←');
    },

    // Process HTML after parsing
    postprocess(html) {
      // Add wrapper or modify output
      return html
        .replace(/<h1>/g, '<h1 class="title">')
        .replace(/<table>/g, '<div class="table-wrapper"><table class="table">')
        .replace(/<\/table>/g, '</table></div>')
        .replace(/<img /g, '<img loading="lazy" ');
    },

    // Process all tokens after lexing but before walkTokens
    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);
          }
          
          // Handle list items
          if (token.type === 'list' && token.items) {
            token.items.forEach(item => {
              if (item.tokens) {
                addMetadata(item.tokens);
              }
            });
          }
        });
      }
      
      addMetadata(tokens);
      return tokens;
    }
  }
});

let idCounter = 0;
function generateUniqueId() {
  return `token-${Date.now()}-${idCounter++}`;
}
```

[Full Hooks Documentation](./hooks.md)

## Extension Context

Within tokenizer and renderer functions, `this` provides context:

### Tokenizer Context

```typescript { .api }
interface TokenizerThis {
  /**
   * Reference to the lexer instance
   * Use for recursive tokenization of nested content
   */
  lexer: Lexer;
  
  /**
   * Tokenizer options
   */
  options: MarkedOptions;
}
```

**Usage**:

```javascript { .api }
tokenizer(src, tokens) {
  const match = src.match(/^custom syntax/);
  if (match) {
    // Access lexer for nested tokenization
    const inlineTokens = this.lexer.inlineTokens('some text');
    
    // Access options
    if (this.options.gfm) {
      // GFM-specific handling
    }

    return {
      type: 'custom',
      raw: match[0],
      tokens: inlineTokens
    };
  }
}
```

### Renderer Context

```typescript { .api }
interface RendererThis {
  /**
   * Reference to the parser instance
   * Use for rendering nested tokens
   */
  parser: Parser;
  
  /**
   * Renderer options
   */
  options: MarkedOptions;
}
```

**Usage**:

```javascript { .api }
renderer(token) {
  // Parse nested block tokens
  const html = this.parser.parse(token.tokens);

  // Parse inline tokens
  const inline = this.parser.parseInline(token.tokens);
  
  // Access options
  if (this.options.breaks) {
    // Handle line breaks differently
  }

  return `<div>${html}</div>`;
}
```

## Extension Stacking

Multiple extensions can handle the same token type. They are applied in order, and if one returns `false`, the next is tried:

```javascript { .api }
import { marked } from "marked";

// First extension - handles mailto links
marked.use({
  renderer: {
    link(token) {
      if (token.href.startsWith('mailto:')) {
        const email = token.href.slice(7);
        const text = this.parser.parseInline(token.tokens);
        return `<a href="mailto:${escapeHtml(email)}" class="email-link">📧 ${text}</a>`;
      }
      return false; // Pass to next renderer
    }
  }
});

// Second extension - handles tel links
marked.use({
  renderer: {
    link(token) {
      if (token.href.startsWith('tel:')) {
        const phone = token.href.slice(4);
        const text = this.parser.parseInline(token.tokens);
        return `<a href="tel:${escapeHtml(phone)}" class="phone-link">📞 ${text}</a>`;
      }
      return false; // Pass to next renderer
    }
  }
});

// Third extension - handles external links
marked.use({
  renderer: {
    link(token) {
      const isExternal = token.href.startsWith('http://') || token.href.startsWith('https://');
      if (isExternal) {
        const text = this.parser.parseInline(token.tokens);
        const titleAttr = token.title ? ` title="${escapeHtml(token.title)}"` : '';
        return `<a href="${escapeHtml(token.href)}"${titleAttr} target="_blank" rel="noopener noreferrer">${text} ↗</a>`;
      }
      return false; // Pass to default renderer
    }
  }
});

// Execution order: email check → phone check → external check → default renderer

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

**Stacking Rules**:
- Extensions are tried in registration order
- Returning `false` passes to next extension
- Last resort is built-in default renderer
- First non-false return value is used

## Real-World Examples

### Add Table of Contents

```javascript { .api }
import { marked } from "marked";

const toc = [];

marked.use({
  walkTokens(token) {
    if (token.type === 'heading' && token.depth <= 3) {
      const id = token.text
        .toLowerCase()
        .replace(/[^\w\s-]/g, '')
        .replace(/\s+/g, '-');
      
      toc.push({
        level: token.depth,
        text: token.text,
        id: 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);

// Generate TOC
const tocHtml = '<nav class="toc" role="navigation" aria-label="Table of Contents">\n' +
  '  <h2>Contents</h2>\n' +
  '  <ul>\n' +
  toc.map(item => {
    const indent = '    '.repeat(item.level - 1);
    return `${indent}<li><a href="#${item.id}">${escapeHtml(item.text)}</a></li>`;
  }).join('\n') +
  '\n  </ul>\n</nav>';

// Combine TOC with content
const fullHtml = tocHtml + '\n' + html;

// Clear TOC for next document
toc.length = 0;

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

### Syntax Highlighting

```javascript { .api }
import { marked } from "marked";
import hljs from "highlight.js";

marked.use({
  renderer: {
    code({ text, lang, escaped }) {
      // Validate language before highlighting
      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) {
          // Fall through to default on error
          console.error('Highlight error:', err);
        }
      }
      
      // Default rendering
      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;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}
```

### Custom Alert Blocks

```javascript { .api }
import { marked } from "marked";

marked.use({
  extensions: [{
    name: 'alert',
    level: 'block',
    start(src) {
      return src.match(/^:::/)?.index;
    },
    tokenizer(src) {
      // Match :::type\ncontent\n::: syntax
      const rule = /^:::(\w+)\n([\s\S]*?)\n:::/;
      const match = rule.exec(src);
      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);
      const icon = getAlertIcon(token.alertType);
      return `<div class="alert alert-${token.alertType}" role="alert">
  <div class="alert-icon" aria-hidden="true">${icon}</div>
  <div class="alert-content">${body}</div>
</div>\n`;
    }
  }],
  childTokens: ['tokens']
});

function getAlertIcon(type) {
  const icons = {
    note: 'ℹ️',
    warning: '⚠️',
    danger: '🚫',
    tip: '💡',
    success: '✅'
  };
  return icons[type] || 'ℹ️';
}

// Usage:
// :::warning
// This is a warning message with **markdown**.
// :::
```

### Math Expression Support

```javascript { .api }
import { marked } from "marked";

marked.use({
  extensions: [
    // Inline math: $x^2$
    {
      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>`;
      }
    },
    // Block math: $$\n...\n$$
    {
      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;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

// Then include MathJax or KaTeX to render the LaTeX
```

### Wiki-Style Links

```javascript { .api }
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: page,
          display: display,
          slug: slug
        };
      }
    },
    renderer(token) {
      return `<a href="/wiki/${encodeURIComponent(token.slug)}" class="wikilink" data-page="${escapeHtml(token.page)}">${escapeHtml(token.display)}</a>`;
    }
  }]
});

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

// Usage: 
// [[Page Name]]          -> links to "page-name" with display "Page Name"
// [[Page Name|Display]]  -> links to "page-name" with display "Display"
```

### Footnote Support

```javascript { .api }
import { marked } from "marked";

const footnotes = {};
let footnoteCounter = 0;

marked.use({
  extensions: [
    // Footnote reference: [^1]
    {
      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 class="footnote-ref"><a href="#fn-${token.id}" id="fnref-${token.id}" role="doc-noteref">${num}</a></sup>`;
      }
    },
    // Footnote definition: [^1]: Text
    {
      name: 'footnoteDef',
      level: 'block',
      start(src) {
        return src.match(/^\[\^/)?.index;
      },
      tokenizer(src) {
        const match = src.match(/^\[\^(\w+)\]:\s+(.+?)(?:\n(?!\s)|$)/s);
        if (match) {
          return {
            type: 'footnoteDef',
            raw: match[0],
            id: match[1],
            text: match[2].trim()
          };
        }
      },
      renderer(token) {
        if (!footnotes[token.id]) {
          footnotes[token.id] = { num: ++footnoteCounter };
        }
        footnotes[token.id].text = token.text;
        return ''; // Definitions don't render inline
      }
    }
  ],
  hooks: {
    postprocess(html) {
      // Append footnotes section at end
      if (Object.keys(footnotes).length === 0) {
        return html;
      }
      
      let footnotesHtml = '<section class="footnotes" role="doc-endnotes">\n<hr>\n<ol>\n';
      
      // Sort by number
      const sortedFootnotes = Object.entries(footnotes)
        .filter(([, data]) => data.text)
        .sort((a, b) => a[1].num - b[1].num);
      
      for (const [id, data] of sortedFootnotes) {
        footnotesHtml += `<li id="fn-${id}" role="doc-endnote">${escapeHtml(data.text)} <a href="#fnref-${id}" role="doc-backlink">↩</a></li>\n`;
      }
      
      footnotesHtml += '</ol>\n</section>';
      
      return html + footnotesHtml;
    }
  }
});

// Reset footnotes before each parse
function resetFootnotes() {
  Object.keys(footnotes).forEach(key => delete footnotes[key]);
  footnoteCounter = 0;
}

// Usage:
// This is text[^1] with footnotes[^2].
//
// [^1]: First footnote
// [^2]: Second footnote

resetFootnotes();
const html = marked.parse(markdown);

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

## Error Handling in Extensions

```javascript { .api }
import { marked } from "marked";

marked.use({
  renderer: {
    code({ text, lang }) {
      try {
        // Potentially failing operation
        const result = processCode(text, lang);
        return result;
      } catch (err) {
        // Handle error gracefully
        console.error('Code processing error:', err);
        
        // Return safe fallback
        return `<pre><code class="error">Error processing code block: ${escapeHtml(err.message)}</code></pre>\n`;
        
        // Or fall back to default
        // return false;
      }
    }
  },
  walkTokens(token) {
    try {
      // Process token
      if (token.type === 'custom') {
        validateCustomToken(token);
      }
    } catch (err) {
      // Log but don't throw (would stop parsing)
      console.error('Token validation error:', err);
      token.validationError = err.message;
    }
  }
});

function processCode(text, lang) {
  // Some code processing that might throw
  return `<pre><code>${escapeHtml(text)}</code></pre>\n`;
}

function validateCustomToken(token) {
  // Some validation that might throw
}

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

## Performance Optimization

```javascript { .api }
import { marked } from "marked";

// Cache expensive operations
const highlightCache = new Map();

marked.use({
  extensions: [{
    name: 'customCode',
    level: 'block',
    start(src) {
      // Efficient start function
      return src.match(/^```/)?.index;
    },
    tokenizer(src) {
      // Efficient regex
      const match = src.match(/^```(\w+)\n([\s\S]*?)\n```/);
      if (match) {
        return {
          type: 'customCode',
          raw: match[0],
          lang: match[1],
          text: match[2]
        };
      }
    },
    renderer(token) {
      // Cache expensive highlighting
      const cacheKey = `${token.lang}:${token.text}`;
      
      if (highlightCache.has(cacheKey)) {
        return highlightCache.get(cacheKey);
      }
      
      const result = expensiveHighlight(token.text, token.lang);
      
      // Implement cache size limit
      if (highlightCache.size > 1000) {
        const firstKey = highlightCache.keys().next().value;
        highlightCache.delete(firstKey);
      }
      
      highlightCache.set(cacheKey, result);
      return result;
    }
  }]
});

function expensiveHighlight(text, lang) {
  // Simulated expensive operation
  return `<pre><code class="language-${lang}">${escapeHtml(text)}</code></pre>\n`;
}

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

## Testing Extensions

```javascript { .api }
import { marked } from "marked";
import assert from "assert";

// Test custom extension
marked.use({
  extensions: [{
    name: 'mention',
    level: 'inline',
    start(src) {
      return src.indexOf('@');
    },
    tokenizer(src) {
      const match = src.match(/^@(\w+)/);
      if (match) {
        return {
          type: 'mention',
          raw: match[0],
          username: match[1]
        };
      }
    },
    renderer(token) {
      return `<a href="/users/${escapeURI(token.username)}" class="mention">@${escapeHtml(token.username)}</a>`;
    }
  }]
});

// Test cases
describe('Mention Extension', () => {
  it('should parse single mention', () => {
    const html = marked.parse('Hello @john');
    assert.strictEqual(
      html,
      '<p>Hello <a href="/users/john" class="mention">@john</a></p>\n'
    );
  });

  it('should parse multiple mentions', () => {
    const html = marked.parse('@jane and @doe');
    assert(html.includes('@jane'));
    assert(html.includes('@doe'));
  });

  it('should not match invalid mentions', () => {
    const html = marked.parse('Email: test@example.com');
    assert(!html.includes('class="mention"'));
  });
  
  it('should handle mentions with other markdown', () => {
    const html = marked.parse('Hello **@john**');
    assert(html.includes('mention'));
    assert(html.includes('<strong>'));
  });
});

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

function escapeURI(str) {
  return encodeURIComponent(str);
}
```

## Common Extension Pitfalls

### 1. Not Returning Undefined

```javascript { .api }
// ❌ BAD
tokenizer(src) {
  if (!src.match(/pattern/)) {
    return null;  // WRONG!
  }
}

// ✓ GOOD
tokenizer(src) {
  if (!src.match(/pattern/)) {
    return undefined;  // or just return;
  }
}
```

### 2. Missing Raw Property

```javascript { .api }
// ❌ BAD
tokenizer(src) {
  const match = src.match(/^pattern/);
  return { type: 'custom' };  // Missing raw!
}

// ✓ GOOD
tokenizer(src) {
  const match = src.match(/^pattern/);
  return { type: 'custom', raw: match[0] };
}
```

### 3. Not Using Start Function

```javascript { .api }
// ❌ SLOW: Tokenizer called for every character
{
  name: 'custom',
  level: 'inline',
  // No start function
  tokenizer(src) {
    // Called very frequently
  }
}

// ✓ FAST: Start function optimizes
{
  name: 'custom',
  level: 'inline',
  start(src) {
    return src.indexOf('@');  // Quickly skip to potential tokens
  },
  tokenizer(src) {
    // Only called when '@' is found
  }
}
```

### 4. Not Handling Nested Tokens

```javascript { .api }
// ❌ BAD: Not parsing nested markdown
renderer(token) {
  return `<div>${token.text}</div>`;  // Renders raw markdown
}

// ✓ GOOD: Parsing nested tokens
renderer(token) {
  const content = this.parser.parse(token.tokens);
  return `<div>${content}</div>`;
}
```

### 5. Not Escaping HTML

```javascript { .api }
// ❌ BAD: XSS vulnerability
renderer(token) {
  return `<div class="${token.className}">${token.text}</div>`;
}

// ✓ GOOD: Escaping user content
renderer(token) {
  return `<div class="${escapeHtml(token.className)}">${escapeHtml(token.text)}</div>`;
}

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

## Extension Best Practices Summary

1. **Always return `undefined`** from tokenizers when no match (not `null` or `false`)
2. **Always include `raw` property** in tokens
3. **Use `start()` function** for performance optimization
4. **Match from start of source** using `^` regex anchor
5. **Handle nested content** using `this.lexer.inlineTokens()` or `this.lexer.blockTokens()`
6. **Parse nested tokens** in renderers using `this.parser.parse()` or `this.parser.parseInline()`
7. **Escape HTML** to prevent XSS vulnerabilities
8. **Return `false`** from renderers to fall back to default
9. **Handle errors gracefully** - don't break entire parse
10. **Cache expensive operations** in renderers
11. **Test thoroughly** with edge cases
12. **Document custom properties** for maintainability
