1款在線的 Markdown 瀏覽器,主要用來展現(xiàn) Markdown 內(nèi)容。支持 HTML 導(dǎo)出,同時(shí)可以方便的添加擴(kuò)大功能。在這個(gè)瀏覽器的基礎(chǔ)又做了1款在線 Github Pages 頁面生成器,可以方便的生成不同主題風(fēng)格的 GitHub Page 頁面。
Prism.js
/ Highlight.js
代碼高亮在上面的基礎(chǔ)上加上了下面的功能
瀏覽器
在線地址 效果預(yù)覽 源碼
生成器
在線地址 效果預(yù)覽 源碼
瀏覽器
生成器
程序使用 marked 將 markdown 格式轉(zhuǎn)為 html 格式,這是1個(gè) js 的庫,可以直接在閱讀器端使用。下面是1個(gè)基本的示例
var htmlContent = marked(mdContent);
$("#content").html(htmlContent);
同時(shí) marked 提供了1些接口,讓我們可以方便的定制自己的功能。具體的可以參考它的 說明文件 。在下面我們會(huì)介紹我們是如何利用這些接口來實(shí)現(xiàn)擴(kuò)大功能。
原始的上傳按鈕太丑了,所以我們需要自定義自己的樣式。這里使用的方式是使用在 input
上面覆蓋1個(gè) button
,用 button
來顯示樣式。同時(shí)我們將 button
的 pointer-events
設(shè)為 none
,就能夠禁止 button
的事件響應(yīng)(具體可以參考這里)。下面是具體的實(shí)現(xiàn)代碼:
html:
<div class="upload-area" id="upload-area">
<input type="file" id="select-file" class="select-file">
<button class="select-file-style" id="drop">選擇或拖拽 Markdown 文件到此</button>
</div>
css
.upload-area {
width: auto;
height: 200px;
margin: 0 2.6em 0 0.4em;
padding: 0;
position: relative;
cursor: pointer;
transition: height 0.5s;
}
.upload-area .select-file {
border-width: 0px;
width: 100%;
height: 200px;
margin: 0;
cursor: pointer;
}
.upload-area .select-file-style {
background: #F5F7FA;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 200px;
border: 0px;
pointer-events: none;
color: #AAB2BD;
font-size: 2em;
line-height: 2em;
font-family: "Microsoft YaHei", "Tahoma", arial;
}
下面是效果圖
由于程序完全是運(yùn)行在閱讀器端,所以我們使用 html5 的 FileReader
來讀取本地文件。FileReader
提供 4 種讀取文件的方式:
readAsBinaryString(Blob|File)
readAsText(Blob|File, opt_encoding)
readAsDataURL(Blob|File)
readAsArrayBuffer(Blob|File)
其中 readAsText
用來讀取文本文件,readAsDataUrl
可以用來讀取圖片。具體的介紹可以參考 這里 。FileReader
1般結(jié)合文件選擇事件或拖拽事件使用,由于通過這兩個(gè)事件可以取得源文件。另外 FileReader
是異步讀取的,通過 onload
事件可以監(jiān)聽文件是不是讀取終了。下面是1個(gè)示例, 通過點(diǎn)擊 <input type= "file">
選擇文件,然后讀取文件內(nèi)容。
document.getElementById("file-select").addEventListener("change", function(e) {
e.stopPropagation();
e.preventDefault();
var reader = new FileReader();
reader.readAsText(this.files[0]);
reader.onload = function (e) {
var content = e.target.result;
//......
};
}, false);
為了方便用戶操作,我們提供了點(diǎn)擊和拖拽兩種方式來上傳文件。現(xiàn)在的主流閱讀器都支持文件拖拽功能,下面是拖拽進(jìn)程中觸發(fā)的事件
事件 | 描寫 |
---|---|
dragstart | 用戶開始拖動(dòng)對象時(shí)觸發(fā)。 |
dragenter | 鼠標(biāo)初次移到目標(biāo)元素并且正在進(jìn)行拖動(dòng)時(shí)觸發(fā)。這個(gè)事件的監(jiān)聽器應(yīng)當(dāng)之指出這個(gè)位置是不是允許放置元素。如果沒有監(jiān)聽器或監(jiān)聽器不履行任何操作,默許情況下不允許放置。 |
dragover | 拖動(dòng)時(shí)鼠標(biāo)移到某個(gè)元素上的時(shí)候觸發(fā)。 |
dragleave | 拖動(dòng)時(shí)鼠標(biāo)離開某個(gè)元素的時(shí)候觸發(fā)。 |
drag | 對象被拖拽時(shí)每次鼠標(biāo)移動(dòng)都會(huì)觸發(fā)。 |
drop | 拖動(dòng)操作結(jié)束,放置元素時(shí)觸發(fā)。 |
dragend | 拖動(dòng)對象時(shí)用戶釋放鼠標(biāo)按鍵的時(shí)候觸發(fā)。 |
另外在拖拽進(jìn)程中是不觸發(fā)鼠標(biāo)事件的。文件讀取完后文件信息會(huì)保存在 DataTransfer
對象中。詳細(xì)的介紹可以參考 這里 。下面是添加事件的示例
fileSelect.addEventListener("dragenter", dragMdEnter, false);
fileSelect.addEventListener("dragleave", dragMdLeave, false);
fileSelect.addEventListener('drop', dropMdFile, false);
讀取拖拽的文件
function dropMdFile(e) {
// 取消閱讀器默許行動(dòng)
e.stopPropagation();
e.preventDefault();
var reader = new FileReader();
reader.readAsText(e.dataTransfer.files[0]);
reader.onload = function (e) {
var content = e.target.result;
//......
};
}
由于沒有服務(wù)器,所以為了顯示本地圖片,使用了替換圖片 src
的方式。首先讀取本地文件,然后將 <img>
的 src
路徑替換為圖片內(nèi)容 。以下所示:
<img src="path">
// 替換為
<img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgI...">
下面是具體的代碼實(shí)現(xiàn):
// 讀取選擇或拖拽的文件(多個(gè)文件)
function processImages(imgFiles) {
var index = 0;
for (i = 0; i < imgFiles.length; i++) {
var file = imgFiles[i];
var reader = new FileReader();
reader.readAsDataURL(file);
(function (reader, file) {
reader.onload = function (e) {
cacheImages[file.name] = e.target.result;
index++;
if (index == length) {
replaceImage();
}
}
})(reader, file);
}
}
// 將路徑替換為圖片內(nèi)容
function replaceImage() {
var images = $("img");
var i;
for (i = 0; i < images.length; i++) {
var imgSrc = images[i].src;
var imgName = getImgName(imgSrc);
if (cacheImages.hasOwnProperty(imgName)) {
images[i].src = cacheImages[imgName];
}
}
}
如果圖片過大,我們可以將圖片緊縮1下,具體方法就是創(chuàng)建1個(gè) canvas
元素,將圖片繪制到 canvas
上,然后將 canvas
轉(zhuǎn)為圖片。這類方式對 jpg
文件緊縮效果較好,對 png
文件緊縮效果不太好。下面是代碼實(shí)現(xiàn):
function compressImage(img, format) {
var max_width = 862;
var canvas = document.createElement('canvas');
var width = img.width;
var height = img.height;
if (format == null || format == "") {
format = "image/png";
}
if (width > max_width) {
height = Math.round(height *= max_width / width);
width = max_width;
}
// resize the canvas and draw the image data into it
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
return canvas.toDataURL(format);
}
為了方便使用,我們可以同時(shí)上傳多個(gè)圖片,我們使用 for
循環(huán)來讀取多個(gè)文件,但是有個(gè)問題是文件的讀取是異步的,也就是說在 for
循環(huán)履行完以后,圖片可能仍在讀取中,當(dāng)圖片讀取完后,再調(diào)用 onload
回調(diào)函數(shù)進(jìn)行處理。簡單1點(diǎn)就是說如何在 for
循環(huán)中正確使用延遲調(diào)用的回調(diào)函數(shù)。看下面的例子:
function print(value, callback) {
console.log("value in print", value);
setTimeout(callback, 1000);
}
for(var i = 0; i < 4; i++) {
var value = i;
print(value, function() {
console.log("value in callback", value);
});
}
上面打的代碼和我們讀取圖片文件的邏輯類似,callback
函數(shù)會(huì)在調(diào)用 print
函數(shù)1秒后履行,下面是輸出結(jié)果
value in print 0
value in print 1
value in print 2
value in print 3
value in callback 3
value in callback 3
value in callback 3
value in callback 3
最后在 callback
中 value
值都是3,這是由于在 js 中沒有塊級(jí)作用域,只有函數(shù)作用域,也就是說下面的兩段代碼是同等的:
for(var i = 0; i < 4; i++) {
var value = i;
// do someting
}
// 同等于
var value;
for(var i = 0; i < 4; i++) {
value = i;
// do someting
}
因此,為了解決這個(gè)問題,我們只需要為循環(huán)中的回調(diào)函數(shù)添加1個(gè)單獨(dú)的作用域便可,我們使用閉包來實(shí)現(xiàn):
for(var i = 0; i < 4; i++) {
var value = i;
(function(value) {
print(value, function() {
console.log("value in callback", value);
});
}(value));
}
我們使用兩款代碼高亮插件 – highlight.js 和 prism.js,根據(jù)喜好可以自由切換。這兩款插件對代碼塊的 html 格式有不同的要求,我們重寫了 marked
中解析代碼塊的方法,根據(jù)高亮方式來生成不同的 html 代碼:
renderer.code = function (code, lang) {
if (Setting.highlight == Constants.highlight) {
return "<pre><code class='" + lang + "'>" + code + "</code></pre>";
}
return "<pre><code class='language-" + lang + "'>" + code + "</code></pre>";
};
然后調(diào)用 highlight.js 和 prism.js 的代碼高亮方法便可
if (Setting.highlight == Constants.highlight) {
$('pre code').each(function (i, block) {
hljs.highlightBlock(block);
});
} else {
// 添加行號(hào)支持
$("pre").addClass("line-numbers");
Prism.highlightAll();
}
為了生成文件的目錄,我們需要首先取得目錄信息,因此我們重寫 marked
的 heading
方法, 將目錄信息保存起來,同時(shí)為每一個(gè)標(biāo)題添加鏈接圖標(biāo)(仿照 github),下面是代碼:
renderer.heading = function (text, level) {
var slug = text.toLowerCase().replace(/[\s]+/g, '-');
if (tocStr.indexOf(slug) != -1) {
slug += "-" + tocDumpIndex;
tocDumpIndex++;
}
tocStr += slug;
toc.push({
level: level,
slug: slug,
title: text
});
return "<h" + level + " id=\"" + slug + "\"><a href=\"#" + slug + "\" class=\"anchor\">" + '' +
'<svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c⑴.5 0⑶⑴.69⑶⑶.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72⑵ 3.25V8.59c.58-.45 1⑴.27 1⑵.09C10 5.22 8.98 4 8 4H4c-.98 0⑵ 1.22⑵ 2.5S3 9 4 9zm9⑶h⑴v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0⑵⑴.22⑵⑵.5 0-.83.42⑴.64 1⑵.09V6.25c⑴.09.53⑵ 1.84⑵ 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3⑴.69 3⑶.5S14.5 6 13 6z"></path></svg>' +
'' + "</a>" + text + "</h" + level + ">";
};
同時(shí)需要加入下面的 css,以是標(biāo)題的鏈接圖片正常顯示:
h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, h4:hover .anchor, h5:hover .anchor, h6:hover .anchor {
text-decoration: none
}
h1:hover .anchor .octicon-link, h2:hover .anchor .octicon-link, h3:hover .anchor .octicon-link, h4:hover .anchor .octicon-link, h5:hover .anchor .octicon-link, h6:hover .anchor .octicon-link {
visibility: visible
}
.octicon {
display: inline-block;
vertical-align: text-top;
fill: currentColor;
}
.anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
}
為了生成目錄,我們只需依照保存的目錄信息,生成 <ul>
和 <li>
標(biāo)簽便可,具體的可以參考源碼中的實(shí)現(xiàn)。
目錄使用的是頁內(nèi)錨鏈接的方式進(jìn)行跳轉(zhuǎn),以下面所示:
<a href="#h1">跳轉(zhuǎn)到 H1</a>
...
<h1 id="h1">我是 H1</h1>
...
默許情況下,頁內(nèi)錨鏈接跳轉(zhuǎn)以后,目標(biāo)標(biāo)簽(上面代碼中的 <h1>
)會(huì)移動(dòng)到頁面的最頂部,但是在我們的程序中有1個(gè)固定的 header,如果跳轉(zhuǎn)到最頂部,目標(biāo)標(biāo)簽會(huì)被 header 遮擋住,所以我們希望目標(biāo)標(biāo)簽移動(dòng)到距離頁面頂部 header-height
的地方。為了實(shí)現(xiàn)我們的需要,只要加入下面的 css 代碼便可。
:target:before {
content:"";
display:block;
height:50px; /* fixed header height*/
margin:-50px 0 0; /* negative fixed header height */
}
Todo 列表實(shí)際上就是 checkbox 的列表,完成的工作用選中的 checkbox 表示,未完成的工作用喂選中的列表表示,以下圖所示:
1般來講,會(huì)將下面情勢的 markdown 代碼解析為 todo 列表
- [x] 完成
- [ ] 未完成
- [ ] 未完成
為了實(shí)現(xiàn)這個(gè)功能,我們重寫 marked
中解析列表的方法,加入對 todo 列表的支持。
renderer.listitem = function (text) {
if (/^\s*\[[x ]\]\s*/.test(text)) {
text = text
.replace(/^\s*\[ \]\s*/, '<input type="checkbox" class="task-list-item-checkbox" disabled> ')
.replace(/^\s*\[x\]\s*/, '<input type="checkbox" class="task-list-item-checkbox" disabled checked> ');
return '<li style="list-style: none">' + text + '</li>';
} else {
return '<li>' + text + '</li>';
}
};
同時(shí)加入下面的樣式:
.task-list-item-checkbox {
margin: 0 0.2em 0.25em -2.3em;
vertical-align: middle;
}
[type="checkbox"], [type="radio"] {
box-sizing: border-box;
padding: 0;
}
現(xiàn)在的閱讀器都已支持 localStorage
,可以方便的存儲(chǔ)數(shù)據(jù)。localStorage
就是1個(gè)對象。我們存儲(chǔ)數(shù)據(jù)就是直接給它添加1個(gè)屬性,可以通過 localStoage["a"]=1
或 localStorage.a = 1
的方式來存儲(chǔ)數(shù)據(jù),但是看起來總覺的不太優(yōu)雅,由于1般使用下面的方式來操作 localStorage
:
localStorage.setItem(key, vlaue);
localStorage.getItem(key);
localStorage.removeItem(key);
另外 localStorage
也有1些局限,使用時(shí)需要注意:
5M
左右,和閱讀器有關(guān)JSON.stringfy
方法將對象進(jìn)行序列化處理以后再保存。使用時(shí)需要使用 JSON.parse
方法將字符串轉(zhuǎn)為對象。通過使用 FileSaver.js,我們可以方便的在閱讀器端生成文件,并提供給用戶下載。使用方法也很簡單:
var blob = new Blob([htmlContent], {type: "text/html;charset=utf⑻"});
saveAs(blob, name);
我們提供了1些擴(kuò)大功能,用來更好的展現(xiàn) markdown 內(nèi)容。在現(xiàn)在的程序中我們可以很方便的添加擴(kuò)大功能,下面會(huì)具體介紹。
為了添加擴(kuò)大,我們首先需要肯定哪些內(nèi)容需要作為擴(kuò)大處理。由于在將 markdown 文件轉(zhuǎn)為 html 的進(jìn)程中,1般是不處理代碼塊中的內(nèi)容的,所以我們使用代碼塊來寄存擴(kuò)大內(nèi)容,通過代碼塊的語言來肯定是哪一種擴(kuò)大。以添加序列圖擴(kuò)大為例:
肯定時(shí)序圖的代碼標(biāo)記
修改 marked
中對代碼塊的解析函數(shù),添加對時(shí)序圖標(biāo)記的支持
var renderer = new marked.Renderer();
var originalCodeFun = function (code, lang) {
if (Setting.highlight == Constants.highlight) {
return "<pre><code class='" + lang + "'>" + code + "</code></pre>";
}
return "<pre><code class='language-" + lang + "'>" + code + "</code></pre>";
};
renderer.code = function (code, language) {
if (language == "seq") {
return "<div class='diagram' id='diagram'>" + code + "</div>"
} else {
return originalCodeFun.call(this, code, language);
}
};
marked.setOptions({
renderer: renderer
});
js-sequence-diagrams
相干文件<link href="{{ bower directory }}/js-sequence-diagrams/dist/sequence-diagram-min.css" rel="stylesheet" />
<script src="{{ bower directory }}/bower-webfontloader/webfont.js" />
<script src="{{ bower directory }}/snap.svg/dist/snap.svg-min.js" />
<script src="{{ bower directory }}/underscore/underscore-min.js" />
<script src="{{ bower directory }}/js-sequence-diagrams/dist/sequence-diagram-min.js" />
$(".diagram").sequenceDiagram({theme: 'simple'});
添加擴(kuò)大會(huì)影響文件的渲染速度,如果不需要某個(gè)擴(kuò)大可以手動(dòng)關(guān)閉。
使用Mathjax 對數(shù)學(xué)公式進(jìn)行支持。關(guān)于Mathjax 語法,請參考這里。下面是添加擴(kuò)大的流程:
<script type="text/x-mathjax-config">
MathJax.Hub.Config({tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']]},
TeX: {
equationNumbers: {
autoNumber: ["AMS"],
useLabelIds: true
}
},
"HTML-CSS": {
linebreaks: {
automatic: true
}
},
SVG: {
linebreaks: {
automatic: true
}
}
});
</script>
<script type="text/javascript" src="http://cdn.bootcss.com/mathjax/2.7.0/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
// content 是需要處理的 html 標(biāo)簽的 id
MathJax.Hub.Queue(["Typeset", MathJax.Hub, "content"]);
使用 emojify.js 來提供對 Emoji 標(biāo)簽的支持。Emoji表情參見 EMOJI CHEAT SHEET。下面是添加擴(kuò)大的流程
<script src="http://cdn.bootcss.com/emojify.js/1.1.0/js/emojify.min.js"></script>
<script type="text/javascript">
emojify.setConfig({
emojify_tag_type: 'div', // Only run emojify.js on this element
only_crawl_id: null, // Use to restrict where emojify.js applies
img_dir: 'http://cdn.bootcss.com/emojify.js/1.0/images/basic', // Directory for emoji images
ignored_tags: { // Ignore the following tags
'SCRIPT': 1,
'TEXTAREA': 1,
'A': 1,
'PRE': 1,
'CODE': 1
}
});
</script>
emojify.run(document.getElementById('content'))
使用 ECharts 來提供對圖表的支持。ECharts 的語法可以參考 官網(wǎng)的示例。下面是使用方法:
肯定 ECharts 在 markdown 中的語法標(biāo)簽
在 code 方法解析中添加對 echarts 的支持
renderer.code = function (code, language) {
switch (language) {
case "echarts":
if (Setting.echarts) {
return loadEcharts(code);
}
return originalCodeFun.call(this, code, language);
}
};
function loadEcharts(text) {
var width = "100%";
var height = "400px";
try {
var options = eval("(" + text + ")");
if (options.hasOwnProperty("width")) {
width = options["width"];
}
if (options.hasOwnProperty("height")) {
height = options["height"];
}
echartIndex++;
echartData.push({
id: echartIndex,
option: options,
previousOption: text
});
return '<div id="echarts-' + echartIndex + '" style="width: ' + width + ';height:' + height + ';"></div>'
} catch (e) {
console.log(e);
return "";
}
}
var chart;
echartData.forEach(function (data) {
if (data.option.theme) {
chart = echarts.init(document.getElementById('echarts-' + data.id), data.option.theme);
} else {
chart = echarts.init(document.getElementById('echarts-' + data.id));
}
chart.setOption(data.option);
});
在生成Github Page頁面時(shí),我們可以選擇添加 多說 或 Disqus 評(píng)論,其中多說就是在導(dǎo)出的頁面中加入下面的代碼
<div class="ds-thread" data-thread-key="" data-title="" data-url=""></div>
<script type="text/javascript">
var duoshuoQuery = {
short_name: ""
};
(function() {
var ds = document.createElement("script");
ds.type = "text/javascript";
ds.async = true;
ds.src = (document.location.protocol == "https:" ? "https:" : "http:") + "http://static.duoshuo.com/embed.js";
ds.charset = "UTF⑻";
(document.getElementsByTagName("head")[0] || document.getElementsByTagName("body")[0]).appendChild(ds);
})();
</script>
其中 data-thread-key
, data-title
, data-url
和 short_name
是需要我們自定義的東西。而Disqus 需要在導(dǎo)出時(shí)插入下面的代碼:
<div id="disqus_thread"></div>
<script type="text/javascript">
var disqus_shortname = '';
var prefix = document.location.protocol == "https:" ? "https:" : "http:"
var disqus_config = function() {
this.page.url = "";
this.page.identifier = ""
};
(function() {
var d = document,
s = d.createElement('script');
s.src = prefix + '//' + disqus_shortname + '.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
其中 disqus_shortname
, page.url
和 page.indertifier
是需要我們自定義的東西。這里需要注意的是 page.url
要使用絕對路徑。
具體的插入邏輯可參考源碼的實(shí)現(xiàn),這里不再贅述。
上一篇 Java多態(tài)性理解