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.

537 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: &str = "fill";
  85. static DEFAULT_FMT: &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 =
  184. optional_arg!(String, args.get("lang"), "`get_taxonomy`: `lang` must be a string")
  185. .unwrap_or_else(|| self.default_lang.clone());
  186. let container = match self.taxonomies.get(&format!("{}-{}", kind, lang)) {
  187. Some(c) => c,
  188. None => {
  189. return Err(format!(
  190. "`get_taxonomy_url` received an unknown taxonomy as kind: {}",
  191. kind
  192. )
  193. .into());
  194. }
  195. };
  196. if let Some(permalink) = container.get(&name) {
  197. return Ok(to_value(permalink).unwrap());
  198. }
  199. Err(format!("`get_taxonomy_url`: couldn't find `{}` in `{}` taxonomy", name, kind).into())
  200. }
  201. }
  202. #[derive(Debug)]
  203. pub struct GetPage {
  204. base_path: PathBuf,
  205. library: Arc<RwLock<Library>>,
  206. }
  207. impl GetPage {
  208. pub fn new(base_path: PathBuf, library: Arc<RwLock<Library>>) -> Self {
  209. Self { base_path: base_path.join("content"), library }
  210. }
  211. }
  212. impl TeraFn for GetPage {
  213. fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
  214. let path = required_arg!(
  215. String,
  216. args.get("path"),
  217. "`get_page` requires a `path` argument with a string value"
  218. );
  219. let full_path = self.base_path.join(&path);
  220. let library = self.library.read().unwrap();
  221. match library.get_page(&full_path) {
  222. Some(p) => Ok(to_value(p.to_serialized(&library)).unwrap()),
  223. None => Err(format!("Page `{}` not found.", path).into()),
  224. }
  225. }
  226. }
  227. #[derive(Debug)]
  228. pub struct GetSection {
  229. base_path: PathBuf,
  230. library: Arc<RwLock<Library>>,
  231. }
  232. impl GetSection {
  233. pub fn new(base_path: PathBuf, library: Arc<RwLock<Library>>) -> Self {
  234. Self { base_path: base_path.join("content"), library }
  235. }
  236. }
  237. impl TeraFn for GetSection {
  238. fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
  239. let path = required_arg!(
  240. String,
  241. args.get("path"),
  242. "`get_section` requires a `path` argument with a string value"
  243. );
  244. let metadata_only = args
  245. .get("metadata_only")
  246. .map_or(false, |c| from_value::<bool>(c.clone()).unwrap_or(false));
  247. let full_path = self.base_path.join(&path);
  248. let library = self.library.read().unwrap();
  249. match library.get_section(&full_path) {
  250. Some(s) => {
  251. if metadata_only {
  252. Ok(to_value(s.to_serialized_basic(&library)).unwrap())
  253. } else {
  254. Ok(to_value(s.to_serialized(&library)).unwrap())
  255. }
  256. }
  257. None => Err(format!("Section `{}` not found.", path).into()),
  258. }
  259. }
  260. }
  261. #[derive(Debug)]
  262. pub struct GetTaxonomy {
  263. library: Arc<RwLock<Library>>,
  264. taxonomies: HashMap<String, Taxonomy>,
  265. default_lang: String,
  266. }
  267. impl GetTaxonomy {
  268. pub fn new(
  269. default_lang: &str,
  270. all_taxonomies: Vec<Taxonomy>,
  271. library: Arc<RwLock<Library>>,
  272. ) -> Self {
  273. let mut taxonomies = HashMap::new();
  274. for taxo in all_taxonomies {
  275. taxonomies.insert(format!("{}-{}", taxo.kind.name, taxo.kind.lang), taxo);
  276. }
  277. Self { taxonomies, library, default_lang: default_lang.to_string() }
  278. }
  279. }
  280. impl TeraFn for GetTaxonomy {
  281. fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
  282. let kind = required_arg!(
  283. String,
  284. args.get("kind"),
  285. "`get_taxonomy` requires a `kind` argument with a string value"
  286. );
  287. let lang =
  288. optional_arg!(String, args.get("lang"), "`get_taxonomy`: `lang` must be a string")
  289. .unwrap_or_else(|| self.default_lang.clone());
  290. match self.taxonomies.get(&format!("{}-{}", kind, lang)) {
  291. Some(t) => Ok(to_value(t.to_serialized(&self.library.read().unwrap())).unwrap()),
  292. None => {
  293. Err(format!("`get_taxonomy` received an unknown taxonomy as kind: {}", kind).into())
  294. }
  295. }
  296. }
  297. }
  298. #[cfg(test)]
  299. mod tests {
  300. use super::{GetTaxonomy, GetTaxonomyUrl, GetUrl, Trans};
  301. use std::collections::HashMap;
  302. use std::sync::{Arc, RwLock};
  303. use tera::{to_value, Function, Value};
  304. use config::{Config, Taxonomy as TaxonomyConfig};
  305. use library::{Library, Taxonomy, TaxonomyItem};
  306. #[test]
  307. fn can_add_cachebust_to_url() {
  308. let config = Config::default();
  309. let static_fn = GetUrl::new(config, HashMap::new());
  310. let mut args = HashMap::new();
  311. args.insert("path".to_string(), to_value("app.css").unwrap());
  312. args.insert("cachebust".to_string(), to_value(true).unwrap());
  313. assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css?t=1");
  314. }
  315. #[test]
  316. fn can_add_trailing_slashes() {
  317. let config = Config::default();
  318. let static_fn = GetUrl::new(config, HashMap::new());
  319. let mut args = HashMap::new();
  320. args.insert("path".to_string(), to_value("app.css").unwrap());
  321. args.insert("trailing_slash".to_string(), to_value(true).unwrap());
  322. assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css/");
  323. }
  324. #[test]
  325. fn can_add_slashes_and_cachebust() {
  326. let config = Config::default();
  327. let static_fn = GetUrl::new(config, HashMap::new());
  328. let mut args = HashMap::new();
  329. args.insert("path".to_string(), to_value("app.css").unwrap());
  330. args.insert("trailing_slash".to_string(), to_value(true).unwrap());
  331. args.insert("cachebust".to_string(), to_value(true).unwrap());
  332. assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css/?t=1");
  333. }
  334. #[test]
  335. fn can_link_to_some_static_file() {
  336. let config = Config::default();
  337. let static_fn = GetUrl::new(config, HashMap::new());
  338. let mut args = HashMap::new();
  339. args.insert("path".to_string(), to_value("app.css").unwrap());
  340. assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css");
  341. }
  342. #[test]
  343. fn can_get_taxonomy() {
  344. let config = Config::default();
  345. let taxo_config = TaxonomyConfig {
  346. name: "tags".to_string(),
  347. lang: config.default_language.clone(),
  348. ..TaxonomyConfig::default()
  349. };
  350. let taxo_config_fr = TaxonomyConfig {
  351. name: "tags".to_string(),
  352. lang: "fr".to_string(),
  353. ..TaxonomyConfig::default()
  354. };
  355. let library = Arc::new(RwLock::new(Library::new(0, 0, false)));
  356. let tag = TaxonomyItem::new(
  357. "Programming",
  358. &taxo_config,
  359. &config,
  360. vec![],
  361. &library.read().unwrap(),
  362. );
  363. let tag_fr = TaxonomyItem::new(
  364. "Programmation",
  365. &taxo_config_fr,
  366. &config,
  367. vec![],
  368. &library.read().unwrap(),
  369. );
  370. let tags = Taxonomy { kind: taxo_config, items: vec![tag] };
  371. let tags_fr = Taxonomy { kind: taxo_config_fr, items: vec![tag_fr] };
  372. let taxonomies = vec![tags.clone(), tags_fr.clone()];
  373. let static_fn =
  374. GetTaxonomy::new(&config.default_language, taxonomies.clone(), library.clone());
  375. // can find it correctly
  376. let mut args = HashMap::new();
  377. args.insert("kind".to_string(), to_value("tags").unwrap());
  378. let res = static_fn.call(&args).unwrap();
  379. let res_obj = res.as_object().unwrap();
  380. assert_eq!(res_obj["kind"], to_value(tags.kind).unwrap());
  381. assert_eq!(res_obj["items"].clone().as_array().unwrap().len(), 1);
  382. assert_eq!(
  383. res_obj["items"].clone().as_array().unwrap()[0].clone().as_object().unwrap()["name"],
  384. Value::String("Programming".to_string())
  385. );
  386. assert_eq!(
  387. res_obj["items"].clone().as_array().unwrap()[0].clone().as_object().unwrap()["slug"],
  388. Value::String("programming".to_string())
  389. );
  390. assert_eq!(
  391. res_obj["items"].clone().as_array().unwrap()[0].clone().as_object().unwrap()
  392. ["permalink"],
  393. Value::String("http://a-website.com/tags/programming/".to_string())
  394. );
  395. assert_eq!(
  396. res_obj["items"].clone().as_array().unwrap()[0].clone().as_object().unwrap()["pages"],
  397. Value::Array(vec![])
  398. );
  399. // Works with other languages as well
  400. let mut args = HashMap::new();
  401. args.insert("kind".to_string(), to_value("tags").unwrap());
  402. args.insert("lang".to_string(), to_value("fr").unwrap());
  403. let res = static_fn.call(&args).unwrap();
  404. let res_obj = res.as_object().unwrap();
  405. assert_eq!(res_obj["kind"], to_value(tags_fr.kind).unwrap());
  406. assert_eq!(res_obj["items"].clone().as_array().unwrap().len(), 1);
  407. assert_eq!(
  408. res_obj["items"].clone().as_array().unwrap()[0].clone().as_object().unwrap()["name"],
  409. Value::String("Programmation".to_string())
  410. );
  411. // and errors if it can't find it
  412. let mut args = HashMap::new();
  413. args.insert("kind".to_string(), to_value("something-else").unwrap());
  414. assert!(static_fn.call(&args).is_err());
  415. }
  416. #[test]
  417. fn can_get_taxonomy_url() {
  418. let config = Config::default();
  419. let taxo_config = TaxonomyConfig {
  420. name: "tags".to_string(),
  421. lang: config.default_language.clone(),
  422. ..TaxonomyConfig::default()
  423. };
  424. let taxo_config_fr = TaxonomyConfig {
  425. name: "tags".to_string(),
  426. lang: "fr".to_string(),
  427. ..TaxonomyConfig::default()
  428. };
  429. let library = Library::new(0, 0, false);
  430. let tag = TaxonomyItem::new("Programming", &taxo_config, &config, vec![], &library);
  431. let tag_fr = TaxonomyItem::new("Programmation", &taxo_config_fr, &config, vec![], &library);
  432. let tags = Taxonomy { kind: taxo_config, items: vec![tag] };
  433. let tags_fr = Taxonomy { kind: taxo_config_fr, items: vec![tag_fr] };
  434. let taxonomies = vec![tags.clone(), tags_fr.clone()];
  435. let static_fn = GetTaxonomyUrl::new(&config.default_language, &taxonomies);
  436. // can find it correctly
  437. let mut args = HashMap::new();
  438. args.insert("kind".to_string(), to_value("tags").unwrap());
  439. args.insert("name".to_string(), to_value("Programming").unwrap());
  440. assert_eq!(
  441. static_fn.call(&args).unwrap(),
  442. to_value("http://a-website.com/tags/programming/").unwrap()
  443. );
  444. // works with other languages
  445. let mut args = HashMap::new();
  446. args.insert("kind".to_string(), to_value("tags").unwrap());
  447. args.insert("name".to_string(), to_value("Programmation").unwrap());
  448. args.insert("lang".to_string(), to_value("fr").unwrap());
  449. assert_eq!(
  450. static_fn.call(&args).unwrap(),
  451. to_value("http://a-website.com/fr/tags/programmation/").unwrap()
  452. );
  453. // and errors if it can't find it
  454. let mut args = HashMap::new();
  455. args.insert("kind".to_string(), to_value("tags").unwrap());
  456. args.insert("name".to_string(), to_value("random").unwrap());
  457. assert!(static_fn.call(&args).is_err());
  458. }
  459. #[test]
  460. fn can_translate_a_string() {
  461. let trans_config = 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. let config = Config::parse(trans_config).unwrap();
  471. let static_fn = Trans::new(config);
  472. let mut args = HashMap::new();
  473. args.insert("key".to_string(), to_value("title").unwrap());
  474. assert_eq!(static_fn.call(&args).unwrap(), "Un titre");
  475. args.insert("lang".to_string(), to_value("en").unwrap());
  476. assert_eq!(static_fn.call(&args).unwrap(), "A title");
  477. args.insert("lang".to_string(), to_value("fr").unwrap());
  478. assert_eq!(static_fn.call(&args).unwrap(), "Un titre");
  479. }
  480. }