普通视图

如何优雅编译一个 Markdown 文档

2024年1月18日 20:50

Markdown 是一种广泛使用的轻量级标记语言,允许人们使用易读易写的纯文本格式编写文档,也是 xLog 主要使用的文章格式,本文就以 xLog Flavored Markdown 为例来说明如何优雅地解析一个 Markdown 文档

架构

解析过程可以用这样一个架构来表示:

flowchart TB subgraph input Markdown end subgraph unified subgraph remark Markdown:::inputClass --string--> remark-parse:::remarkClass --mdast--> remarkPlugins[remark plugins]:::remarkClass remarkPlugins --mdast--> remark-rehype:::remarkClass & mdast-util-toc:::remarkClass end subgraph rehype remark-rehype --hast--> rehypePlugins[rehype plugins]:::rehypeClass rehypePlugins --hast--> hast-util-to-text:::rehypeClass & hast-util-to-html:::rehypeClass & hast-util-to-jsx-runtime:::rehypeClass end rehypePlugins --hast--> unist-util-visit:::rehypeClass end subgraph output mdast-util-toc --tocResult--> TOC:::inputClass hast-util-to-text --string--> plainText[Plain Text]:::inputClass hast-util-to-html --string--> HTML:::inputClass hast-util-to-jsx-runtime --JSX.Element--> ReactElement[React Element]:::inputClass unist-util-visit --custom--> Metadata:::inputClass end style input fill:#bbf7d0,stroke:#4ade80,color:#15803d classDef inputClass fill:#22c55e,stroke:#16a34a style output fill:#bbf7d0,stroke:#4ade80,color:#15803d style unified fill:#bfdbfe,stroke:#60a5fa,color:#1d4ed8 style remark fill:#fecaca,stroke:#f87171,color:#b91c1c classDef remarkClass fill:#f87171,stroke:#dc2626 style rehype fill:#fef08a,stroke:##facc15,color:#a16207 classDef rehypeClass fill:#facc15,stroke:#ca8a04

关键概念:

  • unified:通过语法树和插件来解析、检查、转换和序列化内容的库
  • remark:unified 的生态项目之一,由插件驱动的 Markdown 处理库
  • rehype:unified 的生态项目之一,由插件驱动的 HTML 处理库
  • mdast:remark 使用的用于表示 Markdown 的抽象语法树规范
  • hast:rehype 使用的用于表示 HTML 的抽象语法树规范

简单来说就是把 Markdown 文档交给一个 unified 生态的解析器解析成 unified 可识别的语法树,再通过一系列 unified 生态的插件转换为需要的内容,再通过一系列 unified 生态的工具库输出为需要的格式,下面就从 解析、转换、输出 这三个步骤来分别说明

解析 Parse

flowchart TB subgraph input Markdown end subgraph unified subgraph remark Markdown:::inputClass --string--> remark-parse:::remarkClass end end style input fill:#bbf7d0,stroke:#4ade80,color:#15803d classDef inputClass fill:#22c55e,stroke:#16a34a style unified fill:#bfdbfe,stroke:#60a5fa,color:#1d4ed8 style remark fill:#fecaca,stroke:#f87171,color:#b91c1c classDef remarkClass fill:#f87171,stroke:#dc2626

无论输入是 Markdown、HTML 还是纯文本,都需要将其解析为可操作的格式。这种格式被称为语法树。规范(例如 mdast)定义了这样一个语法树的外观。处理器(如 mdast 的 remark)负责创建它们。

最简单的一步,我们需要解析的是 Markdown,所以这里就应该使用 remark-parse 来把 Markdown 文档编译成 mdast 格式的语法树

对应 xLog Flavored Markdown 中的

const processor = unified().use(remarkParse)

const file = new VFile(content)
const mdastTree = processor.parse(file)

转换 Transform

