初始化提交
This commit is contained in:
599
common/wiki/amWiki/js/amWiki.docs.js
Normal file
599
common/wiki/amWiki/js/amWiki.docs.js
Normal file
@@ -0,0 +1,599 @@
|
||||
/**
|
||||
* amWiki Web端 - 文档加载与渲染模块
|
||||
* @author Tevin
|
||||
*/
|
||||
|
||||
;
|
||||
(function (win, doc, $) {
|
||||
|
||||
'use strict';
|
||||
|
||||
var hljs = win.hljs;
|
||||
var marked = win.marked;
|
||||
var URL_ENCODE_NAME = 'AMWikiUrlEncode'; //记录url编码的键名
|
||||
|
||||
/**
|
||||
* 文档管理
|
||||
* @constructor
|
||||
*/
|
||||
var Docs = function () {
|
||||
this.$e = {
|
||||
win: $(win),
|
||||
//markdown 文档内容容器
|
||||
view: $('#view'),
|
||||
//网页 title 标签
|
||||
title: $('title'),
|
||||
//目录悬浮窗标题
|
||||
contentsTitle: $('#contentsTitle')
|
||||
};
|
||||
this.data = {
|
||||
//记录页面宽度
|
||||
pageWidth: 0
|
||||
};
|
||||
this._initUrlEncode();
|
||||
this._initHashEvent();
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化url编码类型标记
|
||||
* @private
|
||||
*/
|
||||
Docs.prototype._initUrlEncode = function () {
|
||||
/*
|
||||
* 由于win与linux采用不同编码保存中文文件名
|
||||
* 记录浏览器打开当前域名的wiki时服务器中文文件名的编码类型
|
||||
*/
|
||||
if (!localStorage[URL_ENCODE_NAME]) {
|
||||
localStorage[URL_ENCODE_NAME] = 'utf8';
|
||||
} else if (localStorage[URL_ENCODE_NAME] != 'utf8' && localStorage[URL_ENCODE_NAME] != 'gbk') {
|
||||
localStorage[URL_ENCODE_NAME] = 'utf8';
|
||||
}
|
||||
//旧版本记录移除
|
||||
delete localStorage.urlEcode
|
||||
};
|
||||
|
||||
/**
|
||||
* 修正移动端hash变化时滚动位置
|
||||
* @private
|
||||
*/
|
||||
Docs.prototype._initHashEvent = function () {
|
||||
var that = this;
|
||||
this.data.pageWidth = this.$e.win.width();
|
||||
this.$e.win.on('resize', function () {
|
||||
that.data.pageWidth = that.$e.win.width();
|
||||
});
|
||||
this.$e.win.on('hashchange', function (e) {
|
||||
var hash = location.hash.split('#')[1];
|
||||
if (that.data.pageWidth <= 720) {
|
||||
that.$e.view.find('h1,h2,h3').each(function (index, element) {
|
||||
var $title = $(element);
|
||||
var text = $title.text().replace(/\s+/g, '');
|
||||
if (hash == text) {
|
||||
that.$e.win.scrollTop($title.offset().top - 55);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 转换链接文本
|
||||
* @param {String} str
|
||||
* @returns {String}
|
||||
* @private
|
||||
*/
|
||||
Docs.prototype._tramsformLinkText = function (str) {
|
||||
return str
|
||||
.replace(/^\s+|\s+$/g, '') //去除首尾空格
|
||||
.replace(/'/g, ''') //转义单引号
|
||||
.replace(/"/g, '"') //转义双引号,由于双引号无法正确传递给html属性,当作为hash时将删除处理
|
||||
.replace(/\(/g, '(') //转义左圆括号
|
||||
.replace(/\)/g, ')') //转义右圆括号
|
||||
.replace(/\[/g, '[') //转义左中括号
|
||||
.replace(/\]/g, ']') //转义右中括号
|
||||
.replace(/\s/g, ' ');//转义空格
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置文档h1、h2、h3描记
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
Docs.prototype._setTitlesAnchor = function () {
|
||||
var that = this;
|
||||
var $titles = null;
|
||||
var hash = '';
|
||||
var contentsMd = ''; //提取目录为markdown字符串
|
||||
if (location.hash && location.hash.length > 1) {
|
||||
hash = location.hash.split('#')[1];
|
||||
}
|
||||
var anchorHtml = '<a class="anchor" href="#{title}" name="{title}">' +
|
||||
'<svg><use xlink:href="#icon:linkAnchor"></use></svg></a>';
|
||||
$titles = that.$e.view.find('h1,h2,h3');
|
||||
$titles.each(function (index, element) {
|
||||
var $this = $(element);
|
||||
var text1 = that._tramsformLinkText($this.text());
|
||||
var text2 = text1.replace(/"/g, ''); //删除双引号
|
||||
//提取目录
|
||||
if ($this.is('h2')) {
|
||||
contentsMd += '1. [' + text1 + '](#' + text2 + ' "' + text2 + '")\n';
|
||||
} else if ($this.is('h3')) {
|
||||
contentsMd += '\t1. [' + text1 + '](#' + text2 + ' "' + text2 + '")\n';
|
||||
}
|
||||
//设置描记
|
||||
$this.prepend(anchorHtml.replace(/{title}/g, text2));
|
||||
//首次打开页面滚动位置修正
|
||||
if (hash == $this.text().replace(/"/g, '')) {
|
||||
if (that.data.pageWidth <= 720) {
|
||||
that.$e.win.scrollTop($this.offset().top - 55);
|
||||
} else {
|
||||
that.$e.win.scrollTop($this.offset().top);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (contentsMd.indexOf('\t') == 0) {
|
||||
contentsMd = '1.  \n' + contentsMd;
|
||||
}
|
||||
return contentsMd;
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建脚注
|
||||
* @param {String} text
|
||||
* @returns {String}
|
||||
* @private
|
||||
*/
|
||||
Docs.prototype._setFootnote = function (text) {
|
||||
var footnotes = [];
|
||||
var noteReg = /\[\^([ a-zA-Z\d]+)]/g;
|
||||
var footReg = /\[\^([ a-zA-Z\d]+)]: ?([\S\s]+?)(?=\[\^(?:[ a-zA-Z\d]+)]|\n\n|$)/g;
|
||||
var templates = $.trim($('#template\\:footnote').text()).split(/[\r\n]+\s*/g);
|
||||
templates[4] += templates[5] + templates[6] + templates[7] + templates[8];
|
||||
var html = '';
|
||||
//提取脚注内容
|
||||
text = text.replace(footReg, function (match, s1, s2, index) {
|
||||
var title = '';
|
||||
s2 = s2.replace(/"(.*?)"\s*$/, function (m, ss1) {
|
||||
title = ss1;
|
||||
return '';
|
||||
});
|
||||
footnotes.push({
|
||||
index: index,
|
||||
note: s1,
|
||||
content: s2,
|
||||
title: title,
|
||||
used: false
|
||||
});
|
||||
//从页面上删除底部脚注内容
|
||||
return '';
|
||||
});
|
||||
//将脚注的标记转为序号
|
||||
text = text.replace(noteReg, function (match, s1) {
|
||||
for (var i = 0, foot; foot = footnotes[i]; i++) {
|
||||
if (foot.note == s1) {
|
||||
foot.used = true;
|
||||
return templates[0].replace(/{{index}}/g, i + 1 + '').replace('{{title}}', foot.title);
|
||||
}
|
||||
}
|
||||
//当脚注的正文不存在,视标记文本为正文
|
||||
var length = footnotes.push({
|
||||
index: 0,
|
||||
note: s1,
|
||||
content: s1,
|
||||
used: true
|
||||
});
|
||||
return templates[0].replace(/{{index}}/g, length + '');
|
||||
});
|
||||
//生成底部脚注html
|
||||
if (footnotes.length >= 1) {
|
||||
for (var i = 0, foot; foot = footnotes[i]; i++) {
|
||||
if (foot.used) {
|
||||
html += templates[2]
|
||||
.replace('{{index}}', i + 1)
|
||||
.replace('{{content}}', foot.content)
|
||||
.replace('{{back}}', templates[4].replace('{{index}}', i + 1));
|
||||
} else {
|
||||
html += templates[3].replace('{{content}}', foot.content);
|
||||
}
|
||||
}
|
||||
html = templates[1].replace('{{list}}', html);
|
||||
}
|
||||
return text + '\n<br>' + html;
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置js代码块注释显示隐藏
|
||||
* @param {Object} $elm
|
||||
* @private
|
||||
*/
|
||||
Docs.prototype._setJSCommentDisable = function ($elm) {
|
||||
var $disBtn = $('<div class="lang-off-js-comment">注<i>/</i></div>');
|
||||
$disBtn.on('click', function () {
|
||||
var $this = $(this);
|
||||
if ($this.hasClass('on')) {
|
||||
$this.removeClass('on');
|
||||
$elm.find('.hljs-comment').show();
|
||||
} else {
|
||||
$this.addClass('on');
|
||||
$elm.find('.hljs-comment').hide();
|
||||
}
|
||||
});
|
||||
$elm.prepend($disBtn);
|
||||
};
|
||||
|
||||
/**
|
||||
* 复制代码
|
||||
* @param {Object} $elm
|
||||
* @private
|
||||
*/
|
||||
Docs.prototype._copyCode = function ($elm) {
|
||||
var $copyBtn = $('<div class="copy-code icon-docs"></div>');
|
||||
$copyBtn.on('click', function () {
|
||||
var $this = $(this);
|
||||
navigator.clipboard.writeText($elm.text())
|
||||
.then((message) => {
|
||||
console.log('clipboard:复制成功');
|
||||
$copyBtn.removeClass('icon-docs').addClass('icon-ok');
|
||||
setTimeout(() => {
|
||||
$copyBtn.removeClass('icon-ok').addClass('icon-docs');
|
||||
}, 1000);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('clipboard:复制失败');
|
||||
layer.tips('复制失败', $this, {
|
||||
tips: [4, '#3595CC'],
|
||||
time: 1000
|
||||
});
|
||||
});
|
||||
});
|
||||
$elm.prepend($copyBtn);
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析流程图
|
||||
* @param {Object} $elm
|
||||
* @private
|
||||
*/
|
||||
Docs.prototype._createFlowChart = function ($elm) {
|
||||
var code = $elm.text();
|
||||
$elm.text('');
|
||||
var id = 'flowChart' + parseInt(Math.random() * 500);
|
||||
$elm.attr('id', id);
|
||||
var chart = flowchart.parse(code);
|
||||
chart.drawSVG(id, {
|
||||
'line-width': 1.3,
|
||||
'line-length': 56,
|
||||
'line-color': '#666',
|
||||
'text-margin': 10,
|
||||
'font-size': 12,
|
||||
'font': 'normal',
|
||||
'font-family': 'Helvetica',
|
||||
'font-weight': 'normal',
|
||||
'font-color': 'black',
|
||||
'element-color': '#888',
|
||||
'fill': '#fff',
|
||||
'yes-text': '是',
|
||||
'no-text': '否',
|
||||
'arrow-end': 'block-wide-long',
|
||||
'symbols': {},
|
||||
'flowstate': {}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析 Markdown 目录
|
||||
* @param {String} html
|
||||
* @returns {String}
|
||||
* @private
|
||||
*/
|
||||
Docs.prototype._setTOC = function (html) {
|
||||
return html.replace(/\[(TOC|MENU)]/g, '<blockquote class="markdown-contents"></blockquote>');
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析 Markdown 自定义图片大小与对齐方式
|
||||
* @param {String} html
|
||||
* @returns {String}
|
||||
* @private
|
||||
*/
|
||||
Docs.prototype._setImgResize = function (html) {
|
||||
return html.replace(/<img(.*?)src="(.*?)=(\d*[-x×]\d*)(-[lrc])?"/g, function (m, s1, s2, s3, s4) {
|
||||
var imgHtml = '<img' + s1 + 'src="' + s2 + '"';
|
||||
var imgSize = s3.split(/[-x×]/);
|
||||
var align = ({'-l': 'left', '-r': 'right', '-c': 'center'})[s4];
|
||||
if (imgSize[0]) {
|
||||
imgHtml += ' width="' + imgSize[0] + '"';
|
||||
}
|
||||
if (imgSize[1]) {
|
||||
imgHtml += ' height="' + imgSize[1] + '"';
|
||||
}
|
||||
if (align) {
|
||||
imgHtml += ' align="' + align + '"';
|
||||
}
|
||||
return imgHtml;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析 markdown 复选框
|
||||
* @param {String} html
|
||||
* @returns {String}
|
||||
* @private
|
||||
*/
|
||||
Docs.prototype._setCheckbox = function (html) {
|
||||
return html.replace(/\[([√×Xx\s\-_])\]\s(.*?)([<\n\r])/g, function (m, s1, s2, s3, index) {
|
||||
var checkboxHtml = '<input type="checkbox" id="checkbox' + index + '"';
|
||||
checkboxHtml += /\s/.test(s1) ? '>' : 'checked="true">';
|
||||
checkboxHtml += '<label for="checkbox' + index + '">' + s2 + '</label>';
|
||||
return checkboxHtml + s3;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析 markdown 文字飘红
|
||||
* @param {String} html
|
||||
* @returns {String}
|
||||
* @private
|
||||
*/
|
||||
Docs.prototype._setRedText = function (html) {
|
||||
return html.replace(/==(.*?)==/g, function (m, s1) {
|
||||
return '<mark>' + s1 + '</mark>';
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建目录
|
||||
* @param {String} contents - markdown 目录
|
||||
* @private
|
||||
*/
|
||||
Docs.prototype._createContents = function (contents) {
|
||||
var $contents = $('.markdown-contents').html(marked.parse(contents));
|
||||
$('blockquote').each(function () {
|
||||
var $this = $(this);
|
||||
var $links = $this.find('ol>li>a');
|
||||
//至少2条链接才视为目录
|
||||
if ($links.length > 1) {
|
||||
$this.addClass('markdown-contents');
|
||||
$contents = $contents.add($this);
|
||||
}
|
||||
});
|
||||
//自带序号的目录,不再额外显示一层序号
|
||||
$contents.find('ol').each(function () {
|
||||
var $this = $(this);
|
||||
var $links = $this.children('li').children('a');
|
||||
var text1 = $links.eq(0).text(),
|
||||
text2 = $links.eq(1).text();
|
||||
var conditions = [
|
||||
//普通数字类型
|
||||
/^[\((]?(\d+\.?)+[^\d]{2,}/,
|
||||
//汉字序号类型
|
||||
/^[\((【第]?[一二三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+/,
|
||||
//英文序号类型
|
||||
/^(chapter)|(section)|(part)|(step)/i,
|
||||
//罗马序号类型
|
||||
/^[ⅠⅡⅢⅣIⅤⅥⅦⅧⅨⅩⅪⅫLCDM]+\.?/
|
||||
];
|
||||
for (var i = 0, cond; cond = conditions[i]; i++) {
|
||||
if (cond.test(text1) && cond.test(text2)) {
|
||||
$this.addClass('unindex');
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
//没写序号的目录,创建序号
|
||||
var setIndex = function ($elm, index1) {
|
||||
if ($elm.length == 0) {
|
||||
return;
|
||||
}
|
||||
$elm.children('li').each(function (index2) {
|
||||
var $this = $(this);
|
||||
if (!$elm.hasClass('unindex')) {
|
||||
var index = typeof index1 == 'number' ? (index1 + 1) + '.' + (index2 + 1): (index2 + 1) + '.';
|
||||
$this.prepend('<i>' + index + '</i>');
|
||||
}
|
||||
setIndex($this.children('ol'), index2);
|
||||
});
|
||||
};
|
||||
$contents.children('ol').each(function () {
|
||||
setIndex($(this));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 编码 url
|
||||
* 由于服务器可能存在 GBK 或 UTF-8 两种编码,中文路径编码不对需要切换才能访问
|
||||
* @param {String} path
|
||||
* @param {String} type - 是否需要编码类型反转, GBK、UTF-8切换
|
||||
* @returns {String}
|
||||
* @private
|
||||
*/
|
||||
Docs.prototype._encodeUrl = function (path, type) {
|
||||
var url = '';
|
||||
if (typeof AWConfig.libraryPrefix == 'string' && AWConfig.libraryPrefix.length > 0) {
|
||||
url = AWConfig.libraryPrefix.replace(/\\/g, '/').replace(/\/?&/, '/');
|
||||
}
|
||||
path = path.replace(/\\/g, '/');
|
||||
var paths = [];
|
||||
//正常编码
|
||||
if (type == 'normal') {
|
||||
if (localStorage[URL_ENCODE_NAME] == 'utf8') {
|
||||
url += encodeURI(path);
|
||||
} else if (localStorage[URL_ENCODE_NAME] == 'gbk') {
|
||||
paths = path.split('/').map(function (path) {
|
||||
return GBK.encode(path);
|
||||
});
|
||||
url += paths.join('/');
|
||||
}
|
||||
}
|
||||
//反转编码
|
||||
else if (type == 'reverse') {
|
||||
if (localStorage[URL_ENCODE_NAME] == 'utf8') {
|
||||
paths = path.split('/').map(function (path) {
|
||||
return GBK.encode(path);
|
||||
});
|
||||
url += paths.join('/');
|
||||
} else if (localStorage[URL_ENCODE_NAME] == 'gbk') {
|
||||
url += encodeURI(path);
|
||||
}
|
||||
}
|
||||
url += '.md?t=' + (new Date()).getTime();
|
||||
if (url.indexOf('undefined.md') !== -1) return null;
|
||||
return url;
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染文档
|
||||
* @param {String} content - 需要渲染的文档内容
|
||||
* @public
|
||||
*/
|
||||
Docs.prototype.renderDoc = function (content) {
|
||||
var that = this;
|
||||
var html = '';
|
||||
this.cleanView();
|
||||
//增加"\"符转义功能
|
||||
/*content = content.replace(/\\(.)/g, function (m, s1) {
|
||||
return '&#' + s1.charCodeAt(0) + ';';
|
||||
});*/
|
||||
//创建脚注
|
||||
content = this._setFootnote(content);
|
||||
//编译 markdown
|
||||
html = marked.parse(content);
|
||||
//创建目录标记,和悬浮窗格式统一
|
||||
html = this._setTOC(html);
|
||||
//自定义图片大小与对齐方式
|
||||
html = this._setImgResize(html);
|
||||
//复选框
|
||||
html = this._setCheckbox(html);
|
||||
//文字飘红
|
||||
html = this._setRedText(html);
|
||||
//填充到页面
|
||||
this.$e.view.html(html);
|
||||
/*this.$e.view.find('pre code').each(function (i, element) {
|
||||
var $elm = $(element);
|
||||
var src = $elm.attr('src') || '';
|
||||
$elm.attr('src', src.replace(/&#(x)?([^&]{1,5});?/g, function (a, b, c) {
|
||||
return String.fromCharCode(parseInt(c, b ? 16 : 10));
|
||||
}));
|
||||
});*/
|
||||
//功能化代码块
|
||||
this.$e.view.find('pre code').each(function (i, element) {
|
||||
var $elm = $(element);
|
||||
var className = $elm.attr('class') || '';
|
||||
//创建流程图
|
||||
if (className.indexOf('lang-flow') >= 0) {
|
||||
that._createFlowChart($elm);
|
||||
}
|
||||
//创建语法高亮
|
||||
else if (className.indexOf('lang') >= 0) {
|
||||
// hljs.highlightElement(element);
|
||||
let nameList = className.split('-');
|
||||
let language;
|
||||
if (nameList.length >= 2) {
|
||||
if (nameList.length >= 3) {
|
||||
nameList[1] += '-' + nameList[2];
|
||||
}
|
||||
language = nameList[1];
|
||||
}
|
||||
if (!['arduino', 'bash', 'c', 'cpp',
|
||||
'css', 'javascript', 'json', 'lua',
|
||||
'node-repl', 'python', 'python-repl', 'shell',
|
||||
'xml'].includes(language)) {
|
||||
language = null;
|
||||
}
|
||||
if (language) {
|
||||
element.innerHTML = hljs.highlight(element.outerText, {
|
||||
language,
|
||||
ignoreIllegals: false
|
||||
}).value;
|
||||
}
|
||||
that._copyCode($elm);
|
||||
}
|
||||
//创建js注释开关
|
||||
className = $elm.attr('class') || '';
|
||||
if (className.indexOf('javascript') >= 0) {
|
||||
that._setJSCommentDisable($elm);
|
||||
}
|
||||
});
|
||||
//设置网页title
|
||||
var title = this.$e.view.find('h1').eq(0).text();
|
||||
// this.$e.title.text(title);
|
||||
this.$e.contentsTitle.text(title).attr('href', '#' + title.replace(/"/g, ''));
|
||||
//创建描点
|
||||
var contents = this._setTitlesAnchor();
|
||||
//创建目录
|
||||
this._createContents(contents);
|
||||
};
|
||||
|
||||
/**
|
||||
* 读取文档
|
||||
* @param {String} url
|
||||
* @param {Function} callback
|
||||
* @public
|
||||
*/
|
||||
Docs.prototype.getDoc = function (url, callback) {
|
||||
var that = this;
|
||||
var ajaxData = {
|
||||
type: 'get',
|
||||
url: url,
|
||||
dataType: 'text'
|
||||
};
|
||||
$.ajax(ajaxData)
|
||||
.done(function (data) {
|
||||
//如果请求响应包含html,视为报错
|
||||
if (/^\s*<!(DOCTYPE|doctype)/.test(data)) {
|
||||
return callback('fail');
|
||||
}
|
||||
//成功回调
|
||||
callback('done', data);
|
||||
})
|
||||
//请求失败
|
||||
.fail(function () {
|
||||
callback('fail');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载指定页面
|
||||
* @param {String} path - 页面资源地址
|
||||
* @param {Function} callback - 加载完成后的回调,包含参数:
|
||||
* type - 加载页面最终状态,success、error 两种
|
||||
* content - 加载页面成功时,传递加载的内容
|
||||
* @public
|
||||
*/
|
||||
Docs.prototype.loadPage = function (path, callback) {
|
||||
//console.log(path);
|
||||
var that = this;
|
||||
var url = this._encodeUrl(path, 'normal');
|
||||
this.getDoc(url, function (type, data) {
|
||||
if (type == 'fail') {
|
||||
//如果第一失败,转换url编码类型后发送第二次请求
|
||||
var url = that._encodeUrl(path, 'reverse');
|
||||
that.getDoc(url, function (type) {
|
||||
//第二次仍然失败,视为打开文档失败
|
||||
if (type == 'fail') {
|
||||
callback && callback('error');
|
||||
}
|
||||
//如果第二次才成功,转换保存的编码类型标记
|
||||
else if (type == 'done') {
|
||||
localStorage[URL_ENCODE_NAME] = localStorage[URL_ENCODE_NAME] == 'utf8' ? 'gbk' : 'utf8';
|
||||
callback && callback('success', data);
|
||||
}
|
||||
});
|
||||
} else if (type == 'done') {
|
||||
callback && callback('success', data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 清理页面
|
||||
* @public
|
||||
*/
|
||||
Docs.prototype.cleanView = function () {
|
||||
this.$e.view.find('.lang-off-js-comment').off('click');
|
||||
this.$e.view.html('');
|
||||
};
|
||||
|
||||
return win.AWDocs = Docs;
|
||||
|
||||
})(window, document, jQuery);
|
||||
Reference in New Issue
Block a user