text: import COMMONMARK_RULES from './commonmark-rules'
  import Rules from './rules'
  import { extend } from './utilities'
  import RootNode from './root-node'
  import Node from './node'
  var reduce = Array.prototype.reduce
  var leadingNewLinesRegExp = /^\n*/
  var trailingNewLinesRegExp = /\n*$/
  var escapes = [
    [/\\/g, '\\\\'],
    [/\*/g, '\\*'],
    [/^-/g, '\\-'],
    [/^\+ /g, '\\+ '],
    [/^(=+)/g, '\\$1'],
    [/^(#{1,6}) /g, '\\$1 '],
    [/`/g, '\\`'],
    [/^~~~/g, '\\~~~'],
    [/\[/g, '\\['],
    [/\]/g, '\\]'],
    [/^>/g, '\\>'],
    [/_/g, '\\_'],
    [/^(\d+)\. /g, '$1\\. ']
  ]
  
  export default function TurndownService (options) {
    if (!(this instanceof TurndownService)) return new TurndownService(options)
  
    var defaults = {
      rules: COMMONMARK_RULES,
      headingStyle: 'setext',
      hr: '* * *',
      bulletListMarker: '*',
      codeBlockStyle: 'indented',
      fence: '```',
      emDelimiter: '_',
      strongDelimiter: '**',
      linkStyle: 'inlined',
      linkReferenceStyle: 'full',
      br: '  ',
      blankReplacement: function (content, node) {
        return node.isBlock ? '\n\n' : ''
      },
      keepReplacement: function (content, node) {
        return node.isBlock ? '\n\n' + node.outerHTML + '\n\n' : node.outerHTML
      },
      defaultReplacement: function (content, node) {
        return node.isBlock ? '\n\n' + content + '\n\n' : content
      }
    }
    this.options = extend({}, defaults, options)
    this.rules = new Rules(this.options)
  }
  
  TurndownService.prototype = {
    /**
     * The entry point for converting a string or DOM node to Markdown
     * @public
     * @param {String|HTMLElement} input The string or DOM node to convert
     * @returns A Markdown representation of the input
     * @type String
     */
  
    turndown: async function (input) {
      if (!canConvert(input)) {
        throw new TypeError(
          input + ' is not a string, or an element/document/fragment node.'
        )
      }
  
      if (input === '') return ''
  
      var output = await process.call(this, new RootNode(input))
      return postProcess.call(this, output)
    },
  
    /**
     * Add one or more plugins
     * @public
     * @param {Function|Array} plugin The plugin or array of plugins to add
     * @returns The Turndown instance for chaining
     * @type Object
     */
  
    use: function (plugin) {
      if (Array.isArray(plugin)) {
        for (var i = 0; i < plugin.length; i++) this.use(plugin[i])
      } else if (typeof plugin === 'function') {
        plugin(this)
      } else {
        throw new TypeError('plugin must be a Function or an Array of Functions')
      }
      return this
    },
  
    /**
     * Adds a rule
     * @public
     * @param {String} key The unique key of the rule
     * @param {Object} rule The rule
     * @returns The Turndown instance for chaining
     * @type Object
     */
  
    addRule: function (key, rule) {
      this.rules.add(key, rule)
      return this
    },
  
    /**
     * Keep a node (as HTML) that matches the filter
     * @public
     * @param {String|Array|Function} filter The unique key of the rule
     * @returns The Turndown instance for chaining
     * @type Object
     */
  
    keep: function (filter) {
      this.rules.keep(filter)
      return this
    },
  
    /**
     * Remove a node that matches the filter
     * @public
     * @param {String|Array|Function} filter The unique key of the rule
     * @returns The Turndown instance for chaining
     * @type Object
     */
  
    remove: function (filter) {
      this.rules.remove(filter)
      return this
    },
  
    /**
     * Escapes Markdown syntax
     * @public
     * @param {String} string The string to escape
     * @returns A string with Markdown syntax escaped
     * @type String
     */
  
    escape: function (string) {
      return escapes.reduce(function (accumulator, escape) {
        return accumulator.replace(escape[0], escape[1])
      }, string)
    }
  }
  
  /**
   * Reduces a DOM node down to its Markdown string equivalent
   * @private
   * @param {HTMLElement} parentNode The node to convert
   * @returns A Markdown representation of the node
   * @type String
   */
  
  async function process (parentNode) {
    var self = this
    var result = ''
    var len = parentNode.childNodes.length
    var nodeIndex = 0
  
    while (nodeIndex < len) {
      var node = new Node(parentNode.childNodes[nodeIndex])
  
      var replacement = ''
      if (node.nodeType === 3) {
        replacement = node.isCode ? node.nodeValue : self.escape(node.nodeValue)
      } else if (node.nodeType === 1) {
        replacement = await replacementForNode.call(self, node)
      }
  
      result = join(result, replacement)
  
      nodeIndex++
    }
  
    return result
  }
  
  /**
   * Appends strings as each rule requires and trims the output
   * @private
   * @param {String} output The conversion output
   * @returns A trimmed version of the ouput
   * @type String
   */
  
  function postProcess (output) {
    var self = this
    this.rules.forEach(function (rule) {
      if (typeof rule.append === 'function') {
        output = join(output, rule.append(self.options))
      }
    })
  
    return output.replace(/^[\t\r\n]+/, '').replace(/[\t\r\n\s]+$/, '')
  }
  
  /**
   * Converts an element node to its Markdown equivalent
   * @private
   * @param {HTMLElement} node The node to convert
   * @returns A Markdown representation of the node
   * @type String
   */
  
  async function replacementForNode (node) {
    var rule = this.rules.forNode(node)
    var content = process.call(this, node)
    var whitespace = node.flankingWhitespace
    if (whitespace.leading || whitespace.trailing) content = content.trim()
    var resplacement = await rule.replacement(content, node, this.options)
  
    return (
      whitespace.leading +
      resplacement +
      whitespace.trailing
    )
  }
  
  /**
   * Determines the new lines between the current output and the replacement
   * @private
   * @param {String} output The current conversion output
   * @param {String} replacement The string to append to the output
   * @returns The whitespace to separate the current output and the replacement
   * @type String
   */
  
  function separatingNewlines (output, replacement) {
    var newlines = [
      output.match(trailingNewLinesRegExp)[0],
      replacement.match(leadingNewLinesRegExp)[0]
    ].sort()
    var maxNewlines = newlines[newlines.length - 1]
    return maxNewlines.length < 2 ? maxNewlines : '\n\n'
  }
  
  function join (string1, string2) {
    var separator = separatingNewlines(string1, string2)
  
    // Remove trailing/leading newlines and replace with separator
    string1 = string1.replace(trailingNewLinesRegExp, '')
    string2 = string2.replace(leadingNewLinesRegExp, '')
  
    return string1 + separator + string2
  }
  
  /**
   * Determines whether an input can be converted
   * @private
   * @param {String|HTMLElement} input Describe this parameter
   * @returns Describe what it returns
   * @type String|Object|Array|Boolean|Number
   */
  
  function canConvert (input) {
    return (
      input != null && (
        typeof input === 'string' ||
        (input.nodeType && (
          input.nodeType === 1 || input.nodeType === 9 || input.nodeType === 11
        ))
      )
    )
  }
  