文章目录
  1. 1. zola 源码
  2. 2. 命令行参数解析
  3. 3. 构建命令build的实现
    1. 3.1. Site::new()方法
    2. 3.2. Site::load()方法
    3. 3.3. Site::build()方法

Zola是一个快速的静态站点生成工具,使用Rust语言实现。阅读源代码是很好的学习Rust的方式,今天将探索下Zola项目的源代码。

zola命令行只有4个命令,代码分析也将从命令行开始。

  • init 创建site目录结构
  • build 构建site内容,生成目标位于public目录下
  • serve 构建site并启动本地访问的服务器,服务地址默认为`127.0.0.1:1111`
  • check 构建site但并不生成文件,同时会检查markdown文件中的外部链接

zola 源码

Zola的github源码,2个重要的目录是srccomponents, src下是主要是命令行参数解析,components才是站点内容的核心逻辑。

zola github

先来大概看下Cargo.toml 中的依赖关系,其内容如下。clapclap_complete是用于命令行参数解析与bash脚本自动完成; hypertokio用于构建serve命令使用的本地web服务器; 使用相对路径方式依赖于components目录下的siteerrorsconsoleutilslibs, components/site将会是分析的重点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[[bin]]
name = "zola"

[dependencies]
clap = { version = "4", features = ["derive"] }
clap_complete = "4"
# Below is for the serve cmd
hyper = { version = "0.14.1", default-features = false, features = ["runtime", "server", "http2", "http1"] }
tokio = { version = "1.0.1", default-features = false, features = ["rt", "fs", "time"] }
...
site = { path = "components/site" }
errors = { path = "components/errors" }
console = { path = "components/console" }
utils = { path = "components/utils" }
libs = { path = "components/libs" }

命令行参数解析

src目录的内容如下, src/main.rs是程序运行的起点,src/cmd/下的文件是实现各个子命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ tree src
src
├── cli.rs
├── cmd
│   ├── build.rs
│   ├── check.rs
│   ├── init.rs
│   ├── livereload.js
│   ├── mod.rs
│   └── serve.rs
├── main.rs
├── messages.rs
└── prompt.rs

如下是简化的main.rs, 删除了一些参数检查逻辑与判断。先使用Cli::parse()解析命令行参数,将解析结果cli.command使用match匹配枚举类型Command,匹配时再执行模块cmd下的命令。
子命令为Build时,执行的方法为cmd::build()。命令行参数解析逻辑,使用库clap来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
use cli::{Cli, Command};
use clap::{CommandFactory, Parser};
...

mod cli;
mod cmd;
...
fn main() {
let cli = Cli::parse();
...
match cli.command {
Command::Init { name, force } => {
if let Err(e) = cmd::create_new_project(&name, force) {
...
}
}
Command::Build { base_url, output_dir, force, drafts } => {
...
match cmd::build(...) {
...
}
}
Command::Serve {...} => {
...
if let Err(e) = cmd::serve(...) {
...
}
}
Command::Check { drafts } => {
let (root_dir, config_file) = get_config_file_path(&cli_dir, &cli.config);
match cmd::check(&root_dir, &config_file, None, None, drafts) {
..
}
}
Command::Completion { shell } => {
let cmd = &mut Cli::command();
clap_complete::generate(shell, cmd, cmd.get_name().to_string(), &mut std::io::stdout());
}
}
}

构建命令build的实现

构建命令cmd::build()的实现方法在src/cmd/build.rs文件中。主要逻辑是: 先创建Site对象,然后调用其方法site.load(), site.build()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
use std::path::Path;

use errors::{Error, Result};
use site::Site;

use crate::messages;

pub fn build(
root_dir: &Path,
config_file: &Path,
base_url: Option<&str>,
output_dir: Option<&Path>,
force: bool,
include_drafts: bool,
) -> Result<()> {
let mut site = Site::new(root_dir, config_file)?;
if let Some(output_dir) = output_dir {
if !force && output_dir.exists() {
return Err(Error::msg(format!(
"Directory '{}' already exists. Use --force to overwrite.",
output_dir.display(),
)));
}

site.set_output_path(output_dir);
}
if let Some(b) = base_url {
site.set_base_url(b.to_string());
}
if include_drafts {
site.include_drafts();
}
site.load()?;
messages::notify_site_size(&site);
messages::warn_about_ignored_pages(&site);
site.build()
}

Site::new()方法

Site结构体及其实现的方法在src/components目录下。先看下components目录下的内容,每一个子目录都是一个Rust项目目录。site负责整个构建过程,config管理配置,content管理内容如下, markdown负责markdown内容解析与转换,templates加载模板文件。模块的功能划分还是比较明确的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ tree -L 1 components
components
├── config
├── console
├── content
├── errors
├── imageproc
├── libs
├── link_checker
├── markdown
├── search
├── site
├── templates
└── utils

接下来看下结构体Site的实现,位于文件src/components/site/src/lib.rs中。创建Site时有2个参数,一个是站点site的根目录,另一个是配置文件名,Site::new()方法中通过config::get_config解析配置文件config.tomltemplates::load_tera()加载模板文件,由init命令创建的目录结构会转成完整路径后传给Site

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
use config::{get_config, Config, IndexFormat};
use content::{Library, Page, Paginator, Section, Taxonomy};
use templates::{load_tera, render_redirect_template};

