You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

531 lines
18KB

  1. use std::collections::HashMap;
  2. use std::path::PathBuf;
  3. use std::sync::{Arc, Mutex, RwLock};
  4. use tera::{from_value, to_value, Error, Function as TeraFn, Result, Value};
  5. use config::Config;
  6. use image;
  7. use image::GenericImageView;
  8. use library::{Library, Taxonomy};
  9. use utils::site::resolve_internal_link;
  10. use imageproc;
  11. #[macro_use]
  12. mod macros;
  13. mod load_data;
  14. pub use self::load_data::LoadData;
  15. #[derive(Debug)]
  16. pub struct Trans {
  17. config: Config,
  18. }
  19. impl Trans {
  20. pub fn new(config: Config) -> Self {
  21. Self { config }
  22. }
  23. }
  24. impl TeraFn for Trans {
  25. fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
  26. let key = required_arg!(String, args.get("key"), "`trans` requires a `key` argument.");
  27. let lang = optional_arg!(String, args.get("lang"), "`trans`: `lang` must be a string.")
  28. .unwrap_or_else(|| self.config.default_language.clone());
  29. let translations = &self.config.translations[lang.as_str()];
  30. Ok(to_value(&translations[key.as_str()]).unwrap())
  31. }
  32. }
  33. #[derive(Debug)]
  34. pub struct GetUrl {
  35. config: Config,
  36. permalinks: HashMap<String, String>,
  37. }
  38. impl GetUrl {
  39. pub fn new(config: Config, permalinks: HashMap<String, String>) -> Self {
  40. Self { config, permalinks }
  41. }
  42. }
  43. impl TeraFn for GetUrl {
  44. fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
  45. let cachebust =
  46. args.get("cachebust").map_or(false, |c| from_value::<bool>(c.clone()).unwrap_or(false));
  47. let trailing_slash = args
  48. .get("trailing_slash")
  49. .map_or(false, |c| from_value::<bool>(c.clone()).unwrap_or(false));
  50. let path = required_arg!(
  51. String,
  52. args.get("path"),
  53. "`get_url` requires a `path` argument with a string value"
  54. );
  55. if path.starts_with("@/") {
  56. match resolve_internal_link(&path, &self.permalinks) {
  57. Ok(resolved) => Ok(to_value(resolved.permalink).unwrap()),
  58. Err(_) => {
  59. Err(format!("Could not resolve URL for link `{}` not found.", path).into())
  60. }
  61. }
  62. } else {
  63. // anything else
  64. let mut permalink = self.config.make_permalink(&path);
  65. if !trailing_slash && permalink.ends_with('/') {
  66. permalink.pop(); // Removes the slash
  67. }
  68. if cachebust {
  69. permalink = format!("{}?t={}", permalink, self.config.build_timestamp.unwrap());
  70. }
  71. Ok(to_value(permalink).unwrap())
  72. }
  73. }
  74. }
  75. #[derive(Debug)]
  76. pub struct ResizeImage {
  77. imageproc: Arc<Mutex<imageproc::Processor>>,
  78. }
  79. impl ResizeImage {
  80. pub fn new(imageproc: Arc<Mutex<imageproc::Processor>>) -> Self {
  81. Self { imageproc }
  82. }
  83. }
  84. static DEFAULT_OP: &'static str = "fill";
  85. static DEFAULT_FMT: &'static str = "auto";
  86. const DEFAULT_Q: u8 = 75;
  87. impl TeraFn for ResizeImage {
  88. fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
  89. let path = required_arg!(
  90. String,
  91. args.get("path"),
  92. "`resize_image` requires a `path` argument with a string value"
  93. );
  94. let width = optional_arg!(
  95. u32,
  96. args.get("width"),
  97. "`resize_image`: `width` must be a non-negative integer"
  98. );
  99. let height = optional_arg!(
  100. u32,
  101. args.get("height"),
  102. "`resize_image`: `height` must be a non-negative integer"
  103. );
  104. let op = optional_arg!(String, args.get("op"), "`resize_image`: `op` must be a string")
  105. .unwrap_or_else(|| DEFAULT_OP.to_string());
  106. let format =
  107. optional_arg!(String, args.get("format"), "`resize_image`: `format` must be a string")
  108. .unwrap_or_else(|| DEFAULT_FMT.to_string());
  109. let quality =
  110. optional_arg!(u8, args.get("quality"), "`resize_image`: `quality` must be a number")
  111. .unwrap_or(DEFAULT_Q);
  112. if quality == 0 || quality > 100 {
  113. return Err("`resize_image`: `quality` must be in range 1-100".to_string().into());
  114. }
  115. let mut imageproc = self.imageproc.lock().unwrap();
  116. if !imageproc.source_exists(&path) {
  117. return Err(format!("`resize_image`: Cannot find path: {}", path).into());
  118. }
  119. let imageop = imageproc::ImageOp::from_args(path, &op, width, height, &format, quality)
  120. .map_err(|e| format!("`resize_image`: {}", e))?;
  121. let url = imageproc.insert(imageop);
  122. to_value(url).map_err(|err| err.into())
  123. }
  124. }
  125. #[derive(Debug)]
  126. pub struct GetImageMeta {
  127. content_path: PathBuf,
  128. }
  129. impl GetImageMeta {
  130. pub fn new(content_path: PathBuf) -> Self {
  131. Self { content_path }
  132. }
  133. }
  134. impl TeraFn for GetImageMeta {
  135. fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
  136. let path = required_arg!(
  137. String,
  138. args.get("path"),
  139. "`get_image_meta` requires a `path` argument with a string value"
  140. );
  141. let src_path = self.content_path.join(&path);
  142. if !src_path.exists() {
  143. return Err(format!("`get_image_meta`: Cannot find path: {}", path).into());
  144. }
  145. let img = image::open(&src_path)
  146. .map_err(|e| Error::chain(format!("Failed to process image: {}", path), e))?;
  147. let mut map = tera::Map::new();
  148. map.insert(String::from("height"), Value::Number(tera::Number::from(img.height())));
  149. map.insert(String::from("width"), Value::Number(tera::Number::from(img.width())));
  150. Ok(Value::Object(map))
  151. }
  152. }
  153. #[derive(Debug)]
  154. pub struct GetTaxonomyUrl {
  155. taxonomies: HashMap<String, HashMap<String, String>>,
  156. default_lang: String,
  157. }
  158. impl GetTaxonomyUrl {
  159. pub fn new(default_lang: &str, all_taxonomies: &[Taxonomy]) -> Self {
  160. let mut taxonomies = HashMap::new();
  161. for taxo in all_taxonomies {
  162. let mut items = HashMap::new();
  163. for item in &taxo.items {
  164. items.insert(item.name.clone(), item.permalink.clone());
  165. }
  166. taxonomies.insert(format!("{}-{}", taxo.kind.name, taxo.kind.lang), items);
  167. }
  168. Self { taxonomies, default_lang: default_lang.to_string() }
  169. }
  170. }
  171. impl TeraFn for GetTaxonomyUrl {
  172. fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
  173. let kind = required_arg!(
  174. String,
  175. args.get("kind"),
  176. "`get_taxonomy_url` requires a `kind` argument with a string value"
  177. );
  178. let name = required_arg!(
  179. String,
  180. args.get("name"),
  181. "`get_taxonomy_url` requires a `name` argument with a string value"
  182. );
  183. let lang = optional_arg!(String, args.get("lang"), "`get_taxonomy`: `lang` must be a string")
  184. .unwrap_or_else(|| self.default_lang.clone());
  185. let container = match self.taxonomies.get(&format!("{}-{}", kind, lang)) {
  186. Some(c) => c,
  187. None => {
  188. return Err(format!(
  189. "`get_taxonomy_url` received an unknown taxonomy as kind: {}",
  190. kind
  191. )
  192. .into());
  193. }
  194. };
  195. if let Some(permalink) = container.get(&name) {
  196. return Ok(to_value(permalink).unwrap());
  197. }
  198. Err(format!("`get_taxonomy_url`: couldn't find `{}` in `{}` taxonomy", name, kind).into())
  199. }
  200. }
  201. #[derive(Debug)]
  202. pub struct GetPage {
  203. base_path: PathBuf,
  204. library: Arc<RwLock<Library>>,
  205. }
  206. impl GetPage {
  207. pub fn new(base_path: PathBuf, library: Arc<RwLock<Library>>) -> Self {
  208. Self { base_path: base_path.join("content"), library }
  209. }
  210. }
  211. impl TeraFn for GetPage {
  212. fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
  213. let path = required_arg!(
  214. String,
  215. args.get("path"),
  216. "`get_page` requires a `path` argument with a string value"
  217. );
  218. let full_path = self.base_path.join(&path);
  219. let library = self.library.read().unwrap();
  220. match library.get_page(&full_path) {
  221. Some(p) => Ok(to_value(p.to_serialized(&library)).unwrap()),
  222. None => Err(format!("Page `{}` not found.", path).into()),
  223. }
  224. }
  225. }
  226. #[derive(Debug)]
  227. pub struct GetSection {
  228. base_path: PathBuf,
  229. library: Arc<RwLock<Library>>,
  230. }
  231. impl GetSection {
  232. pub fn new(base_path: PathBuf, library: Arc<RwLock<Library>>) -> Self {
  233. Self { base_path: base_path.join("content"), library }
  234. }
  235. }
  236. impl TeraFn for GetSection {
  237. fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
  238. let path = required_arg!(
  239. String,
  240. args.get("path"),
  241. "`get_section` requires a `path` argument with a string value"
  242. );
  243. let metadata_only = args
  244. .get("metadata_only")
  245. .map_or(false, |c| from_value::<bool>(c.clone()).unwrap_or(false));
  246. let full_path = self.base_path.join(&path);
  247. let library = self.library.read().unwrap();
  248. match library.get_section(&full_path) {
  249. Some(s) => {
  250. if metadata_only {
  251. Ok(to_value(s.to_serialized_basic(&library)).unwrap())
  252. } else {
  253. Ok(to_value(s.to_serialized(&library)).unwrap())
  254. }
  255. }
  256. None => Err(format!("Section `{}` not found.", path).into()),
  257. }
  258. }
  259. }
  260. #[derive(Debug)]
  261. pub struct GetTaxonomy {
  262. library: Arc<RwLock<Library>>,
  263. taxonomies: HashMap<String, Taxonomy>,
  264. default_lang: String,
  265. }
  266. impl GetTaxonomy {
  267. pub fn new(default_lang: &str, all_taxonomies: Vec<Taxonomy>, library: Arc<RwLock<Library>>) -> Self {
  268. let mut taxonomies = HashMap::new();
  269. for taxo in all_taxonomies {
  270. taxonomies.insert(format!("{}-{}", taxo.kind.name, taxo.kind.lang), taxo);
  271. }
  272. Self { taxonomies, library, default_lang: default_lang.to_string() }
  273. }
  274. }
  275. impl TeraFn for GetTaxonomy {
  276. fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
  277. let kind = required_arg!(
  278. String,
  279. args.get("kind"),
  280. "`get_taxonomy` requires a `kind` argument with a string value"
  281. );
  282. let lang = optional_arg!(String, args.get("lang"), "`get_taxonomy`: `lang` must be a string")
  283. .unwrap_or_else(|| self.default_lang.clone());
  284. match self.taxonomies.get(&format!("{}-{}", kind, lang)) {
  285. Some(t) => Ok(to_value(t.to_serialized(&self.library.read().unwrap())).unwrap()),
  286. None => {
  287. Err(format!("`get_taxonomy` received an unknown taxonomy as kind: {}", kind).into())
  288. }
  289. }
  290. }
  291. }
  292. #[cfg(test)]
  293. mod tests {
  294. use super::{GetTaxonomy, GetTaxonomyUrl, GetUrl, Trans};
  295. use std::collections::HashMap;
  296. use std::sync::{Arc, RwLock};
  297. use tera::{to_value, Function, Value};
  298. use config::{Config, Taxonomy as TaxonomyConfig};
  299. use library::{Library, Taxonomy, TaxonomyItem};
  300. #[test]
  301. fn can_add_cachebust_to_url() {
  302. let config = Config::default();
  303. let static_fn = GetUrl::new(config, HashMap::new());
  304. let mut args = HashMap::new();
  305. args.insert("path".to_string(), to_value("app.css").unwrap());
  306. args.insert("cachebust".to_string(), to_value(true).unwrap());
  307. assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css?t=1");
  308. }
  309. #[test]
  310. fn can_add_trailing_slashes() {
  311. let config = Config::default();
  312. let static_fn = GetUrl::new(config, HashMap::new());
  313. let mut args = HashMap::new();
  314. args.insert("path".to_string(), to_value("app.css").unwrap());
  315. args.insert("trailing_slash".to_string(), to_value(true).unwrap());
  316. assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css/");
  317. }
  318. #[test]
  319. fn can_add_slashes_and_cachebust() {
  320. let config = Config::default();
  321. let static_fn = GetUrl::new(config, HashMap::new());
  322. let mut args = HashMap::new();
  323. args.insert("path".to_string(), to_value("app.css").unwrap());
  324. args.insert("trailing_slash".to_string(), to_value(true).unwrap());
  325. args.insert("cachebust".to_string(), to_value(true).unwrap());
  326. assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css/?t=1");
  327. }
  328. #[test]
  329. fn can_link_to_some_static_file() {
  330. let config = Config::default();
  331. let static_fn = GetUrl::new(config, HashMap::new());
  332. let mut args = HashMap::new();
  333. args.insert("path".to_string(), to_value("app.css").unwrap());
  334. assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css");
  335. }
  336. #[test]
  337. fn can_get_taxonomy() {
  338. let config = Config::default();
  339. let taxo_config = TaxonomyConfig {
  340. name: "tags".to_string(),
  341. lang: config.default_language.clone(),
  342. ..TaxonomyConfig::default()
  343. };
  344. let taxo_config_fr = TaxonomyConfig {
  345. name: "tags".to_string(),
  346. lang: "fr".to_string(),
  347. ..TaxonomyConfig::default()
  348. };
  349. let library = Arc::new(RwLock::new(Library::new(0, 0, false)));
  350. let tag = TaxonomyItem::new(
  351. "Programming",
  352. &taxo_config,
  353. &config,
  354. vec![],
  355. &library.read().unwrap(),
  356. );
  357. let tag_fr = TaxonomyItem::new(
  358. "Programmation",
  359. &taxo_config_fr,
  360. &config,
  361. vec![],
  362. &library.read().unwrap(),
  363. );
  364. let tags = Taxonomy { kind: taxo_config, items: vec![tag] };
  365. let tags_fr = Taxonomy { kind: taxo_config_fr, items: vec![tag_fr] };
  366. let taxonomies = vec![tags.clone(), tags_fr.clone()];
  367. let static_fn = GetTaxonomy::new(&config.default_language, taxonomies.clone(), library.clone())
  368. ;
  369. // can find it correctly
  370. let mut args = HashMap::new();
  371. args.insert("kind".to_string(), to_value("tags").unwrap());
  372. let res = static_fn.call(&args).unwrap();
  373. let res_obj = res.as_object().unwrap();
  374. assert_eq!(res_obj["kind"], to_value(tags.kind).unwrap());
  375. assert_eq!(res_obj["items"].clone().as_array().unwrap().len(), 1);
  376. assert_eq!(
  377. res_obj["items"].clone().as_array().unwrap()[0].clone().as_object().unwrap()["name"],
  378. Value::String("Programming".to_string())
  379. );
  380. assert_eq!(
  381. res_obj["items"].clone().as_array().unwrap()[0].clone().as_object().unwrap()["slug"],
  382. Value::String("programming".to_string())
  383. );
  384. assert_eq!(
  385. res_obj["items"].clone().as_array().unwrap()[0].clone().as_object().unwrap()
  386. ["permalink"],
  387. Value::String("http://a-website.com/tags/programming/".to_string())
  388. );
  389. assert_eq!(
  390. res_obj["items"].clone().as_array().unwrap()[0].clone().as_object().unwrap()["pages"],
  391. Value::Array(vec![])
  392. );
  393. // Works with other languages as well
  394. let mut args = HashMap::new();
  395. args.insert("kind".to_string(), to_value("tags").unwrap());
  396. args.insert("lang".to_string(), to_value("fr").unwrap());
  397. let res = static_fn.call(&args).unwrap();
  398. let res_obj = res.as_object().unwrap();
  399. assert_eq!(res_obj["kind"], to_value(tags_fr.kind).unwrap());
  400. assert_eq!(res_obj["items"].clone().as_array().unwrap().len(), 1);
  401. assert_eq!(
  402. res_obj["items"].clone().as_array().unwrap()[0].clone().as_object().unwrap()["name"],
  403. Value::String("Programmation".to_string())
  404. );
  405. // and errors if it can't find it
  406. let mut args = HashMap::new();
  407. args.insert("kind".to_string(), to_value("something-else").unwrap());
  408. assert!(static_fn.call(&args).is_err());
  409. }
  410. #[test]
  411. fn can_get_taxonomy_url() {
  412. let config = Config::default();
  413. let taxo_config = TaxonomyConfig {
  414. name: "tags".to_string(),
  415. lang: config.default_language.clone(),
  416. ..TaxonomyConfig::default()
  417. };
  418. let taxo_config_fr = TaxonomyConfig {
  419. name: "tags".to_string(),
  420. lang: "fr".to_string(),
  421. ..TaxonomyConfig::default()
  422. };
  423. let library = Library::new(0, 0, false);
  424. let tag = TaxonomyItem::new("Programming", &taxo_config, &config, vec![], &library);
  425. let tag_fr = TaxonomyItem::new("Programmation", &taxo_config_fr, &config, vec![], &library);
  426. let tags = Taxonomy { kind: taxo_config, items: vec![tag] };
  427. let tags_fr = Taxonomy { kind: taxo_config_fr, items: vec![tag_fr] };
  428. let taxonomies = vec![tags.clone(), tags_fr.clone()];
  429. let static_fn = GetTaxonomyUrl::new(&config.default_language, &taxonomies);
  430. // can find it correctly
  431. let mut args = HashMap::new();
  432. args.insert("kind".to_string(), to_value("tags").unwrap());
  433. args.insert("name".to_string(), to_value("Programming").unwrap());
  434. assert_eq!(
  435. static_fn.call(&args).unwrap(),
  436. to_value("http://a-website.com/tags/programming/").unwrap()
  437. );
  438. // works with other languages
  439. let mut args = HashMap::new();
  440. args.insert("kind".to_string(), to_value("tags").unwrap());
  441. args.insert("name".to_string(), to_value("Programmation").unwrap());
  442. args.insert("lang".to_string(), to_value("fr").unwrap());
  443. assert_eq!(
  444. static_fn.call(&args).unwrap(),
  445. to_value("http://a-website.com/fr/tags/programmation/").unwrap()
  446. );
  447. // and errors if it can't find it
  448. let mut args = HashMap::new();
  449. args.insert("kind".to_string(), to_value("tags").unwrap());
  450. args.insert("name".to_string(), to_value("random").unwrap());
  451. assert!(static_fn.call(&args).is_err());
  452. }
  453. #[test]
  454. fn can_translate_a_string() {
  455. let trans_config = r#"
  456. base_url = "https://remplace-par-ton-url.fr"
  457. default_language = "fr"
  458. [translations]
  459. [translations.fr]
  460. title = "Un titre"
  461. [translations.en]
  462. title = "A title"
  463. "#;
  464. let config = Config::parse(trans_config).unwrap();
  465. let static_fn = Trans::new(config);
  466. let mut args = HashMap::new();
  467. args.insert("key".to_string(), to_value("title").unwrap());
  468. assert_eq!(static_fn.call(&args).unwrap(), "Un titre");
  469. args.insert("lang".to_string(), to_value("en").unwrap());
  470. assert_eq!(static_fn.call(&args).unwrap(), "A title");
  471. args.insert("lang".to_string(), to_value("fr").unwrap());
  472. assert_eq!(static_fn.call(&args).unwrap(), "Un titre");
  473. }
  474. }