Skip to content

Commit 2dd19a4

Browse files
committed
Remove references to i18n in renderer
1 parent fdeff79 commit 2dd19a4

File tree

6 files changed

+126
-162
lines changed

6 files changed

+126
-162
lines changed

src/custom_component_renderer/book_directory_renderer.rs

Lines changed: 68 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,118 +3,103 @@ use super::CustomComponent;
33
use crate::custom_component_renderer::error::Result;
44
use lol_html::html_content::ContentType;
55
use lol_html::{element, RewriteStrSettings};
6+
use mdbook::renderer::RenderContext;
7+
use serde_json::to_value;
8+
use std::collections::HashMap;
69
use std::fs;
710
use std::io::{Read, Write};
811
use std::path::{Path, PathBuf};
9-
10-
use std::collections::BTreeMap;
11-
12-
use serde::Deserialize;
13-
14-
/// Configuration specific to the i18n-helpers component.
15-
#[derive(Deserialize, Debug)]
16-
pub struct I18nConfiguration {
17-
pub languages: BTreeMap<String, String>,
18-
pub default_language: Option<String>,
19-
#[serde(default)]
20-
pub translate_all_languages: bool,
21-
}
12+
use std::sync::Arc;
2213

2314
pub struct RenderingContext<'a> {
2415
pub path: PathBuf,
25-
pub language: String,
26-
pub i18_config: &'a I18nConfiguration,
27-
pub language_to_rendered_path: BTreeMap<String, PathBuf>,
16+
pub language: Option<String>,
17+
pub serialized_ctx: &'a serde_json::Value,
18+
pub ctx: &'a RenderContext,
2819
}
2920

3021
impl<'a> RenderingContext<'a> {
3122
fn new(
3223
path: PathBuf,
33-
book_dir: PathBuf,
34-
language: String,
35-
i18_config: &'a I18nConfiguration,
24+
language: Option<String>,
25+
serialized_ctx: &'a serde_json::Value,
26+
ctx: &'a RenderContext,
3627
) -> Result<Self> {
37-
let html_dir = book_dir.join("html");
38-
let mut language_to_rendered_path: BTreeMap<String, PathBuf> = BTreeMap::new();
39-
for identifier in i18_config.languages.keys() {
40-
let mut relative_path = path.strip_prefix(&html_dir)?.to_owned();
41-
if let Ok(without_lang) = relative_path.strip_prefix(&language) {
42-
relative_path = without_lang.to_owned();
43-
}
44-
if Some(identifier) != i18_config.default_language.as_ref() {
45-
relative_path = Path::new(identifier).join(relative_path).to_owned();
46-
}
47-
language_to_rendered_path.insert(
48-
identifier.clone(),
49-
Path::new("/").join(relative_path).to_owned(),
50-
);
51-
}
5228
Ok(RenderingContext {
5329
path,
5430
language,
55-
i18_config,
56-
language_to_rendered_path,
31+
serialized_ctx,
32+
ctx,
5733
})
5834
}
5935
}
6036

6137
pub(crate) struct BookDirectoryRenderer {
62-
book: mdbook::MDBook,
63-
book_dir: PathBuf,
38+
ctx: Arc<RenderContext>,
39+
serialized_ctx: serde_json::Value,
6440
components: Vec<CustomComponent>,
65-
languages_paths: BTreeMap<String, PathBuf>,
6641
}
6742

