ydr-util
Version:
264 lines (214 loc) • 7.05 kB
JavaScript
/*!
* xss 安全【非常重要】
* @author ydr.me
* @create 2014-11-22 23:11
*/
'use strict';
var marked = require('marked');
var typeis = require('./typeis.js');
var dato = require('./dato.js');
var crypto = require('./crypto.js');
var url = require('url');
var REG_DOUBLE = /^\/\//;
var REG_POINT = /\./g;
var REG_LT = /</g;
var REG_GT = />/g;
var REG_SHAP = /^#/;
var REG_NOT_WORD = /[^\w]/g;
var RGE_FIRST = /^-/;
var REG_TOC = /^#heading(-\d-\d+-.*)$/;
// 空白
//var REG_SPACE = /[\x00-\x20\x7F-\xA0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000\uFEFF\t\v]{1,}/g;
var REG_LONG_BREAK_LINE = /[\n\r]{3,}/g;
// 自动关闭标签是安全的,如 br、hr、img 等
var REG_CLOSE_TAGNAME = /(?!```)<([a-z\d]+)\b[\s\S]*?>([\s\S]*?)<\/\1>(?!```)/ig;
var REG_PRE = /```[\s\S]*?```/g;
var REG_PATH = /^(\/|\.{0,2})(\/[^/]+)+$/;
// 影响页面的危险标签
var dangerTagNameList = 'script iframe frameset body head html link'.split(' ');
var filterDefaults = {
/**
* link 配置
* 1、无域地址都在当前窗口打开
* 2、符合 hosts 内的域名都在当前窗口打开
* 3、其他都在新窗口打开
* 4、不合法的URL直接返回空
*/
link: {
/**
* 不需要新窗口打开的域名
* @type Array
*/
hosts: [],
filter: function (href, title, text) {
return _buildLink(href, title, text, true);
}
}
};
/**
* markdown 语法安全过滤,虽然 markdown 支持兼容 HTML 标签,但为了安全考虑,
* 这里必须去掉相当一部分的标签
* @param source {String} 原始内容
* @param [moreDangerTagNameList] {Array} 更多危险标签,危险标签是会被直接删除的
* @returns {string} 过滤后的内容
*/
exports.mdSafe = function (source, moreDangerTagNameList) {
var list = source.split(REG_PRE);
var pres = source.match(REG_PRE) || [''];
var ret = '';
var i = 0;
if (typeis(moreDangerTagNameList) !== 'array') {
moreDangerTagNameList = [];
}
// 过滤不安全 HTML 标签
list = list.map(function (item) {
return item
.replace(REG_CLOSE_TAGNAME, function ($0, $1) {
$1 = $1.toLowerCase();
if (dangerTagNameList.indexOf($1) > -1 || moreDangerTagNameList.indexOf($1) > -1) {
return '';
} else {
return $0.replace(REG_LT, '<').replace(REG_GT, '>');
}
})
.replace(REG_LONG_BREAK_LINE, '\n\n\n');
});
list.forEach(function (item, j) {
if (j > 0) {
ret += pres[i++];
}
ret += item;
});
return ret;
//var tokens = marked.lexer(source);
//var toc = '<!--toc start-->';
//
//tokens.forEach(function (token) {
// if (token.type !== 'heading') {
// return;
// }
//
// var depth = new Array((token.depth - 1) * 4 + 1).join(' ');
//
// toc += depth + '- [' + token.text + '](#heading-' + crypto.md5(token.text) + ')\n';
//});
//
//return toc + '\n\n<!--toc end-->' + ret;
};
/**
* table of content
* @param source {String} 原始内容
* @returns {string}
*/
exports.mdTOC = function (source) {
var tokens = marked.lexer(source);
var toc = '\n\n';
var index = 0;
tokens.forEach(function (token) {
if (token.type !== 'heading') {
return;
}
var depth = new Array((token.depth - 1) * 4 + 1).join(' ');
toc += depth + '- [' + token.text + '](#heading-' + token.depth + '-' + (index++) + '-' + crypto.md5(token.text) + ')\n';
});
return toc + '\n\n';
};
/**
* markdown 内容渲染成 HTML 内容
* @param source {String} 原始 markdown 内容
* @param [filterOptions] {Object} 配置
*/
exports.mdRender = function (source, filterOptions) {
var markedRender = new marked.Renderer();
filterOptions = dato.extend(true, {}, filterDefaults, filterOptions);
// 定义 A 链接的 target
markedRender.link = function (href, title, text) {
if (REG_SHAP.test(href)) {
return _buildLink(href, title, text, false);
}
var fixHref = REG_DOUBLE.test(href) ? 'http:' + href : href;
var parse = url.parse(fixHref);
var host = parse.host;
var inHost = false;
// 非 URL && 非 PATH
if (!typeis.url(href) && !REG_PATH.test(href)) {
return '';
}
if (!host) {
return _buildLink(href, title, text, false);
}
dato.each(filterOptions.link.hosts, function (index, item) {
if (_regExp(item).test(host)) {
inHost = true;
return false;
}
});
// 指定域内的 NO _blank
if (inHost) {
return _buildLink(href, title, text, false);
}
// 其他的使用传入对象处理
return filterOptions.link.filter(href, title, text);
};
var index = 0;
markedRender.heading = function (text, level) {
var href = crypto.md5(text);
var html = '<h' + level + ' id="heading-' + level + '-' + index + '-' + href + '"><a class="heading-link" ' +
'href="#toc-' + level + '-' + index + '-' + href + '">' +
text + '</a></h' + level + '>';
index++;
return html;
};
marked.setOptions({renderer: markedRender});
return marked(source);
};
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/**
* 生成链接
* @param href
* @param title
* @param text
* @param isBlank
* @returns {string}
* @private
*/
function _buildLink(href, title, text, isBlank) {
text = text.trim();
return '<a href="' + href + '"' +
(REG_TOC.test(href) ? ' id="toc' + href.replace(REG_TOC, '$1') + '"' : '') +
(isBlank ? ' target="_blank"' : '') +
(title ? ' ' + title : '') +
'>' + (text || href) + '</a>';
}
/**
* 生成正则
* @param regstr
* @returns {RegExp}
* @private
*/
function _regExp(regstr) {
var arr = regstr.split('*.');
var ret = '';
arr = arr.map(function (item) {
return item.replace(REG_POINT, '\\.');
});
arr.forEach(function (item, index) {
if (index > 0) {
ret += '([^.]+\\.)*';
}
ret += item;
});
return new RegExp('^' + ret + '$', 'i');
}
//var fs = require('fs');
//var path = require('path');
//var file1 = path.join(__dirname, '../test/test.md');
//var file2 = path.join(__dirname, '../test/test.html');
//var md1 = fs.readFileSync(file1, 'utf8');
//
//var toc = exports.mdRender(exports.mdTOC(md1));
//var content = exports.mdRender(md1);
//
//fs.writeFileSync(file2, toc + content, 'utf8');