Rust library to generate static websites
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

perf(markdown): Try lazy loading data (#15)

* perf(markdown): Try lazy loading data

* fix: labeller

authored by

Erika and committed by
GitHub
810c0729 f115fd6a

+104 -68
.github/labeler.yaml .github/labeler.yml
+31 -2
crates/maudit/src/content.rs
··· 218 218 pub id: String, 219 219 render: OptionalContentRenderFn, 220 220 pub raw_content: Option<String>, 221 - pub data: T, 221 + data_loader: Option<Box<dyn Fn() -> T + Send + Sync>>, 222 + cached_data: std::sync::OnceLock<T>, 222 223 pub file_path: Option<PathBuf>, 223 224 } 224 225 ··· 236 237 id, 237 238 render, 238 239 raw_content, 239 - data, 240 + data_loader: None, 241 + cached_data: std::sync::OnceLock::from(data), 242 + file_path, 243 + } 244 + } 245 + 246 + pub fn new_lazy( 247 + id: String, 248 + render: OptionalContentRenderFn, 249 + raw_content: Option<String>, 250 + data_loader: Box<dyn Fn() -> T + Send + Sync>, 251 + file_path: Option<PathBuf>, 252 + ) -> Self { 253 + Self { 254 + id, 255 + render, 256 + raw_content, 257 + data_loader: Some(data_loader), 258 + cached_data: std::sync::OnceLock::new(), 240 259 file_path, 241 260 } 261 + } 262 + 263 + pub fn data(&self) -> &T { 264 + self.cached_data.get_or_init(|| { 265 + if let Some(ref loader) = self.data_loader { 266 + loader() 267 + } else { 268 + panic!("No data loader available and no cached data") 269 + } 270 + }) 242 271 } 243 272 244 273 pub fn render(&self) -> String {
+50 -43
crates/maudit/src/content/markdown.rs
··· 28 28 /// fn render(&self, ctx: &mut RouteContext) -> Markup { 29 29 /// let articles = ctx.content.get_source::<ArticleContent>("articles"); 30 30 /// let article = articles.get_entry("my-article"); 31 - /// let headings = article.data.get_headings(); // returns a Vec<MarkdownHeading> 31 + /// let headings = article.data().get_headings(); // returns a Vec<MarkdownHeading> 32 32 /// let toc = html! { 33 33 /// ul { 34 34 /// @for heading in headings { ··· 40 40 /// }; 41 41 /// html! { 42 42 /// main { 43 - /// h1 { (article.data.title) } 43 + /// h1 { (article.data().title) } 44 44 /// nav { (toc) } 45 45 /// } 46 46 /// } ··· 154 154 /// ``` 155 155 pub fn glob_markdown<T>(pattern: &str) -> Vec<ContentEntry<T>> 156 156 where 157 - T: DeserializeOwned + MarkdownContent + InternalMarkdownContent, 157 + T: DeserializeOwned + MarkdownContent + InternalMarkdownContent + Send + Sync + 'static, 158 158 { 159 159 let mut entries = vec![]; 160 160 161 161 for entry in glob_fs(pattern).unwrap() { 162 - let mut slugger = slugger::Slugger::new(); 163 162 let entry = entry.unwrap(); 164 163 165 164 if let Some(extension) = entry.extension() { ··· 172 171 let id = entry.file_stem().unwrap().to_str().unwrap().to_string(); 173 172 let content = std::fs::read_to_string(&entry).unwrap(); 174 173 175 - let mut options = Options::empty(); 176 - options.insert( 177 - Options::ENABLE_YAML_STYLE_METADATA_BLOCKS | Options::ENABLE_HEADING_ATTRIBUTES, 178 - ); 174 + // Clone content for the closure 175 + let content_clone = content.clone(); 176 + let data_loader = Box::new(move || { 177 + let mut slugger = slugger::Slugger::new(); 178 + 179 + let mut options = Options::empty(); 180 + options.insert( 181 + Options::ENABLE_YAML_STYLE_METADATA_BLOCKS | Options::ENABLE_HEADING_ATTRIBUTES, 182 + ); 179 183 180 - let mut frontmatter = String::new(); 181 - let mut in_frontmatter = false; 184 + let mut frontmatter = String::new(); 185 + let mut in_frontmatter = false; 182 186 183 - let mut content_events = Vec::new(); 184 - for (event, _) in Parser::new_ext(&content, options).into_offset_iter() { 185 - match event { 186 - Event::Start(Tag::MetadataBlock(_)) => in_frontmatter = true, 187 - Event::End(TagEnd::MetadataBlock(_)) => in_frontmatter = false, 188 - Event::Text(ref text) => { 189 - if in_frontmatter { 190 - frontmatter.push_str(text); 191 - } else { 192 - content_events.push(event); 187 + let mut content_events = Vec::new(); 188 + for (event, _) in Parser::new_ext(&content_clone, options).into_offset_iter() { 189 + match event { 190 + Event::Start(Tag::MetadataBlock(_)) => in_frontmatter = true, 191 + Event::End(TagEnd::MetadataBlock(_)) => in_frontmatter = false, 192 + Event::Text(ref text) => { 193 + if in_frontmatter { 194 + frontmatter.push_str(text); 195 + } else { 196 + content_events.push(event); 197 + } 193 198 } 199 + _ => content_events.push(event), 194 200 } 195 - _ => content_events.push(event), 196 201 } 197 - } 198 202 199 - // TODO: Prettier errors for serialization errors (e.g. missing fields) 200 - let mut parsed = serde_yml::from_str::<T>(&frontmatter).unwrap(); 203 + // TODO: Prettier errors for serialization errors (e.g. missing fields) 204 + let mut parsed = serde_yml::from_str::<T>(&frontmatter).unwrap(); 201 205 202 - let headings_internal = find_headings(&content_events); 206 + let headings_internal = find_headings(&content_events); 203 207 204 - let mut headings = vec![]; 205 - for heading in headings_internal { 206 - let heading_content = get_text_from_events(&content_events[heading.start..heading.end]); 207 - let slug: String = slugger.slugify(&heading_content); 208 + let mut headings = vec![]; 209 + for heading in headings_internal { 210 + let heading_content = 211 + get_text_from_events(&content_events[heading.start..heading.end]); 212 + let slug: String = slugger.slugify(&heading_content); 208 213 209 - headings.push(MarkdownHeading { 210 - title: heading_content, 211 - id: heading.id.unwrap_or(slug), 212 - level: heading.level as u8, 213 - classes: heading.classes, 214 - }); 215 - } 214 + headings.push(MarkdownHeading { 215 + title: heading_content, 216 + id: heading.id.unwrap_or(slug), 217 + level: heading.level as u8, 218 + classes: heading.classes, 219 + }); 220 + } 216 221 217 - parsed.set_headings(headings); 222 + parsed.set_headings(headings); 223 + parsed 224 + }); 218 225 219 - entries.push(ContentEntry { 226 + entries.push(ContentEntry::new_lazy( 220 227 id, 221 - render: Some(Box::new(render_markdown)), 222 - raw_content: Some(content), 223 - file_path: Some(entry), 224 - data: parsed, 225 - }); 228 + Some(Box::new(render_markdown)), 229 + Some(content), 230 + data_loader, 231 + Some(entry), 232 + )); 226 233 } 227 234 228 235 entries
+1 -1
crates/maudit/src/page.rs
··· 85 85 /// (logo) 86 86 /// ul { 87 87 /// @for entry in last_entries { 88 - /// li { (entry.data.title) } 88 + /// li { (entry.data().title) } 89 89 /// } 90 90 /// } 91 91 /// }
+3 -3
crates/oubli/src/archetypes/blog.rs
··· 19 19 main { 20 20 @for entry in &blog_entries.entries { 21 21 a href=(get_page_url(&route, BlogEntryParams { entry: entry.id.clone() })) { 22 - h2 { (entry.data.title) } 23 - p { (entry.data.description) } 22 + h2 { (entry.data().title) } 23 + p { (entry.data().description) } 24 24 } 25 25 } 26 26 } ··· 57 57 .get_source::<BlogEntryContent>(stringified_ident); 58 58 let blog_entry = blog_entries.get_entry(&params.entry); 59 59 60 - let headings = blog_entry.data.get_headings(); 60 + let headings = blog_entry.data().get_headings(); 61 61 println!("{:?}", headings); 62 62 63 63 layout(name, blog_entry.render())
+1 -1
examples/blog/src/pages/article.rs
··· 25 25 let articles = ctx.content.get_source::<ArticleContent>("articles"); 26 26 let article = articles.get_entry(&params.article); 27 27 28 - let headings = article.data.get_headings(); 28 + let headings = article.data().get_headings(); 29 29 println!("{:?}", headings); 30 30 31 31 layout(article.render())
+2 -2
examples/blog/src/pages/index.rs
··· 19 19 @for entry in &articles.entries { 20 20 li { 21 21 a href=(get_page_url(&Article, ArticleParams { article: entry.id.clone() })) { 22 - h2 { (entry.data.title) } 22 + h2 { (entry.data().title) } 23 23 } 24 - p { (entry.data.description) } 24 + p { (entry.data().description) } 25 25 } 26 26 } 27 27 }
+1 -1
examples/oubli-basics/src/pages/index.rs
··· 17 17 (logo) 18 18 h1 { "Hello World" } 19 19 @for archetype in &archetype_store.entries { 20 - a href=(archetype.id) { (archetype.data.title) } 20 + a href=(archetype.id) { (archetype.data().title) } 21 21 } 22 22 }) 23 23 .into()
+2 -2
website/src/layout/docs_sidebars.rs
··· 9 9 let mut sections = std::collections::HashMap::new(); 10 10 11 11 for entry in content.entries.iter() { 12 - if let Some(section) = &entry.data.section { 12 + if let Some(section) = &entry.data().section { 13 13 sections.entry(section).or_insert_with(Vec::new).push(entry); 14 14 } 15 15 } ··· 43 43 @let url = format!("/docs/{}", entry.id); 44 44 @let is_current_page = url == ctx.current_url; 45 45 li."border-l-2"."hover:border-brand-red"."pl-3"."py-1".(if is_current_page { "text-brand-red border-brand-red" } else { "border-borders" }) { 46 - a.block href=(format!("/docs/{}/", entry.id)) { (entry.data.title) } // TODO: Use type-safe routing 46 + a.block href=(format!("/docs/{}/", entry.id)) { (entry.data().title) } // TODO: Use type-safe routing 47 47 } 48 48 } 49 49 }
+5 -5
website/src/pages/docs.rs
··· 13 13 .get_source::<DocsContent>("docs") 14 14 .get_entry("index"); 15 15 16 - let headings = index_page.data.get_headings().clone(); 16 + let headings = index_page.data().get_headings().clone(); 17 17 18 18 docs_layout(render_entry(index_page), ctx, &headings) 19 19 } ··· 22 22 fn render_entry(entry: &ContentEntry<DocsContent>) -> Markup { 23 23 html! { 24 24 section.mb-4.border-b."border-[#e9e9e7]".pb-2 { 25 - @if let Some(section) = &entry.data.section { 25 + @if let Some(section) = &entry.data().section { 26 26 p.text-sm.font-bold { (section) } 27 27 } 28 - h2.text-5xl.font-bold.mb-2 { (entry.data.title) } 29 - @if let Some(description) = &entry.data.description { 28 + h2.text-5xl.font-bold.mb-2 { (entry.data().title) } 29 + @if let Some(description) = &entry.data().description { 30 30 h3.text-lg { (description) } 31 31 } 32 32 } ··· 60 60 .get_source::<DocsContent>("docs") 61 61 .get_entry(&slug); 62 62 63 - let headings = entry.data.get_headings().clone(); 63 + let headings = entry.data().get_headings().clone(); 64 64 docs_layout(render_entry(entry), ctx, &headings) 65 65 } 66 66 }
+8 -8
website/src/pages/news.rs
··· 17 17 let mut articles_by_year: BTreeMap<String, Vec<_>> = BTreeMap::new(); 18 18 19 19 for article in &content.entries { 20 - if let Some(date) = &article.data.date { 20 + if let Some(date) = &article.data().date { 21 21 // Extract year from date (format: 2025-08-16) 22 22 let year = date.split('-').next().unwrap_or("Unknown").to_string(); 23 23 articles_by_year ··· 42 42 ul.space-y-8 { 43 43 @for article in articles { 44 44 li.border-b.border-gray-200.pb-4 { 45 - @if let Some(date) = &article.data.date { 45 + @if let Some(date) = &article.data().date { 46 46 p.text-sm.font-bold { (date) } 47 47 } 48 48 h3.text-5xl { 49 49 a."hover:text-brand-red" href=(article.id) { 50 - (article.data.title) 50 + (article.data().title) 51 51 } 52 52 } 53 - @if let Some(description) = &article.data.description { 53 + @if let Some(description) = &article.data().description { 54 54 p.text-lg.text-gray-600 { (description) } 55 55 } 56 56 } ··· 106 106 html! { 107 107 div.container.mx-auto."py-10"."pb-24"."max-w-[80ch]"."px-8"."sm:px-0" { 108 108 section.mb-4.border-b."border-[#e9e9e7]".pb-2 { 109 - @if let Some(date) = &entry.data.date { 109 + @if let Some(date) = &entry.data().date { 110 110 p.text-sm.font-bold { (date) } 111 111 } 112 - h1."text-6xl"."sm:text-7xl".font-bold { (entry.data.title) } 113 - @if let Some(description) = &entry.data.description { 112 + h1."text-6xl"."sm:text-7xl".font-bold { (entry.data().title) } 113 + @if let Some(description) = &entry.data().description { 114 114 p.text-xl."sm:text-2xl" { (description) } 115 115 } 116 116 } ··· 119 119 (PreEscaped(entry.render())) 120 120 } 121 121 122 - @if let Some(author) = &entry.data.author { 122 + @if let Some(author) = &entry.data().author { 123 123 h2."text-xl".font-bold.mt-12.text-center { (author) } 124 124 } 125 125 }