flowchart TB subgraph remark remark-parse:::remarkClass --mdast--> remarkPlugins[remark plugins]:::remarkClass remarkPlugins --mdast--> remark-rehype:::remarkClass end subgraph rehype remark-rehype --hast--> rehypePlugins[rehype plugins]:::rehypeClass end style remark fill:#fecaca,stroke:#f87171,color:#b91c1c classDef remarkClass fill:#f87171,stroke:#dc2626 style rehype fill:#fef08a,stroke:##facc15,color:#a16207 classDef rehypeClass fill:#facc15,stroke:#ca8a04

这就是魔法发生的地方。用户组合插件以及它们运行的顺序。插件在此阶段插入并转换和检查它们获得的格式。

这一步最为关键,不仅包含了从 Markdown 到 HTML 的转换,还包含我们想在编译过程中夹带的私货,比如增加一些非标准的语法糖、清理 HTML 防止 XSS、增加语法高亮、嵌入自定义组件等

unified 的插件非常多,更新也比较及时,基本需求几乎都能满足,对于不能满足的特定需求,自己编写转换脚本也很容易实现

里面有一个特殊的插件是 remark-rehype,它会把 mdast 语法树转为 hast 语法树,所以在它之前必须使用处理 Markdown 的 remark 插件,在它之后必须使用处理 HTML 的 rehype 插件

xLog Flavored Markdown 中就加入了非常多的转换插件

const processor = unified()
  .use(remarkParse)
  .use(remarkGithubAlerts)
  .use(remarkBreaks)
  .use(remarkFrontmatter, ["yaml"])
  .use(remarkGfm, {
    singleTilde: false,
  })
  .use(remarkDirective)
  .use(remarkDirectiveRehype)
  .use(remarkCalloutDirectives)
  .use(remarkYoutube)
  .use(remarkMath, {
    singleDollarTextMath: false,
  })
  .use(remarkPangu)
  .use(emoji)
  .use(remarkRehype, { allowDangerousHtml: true })
  .use(rehypeRaw)
  .use(rehypeIpfs)
  .use(rehypeSlug)
  .use(rehypeAutolinkHeadings, {
    behavior: "append",
    properties: {
      className: "xlog-anchor",
      ariaHidden: true,
      tabIndex: -1,
    },
    content(node) {
      return [
        {
          type: "text",
          value: "#",
        },
      ]
    },
  })
  .use(rehypeSanitize, strictMode ? undefined : sanitizeScheme)
  .use(rehypeTable)
  .use(rehypeExternalLink)
  .use(rehypeMermaid)
  .use(rehypeWrapCode)
  .use(rehypeInferDescriptionMeta)
  .use(rehypeEmbed, {
    transformers,
  })
  .use(rehypeRemoveH1)
  .use(rehypePrism, {
    ignoreMissing: true,
    showLineNumbers: true,
  })
  .use(rehypeKatex, {
    strict: false,
  })
  .use(rehypeMention)

const hastTree = pipeline.runSync(mdastTree, file)

下面介绍部分用到的插件

  • remarkGithubAlerts:增加 GitHub 风格的 Alerts 语法,演示
  • remarkBreaks:不再需要空一行才能被识别为新的自然段
  • remarkFrontmatter:支持前置内容(YAML、TOML 等)
  • remarkGfm:支持非标准的 GitHub 在原版 Markdown 语法上扩展的一系列语法(但其实这系列语法已经被非常广泛使用,成为了事实意义上的标准)
  • remarkDirective remarkDirectiveRehyp:支持非标准的 Markdown 通用指令提案
  • remarkMath rehypeKatex:支持复杂的数学公式,演示
  • rehypeRaw:支持 Markdown 中夹杂的自定义 HTML
  • rehypeIpfs:自定义插件,为图片、音频、视频支持 ipfs:// 协议的地址
  • rehypeSlug:为标题添加 id
  • rehypeAutolinkHeadings:为标题添加指向自身的链接 rel = "noopener noreferrer"
  • rehypeSanitize:清理 HTML,用于确保 HTML 安全避免 XSS 攻击
  • rehypeExternalLink:自定义插件,给外部链接添加 target="_blank"rel="noopener noreferrer"
  • rehypeMermaid:自定义插件,渲染绘图和制表工具 Mermaid,本文的架构图就是通过 Mermaid 渲染的
  • rehypeInferDescriptionMeta:用于自动生成文档的描述
  • rehypeEmbed:自定义插件,用于根据链接自动嵌入 YouTube、Twitter、GitHub 等卡片
  • rehypeRemoveH1:自定义插件,用于把 h1 转为 h2
  • rehypePrism:支持语法高亮
  • rehypeMention:自定义插件,支持 @DIYgod 这样艾特其他 xLog 用户

