Skip to content

Commit

Permalink
Create mdbook-tera-backend renderer (#80)
Browse files Browse the repository at this point in the history
Create renderer

Co-authored-by: Martin Geisler <martin@geisler.net>
  • Loading branch information
sakex and mgeisler authored Nov 9, 2023
1 parent cdb6d41 commit 3044744
Show file tree
Hide file tree
Showing 9 changed files with 728 additions and 5 deletions.
367 changes: 367 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
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"
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
This repository contains the following crates that provide extensions and
infrastructure for [mdbook](https://github.com/rust-lang/mdBook/):

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

## Showcases

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

## Changelog

Please see the [i18n-helpers/CHANGELOG](CHANGELOG) for details on the changes in
each release.

### `mdbook-tera-backend`

Run

```shell
$ cargo install mdbook-tera-backend
```

## Contact

For questions or comments, please contact
Expand Down
20 changes: 20 additions & 0 deletions mdbook-tera-backend/Cargo.toml
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"
80 changes: 80 additions & 0 deletions mdbook-tera-backend/README.md
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.
35 changes: 35 additions & 0 deletions mdbook-tera-backend/src/main.rs
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(())
}
2 changes: 2 additions & 0 deletions mdbook-tera-backend/src/tera_renderer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod custom_component;
pub mod renderer;
37 changes: 37 additions & 0 deletions mdbook-tera-backend/src/tera_renderer/custom_component.rs
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, &current_dir.join(template_dir))?;
}

Ok(tera_template)
}
}
174 changes: 174 additions & 0 deletions mdbook-tera-backend/src/tera_renderer/renderer.rs
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(())
}
}

0 comments on commit 3044744

Please sign in to comment.