一款在線的 Markdown 閱讀器,主要用來展現 Markdown 內容。支持 HTML 導出,同時能夠方便的添加擴展功能。在這個閱讀器的基礎又作了一款在線 Github Pages 頁面生成器,能夠方便的生成不一樣主題風格的 GitHub Page 頁面。javascript
Prism.js
/ Highlight.js
代碼高亮在上面的基礎上加上了下面的功能css
閱讀器java
生成器css3
程序使用 marked 將 markdown 格式轉爲 html 格式,這是一個 js 的庫,能夠直接在瀏覽器端使用。下面是一個基本的示例git
var htmlContent = marked(mdContent); $("#content").html(htmlContent);
同時 marked 提供了一些接口,讓咱們能夠方便的定製本身的功能。具體的能夠參考它的 說明文件 。在下面咱們會介紹咱們是如何利用這些接口來實現擴展功能。github
原始的上傳按鈕太醜了,因此咱們須要自定義本身的樣式。這裏使用的方式是使用在 input
上面覆蓋一個 button
,用 button
來顯示樣式。同時咱們將 button
的 pointer-events
設爲 none
,就能夠阻止 button
的事件響應(具體能夠參考這裏)。下面是具體的實現代碼:
html:web
<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>
csscanvas
.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; }
下面是效果圖
由於程序徹底是運行在瀏覽器端,因此咱們使用 html5 的 FileReader
來讀取本地文件。FileReader
提供 4 種讀取文件的方式
readAsBinaryString(Blob|File)
readAsText(Blob|File, opt_encoding)
readAsDataURL(Blob|File)
readAsArrayBuffer(Blob|File)
其中 readAsText
用來讀取文本文件,readAsDataUrl
能夠用來讀取圖片。具體的介紹能夠參考 這裏 。FileReader
通常結合文件選擇事件或者拖拽事件使用,由於經過這兩個事件能夠得到源文件。另外 FileReader
是異步讀取的,經過 onload
事件能夠監聽文件是否讀取完畢。下面是一個示例, 經過點擊 <input type= "file">
選擇文件,而後讀取文件內容。
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);
爲了方便用戶操做,咱們提供了點擊和拖拽兩種方式來上傳文件。如今的主流瀏覽器都支持文件拖拽功能,下面是拖拽過程當中觸發的事件
事件 | 描述 |
---|---|
dragstart | 用戶開始拖動對象時觸發。 |
dragenter | 鼠標初次移到目標元素而且正在進行拖動時觸發。這個事件的監聽器應該之指出這個位置是否容許放置元素。若是沒有監聽器或者監聽器不執行任何操做,默認狀況下不容許放置。 |
dragover | 拖動時鼠標移到某個元素上的時候觸發。 |
dragleave | 拖動時鼠標離開某個元素的時候觸發。 |
drag | 對象被拖拽時每次鼠標移動都會觸發。 |
drop | 拖動操做結束,放置元素時觸發。 |
dragend | 拖動對象時用戶釋放鼠標按鍵的時候觸發。 |
另外在拖拽過程當中是不觸發鼠標事件的。文件讀取完後文件信息會保存在 DataTransfer
對象中。詳細的介紹能夠參考 這裏 。下面是添加事件的示例
fileSelect.addEventListener("dragenter", dragMdEnter, false); fileSelect.addEventListener("dragleave", dragMdLeave, false); fileSelect.addEventListener('drop', dropMdFile, false);
讀取拖拽的文件
function dropMdFile(e) { // 取消瀏覽器默認行爲 e.stopPropagation(); e.preventDefault(); var reader = new FileReader(); reader.readAsText(e.dataTransfer.files[0]); reader.onload = function (e) { var content = e.target.result; //...... }; }
由於沒有服務器,因此爲了顯示本地圖片,使用了替換圖片 src
的方式。首先讀取本地文件,而後將 <img>
的 src
路徑替換爲圖片內容 。以下所示:
<img src="path"> // 替換爲 <img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgI...">
下面是具體的代碼實現:
// 讀取選擇或者拖拽的文件(多個文件) 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); } } // 將路徑替換爲圖片內容 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]; } } }
若是圖片過大,咱們能夠將圖片壓縮一下,具體方法就是建立一個 canvas
元素,將圖片繪製到 canvas
上,而後將 canvas
轉爲圖片。這種方式對 jpg
文件壓縮效果較好,對 png
文件壓縮效果不太好。下面是代碼實現:
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); }
爲了方便使用,咱們能夠同時上傳多個圖片,咱們使用 for
循環來讀取多個文件,可是有個問題是文件的讀取是異步的,也就是說在 for
循環執行完以後,圖片可能仍在讀取中,當圖片讀取完後,再調用 onload
回調函數進行處理。簡單一點就是說如何在 for
循環中正確使用延遲調用的回調函數。看下面的例子:
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
函數會在調用 print
函數1秒後執行,下面是輸出結果
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 中沒有塊級做用域,只有函數做用域,也就是說下面的兩段代碼是等同的:
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 }
所以,爲了解決這個問題,咱們只須要爲循環中的回調函數添加一個單獨的做用域便可,咱們使用閉包來實現:
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,根據喜愛能夠自由切換。這兩款插件對代碼塊的 html 格式有不一樣的要求,咱們重寫了 marked
中解析代碼塊的方法,根據高亮方式來生成不一樣的 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>"; };
而後調用 highlight.js 和 prism.js 的代碼高亮方法便可
if (Setting.highlight == Constants.highlight) { $('pre code').each(function (i, block) { hljs.highlightBlock(block); }); } else { // 添加行號支持 $("pre").addClass("line-numbers"); Prism.highlightAll(); }
爲了生成文件的目錄,咱們須要首先得到目錄信息,所以咱們重寫 marked
的 heading
方法, 將目錄信息保存起來,同時爲每一個標題添加連接圖標(仿照 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-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>' + '' + "</a>" + text + "</h" + level + ">"; };
同時須要加入下面的 css,以是標題的連接圖片正常顯示:
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>
標籤便可,具體的能夠參考源碼中的實現。
目錄使用的是頁內錨連接的方式進行跳轉,以下面所示:
<a href="#h1">跳轉到 H1</a> ... <h1 id="h1">我是 H1</h1> ...
默認狀況下,頁內錨連接跳轉以後,目標標籤(上面代碼中的 <h1>
)會移動到頁面的最頂部,可是在咱們的程序中有一個固定的 header,若是跳轉到最頂部,目標標籤會被 header 遮擋住,因此咱們但願目標標籤移動到距離頁面頂部 header-height
的地方。爲了實現咱們的須要,只要加入下面的 css 代碼便可。
:target:before { content:""; display:block; height:50px; /* fixed header height*/ margin:-50px 0 0; /* negative fixed header height */ }
Todo 列表實際上就是 checkbox 的列表,完成的工做用選中的 checkbox 表示,未完成的工做用喂選中的列表表示,以下圖所示:
通常來講,會將下面形式的 markdown 代碼解析爲 todo 列表
- [x] 完成 - [ ] 未完成 - [ ] 未完成
爲了實現這個功能,咱們重寫 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>'; } };
同時加入下面的樣式:
.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; }
如今的瀏覽器都已經支持 localStorage
,能夠方便的存儲數據。localStorage
就是一個對象。咱們存儲數據就是直接給它添加一個屬性,能夠經過 localStoage["a"]=1
或者 localStorage.a = 1
的方式來存儲數據,可是看起來總覺的不太優雅,由於通常使用下面的方式來操做 localStorage
:
localStorage.setItem(key, vlaue); localStorage.getItem(key); localStorage.removeItem(key);
另外 localStorage
也有一些侷限,使用時須要注意:
5M
左右,和瀏覽器有關JSON.stringfy
方法將對象進行序列化處理以後再保存。使用時須要使用 JSON.parse
方法將字符串轉爲對象。經過使用 FileSaver.js,咱們能夠方便的在瀏覽器端生成文件,並提供給用戶下載。使用方法也很簡單:
var blob = new Blob([htmlContent], {type: "text/html;charset=utf-8"}); saveAs(blob, name);
咱們提供了一些擴展功能,用來更好的展現 markdown 內容。在如今的程序中咱們能夠很方便的添加擴展功能,下面會具體介紹。
爲了添加擴展,咱們首先須要肯定哪些內容須要做爲擴展處理。由於在將 markdown 文件轉爲 html 的過程當中,通常是不處理代碼塊中的內容的,因此咱們使用代碼塊來存放擴展內容,經過代碼塊的語言來肯定是哪一種擴展。以添加序列圖擴展爲例:
肯定時序圖的代碼標記
marked
中對於代碼塊的解析函數,添加對於時序圖標記的支持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'});
添加擴展會影響文件的渲染速度,若是不須要某個擴展能夠手動關閉。
使用Mathjax 對數學公式進行支持。關於Mathjax 語法,請參考這裏。下面是添加擴展的流程:
<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 標籤的 id MathJax.Hub.Queue(["Typeset", MathJax.Hub, "content"]);
使用 emojify.js 來提供對 Emoji 標籤的支持。Emoji表情參見 EMOJI CHEAT SHEET。下面是添加擴展的流程
<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 的語法能夠參考 官網的示例。下面是使用方法:
肯定 ECharts 在 markdown 中的語法標籤
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頁面時,咱們能夠選擇添加 多說 或者 Disqus 評論,其中多說就是在導出的頁面中加入下面的代碼
<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:") + "//static.duoshuo.com/embed.js"; ds.charset = "UTF-8"; (document.getElementsByTagName("head")[0] || document.getElementsByTagName("body")[0]).appendChild(ds); })(); </script>
其中 data-thread-key
, data-title
, data-url
和 short_name
是須要咱們自定義的東西。而Disqus 須要在導出時插入下面的代碼:
<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
要使用絕對路徑。
具體的插入邏輯可參考源碼的實現,這裏再也不贅述。