输出 Stringify

flowchart TB subgraph unified subgraph remark remarkPlugins[remark plugins]:::remarkClass --mdast--> mdast-util-toc:::remarkClass end subgraph rehype rehypePlugins[rehype plugins]:::rehypeClass rehypePlugins --hast--> hast-util-to-text:::rehypeClass & hast-util-to-html:::rehypeClass & hast-util-to-jsx-runtime:::rehypeClass end rehypePlugins --hast--> unist-util-visit:::rehypeClass end subgraph output mdast-util-toc --tocResult--> TOC:::inputClass hast-util-to-text --string--> plainText[Plain Text]:::inputClass hast-util-to-html --string--> HTML:::inputClass hast-util-to-jsx-runtime --JSX.Element--> ReactElement[React Element]:::inputClass unist-util-visit --custom--> Metadata:::inputClass end classDef inputClass fill:#22c55e,stroke:#16a34a style output fill:#bbf7d0,stroke:#4ade80,color:#15803d style unified fill:#bfdbfe,stroke:#60a5fa,color:#1d4ed8 style remark fill:#fecaca,stroke:#f87171,color:#b91c1c classDef remarkClass fill:#f87171,stroke:#dc2626 style rehype fill:#fef08a,stroke:##facc15,color:#a16207 classDef rehypeClass fill:#facc15,stroke:#ca8a04

最后一步是将(调整后的)格式转换为 Markdown、HTML 或纯文本(可能与输入格式不同!)

unified 的工具库也很多,可以输出各种我们需要的格式

比如 xLog 需要在文章右侧展示自动生成的目录、需要输出纯文本来计算预估阅读时间和生成 AI 摘要、需要生成 HTML 来给 RSS 使用、需要生成 React Element 来渲染到页面、需要提取文章的图片和描述来展示文章卡片,就分别使用了 mdast-util-toc、hast-util-to-text、hast-util-to-html、hast-util-to-jsx-runtime、unist-util-visit 这些工具

对应 xLog Flavored Markdown 中的

{
  toToc: () =>
    mdastTree &&
    toc(mdastTree, {
      tight: true,
      ordered: true,
    }),
  toHTML: () => hastTree && toHtml(hastTree),
  toElement: () =>
    hastTree &&
    toJsxRuntime(hastTree, {
      Fragment,
      components: {
        // @ts-expect-error
        img: AdvancedImage,
        mention: Mention,
        mermaid: Mermaid,
        // @ts-expect-error
        audio: APlayer,
        // @ts-expect-error
        video: DPlayer,
        tweet: Tweet,
        "github-repo": GithubRepo,
        "xlog-post": XLogPost,
        // @ts-expect-error
        style: Style,
      },
      ignoreInvalidStyle: true,
      jsx,
      jsxs,
      passNode: true,
    }),
  toMetadata: () => {
    let metadata = {
      frontMatter: undefined,
      images: [],
      audio: undefined,
      excerpt: undefined,
    } as {
      frontMatter?: Record<string, any>
      images: string[]
      audio?: string
      excerpt?: string
    }

    metadata.excerpt = file.data.meta?.description || undefined

    if (mdastTree) {
      visit(mdastTree, (node, index, parent) => {
        if (node.type === "yaml") {
          metadata.frontMatter = jsYaml.load(node.value) as Record<
            string,
            any
          >
        }
      })
    }
    if (hastTree) {
      visit(hastTree, (node, index, parent) => {
        if (node.type === "element") {
          if (
            node.tagName === "img" &&
            typeof node.properties.src === "string"
          ) {
            metadata.images.push(node.properties.src)
          }
          if (node.tagName === "audio") {
            if (typeof node.properties.cover === "string") {
              metadata.images.push(node.properties.cover)
            }
            if (!metadata.audio && typeof node.properties.src === "string") {
              metadata.audio = node.properties.src
            }
          }
        }
      })
    }

    return metadata
  },
}

