一個 Markdown 編輯器的實(shí)現(xiàn)

Mango logo
Mango logo

起因

很早就接觸了 Markdown虹蓄,也用過幾款 Markdown 編輯器犀呼。由于我用的是 Linux,一直無法在 Linux 上找到一款美觀順手的編輯器薇组。Mac 上貌似有不少優(yōu)秀的編輯器圆凰,可一直無緣得見。

其實(shí)很早就有了自己實(shí)現(xiàn)一個 Markdown 編輯器的想法体箕,可一直覺得像編輯器這樣的東西做起來應(yīng)該不會太簡單,工作量應(yīng)該會非常大挑童。我也一直沒有弄明白這其中的原理是什么累铅,雖然網(wǎng)上有不少開源的 Markdown 編輯器,但在沒有說明的情況下閱讀別人的代碼是一件十分困難的事情站叼,所以也一直沒有去讀娃兽。

直到最近讀到了一片文章:Node Webkit (NW.js) tutorial: creating a Markdown editor。在這篇文章里作者簡述了一個極其簡單的 Markdown 編輯器的實(shí)現(xiàn)尽楔,作者用到的技術(shù)雖然我不太熟悉投储,不過原理我還是看懂了。就在這篇文章的基礎(chǔ)上阔馋,我開始實(shí)現(xiàn)自己的 Markdown 編輯器: Mango玛荞,已經(jīng)在 github 上開源。

我給自己的編輯器取名為 Mango ---- 一種水果的名字呕寝,logo 為藍(lán)底白字的一個 M (見上圖)勋眯,M 既代表 Markdown 也代表 Mango,字體是在 PhotoShop 里隨便選了一種看得過去的字體。logo 的設(shè)計(jì)模仿了另一個 Markdown 編輯器(Remarkable)的設(shè)計(jì)客蹋。有了 logo 之后就可以開始動工了塞蹭。

一開始我本來打算用 gtk+ 來寫,不過我對 C 語言的一些第三方庫了解得不多讶坯,不知道能否方便地實(shí)現(xiàn)我想要的功能番电,比如代碼高亮,LaTeX 支持辆琅,而 JavaScript 在這方面有非常成熟的庫漱办。而我又是一個對新技術(shù)非常感興趣的人,所以想嘗試一下用我沒有接觸過的一些技術(shù)來實(shí)現(xiàn)涎跨。于是選擇了跟上文作者相同的技術(shù):NW.js 來實(shí)現(xiàn)洼冻。

NW.js 又叫 node-webkit,把 Node.js 跟 Chromium 結(jié)合在了一起隅很,使得可以用 web 的技術(shù)來寫桌面 App撞牢,不僅可以使用 html、css叔营、js屋彪,還可以使用 Node 大量的第三方庫,而且輕松跨平臺绒尊,實(shí)在是一種相當(dāng)酷的技術(shù)畜挥,更多的介紹請參見項(xiàng)目主頁。不過我之前并沒有學(xué)過Node.js婴谱,我的前端技術(shù)(html蟹但、css、js)也只是屬于在 W3Schools 上速成的水平谭羔。所以在頭三天花了一些時間學(xué)習(xí) Node华糖,以及惡補(bǔ)了一些 JavaScript 的知識。

開始實(shí)現(xiàn)

說實(shí)話瘟裸,“會寫一個” 跟 “寫了一個” 的區(qū)別真的相當(dāng)大客叉,雖然原理都弄明白了,可真正做起來還是有相當(dāng)大的困難话告。這也是我寫這篇文章的原因兼搏,希望給后續(xù)想自己實(shí)現(xiàn)一個編輯器的人一些幫助。

其實(shí)我需要的功能不多沙郭,一個美觀的 UI佛呻,代碼高亮,LaTeX支持(我是數(shù)學(xué)系的病线,這個是必須的)件相,實(shí)時預(yù)覽和同步滾動再扭,以及方便的導(dǎo)入導(dǎo)出功能,尤其是在導(dǎo)出 HTML 和 PDF 后仍能保持美觀的 UI夜矗。在很多方面馬克飛象都做得很好泛范,而且功能比我要求的多,但卻無法讀寫本地文件紊撕,同步功能也不是免費(fèi)的罢荡。而NW.js 可以通過 Node 的模塊輕松實(shí)現(xiàn)讀寫文件的功能。

