Skip to main content
Turndown can be extended with custom rules to handle specialized HTML elements or customize the conversion output. A rule is a plain JavaScript object with filter and replacement properties.

Rule Structure

Every rule consists of two main parts:
{
  filter: 'element-name',  // or array or function
  replacement: function (content, node, options) {
    return '/* markdown output */'
  }
}

Creating Your First Custom Rule

1

Understand the basic structure

Let’s create a rule for converting <del> elements to strikethrough syntax:
var TurndownService = require('turndown')
var turndownService = new TurndownService()

turndownService.addRule('strikethrough', {
  filter: ['del', 's', 'strike'],
  replacement: function (content) {
    return '~' + content + '~'
  }
})
2

Test the rule

Now use it to convert HTML:
var html = '<p>This is <del>deleted</del> text</p>'
var markdown = turndownService.turndown(html)

console.log(markdown)
// Output: This is ~deleted~ text
3

Chain multiple rules

The addRule method returns the service instance, enabling method chaining:
turndownService
  .addRule('strikethrough', {
    filter: ['del', 's', 'strike'],
    replacement: function (content) {
      return '~' + content + '~'
    }
  })
  .addRule('underline', {
    filter: 'u',
    replacement: function (content) {
      return '_' + content + '_'
    }
  })

Filter Types

The filter property determines which elements a rule applies to. There are three types of filters:

String Filter

Select elements by tag name:
// From commonmark-rules.js
rules.paragraph = {
  filter: 'p',
  replacement: function (content) {
    return '\n\n' + content + '\n\n'
  }
}
Tag names in filters should be lowercase, regardless of their case in the HTML document.

Array Filter

Select multiple element types with a single rule:
// From commonmark-rules.js
rules.heading = {
  filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
  replacement: function (content, node, options) {
    var hLevel = Number(node.nodeName.charAt(1))
    
    if (options.headingStyle === 'setext' && hLevel < 3) {
      var underline = repeat((hLevel === 1 ? '=' : '-'), content.length)
      return '\n\n' + content + '\n' + underline + '\n\n'
    } else {
      return '\n\n' + repeat('#', hLevel) + ' ' + content + '\n\n'
    }
  }
}
Another example for emphasis:
// From commonmark-rules.js
rules.emphasis = {
  filter: ['em', 'i'],
  replacement: function (content, node, options) {
    if (!content.trim()) return ''
    return options.emDelimiter + content + options.emDelimiter
  }
}

Function Filter