这样我们就优雅地从原始 Markdown 文档开始,获得了我们需要的各种格式的输出

除此之外,我们还能利用解析出的 unified 语法树来编写一个可以左右同步滚动和实时预览的 Markdown 编辑器,可以参考 xLog 的双栏 Markdown 编辑器(代码),有机会我们下次再聊

4 月新番太好看了!吹爆!

2023年5月25日 07:32

今年从 1 月就一直没什么好看的作品,到了 4 月突然爆发,让我非常激动。现在播出过半了,是时候好好说说感受了

以下当然不能囊括 4 月所有的好作品,因为实在太多了,只是说一说符合我口味的几部,按我个人的喜爱程度来排序

以下会包含大量剧透,还没看的小伙伴请酌情观看

鬼灭之刃刀匠村篇

刀匠村是为鬼杀队锻造日轮刀的刀匠们居住的地方,位置及其隐蔽且高度保密,这部讲的是刀匠村被上弦之肆・半天狗和上弦之伍・玉壶找到并发动偷袭,正在刀匠村的炭治郎、恋柱・甘露寺蜜璃、霞柱・时透无一郎、不死川玄弥努力保护刀匠们,对抗上弦的故事

上弦之肆・半天狗这个角色非常有趣,本体又小又丑,非常弱小无助又胆小,总感觉像是来送人头的

image [BeanSub&FZSD&LoliHouse] Kimetsu no Yaiba - 51 [WebRip 1080p HEVC-10bit AAC ASSx2]-0001 image

但实际上非常扮猪吃老虎,控制的几个鬼(还不知道他们是什么关系)喜、怒、哀、乐、憎,都非常强,压迫感很足,本体防御力也无敌,被大家排着队砍也砍不死

image image

弥豆子的表现也很亮眼,鬼化弥豆子超帅

image

image

还有可爱的蜜璃

虽然整体节奏还是略显拖沓,但经过游郭篇的洗礼,我本身预期也很低,目前已经远远超出了我的预期,文戏比之前明显少了好多,打斗也更加燃爆,看得非常上头

image

image

再加上我有粉丝加成,荣登我的周指活排名第一名

谢谢飞碟社款待

类别评分 / 10
剧情7
画面10
个人加成10

为美好的世界献上爆焰!

脑子有问题的红魔族第一天才中二病魔法师慧慧的故事,喜欢的东西是爆裂魔法,特技是爆裂魔法,兴趣是爆裂魔法,唯一的真爱也是爆裂魔法

慧慧小时候被一位使用爆裂魔法的巨乳魔法师所救而爱上了爆裂魔法

[Lilith-Raws] Kono Subarashii Sekai ni Bakuen wo! - 01 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]-0008

虽然爆裂魔法极不实用且难以操纵,是出了名的搞笑魔法,“只有学得上级魔法才能独当一面。爆裂魔法之流不过是搞笑魔法罢了”,但慧慧还是不顾嘲笑,为了爱好而独自努力

[Lilith-Raws] Kono Subarashii Sekai ni Bakuen wo! - 01 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]-0002 [Lilith-Raws] Kono Subarashii Sekai ni Bakuen wo! - 01 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]-0147

最后使用所有技能点数学到了爆裂魔法

比黑色还要黑 暗之漆黑
融合著我之真红吧
觉醒的时刻已经到来
坠入无谬之境界
形成无形之扭曲
出现吧!
Explosion!

[Lilith-Raws] Kono Subarashii Sekai ni Bakuen wo! - 05 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]-0055

