-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create mdbook-tera-backend renderer (#80)
Create renderer Co-authored-by: Martin Geisler <martin@geisler.net>
- Loading branch information
Showing
9 changed files
with
728 additions
and
5 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
[workspace] | ||
members = ["i18n-helpers"] | ||
members = ["i18n-helpers", "mdbook-tera-backend"] | ||
default-members = ["i18n-helpers"] | ||
resolver = "2" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
[package] | ||
name = "mdbook-tera-backend" | ||
version = "0.0.1" | ||
authors = ["Martin Geisler <mgeisler@google.com>", "Alexandre Senges <asenges@google.com>"] | ||
categories = ["template-engine"] | ||
edition = "2021" | ||
keywords = ["mdbook", "tera", "renderer", "template"] | ||
license = "Apache-2.0" | ||
repository = "https://github.com/google/mdbook-i18n-helpers" | ||
description = "Plugin to extend mdbook with Tera templates and custom HTML components." | ||
|
||
[dependencies] | ||
anyhow = "1.0.75" | ||
mdbook = { version = "0.4.25", default-features = false } | ||
serde = "1.0" | ||
serde_json = "1.0.91" | ||
tera = "1.19.1" | ||
|
||
[dev-dependencies] | ||
tempdir = "0.3.7" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
# Tera backend extension for `mdbook` | ||
|
||
[![Visit crates.io](https://img.shields.io/crates/v/mdbook-i18n-helpers?style=flat-square)](https://crates.io/crates/mdbook-tera-backend) | ||
[![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) | ||
[![GitHub contributors](https://img.shields.io/github/contributors/google/mdbook-i18n-helpers?style=flat-square)](https://github.com/google/mdbook-i18n-helpers/graphs/contributors) | ||
[![GitHub stars](https://img.shields.io/github/stars/google/mdbook-i18n-helpers?style=flat-square)](https://github.com/google/mdbook-i18n-helpers/stargazers) | ||
|
||
This `mdbook` backend makes it possible to use | ||
[tera](https://github.com/Keats/tera) templates and expand the capabilities of | ||
your books. It works on top of the default HTML backend. | ||
|
||
## Installation | ||
|
||
Run | ||
|
||
```shell | ||
$ cargo install mdbook-tera-backend | ||
``` | ||
|
||
## Usage | ||
|
||
### Configuring the backend | ||
|
||
To enable the backend, simply add `[output.tera-backend]` to your `book.toml`, | ||
and configure the place where youre templates will live. For instance | ||
`theme/templates`: | ||
|
||
```toml | ||
[output.html] # You must still enable the html backend. | ||
[output.tera-backend] | ||
template_dir = "theme/templates" | ||
``` | ||
|
||
### Creating templates | ||
|
||
Create your template files in the same directory as your book. | ||
|
||
```html | ||
<!-- ./theme/templates/hello_world.html --> | ||
<div> | ||
Hello world! | ||
</div> | ||
``` | ||
|
||
### Using templates in `index.hbs` | ||
|
||
Since the HTML renderer will first render Handlebars templates, we need to tell | ||
it to ignore Tera templates using `{{{{raw}}}}` blocks: | ||
|
||
```html | ||
{{{{raw}}}} | ||
{% set current_language = ctx.config.book.language %} | ||
<p>Current language: {{ current_language }}</p> | ||
{% include "hello_world.html" %} | ||
{{{{/raw}}}} | ||
``` | ||
|
||
Includes names are based on the file name and not the whole file path. | ||
|
||
### Tera documentation | ||
|
||
Find out all you can do with Tera templates | ||
[here](https://keats.github.io/tera/docs/). | ||
|
||
## Changelog | ||
|
||
Please see [CHANGELOG](../CHANGELOG.md) for details on the changes in each | ||
release. | ||
|
||
## Contact | ||
|
||
For questions or comments, please contact | ||
[Martin Geisler](mailto:mgeisler@google.com) or | ||
[Alexandre Senges](mailto:asenges@google.come) or start a | ||
[discussion](https://github.com/google/mdbook-i18n-helpers/discussions). We | ||
would love to hear from you. | ||
|
||
--- | ||
|
||
This is not an officially supported Google product. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
mod tera_renderer; | ||
|
||
use anyhow::{anyhow, Context}; | ||
use mdbook::renderer::RenderContext; | ||
use std::io; | ||
|
||
use crate::tera_renderer::custom_component::TeraRendererConfig; | ||
use crate::tera_renderer::renderer::Renderer; | ||
|
||
/// Re-renders HTML files outputed by the HTML backend with Tera templates. | ||
/// Please make sure the HTML backend is enabled. | ||
fn main() -> anyhow::Result<()> { | ||
let mut stdin = io::stdin(); | ||
let ctx = RenderContext::from_json(&mut stdin).unwrap(); | ||
if ctx.config.get_renderer("html").is_none() { | ||
return Err(anyhow!( | ||
"Could not find the HTML backend. Please make sure the HTML backend is enabled." | ||
)); | ||
} | ||
let config: TeraRendererConfig = ctx | ||
.config | ||
.get_deserialized_opt("output.tera-backend") | ||
.context("Failed to get tera-backend config")? | ||
.context("No tera-backend config found")?; | ||
|
||
let tera_template = config | ||
.create_template(&ctx.root) | ||
.context("Failed to create components")?; | ||
|
||
let mut renderer = Renderer::new(ctx, tera_template); | ||
|
||
renderer.render_book().context("Failed to render book")?; | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pub mod custom_component; | ||
pub mod renderer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
use anyhow::Result; | ||
use serde::Deserialize; | ||
use std::path::{Path, PathBuf}; | ||
use tera::Tera; | ||
|
||
/// Configuration in `book.toml` `[output.tera-renderer]`. | ||
#[derive(Deserialize)] | ||
pub struct TeraRendererConfig { | ||
/// Relative path to the templates directory from the `book.toml` directory. | ||
pub template_dir: Option<PathBuf>, | ||
} | ||
|
||
/// Recursively add all templates in the `template_dir` to the `tera_template`. | ||
fn add_templates_recursively(tera_template: &mut Tera, directory: &Path) -> Result<()> { | ||
for entry in std::fs::read_dir(directory)? { | ||
let entry = entry?; | ||
let path = entry.path(); | ||
if path.is_dir() { | ||
add_templates_recursively(tera_template, &path)?; | ||
} else { | ||
tera_template.add_template_file(&path, path.file_name().unwrap().to_str())?; | ||
} | ||
} | ||
Ok(()) | ||
} | ||
|
||
impl TeraRendererConfig { | ||
/// Create the `tera_template` and add all templates in the `template_dir` to it. | ||
pub fn create_template(&self, current_dir: &Path) -> Result<Tera> { | ||
let mut tera_template = Tera::default(); | ||
if let Some(template_dir) = &self.template_dir { | ||
add_templates_recursively(&mut tera_template, ¤t_dir.join(template_dir))?; | ||
} | ||
|
||
Ok(tera_template) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
use anyhow::{anyhow, Result}; | ||
use mdbook::renderer::RenderContext; | ||
use std::path::Path; | ||
use tera::Tera; | ||
|
||
/// Renderer for the tera backend. | ||
/// | ||
/// This will read all the files in the `RenderContext` and render them using the `Tera` template. | ||
/// ``` | ||
pub struct Renderer { | ||
ctx: RenderContext, | ||
tera_template: Tera, | ||
} | ||
|
||
impl Renderer { | ||
/// Create a new `Renderer` from the `RenderContext` and `Tera` template. | ||
pub fn new(ctx: RenderContext, tera_template: Tera) -> Self { | ||
Renderer { ctx, tera_template } | ||
} | ||
|
||
/// Render the book. This goes through the output of the HTML renderer | ||
/// by considering all the output HTML files as input to the Tera template. | ||
/// It overwrites the preexisting files with their Tera-rendered version. | ||
pub fn render_book(&mut self) -> Result<()> { | ||
let dest_dir = self.ctx.destination.parent().unwrap().join("html"); | ||
if !dest_dir.is_dir() { | ||
return Err(anyhow!( | ||
"{dest_dir:?} is not a directory. Please make sure the HTML renderer is enabled." | ||
)); | ||
} | ||
self.render_book_directory(&dest_dir) | ||
} | ||
|
||
/// Render the book directory located at `path` recursively. | ||
fn render_book_directory(&mut self, path: &Path) -> Result<()> { | ||
for entry in path.read_dir()? { | ||
let entry = entry?; | ||
let path = entry.path(); | ||
if path.is_dir() { | ||
self.render_book_directory(&path)?; | ||
} else { | ||
self.process_file(&path)?; | ||
} | ||
} | ||
Ok(()) | ||
} | ||
|
||
/// Reads the file at `path` and renders it. | ||
fn process_file(&mut self, path: &Path) -> Result<()> { | ||
if path.extension().unwrap_or_default() != "html" { | ||
return Ok(()); | ||
} | ||
let file_content = std::fs::read_to_string(path)?; | ||
let output = self.render_file_content(&file_content, path)?; | ||
Ok(std::fs::write(path, output)?) | ||
} | ||
|
||
/// Creates the rendering context to be passed to the templates. | ||
/// | ||
/// # Arguments | ||
/// | ||
/// `path`: The path to the file that will be added as extra context to the renderer. | ||
fn create_context(&mut self, path: &Path) -> Result<tera::Context> { | ||
let mut context = tera::Context::new(); | ||
let book_dir = self.ctx.destination.parent().unwrap(); | ||
let relative_path = path.strip_prefix(book_dir).unwrap(); | ||
context.insert("path", &relative_path); | ||
context.insert("book_dir", &self.ctx.destination.parent().unwrap()); | ||
|
||
Ok(context) | ||
} | ||
|
||
/// Rendering logic for an individual file. | ||
fn render_file_content(&mut self, file_content: &str, path: &Path) -> Result<String> { | ||
let tera_context = self.create_context(path)?; | ||
|
||
let rendered_file = self | ||
.tera_template | ||
.render_str(file_content, &tera_context) | ||
.map_err(|e| anyhow!("Error rendering file {path:?}: {e:?}"))?; | ||
Ok(rendered_file) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use tempdir::TempDir; | ||
|
||
use super::*; | ||
use crate::tera_renderer::custom_component::TeraRendererConfig; | ||
use anyhow::Result; | ||
|
||
const RENDER_CONTEXT_STR: &str = r#" | ||
{ | ||
"version":"0.4.32", | ||
"root":"", | ||
"book":{ | ||
"sections": [], | ||
"__non_exhaustive": null | ||
}, | ||
"destination": "", | ||
"config":{ | ||
"book":{ | ||
"authors":[ | ||
"Martin Geisler" | ||
], | ||
"language":"en", | ||
"multilingual":false, | ||
"src":"src", | ||
"title":"Comprehensive Rust 🦀" | ||
}, | ||
"build":{ | ||
"build-dir":"book", | ||
"use-default-preprocessors":true | ||
}, | ||
"output":{ | ||
"tera-backend": { | ||
"template_dir": "templates" | ||
}, | ||
"renderers":[ | ||
"html", | ||
"tera-backend" | ||
] | ||
} | ||
} | ||
}"#; | ||
|
||
const HTML_FILE: &str = r#" | ||
<!DOCTYPE html> | ||
{% include "test_template.html" %} | ||
PATH: {{ path }} | ||
</html> | ||
"#; | ||
|
||
const TEMPLATE_FILE: &str = "RENDERED"; | ||
|
||
const RENDERED_HTML_FILE: &str = r#" | ||
<!DOCTYPE html> | ||
RENDERED | ||
PATH: html/test.html | ||
</html> | ||
"#; | ||
|
||
#[test] | ||
fn test_renderer() -> Result<()> { | ||
let mut ctx = RenderContext::from_json(RENDER_CONTEXT_STR.as_bytes()).unwrap(); | ||
|
||
let tmp_dir = TempDir::new("output")?; | ||
let html_path = tmp_dir.path().join("html"); | ||
let templates_path = tmp_dir.path().join("templates"); | ||
|
||
std::fs::create_dir(&html_path)?; | ||
std::fs::create_dir(&templates_path)?; | ||
|
||
let html_file_path = html_path.join("test.html"); | ||
std::fs::write(&html_file_path, HTML_FILE)?; | ||
std::fs::write(templates_path.join("test_template.html"), TEMPLATE_FILE)?; | ||
|
||
ctx.destination = tmp_dir.path().join("tera-renderer"); | ||
ctx.root = tmp_dir.path().to_owned(); | ||
|
||
let config: TeraRendererConfig = ctx | ||
.config | ||
.get_deserialized_opt("output.tera-backend")? | ||
.ok_or_else(|| anyhow!("No tera backend configuration."))?; | ||
|
||
let tera_template = config.create_template(&ctx.root)?; | ||
let mut renderer = Renderer::new(ctx, tera_template); | ||
renderer.render_book().expect("Failed to render book"); | ||
|
||
assert_eq!(std::fs::read_to_string(html_file_path)?, RENDERED_HTML_FILE); | ||
Ok(()) | ||
} | ||
} |