什么是 Markdown 呢对扶?Markdown只是一種標(biāo)記語言(Markup language)区赵,不過比HTML簡單直觀,非常適合寫作和記筆記浪南。瀏覽器并不能直接解析 Markdown笼才,而是所以我們首先需要通過Markdown解析器(parser)把 Markdown 的語法解析成 HTML 語法,再由瀏覽器的引擎渲染成我們所見的頁面络凿。原理就是這么簡單骡送。parser并不需要我們自己寫,已經(jīng)有很多 Markdown的實(shí)現(xiàn)了絮记,這里我選了Marked摔踱。所以我們只需要在左邊放一個 Editor,編輯 Markdown 源碼怨愤,然后實(shí)時把 Editor 里面的 Markdown 通過 Marked 轉(zhuǎn)換成 HTML 放在右邊的 Viewer 里就可以了派敷。要實(shí)現(xiàn)實(shí)時預(yù)覽,必須監(jiān)聽 Editor 里的變化撰洗,每次有所改變的時候篮愉,重新用 Marked 解析一次(放在reload()函數(shù)里)。

同步滾動實(shí)現(xiàn)

同步滾動功能實(shí)際上非常簡單差导,只要監(jiān)聽 Editor 和 Viewer 的滾動事件潜支,每次一個滾動的時候改變另一個的滾動軸,使得它們的百分比一樣柿汛。就是下面的代碼(我也是 google 來的):

var $divs = $('textarea#editor, div#preview');
var sync = function(e){
   var $other = $divs.not(this).off('scroll'), other = $other.get(0);
   var percentage = this.scrollTop / (this.scrollHeight - this.offsetHeight);
   other.scrollTop = percentage * (other.scrollHeight - other.offsetHeight);
   setTimeout( function(){ $other.on('scroll', sync ); },200);
}
$divs.on('scroll', sync);

代碼高亮實(shí)現(xiàn)

代碼高亮我選擇了 highlight.js,只要把 highlight.js 的代碼嵌入 html,然后在每次更新頁面的時候埠对,重新初始化一下络断,就是在reload()函數(shù)里嵌入如下兩行代碼:

hljs.initHighlighting.called = false;
hljs.initHighlighting();

LaTex支持

這個是最難實(shí)現(xiàn)的,也是我花時間最多的项玛。所以我會詳細(xì)講一講具體的做法貌笨。首先 MathJax 庫肯定是首選,渲染出來的數(shù)學(xué)公式非常漂亮,可以見下圖:

要想實(shí)現(xiàn)數(shù)學(xué)公式的實(shí)時渲染襟沮,就必須在reload()函數(shù)里調(diào)用 MathJax 的Typeset方法重新渲染一遍整個數(shù)學(xué)公式锥惋,而渲染需要有一定的時間昌腰,這就造成了在每次輸入的時候有數(shù)學(xué)公式的地方都會不斷的跳(不知如何形容,就是你首先會看到源碼膀跌,然后看到數(shù)學(xué)公式)遭商,這真的是一個非常影響用戶體驗(yàn)的問題。國內(nèi)一些在線編輯器做得非常好捅伤,沒有這個問題劫流,不過國外的 stackedit仍然有這個問題,只要輸入速度快一點(diǎn)丛忆,數(shù)學(xué)公式會不斷變大變小祠汇。

解決這個問題的一個方法是:首先把經(jīng)由 Marked 解析出來的 html 源碼放入一個 buffer 里,而這個 buffer 是不顯示的熄诡。然后由 MathJax 把 buffer 里的 html 中的數(shù)學(xué)公式排版成可見的格式可很,然后再把 buffer 里的 html 送到 Viewer 顯示出來,這樣 Viewer 得到的 html 就總是經(jīng)過 MathJax 排版過的凰浮。這里有一個問題我抠,就是Typeset函數(shù)是異步的,我們必須要在Typeset函數(shù)完成后导坟,再把 buffer 里的 html 送到 Viewer屿良,這里要借助一下 MathJax 提供的Queue。部分代碼如下:

//reload函數(shù)部分片段
var resultDiv = global.$('.md_result');
var buffer = global.window.document.getElementById("buffer");
var textEditor = global.$('#editor');
var text = textEditor.val();