#[derive(Debug)]
pub struct Site {
/// zola site的根目录
pub base_path: PathBuf,
/// 解析过的Config配置数据
pub config: Config,
pub tera: Tera,
imageproc: Arc<Mutex<imageproc::Processor>>,
// the live reload port to be used if there is one
pub live_reload: Option<u16>,
pub output_path: PathBuf,
content_path: PathBuf,
pub static_path: PathBuf,
pub taxonomies: Vec<Taxonomy>,
/// .md 文件(section and pages)与permalink的映射
/// 用于处理内容中有相对路径的链接
pub permalinks: HashMap<String, String>,
/// 包含站点中所有的pages与sections
pub library: Arc<RwLock<Library>>,
/// 是否加载草稿draft pages
include_drafts: bool,
build_mode: BuildMode,
shortcode_definitions: HashMap<String, ShortcodeDefinition>,
}

impl Site {
/// 分析指定目录下的site, 默认是当前目录。path是根目录,config_file是配置文件名
/// Passing in a path is used in tests and when --root argument is passed
pub fn new<P: AsRef<Path>, P2: AsRef<Path>>(path: P, config_file: P2) -> Result<Site> {
let path = path.as_ref();
let config_file = config_file.as_ref();
let mut config = get_config(&path.join(config_file))?;

if let Some(theme) = config.theme.clone() {
// Grab data from the extra section of the theme
config.merge_with_theme(path.join("themes").join(&theme).join("theme.toml"), &theme)?;
}

let tera = load_tera(path, &config)?;
let shortcode_definitions = utils::templates::get_shortcodes(&tera);

let content_path = path.join("content");
let static_path = path.join("static");
let imageproc = imageproc::Processor::new(path.to_path_buf(), &config);
let output_path = path.join(config.output_dir.clone());

let site = Site {
base_path: path.to_path_buf(),
config,
tera,
imageproc: Arc::new(Mutex::new(imageproc)),
live_reload: None,
output_path,
content_path,
static_path,
taxonomies: Vec::new(),
permalinks: HashMap::new(),
include_drafts: false,
// We will allocate it properly later on
library: Arc::new(RwLock::new(Library::default())),
build_mode: BuildMode::Disk,
shortcode_definitions,
};

Ok(site)
}
...
}

Site::load()方法

Site::load() 读取目录content下所有文件,创建pages与sections实例。 这里只列出重要的代码行。主要逻辑是: 遍历content文件夹进行循环处理,是子目录且有_index.md时新增section, 普通文件新增page, 加载时会将md转换成html文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
pub fn load(&mut self) -> Result<()> {
self.library = Arc::new(RwLock::new(Library::new(&self.config)));
// 使用WalkDir遍历文件夹
let mut dir_walker =
WalkDir::new(self.base_path.join("content")).follow_links(true).into_iter();

...
let mut pages = Vec::new();
let mut sections = HashSet::new();

// 循环处理
loop {
let entry: DirEntry = match dir_walker.next() {
None => break,
Some(Err(_)) => continue,
Some(Ok(entry)) => entry,
};
let path = entry.path();
let file_name = match path.file_name() {
None => continue,
Some(name) => name.to_str().unwrap(),
};

// 是否为section
if path.is_dir() {
// 如果是目录,且包含_index.md文件,则是新增一个section

for index_file in index_files {
let section =
Section::from_file(index_file.path(), &self.config, &self.base_path)?;
sections.insert(section.components.join("/"));
...
self.add_section(section, false)?;
}
} else {
// 否则普通.md文件时是新增一个page
let page = Page::from_file(path, &self.config, &self.base_path)?;
pages.push(page);
}
}
self.create_default_index_sections()?;

for page in pages {
...
self.add_page(page, false)?;
}
...

// 加载Tera模板中的使用的全局方法
// taxonomy Tera fns are loaded in `register_early_global_fns`
// so we do need to populate it first.
self.populate_taxonomies()?;
tpls::register_early_global_fns(self)?;
self.populate_sections();
self.render_markdown()?;
...
tpls::register_tera_global_fns(self);
...

Ok(())
}

Site::build()方法

Site::build() 将生成的内容写入public目录,主体就是调用各种render方法生成文件。 其中生成内容在render_sections()方法中,一个section会包含多个page,所以是由render_section方法调用render_page方法。

  • render_sections()是生成section列表,循环调用render_section()方法; render_section()中又对section.page 循环调用render_page()
  • render_section()render_page()方法将分别调用两个结构体SectionPagerender_html()方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
pub fn build(&self) -> Result<()> {
// Do not clean on `zola serve` otherwise we end up copying assets all the time
if self.build_mode == BuildMode::Disk {
self.clean()?;
}

// Generate/move all assets before markdown any content
if let Some(ref theme) = self.config.theme {
let theme_path = self.base_path.join("themes").join(theme);
if theme_path.join("sass").exists() {
sass::compile_sass(&theme_path, &self.output_path)?;
}
}

if self.config.compile_sass {
sass::compile_sass(&self.base_path, &self.output_path)?;
}

if self.config.build_search_index {
self.build_search_index()?;
}

// Render aliases first to allow overwriting
self.render_aliases()?;
self.render_sections()?;
self.render_orphan_pages()?;
self.render_sitemap()?;
...

self.render_themes_css()?;
self.render_404()?;
self.render_robots()?;
self.render_taxonomies()?;
// We process images at the end as we might have picked up images to process from markdown
// or from templates
self.process_images()?;
// Processed images will be in static so the last step is to copy it
self.copy_static_directories()?;

Ok(())
}

参考:

文章目录
  1. 1. zola 源码
  2. 2. 命令行参数解析
  3. 3. 构建命令build的实现
    1. 3.1. Site::new()方法
    2. 3.2. Site::load()方法
    3. 3.3. Site::build()方法