普通视图

上古神器 Beancount:Crypto 与 AI 时代的复式记账终极方案

2025年12月12日 16:58

记账

记账这件事,本质上是在回答三个问题:

  • 钱从哪来?
  • 钱现在在哪?
  • 钱最后去哪了?

听起来很简单,但只要你的金融活动稍微复杂一点,这三问立刻变成一道「多变量方程」:

  • 多个账户:银行卡、支付宝、信用卡、交易所、链上钱包……
  • 多种资产类型:储蓄、贷款、投资……
  • 多种货币:CNY、USD、BTC、各种山寨币……
  • 多个时间维度:分期、预支、借贷……

也正因为如此,我前后换过好几套方案。

2016–2021:网易有钱 - 当时需求很朴素:记人民币收入、储蓄、日常开销。它能自动同步银行流水、支付宝微信账单,对国内场景非常方便。

2021–2024:MoneyWiz - 出国后我需要多币种,并且主要使用英国的银行账户。MoneyWiz 在多币种、海外账户同步上很顺手。

2024:我开始记录加密资产,这时候情况开始非常复杂了,我不只想知道「总额涨了多少」,而是想要:每一笔买入/卖出的明细、不同资产的占比、每日的浮盈浮亏、更严谨的收益率计算。

MoneyWiz 对投资/加密资产支持很有限;我又试了有知有行,它能记录投资前后总额并算收益率,但无法记录到每笔明细,很多价格需要手动维护,浮盈浮亏也无法自动计算。

最后我接触到复式记账和 Beancount,彻底解决了我的问题。

我才意识到,很多时候我们觉得记账麻烦,并不是 UI 不够漂亮、交互不够顺滑,而是记账方法不够科学。当方法足够科学时,你甚至不需要 UI,一个 CLI + 几个纯文本文件 + 一套清晰语法,就能把财务世界描述得非常准确。


复式记账法

复式记账乍看很反直觉,但一旦掌握,会发现它非常强大和灵活,而且会迫使你用更清晰的方式理解金融活动。

五个桶(账户类型)

复式记账会把所有账户归到五类(可以理解成五个装豆子的桶):

  • 资产 Assets:现金、银行存款、有价证券、链上资产……
  • 负债 Liabilities:信用卡欠款、房贷、车贷……
  • 收入 Income:工资、奖金、利息……
  • 花费 Expenses:吃饭、购物、旅行、订阅……
  • 权益 Equity:调节/归档用的「底座」,例如期初余额、误差修正等

所有金融活动都是「桶之间的转移」

  • 收入 → 资产:发工资(收入桶减少,资产桶增加)
  • 资产 → 花费:买东西(资产桶减少,花费桶增加)
  • 负债 → 资产:借钱(负债桶增加,资产桶增加)
  • 资产 → 负债:还款(资产桶减少,负债桶减少)
  • 花费 → 资产:退货(花费桶减少,资产桶增加)
  • 资产 → 收入:投资亏损(资产桶减少,收入桶减少)

你会发现:按这种符号约定,收入账户的余额经常是负数。这不是 bug,而是记账视角不同。

可以这样想象:把「收入」想成一个装着你一生(过去和未来)劳动成果的桶。每次你拿到工资,其实是在从这个桶里取出一部分,放进你的资产里。你能取的越多,收入桶里的数值就越往负的方向走。

所以收入为负数并不表示「你欠了收入」,只是因为你在从这个桶取出资产。这也涉及到复式记账法的一个核心规则:一笔交易里,所有数字相加必须等于 0。这也是它检验正确性的关键:你少写一笔、写错一位数,系统就会当场报警。

在 Beancount 常见的符号约定下,你会看到类似这样的「会计恒等式」写法:

(收入 + 负债) + (资产 + 花费) = 0

你也可以把它理解为更传统的表达:你赚到的、借来的,最终要么变成你拥有的资产,要么被花掉。


Beancount