buffer.innerHTML = (marked(text));
MathJax.Hub.Queue(["Typeset",MathJax.Hub,buffer],
                      ["preview",this]);
//preview函數(shù)里面實(shí)現(xiàn)了把buffer里的html送到Viewer:resultDiv.html(buffer.innerHTML);

看起來非常完美惫周,可我經(jīng)過測試之后發(fā)現(xiàn)問題任然存在尘惧。原因是因?yàn)槲覀儾粩嗑庉媽?dǎo)致reload函數(shù)頻繁觸發(fā),可能第二個reload函數(shù)運(yùn)行到buffer.innerHTML = (marked(text))這一步的時候递递,前一個preview函數(shù)剛好運(yùn)行resultDiv.html(buffer.innerHTML)喷橙,而此時的buffer.innerHTML是未經(jīng)Typeset函數(shù)處理的 。所以我想了個加鎖(lock)的辦法登舞,就是在前一個preview函數(shù)沒有運(yùn)行完的時候贰逾,后來的reload函數(shù)不能運(yùn)行buffer.innerHTML = (marked(text))這段代碼。代碼如下:

function reload(){
    if (lock == false) {
        buffer.innerHTML = (marked(text));
        MathJax.Hub.Queue(["Typeset",MathJax.Hub,buffer],
                      ["preview",this]);
    }
}
function preview(){
    if (lock == false){
        lock = true;
        resultDiv.html(buffer.innerHTML);
        lock = false;
    }
}

當(dāng)然加鎖之后實(shí)時更新可能會有一次延遲菠秒,不過這個問題不大疙剑。

這里還有一個問題,就是 LaTeX 的語法跟 Markdown 的語法有部分沖突践叠,主要是雙下劃線_..._\言缤,LaTeX 里使用_表示下標(biāo),當(dāng)有兩個下標(biāo)的時候禁灼,會先被 Marked 解析為斜體管挟,然后 LaTeX 就無法渲染了。\\會被 Marked 轉(zhuǎn)義成\弄捕,這樣 LaTeX 里就無法使用\\了僻孝,必須使用\\\导帝。要解決這個問題必須修改 parser,要不然就重新實(shí)現(xiàn) parser 使得 parser 不解析$$...$$$...$中的內(nèi)容穿铆。這里參考了讓marked與MathJax和諧共存這篇文章的解決辦法您单,修改了 Marked 的部分源碼,不過就無法在 Mango 中使用_..._來表示斜體了悴务,可以使用*...*睹限。

導(dǎo)出功能實(shí)現(xiàn)

一個合格的 Markdown 必然要有導(dǎo)出 HTML 和 PDF 的功能。導(dǎo)出 HTML 的功能比較容易實(shí)現(xiàn)讯檐,因?yàn)檎麄€界面本身就是 HTML羡疗,只要把不該出現(xiàn)的東西(比如工具欄,編輯區(qū))在導(dǎo)出的時候隱藏掉就可以了别洪。而 PDF 的功能有些困難叨恨。這里我不得不吐槽一下 npm。npm 雖然非常好用挖垛,庫也非常龐大痒钝,隨手一搜發(fā)現(xiàn)很多庫都可以實(shí)現(xiàn)此功能,但是這些庫的質(zhì)量參差不齊痢毒,有些文檔都寫不清楚送矩,上手相當(dāng)有困難。我也是試了幾種不同的庫才終于找到一個有用的:phantom-html2pdf哪替。不過這個庫也好不到哪里去栋荸,文檔不太清楚,作者貌似也不太管事凭舶,別人在 github 上提了幾個 issue 都沒有得到回應(yīng)晌块。我也提了一個,是關(guān)于使用多個css的問題帅霜,作者理都不理我匆背。。身冀。具體的實(shí)現(xiàn)請參見exportToHTMLexportToPDF這兩個函數(shù)钝尸,比較簡單,就不細(xì)說了搂根。

美觀的 UI

