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 ? '\n\n' : '' }, // 保留规则的替换方法,将元素的 HTML 保持不变 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: 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(/^[\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 */ // 获取节点的外部 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 = '\n\n'.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 )) ) ) }