之后的外出冒险也很有趣

[Lilith-Raws] Kono Subarashii Sekai ni Bakuen wo! - 07 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]-0012

[Lilith-Raws] Kono Subarashii Sekai ni Bakuen wo! - 08 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]-0021

之前看《为美好的世界献上祝福!》就被慧慧的中二吸引了,结果在红魔乡发现慧慧的中二只是一个普通水平,印象最深的是魔法学校最重要的一节课是战斗前要摆什么样的中二姿势

[Lilith-Raws] Kono Subarashii Sekai ni Bakuen wo! - 01 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]-0096 [Lilith-Raws] Kono Subarashii Sekai ni Bakuen wo! - 01 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]-0094 [Lilith-Raws] Kono Subarashii Sekai ni Bakuen wo! - 01 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]-0190

缺点也是有的,作画穷了点,有不少崩坏的地方,有些画面还是挺出戏的,特别是扎堆在作画全都不要钱一样的 4 月新番中显得更惨了(但还算说得过去)(后面还有个说不过去的)

类别评分 / 10
剧情8
画面6
个人加成10

天国大魔境

刚说的精美作画就来了,从第一集就被震撼到了,不要钱一样的作画,末日废土乌托邦剧情,流畅的打戏完美的运镜,轻松愉快加简单的涩涩又不失紧张刺激的氛围把控,就像完美的考试标准答案一样,刚开始还担心开头质量过于高了后面会不会质量下降,但是完全没有!

剧情还没有完全揭秘,现在知道的是成为废墟的日本栖息着一种食人的怪物蛭子,人们艰难地生存着,还有一个号称 “天国” 的乌托邦,被安全的围墙覆盖,里面科技发达,被圈养的孩子们生活富足

墙外很危险

image

墙内看似天国但剧情暗示了更大的危险

[SweetSub&LoliHouse] Heavenly Delusion - 05 [WebRip 1080p HEVC-10bit AAC ASSx2]-0006 image

生活在墙外的主角真流和斩子被托付前往天国寻找和真流长相一样的人,期间遇到了各种各样的人

不愿相信儿子已经变成没有人性的怪物蛭子的母亲,诱骗路人喂给儿子吃,最后被儿子变成的蛭子杀死

image [SweetSub&LoliHouse] Heavenly Delusion - 02 [WebRip 1080p HEVC-10bit AAC ASSx2]-0001

梦想跟看上的客人爱爱来赚大钱的酒店小老板,也会跟骗子强盗合作诱拐冒险者

image image

做人体实验的冷血医生,又只是一个为了让妻子可以作为人类有尊严死去的痴情男,成功让妻子死去后抱着妻子的尸体自杀

image image image

而义正严词反对人体实验的伦理组织首领只是为了给医生下圈套抢夺资源

image [SweetSub&LoliHouse] Heavenly Delusion - 08 [WebRip 1080p HEVC-10bit AAC ASSx2]-0013

每个人都能被不长的篇幅刻画地如此鲜活,在这样的废土世界,努力艰难不择手段地活着,即使罪大恶极也能让我深深理解,感到同情和惋惜

让我对制作组献上最大的敬意,如果排除个人因素,这部作品将是遥遥领先的 4 月霸权

类别评分 / 10
剧情10
画面10
个人加成5

地狱乐

在德川幕府时期,有一个鲜花遍地,蝴蝶翩飞的名为 “神仙岛” 的地方,前往该岛的调查队只有极少数的残肢和鲜花能够随着小船漂流回来

最强死刑犯忍者画眉丸与处刑人山田浅右卫门佐切一同被派往此岛,寻找不老不死的仙药以换取无罪释放,并在岛上与怪物以及其他死刑犯互相残杀

里面的怪物是这样的画风

灶神

image image [BeanSub&FZSD&LoliHouse] Jigokuraku - 05 [WebRip 1080p HEVC-10bit AAC ASSx2]-0035

天仙

