Skip to content

Commit

Permalink
Remove references to i18n in renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
sakex committed Sep 22, 2023
1 parent fdeff79 commit 2dd19a4
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 162 deletions.
153 changes: 68 additions & 85 deletions src/custom_component_renderer/book_directory_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,118 +3,103 @@ use super::CustomComponent;
use crate::custom_component_renderer::error::Result;
use lol_html::html_content::ContentType;
use lol_html::{element, RewriteStrSettings};
use mdbook::renderer::RenderContext;
use serde_json::to_value;
use std::collections::HashMap;
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};

use std::collections::BTreeMap;

use serde::Deserialize;

/// Configuration specific to the i18n-helpers component.
#[derive(Deserialize, Debug)]
pub struct I18nConfiguration {
pub languages: BTreeMap<String, String>,
pub default_language: Option<String>,
#[serde(default)]
pub translate_all_languages: bool,
}
use std::sync::Arc;

pub struct RenderingContext<'a> {
pub path: PathBuf,
pub language: String,
pub i18_config: &'a I18nConfiguration,
pub language_to_rendered_path: BTreeMap<String, PathBuf>,
pub language: Option<String>,
pub serialized_ctx: &'a serde_json::Value,
pub ctx: &'a RenderContext,
}

impl<'a> RenderingContext<'a> {
fn new(
path: PathBuf,
book_dir: PathBuf,
language: String,
i18_config: &'a I18nConfiguration,
language: Option<String>,
serialized_ctx: &'a serde_json::Value,
ctx: &'a RenderContext,
) -> Result<Self> {
let html_dir = book_dir.join("html");
let mut language_to_rendered_path: BTreeMap<String, PathBuf> = BTreeMap::new();
for identifier in i18_config.languages.keys() {
let mut relative_path = path.strip_prefix(&html_dir)?.to_owned();
if let Ok(without_lang) = relative_path.strip_prefix(&language) {
relative_path = without_lang.to_owned();
}
if Some(identifier) != i18_config.default_language.as_ref() {
relative_path = Path::new(identifier).join(relative_path).to_owned();
}
language_to_rendered_path.insert(
identifier.clone(),
Path::new("/").join(relative_path).to_owned(),
);
}
Ok(RenderingContext {
path,
language,
i18_config,
language_to_rendered_path,
serialized_ctx,
ctx,
})
}
}

pub(crate) struct BookDirectoryRenderer {
book: mdbook::MDBook,
book_dir: PathBuf,
ctx: Arc<RenderContext>,
serialized_ctx: serde_json::Value,
components: Vec<CustomComponent>,
languages_paths: BTreeMap<String, PathBuf>,
}

