blog_tools/medium/
types.rs

1use std::{collections::HashMap, fs, path::PathBuf};
2
3use chrono::{Datelike, NaiveDate};
4use markdown::{to_html_with_options, Options};
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    common::{get_json_data, preview::get_preview, toc, BlogError, BlogJson},
9    high::HighBlogEntry,
10    types::Blog,
11};
12
13/// The main `MediumBlog` which stores all relevant information for the blog
14///
15/// `hash` contains a map from the url slug, which is constructed from the
16/// "slug" field in the `BlogJson` and the date, to the `BlogEntry`
17///
18/// `entries` contains a date-sorted (newest first) `Vec` of `BlogEntry`.
19/// Note that `entries` and `hash` contain the same information
20/// but in different formats for performance reasons
21///
22/// `tags` is an unsorted `Vec` of all unique tags used in the blog
23///
24#[derive(Serialize, Deserialize)]
25pub struct MediumBlog {
26    /// URL slug to individual blog
27    ///
28    /// Useful when you have a GET request to /blog/\<date\>/\<slug\>
29    pub hash: HashMap<String, MediumBlogEntry>,
30    /// `Vec` of blog posts, sorted by date
31    ///
32    /// Useful when you want to list all blog posts e.g. on an index page
33    pub entries: Vec<MediumBlogEntry>,
34    /// `Vec` of all unique tags
35    ///
36    /// Useful when you want to list all tags e.g. on an index page
37    pub tags: Vec<String>,
38    /// `String` representation of the sitemap
39    pub sitemap: String,
40}
41
42/// An individual blog post. You will need to render this using `render`
43#[derive(Debug, Serialize, Deserialize, Clone)]
44pub struct MediumBlogEntry {
45    /// Title of the blog post
46    title: String,
47    /// Date published
48    date: NaiveDate,
49    /// Description
50    desc: Option<String>,
51    /// The URL slug
52    slug: String,
53    /// `Vec` of tags for this blog
54    tags: Vec<String>,
55    /// Table of contents
56    toc: Option<String>,
57    /// Optional `Vec` of keywords. Intended for SEO in comparison to tags
58    keywords: Option<Vec<String>>,
59    /// Optional canonical link, intended for SEO
60    canonical_link: Option<String>,
61    /// Optional author name
62    author_name: Option<String>,
63    /// Optional URL for the author
64    author_webpage: Option<String>,
65    /// Preview of the blogpost, useful for showing on index pages
66    preview: String,
67    file_name: String, // ! can't be present in `high` or `low`
68    last_modified: Option<NaiveDate>,
69    priority: Option<f64>,
70}
71
72impl Blog for MediumBlogEntry {
73    fn create<T: AsRef<std::path::Path>>(
74        blog: T,
75        toc_generation_func: Option<&dyn Fn(&markdown::mdast::Node) -> String>,
76        preview_chars: Option<usize>,
77    ) -> Result<Self, BlogError> {
78        let json = get_json_data(&blog)?;
79
80        let markdown = match fs::read_to_string(&blog) {
81            Ok(x) => x,
82            Err(y) => return Err(BlogError::File(y)),
83        };
84
85        let html = match to_html_with_options(
86            &markdown,
87            &Options {
88                compile: markdown::CompileOptions {
89                    allow_dangerous_html: true,
90                    allow_dangerous_protocol: true,
91
92                    ..markdown::CompileOptions::default()
93                },
94                ..markdown::Options::default()
95            },
96        ) {
97            Ok(x) => x,
98            Err(y) => return Err(BlogError::Markdown(y.to_string())),
99        };
100
101        let preview: String = get_preview(&html, preview_chars);
102
103        let toc = toc(&markdown, toc_generation_func)?;
104
105        let file_name = match blog.as_ref().file_name() {
106            Some(x) => x.to_str().unwrap().to_string(),
107            None => return Err(BlogError::FileNotFound),
108        };
109
110        return Ok(MediumBlogEntry::new(json, toc, preview, file_name));
111    }
112
113    fn get_title(&self) -> String {
114        return self.title.clone();
115    }
116
117    fn get_date_listed(&self) -> NaiveDate {
118        return self.date.clone();
119    }
120
121    fn get_description(&self) -> Option<String> {
122        return self.desc.clone();
123    }
124
125    fn get_html(&self) -> String {
126        todo!();
127    }
128
129    fn get_full_slug(&self) -> String {
130        return format!("{}/{}", self.get_date_listed(), self.get_part_slug());
131    }
132
133    fn get_part_slug(&self) -> String {
134        return self.slug.clone();
135    }
136
137    fn get_tags(&self) -> Vec<String> {
138        return self.tags.clone();
139    }
140
141    fn get_table_of_contents(&self) -> Option<String> {
142        return self.toc.clone();
143    }
144
145    fn get_keywords(&self) -> Option<Vec<String>> {
146        return self.keywords.clone();
147    }
148
149    fn get_canonicle_link(&self) -> Option<String> {
150        return self.canonical_link.clone();
151    }
152
153    fn get_author_name(&self) -> Option<String> {
154        return self.author_name.clone();
155    }
156
157    fn get_author_webpage(&self) -> Option<String> {
158        return self.author_webpage.clone();
159    }
160
161    fn get_preview(&self) -> String {
162        return self.preview.clone();
163    }
164
165    fn get_last_modified(&self) -> Option<NaiveDate> {
166        return self.last_modified.clone();
167    }
168
169    fn get_priority(&self) -> Option<f64> {
170        return self.priority.clone();
171    }
172}
173
174impl MediumBlogEntry {
175    pub(crate) fn new(
176        json: BlogJson,
177        toc: Option<String>,
178        preview: String,
179        file_name: String,
180    ) -> Self {
181        return MediumBlogEntry {
182            title: json.title,
183            date: json.date,
184            desc: json.desc,
185            slug: json.slug,
186            tags: json.tags,
187            toc: toc,
188            keywords: json.keywords,
189            canonical_link: json.canonical_link,
190            author_name: json.author_name,
191            author_webpage: json.author_webpage,
192            preview: preview,
193            file_name,
194            last_modified: json.last_modified,
195            priority: json.priority,
196        };
197    }
198
199    /// Use this function to render a `MediumBlogEntry` into a `HighBlogEntry`,
200    /// which then contains the full blog HTML you can return to a user
201    pub fn render(&self, base: PathBuf) -> Result<HighBlogEntry, BlogError> {
202        let year = self.date.year();
203        let path = base
204            .join(format!("{}", year))
205            .join(format!("{}", self.date))
206            .join(self.file_name.clone());
207
208        let md = match fs::read_to_string(path) {
209            Ok(x) => x,
210            Err(y) => return Err(BlogError::File(y)),
211        };
212
213        let html = match to_html_with_options(
214            &md,
215            &Options {
216                compile: markdown::CompileOptions {
217                    allow_dangerous_html: true,
218                    allow_dangerous_protocol: true,
219
220                    ..markdown::CompileOptions::default()
221                },
222                ..markdown::Options::default()
223            },
224        ) {
225            Ok(x) => x,
226            Err(y) => return Err(BlogError::Markdown(y.to_string())),
227        };
228
229        let high = HighBlogEntry::new_from_medium(self, html);
230
231        return Ok(high);
232    }
233}