Beancount 是目前我用过最舒服、也最适合复杂财务世界的纯文本复式记账工具。

它不是复式记账软件的开创者,更早的代表是 Ledger。但 Beancount 在 Ledger 的概念上做了不少改良,也经历了时间的考验(2007 年开始)。

学习 Beancount 最直接的方法是看官方文档:Getting Started with BeancountBeancount Language Syntax,或者看这篇文章也很不错:Beancount —— 命令行复式簿记 | wzyboy’s blog

这篇博客我不会展开讲语法和使用细节,而是用几个例子让你快速感受 Beancount 的强大之处和为什么说它更适合 Crypto 和 AI 时代。

处理复杂交易

使用 Beancount 可以优雅处理复杂交易,以下例子涉及多对多,多账户 + 多人复杂交易,但是可以描述很清楚

2016-02-05 * "饭店" "和室友吃饭"
  Assets:Cash:Wallet                         -300.00 CNY ; 我用现金支付
  Assets:Receivables:Bob                     -200.00 CNY ; 室友帮我付的现金
  Expenses:Food:DiningOut                    +250.00 CNY ; AA 我的一半
  Assets:Receivables:Bob                     +250.00 CNY ; AA 室友的一半

这里同时发生了:

  • 你付了部分现金
  • 室友也付了部分现金
  • AA 分摊
  • 你对室友产生/抵消应收

最后只需要看 Assets:Receivables:Bob 余额就可以知道你欠室友多少钱,看 Expenses:Food:DiningOut 余额就可以知道每个月吃饭开销花了多少钱。

如果用「单式记账 + 备注」硬堆,后面基本一定算不清,而复式记账把结构直接写出来,之后统计就变得非常自然。

处理多币种交易

跨币种交易通常涉及汇率问题,Beancount 也能优雅处理

2016-02-10 * "商店" "买东西"
 Assets:Cash                                 -200.00 USD
 Liabilities:CMB:CreditCards                 -650.00 CNY @@ 100.00 USD
 Expenses:Clothing:Pants                     +150.00 USD
 Expenses:Clothing:Shoes                     +150.00 USD

这类「刷卡以人民币记账、但实际消费以美元计价」的场景,在跨境生活里非常常见。Beancount 的表达方式很直接:用一条记录把两边币种的关系和当时汇率绑定住,之后查询/报表都能基于它继续推导。

更适合 Crypto 交易

Beancount 不限制货币单位:USD、CNY、BTC、USDT、甚至任何你自定义的 token 都可以。再配合类似 SQL 的查询工具 bean-query,拿来做加密资产投资分析非常顺手。

举一个看起来很复杂,但记录很简洁的例子:

2025-07-23 price BTC 1 USD

2025-07-24 * "OKX" "买BTC"
  Assets:Crypto:USD            -150 USD
  Assets:Crypto:BTC            149 BTC  {1 USD}
  Expenses:TradingFee:Crypto      1 USD

2025-07-24 * "OKX" "买BTC"
  Assets:Crypto:USD             -60 USD
  Assets:Crypto:BTC             19 BTC  {3 USD}
  Expenses:TradingFee:Crypto      3 USD

2025-07-24 * "OKX" "卖BTC"
  Assets:Crypto:BTC           -198 BTC  {} @ 2 USD
  Assets:Crypto:USD             298 USD
  Income:Investment

2025-07-25 price BTC 4 USD

这个例子里包含了:

  • 多次买入,成本不同
  • 手续费
  • 卖出时自动匹配成本({} 让 Beancount 自己去找)
  • 市价(price)随时间变化

仅基于这些记录,Beancount 可以自动算出:

  • 总收益/已实现收益/未实现收益
  • 各类收益率(例如资金加权收益率 Modified Dietz、时间加权收益率 TWRR、甚至你自定义的任意收益计算)

换句话说:你只负责把事实写清楚,任何复杂计算都可以交给 Beancount。