6843
impl BookDirectoryRenderer {
69-
pub(crate) fn new(book: mdbook::MDBook, book_dir: PathBuf) -> BookDirectoryRenderer {
70-
let default_language = config.default_language.clone();
71-
let languages_paths = config
72-
.languages
73-
.keys()
74-
.filter(|language| {
75-
default_language.is_none() || *language != default_language.as_ref().unwrap()
76-
})
77-
.map(|language| (language.clone(), book_dir.join("html").join(language)))
78-
.collect::<BTreeMap<String, PathBuf>>();
79-
BookDirectoryRenderer {
80-
config,
81-
book,
82-
languages_paths,
83-
book_dir,
44+
pub(crate) fn new(ctx: RenderContext) -> Result<BookDirectoryRenderer> {
45+
Ok(BookDirectoryRenderer {
46+
serialized_ctx: serde_json::to_value(&ctx)?,
47+
ctx: Arc::new(ctx),
8448
components: Vec::new(),
85-
}
49+
})
8650
}
8751

8852
pub(crate) fn render_book(&mut self) -> Result<()> {
89-
let html_dir = self.book_dir.join("html");
90-
if !html_dir.is_dir() {
53+
let dest_dir = &self
54+
.ctx
55+
.destination
56+
.parent()
57+
.ok_or_else(|| {
58+
RendererError::InvalidPath(format!(
59+
"Destination directory {:?} has no parent",
60+
self.ctx.destination
61+
))
62+
})?
63+
.to_owned();
64+
if !dest_dir.is_dir() {
9165
return Err(RendererError::InvalidPath(format!(
9266
"{:?} is not a directory",
93-
self.book_dir
67+
dest_dir
9468
)));
9569
}
96-
self.render_book_directory(&html_dir)
70+
self.render_book_directory(&dest_dir)
9771
}
9872

99-
pub(crate) fn add_component(&mut self, component: CustomComponent) {
100-
self.components.push(component);
73+
fn create_get_context_function(&self) -> impl tera::Function {
74+
let ctx_rx = Arc::clone(&self.ctx);
75+
move |args: &HashMap<String, serde_json::value::Value>| -> tera::Result<tera::Value> {
76+
let key = args
77+
.get("key")
78+
.ok_or_else(|| tera::Error::from(format!("No key argument provided")))?
79+
.as_str()
80+
.ok_or_else(|| {
81+
tera::Error::from(format!("Key has invalid type, expected string"))
82+
})?;
83+
let value = ctx_rx
84+
.config
85+
.get(key)
86+
.ok_or_else(|| tera::Error::from(format!("Could not find key {key} in config")))?;
87+
let value = to_value(value)?;
88+
Ok(value)
89+
}
10190
}
10291

103-
fn extract_language_from_path(&self, path: &Path) -> String {
104-
for (language, language_path) in &self.languages_paths {
105-
if path.starts_with(language_path) {
106-
return language.clone();
107-
}
108-
}
109-
self.config.default_language.clone().unwrap_or_default()
92+
pub(crate) fn add_component(&mut self, mut component: CustomComponent) {
93+
component.register_function("get_context", self.create_get_context_function());
94+
self.components.push(component);
11095
}
11196

11297
fn render_components(&mut self, file_content: &str, path: &Path) -> Result<String> {
11398
let rendering_context = RenderingContext::new(
11499
path.to_owned(),
115-
self.book_dir.clone(),
116-
self.extract_language_from_path(path),
117-
&self.config,
100+
self.ctx.config.book.language.clone(),
101+
&self.serialized_ctx,
102+
&self.ctx,
118103
)?;
119104
let custom_components_handlers = self
120105
.components
@@ -192,11 +177,11 @@ mod tests {
192177
[output.html]
193178
curly-quotes = true
194179
195-
[output.i18n-helpers]
180+
[output.i18n]
196181
default_language = "en"
197182
translate_all_languages = false
198183
199-
[output.i18n-helpers.languages]
184+
[output.i18n.languages]
200185
"en" = "English"
201186
"es" = "Spanish (Español)"
202187
"ko" = "Korean (한국어)"
@@ -222,25 +207,23 @@ mod tests {
222207
std::fs::write(dir.path().join("src/SUMMARY.md"), "")
223208
.expect("Failed to write initial SUMMARY.md");
224209

225-
let mut languages = BTreeMap::new();
226-
languages.insert(String::from("en"), String::from("English"));
227-
languages.insert(String::from("fr"), String::from("French"));
228-
let mock_config = I18nConfiguration {
229-
languages,
230-
default_language: Some(String::from("en")),
231-
translate_all_languages: true,
232-
};
233-
let mdbook = mdbook::MDBook::load(&dir.path()).expect("Failed to load book");
234-
235-
let mut renderer = BookDirectoryRenderer::new(mock_config, mdbook, dir.path().to_owned());
210+
let mdbook = mdbook::MDBook::load(dir.path()).expect("Failed to load mdbook");
211+
let ctx = RenderContext::new(
212+
dir.path(),
213+
mdbook.book,
214+
mdbook.config,
215+
dir.path().join("i18n-helpers"),
216+
);
217+
218+
let mut renderer = BookDirectoryRenderer::new(ctx).expect("Failed to create renderer");
236219
renderer.add_component(standard_templates::create_language_picker_component());
237220
renderer.render_book().expect("Failed to render book");
238221

239222
let mut output = String::new();
240223
let mut file = File::open(dir.path().join("html/test.html")).unwrap();
241224
file.read_to_string(&mut output).unwrap();
242225

243-
const EXPECTED: &str = "<html><body><button id=\"language-toggle0\" class=\"icon-button\" type=\"button\"\n title=\"Change language\" aria-label=\"Change language\"\n aria-haspopup=\"true\" aria-expanded=\"false\"\n aria-controls=\"language-list0\">\n <i class=\"fa fa-globe\"></i>\n</button>\n<ul id=\"language-list0\" class=\"theme-popup\" aria-label=\"Languages\"\n role=\"menu\" style=\"left: auto; right: 10px;\">\n \n <li role=\"none\">\n <a id=\"en\"\n href=\"/test.html\"\n style=\"color: inherit;\">\n <button role=\"menuitem\" class=\"theme theme-selected \">\n English\n </button>\n </a>\n </li>\n \n <li role=\"none\">\n <a id=\"fr\"\n href=\"/fr/test.html\"\n style=\"color: inherit;\">\n <button role=\"menuitem\" class=\"theme \">\n French\n </button>\n </a>\n </li>\n \n</ul>\n\n<script>\n let langToggle = document.getElementById(\"language-toggle0\");\n let langList = document.getElementById(\"language-list0\");\n \n langToggle.addEventListener(\"click\", (event) => {{\n langList.style.display = langList.style.display == \"block\" ? \"none\" : \"block\";\n }});\n \n</script>\n\n<style>\n [dir=rtl] #language-list0 {\n left: 10px;\n right: auto;\n }\n \n</style>\n</html>";
226+
const EXPECTED: &str = "<html><body><button id=\"language-toggle0\" class=\"icon-button\" type=\"button\"\n title=\"Change language\" aria-label=\"Change language\"\n aria-haspopup=\"true\" aria-expanded=\"false\"\n aria-controls=\"language-list0\">\n <i class=\"fa fa-globe\"></i>\n</button>\n<ul id=\"language-list0\" class=\"theme-popup\" aria-label=\"Languages\"\n role=\"menu\" style=\"left: auto; right: 10px;\">\n \n <li role=\"none\">\n <a id=\"en\"\n href=\"/test.html\"\n style=\"color: inherit;\">\n <button role=\"menuitem\" class=\"theme theme-selected \">\n English\n </button>\n </a>\n </li>\n \n <li role=\"none\">\n <a id=\"es\"\n href=\"/es/test.html\"\n style=\"color: inherit;\">\n <button role=\"menuitem\" class=\"theme \">\n Spanish (Español)\n </button>\n </a>\n </li>\n \n <li role=\"none\">\n <a id=\"ko\"\n href=\"/ko/test.html\"\n style=\"color: inherit;\">\n <button role=\"menuitem\" class=\"theme \">\n Korean (한국어)\n </button>\n </a>\n </li>\n \n <li role=\"none\">\n <a id=\"pt-BR\"\n href=\"/pt-BR/test.html\"\n style=\"color: inherit;\">\n <button role=\"menuitem\" class=\"theme \">\n Brazilian Portuguese (Português do Brasil)\n </button>\n </a>\n </li>\n \n</ul>\n\n<script>\n let langToggle = document.getElementById(\"language-toggle0\");\n let langList = document.getElementById(\"language-list0\");\n \n langToggle.addEventListener(\"click\", (event) => {{\n langList.style.display = langList.style.display == \"block\" ? \"none\" : \"block\";\n }});\n \n</script>\n\n<style>\n [dir=rtl] #language-list0 {\n left: 10px;\n right: auto;\n }\n \n</style>\n</html>";
244227

245228
assert_eq!(output, EXPECTED);
246229
}

src/custom_component_renderer/custom_component.rs

Lines changed: 46 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,76 @@
11
use std::cell::RefCell;
2-
use std::collections::{BTreeMap, HashMap};
3-
use std::path::PathBuf;
2+
use std::collections::HashMap;
43

5-
use serde_json::{from_value, to_value};
64
use tera::Tera;
75

86
use crate::Result;
97

108
use super::RenderingContext;
119

10+
const TEMPLATE_NAME: &str = "template";
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+
1233
pub struct CustomComponent {
13-
template: String,
34+
template: Tera,
1435
name: String,
15-
id: RefCell<u32>,
36+
/// Used to generate unique ids for each component to prevent collisions in javascript with query selectors.
37+
counter: RefCell<u32>,
1638
}
1739

1840
impl CustomComponent {
19-
pub fn new(name: &str, template: &str) -> CustomComponent {
20-
CustomComponent {
41+
pub fn new(name: &str, template_str: &str) -> Result<CustomComponent> {
42+
let mut template = Tera::default();
43+
template.add_raw_template(TEMPLATE_NAME, template_str)?;
44+
template.register_function("strip_prefix", make_strip_prefix_function());
45+
Ok(CustomComponent {
2146
name: String::from(name),
22-
template: String::from(template),
23-
id: RefCell::new(0),
24-
}
47+
counter: RefCell::new(0),
48+
template,
49+
})
2550
}
2651

27-
fn make_language_to_rendered_path_function(
28-
language_to_rendered_path: BTreeMap<String, PathBuf>,
29-
) -> impl tera::Function {
30-
Box::new(
31-
move |args: &HashMap<String, tera::Value>| -> tera::Result<tera::Value> {
32-
match args.get("identifier") {
33-
Some(val) => match from_value::<String>(val.clone()) {
34-
Ok(v) => Ok(to_value(language_to_rendered_path.get(&v).ok_or_else(
35-
|| {
36-
tera::Error::from(
37-
"No language with the provided indentifier was found",
38-
)
39-
},
40-
)?)?),
41-
Err(_) => Err("Failed to deserialize argument".into()),
42-
},
43-
None => Err("Identifier argument not provided".into()),
44-
}
45-
},
46-
)
52+
pub fn register_function(&mut self, name: &str, function: impl tera::Function + 'static) {
53+
self.template.register_function(name, function);
4754
}
4855

49-
fn create_context(
50-
&self,
51-
tera_obj: &mut Tera,
52-
rendering_context: &RenderingContext,
53-
) -> tera::Context {
54-
let id = self.id.replace_with(|&mut id| id + 1);
56+
fn create_context(&self, rendering_context: &RenderingContext) -> tera::Context {
57+
let counter = self.counter.replace_with(|&mut counter| counter + 1);
5558
let mut context = tera::Context::new();
56-
context.insert("id", &id);
57-
if rendering_context.language != "en" {
58-
println!("LANGUAGE {}", rendering_context.language);
59-
}
60-
59+
context.insert("counter", &counter);
6160
context.insert("language", &rendering_context.language);
62-
context.insert(
63-
"default_language",
64-
&rendering_context.i18_config.default_language,
65-
);
66-
context.insert("languages", &rendering_context.i18_config.languages);
6761
context.insert("path", &rendering_context.path);
68-
69-
let language_to_rendered_path = rendering_context.language_to_rendered_path.clone();
70-
let language_to_rendered_path_function =
71-
CustomComponent::make_language_to_rendered_path_function(language_to_rendered_path);
72-
tera_obj.register_function(
73-
"language_to_rendered_path",
74-
language_to_rendered_path_function,
62+
context.insert("ctx", &rendering_context.serialized_ctx);
63+
context.insert(
64+
"book_dir",
65+
&rendering_context.ctx.destination.parent().unwrap(),
7566
);
7667

7768
context
7869
}
7970

8071
pub fn render(&self, rendering_context: &RenderingContext) -> Result<String> {
81-
let mut tera = Tera::default();
82-
tera.add_raw_template("template", &self.template)?;
83-
84-
let context = self.create_context(&mut tera, rendering_context);
85-
let output = tera.render("template", &context)?;
72+
let context = self.create_context(rendering_context);
73+
let output = self.template.render(TEMPLATE_NAME, &context)?;
8674
Ok(output)
8775
}
8876

src/custom_component_renderer/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pub enum RendererError {
1616
Mdbook(#[from] mdbook::errors::Error),
1717
#[error("Error in strip_prefix call: {0}")]
1818
StripPrefixError(#[from] StripPrefixError),
19+
#[error("Serde error: {0}")]
20+
SerdeError(#[from] serde_json::Error),
1921
}
2022

2123
pub type Result<T> = std::result::Result<T, RendererError>;

src/custom_component_renderer/standard_templates.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ mod templates {
66

77
pub fn create_language_picker_component() -> CustomComponent {
88
CustomComponent::new("LanguagePicker", templates::LANGUAGE_PICKER)
9+
.expect("Failed to create language picker component")
910
}

0 commit comments

Comments
 (0)