Skip to content

Commit 3538b57

Browse files
committed
Create renderer
1 parent 970d36c commit 3538b57

File tree

8 files changed

+1076
-7
lines changed

8 files changed

+1076
-7
lines changed

Cargo.lock

Lines changed: 701 additions & 6 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"

mdbook-tera-backend/Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "mdbook-tera-backend"
3+
version = "0.0.1"
4+
authors = ["Martin Geisler <[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 = "Plugins for a mdbook translation workflow based on Gettext."
11+
12+
[dependencies]
13+
chrono = { version = "0.4.31", default-features = false, features = ["alloc"] }
14+
lol_html = "1.2.0"
15+
mdbook = { version = "0.4.25", default-features = false }
16+
serde = "1.0"
17+
serde_json = "1.0.91"
18+
tera = "1.19.1"
19+
thiserror = "1.0.48"
20+
21+
[dev-dependencies]
22+
tempfile = "3.5.0"

mdbook-tera-backend/src/main.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
mod tera_renderer;
2+
3+
use mdbook::renderer::RenderContext;
4+
use std::io;
5+
6+
use crate::tera_renderer::*;
7+
8+
fn main() {
9+
let mut stdin = io::stdin();
10+
// Get the configs
11+
let ctx = RenderContext::from_json(&mut stdin).unwrap();
12+
let config: TeraRendererConfig = ctx
13+
.config
14+
.get_deserialized_opt("output.tera-backend")
15+
.expect("Failed to get Gaia config")
16+
.unwrap();
17+
18+
let components = config
19+
.create_components(&ctx.root)
20+
.expect("Failed to create components");
21+
22+
let mut renderer = Renderer::new(ctx).expect("Failed to create renderer");
23+
24+
for component in components {
25+
renderer.add_component(component);
26+
}
27+
28+
renderer.render_book().expect("Failed to render book");
29+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
use serde::Deserialize;
2+
use std::cell::RefCell;
3+
use std::collections::{BTreeMap, HashMap};
4+
use std::path::{Path, PathBuf};
5+
6+
use tera::Tera;
7+
8+
use crate::{RendererError, Result};
9+
10+
use super::RenderingContext;
11+
12+
fn make_strip_prefix_function() -> impl tera::Function {
13+
move |args: &HashMap<String, serde_json::value::Value>| -> tera::Result<tera::Value> {
14+
let string = args
15+
.get("s")
16+
.ok_or_else(|| tera::Error::from(format!("No s argument provided")))?
17+
.as_str()
18+
.ok_or_else(|| tera::Error::from(format!("S has invalid type, expected string")))?;
19+
let prefix = args
20+
.get("prefix")
21+
.ok_or_else(|| tera::Error::from(format!("No prefix argument provided")))?
22+
.as_str()
23+
.ok_or_else(|| {
24+
tera::Error::from(format!("Prefix has invalid type, expected string"))
25+
})?;
26+
string
27+
.strip_prefix(prefix)
28+
.map(|s| tera::Value::String(s.to_owned()))
29+
.ok_or_else(|| tera::Error::from(format!("Could not strip prefix")))
30+
}
31+
}
32+
33+
pub struct CustomComponent {
34+
template: Tera,
35+
name: String,
36+
/// Used to generate unique ids for each component to prevent collisions in javascript with query selectors.
37+
counter: RefCell<u32>,
38+
}
39+
40+
impl CustomComponent {
41+
pub fn new(name: &str, template_str: &str, dependencies: &[&Self]) -> Result<CustomComponent> {
42+
let mut template = Tera::default();
43+
for dep in dependencies {
44+
template.extend(&dep.template)?;
45+
}
46+
template.add_raw_template(name, template_str)?;
47+
template.register_function("strip_prefix", make_strip_prefix_function());
48+
49+
Ok(CustomComponent {
50+
name: String::from(name),
51+
counter: RefCell::new(0),
52+
template,
53+
})
54+
}
55+
56+
pub fn register_function(&mut self, name: &str, function: impl tera::Function + 'static) {
57+
self.template.register_function(name, function);
58+
}
59+
60+
fn create_context(
61+
&self,
62+
rendering_context: &RenderingContext,
63+
attributes: BTreeMap<String, String>,
64+
) -> tera::Context {
65+
let counter = self.counter.replace_with(|&mut counter| counter + 1);
66+
let mut context = tera::Context::new();
67+
context.insert("counter", &counter);
68+
context.insert("language", &rendering_context.language);
69+
context.insert("path", &rendering_context.path);
70+
context.insert("ctx", &rendering_context.serialized_ctx);
71+
context.insert(
72+
"book_dir",
73+
&rendering_context.ctx.destination.parent().unwrap(),
74+
);
75+
context.insert("attributes", &attributes);
76+
77+
context
78+
}
79+
80+
pub fn render(
81+
&self,
82+
rendering_context: &RenderingContext,
83+
attributes: BTreeMap<String, String>,
84+
) -> Result<String> {
85+
let context = self.create_context(rendering_context, attributes);
86+
let output = self.template.render(&self.name, &context)?;
87+
Ok(output)
88+
}
89+
90+
pub fn component_name(&self) -> String {
91+
self.name.clone()
92+
}
93+
}
94+
95+
#[derive(Deserialize)]
96+
pub struct TeraComponentConfig {
97+
pub name: String,
98+
pub path: PathBuf,
99+
100+
#[serde(default)]
101+
pub dependencies: Vec<String>,
102+
}
103+
104+
#[derive(Deserialize)]
105+
pub struct TeraRendererConfig {
106+
pub components: Vec<TeraComponentConfig>,
107+
}
108+
109+
impl TeraRendererConfig {
110+
pub fn create_components(&self, current_dir: &Path) -> Result<Vec<CustomComponent>> {
111+
let mut name_to_component = HashMap::new();
112+
for component in &self.components {
113+
let component_path = current_dir.join(&component.path);
114+
let template_str = std::fs::read_to_string(&component_path)?;
115+
let dependencies = component
116+
.dependencies
117+
.iter()
118+
.map(|name| {
119+
name_to_component.get(name).ok_or_else(|| {
120+
RendererError::DependencyNotFound(format!(
121+
"Could not find depdendency {}",
122+
name
123+
))
124+
})
125+
})
126+
.collect::<Result<Vec<_>>>()?;
127+
let new_component =
128+
CustomComponent::new(&component.name, &template_str, &dependencies)?;
129+
name_to_component.insert(component.name.clone(), new_component);
130+
}
131+
Ok(name_to_component.into_values().collect())
132+
}
133+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use std::path::StripPrefixError;
2+
3+
use thiserror::Error;
4+
5+
#[derive(Error, Debug)]
6+
pub enum RendererError {
7+
#[error("IO Error: {0}")]
8+
IoError(#[from] std::io::Error),
9+
#[error("Invalid path: {0}")]
10+
InvalidPath(String),
11+
#[error("Error rendering tera template: {0}")]
12+
TeraError(#[from] tera::Error),
13+
#[error("HTML Error: {0}")]
14+
HtmlRewritingError(#[from] lol_html::errors::RewritingError),
15+
#[error("Mdbook Error: {0}")]
16+
Mdbook(#[from] mdbook::errors::Error),
17+
#[error("Error in strip_prefix call: {0}")]
18+
StripPrefixError(#[from] StripPrefixError),
19+
#[error("Serde error: {0}")]
20+
SerdeError(#[from] serde_json::Error),
21+
#[error("Serde error: {0}")]
22+
DependencyNotFound(String),
23+
}
24+
25+
pub type Result<T> = std::result::Result<T, RendererError>;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
mod custom_component;
2+
mod error;
3+
mod renderer;
4+
5+
pub(crate) use custom_component::*;
6+
pub(crate) use error::*;
7+
pub(crate) use renderer::*;
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
use super::error::RendererError;
2+
use super::CustomComponent;
3+
use crate::tera_renderer::error::Result;
4+
use lol_html::html_content::ContentType;
5+
use lol_html::{element, RewriteStrSettings};
6+
use mdbook::renderer::RenderContext;
7+
use serde_json::to_value;
8+
use std::collections::{BTreeMap, HashMap};
9+
use std::fs;
10+
use std::io::{Read, Write};
11+
use std::path::{Path, PathBuf};
12+
use std::sync::Arc;
13+
14+
pub struct RenderingContext<'a> {
15+
pub path: PathBuf,
16+
pub language: Option<String>,
17+
pub serialized_ctx: &'a serde_json::Value,
18+
pub ctx: &'a RenderContext,
19+
}
20+
21+
impl<'a> RenderingContext<'a> {
22+
fn new(
23+
path: PathBuf,
24+
language: Option<String>,
25+
serialized_ctx: &'a serde_json::Value,
26+
ctx: &'a RenderContext,
27+
) -> Result<Self> {
28+
Ok(RenderingContext {
29+
path,
30+
language,
31+
serialized_ctx,
32+
ctx,
33+
})
34+
}
35+
}
36+
37+
pub(crate) struct Renderer {
38+
ctx: Arc<RenderContext>,
39+
serialized_ctx: serde_json::Value,
40+
components: Vec<CustomComponent>,
41+
}
42+
43+
impl Renderer {
44+
pub(crate) fn new(ctx: RenderContext) -> Result<Renderer> {
45+
Ok(Renderer {
46+
serialized_ctx: serde_json::to_value(&ctx)?,
47+
ctx: Arc::new(ctx),
48+
components: Vec::new(),
49+
})
50+
}
51+
52+
pub(crate) fn add_component(&mut self, mut component: CustomComponent) {
53+
component.register_function("get_context", self.create_get_context_function());
54+
self.components.push(component);
55+
}
56+
57+
fn create_get_context_function(&self) -> impl tera::Function {
58+
let ctx_rx = Arc::clone(&self.ctx);
59+
move |args: &HashMap<String, serde_json::value::Value>| -> tera::Result<tera::Value> {
60+
let key = args
61+
.get("key")
62+
.ok_or_else(|| tera::Error::from(format!("No key argument provided")))?
63+
.as_str()
64+
.ok_or_else(|| {
65+
tera::Error::from(format!("Key has invalid type, expected string"))
66+
})?;
67+
let value = ctx_rx
68+
.config
69+
.get(key)
70+
.ok_or_else(|| tera::Error::from(format!("Could not find key {key} in config")))?;
71+
let value = to_value(value)?;
72+
Ok(value)
73+
}
74+
}
75+
76+
pub(crate) fn render_book(&mut self) -> Result<()> {
77+
let dest_dir = &self
78+
.ctx
79+
.destination
80+
.parent()
81+
.ok_or_else(|| {
82+
RendererError::InvalidPath(format!(
83+
"Destination directory {:?} has no parent",
84+
self.ctx.destination
85+
))
86+
})?
87+
.to_owned();
88+
if !dest_dir.is_dir() {
89+
return Err(RendererError::InvalidPath(format!(
90+
"{:?} is not a directory",
91+
dest_dir
92+
)));
93+
}
94+
self.render_book_directory(&dest_dir)
95+
}
96+
97+
fn render_book_directory(&mut self, path: &Path) -> Result<()> {
98+
for entry in path.read_dir()? {
99+
let entry = entry?;
100+
let path = entry.path();
101+
if path.is_dir() {
102+
self.render_book_directory(&path)?;
103+
} else {
104+
self.process_file(&path)?;
105+
}
106+
}
107+
Ok(())
108+
}
109+
110+
fn process_file(&mut self, path: &Path) -> Result<()> {
111+
if path.extension().unwrap_or_default() != "html" {
112+
return Ok(());
113+
}
114+
let mut file_content = String::new();
115+
{
116+
let mut file = fs::File::open(path)?;
117+
file.read_to_string(&mut file_content)?;
118+
}
119+
120+
let output = self.render_components(&file_content, path)?;
121+
let mut output_file = fs::File::create(path)?;
122+
output_file.write_all(output.as_bytes())?;
123+
Ok(())
124+
}
125+
126+
fn render_components(&mut self, file_content: &str, path: &Path) -> Result<String> {
127+
let rendering_context = RenderingContext::new(
128+
path.to_owned(),
129+
self.ctx.config.book.language.clone(),
130+
&self.serialized_ctx,
131+
&self.ctx,
132+
)?;
133+
let custom_components_handlers = self
134+
.components
135+
.iter()
136+
.map(|component| {
137+
element!(component.component_name(), |el| {
138+
let attributes: BTreeMap<String, String> = el
139+
.attributes()
140+
.iter()
141+
.map(|attribute| (attribute.name(), attribute.value()))
142+
.collect();
143+
let rendered = component.render(&rendering_context, attributes)?;
144+
el.replace(&rendered, ContentType::Html);
145+
Ok(())
146+
})
147+
})
148+
.collect();
149+
let output = lol_html::rewrite_str(
150+
file_content,
151+
RewriteStrSettings {
152+
element_content_handlers: custom_components_handlers,
153+
..RewriteStrSettings::default()
154+
},
155+
)?;
156+
Ok(output)
157+
}
158+
}

0 commit comments

Comments
 (0)