我的 Beancount 实践

把账本当代码管理。

编辑器

我用 VSCode,配合 BeancountBeancount Formatter 和以下配置实现语法高亮、自动补全、错误检查、格式化等。

{
  "beancount.mainBeanFile": "main.bean",
  "beancount.runFavaOnActivate": false,
  "beancount.completePayeeNarration": true,
  "beancount.fixedCJKWidth": true,
  "beancount.inputMethods": ["pinyin"],
  "beancount.instantAlignment": false,
  "editor.formatOnSave": true
}

项目结构

Beancount 天然支持模块化:不同类型交易拆到不同文件,再 include 进主入口,维护成本非常低。

.
├── books
│   ├── 2025
│   │   ├── 2025.bean
│   │   ├── cn.bean
│   │   ├── crypto.bean
│   │   ├── daily.bean
│   │   ├── periodic.bean
│   │   ├── prices.bean
│   └── books.bean
├── scripts
│   ├── importer-dbs-credit.js
│   ├── importer-dbs-debit.js
│   ├── importer-okx.js
├── accounts.bean
├── commodity.bean
├── config.pbtxt
├── dashboards.yaml
├── main.bean

main.bean 是入口文件,集中放配置、插件和 include:

option "title" "DIYgod's Book"
option "operating_currency" "USD"
option "booking_method" "LIFO"
option "account_rounding" "Equity:Rounding-Error"

plugin "beancount_periodic.recur"
plugin "beancount_periodic.split"

2024-01-01 custom "fava-option" "auto-reload" "true"
2024-01-01 custom "fava-option" "default_page" "income_statement/?conversion=units&interval=day"

2024-01-01 custom "fava-extension" "fava_portfolio_returns" "{
  'beangrow_config': 'config.pbtxt',
}"

2024-01-01 custom "fava-extension" "fava_dashboards" "{
  'config': 'dashboards.yaml'
}"

include "accounts.bean"
include "books/books.bean"
include "commodity.bean"

accounts.bean 定义所有账户(略去部分):

; Expenses 费用 —— 外出就餐购物旅行等
1995-06-09 open Expenses:Food:Restaurant
; ... 省略若干行 ...

; Income 收入 —— 工资奖金等
2020-04-01 open Income:Salary
; ... 省略若干行 ...

; Assets 资产 —— 现金银行存款有价证券等
2024-01-01 open Assets:Bank:DBS SGD
2024-01-01 open Assets:Crypto:OKX:BTC BTC
; ... 省略若干行 ...

; Liabilities 负债 —— 信用卡房贷车贷等
2024-01-01 open Liabilities:Bank:DBS SGD
; ... 省略若干行 ...

; Equity 权益 —— 用于存放某个时间段开始前已有的豆子
2024-01-01 open Equity:Opening-Balances
2024-01-01 open Equity:Rounding-Error

commodity.bean 定义货币/资产,并用 price 指定价格来源(后面会讲自动更新):

2025-01-01 commodity USDT
  name: "Tether"

2025-01-01 commodity BTC
  name: "Bitcoin"
  price: "USD:coinbase/BTC-USD"

2025-01-01 commodity USD
  name: "United States Dollar"

2025-01-01 commodity QQQ
  name: "NASDAQ-100 Index"
  price: "USD:alphavantage/price:QQQ:USD"

books/2025/prices.bean 用于记录每日价格(自动生成),让持仓浮盈浮亏可被计算:

2025-01-01 price BTC 93346.48 USD
2025-01-02 price BTC 94384.76 USD
; ... 省略若干行 ...

几种常见的 Crypto 交易

下面这些例子来自我的实际习惯:交易不是手填的,而是通过 scripts/importer-okx.js 从 OKX 交易记录自动生成。

稳定币现货交易

1)买入:用 USDT 买 BTC + 手续费