[BeanSub&FZSD&LoliHouse] Jigokuraku - 07 [WebRip 1080p HEVC-10bit AAC ASSx2]-0032

被天仙抓到会被炼成丹

[BeanSub&FZSD&LoliHouse] Jigokuraku - 07 [WebRip 1080p HEVC-10bit AAC ASSx2]-0031 [BeanSub&FZSD&LoliHouse] Jigokuraku - 05 [WebRip 1080p HEVC-10bit AAC ASSx2]-0033

非常诡异荒诞

每个死囚也都有自己的故事,并不是黑白分明,经历都很有趣

image image

还出现了一个怪力萝莉

image

仙药和小岛的真相依然扑朔迷离,后面肯定还会有更诡异更强大的怪物,很期待

类别评分 / 10
剧情8
画面8
个人加成0

【我推的孩子】

妇科男医生和早逝女病人遇难转生为自己单推偶像星野爱的双胞胎孩子阿库亚和露比,三年后星野爱被害身亡,阿库亚立志寻找母亲遇害的真相和真凶,露比梦想着成为母亲那样的偶像,兄妹应对演艺圈的各种纷争和阴谋

一个多小时的首集太惊喜了,像看了一部电影,我觉得可以算得上 4 月最佳单集了

但可能是开头调起太高了,期待也拉得很高,后面几集略显平淡了一点,估计是高低高的节奏,现在是憋大招中,最后会有一个超感人哭爆的结局,准备好纸巾了

虽然如此,但如果中间也能再多给星野爱一些画面就更好了

另外它的 OP 也超好听,我单曲循环了好几天

类别评分 / 10
剧情7
画面7
个人加成0

我家的英雄

疼爱独生女零花的微不足道上班族・鸟栖哲雄,某天因为工作关系和开始一个人生活的零花相约见面,却发现她脸上有被殴打的痕迹。在回家的路上,哲雄看见了像是犯人的男人,并尾随在其後。隔天偷偷回到女儿的公寓,结果却发生了使整个家庭的命运为之一变的事件。父亲为了女儿、为了家庭,走上了修罗之道。高潮迭起的悬疑故事开幕。

image

剧情很精彩,但制作非常非常贫穷,挺可惜的,就是刚才说的说不过去的那个

好在剧情底子非常强,再垃圾的制作也能感受到挺强的紧张感,也能捏着鼻子津津有味地看下去

就不截图了,画面实在看不下去

类别评分 / 10
剧情8
画面1
个人加成0

偶像大师 灰姑娘女孩 U149

讲述一群小学生偶像在早期没有制作人、没有工作的情况下,仍然一步步向着梦想前进的故事。

其实也没什么剧情,就是过家家... 但是很可爱,非常刑

image image image image image

类别评分 / 10
剧情1
画面9
个人加成0

跃动青春

岩仓美津未从乡下的小初中,以第一名的成绩考入了东京的高升学率高中。
这位乡村神童心怀完美的人生蓝图、独自来到东京。她成绩优异,却与他人有着独特的距离感,稍显格格不入。
她虽然偶尔会失败,但还是凭借天真的性格一点点打动班上的同学,使他们那各不相同的性格逐渐交叠。
相遇、相知、最终心意相通。
人人都会有心烦和焦躁之时。而无可替代的朋友,定将带来互相理解的契机。
这是个偶有杂音却能让人不知不觉快乐起来的校园生活喜剧!

image

不管是画面还是剧情看着都很舒服,作品本身是非常棒的,喜欢,但这种平淡剧情不是我喜欢的类型,只能简单吹爆

类别评分 / 10
剧情8
画面8
个人加成-5

其他

另外还有《机动战士高达 水星的魔女》《我心里危险的东西》《放学后失眠的你》也是我正在追的,但我平时能看番的空闲时间没那么多,这几部进度还没跟上,等我全看完可能会写一篇 4 月新番的总结,有机会到时候再一起说

最后祝大家追番愉快!玩王国之泪之余也不要忘记追番哦!

❌