|
| 1 | +use std::collections::HashSet; |
| 2 | +use std::path::Path; |
| 3 | +use std::sync::Arc; |
| 4 | + |
| 5 | +use anyhow::anyhow; |
| 6 | +use rst_parser::parse; |
| 7 | +use rst_renderer::render_html; |
| 8 | +use serde::de::Error; |
| 9 | +use serde::{Deserialize, Deserializer, Serialize, Serializer}; |
| 10 | +use time::Date; |
| 11 | + |
| 12 | +use crate::lang::Language; |
| 13 | + |
| 14 | +#[derive(Debug, Copy, Clone, PartialEq, Eq)] |
| 15 | +struct MyDate(Date); |
| 16 | + |
| 17 | +#[derive(Debug, Clone)] |
| 18 | +pub(crate) struct News { |
| 19 | + posts: Arc<Vec<Arc<Post>>>, |
| 20 | + small_posts: Arc<Vec<Arc<SmallPost>>>, |
| 21 | +} |
| 22 | + |
| 23 | +#[derive(Deserialize)] |
| 24 | +pub(crate) struct WrittenNewsMeta { |
| 25 | + title: String, |
| 26 | + published: MyDate, |
| 27 | + modified: Option<MyDate>, |
| 28 | + description: String, |
| 29 | + keywords: Vec<String>, |
| 30 | + authors: Vec<String>, |
| 31 | + image: Option<String>, |
| 32 | +} |
| 33 | + |
| 34 | +#[derive(Serialize, Debug, Clone)] |
| 35 | +pub(crate) struct Post { |
| 36 | + slug: String, |
| 37 | + lang: Language, |
| 38 | + idx: u32, |
| 39 | + title: String, |
| 40 | + published: MyDate, |
| 41 | + modified: Option<MyDate>, |
| 42 | + description: String, |
| 43 | + keywords: Vec<String>, |
| 44 | + authors: Vec<String>, |
| 45 | + image: Option<String>, |
| 46 | + body: String, |
| 47 | +} |
| 48 | + |
| 49 | +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] |
| 50 | +pub(crate) struct SmallPost { |
| 51 | + slug: String, |
| 52 | + lang: Language, |
| 53 | + idx: u32, |
| 54 | + title: String, |
| 55 | + published: MyDate, |
| 56 | + modified: Option<MyDate>, |
| 57 | + description: String, |
| 58 | + keywords: Vec<String>, |
| 59 | + authors: Vec<String>, |
| 60 | + image: Option<String>, |
| 61 | +} |
| 62 | + |
| 63 | +impl News { |
| 64 | + pub(crate) async fn load(directory: &Path) -> anyhow::Result<Self> { |
| 65 | + let mut posts = Vec::new(); |
| 66 | + |
| 67 | + let mut dir = tokio::fs::read_dir(directory).await?; |
| 68 | + while let Some(entry) = dir.next_entry().await? { |
| 69 | + if entry.file_type().await?.is_dir() { |
| 70 | + continue; |
| 71 | + } |
| 72 | + |
| 73 | + let path = entry.path(); |
| 74 | + let content = tokio::fs::read_to_string(path.as_path()).await?; |
| 75 | + let content = content.trim_start(); |
| 76 | + let content = content.strip_prefix("---").unwrap(); |
| 77 | + let (meta, text) = content.split_once("---").unwrap(); |
| 78 | + |
| 79 | + let meta: WrittenNewsMeta = serde_yaml::from_str(meta)?; |
| 80 | + let file_name = path.file_name().unwrap().to_str().unwrap(); |
| 81 | + |
| 82 | + if file_name.starts_with('_') { |
| 83 | + continue; |
| 84 | + } |
| 85 | + |
| 86 | + let is_rst_file = file_name.ends_with(".rst"); |
| 87 | + |
| 88 | + let (idx, lang, slug) = parse_file_name(file_name)?; |
| 89 | + |
| 90 | + let body = if is_rst_file { |
| 91 | + let mut buffer: Vec<u8> = Vec::new(); |
| 92 | + let parsed_rst = parse(text) |
| 93 | + .map_err(|e| { |
| 94 | + eprintln!("cannot parse rst file {} with error {}", &file_name, e); |
| 95 | + }) |
| 96 | + .unwrap_or_default(); |
| 97 | + render_html(&parsed_rst, &mut buffer, true) |
| 98 | + .map_err(|e| { |
| 99 | + eprintln!( |
| 100 | + "cannot render rst file to html {} with error {}", |
| 101 | + &file_name, e |
| 102 | + ); |
| 103 | + }) |
| 104 | + .unwrap_or_default(); |
| 105 | + String::from_utf8(buffer)? |
| 106 | + } else { |
| 107 | + markdown::to_html(text) |
| 108 | + }; |
| 109 | + |
| 110 | + posts.push(Arc::new(Post { |
| 111 | + slug: slug.to_string(), |
| 112 | + lang, |
| 113 | + idx, |
| 114 | + title: meta.title, |
| 115 | + published: meta.published, |
| 116 | + modified: meta.modified, |
| 117 | + description: meta.description, |
| 118 | + keywords: meta.keywords, |
| 119 | + authors: meta.authors, |
| 120 | + image: meta.image, |
| 121 | + body, |
| 122 | + })); |
| 123 | + } |
| 124 | + |
| 125 | + posts.sort_by(|a, b| b.idx.cmp(&a.idx)); |
| 126 | + |
| 127 | + let small_posts = posts |
| 128 | + .iter() |
| 129 | + .map(|post| { |
| 130 | + Arc::new(SmallPost { |
| 131 | + slug: post.slug.clone(), |
| 132 | + lang: post.lang, |
| 133 | + idx: post.idx, |
| 134 | + title: post.title.clone(), |
| 135 | + published: post.published, |
| 136 | + modified: post.modified, |
| 137 | + description: post.description.clone(), |
| 138 | + keywords: post.keywords.clone(), |
| 139 | + authors: post.authors.clone(), |
| 140 | + image: post.image.clone(), |
| 141 | + }) |
| 142 | + }) |
| 143 | + .collect(); |
| 144 | + |
| 145 | + Ok(News { |
| 146 | + posts: Arc::new(posts), |
| 147 | + small_posts: Arc::new(small_posts), |
| 148 | + }) |
| 149 | + } |
| 150 | + |
| 151 | + pub(crate) fn posts(&self, lang: Language) -> Vec<Arc<SmallPost>> { |
| 152 | + self |
| 153 | + .small_posts |
| 154 | + .iter() |
| 155 | + .filter(|post| post.lang == lang) |
| 156 | + .cloned() |
| 157 | + .collect() |
| 158 | + } |
| 159 | + |
| 160 | + pub(crate) fn find_post(&self, lang: Language, slug: &str) -> Option<Arc<Post>> { |
| 161 | + self |
| 162 | + .posts |
| 163 | + .iter() |
| 164 | + .find(|post| post.lang == lang && post.slug == slug) |
| 165 | + .cloned() |
| 166 | + } |
| 167 | + |
| 168 | + pub(crate) fn search_by_keywords( |
| 169 | + &self, |
| 170 | + lang: Language, |
| 171 | + keywords: &Vec<String>, |
| 172 | + ) -> Vec<Arc<SmallPost>> { |
| 173 | + let posts = self |
| 174 | + .small_posts |
| 175 | + .iter() |
| 176 | + .filter(|post| post.lang == lang) |
| 177 | + .collect::<Vec<_>>(); |
| 178 | + |
| 179 | + let keywords_set = keywords.iter().collect::<HashSet<_>>(); |
| 180 | + |
| 181 | + let mut or = posts |
| 182 | + .iter() |
| 183 | + .filter(|post| { |
| 184 | + post |
| 185 | + .keywords |
| 186 | + .iter() |
| 187 | + .collect::<HashSet<_>>() |
| 188 | + .intersection(&keywords_set) |
| 189 | + .next() |
| 190 | + .is_some() |
| 191 | + }) |
| 192 | + .cloned() |
| 193 | + .cloned() |
| 194 | + .collect::<Vec<_>>(); |
| 195 | + |
| 196 | + let mut and = posts |
| 197 | + .iter() |
| 198 | + .filter(|post| { |
| 199 | + !or.contains(post) |
| 200 | + && post |
| 201 | + .keywords |
| 202 | + .iter() |
| 203 | + .collect::<HashSet<_>>() |
| 204 | + .intersection(&keywords_set) |
| 205 | + .count() |
| 206 | + == keywords.len() |
| 207 | + }) |
| 208 | + .cloned() |
| 209 | + .cloned() |
| 210 | + .collect::<Vec<_>>(); |
| 211 | + |
| 212 | + or.append(&mut and); |
| 213 | + |
| 214 | + or |
| 215 | + } |
| 216 | + |
| 217 | + pub(crate) fn keywords(&self) -> HashSet<String> { |
| 218 | + self |
| 219 | + .small_posts |
| 220 | + .iter() |
| 221 | + .flat_map(|post| post.keywords.clone()) |
| 222 | + .collect() |
| 223 | + } |
| 224 | +} |
| 225 | + |
| 226 | +pub(crate) fn parse_file_name(file_name: &str) -> anyhow::Result<(u32, Language, &str)> { |
| 227 | + let mut split = file_name.split('.'); |
| 228 | + |
| 229 | + let idx = split |
| 230 | + .next() |
| 231 | + .ok_or_else(|| anyhow!("Index missing in file name {}", file_name))? |
| 232 | + .parse()?; |
| 233 | + let slug = split |
| 234 | + .next() |
| 235 | + .ok_or_else(|| anyhow!("Slug missing in file name {}", file_name))?; |
| 236 | + let lang = split |
| 237 | + .next() |
| 238 | + .ok_or_else(|| anyhow!("Language missing in file name {}", file_name))? |
| 239 | + .try_into()?; |
| 240 | + |
| 241 | + Ok((idx, lang, slug)) |
| 242 | +} |
| 243 | + |
| 244 | +impl Serialize for MyDate { |
| 245 | + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> |
| 246 | + where |
| 247 | + S: Serializer, |
| 248 | + { |
| 249 | + let s = format!( |
| 250 | + "{:0>4}-{:0>2}-{:0>2}", |
| 251 | + self.0.year(), |
| 252 | + self.0.month() as u8, |
| 253 | + self.0.day() |
| 254 | + ); |
| 255 | + |
| 256 | + serializer.serialize_str(&s) |
| 257 | + } |
| 258 | +} |
| 259 | + |
| 260 | +impl<'de> Deserialize<'de> for MyDate { |
| 261 | + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> |
| 262 | + where |
| 263 | + D: Deserializer<'de>, |
| 264 | + { |
| 265 | + let s = String::deserialize(deserializer)?; |
| 266 | + let mut split = s.split('-'); |
| 267 | + |
| 268 | + let year = split |
| 269 | + .next() |
| 270 | + .ok_or_else(|| Error::custom(format!("Invalid date format {}", s)))? |
| 271 | + .parse() |
| 272 | + .map_err(|e| Error::custom(format!("{}", e)))?; |
| 273 | + |
| 274 | + let month: u8 = split |
| 275 | + .next() |
| 276 | + .ok_or_else(|| Error::custom(format!("Invalid date format {}", s)))? |
| 277 | + .parse() |
| 278 | + .map_err(|e| Error::custom(format!("{}", e)))?; |
| 279 | + |
| 280 | + let day = split |
| 281 | + .next() |
| 282 | + .ok_or_else(|| Error::custom(format!("Invalid date format {}", s)))? |
| 283 | + .parse() |
| 284 | + .map_err(|e| Error::custom(format!("{}", e)))?; |
| 285 | + |
| 286 | + Date::from_calendar_date( |
| 287 | + year, |
| 288 | + month |
| 289 | + .try_into() |
| 290 | + .map_err(|e| Error::custom(format!("{}", e)))?, |
| 291 | + day, |
| 292 | + ) |
| 293 | + .map_err(|e| Error::custom(format!("{}", e))) |
| 294 | + .map(MyDate) |
| 295 | + } |
| 296 | +} |
0 commit comments