2025-11-14 * "OKX" "Buy BTC with USDT"
  Assets:Crypto:OKX:USDT                       -3022.493 USDT
  Assets:Crypto:OKX:BTC                     0.0302113288 BTC {100000 USDT}
  Expenses:TradingFee:Crypto:OKX

这里我把手续费留空,让 Beancount 自动补齐(利用「每笔交易加起来为 0」的规则)。

但如果你的 BTC 可能来自 USDT、USDC 等不同稳定币,之后统计收益会涉及汇率与币种转换。为了让分析更统一,我常用一个过渡账户把成本统一成 USD:

2025-11-14 * "OKX" "Buy BTC with USDT"
  Assets:Crypto:OKX:USDT                       -3022.493 USDT
  Equity:Exchange                               3022.493 USDT
  Equity:Exchange                              -3022.493 USDT @ 1 USD
  Assets:Crypto:OKX:BTC                     0.0302113288 BTC {100000 USD}
  Expenses:TradingFee:Crypto:OKX

Equity:Exchange 的作用很像「统一换算层」:把不同稳定币交易都折算成 USD 口径,后续报表会非常舒服。

2)卖出:卖 BTC 得 USDC + 自动算收益

2025-09-18 * "OKX" "Sell BTC for USDC"
  Assets:Crypto:OKX:BTC                        -0.100011 BTC {} @ 117600 USD
  Equity:Exchange                         11756.00101788 USD
  Equity:Exchange                        -11756.00101788 USD @ 1 USDC
  Assets:Crypto:OKX:USDC                  11756.00101788 USDC
  Expenses:TradingFee:Crypto:OKX             -5.29258212 USD
  Income:Investment
  • {} @ 117600 USD:成本价由 Beancount 自动从历史记录里匹配,卖出价是 117600 USD
  • 多出来的部分进 Income:Investment:收益就这样自然出来了

如果你不需要统一换算,也可以简化成:

2025-09-18 * "OKX" "Sell BTC for USDC"
  Assets:Crypto:OKX:BTC                        -0.100011 BTC {} @ 117600 USDC
  Assets:Crypto:OKX:USDC                  11756.00101788 USDC
  Expenses:TradingFee:Crypto:OKX             -5.29258212 USDC
  Income:Investment

币币现货交易

用 BTC 买 ETH

2025-08-22 * "OKX" "Buy ETH with BTC"
  Assets:Crypto:OKX:BTC            -0.025828016279999998 BTC {} @ 112480.295 USD
  Equity:Exchange                       2,905.1428904392 USD
  Equity:Exchange                      -2,905.1428904392 USD
  Assets:Crypto:OKX:ETH                     0.6820274505 ETH {4224.16 USD}
  Income:Investment
  Expenses:TradingFee:Crypto:OKX            0.0001705495 ETH {4224.16 USD}

币币交易的成本/收益更容易乱,但把过程统一到 USD 口径后,就会变得非常清晰:用 BTC 换到的 ETH,有明确的 USD 成本;手续费和之后的收益也能被正确计入。

U 本位合约

U 本位合约在交易所内部不是真的卖出 BTC,但为了把盈亏落到资产变化上,我用一个「借币卖出/买回」近似模型来表达:

  • 开仓做空:等价于「借 BTC 卖出,换成 USDT」
  • 平仓:等价于「用 USDT 买回 BTC 归还」
  • 盈亏会自然体现在最后多出来(或少了)的部分上
2025-12-01 * "OKX" "合约 BTC-USDT 做空开仓"
  Assets:Crypto:OKX:Futures:BTC                      -10 BTC @ 100000 USD
  Equity:Exchange                                1000000 USD
  Equity:Exchange                               -1000000 USD @ 1 USDT
  Assets:Crypto:OKX:Futures:USDT                 1000000 USDT

2025-12-10 * "OKX" "合约 BTC-USDT 做空平仓"
  Assets:Crypto:OKX:Futures:USDT                 1000000 USDT
  Equity:Exchange                                1000000 USDT
  Equity:Exchange                               -1000000 USDT @ 1 USD
  Assets:Crypto:OKX:Futures:BTC                      -10 BTC {80000 USD}
  Income:Investment

