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.

563 lines
19KB

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