Preprocessors

一个预处理器只是一些代码,运行在加载书之后,和渲染之前,允许您更新和改变本书。可能的用例是:

  • 创建自定义帮助程序{{#include /path/to/file.md}}
  • 更新链接[some chapter](some_chapter.md)自动更改为[some chapter](some_chapter.html),这是 HTML 渲染器功能
  • 用 latex 样式($$ \frac{1}{3} $$)的表达式代替为 mathjax 的等价物

勾住 MDBook

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

虽然预处理器可以进行硬编码,以指定应该运行哪个后端,来处理如preprocessor.foo.renderer的字段(但奇奇怪怪的是,像 MathJax 用于非 HTML 渲染器没有意义).

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

[preprocessor.foo]
# 指定命令的使用
command = "python3 /path/to/foo.py"
#  `foo` 预处理器 只被用于 HTML 和 EPUB 的渲染器
renderer = ["html", "epub"]

在典型的 unix 样式中,插件的所有输入都被写入stdin作为 JSON,和mdbook将从stdout中读取,如果它是期待的输出.

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

Example 无操作预处理器
// nop-preprocessors.rs

extern crate clap;
extern crate mdbook;
extern crate serde_json;

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

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())?;

    if ctx.mdbook_version != mdbook::MDBOOK_VERSION {
        // We should probably use the `semver` crate to check compatibility
        // here...
        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") {
                    return Err("Boom!!1!".into());
                }
            }

            // 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的每一章可以通过Book::for_each_mut()成为可变权限,然后随着serde_json箱写到stdout.

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

chapter.content只是一个恰好是 markdown 的字符串。虽然完全可以使用正则表达式或进行手动查找和替换,但您可能希望将输入处理为更加计算机友好的内容。该pulldown-cmarkcrate 实现了一个基于事件,生产质量的 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))
    })
}
}

对于其他的一切,看完整的例子.