规则集包含一系列规则,决定各种标签如何反编译。单个规则的格式是:
代码语言:javascript复制{
filter: String | String[] | function(node),
replacement: function(node, content, options),
}
filter
字段用于判断节点是否适用单条规则。如果它是字符串,则判断node.nodeName === filter
;如果它是字符串数组,则判断filter.includes(node.nodeName)
;如果它是函数,则判断filter(node)
。
replacement
字段是个函数,接受单个节点,该节点的内部 Markdown,以及配置项,返回节点的外部 Markdown。
规则集rules
是一个对象,属性名是规则名称,值是对应的规则对象。
段落
这个没啥好说的,前后插入两个换行符。
代码语言:javascript复制rules.paragraph = {
filter: 'p',
replacement: function (content) {
return 'nn' content 'nn'
}
};
换行
换行的规则在各个编辑器中是不统一的,经典的 Markdown 需要两个空格加一个换行符。但 GH 风格的只需要一个换行符。options.br
用于配置换行符之前应该添加的字符。
rules.lineBreak = {
filter: 'br',
replacement: function (content, node, options) {
return options.br 'n'
}
};
标题
标题也就是<h1>
到<h6>
六个标签。Markdown 规范中有两种表达形式第一种是 ATX,也就是一堆井号后跟标题内容,井号数量就是标题级别。还有一种Setext
,在标题内容的下一行添加相同长度的分隔符。一级标题分隔符是等号,其它的都是连字符。
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 (
'nn' content 'n' underline 'nn'
)
} else {
return 'nn' repeat('#', hLevel) ' ' content 'nn'
}
}
};
引用
首先裁掉开头和结尾的换行符,避免无意义空行。然后在所有行开头插入>
,最后前后添加两个换行符。
rules.blockquote = {
filter: 'blockquote',
replacement: function (content) {
content = content.replace(/^n |n $/g, '');
content = content.replace(/^/gm, '> ');
return 'nn' content 'nn'
}
};
列表
判断这个列表是顶级列表还是子列表。如果是子列表就在前方添加一个换行。如果是顶级列表就前后添加两个换行。
代码语言:javascript复制rules.list = {
filter: ['ul', 'ol'],
replacement: function (content, node) {
var parent = node.parentNode;
if (parent.nodeName === 'LI' && parent.lastElementChild === node) {
return 'n' content
} else {
return 'nn' content 'nn'
}
}
};
列表项
列表项可以是有序或者无序列表的元素。每个列表项的开头都会有一个前缀,对于有序列表,它是数字和点,例如1.
,对于无序列表,它是一个加减或乘号后跟空格,例如
。
首先清除无意义的空行,保持最后面有一个换行符,添加缩进。
然后判断列表的种类,如果是无序的,那么前缀就是符号加空格,符号由options.bulletListMarker
定义。如果是有序的,获取其start
属性(没有则为 0),然后加上列表项的位置计算出序号。
rules.listItem = {
filter: 'li',
replacement: function (content, node, options) {
content = content
.replace(/^n /, '') // remove leading newlines
.replace(/n $/, 'n') // replace trailing newlines with just a single one
.replace(/n/gm, 'n '); // indent
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) '. ';
}
return (
prefix content (node.nextSibling && !/n$/.test(content) ? 'n' : '')
)
}
};
代码块(缩进)
用户可以在选项中配置代码块是否反编译成缩进形式,还是三个反引号形式。
在缩进形式中,Markdown 就是它底下的<code>
对象的文本,每行加上四个空格的前缀。
rules.indentedCodeBlock = {
filter: function (node, options) {
return (
options.codeBlockStyle === 'indented' &&
node.nodeName === 'PRE' &&
node.firstChild &&
node.firstChild.nodeName === 'CODE'
)
},
replacement: function (content, node, options) {
return (
'nn '
node.firstChild.textContent.replace(/n/g, 'n ')
'nn'
)
}
};
代码块(反引号)
反引号代码块支持语法高亮,所以重点在于获取高亮语言。语言由底下的<code>
元素以language-xxx
形式制定。
分隔符是至少三个重复的`
或~
。如果字符在代码里面出现,就需要多加一个,例如三个反引号在代码中出现,就要变成四个。所以代码使用正则匹配三个以上的字符,然后计算最大数量加一,作为分隔符中字符的最终数量。
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 (
'nn' fence language 'n'
code.replace(/n$/, '')
'n' fence 'nn'
)
}
};
分割线
分割线支持* * *
和- - -
两种,由用户通过options.hr
制定。
rules.horizontalRule = {
filter: 'hr',
replacement: function (content, node, options) {
return 'nn' options.hr 'nn'
}
};
内联链接
代码只是获取链接、文本、标题三部分然后拼一起,没啥难度。
代码语言:javascript复制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');
var title = cleanAttribute(node.getAttribute('title'));
if (title) title = ' "' title '"';
return '[' content '](' href title ')'
}
};
引用链接
Markdown 的链接有内联式[content](href "title")
和引用式[content][ref]
。
代码需要根据不同格式选项创建引用表,然后把引用表推到references
数组中。
引用和在段落之后插入,也可以放到整片文章末尾。当需要插入的时候,调用append()
方法,它会将所有引用连接到一起,变成 Markdown,然后清空引用表。
rules.referenceLink = {
filter: function (node, options) {
return (
options.linkStyle === 'referenced' &&
node.nodeName === 'A' &&
node.getAttribute('href')
)
},
replacement: function (content, node, options) {
var href = node.getAttribute('href');
var title = cleanAttribute(node.getAttribute('title'));
if (title) title = ' "' title '"';
var replacement;
var reference;
switch (options.linkReferenceStyle) {
case 'collapsed':
replacement = '[' content '][]';
reference = '[' content ']: ' href title;
break
case 'shortcut':
replacement = '[' content ']';
reference = '[' content ']: ' href title;
break
default:
var id = this.references.length 1;
replacement = '[' content '][' id ']';
reference = '[' id ']: ' href title;
}
this.references.push(reference);
return replacement
},
references: [],
append: function (options) {
var references = '';
if (this.references.length) {
references = 'nn' this.references.join('n') 'nn';
this.references = []; // Reset references
}
return references
}
};
粗体斜体
粗体和斜体用分隔符包围,分隔符可以是*
也可以是_
,粗体两个,斜体一个。
代码做了一个优化就是排除掉没有内容的粗体和斜体。
代码语言:javascript复制rules.emphasis = {
filter: ['em', 'i'],
replacement: function (content, node, options) {
if (!content.trim()) return ''
return options.emDelimiter content options.emDelimiter
}
};
rules.strong = {
filter: ['strong', 'b'],
replacement: function (content, node, options) {
if (!content.trim()) return ''
return options.strongDelimiter content options.strongDelimiter
}
};
内联代码
内联代码是由至少一个反引号包围的文本。当分隔符在文本中出现时,可以增加分隔符中反引号的个数。
所以分隔符的最大字符数就是文本中最大的连续反引号数量加一。
代码语言:javascript复制rules.code = {
filter: function (node) {
var hasSiblings = node.previousSibling || node.nextSibling;
var isCodeBlock = node.parentNode.nodeName === 'PRE' && !hasSiblings;
return node.nodeName === 'CODE' && !isCodeBlock
},
replacement: function (content) {
if (!content) return ''
content = content.replace(/r?n|r/g, ' ');
var extraSpace = /^`|^ .*?[^ ].* $|`$/.test(content) ? ' ' : '';
var delimiter = '`';
var matches = content.match(/` /gm) || [];
while (matches.indexOf(delimiter) !== -1) delimiter = delimiter '`';
return delimiter extraSpace content extraSpace delimiter
}
};
图片
把标题、连接和替代文本找到然后拼起来。
代码语言:javascript复制rules.image = {
filter: 'img',
replacement: function (content, node) {
var alt = cleanAttribute(node.getAttribute('alt'));
var src = node.getAttribute('src') || '';
var title = cleanAttribute(node.getAttribute('title'));
var titlePart = title ? ' "' title '"' : '';
return src ? '![' alt ']' '(' src titlePart ')' : ''
}
};
cleanAttribute()
将连续换行变成单个换行,将行首空格移除。
代码语言:javascript复制function cleanAttribute (attribute) {
return attribute ? attribute.replace(/(n s*) /g, 'n') : ''
}