對于一個優(yōu)秀的軟件來說珍促,一個好的 UI 必然會為其增色不少。Markdown 解析器只是把 Markdown 轉(zhuǎn)為 HTML兄墅,而沒有規(guī)定格式,所以不同的編輯器轉(zhuǎn)化出來的格式并不是一樣的澳叉,簡書有簡書的 UI隙咸,Medium 有 Medium 的 UI沐悦,馬克飛象有馬克飛象的 UI。我個人非常喜歡馬克飛象和作業(yè)部落的字體顏色五督,所以在 Mango 中選了跟它們一樣的字體顏色藏否。我的css水平真的非常差,不過幸好 bootstrap 提供了不錯的格式充包,再此基礎(chǔ)上修改一些就可以了副签。其中blockquote的格式是 google 來的(在一個專門講 css 技巧的網(wǎng)站)。具體的css代碼可以見preview.css.為了在導(dǎo)出的時候仍然有美觀的 UI基矮,css都是直接在 html 里面寫的淆储,并沒有外鏈。

結(jié)語

NW.js 的優(yōu)點(diǎn)和缺點(diǎn)

說實(shí)話 NW.js 非常好用家浇,及其方便容易就可以創(chuàng)建一個桌面App本砰,Node 大量的第三方包讓你幾乎可以找到任何你想要的功能,可是必須要在 NW.js 環(huán)境才能運(yùn)行钢悲,可是 NW 可執(zhí)行文件有70多MB5愣睢!莺琳!即使你的程序很小还棱,打包在一起也會十分龐大。如果你的程序也非常大惭等,那就更麻煩了珍手。比如在 Mango 中為了有 PDF 導(dǎo)出功能,需要phantomjs咕缎,可這個包有30多MB珠十,這就使得程序非常大了。

另外凭豪,報錯信息太不詳細(xì)了焙蹭,經(jīng)常解決一個 bug 花很長時間,總是報一些百思不得其解的錯(不知道到這是 NW.js 的原因還是 JavaScript 的原因)嫂伞。

Mango 的未來

其實(shí) Mango 還很不完善孔厉,比如連查找替換的功能都沒有,也沒有其他編輯器的流程圖功能帖努。因?yàn)?Mango 的定位是用來記筆記和寫一些小文章(我想這也是所有 Markdown 編輯器的定位)撰豺,又不是寫代碼,所以我想查找替換的功能很少會用到拼余。而流程圖污桦,語法太繁瑣,違背了簡約的原則匙监,而且估計(jì)也很少會用凡橱,所以也沒有實(shí)現(xiàn)了小作。其實(shí)還是有一些功能我想做的,比如與一些云服務(wù)相結(jié)合稼钩,實(shí)時同步到云端(就像馬克飛象那樣顾稀,當(dāng)然也不一定跟印象筆記結(jié)合)。另一個是實(shí)現(xiàn)一些自定義的功能坝撑,比如自定義css等静秆。如果 Mango 有用戶使用的話,我將繼續(xù)完善巡李。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末抚笔,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子击儡,更是在濱河造成了極大的恐慌塔沃,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阳谍,死亡現(xiàn)場離奇詭異蛀柴,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)矫夯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進(jìn)店門鸽疾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人训貌,你說我怎么就攤上這事制肮。” “怎么了递沪?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵豺鼻,是天一觀的道長。 經(jīng)常有香客問我款慨,道長儒飒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任檩奠,我火速辦了婚禮桩了,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘埠戳。我一直安慰自己井誉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布整胃。 她就那樣靜靜地躺著颗圣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上在岂,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天荚藻,我揣著相機(jī)與錄音,去河邊找鬼洁段。 笑死,一個胖子當(dāng)著我的面吹牛共郭,可吹牛的內(nèi)容都是我干的祠丝。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼除嘹,長吁一口氣:“原來是場噩夢啊……” “哼写半!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起尉咕,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤叠蝇,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后年缎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體悔捶,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年单芜,在試婚紗的時候發(fā)現(xiàn)自己被綠了蜕该。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡洲鸠,死狀恐怖堂淡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情扒腕,我是刑警寧澤绢淀,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站瘾腰,受9級特大地震影響皆的,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜居灯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一祭务、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧怪嫌,春花似錦义锥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春柱恤,著一層夾襖步出監(jiān)牢的瞬間数初,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工梗顺, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留泡孩,地道東北人。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓寺谤,卻偏偏與公主長得像仑鸥,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子变屁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容