12/18/21 Saturday 11PM
我学了点Rust语言,用Rust语言写了个Mdbook预处理插件。
预处理插件
Mdbook预处理插件是一个可执行程序,Mdbook做预处理时会把markdown内容以及配置通过标准输入发给它,它做预处理,把结果输出到标准输出。
以下是本博客的book.toml
配置:
[preprocessor.blog]
这样Mdbook会通过标准输入发配置上下文和书的内容给mdbook-blog
,mdbook-blog
做预处理,把结果打到标准输出。
因此我需要写这个mdbook-blog
,Cargo.toml
:
[package]
name = "mdbook-blog"
version = "0.1.0"
edition = "2018"
[dependencies]
anyhow = "1.0.43"
chrono = "0.4.19"
clap = "2.33.3"
filetime = "0.2.15"
mdbook = "0.4.12"
serde_json = "1.0.67"
实现在src/main.rs
:
use anyhow::Result;
use chrono::{prelude::DateTime, Local};
use clap::{App, Arg, SubCommand};
use filetime::FileTime;
use std::time::{Duration, UNIX_EPOCH};
use mdbook::{
book::{BookItem, BookItem::Chapter},
preprocess::{CmdPreprocessor, PreprocessorContext},
};
use serde_json::to_writer;
use std::{
fs::{metadata, File},
io::{stdin, stdout, BufWriter, Write},
path::Path,
process,
};
fn modified<P: AsRef<Path>>(p1: P, p2: P) -> Result<bool> {
Ok(FileTime::from_last_modification_time(&metadata(p1)?)
> FileTime::from_last_modification_time(&metadata(p2)?))
}
fn last_modification_time<P: AsRef<Path>>(p: P) -> Result<String> {
let dt = DateTime::<Local>::from(
UNIX_EPOCH
+ Duration::from_secs(
FileTime::from_last_modification_time(&metadata(p)?).seconds() as u64,
),
);
Ok(format!(
"{}",
dt.format("<div style=\"text-align: right\"><code>%x %A %_I%p</code></div>")
))
}
fn insert_timestamp(content: &str, timestamp: &str) -> Result<String> {
let mut s = String::new();
let mut lines = content.lines();
let mut next_line = || loop {
if let Some(line) = lines.next() {
if line != "" {
break line;
}
continue;
} else {
break "";
}
};
let line1 = next_line();
if !line1.contains("`") && !line1.contains("<code>") {
s.push_str(&format!("{}\n\n", timestamp));
}
s.push_str(&format!("{}\n", line1));
while let Some(line) = lines.next() {
s.push_str(line);
s.push('\n');
}
Ok(s)
}
fn write_index_html<P: AsRef<Path>>(index: P, target: &str) -> Result<()> {
let file = File::create(index)?;
let mut writer = BufWriter::new(&file);
write!(
&mut writer,
"<head><meta http-equiv=\"refresh\" content=\"0;url={}\"></head>",
target
)?;
Ok(())
}
fn handle(ctx: &PreprocessorContext, section: &mut BookItem) -> Result<()> {
if let Chapter(ref mut ch) = *section {
let ref src = ctx.config.book.src;
if let Some(ref path) = ch.path {
let created_at = last_modification_time(src.join(path))?;
let timestamp = format!("{}", created_at);
ch.content = insert_timestamp(&ch.content, ×tamp)?;
if ch.parent_names.len() == 0 {
let res = modified(src.join(path), src.join("index.html"));
if let Ok(false) = res {
return Ok(());
}
write_index_html(
src.join("index.html"),
path.with_extension("html").to_str().unwrap(),
)?;
}
}
}
Ok(())
}
fn run() -> Result<()> {
let (ctx, mut book) = CmdPreprocessor::parse_input(stdin())?;
book.for_each_mut(|section: &mut BookItem| handle(&ctx, section).unwrap());
to_writer(stdout(), &book)?;
Ok(())
}
fn main() {
let matches = App::new("mdbook-blog")
.about("A mdbook preprocessor for blog")
.subcommand(
SubCommand::with_name("supports")
.arg(Arg::with_name("renderer").required(true))
.about("check whether a renderer is supported"),
)
.get_matches();
if let Some(supports) = matches.subcommand_matches("supports") {
let renderer = supports.value_of("renderer").expect("Required argument");
if renderer != "" {
process::exit(0);
}
process::exit(1);
}
run().unwrap_or_default();
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::remove_file;
use std::io::Read;
#[test]
fn test_newer_than() {
assert!(modified("not-exists-1", "not-exists-2").is_err());
let res = modified("src/main.rs", "src/main.rs");
assert!(res.is_ok());
assert_eq!(false, res.unwrap());
}
#[test]
fn test_write_index_html() {
let index = Path::new("/tmp/index.html");
let target = "2021/euler.html";
let res = write_index_html(index, target);
assert!(res.is_ok());
let mut file = File::open(index).unwrap();
let mut s = String::new();
let size = Read::read_to_string(&mut file, &mut s).unwrap();
assert_eq!(72, size);
assert_eq!(
"<head><meta http-equiv=\"refresh\" content=\"\
0;url=2021/euler.html\"></head>",
s
);
remove_file(index).unwrap();
}
#[test]
fn test_insert_timestamp() {
let text = "hello\nworld!";
let res = insert_timestamp(text, "timestamp").unwrap();
assert_eq!("timestamp\n\nhello\nworld!\n", res);
}
#[test]
fn test_creation_time() {
let ctime = last_modification_time("src/main.rs");
assert_ne!(String::from(""), ctime.unwrap());
}
}
Mdbook与博客
Mdbook为写书而有,并非为博客所写。我把我的博客架构在Mdbook上也就是图省事。博客需要有一个首页,一般的首页是把最新几篇摘要生成一个列表放在那。我这个插件是把首页重定向到最新一篇。这实现简单,而且用起来还不错。
Rust的学习
这个Mdbook插件是我用Rust写的第一个程序。通过这个程序的编写,我对 Rust的包结构,模块组织,基本语法以及错误处理有了点认识。另外着重看了下Rust的自动测试。
Rust把自动测试分为3类:
- 单元测试,
- 文档测试,
- 集成测试。
其中文档测试是其他语言所不具备的。写在文档里的例子,在cargo test
的时候也会被执行。
单元测试如何做,敏捷开发界有许多争议,比如私有方法是否要覆盖之类。但单元测试是否要做,大概是没有争议的。Rust选了比较实际的路线,把选择权给了开发。
集成测试其实就是接口测试,只测对外暴露的接口。libs
集成测试没问题,binary
没有对外接口,咋做呢?Rust给了个方案,同时后src/main.rs
和src/libs.rs
,让binary
也可以成为libs
,这样问题就解决了。
综合其上,三种测试统称为自动测试。一个遗留问题是代码覆盖率,不知如何做。