【应用】Markdown 在线阅读器

【应用】Markdown 在线阅读器,第1张

概述前言一款在线的 Markdown 阅读器,主要用来展示 Markdown 内容。支持 HTML 导出,同时可以方便的添加扩展功能。在这个阅读器的基础又做了一款在线 Github Pages 页面生成器,可以方便的生成不同主题风格的 GitHub Page 页面。功能阅读器支持文件拖拽兼容移动端Pri 前言

1款在线的 Markdown 浏览器,主要用来展现 Markdown 内容。支持 HTML 导出,同时可以方便的添加扩大功能。在这个浏览器的基础又做了1款在线 Github Pages 页面生成器,可以方便的生成不同主题风格的 GitHub Page 页面。

功能浏览器支持文件拖拽兼容移动端Prism.Js / Highlight.Js 代码高亮自动生成目录本地图片显示导出 HTML (包括样式)扩大功能
Toto 列表MathJax 时序图 (Js sequence diagrams)Emoji (Emojify.Js)图表 (ECharts)Github Page 生成器

在上面的基础上加上了下面的功能

支持多种页面主题
ArchitectCaymanMinimalModernistSlateTime machine评论
多说disqus地址

浏览器
在线地址  效果预览  源码

生成器
在线地址  效果预览  源码

效果

浏览器

生成器

实现文件解析

程序使用 marked 将 markdown 格式转为 HTML 格式,这是1个 Js 的库,可以直接在阅读器端使用。下面是1个基本的示例

var HTMLContent = marked(mdContent);$("#content").HTML(HTMLContent);

同时 marked 提供了1些接口,让我们可以方便的定制自己的功能。具体的可以参考它的 说明文件 。在下面我们会介绍我们是如何利用这些接口来实现扩大功能。

文件上传自定义上传按钮样式

原始的上传按钮太丑了,所以我们需要自定义自己的样式。这里使用的方式是使用在 input 上面覆盖1个 button,用 button 来显示样式。同时我们将 buttonpointer-events 设为 none,就能够禁止 button 的事件响应(具体可以参考这里)。下面是具体的实现代码:
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;}

下面是效果图

@H_265_419@读取文件内容

由于程序完全是运行在阅读器端,所以我们使用 HTML5 的 fileReader 来读取本地文件。fileReader 提供 4 种读取文件的方式:

readAsBinaryString(Blob|file)readAsText(Blob|file,opt_enCoding)readAsDataURL(Blob|file)readAsArrayBuffer(Blob|file)

其中 readAsText 用来读取文本文件,readAsDataUrl 可以用来读取图片。具体的介绍可以参考 这里 。fileReader 1般结合文件选择事件或拖拽事件使用,由于通过这两个事件可以取得源文件。另外 fileReader 是异步读取的,通过 onload 事件可以监听文件是不是读取终了。下面是1个示例,通过点击 <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];        }    }}

如果图片过大,我们可以将图片紧缩1下,具体方法就是创建1个 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,wIDth,height);    return canvas.toDataURL(format);}
循环中使用异步回调函数

为了方便使用,我们可以同时上传多个图片,我们使用 for 循环来读取多个文件,但是有个问题是文件的读取是异步的,也就是说在 for 循环履行完以后,图片可能仍在读取中,当图片读取完后,再调用 onload 回调函数进行处理。简单1点就是说如何在 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 0value in print 1value in print 2value in print 3value in callback 3value in callback 3value in callback 3value 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}

因此,为了解决这个问题,我们只需要为循环中的回调函数添加1个单独的作用域便可,我们使用闭包来实现:

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();}
目录

为了生成文件的目录,我们需要首先取得目录信息,因此我们重写 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"  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 + ">";};

同时需要加入下面的 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> )会移动到页面的最顶部,但是在我们的程序中有1个固定的 header,如果跳转到最顶部,目标标签会被 header 遮挡住,所以我们希望目标标签移动到距离页面顶部 header-height 的地方。为了实现我们的需要,只要加入下面的 CSS 代码便可。

:target:before {    content:"";    display:block;    height:50px; /* fixed header height*/    margin:-50px 0 0; /* negative fixed header height */}
@H_546_1301@Todo 列表

Todo 列表实际上就是 checkBox 的列表,完成的工作用选中的 checkBox 表示,未完成的工作用喂选中的列表表示,以下图所示:

1般来讲,会将下面情势的 markdown 代码解析为 todo 列表

- [x] 完成- [ ] 未完成- [ ] 未完成

为了实现这个功能,我们重写 marked 中解析列表的方法,加入对 todo 列表的支持。

renderer.Listitem = function (text) {    if (/^\s*\[[x ]\]\s*/.test(text)) {        text = text            .replace(/^\s*\[ \]\s*/,'<input type="checkBox"  Disabled> ')            .replace(/^\s*\[x\]\s*/,'<input type="checkBox"  Disabled checked> ');        return '<li >' + 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 就是1个对象。我们存储数据就是直接给它添加1个属性,可以通过 localStoage["a"]=1localstorage.a = 1 的方式来存储数据,但是看起来总觉的不太优雅,由于1般使用下面的方式来 *** 作 localstorage

localstorage.setItem(key,vlaue);localstorage.getItem(key);localstorage.removeItem(key);

另外 localstorage 也有1些局限,使用时需要注意:

存储空间有限制,1般是 5M 左右,和阅读器有关用户清除阅读器缓存以后有可能丢失本地缓存的数据不能直接存对象,要先使用 JsON.stringfy 方法将对象进行序列化处理以后再保存。使用时需要使用 JsON.parse 方法将字符串转为对象。导出文件

通过使用 fileSaver.Js,我们可以方便的在阅读器端生成文件,并提供给用户下载。使用方法也很简单:

var blob = new Blob([HTMLContent],{type: "text/HTML;charset=utf⑻"});saveAs(blob,name);
扩大

我们提供了1些扩大功能,用来更好的展现 markdown 内容。在现在的程序中我们可以很方便的添加扩大功能,下面会具体介绍。

自定义扩大

为了添加扩大,我们首先需要肯定哪些内容需要作为扩大处理。由于在将 markdown 文件转为 HTML 的进程中,1般是不处理代码块中的内容的,所以我们使用代码块来寄存扩大内容,通过代码块的语言来肯定是哪一种扩大。以添加序列图扩大为例:

肯定时序图的代码标记

修改 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 标签的 IDMathJax.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,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 + '" hljs-string">';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⑻";        (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 要使用绝对路径。

具体的插入逻辑可参考源码的实现,这里不再赘述。

总结

以上是内存溢出为你收集整理的【应用】Markdown 在线阅读器全部内容,希望文章能够帮你解决【应用】Markdown 在线阅读器所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/web/1015807.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-22
下一篇 2022-05-22

发表评论

登录后才能评论

评论列表(0条)

保存