Use a function for complex matching logic:
// From commonmark-rules.js - Inline link rule
rules.inlineLink = {
  filter: function (node, options) {
    return (
      options.linkStyle === 'inlined' &&
      node.nodeName === 'A' &&
      node.getAttribute('href')
    )
  },
  replacement: function (content, node) {
    var href = node.getAttribute('href')
    if (href) href = href.replace(/([()])/g, '\\$1')
    var title = cleanAttribute(node.getAttribute('title'))
    if (title) title = ' "' + title.replace(/"/g, '\\"') + '"'
    return '[' + content + '](' + href + title + ')'
  }
}
Another example for fenced code blocks:
// From commonmark-rules.js
rules.fencedCodeBlock = {
  filter: function (node, options) {
    return (
      options.codeBlockStyle === 'fenced' &&
      node.nodeName === 'PRE' &&
      node.firstChild &&
      node.firstChild.nodeName === 'CODE'
    )
  },
  replacement: function (content, node, options) {
    var className = node.firstChild.getAttribute('class') || ''
    var language = (className.match(/language-(\S+)/) || [null, ''])[1]
    var code = node.firstChild.textContent
    
    var fenceChar = options.fence.charAt(0)
    var fenceSize = 3
    var fenceInCodeRegex = new RegExp('^' + fenceChar + '{3,}', 'gm')
    
    var match
    while ((match = fenceInCodeRegex.exec(code))) {
      if (match[0].length >= fenceSize) {
        fenceSize = match[0].length + 1
      }
    }
    
    var fence = repeat(fenceChar, fenceSize)
    
    return (
      '\n\n' + fence + language + '\n' +
      code.replace(/\n$/, '') +
      '\n' + fence + '\n\n'
    )
  }
}

Replacement Function

The replacement function determines the Markdown output. It receives three parameters:
ParameterTypeDescription
contentStringThe Markdown content of the node’s children
nodeNodeThe DOM node being converted
optionsObjectThe TurndownService options

Example: List Items

Here’s how Turndown handles list items from commonmark-rules.js:
rules.listItem = {
  filter: 'li',
  replacement: function (content, node, options) {
    var prefix = options.bulletListMarker + '   '
    var parent = node.parentNode
    
    if (parent.nodeName === 'OL') {
      var start = parent.getAttribute('start')
      var index = Array.prototype.indexOf.call(parent.children, node)
      prefix = (start ? Number(start) + index : index + 1) + '.  '
    }
    
    var isParagraph = /\n$/.test(content)
    content = trimNewlines(content) + (isParagraph ? '\n' : '')
    content = content.replace(/\n/gm, '\n' + ' '.repeat(prefix.length))
    
    return prefix + content + (node.nextSibling ? '\n' : '')
  }
}

Example: Blockquotes

A simpler example from commonmark-rules.js:
rules.blockquote = {
  filter: 'blockquote',
  replacement: function (content) {
    content = trimNewlines(content).replace(/^/gm, '> ')
    return '\n\n' + content + '\n\n'
  }
}

Complete Custom Rule Examples

Highlight/Mark Elements

turndownService.addRule('highlight', {
  filter: 'mark',
  replacement: function (content) {
    return '==' + content + '=='
  }
})

// Usage
var html = '<p>This is <mark>highlighted</mark> text</p>'
var markdown = turndownService.turndown(html)
// Output: This is ==highlighted== text

Custom Div Classes

turndownService.addRule('alert', {
  filter: function (node) {
    return (
      node.nodeName === 'DIV' &&
      node.classList.contains('alert')
    )
  },
  replacement: function (content, node) {
    var type = node.getAttribute('data-type') || 'info'
    return '\n\n> **' + type.toUpperCase() + '**: ' + content + '\n\n'
  }
})

// Usage
var html = '<div class="alert" data-type="warning">Be careful!</div>'
var markdown = turndownService.turndown(html)
// Output: > **WARNING**: Be careful!

Abbreviations

turndownService.addRule('abbreviation', {
  filter: 'abbr',
  replacement: function (content, node) {
    var title = node.getAttribute('title')
    return title ? content + ' (' + title + ')' : content
  }
})

// Usage
var html = '<abbr title="HyperText Markup Language">HTML</abbr>'
var markdown = turndownService.turndown(html)
// Output: HTML (HyperText Markup Language)

Keyboard Input

turndownService.addRule('keyboard', {
  filter: 'kbd',
  replacement: function (content) {
    return '<kbd>' + content + '</kbd>'
  }
})

// Usage
var html = '<p>Press <kbd>Ctrl</kbd>+<kbd>C</kbd> to copy</p>'
var markdown = turndownService.turndown(html)
// Output: Press <kbd>Ctrl</kbd>+<kbd>C</kbd> to copy

Rule Precedence

When Turndown processes a node, it iterates through rules in this order:
1

Blank rule

Handles blank elements (only whitespace). Overrides all other rules.
2

Added rules

Custom rules added via addRule(). Processed in the order they were added.
3

CommonMark rules

Built-in rules from commonmark-rules.js for standard Markdown elements.
4

Keep rules

Elements to keep as HTML (via keep() method).
5

Remove rules

Elements to remove entirely (via remove() method).
6

Default rule

Fallback rule for unrecognized elements.
Custom rules added via addRule() take precedence over built-in CommonMark rules. This allows you to override default behavior.

Advanced Pattern: Accessing Node Properties

Rules can access any DOM node property:
turndownService.addRule('imageWithDimensions', {
  filter: function (node) {
    return (
      node.nodeName === 'IMG' &&
      node.getAttribute('src') &&
      (node.getAttribute('width') || node.getAttribute('height'))
    )
  },
  replacement: function (content, node) {
    var alt = node.getAttribute('alt') || ''
    var src = node.getAttribute('src')
    var width = node.getAttribute('width')
    var height = node.getAttribute('height')
    var dimensions = width || height ? ' =' + (width || '') + 'x' + (height || '') : ''
    
    return '![' + alt + '](' + src + dimensions + ')'
  }
})

Next Steps

API Reference

Explore the complete API documentation for addRule

GFM Plugin

Learn about the GitHub Flavored Markdown plugin

Build docs developers (and LLMs) love