impl BookDirectoryRenderer {
pub(crate) fn new(book: mdbook::MDBook, book_dir: PathBuf) -> BookDirectoryRenderer {
let default_language = config.default_language.clone();
let languages_paths = config
.languages
.keys()
.filter(|language| {
default_language.is_none() || *language != default_language.as_ref().unwrap()
})
.map(|language| (language.clone(), book_dir.join("html").join(language)))
.collect::<BTreeMap<String, PathBuf>>();
BookDirectoryRenderer {
config,
book,
languages_paths,
book_dir,
pub(crate) fn new(ctx: RenderContext) -> Result<BookDirectoryRenderer> {
Ok(BookDirectoryRenderer {
serialized_ctx: serde_json::to_value(&ctx)?,
ctx: Arc::new(ctx),
components: Vec::new(),
}
})
}

pub(crate) fn render_book(&mut self) -> Result<()> {
let html_dir = self.book_dir.join("html");
if !html_dir.is_dir() {
let dest_dir = &self
.ctx
.destination
.parent()
.ok_or_else(|| {
RendererError::InvalidPath(format!(
"Destination directory {:?} has no parent",
self.ctx.destination
))
})?
.to_owned();
if !dest_dir.is_dir() {
return Err(RendererError::InvalidPath(format!(
"{:?} is not a directory",
self.book_dir
dest_dir
)));
}
self.render_book_directory(&html_dir)
self.render_book_directory(&dest_dir)
}

pub(crate) fn add_component(&mut self, component: CustomComponent) {
self.components.push(component);
fn create_get_context_function(&self) -> impl tera::Function {
let ctx_rx = Arc::clone(&self.ctx);
move |args: &HashMap<String, serde_json::value::Value>| -> tera::Result<tera::Value> {
let key = args
.get("key")
.ok_or_else(|| tera::Error::from(format!("No key argument provided")))?
.as_str()
.ok_or_else(|| {
tera::Error::from(format!("Key has invalid type, expected string"))
})?;
let value = ctx_rx
.config
.get(key)
.ok_or_else(|| tera::Error::from(format!("Could not find key {key} in config")))?;
let value = to_value(value)?;
Ok(value)
}
}

fn extract_language_from_path(&self, path: &Path) -> String {
for (language, language_path) in &self.languages_paths {
if path.starts_with(language_path) {
return language.clone();
}
}
self.config.default_language.clone().unwrap_or_default()
pub(crate) fn add_component(&mut self, mut component: CustomComponent) {
component.register_function("get_context", self.create_get_context_function());
self.components.push(component);
}

fn render_components(&mut self, file_content: &str, path: &Path) -> Result<String> {
let rendering_context = RenderingContext::new(
path.to_owned(),
self.book_dir.clone(),
self.extract_language_from_path(path),
&self.config,
self.ctx.config.book.language.clone(),
&self.serialized_ctx,
&self.ctx,
)?;
let custom_components_handlers = self
.components
Expand Down Expand Up @@ -192,11 +177,11 @@ mod tests {
[output.html]
curly-quotes = true
[output.i18n-helpers]
[output.i18n]
default_language = "en"
translate_all_languages = false
[output.i18n-helpers.languages]
[output.i18n.languages]
"en" = "English"
"es" = "Spanish (Español)"
"ko" = "Korean (한국어)"
Expand All @@ -222,25 +207,23 @@ mod tests {
std::fs::write(dir.path().join("src/SUMMARY.md"), "")
.expect("Failed to write initial SUMMARY.md");

let mut languages = BTreeMap::new();
languages.insert(String::from("en"), String::from("English"));
languages.insert(String::from("fr"), String::from("French"));
let mock_config = I18nConfiguration {
languages,
default_language: Some(String::from("en")),
translate_all_languages: true,
};
let mdbook = mdbook::MDBook::load(&dir.path()).expect("Failed to load book");

let mut renderer = BookDirectoryRenderer::new(mock_config, mdbook, dir.path().to_owned());
let mdbook = mdbook::MDBook::load(dir.path()).expect("Failed to load mdbook");
let ctx = RenderContext::new(
dir.path(),
mdbook.book,
mdbook.config,
dir.path().join("i18n-helpers"),
);

let mut renderer = BookDirectoryRenderer::new(ctx).expect("Failed to create renderer");
renderer.add_component(standard_templates::create_language_picker_component());
renderer.render_book().expect("Failed to render book");

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

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>";
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>";

assert_eq!(output, EXPECTED);
}
Expand Down
104 changes: 46 additions & 58 deletions src/custom_component_renderer/custom_component.rs
Original file line number Diff line number Diff line change
@@ -1,88 +1,76 @@
use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap};
use std::path::PathBuf;
use std::collections::HashMap;

use serde_json::{from_value, to_value};
use tera::Tera;

use crate::Result;

use super::RenderingContext;

const TEMPLATE_NAME: &str = "template";

fn make_strip_prefix_function() -> impl tera::Function {
move |args: &HashMap<String, serde_json::value::Value>| -> tera::Result<tera::Value> {
let string = args
.get("s")
.ok_or_else(|| tera::Error::from(format!("No s argument provided")))?
.as_str()
.ok_or_else(|| tera::Error::from(format!("S has invalid type, expected string")))?;
let prefix = args
.get("prefix")
.ok_or_else(|| tera::Error::from(format!("No prefix argument provided")))?
.as_str()
.ok_or_else(|| {
tera::Error::from(format!("Prefix has invalid type, expected string"))
})?;
string
.strip_prefix(prefix)
.map(|s| tera::Value::String(s.to_owned()))
.ok_or_else(|| tera::Error::from(format!("Could not strip prefix")))
}
}

pub struct CustomComponent {
template: String,
template: Tera,
name: String,
id: RefCell<u32>,
/// Used to generate unique ids for each component to prevent collisions in javascript with query selectors.
counter: RefCell<u32>,
}

impl CustomComponent {
pub fn new(name: &str, template: &str) -> CustomComponent {
CustomComponent {
pub fn new(name: &str, template_str: &str) -> Result<CustomComponent> {
let mut template = Tera::default();
template.add_raw_template(TEMPLATE_NAME, template_str)?;
template.register_function("strip_prefix", make_strip_prefix_function());
Ok(CustomComponent {
name: String::from(name),
template: String::from(template),
id: RefCell::new(0),
}
counter: RefCell::new(0),
template,
})
}

fn make_language_to_rendered_path_function(
language_to_rendered_path: BTreeMap<String, PathBuf>,
) -> impl tera::Function {
Box::new(
move |args: &HashMap<String, tera::Value>| -> tera::Result<tera::Value> {
match args.get("identifier") {
Some(val) => match from_value::<String>(val.clone()) {
Ok(v) => Ok(to_value(language_to_rendered_path.get(&v).ok_or_else(
|| {
tera::Error::from(
"No language with the provided indentifier was found",
)
},
)?)?),
Err(_) => Err("Failed to deserialize argument".into()),
},
None => Err("Identifier argument not provided".into()),
}
},
)
pub fn register_function(&mut self, name: &str, function: impl tera::Function + 'static) {
self.template.register_function(name, function);
}

fn create_context(
&self,
tera_obj: &mut Tera,
rendering_context: &RenderingContext,
) -> tera::Context {
let id = self.id.replace_with(|&mut id| id + 1);
fn create_context(&self, rendering_context: &RenderingContext) -> tera::Context {
let counter = self.counter.replace_with(|&mut counter| counter + 1);
let mut context = tera::Context::new();
context.insert("id", &id);
if rendering_context.language != "en" {
println!("LANGUAGE {}", rendering_context.language);
}

context.insert("counter", &counter);
context.insert("language", &rendering_context.language);
context.insert(
"default_language",
&rendering_context.i18_config.default_language,
);
context.insert("languages", &rendering_context.i18_config.languages);
context.insert("path", &rendering_context.path);

let language_to_rendered_path = rendering_context.language_to_rendered_path.clone();
let language_to_rendered_path_function =
CustomComponent::make_language_to_rendered_path_function(language_to_rendered_path);
tera_obj.register_function(
"language_to_rendered_path",
language_to_rendered_path_function,
context.insert("ctx", &rendering_context.serialized_ctx);
context.insert(
"book_dir",
&rendering_context.ctx.destination.parent().unwrap(),
);

context
}

pub fn render(&self, rendering_context: &RenderingContext) -> Result<String> {
let mut tera = Tera::default();
tera.add_raw_template("template", &self.template)?;

let context = self.create_context(&mut tera, rendering_context);
let output = tera.render("template", &context)?;
let context = self.create_context(rendering_context);
let output = self.template.render(TEMPLATE_NAME, &context)?;
Ok(output)
}

Expand Down
2 changes: 2 additions & 0 deletions src/custom_component_renderer/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub enum RendererError {
Mdbook(#[from] mdbook::errors::Error),
#[error("Error in strip_prefix call: {0}")]
StripPrefixError(#[from] StripPrefixError),
#[error("Serde error: {0}")]
SerdeError(#[from] serde_json::Error),
}

pub type Result<T> = std::result::Result<T, RendererError>;
1 change: 1 addition & 0 deletions src/custom_component_renderer/standard_templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ mod templates {

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

0 comments on commit 2dd19a4

Please sign in to comment.