【mdBook】7.2 替代后端

作者:liuyuan77日期:10/2/2025

"后端"是 mdbook 在书籍渲染过程中调用的一个程序。该程序通过 stdin 接收书籍的 JSON 表示和配置信息。一旦后端接收到这些信息,它可以自由地执行任何操作。

有关使用后端的更多信息,请参阅"配置渲染器"章节。

社区已开发了多个后端。请参阅"第三方插件" Wiki 页面以获取可用后端列表。

设置

本页将引导您创建一个简单的字数统计程序形式的替代后端。虽然将使用 Rust 编写,但没有理由不能使用 Python 或 Ruby 等语言实现。

创建项目

首先创建一个新的二进制程序并添加 mdbook 作为依赖:

1$ cargo new --bin mdbook-wordcount
2$ cd mdbook-wordcount
3$ cargo add mdbook
4

基础框架

当我们的 mdbook-wordcount 插件被调用时,mdbook 将通过插件的 stdin 发送 RenderContext 的 JSON 版本。为方便起见,有一个 RenderContext::from_json() 构造函数可以加载 RenderContext

这是我们后端加载书籍所需的所有样板代码:

1// src/main.rs
2extern crate mdbook;
3
4use std::io;
5use mdbook::renderer::RenderContext;
6
7fn main() {
8    let mut stdin = io::stdin();
9    let ctx = RenderContext::from_json(&mut stdin).unwrap();
10}
11

注意RenderContext 包含一个 version 字段。这让后端能够确定它们是否与调用它们的 mdbook 版本兼容。建议后端使用 semver crate 检查此字段,并在可能存在兼容性问题时发出警告。

检查书籍

现在我们的后端有了书籍的副本,让我们统计每个章节的字数!

由于 RenderContext 包含一个 Book 字段(book),而 Book 有用于迭代 Book 中所有项目的 Book::iter() 方法,这一步变得和第一步一样简单。

1fn main() {
2    let mut stdin = io::stdin();
3    let ctx = RenderContext::from_json(&mut stdin).unwrap();
4
5    for item in ctx.book.iter() {
6        if let BookItem::Chapter(ref ch) = *item {
7            let num_words = count_words(ch);
8            println!("{}: {}", ch.name, num_words);
9        }
10    }
11}
12
13fn count_words(ch: &Chapter) -> usize {
14    ch.content.split_whitespace().count()
15}
16

启用后端

现在我们有了基础功能,我们想要实际使用它。首先安装程序:

1$ cargo install --path .
2

然后切换到您想要统计字数的特定书籍目录,并更新其 book.toml 文件:

1[book]
2title = "mdBook Documentation"
3description = "Create book from markdown files. Like Gitbook but implemented in Rust"
4authors = ["Mathieu David", "Michael-F-Bryan"]
5
6[output.html]
7
8[output.wordcount]
9

重要说明:如果要添加自定义后端,还需要确保添加 HTML 后端,即使其表保持为空。

现在您只需要像平常一样构建书籍,一切应该正常工作:

1$ mdbook build
2...
32018-01-16 07:31:15 [INFO] (mdbook::renderer): 调用 "mdbook-wordcount" 渲染器
4mdBook: 126
5Command Line Tool: 224
6init: 283
7build: 145
8...
9

自定义命令

如果我们不需要指定字数统计后端的完整名称/路径,是因为 mdbook 会尝试按照约定推断程序的名称。foo 后端的可执行文件通常称为 mdbook-foo,并在 book.toml 中有相关的 [output.foo] 条目。要明确告诉 mdbook 调用什么命令,可以使用 command 字段:

1[output.wordcount]
2command = "python /path/to/wordcount.py"
3

配置

假设您不想统计特定章节的字数,可以通过在 book.toml 配置文件中向 [output.foo] 表添加项来实现。

创建配置结构

首先将 serdeserde_derive 添加到您的 Cargo.toml

1$ cargo add serde serde_derive
2

然后创建配置结构体:

1extern crate serde;
2#[macro_use]
3extern crate serde_derive;
4
5...
6
7#[derive(Debug, Default, Serialize, Deserialize)]
8#[serde(default, rename_all = "kebab-case")]
9pub struct WordcountConfig {
10  pub ignores: Vec<String>,
11}
12

使用配置

现在我们只需要从 RenderContext 反序列化 WordcountConfig,然后添加检查以确保跳过被忽略的章节:

