robot_wiki/amWiki/js/amWiki.docs.js

540 lines
18 KiB
JavaScript
Raw Normal View History

2024-09-12 11:24:29 +08:00
/**
* 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, '&#39;') //转义单引号
.replace(/"/g, '&#34;') //转义双引号由于双引号无法正确传递给html属性当作为hash时将删除处理
.replace(/\(/g, '&#40;') //转义左圆括号
.replace(/\)/g, '&#41;') //转义右圆括号
.replace(/\[/g, '&#91;') //转义左中括号
.replace(/\]/g, '&#93;'); //转义右中括号
};
/**
* 设置文档h1h2h3描记
* @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(/&#34;/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. &#12288;\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._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(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 - 是否需要编码类型反转 GBKUTF-8切换
* @returns {String}
* @private
*/
Docs.prototype._encodeUrl = function (path, type) {
var url = 'library/';
if (typeof AWConfig.libraryPrefix == 'string' && AWConfig.libraryPrefix.length > 0) {
url = AWConfig.libraryPrefix.replace(/\\/g, '/').replace(/\/?&/, '/');
}
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();
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(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 className = $elm.attr('class') || '';
//创建流程图
if (className.indexOf('lang-flow') >= 0) {
that._createFlowChart($elm);
}
//创建语法高亮
else if (className.indexOf('lang') >= 0) {
hljs.highlightBlock(element);
}
//创建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 - 加载页面最终状态successerror 两种
* 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);