# How Markdown Rendering Is Customized Using marked.js in the markdown-here Extension

> Customize Markdown rendering with markedjs by creating a custom Renderer instance. Override methods and pass options to the marked parser for tailored output. Learn how.

- Repository: [Adam Pritchard/markdown-here](https://github.com/adam-p/markdown-here)
- Tags: internals
- Published: 2026-03-05

---

**Markdown rendering is customized using marked.js by creating a custom `Renderer` instance that overrides specific methods such as `heading` and `link`, then passing this renderer along with tailored options to the `marked()` parser function.**

The markdown-here extension demonstrates a clean approach to extending the marked.js library without maintaining a fork. In [`src/common/markdown-render.js`](https://github.com/adam-p/markdown-here/blob/main/src/common/markdown-render.js), the project implements Markdown rendering customized using marked.js by wrapping the core parser with user-configurable preferences for anchors, syntax highlighting, and math support.

## Creating a Custom marked.js Renderer Instance

The customization begins by instantiating a base renderer and preserving references to the default implementations. This allows selective overrides while maintaining fallback behavior for unmodified methods.

According to the source code in [`src/common/markdown-render.js`](https://github.com/adam-p/markdown-here/blob/main/src/common/markdown-render.js) at lines 35-37, the extension initializes a new `marked.Renderer()` object:

```javascript
var markedRenderer = new marked.Renderer();

```

This object exposes all default rendering callbacks—including `heading`, `link`, `code`, and `list`—which can be replaced or wrapped to alter output generation.

### Overriding the Heading Method for Anchor Support

At lines 42-56, the extension overrides the `heading` method to inject anchor tags before header text when the user enables the `header-anchors-enabled` preference. The implementation stores the original renderer and invokes it via `.call()` when the feature is disabled:

```javascript
const defaultHeadingRenderer = markedRenderer.heading;

markedRenderer.heading = function (text, level, raw) {
  if (userprefs['header-anchors-enabled']) {
    const anchorName = text.toLowerCase().replace(/[^\w]+/g, '-');
    const anchor = `<a href="#" name="${anchorName}"></a>`;
    return `<h${level}>${anchor}${text}</h${level}>\n`;
  }
  return defaultHeadingRenderer.call(this, text, level, raw);
};

```

This pattern ensures that standard heading rendering remains intact while adding optional anchor functionality for table-of-contents generation.

### Sanitizing URLs in the Link Method

The link renderer customization at lines 58-73 addresses two specific requirements: ensuring bare URLs receive an `https://` scheme and normalizing fragment-only links (`#...`) to match the anchor sanitization logic used in headings.

```javascript
const defaultLinkRenderer = markedRenderer.link;

markedRenderer.link = function (href, title, text) {
  // Auto-prepend https:// to scheme-less URLs
  href = href.replace(/^(?!#)([^:]+)$/, 'https://$1');
  
  // Sanitize fragment links when anchors are enabled
  if (userprefs['header-anchors-enabled'] && href[0] === '#') {
    href = '#' + href.slice(1).toLowerCase().replace(/[^\w]+/g, '-');
  }
  
  return defaultLinkRenderer.call(this, href, title, text);
};

```

This modification prevents broken links in email clients while maintaining compatibility with the extension's anchor generation system.

## Configuring marked.js Options for Syntax Highlighting

The extension builds a comprehensive options object at lines 75-96 that combines the custom renderer with additional marked.js features and third-party integrations.

**Key configuration parameters include:**

- **`renderer`**: The custom `markedRenderer` instance created above.
- **`gfm`**: Enabled by default for GitHub Flavored Markdown support.
- **`tables`**: Activated to parse GitHub-style tables.
- **`breaks`**: Controlled by the `gfm-line-breaks-enabled` user preference.
- **`langPrefix`**: Set to `'hljs language-'` to ensure CSS classes align with highlight.js expectations.
- **`highlight`**: A callback function that delegates code block syntax highlighting to highlight.js when available.

The highlight integration checks for language support before processing:

```javascript
highlight: function (code, lang) {
  if (lang && hljs.getLanguage(lang.toLowerCase())) {
    return hljs.highlight(code, { 
      language: lang.toLowerCase(), 
      ignoreIllegals: true 
    }).value;
  }
  return code;
}

```

## Assembling the Final marked.js Rendering Pipeline

The `markdownRender` function defined in [`src/common/markdown-render.js`](https://github.com/adam-p/markdown-here/blob/main/src/common/markdown-render.js) orchestrates the complete transformation pipeline. At lines 98-100, it executes the parser with the assembled configuration:

```javascript
function markdownRender(mdText, userprefs, marked, hljs) {
  // ... renderer setup and options construction ...
  
  return marked(mdText, markedOptions);
}

```

This approach keeps the heavy parsing logic in the extension's background script while allowing content scripts to request rendered HTML on demand.

## Complete Implementation Example

The following example demonstrates how markdown-here wires all customization layers together into a standalone rendering function:

```javascript
function renderMarkdown(mdText, userprefs, marked, hljs) {
  // Step 1: Create custom renderer
  const renderer = new marked.Renderer();
  const origHeading = renderer.heading;
  const origLink = renderer.link;
  
  // Step 2: Override heading for anchor support
  renderer.heading = function (text, level, raw) {
    if (userprefs['header-anchors-enabled']) {
      const anchor = text.toLowerCase().replace(/[^\w]+/g, '-');
      return `<h${level}><a href="#" name="${anchor}"></a>${text}</h${level}>\n`;
    }
    return origHeading.call(this, text, level, raw);
  };
  
  // Step 3: Override link for URL sanitization
  renderer.link = function (href, title, text) {
    href = href.replace(/^(?!#)([^:]+)$/, 'https://$1');
    if (userprefs['header-anchors-enabled'] && href[0] === '#') {
      href = '#' + href.slice(1).toLowerCase().replace(/[^\w]+/g, '-');
    }
    return origLink.call(this, href, title, text);
  };
  
  // Step 4: Configure marked.js options
  const options = {
    renderer: renderer,
    gfm: true,
    tables: true,
    smartLists: true,
    breaks: userprefs['gfm-line-breaks-enabled'],
    smartypants: true,
    langPrefix: 'hljs language-',
    math: userprefs['math-enabled'] ? mathify : null,
    highlight: (code, lang) => {
      if (lang && hljs.getLanguage(lang.toLowerCase())) {
        return hljs.highlight(code, { language: lang.toLowerCase() }).value;
      }
      return code;
    }
  };
  
  // Step 5: Execute rendering
  return marked(mdText, options);
}

```

## Summary

- **Custom Renderer Pattern**: markdown-here creates a new `marked.Renderer()` instance and preserves default methods before overriding specific behaviors.
- **Anchor Injection**: The `heading` method is customized at lines 42-56 in [`src/common/markdown-render.js`](https://github.com/adam-p/markdown-here/blob/main/src/common/markdown-render.js) to inject named anchors when `header-anchors-enabled` is active.
- **URL Normalization**: The `link` method override at lines 58-73 ensures bare URLs use HTTPS and fragment links match anchor sanitization rules.
- **Syntax Highlighting Integration**: The `highlight` callback and `langPrefix` option bind marked.js output to highlight.js CSS classes.
- **Preference-Driven Configuration**: User settings like `gfm-line-breaks-enabled` and `math-enabled` flow directly into the `markedOptions` object passed to the parser.

## Frequently Asked Questions

### How does markdown-here override marked.js default behavior without forking the library?

The extension uses JavaScript's prototype-based inheritance to create a custom `Renderer` instance that delegates to original methods via `.call()`. By storing references to default implementations like `defaultHeadingRenderer` before reassignment, the code can invoke original behavior when user preferences disable custom features. This wrapper pattern isolates modifications in [`src/common/markdown-render.js`](https://github.com/adam-p/markdown-here/blob/main/src/common/markdown-render.js) while leaving [`src/common/marked.js`](https://github.com/adam-p/markdown-here/blob/main/src/common/marked.js) untouched for easy updates.

### What marked.js rendering options does markdown-here expose to end users?

The extension surfaces four primary user-controllable options through the `markedOptions` object: **`gfm-line-breaks-enabled`** toggles the `breaks` flag for GitHub-style line breaks, **`header-anchors-enabled`** controls anchor injection in headings, **`math-enabled`** activates mathematical expression processing via the `math` callback, and **`smartypants`** enables typographic punctuation conversion. These preferences are read from browser storage and injected at render time.

### How does the custom renderer integrate syntax highlighting with highlight.js?

The integration occurs through the **`highlight`** callback defined in the options object at lines 75-96. When marked.js encounters a fenced code block with a language specifier, it invokes this callback with the code content and language name. The function checks `hljs.getLanguage()` for support, then returns highlighted markup using highlight.js's API. The **`langPrefix`** option ensures generated `<code>` tags carry the `hljs language-*` classes required for highlight.js CSS styling.

### Where does the actual marked.js parsing occur in the extension architecture?

The core parsing happens in [`src/common/markdown-render.js`](https://github.com/adam-p/markdown-here/blob/main/src/common/markdown-render.js) within the **`markdownRender()`** function at lines 98-100, which executes `marked(mdText, markedOptions)`. This file acts as a thin wrapper around the marked.js library defined in [`src/common/marked.js`](https://github.com/adam-p/markdown-here/blob/main/src/common/marked.js). The rendered HTML is then passed to [`src/common/markdown-here.js`](https://github.com/adam-p/markdown-here/blob/main/src/common/markdown-here.js) for injection into the email composition window, keeping the parsing logic decoupled from the DOM manipulation layer.