1fn main() {
2    let mut stdin = io::stdin();
3    let ctx = RenderContext::from_json(&mut stdin).unwrap();
4    let cfg: WordcountConfig = ctx.config
5        .get_deserialized("output.wordcount")
6        .unwrap_or_default();
7
8    for item in ctx.book.iter() {
9        if let BookItem::Chapter(ref ch) = *item {
10            if cfg.ignores.contains(&ch.name) {
11                continue;
12            }
13
14            let num_words = count_words(ch);
15            println!("{}: {}", ch.name, num_words);
16        }
17    }
18}
19

输出和错误处理

输出到文件

除了在构建书籍时将字数统计打印到终端外,将其输出到某个文件也是个好主意。mdbook 通过 RenderContext 中的 destination 字段告诉后端应该将任何生成的输出放置在何处。

1use std::fs::{self, File};
2use std::io::{self, Write};
3use mdbook::renderer::RenderContext;
4use mdbook::book::{BookItem, Chapter};
5
6fn main() {
7    ...
8
9    let _ = fs::create_dir_all(&ctx.destination);
10    let mut f = File::create(ctx.destination.join("wordcounts.txt")).unwrap();
11
12    for item in ctx.book.iter() {
13        if let BookItem::Chapter(ref ch) = *item {
14            ...
15
16            let num_words = count_words(ch);
17            println!("{}: {}", ch.name, num_words);
18            writeln!(f, "{}: {}", ch.name, num_words).unwrap();
19        }
20    }
21}
22

注意:不能保证目标目录存在或为空,因此最好使用 fs::create_dir_all() 创建它。

错误处理

如果在处理书籍时发生错误,mdbook 会将非零退出代码解释为渲染失败。

例如,如果我们想确保所有章节都有偶数个单词,在遇到奇数时出错:

1use std::process;
2...
3
4fn main() {
5    ...
6
7    for item in ctx.book.iter() {
8        if let BookItem::Chapter(ref ch) = *item {
9            ...
10
11            let num_words = count_words(ch);
12            println!("{}: {}", ch.name, num_words);
13            writeln!(f, "{}: {}", ch.name, num_words).unwrap();
14
15            if cfg.deny_odds && num_words % 2 == 1 {
16              eprintln!("{} has an odd number of words!", ch.name);
17              process::exit(1);
18            }
19        }
20    }
21}
22
23#[derive(Debug, Default, Serialize, Deserialize)]
24#[serde(default, rename_all = "kebab-case")]
25pub struct WordcountConfig {
26    pub ignores: Vec<String>,
27    pub deny_odds: bool,
28}
29

现在,如果我们重新安装后端并构建一本书:

1$ cargo install --path . --force
2$ mdbook build /path/to/book
3...
42018-01-16 21:21:39 [INFO] (mdbook::renderer): 调用 "wordcount" 渲染器
5mdBook: 126
6Command Line Tool: 224
7init: 283
8init has an odd number of words!
92018-01-16 21:21:39 [ERROR] (mdbook::renderer): 渲染器以非零返回代码退出。
10

输出建议

鼓励插件遵循"静默规则",只在必要时生成输出(例如生成错误或警告)。所有环境变量都会传递给后端,允许您使用通常的 RUST_LOG 来控制日志详细程度。

总结

虽然这个示例是人为设计的,但希望它能足够展示如何为 mdbook 创建替代后端。本章开头提到的现有后端应该作为实际如何完成的良好示例,因此请随时浏览源代码或提出问题。


【mdBook】7.2 替代后端》 是转载文章,点击查看原文


相关推荐


JavaScript中国手机号校验
蜚鸣9/30/2025

中国手机号校验方法摘要:中国手机号为11位数字,常见号段包括移动(134-139等)、联通(130-132等)、电信(133等)和虚拟运营商(170/171)。校验方法包括:1)基础正则校验(/^1[3-9]\d{9}$/);2)精确号段校验;3)号段数组比对法。建议采用前端简单校验+后端严格验证+短信验证的完整流程。国际号码可使用/^+?[1-9]\d{1,14}$/进行通用校验。(150字)


ZooKeeper详解
三坛海会大神5559/30/2025

Zookeeper从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的反应。它的设计目的是简化分布式系统的管理,保证多个节点之间的数据一致性和协调工作。对于已经存在Leader的情况,机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器来说,仅仅需要和 Leader机器建立连接,并进行状态同步即可。


《Local_Pdf_Chat_RAG 深度学习笔记:PDF 本地化对话的 RAG 原理与实践》
Bug Spray2025/10/2

