Skip to content

Commit 3044744

Browse files
sakexmgeisler
andauthored
Create mdbook-tera-backend renderer (#80)
Create renderer Co-authored-by: Martin Geisler <[email protected]>
1 parent cdb6d41 commit 3044744

File tree

9 files changed

+728
-5
lines changed

9 files changed

+728
-5
lines changed

Cargo.lock

Lines changed: 367 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[workspace]
2-
members = ["i18n-helpers"]
2+
members = ["i18n-helpers", "mdbook-tera-backend"]
33
default-members = ["i18n-helpers"]
44
resolver = "2"

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
This repository contains the following crates that provide extensions and
99
infrastructure for [mdbook](https://github.com/rust-lang/mdBook/):
1010

11-
- [mdbook-i18n-helpers](i18n-helpers/README.md): Gettext translation support for
12-
[mdbook](https://github.com/rust-lang/mdBook/)
11+
- [mdbook-i18n-helpers](./i18n-helpers/README.md): Gettext translation support
12+
for [mdbook](https://github.com/rust-lang/mdBook/)
13+
- [mdbook-tera-backend](./mdbook-tera-backend/README.md): Tera templates
14+
extension for [mdbook](https://github.com/rust-lang/mdBook/)'s HTML renderer.
1315

1416
## Showcases
1517

@@ -39,11 +41,17 @@ cargo install mdbook-i18n-helpers
3941
Please see [USAGE](i18n-helpers/USAGE.md) for how to translate your
4042
[mdbook](https://github.com/rust-lang/mdBook/) project.
4143

42-
## Changelog
43-
4444
Please see the [i18n-helpers/CHANGELOG](CHANGELOG) for details on the changes in
4545
each release.
4646

47+
### `mdbook-tera-backend`
48+
49+
Run
50+
51+
```shell
52+
$ cargo install mdbook-tera-backend
53+
```
54+
4755
## Contact
4856

4957
For questions or comments, please contact

mdbook-tera-backend/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "mdbook-tera-backend"
3+
version = "0.0.1"
4+
authors = ["Martin Geisler <[email protected]>", "Alexandre Senges <[email protected]>"]
5+
categories = ["template-engine"]
6+
edition = "2021"
7+
keywords = ["mdbook", "tera", "renderer", "template"]
8+
license = "Apache-2.0"
9+
repository = "https://github.com/google/mdbook-i18n-helpers"
10+
description = "Plugin to extend mdbook with Tera templates and custom HTML components."
11+
12+
[dependencies]
13+
anyhow = "1.0.75"
14+
mdbook = { version = "0.4.25", default-features = false }
15+
serde = "1.0"
16+
serde_json = "1.0.91"
17+
tera = "1.19.1"
18+
19+
[dev-dependencies]
20+
tempdir = "0.3.7"

mdbook-tera-backend/README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Tera backend extension for `mdbook`
2+
3+
[![Visit crates.io](https://img.shields.io/crates/v/mdbook-i18n-helpers?style=flat-square)](https://crates.io/crates/mdbook-tera-backend)
4+
[![Build workflow](https://img.shields.io/github/actions/workflow/status/google/mdbook-i18n-helpers/test.yml?style=flat-square)](https://github.com/google/mdbook-i18n-helpers/actions/workflows/test.yml?query=branch%3Amain)
5+
[![GitHub contributors](https://img.shields.io/github/contributors/google/mdbook-i18n-helpers?style=flat-square)](https://github.com/google/mdbook-i18n-helpers/graphs/contributors)
6+
[![GitHub stars](https://img.shields.io/github/stars/google/mdbook-i18n-helpers?style=flat-square)](https://github.com/google/mdbook-i18n-helpers/stargazers)
7+
8+
This `mdbook` backend makes it possible to use
9+
[tera](https://github.com/Keats/tera) templates and expand the capabilities of
10+
your books. It works on top of the default HTML backend.
11+
12+
## Installation
13+
14+
Run
15+
16+
```shell
17+
$ cargo install mdbook-tera-backend
18+
```
19+
20+
## Usage
21+
22+
### Configuring the backend
23+
24+
To enable the backend, simply add `[output.tera-backend]` to your `book.toml`,
25+
and configure the place where youre templates will live. For instance
26+
`theme/templates`:
27+
28+
```toml
29+
[output.html] # You must still enable the html backend.
30+
[output.tera-backend]
31+
template_dir = "theme/templates"
32+
```
33+
34+
### Creating templates
35+
36+
Create your template files in the same directory as your book.
37+
38+
```html
39+
<!-- ./theme/templates/hello_world.html -->
40+
<div>
41+
Hello world!
42+
</div>
43+
```
44+
45+
### Using templates in `index.hbs`
46+
47+
Since the HTML renderer will first render Handlebars templates, we need to tell
48+
it to ignore Tera templates using `{{{{raw}}}}` blocks:
49+
50+
```html
51+
{{{{raw}}}}
52+
{% set current_language = ctx.config.book.language %}
53+
<p>Current language: {{ current_language }}</p>
54+
{% include "hello_world.html" %}
55+
{{{{/raw}}}}
56+
```
57+
58+
Includes names are based on the file name and not the whole file path.
59+
60+
### Tera documentation
61+
62+
Find out all you can do with Tera templates
63+
[here](https://keats.github.io/tera/docs/).
64+
65+
## Changelog
66+
67+
Please see [CHANGELOG](../CHANGELOG.md) for details on the changes in each
68+
release.
69+
70+
## Contact
71+
72+
For questions or comments, please contact
73+
[Martin Geisler](mailto:[email protected]) or
74+
[Alexandre Senges](mailto:[email protected]) or start a
75+
[discussion](https://github.com/google/mdbook-i18n-helpers/discussions). We
76+
would love to hear from you.
77+
78+
---
79+
80+
This is not an officially supported Google product.

mdbook-tera-backend/src/main.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
mod tera_renderer;
2+
3+
use anyhow::{anyhow, Context};
4+
use mdbook::renderer::RenderContext;
5+
use std::io;
6+
7+
use crate::tera_renderer::custom_component::TeraRendererConfig;
8+
use crate::tera_renderer::renderer::Renderer;
9+
10+
/// Re-renders HTML files outputed by the HTML backend with Tera templates.
11+
/// Please make sure the HTML backend is enabled.
12+
fn main() -> anyhow::Result<()> {
13+
let mut stdin = io::stdin();
14+
let ctx = RenderContext::from_json(&mut stdin).unwrap();
15+
if ctx.config.get_renderer("html").is_none() {
16+
return Err(anyhow!(
17+
"Could not find the HTML backend. Please make sure the HTML backend is enabled."
18+
));
19+
}
20+
let config: TeraRendererConfig = ctx
21+
.config
22+
.get_deserialized_opt("output.tera-backend")
23+
.context("Failed to get tera-backend config")?
24+
.context("No tera-backend config found")?;
25+
26+
let tera_template = config
27+
.create_template(&ctx.root)
28+
.context("Failed to create components")?;
29+
30+
let mut renderer = Renderer::new(ctx, tera_template);
31+
32+
renderer.render_book().context("Failed to render book")?;
33+
34+
Ok(())
35+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod custom_component;
2+
pub mod renderer;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use anyhow::Result;
2+
use serde::Deserialize;
3+
use std::path::{Path, PathBuf};
4+
use tera::Tera;
5+
6+
/// Configuration in `book.toml` `[output.tera-renderer]`.
7+
#[derive(Deserialize)]
8+
pub struct TeraRendererConfig {
9+
/// Relative path to the templates directory from the `book.toml` directory.
10+
pub template_dir: Option<PathBuf>,
11+
}
12+
13+
/// Recursively add all templates in the `template_dir` to the `tera_template`.
14+
fn add_templates_recursively(tera_template: &mut Tera, directory: &Path) -> Result<()> {
15+
for entry in std::fs::read_dir(directory)? {
16+
let entry = entry?;
17+
let path = entry.path();
18+
if path.is_dir() {
19+
add_templates_recursively(tera_template, &path)?;
20+
} else {
21+
tera_template.add_template_file(&path, path.file_name().unwrap().to_str())?;
22+
}
23+
}
24+
Ok(())
25+
}
26+
27+
impl TeraRendererConfig {
28+
/// Create the `tera_template` and add all templates in the `template_dir` to it.
29+
pub fn create_template(&self, current_dir: &Path) -> Result<Tera> {
30+
let mut tera_template = Tera::default();
31+
if let Some(template_dir) = &self.template_dir {
32+
add_templates_recursively(&mut tera_template, &current_dir.join(template_dir))?;
33+
}
34+
35+
Ok(tera_template)
36+
}
37+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
use anyhow::{anyhow, Result};
2+
use mdbook::renderer::RenderContext;
3+
use std::path::Path;
4+
use tera::Tera;
5+
6+
/// Renderer for the tera backend.
7+
///
8+
/// This will read all the files in the `RenderContext` and render them using the `Tera` template.
9+
/// ```
10+
pub struct Renderer {
11+
ctx: RenderContext,
12+
tera_template: Tera,
13+
}
14+
15+
impl Renderer {
16+
/// Create a new `Renderer` from the `RenderContext` and `Tera` template.
17+
pub fn new(ctx: RenderContext, tera_template: Tera) -> Self {
18+
Renderer { ctx, tera_template }
19+
}
20+
21+
/// Render the book. This goes through the output of the HTML renderer
22+
/// by considering all the output HTML files as input to the Tera template.
23+
/// It overwrites the preexisting files with their Tera-rendered version.
24+
pub fn render_book(&mut self) -> Result<()> {
25+
let dest_dir = self.ctx.destination.parent().unwrap().join("html");
26+
if !dest_dir.is_dir() {
27+
return Err(anyhow!(
28+
"{dest_dir:?} is not a directory. Please make sure the HTML renderer is enabled."
29+
));
30+
}
31+
self.render_book_directory(&dest_dir)
32+
}
33+
34+
/// Render the book directory located at `path` recursively.
35+
fn render_book_directory(&mut self, path: &Path) -> Result<()> {
36+
for entry in path.read_dir()? {
37+
let entry = entry?;
38+
let path = entry.path();
39+
if path.is_dir() {
40+
self.render_book_directory(&path)?;
41+
} else {
42+
self.process_file(&path)?;
43+
}
44+
}
45+
Ok(())
46+
}
47+
48+
/// Reads the file at `path` and renders it.
49+
fn process_file(&mut self, path: &Path) -> Result<()> {
50+
if path.extension().unwrap_or_default() != "html" {
51+
return Ok(());
52+
}
53+
let file_content = std::fs::read_to_string(path)?;
54+
let output = self.render_file_content(&file_content, path)?;
55+
Ok(std::fs::write(path, output)?)
56+
}
57+
58+
/// Creates the rendering context to be passed to the templates.
59+
///
60+
/// # Arguments
61+
///
62+
/// `path`: The path to the file that will be added as extra context to the renderer.
63+
fn create_context(&mut self, path: &Path) -> Result<tera::Context> {
64+
let mut context = tera::Context::new();
65+
let book_dir = self.ctx.destination.parent().unwrap();
66+
let relative_path = path.strip_prefix(book_dir).unwrap();
67+
context.insert("path", &relative_path);
68+
context.insert("book_dir", &self.ctx.destination.parent().unwrap());
69+
70+
Ok(context)
71+
}
72+
73+
/// Rendering logic for an individual file.
74+
fn render_file_content(&mut self, file_content: &str, path: &Path) -> Result<String> {
75+
let tera_context = self.create_context(path)?;
76+
77+
let rendered_file = self
78+
.tera_template
79+
.render_str(file_content, &tera_context)
80+
.map_err(|e| anyhow!("Error rendering file {path:?}: {e:?}"))?;
81+
Ok(rendered_file)
82+
}
83+
}
84+
85+
#[cfg(test)]
86+
mod test {
87+
use tempdir::TempDir;
88+
89+
use super::*;
90+
use crate::tera_renderer::custom_component::TeraRendererConfig;
91+
use anyhow::Result;
92+
93+
const RENDER_CONTEXT_STR: &str = r#"
94+
{
95+
"version":"0.4.32",
96+
"root":"",
97+
"book":{
98+
"sections": [],
99+
"__non_exhaustive": null
100+
},
101+
"destination": "",
102+
"config":{
103+
"book":{
104+
"authors":[
105+
"Martin Geisler"
106+
],
107+
"language":"en",
108+
"multilingual":false,
109+
"src":"src",
110+
"title":"Comprehensive Rust 🦀"
111+
},
112+
"build":{
113+
"build-dir":"book",
114+
"use-default-preprocessors":true
115+
},
116+
"output":{
117+
"tera-backend": {
118+
"template_dir": "templates"
119+
},
120+
"renderers":[
121+
"html",
122+
"tera-backend"
123+
]
124+
}
125+
}
126+
}"#;
127+
128+
const HTML_FILE: &str = r#"
129+
<!DOCTYPE html>
130+
{% include "test_template.html" %}
131+
PATH: {{ path }}
132+
</html>
133+
"#;
134+
135+
const TEMPLATE_FILE: &str = "RENDERED";
136+
137+
const RENDERED_HTML_FILE: &str = r#"
138+
<!DOCTYPE html>
139+
RENDERED
140+
PATH: html/test.html
141+
</html>
142+
"#;
143+
144+
#[test]
145+
fn test_renderer() -> Result<()> {
146+
let mut ctx = RenderContext::from_json(RENDER_CONTEXT_STR.as_bytes()).unwrap();
147+
148+
let tmp_dir = TempDir::new("output")?;
149+
let html_path = tmp_dir.path().join("html");
150+
let templates_path = tmp_dir.path().join("templates");
151+
152+
std::fs::create_dir(&html_path)?;
153+
std::fs::create_dir(&templates_path)?;
154+
155+
let html_file_path = html_path.join("test.html");
156+
std::fs::write(&html_file_path, HTML_FILE)?;
157+
std::fs::write(templates_path.join("test_template.html"), TEMPLATE_FILE)?;
158+
159+
ctx.destination = tmp_dir.path().join("tera-renderer");
160+
ctx.root = tmp_dir.path().to_owned();
161+
162+
let config: TeraRendererConfig = ctx
163+
.config
164+
.get_deserialized_opt("output.tera-backend")?
165+
.ok_or_else(|| anyhow!("No tera backend configuration."))?;
166+
167+
let tera_template = config.create_template(&ctx.root)?;
168+
let mut renderer = Renderer::new(ctx, tera_template);
169+
renderer.render_book().expect("Failed to render book");
170+
171+
assert_eq!(std::fs::read_to_string(html_file_path)?, RENDERED_HTML_FILE);
172+
Ok(())
173+
}
174+
}

0 commit comments

Comments
 (0)