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.

132 lines
4.8KB

  1. use percent_encoding::percent_decode;
  2. use std::collections::HashMap;
  3. use std::hash::BuildHasher;
  4. use unicode_segmentation::UnicodeSegmentation;
  5. use errors::{bail, Result};
  6. /// Get word count and estimated reading time
  7. pub fn get_reading_analytics(content: &str) -> (usize, usize) {
  8. let word_count: usize = content.unicode_words().count();
  9. // https://help.medium.com/hc/en-us/articles/214991667-Read-time
  10. // 275 seems a bit too high though
  11. (word_count, ((word_count + 199) / 200))
  12. }
  13. #[derive(Debug, PartialEq, Clone)]
  14. pub struct ResolvedInternalLink {
  15. pub permalink: String,
  16. // The 2 fields below are only set when there is an anchor
  17. // as we will need that to check if it exists after the markdown rendering is done
  18. pub md_path: Option<String>,
  19. pub anchor: Option<String>,
  20. }
  21. /// Resolves an internal link (of the `@/posts/something.md#hey` sort) to its absolute link and
  22. /// returns the path + anchor as well
  23. pub fn resolve_internal_link<S: BuildHasher>(
  24. link: &str,
  25. permalinks: &HashMap<String, String, S>,
  26. ) -> Result<ResolvedInternalLink> {
  27. // First we remove the ./ since that's zola specific
  28. let clean_link = link.replacen("@/", "", 1);
  29. // Then we remove any potential anchor
  30. // parts[0] will be the file path and parts[1] the anchor if present
  31. let parts = clean_link.split('#').collect::<Vec<_>>();
  32. // If we have slugification turned off, we might end up with some escaped characters so we need
  33. // to decode them first
  34. let decoded = &*percent_decode(parts[0].as_bytes()).decode_utf8_lossy();
  35. match permalinks.get(decoded) {
  36. Some(p) => {
  37. if parts.len() > 1 {
  38. Ok(ResolvedInternalLink {
  39. permalink: format!("{}#{}", p, parts[1]),
  40. md_path: Some(decoded.to_string()),
  41. anchor: Some(parts[1].to_string()),
  42. })
  43. } else {
  44. Ok(ResolvedInternalLink { permalink: p.to_string(), md_path: None, anchor: None })
  45. }
  46. }
  47. None => bail!(format!("Relative link {} not found.", link)),
  48. }
  49. }
  50. #[cfg(test)]
  51. mod tests {
  52. use std::collections::HashMap;
  53. use super::{get_reading_analytics, resolve_internal_link};
  54. #[test]
  55. fn can_resolve_valid_internal_link() {
  56. let mut permalinks = HashMap::new();
  57. permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string());
  58. let res = resolve_internal_link("@/pages/about.md", &permalinks).unwrap();
  59. assert_eq!(res.permalink, "https://vincent.is/about");
  60. }
  61. #[test]
  62. fn can_resolve_valid_root_internal_link() {
  63. let mut permalinks = HashMap::new();
  64. permalinks.insert("about.md".to_string(), "https://vincent.is/about".to_string());
  65. let res = resolve_internal_link("@/about.md", &permalinks).unwrap();
  66. assert_eq!(res.permalink, "https://vincent.is/about");
  67. }
  68. #[test]
  69. fn can_resolve_internal_links_with_anchors() {
  70. let mut permalinks = HashMap::new();
  71. permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string());
  72. let res = resolve_internal_link("@/pages/about.md#hello", &permalinks).unwrap();
  73. assert_eq!(res.permalink, "https://vincent.is/about#hello");
  74. assert_eq!(res.md_path, Some("pages/about.md".to_string()));
  75. assert_eq!(res.anchor, Some("hello".to_string()));
  76. }
  77. #[test]
  78. fn can_resolve_escaped_internal_links() {
  79. let mut permalinks = HashMap::new();
  80. permalinks.insert(
  81. "pages/about space.md".to_string(),
  82. "https://vincent.is/about%20space/".to_string(),
  83. );
  84. let res = resolve_internal_link("@/pages/about%20space.md#hello", &permalinks).unwrap();
  85. assert_eq!(res.permalink, "https://vincent.is/about%20space/#hello");
  86. assert_eq!(res.md_path, Some("pages/about space.md".to_string()));
  87. assert_eq!(res.anchor, Some("hello".to_string()));
  88. }
  89. #[test]
  90. fn errors_resolve_inexistant_internal_link() {
  91. let res = resolve_internal_link("@/pages/about.md#hello", &HashMap::new());
  92. assert!(res.is_err());
  93. }
  94. #[test]
  95. fn reading_analytics_empty_text() {
  96. let (word_count, reading_time) = get_reading_analytics(" ");
  97. assert_eq!(word_count, 0);
  98. assert_eq!(reading_time, 0);
  99. }
  100. #[test]
  101. fn reading_analytics_short_text() {
  102. let (word_count, reading_time) = get_reading_analytics("Hello World");
  103. assert_eq!(word_count, 2);
  104. assert_eq!(reading_time, 1);
  105. }
  106. #[test]
  107. fn reading_analytics_long_text() {
  108. let mut content = String::new();
  109. for _ in 0..1000 {
  110. content.push_str(" Hello world");
  111. }
  112. let (word_count, reading_time) = get_reading_analytics(&content);
  113. assert_eq!(word_count, 2000);
  114. assert_eq!(reading_time, 10);
  115. }
  116. }