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

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, 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 at lines 35-37, the extension initializes a new marked.Renderer() object:

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:

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.

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.

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:

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 orchestrates the complete transformation pipeline. At lines 98-100, it executes the parser with the assembled configuration:

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:

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 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 while leaving 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 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. The rendered HTML is then passed to src/common/markdown-here.js for injection into the email composition window, keeping the parsing logic decoupled from the DOM manipulation layer.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →