avatar

光和尘

有花满渚、有酒盈瓯

目录检索关于我

🔖 随笔胡言乱语

起因

大约是刚上大二的时候吧,罗鸽推荐我去 logdown 写博客和解题报告,那是我头一回接触 Markdown,在此之前除了偶尔用 Tex 写写数学公式外都是用的 WYSIWYG 编辑器写文档。因为很少用到复杂的排版,而 logdown 也内置了 mathjax,满足我写文档的需求已经绰绰有余了,比 Tex 更简洁的语法让它一度成为了我写文档的首选方式。

那会儿 logdown 才建站不久,当时觉得界面还蛮好看的,只不过它的服务器应该挂在了外网,用校园网访问网速慢得有些难以忍受,于是过了不久便放弃它了。想到这里突然心血来潮地又跑去观摩了下,发现界面好像和 N 年前相比没太大差别,重置密码的服务好像都挂掉了。犹记得那时站长在讨论页积极地回复别人提的问题和建议的情形,还列出了一部分开发计划。那时一切都彷佛才刚刚开始,处处都是热闹的声音;“考古”有时也蛮心情复杂的。

从 logdown 到 hexo

又过了不久,基于 github 的静态页面托管服务的静态博客生成器火了起来,hexo 是其中的佼佼者。那个时候精力都放在了竞赛上,只会写写单文件的 C/C++ 代码,对于工程实在是一窍不通,当听闻这个时下最火的静态博客生成器之一的作者居然是个高中生时,心里一时感佩不已。

在罗鸽的建议下选择了 yilia 这个主题,它应该可以算作 hexo 最时髦的几个主题之一了,饶是如此也仍有些地方感到不太满意,比如代码块不能折叠、复制,比如文章目录太丑陋等等(当然,瑕不掩瑜,后来 yilia 的作者重新设计了此主题,我现在的博客也几乎都照抄了它的样式)。那个时候实在是太菜了(虽然现在也是),所以拜托罗鸽帮忙做了一些修改和定制。搭建好博客后,开心得像刚得了一件有趣玩具的孩子,爱不释手地把玩它,结果很快就悲剧了。

放弃 hexo

那时 hexo 解析 Markdown 的做法是使用类似 markdown-it 的解析器兼渲染器将 Markdown 语法的字符串直接转成 HTML 字符串,而 mathjax 支持的是独立于 Markdown 之外的 Tex 语法,它通常工作于解析完毕后的 HTML 字符串中;而由于 Markdown 支持将两个下划线解析成 Emphasis / Strong Emphasis 的语法,因而经常错误地破坏 Tex 语法中的下划线表达式。比如 x_1 + x_2 在 Tex 中写成 $x_1 + x_2$ 即可,而在那时 hexo 提供的 Markdown 解析渲染器 + mathjax 的组合下必须写成 $x\_1 + x\_2$ 才能得到正确解析。花费了不少时间和精力都没能很好的解决这个问题,当时有一个做法(有道云笔记正是采用此方案)是将 x_1 + x_2 写成 `$x_1 + x_2$`,这样就能避免 Markdown 的解析渲染器错误地解析下划线了;然而我讨厌这种麻烦的写法,何况只有一部分支持 Markdown 的平台或编辑器采用此做法,兼容性并不好。

除此之外,Markdown 规定了对于软换行要将换行符折叠成一个空格,这对于英文排版来说自然没问题,单词之间本来就需要用空格分隔;但应用到中文排版时就有些丑陋了,虽然不少编辑器都支持 Markdown 的“伪换行”,但这个方案仍然不够优雅。比如使用 vim 在内容间跳跃的效率降低了;又比如在使用 git 之类的版本管理工具时,短多行总比长单行更容易做对比。我还是偏爱手动换行以在源码中获得良好的排版视觉效果。

诸如此类的问题促使我萌生了要自己写一个 Markdown 解析器的念头。

碎碎念

说来惭愧,大学的时候因为太懒了各种原因一直没有动手去做这件事,博客倒是很痛快地停了很久没去更新。大四的时候尝试了 Typescript,未曾想从此便成了自己的主力开发语言,不过当时也只是用它写一些后来再没有派上用场的各类命令行小工具(想想好像折腾了不少乱七八糟的东西)。

毕业后,我来到了杭州从事后端开发,前半年老大让我兼着做一些数据开发的工作,工作量一下子就跑满了,直到2019年初任务才少了下来。期间学的东西也蛮杂,为了提升效率学习了不少命令行的东西,因为工作原因又补了不少后端和数据库的知识,而出于自身的兴趣也额外学了一些前端的东西(当时罗鸽还逼着我写一个 WYSIWYG 编辑器,可真是太为难我了)。因为学得东西零碎又杂,奈何记忆喂狗,所以记笔记的需求又变多了起来,这时也尝试了各种笔记平台。还是有各种不满意,此外还是想要将所有的内容(图片、文字、代码)都保存在本地,想要搭建一个清爽的个人博客的想法又日益强烈起来。

开始造轮子

正式着手做这件事是在2019年4月,那时工作任务已经减轻了很多,正好有时间精力去考虑这件事。当时已经有较多的 React 使用经验了,因此初始的想法便是将 Markdown 语法的字符串解析成 JSON 数据,然后再由 React 完成渲染工作,即将整个工作分解成解析阶段渲染阶段两部分,分别交由解析器渲染器完成。后来也延续了此思路,只可惜当时太纠结于一些局部的数据语法,没能想到从各类数据类型进行划分从而制定一个上层的解析策略。大约折腾了两个多月,只积累下一些混乱、零散的代码,浅尝辄止后又搁置了一段时间。

2019年7月离开杭州转而去广州工作,新工作相当轻松,一下子腾出了很多时间和精力可以专注于自己的事情。这个期间出于工作的原因折腾了一阵其它各种工具,在开发它们的时候也是激情澎拜,觉得肯定能提升不少效率,结果无一例外都烂尾了,不过也积累了不少开发工具和库的经验。这个时候才真正下定决心要把 Markdown 解析器写出来。

反思上次的半途而废,决定先读一下 Markdown 的规范,于是把目光瞄向了 Github Flavor Markdown(简称 GFM),好在它定义得足够详细, 对传统的 Markdown 及其几个广泛使用的变种进行了讨论,废除了一些存在歧义的情况,也给了足够多的示例。所以直接以它作为标准进行实现就好了。之后要解决的问题是定义每种数据的结构类型。在2020年1月的时候看到了 mdast,它定义了常见的 Markdown 语法的抽象语法树,参考它的定义制订了一个兼容的版本。接下来是最受煎熬的工作了:根据 GFM 给出的示例生成测试用例,但是它在示例中的 Output 内容给出的是 HTML 字符串,需要参照它编写对应的 AST 结构的数据。麻烦的是,抽象语法树中的节点还要求支持 position 信息,所以那一阵子经常调测试用例调到午夜(太惨了)。以 GFM example#586 为例,希望能带来一个直观的印象:

  • Markdown 字符串:

    source.md 
    ![foo](train.jpg)
  • HTML 格式的输出:

    output.html 
    <img src="train.jpg" alt="foo" />
  • 需要转成的 AST 数据:

    ast.json 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    "type": "image",
    "url": "train.jpg",
    "alt": "foo",
    "position": {
    "start": {
    "line": 1,
    "column": 1,
    "offset": 0
    },
    "end": {
    "line": 1,
    "column": 18,
    "offset": 17
    }
    }
    }

出于扩展性的考虑,我并不满足于单纯地实现 GFM 中定义的规则,还想要集成更多的语法支持,比如脚注[1],或者一些自定义的语法。受到 Koa 中间件思想的影响,我把解析器设计成了由核心算法驱动一个分词器(Tokenizers)列表的方式来完成解析工作,同时提供了一系列的生命周期钩子,这样当定义一个新的数据类型的分词器时,只需要实现对应的生命周期函数,然后把它放入到解析器的分词器列表中合适的位置上就可以和其它分词器协同工作了。

之后断断续续地实现了负责解析 GFM 中提到的各类数据类型的分词器,在这个过程中,好几次因为发现现有算法无法通过一些测试用例而又不得不重新审视甚至重写算法;尤其是内联数据的解析,改了好几个版本,最终版本的接口也有些复杂,需要讨论多种情况(一方面也是出于效率的考虑,或许以后会进行整合优化)。就这样,磕磕绊绊地写了一年多,中间改了好几次接口,核心算法也重写了几回,直到2021年7月才算基本完工(当然,中间因为心血来潮造了不少轮子耽搁了一些时间)。最大的感慨是:早期开始写测试用例实在是太明智了,节约了很多的时间(所以我那时也算是“头发渐疏终不悔”了吧)。

起个中意的名字很重要

我一直很纠结,不知道该给解析器起个什么样的名字才好,希望它尽可能简洁又包含某种含义(既然是下定决心要花费上一番功夫的东西,当然希望它能够记录下某些东西啦)。有多纠结呢,连代码都只写了类似伪码的东西,随时为想好名字后重建仓库做准备。最后从 世界の終わり的『花鳥風月』的歌词里找到了 yozora 这个词,它是日语「よぞら」的罗马音,意为夜空,多少也有些出于纪念这首陪伴了我许多个静谧夜晚的歌曲的缘故吧(这样说来我起名还是蛮随便的23333)。美中不足的是,github 上这个名字已经被占用了。只好退而求其次,在 github 上使用 yozorajs 这个组织名了。

画一个中意的图标

有了中意的名字后,自然少不了一个中意的图标啦(其实距离起名隔了一年半才画的)。

yozora-logo

虽然画得很粗糙,但我确实很中意它。灵感源于前不久去见一位久未谋面的朋友、一位很想见的朋友,在回程的列车上看到那句“树深时见鹿”,当下心间便翻腾着许多思绪。时隔两周后画下了它:这既是头鹿又是棵树,既是可乐也是雪碧,即是星空也是 Y。当然,我画得有些太丑了,想看出来恐怕需要一些想象力才行。

其它一些工作

除了解析器,还写了基于 React 的渲染器 yozora-react,负责将 yozora 解析成的抽象语法树渲染成页面,本文的主体内容就是用它渲染的。为了支持一些可在线编辑的 Demo,还实现了 @yozora/react-code-editor@yozora/react-code-live,灵感源于 react-live。为什么不直接用 react-live 呢,因为想要支持行号、可折叠、滚动等零碎的东西,更重要的是,想要支持其它类型语言的实时编辑渲染的能力,如:

  • 这是一个 JSX 在线 Demo

    JSX Demo 
  • 这是一个 Mathjax 在线 Demo (暂不支持语法高亮)

    Tex Demo 
    f(x) = \left\lbrace \begin{aligned} &\sqrt{x^2 + 1}, &x <= 0\\ &\frac{x + 1}{x ^ 3 + 2}, &x > 0\\ \end{aligned}\right.
  • 这是一个 Graphviz 在线 Demo

    Graphviz Demo 

有了自己的 Markdown 解析器和渲染器后,用 gatsby 搭建了现在这个博客,为了让它们能够协调工作,又开发了 @yozora/gatsby-transformer,有些太折腾了。

总觉得有些过于啰嗦了,还是就此打住吧。

小结

  • 关于博客

    其实还是有不少地方感到不满意的。比如字体和颜色混乱,比如布局和背景仍然有些别扭,过渡动画也很生硬,最重要的是页面的加载速度很慢(做了一番尝试也没能顺利优化)。

  • 关于 yozora

    磕磕绊绊花了不少时间,前后也改了好几次核心算法,写工程项目需要考虑的问题也更多;不过完整地实现某个规范还是挺有成就感的。虽然实现了 GFM 规范 中提到的规则并通过几乎所有(除了 example-653 ,因为渲染器不打算支持原生的 HTML Tag,所以懒得做过滤,如果有需要自行做一下过滤就好了)由规范中提到的示例改造的测试用例,但应该还是有一些情况没有考虑到,以后碰到了再慢慢完善吧。此外,由于整个项目完全用 Typescript 编写,且零外部依赖,所以有意将其转成 Deno 项目;不过短期内应该不会动手做这件事,太懒了想要尝试其它事物了。

    目前已经有其它支持将 Markdown 语法的字符串转成 mdast 的项目了,而且已经被广泛使用(我在写 yozora 时它们还未被开发出来或者我未找到相关项目),不过我没有仔细地拿它们和 yozora 做对比(也无意在此处讨论);甚至 mdx 进一步支持直接在 Markdown 中写 JSX 代码。所以多少还是会觉得有些冷清。

一些感悟

写完了但没有完全写完,总是有各种各样的不满意,甚至会觉得有些拿不出手;这也或许是坚持去做某件事的风险?当某一刻发现自己所曾热衷于的某项事物,原来只是自以为是地赋予了自己某种使命感,于是笃定不移地以近乎悲壮的心情将满腔热血白白地浇灌在一片极其平凡亦或是无人问津的土壤上,又或是在这个过程里总算认识到了自己的平庸;无论哪一种都不太让人感到高兴啊。虽然彷佛花了很大的力气做了一件一点也不酷的事情(又菜又任性),但其实在这个过程中好几次都会遥想写下此文时的心情(又菜又爱炫耀)。许是半途而废的次数太多了,所以才很想要能够坚持完成某件事情(某种程度上来说恐怕都快要发展成执念了)。不过话说回来,人生在世,岂有那么多是非要做到不可又或者是平白无故的毫无意义呢。

时间总比预料的要流逝得快上一些,我依然没想明白该怎么对付它或者说该如何与它和平共处,它只自顾自地飞驰而过,我却怎么也无法将之忽视。最近看到一句诗多少有些释怀,分享于此与诸君共勉:“有花堪折直须折,莫待无花空折枝”。

嘛,谨以此纪念这段不算短暂却也飞逝而去又去而不复返的寻常时光。

© 2017-2022 光和尘有花满渚、有酒盈瓯

Comments