Turndown 源码分析:四、`turndown.js`

2023-10-13 09:22:37 浏览数 (2)

代码语言:javascript复制
import COMMONMARK_RULES from './commonmark-rules'
import Rules from './rules'
import { extend, trimLeadingNewlines, trimTrailingNewlines } from './utilities'
import RootNode from './root-node'
import Node from './node'
var reduce = Array.prototype.reduce

// 定义替换模式表
// 第一个元素是模式串,第二个元射弩是替换字符串
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) {
  // 如果不是用 new 调用的强制用 new 调用
  if (!(this instanceof TurndownService)) return new TurndownService(options)

  // 定义默认配置
  var defaults = {
    rules: COMMONMARK_RULES,    // 反编译各个元素的规则
    headingStyle: 'setext',     // 标题格式 atx || setext
    hr: '* * *',                // 行分隔符格式
    bulletListMarker: '*',      // 列表前缀
    codeBlockStyle: 'indented', // 代码块格式
    fence: '```',               // 代码快分隔符
    emDelimiter: '_',           // 斜体分隔符
    strongDelimiter: '**',      // 粗体分隔符
    linkStyle: 'inlined',       // 链接格式
    linkReferenceStyle: 'full', // 引用链接的格式
    br: '  ',                   // 换行后缀
    preformattedCode: false,    // 是否格式化代码
    // 移除规则的替换方法,移除元素内容
    blankReplacement: function (content, node) {
      return node.isBlock ? 'nn' : ''
    },
    // 保留规则的替换方法,将元素的 HTML 保持不变
    keepReplacement: function (content, node) {
      return node.isBlock ? 'nn'   node.outerHTML   'nn' : node.outerHTML
    },
    // 默认的替换方法,将元素的标签去除,内容保持不变
    defaultReplacement: function (content, node) {
      return node.isBlock ? 'nn'   content   'nn' : 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: function (input) {
    // 判断是否是字符串或者元素
    if (!canConvert(input)) {
      throw new TypeError(
        input   ' is not a string, or an element/document/fragment node.'
      )
    }
    // 空串处理
    if (input === '') return ''
   // 将输入包含在单个根节点中,然后处理每个子元素
    var output = process.call(this, new RootNode(input, this.options))
    // 后处理每个子元素
    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
 */

// 获取节点的内部 MD,等于所有子节点外部MD的连接
function process (parentNode) {
  var self = this
  // 遍历每个子节点,解析它的 MD 内容,之后合并
  return reduce.call(parentNode.childNodes, function (output, node) {
    // 给节点添加一些属性
    node = new Node(node, self.options)

    var replacement = ''
    if (node.nodeType === 3) {
      // 如果是个文本,判断它是不是代码块的一部分,不是的话对其转义。
      replacement = node.isCode ? node.nodeValue : self.escape(node.nodeValue)
    } else if (node.nodeType === 1) {
      // 如果是元素,那么获取其外部MD
      replacement = replacementForNode.call(self, node)
    }
    // 连接每个部分
    return join(output, replacement)
  }, '')
}

/**
 * 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
  // 对于每个规则,检查是否有 append 方法
  // 如果存在,就调用它获取文本,并追加到 output 上
  this.rules.forEach(function (rule) {
    if (typeof rule.append === 'function') {
      output = join(output, rule.append(self.options))
    }
  })
  // 将首尾空白移除
  return output.replace(/^[trn] /, '').replace(/[trns] $/, '')
}

/**
 * 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
 */

// 获取节点的外部 MD
function replacementForNode (node) {
  // 获取适用于该节点的规则
  var rule = this.rules.forNode(node)
  // 获取该节点的 MD 内容
  var content = process.call(this, node)
  // 为该节点加上是适当的前导和尾随空白
  var whitespace = node.flankingWhitespace
  if (whitespace.leading || whitespace.trailing) content = content.trim()
  // 使用规则的替换函数生成整个节点的 MD,然后拼接空白并返回
  return (
    whitespace.leading  
    rule.replacement(content, node, this.options)  
    whitespace.trailing
  )
}

/**
 * Joins replacement to the current output with appropriate number of new lines
 * @private
 * @param {String} output The current conversion output
 * @param {String} replacement The string to append to the output
 * @returns Joined output
 * @type String
 */

function join (output, replacement) {
  // 移除第一个字符串的尾随换行
  var s1 = trimTrailingNewlines(output)
  // 移除第二个字符串的前导换行
  var s2 = trimLeadingNewlines(replacement)
  // 计算尾随和前导换行长度,取最大值为 nls
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length)
  // 填充等于 nls 个换行
  var separator = 'nn'.substring(0, nls)
  // 拼接字符串并返回
  return s1   separator   s2
}

/**
 * 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) {
  // 如果输入是字符串或者 HTML 元素,或者其他特定类型节点则返回真
  return (
    input != null && (
      typeof input === 'string' ||
      (input.nodeType && (
        input.nodeType === 1 || input.nodeType === 9 || input.nodeType === 11
      ))
    )
  )
}

0 人点赞