【應用】Markdown 在線閱讀器

前言

一款在線的 Markdown 閱讀器,主要用來展現 Markdown 內容。支持 HTML 導出,同時能夠方便的添加擴展功能。在這個閱讀器的基礎又作了一款在線 Github Pages 頁面生成器,能夠方便的生成不一樣主題風格的 GitHub Page 頁面。javascript

功能

閱讀器

Github Page 生成器

在上面的基礎上加上了下面的功能css

地址

閱讀器
在線地址  效果預覽  源碼html

生成器
在線地址  效果預覽  源碼html5

效果

閱讀器java

生成器css3

實現

文件解析

程序使用 marked 將 markdown 格式轉爲 html 格式,這是一個 js 的庫,能夠直接在瀏覽器端使用。下面是一個基本的示例git

var htmlContent = marked(mdContent);
$("#content").html(htmlContent);

同時 marked 提供了一些接口,讓咱們能夠方便的定製本身的功能。具體的能夠參考它的 說明文件 。在下面咱們會介紹咱們是如何利用這些接口來實現擴展功能。github

文件上傳

自定義上傳按鈕樣式

原始的上傳按鈕太醜了,因此咱們須要自定義本身的樣式。這裏使用的方式是使用在 input 上面覆蓋一個 button,用 button 來顯示樣式。同時咱們將 buttonpointer-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

最後在 callbackvalue 值都是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.jsprism.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();
}

目錄

爲了生成文件的目錄,咱們須要首先得到目錄信息,所以咱們重寫 markedheading 方法, 將目錄信息保存起來,同時爲每一個標題添加連接圖標(仿照 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 列表

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" />
  • 渲染 Markdown 文件時,調用相關函數
$(".diagram").sequenceDiagram({theme: 'simple'});

添加擴展會影響文件的渲染速度,若是不須要某個擴展能夠手動關閉。

Mathjax

使用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>
  • 將 markdown 文件轉爲 html 以後,調用 Mathjax 中的方法將對應標記轉爲數學公式。
// content 是須要處理的 html 標籤的 id
MathJax.Hub.Queue(["Typeset", MathJax.Hub, "content"]);

Emoji

使用 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>
  • 將 markdown 文件轉爲 html 以後,調用 emojify 中的方法將對應標記轉換 emoji 表情。
emojify.run(document.getElementById('content'))

圖表 (ECharts)

使用 ECharts 來提供對圖表的支持。ECharts 的語法能夠參考 官網的示例。下面是使用方法:

  • 肯定 ECharts 在 markdown 中的語法標籤

  • 在 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 "";
    }
}
  • 將 markdown 文件轉爲 html 以後,調用 echarts 中的方法,將對應的 div 轉爲圖表:
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-urlshort_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.urlpage.indertifier 是須要咱們自定義的東西。這裏須要注意的是 page.url 要使用絕對路徑。

具體的插入邏輯可參考源碼的實現,這裏再也不贅述。

相關文章
相關標籤/搜索