在处理本地 PDF 文档的智能对话需求时,很多人会面临 “云端依赖泄露隐私”“开源项目部署门槛高” 的问题 —— 而 GitHub 上的 Local_Pdf_Chat_RAG 项目,恰好以 “本地化部署 + RAG 技术” 为核心,解决了 PDF 文档的高效检索与自然语言对话难题。近期我系统学习了这个项目,从环境搭建、代码核心模块拆解,到实际场景测试与调优,踩过不少新手常见的坑,也总结了一套可复用的实践经验。因此整理这份学习笔记,一方面记录自己的技术成长,另一方面希望能帮到同样想入门 “本地 P


神经网络中的损失函数:常见类型与应用场景(代码演示)
fyakm2025/10/2

在神经网络的世界里,损失函数是一个非常重要的概念。它就像是一个“裁判”,能够衡量模型预测结果与真实结果之间的差距。通过了解损失函数的常见类型和应用场景,我们可以根据不同的需求选择合适的损失函数,从而避免因损失函数选择不合理导致的模型性能不佳问题。接下来,我们就一起深入了解一下损失函数的相关知识,并通过Python代码来演示不同损失函数的计算和效果。 目录 损失函数的类型和应用场景均方误差损失函数(Mean Squared Error, MSE)交叉熵损失函数(Cross - Entro


vue2动态实现多Y轴echarts图表,及节点点击事件
四月_h2025/10/3

父组件 <template> <div class="app-container"> <div class="content"> <el-form :model="echartsqueryParams" ref="echartsqueryForm" :inline="true" > <el-form-item label="号" prop="furnaceNumber"> <e


豆包怎么部署到本地?一文读懂AI工具的本地化安装全流程
Nightowls__2025/10/4

在数据自主性与即时响应需求的双重推动下,AI 模型的本地部署成为众多企业和个人用户的追求,豆包作为热门 AI 模型,其本地部署也备受关注。豆包本地部署在哪里?又该如何实现高效且安全的本地化运行呢? 一、为什么要本地部署豆包? 在数据自主性与即时响应需求的双重推动下,AI模型的本地部署逐渐成为企业及个人用户优化服务、保障隐私的关键策略。对于豆包这一主流AI模型而言,本地部署具有显著的优势: 满足多元场景需求:不同行业和领域对AI模型的应用需求千差万别,本地部署豆包能够确保模型在复杂网


QPS和TPS的区别,在实际项目中,如何准确测量和监控QPS和TPS?
IT橘子皮2025/10/5

QPS(Queries Per Second,每秒查询率)和TPS(Transactions Per Second,每秒事务数)是衡量系统性能的两个关键指标,虽然常被混淆,但存在以下核心区别: 1. ​定义与范围​ ​QPS​:表示服务器每秒能响应的查询次数,通常用于衡量特定查询服务器(如DNS、数据库)的处理能力。例如,一次页面加载可能触发多次查询(如HTML、CSS、JS请求),每个查询均计入QPS。 ​TPS​:表示每秒完成的事务数量。一个事务涵盖客户端请求、服务器内部处理及响应的完整流


Python 的 UDP 编程
hubenchang05152025/10/6

#Python 的 UDP 编程 用户数据报协议(User Datagram Protocol) 是一个 无连接、非可靠 的传输层协议,和 TCP 并列,是互联网中最常见的协议之一。 UDP 程序不存在连接,只需要绑定自身地址并收发数据即可。下面是一个示例,它创建了两个 socket,从一个向另一个发送数据。 import socket # 创建 UDP socket sock1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock2


一个基于 ASP.NET Core 的开源、模块化、多租户应用框架和内容管理系统
追逐时光者2025/10/8

前言 今天大姚给大家分享一个基于 ASP.NET Core 的开源、模块化、多租户应用框架和内容管理系统:OrchardCore。 项目介绍 OrchardCore 是一个开源的(BSD-3-Clause license)、模块化的、支持多租户的应用程序框架,使用 ASP.NET Core 构建。同时,它也是一个基于该框架的内容管理系统(CMS)。 DotNetGuide编程学院 DotNetGuide编程学院是一个专注于C#/.NET/.NET Core学习、工作、面试干货和实战教程分享的知识


JDK8 新特性 - Stream 流详解
chirrupy_hamal2025/10/9

文章目录 一、认识 Stream二、Stream 的常用方法1、如何获取 Stream 流2、Stream 流常见的中间方法2.3、Stream 流常见的终结方法 一、认识 Stream 二、Stream 的常用方法 1、如何获取 Stream 流 2、Stream 流常见的中间方法 代码简化 s -> s.getName() Studet::getName 代码简化 2.3、Stream 流常见的终结方法 报错

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0