预处理器

预处理器只是一小段代码,它在书籍加载后和渲染之前立即运行,允许您更新和修改书籍。 可能的用例是:

  • 创建一个自定义 helpers 例如 {{#include /path/to/file.md}}
  • 更新链接,以便HTML渲染器渲染时将 [部分章节1](some_chapter1.md) 自动更改为 [部分章节2](some_chapter2.html)
  • 替换 latex 风格的表达式 ($$ \frac{1}{3} $$) 为 等价的 mathjax 表达式

连接到 MDBook

MDBook 使用一种相当简单的机制来发现第三方插件。 一个新表被添加到 book.toml(例如 foo 预处理器的 preprocessor.foo), 然后 mdbook 将尝试调用 mdbook-foo 程序作为构建过程的一部分。

预处理器可以硬编码来指定preprocessor.foo.renderer 键, 指定他应该运行那些后端。 例如,将 MathJax 用于非 HTML 渲染器是没有意义的。

[book]
title = "My Book"
authors = ["Michael-F-Bryan"]

[preprocessor.foo]
# The command can also be specified manually
command = "python3 /path/to/foo.py"
# Only run the `foo` preprocessor for the HTML and EPUB renderer
renderer = ["html", "epub"]

一旦定义了预处理器并开始构建过程,mdBook 将执行 preprocessor.foo.command 键中定义的命令两次。

它第一次运行为预处理器确定它是否支持给定的渲染器。

mdBook 向进程传递两个参数:第一个参数是字符串supports,第二个参数是渲染器名称。

如果预处理器支持给定的渲染器,则它应该以状态代码 0 退出,如果不支持,则返回非零退出代码。

如果预处理器支持渲染器,则 mdbook 将第二次运行它,且将 JSON 数据传递到 stdin。 JSON 包含一个 [context, book] 数组,其中 context 是序列化的对象 PreprocessorContext,而 book 是包含书籍内容的 Book 对象。

预处理器应该将 Book 对象的 JSON 格式返回到标准输出,并带有它希望执行的任何修改。

最简单的入门方法是创建您自己的 Preprocessor trait 实现(例如在 lib.rs 中), 然后创建一个 shell 二进制文件,将输入转换为正确的 Preprocessor 方法。 为方便起见,examples/ 目录中有一个示例 no-op 预处理器,它可以很容易地适用于其他预处理器。

示例预处理器 no-op
// nop-preprocessors.rs

use crate::nop_lib::Nop;
use clap::{App, Arg, ArgMatches, SubCommand};
use mdbook::book::Book;
use mdbook::errors::Error;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
use semver::{Version, VersionReq};
use std::io;
use std::process;

pub fn make_app() -> App<'static, 'static> {
    App::new("nop-preprocessor")
        .about("A mdbook preprocessor which does precisely nothing")
        .subcommand(
            SubCommand::with_name("supports")
                .arg(Arg::with_name("renderer").required(true))
                .about("Check whether a renderer is supported by this preprocessor"),
        )
}

fn main() {
    let matches = make_app().get_matches();

    // Users will want to construct their own preprocessor here
    let preprocessor = Nop::new();

    if let Some(sub_args) = matches.subcommand_matches("supports") {
        handle_supports(&preprocessor, sub_args);
    } else if let Err(e) = handle_preprocessing(&preprocessor) {
        eprintln!("{}", e);
        process::exit(1);
    }
}

fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
    let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;

    let book_version = Version::parse(&ctx.mdbook_version)?;
    let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;

    if !version_req.matches(&book_version) {
        eprintln!(
            "Warning: The {} plugin was built against version {} of mdbook, \
             but we're being called from version {}",
            pre.name(),
            mdbook::MDBOOK_VERSION,
            ctx.mdbook_version
        );
    }

    let processed_book = pre.run(&ctx, book)?;
    serde_json::to_writer(io::stdout(), &processed_book)?;

    Ok(())
}

fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
    let renderer = sub_args.value_of("renderer").expect("Required argument");
    let supported = pre.supports_renderer(renderer);

    // Signal whether the renderer is supported by exiting with 1 or 0.
    if supported {
        process::exit(0);
    } else {
        process::exit(1);
    }
}

/// The actual implementation of the `Nop` preprocessor. This would usually go
/// in your main `lib.rs` file.
mod nop_lib {
    use super::*;

    /// A no-op preprocessor.
    pub struct Nop;

    impl Nop {
        pub fn new() -> Nop {
            Nop
        }
    }

    impl Preprocessor for Nop {
        fn name(&self) -> &str {
            "nop-preprocessor"
        }

        fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book, Error> {
            // In testing we want to tell the preprocessor to blow up by setting a
            // particular config value
            if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) {
                if nop_cfg.contains_key("blow-up") {
                    anyhow::bail!("Boom!!1!");
                }
            }

            // we *are* a no-op preprocessor after all
            Ok(book)
        }

        fn supports_renderer(&self, renderer: &str) -> bool {
            renderer != "not-supported"
        }
    }
}

实现预处理器的提示

通过将 mdbook 作为库引入,预处理器可以访问现有的基础架构来处理书籍。

例如,自定义预处理器可以使用 CmdPreprocessor::parse_input() 函数反序列化写入stdin的 JSON。 然后可以通过 Book::for_each_mut() 就地修改 Book 的每一章,然后使用 serde_json crate 写入 stdout

章节可以直接访问(通过递归迭代章节)或通过 Book::for_each_mut() 便捷方法访问。

chapter.content 只是一个字符串,恰好是 Markdown。 虽然完全可以使用正则表达式或进行手动查找和替换,但您可能希望将输入处理为对计算机更友好的内容。 pulldown-cmark crate 实现了一个生产质量的基于事件的 Markdown 解析器,使用 pulldown-cmark-to-cmark 允许您将事件转换回 Markdown 文本。

以下代码块显示了如何从 Markdown 中删除所有强调,而不会意外破坏文档。


#![allow(unused)]
fn main() {
fn remove_emphasis(
    num_removed_items: &mut usize,
    chapter: &mut Chapter,
) -> Result<String> {
    let mut buf = String::with_capacity(chapter.content.len());

    let events = Parser::new(&chapter.content).filter(|e| {
        let should_keep = match *e {
            Event::Start(Tag::Emphasis)
            | Event::Start(Tag::Strong)
            | Event::End(Tag::Emphasis)
            | Event::End(Tag::Strong) => false,
            _ => true,
        };
        if !should_keep {
            *num_removed_items += 1;
        }
        should_keep
    });

    cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
        Error::from(format!("Markdown serialization failed: {}", err))
    })
}
}

对于其他所有内容,请查看完整示例

用不同的语言实现预处理器

mdBook 利用 stdinstdout 与预处理器通信的事实使得用 Rust 以外的语言实现它们变得容易。

下面的代码展示了如何在Python中实现一个简单的预处理器,将修改第一章的内容。

下面的示例遵循上面显示的配置,其中 preprocessor.foo.command 实际上指向一个 Python 脚本。

import json
import sys


if __name__ == '__main__':
    if len(sys.argv) > 1: # 我们检查我们是否收到任何参数
        if sys.argv[1] == "supports": 
            # 那么我们最好返回 0 的退出状态代码,因为另一个参数将只是渲染器的名称
            sys.exit(0)

    # 从标准输入加载上下文和书籍表示
    context, book = json.load(sys.stdin)
    # 现在,我们可以修改第一章的内容
    book['sections'][0]['Chapter']['content'] = '# Hello'
    # 我们完成了本书的修改,我们可以将它打印到标准输出,
    print(json.dumps(book))