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.

481 lines
16KB

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