真实合约还会涉及保证金、资金费、强平、结算币种差异等。更严谨的做法可以继续拆分账户模型,但这套写法已经能在大多数场景里把结果记清楚、算出来。

币本位合约

币本位合约更复杂:仓位通常是「USD 计价的 BTC 价格敞口」,而不是固定数量 BTC。想按每日价格计算浮盈浮亏会很复杂,需要「双账模型」(事实账 + 估值账)。

我目前选择先把它简化成:平仓时手动算出以 BTC 为单位的收益,只记录最终 BTC 变化,只是这样会牺牲当前持仓未实现盈利的数据:

2025-11-21 * "OKX" "合约 BTC-USD 1.0"
  Income:Investment
  Assets:Crypto:OKX:BTC                0.007433571994392 BTC {85400 USD}

自动化脚本 + AI 记账

通过自动化脚本和 AI,可以极大简化记账工作量,让「记账」变成「对账」。

自动生成交易记录

Beancount 是纯文本,这意味着它天生适合自动化生成记录和更新资产价格。

社区里有很多导入工具,比如:beangulpsmart_importer

但我自己用得最多的还是自写脚本(例如 OKX 的 scripts/importer-okx.js、DBS 的 scripts/importer-dbs.js)。原理都类似:从接口或者网页获取交易数据 → 转成 Beancount 文本。

对于一些没有接口、甚至没有账单导出的账户(比如我现在用的 Trust),直接截图丢给 AI 识别也很省心:

自动更新价格

Beancount 本身是离线系统,不会自动从互联网拉价格,所以我每天会把最新的币价/股价写进 books/2025/prices.bean,让报表能计算当日估值与未实现盈利。

官方提供了开箱即用的 beanprice,支持 Coinbase、Coinmarketcap、Alphavantage 等来源,一个命令就能更新:

bean-price -i --update-rate daily --no-cache main.bean >> books/2025/prices.bean

AI 分析与可视化

很多人把记账停在「记下来」就结束了,但真正有价值又容易被遗忘的是记录之后如何分析。

Beancount 在分析和可视化能力上非常夸张:

  • 可以装插件的 Web 界面:Fava
  • 类 SQL 查询语言:BQL(Beancount Query Language)

先看一些我常用的效果:

投资品的价格走势图(Fava 自带):

不同投资品的收益曲线,以及各种加权收益率(插件 Fava Portfolio Returns):

特定资产的变化曲线、分布占比、列表(插件 Fava Dashboards + 自定义 BQL):

更爽的是:现在有了 vibe coding,时代变了。 现在甚至不需要先学会 BQL 怎么写——可以直接让 AI 写。

比如我会这样提需求:

利用 Fava Dashboards 插件,在 dashboards.yaml 编写 BQL 实现一个展示我所有加密货币持仓随时间变化的曲线图。

曾经要折腾好久的个性化财务分析,现在可能就是一句话。


结语

我用了 Beancount 一年,几乎全部需求都很好得到了满足,非常满意,也在公司内部做过分享。这篇博客就是把分享内容整理成文字,希望能把它推荐给更多人。

我也承认,Beancount 的门槛对大多数人来说不低——它更像「写账本的编程语言」,而不是一个点点点的 App。 但我同样相信:随着 AI 发展,这些门槛会越来越低。未来每个人都可以用更低成本驾驭这种强大的工具,实现对自己财务的全面掌控。

如果你也正在经历:

  • 多币种生活
  • 投资品越来越多
  • 账越记越乱
  • 分析越来越复杂

那我建议你至少花半天时间试试 Beancount。 它可能会让你第一次觉得:原来我终于能把钱讲清楚了。

如何优雅编译一个 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 月新番的总结,有机会到时候再一起说

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

❌