Stupiod blog engine
0
fork

Configure Feed

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

Implement sitemap derivation

I think this structure will be able to
carry us to the glorious finish line.

+457 -269
-228
src/file.rs
··· 14 14 15 15 use crate::frontmatter::FrontMatter; 16 16 17 - fn make_mdast(data: &str) -> anyhow::Result<mdast::Node> { 18 - let options = { 19 - let mut out = ParseOptions::gfm(); 20 - out.constructs.frontmatter = true; 21 - out 22 - }; 23 - let ast = to_mdast(data, &options).map_err(|e| anyhow!("failed to parse markdown: {e}"))?; 24 - Ok(ast) 25 - } 26 - 27 - fn write_md_ast<'root>(writer: &mut impl io::Write, ast: &'root mdast::Node) -> anyhow::Result<()> { 28 - enum Work<'a> { 29 - Node(&'a mdast::Node), 30 - Lit(&'static str), 31 - Str(String), 32 - } 33 - 34 - let mut footnote_ids = Sequential::<&'root str>::default(); 35 - let mut footnote_defs = 36 - Vec::<Option<(&'root str, &'root [mdast::Node])>>::with_capacity(1 << 6); 37 - 38 - // Work contains unprocessed nodes. 39 - // 40 - // The capacity should be the largest amount of children or nesting we expect to see. 41 - let mut q: Vec<Work<'_>> = Vec::with_capacity(1 << 8); 42 - macro_rules! children { 43 - ($x:expr) => { 44 - q.extend($x.iter().rev().map(Work::Node)) 45 - }; 46 - } 47 - macro_rules! lit { 48 - ($x:expr) => { 49 - q.push(Work::Lit($x)) 50 - }; 51 - } 52 - macro_rules! fmt { 53 - ($fmt:literal, $($xs:expr),*) => { 54 - q.push(Work::Str( 55 - format!($fmt $(, $xs)*) 56 - )) 57 - }; 58 - } 59 - q.push(Work::Node(ast)); 60 - while let Some(work) = q.pop() { 61 - let node = match work { 62 - Work::Str(s) => { 63 - writer.write_all(s.as_bytes())?; 64 - continue; 65 - } 66 - Work::Lit(s) => { 67 - writer.write_all(s.as_bytes())?; 68 - continue; 69 - } 70 - Work::Node(node) => node, 71 - }; 72 - use mdast::Node::*; 73 - match node { 74 - Root(n) => { 75 - children!(n.children); 76 - } 77 - Paragraph(n) => { 78 - lit!("</p>"); 79 - children!(n.children); 80 - lit!("\n<p>"); 81 - } 82 - Blockquote(n) => { 83 - lit!("</blockquote>"); 84 - children!(n.children); 85 - lit!("\n<blockquote>"); 86 - } 87 - FootnoteDefinition(n) => { 88 - let id = footnote_ids.value(&n.identifier); 89 - let id_usize = id as usize; 90 - footnote_defs.resize(footnote_defs.len().max(id_usize + 1), None); 91 - footnote_defs[id_usize] = Some((n.identifier.as_str(), n.children.as_slice())); 92 - } 93 - List(n) => { 94 - if n.ordered { 95 - lit!("</ol>"); 96 - children!(n.children); 97 - lit!("\n<ol>"); 98 - } else { 99 - lit!("</ul>"); 100 - children!(n.children); 101 - lit!("\n<ul>"); 102 - } 103 - } 104 - ListItem(n) => { 105 - lit!("</li>"); 106 - match (n.spread, n.children.as_slice()) { 107 - (false, [Paragraph(inner)]) => { 108 - children!(inner.children); 109 - } 110 - (_, children) => { 111 - children!(children); 112 - } 113 - } 114 - lit!("<li>"); 115 - } 116 - Yaml(_) => { 117 - // Ignore front matter 118 - } 119 - Break(_) => { 120 - lit!("\n<br/>"); 121 - } 122 - InlineCode(n) => { 123 - fmt!("<code>{}</code>", &n.value); 124 - } 125 - Delete(n) => { 126 - lit!("</del>"); 127 - children!(n.children); 128 - lit!("<del>"); 129 - } 130 - Emphasis(n) => { 131 - lit!("</em>"); 132 - children!(n.children); 133 - lit!("<em>"); 134 - } 135 - FootnoteReference(n) => { 136 - let id = footnote_ids.value(&n.identifier); 137 - fmt!( 138 - "<sup><a href=\"#fn-{}\">{}</a></sup>", 139 - &n.identifier, 140 - id + 1 141 - ); 142 - } 143 - Html(n) => { 144 - fmt!("{}", n.value); 145 - } 146 - Image(n) => { 147 - let title = n 148 - .title 149 - .as_ref() 150 - .map(|x| format!("title={x}")) 151 - .unwrap_or_default(); 152 - fmt!("\n<img src={} alt={} {}/>", n.url, n.alt, title); 153 - } 154 - Strong(n) => { 155 - lit!("</strong>"); 156 - children!(n.children); 157 - lit!("<strong>"); 158 - } 159 - Link(n) => { 160 - lit!("</a>"); 161 - children!(n.children); 162 - fmt!("<a href={}>", n.url); 163 - } 164 - Code(n) => { 165 - fmt!("\n<pre><code>{}</code></pre>", n.value); 166 - } 167 - InlineMath(n) => { 168 - fmt!("<code>${}$</code>", n.value); 169 - } 170 - Math(n) => { 171 - fmt!("\n<pre>\n<code>\n$$\n{}\n$$\n</code>\n</pre>", n.value); 172 - } 173 - Text(n) => { 174 - writer.write_all(n.value.as_bytes())?; 175 - } 176 - ThematicBreak(_) => { 177 - lit!("\n<hr />"); 178 - } 179 - Table(n) => { 180 - lit!("\n</table>"); 181 - children!(n.children); 182 - lit!("\n<table>"); 183 - } 184 - TableRow(n) => { 185 - lit!("\n</tr>"); 186 - children!(n.children); 187 - lit!("\n<tr>"); 188 - } 189 - TableCell(n) => { 190 - lit!("</th>"); 191 - children!(n.children); 192 - lit!("\n<th>"); 193 - } 194 - Heading(n) => { 195 - fmt!("</h{}>", n.depth); 196 - children!(n.children); 197 - fmt!("\n<h{}>", n.depth); 198 - } 199 - MdxJsxFlowElement(_) => unimplemented!("MdxJsxFlowElement"), 200 - MdxjsEsm(_) => unimplemented!("MdxjsEsm"), 201 - Toml(_) => unimplemented!("Toml"), 202 - MdxTextExpression(_) => unimplemented!("MdxTextExpression"), 203 - ImageReference(_) => unimplemented!("ImageReference"), 204 - MdxJsxTextElement(_) => unimplemented!("MdxJsxTextElement"), 205 - LinkReference(_) => unimplemented!("LinkReference"), 206 - MdxFlowExpression(_) => unimplemented!("MdxFlowExpression"), 207 - Definition(_) => unimplemented!("Definition"), 208 - } 209 - } 210 - write!(writer, "<section class=\"footnotes\">\n<ol>\n")?; 211 - for def in footnote_defs.into_iter() { 212 - match def { 213 - None => { 214 - write!(writer, "<li>???</li>\n")?; 215 - } 216 - Some((identifier, children)) => { 217 - write!(writer, "<li id=\"fn-{identifier}\">")?; 218 - for n in children { 219 - write_md_ast(writer, n)?; 220 - } 221 - write!(writer, "</li>\n")?; 222 - } 223 - } 224 - } 225 - write!(writer, "</ol>\n</section>")?; 226 - write!(writer, "\n")?; 227 - Ok(()) 228 - } 229 - 230 - pub fn find_yaml_frontmatter<'root>(ast: &'root mdast::Node) -> Option<&'root str> { 231 - let mut q = vec![ast]; 232 - while let Some(n) = q.pop() { 233 - use mdast::Node::*; 234 - match n { 235 - Root(n) => { 236 - q.extend(n.children.iter()); 237 - } 238 - Yaml(n) => return Some(&n.value), 239 - _ => {} 240 - } 241 - } 242 - None 243 - } 244 - 245 17 /// Represents a file we process in our blog engine. 246 18 pub struct File<'a> { 247 19 rel_path: &'a Path,
src/file/counter.rs src/markdown/counter.rs
+40 -41
src/main.rs
··· 1 - use anyhow::{Context, anyhow}; 2 - use minijinja::Environment; 1 + use anyhow::anyhow; 2 + use minijinja::{Environment, context}; 3 3 use std::{ 4 - borrow::Cow, 5 4 fs::{self}, 5 + io::{BufWriter, Write}, 6 6 path::PathBuf, 7 7 }; 8 8 9 9 mod frontmatter; 10 - 11 - mod file; 12 - use file::File; 13 - 14 10 mod fs_utils; 11 + mod markdown; 12 + mod sitemap; 13 + 15 14 use fs_utils::copy_dir; 15 + use sitemap::SiteMap; 16 + 17 + use crate::markdown::{make_mdast, write_md_ast}; 16 18 17 19 /// A static string for usage errors. 18 20 const USAGE: &str = "usage: clog <input_dir> <output_dir>"; ··· 60 62 let env = Environment::new(); 61 63 let template_data = fs::read_to_string(self.template_dir.join("index.html"))?; 62 64 let template = env.template_from_str(&template_data)?; 63 - let mut dirs = vec![Cow::Borrowed(&self.content_dir)]; 64 - while let Some(dir) = dirs.pop() { 65 - for entry in fs::read_dir(dir.as_path())? { 66 - let entry = entry?; 67 - let file_type = entry.file_type()?; 68 - if !file_type.is_file() { 69 - if file_type.is_dir() { 70 - dirs.push(Cow::Owned(entry.path())); 71 - } 72 - continue; 73 - } 74 - let path = entry.path(); 75 - let Some(extension) = path.extension() else { 76 - continue; 77 - }; 78 - // Copy any images "in place". 79 - // Contains won't work because of the need to cast. 80 - if ["png", "jpg"].into_iter().any(|x| x == extension) { 81 - let rel_path = path.strip_prefix(&self.content_dir)?; 82 - let out_path = self.output_dir.join(rel_path); 83 - if let Some(parent) = out_path.parent() { 84 - fs::create_dir_all(parent)?; 85 - } 86 - fs::copy(&path, &self.output_dir.join(rel_path))?; 87 - continue; 88 - } 89 - // Skip non-markdown files. 90 - if !path.extension().map(|x| x == "md").unwrap_or(true) { 91 - continue; 92 - } 93 - File::read(&self.content_dir, &path) 94 - .with_context(|| format!("failed to read file: {:?}", &path))? 95 - .write(&self.output_dir, template.clone()) 96 - .with_context(|| format!("failed to write file: {:?}", &path))?; 65 + let site_map = SiteMap::build(&self.content_dir, &self.output_dir)?; 66 + for file in site_map.statics() { 67 + if let Some(parent) = file.out_path.parent() { 68 + fs::create_dir_all(parent)?; 69 + } 70 + fs::copy(&file.in_path, &file.out_path)?; 71 + } 72 + let mut buf = Vec::with_capacity(1 << 14); 73 + for page in site_map.pages() { 74 + let content = fs::read_to_string(&page.in_path)?; 75 + let md = make_mdast(&content)?; 76 + let body = { 77 + buf.clear(); 78 + write_md_ast(&mut buf, &md)?; 79 + String::from_utf8_lossy(&buf) 80 + }; 81 + if let Some(parent) = page.out_path.parent() { 82 + fs::create_dir_all(parent)?; 97 83 } 84 + let file = fs::File::create(&page.out_path)?; 85 + let mut writer = BufWriter::new(file); 86 + let ctx = context! { 87 + body => body, 88 + title => page.front_matter.title, 89 + date => page.front_matter.date, 90 + authors => page.front_matter.authors, 91 + published => page.front_matter.published, 92 + link => page.front_matter.link, 93 + tags => page.front_matter.tags, 94 + }; 95 + template.render_to_write(ctx, &mut writer)?; 96 + writer.flush()?; 98 97 } 99 98 Ok(()) 100 99 }
+238
src/markdown.rs
··· 1 + use anyhow::anyhow; 2 + use markdown::{ParseOptions, mdast, to_mdast}; 3 + use std::io; 4 + 5 + mod counter; 6 + 7 + use counter::Sequential; 8 + 9 + pub fn make_mdast(data: &str) -> anyhow::Result<mdast::Node> { 10 + let options = { 11 + let mut out = ParseOptions::gfm(); 12 + out.constructs.frontmatter = true; 13 + out 14 + }; 15 + let ast = to_mdast(data, &options).map_err(|e| anyhow!("failed to parse markdown: {e}"))?; 16 + Ok(ast) 17 + } 18 + 19 + pub fn write_md_ast<'root>( 20 + writer: &mut impl io::Write, 21 + ast: &'root mdast::Node, 22 + ) -> anyhow::Result<()> { 23 + enum Work<'a> { 24 + Node(&'a mdast::Node), 25 + Lit(&'static str), 26 + Str(String), 27 + } 28 + 29 + let mut footnote_ids = Sequential::<&'root str>::default(); 30 + let mut footnote_defs = 31 + Vec::<Option<(&'root str, &'root [mdast::Node])>>::with_capacity(1 << 6); 32 + 33 + // Work contains unprocessed nodes. 34 + // 35 + // The capacity should be the largest amount of children or nesting we expect to see. 36 + let mut q: Vec<Work<'_>> = Vec::with_capacity(1 << 8); 37 + macro_rules! children { 38 + ($x:expr) => { 39 + q.extend($x.iter().rev().map(Work::Node)) 40 + }; 41 + } 42 + macro_rules! lit { 43 + ($x:expr) => { 44 + q.push(Work::Lit($x)) 45 + }; 46 + } 47 + macro_rules! fmt { 48 + ($fmt:literal, $($xs:expr),*) => { 49 + q.push(Work::Str( 50 + format!($fmt $(, $xs)*) 51 + )) 52 + }; 53 + } 54 + q.push(Work::Node(ast)); 55 + while let Some(work) = q.pop() { 56 + let node = match work { 57 + Work::Str(s) => { 58 + writer.write_all(s.as_bytes())?; 59 + continue; 60 + } 61 + Work::Lit(s) => { 62 + writer.write_all(s.as_bytes())?; 63 + continue; 64 + } 65 + Work::Node(node) => node, 66 + }; 67 + use mdast::Node::*; 68 + match node { 69 + Root(n) => { 70 + children!(n.children); 71 + } 72 + Paragraph(n) => { 73 + lit!("</p>"); 74 + children!(n.children); 75 + lit!("\n<p>"); 76 + } 77 + Blockquote(n) => { 78 + lit!("</blockquote>"); 79 + children!(n.children); 80 + lit!("\n<blockquote>"); 81 + } 82 + FootnoteDefinition(n) => { 83 + let id = footnote_ids.value(&n.identifier); 84 + let id_usize = id as usize; 85 + footnote_defs.resize(footnote_defs.len().max(id_usize + 1), None); 86 + footnote_defs[id_usize] = Some((n.identifier.as_str(), n.children.as_slice())); 87 + } 88 + List(n) => { 89 + if n.ordered { 90 + lit!("</ol>"); 91 + children!(n.children); 92 + lit!("\n<ol>"); 93 + } else { 94 + lit!("</ul>"); 95 + children!(n.children); 96 + lit!("\n<ul>"); 97 + } 98 + } 99 + ListItem(n) => { 100 + lit!("</li>"); 101 + match (n.spread, n.children.as_slice()) { 102 + (false, [Paragraph(inner)]) => { 103 + children!(inner.children); 104 + } 105 + (_, children) => { 106 + children!(children); 107 + } 108 + } 109 + lit!("<li>"); 110 + } 111 + Yaml(_) => { 112 + // Ignore front matter 113 + } 114 + Break(_) => { 115 + lit!("\n<br/>"); 116 + } 117 + InlineCode(n) => { 118 + fmt!("<code>{}</code>", &n.value); 119 + } 120 + Delete(n) => { 121 + lit!("</del>"); 122 + children!(n.children); 123 + lit!("<del>"); 124 + } 125 + Emphasis(n) => { 126 + lit!("</em>"); 127 + children!(n.children); 128 + lit!("<em>"); 129 + } 130 + FootnoteReference(n) => { 131 + let id = footnote_ids.value(&n.identifier); 132 + fmt!( 133 + "<sup><a href=\"#fn-{}\">{}</a></sup>", 134 + &n.identifier, 135 + id + 1 136 + ); 137 + } 138 + Html(n) => { 139 + fmt!("{}", n.value); 140 + } 141 + Image(n) => { 142 + let title = n 143 + .title 144 + .as_ref() 145 + .map(|x| format!("title={x}")) 146 + .unwrap_or_default(); 147 + fmt!("\n<img src={} alt={} {}/>", n.url, n.alt, title); 148 + } 149 + Strong(n) => { 150 + lit!("</strong>"); 151 + children!(n.children); 152 + lit!("<strong>"); 153 + } 154 + Link(n) => { 155 + lit!("</a>"); 156 + children!(n.children); 157 + fmt!("<a href={}>", n.url); 158 + } 159 + Code(n) => { 160 + fmt!("\n<pre><code>{}</code></pre>", n.value); 161 + } 162 + InlineMath(n) => { 163 + fmt!("<code>${}$</code>", n.value); 164 + } 165 + Math(n) => { 166 + fmt!("\n<pre>\n<code>\n$$\n{}\n$$\n</code>\n</pre>", n.value); 167 + } 168 + Text(n) => { 169 + writer.write_all(n.value.as_bytes())?; 170 + } 171 + ThematicBreak(_) => { 172 + lit!("\n<hr />"); 173 + } 174 + Table(n) => { 175 + lit!("\n</table>"); 176 + children!(n.children); 177 + lit!("\n<table>"); 178 + } 179 + TableRow(n) => { 180 + lit!("\n</tr>"); 181 + children!(n.children); 182 + lit!("\n<tr>"); 183 + } 184 + TableCell(n) => { 185 + lit!("</th>"); 186 + children!(n.children); 187 + lit!("\n<th>"); 188 + } 189 + Heading(n) => { 190 + fmt!("</h{}>", n.depth); 191 + children!(n.children); 192 + fmt!("\n<h{}>", n.depth); 193 + } 194 + MdxJsxFlowElement(_) => unimplemented!("MdxJsxFlowElement"), 195 + MdxjsEsm(_) => unimplemented!("MdxjsEsm"), 196 + Toml(_) => unimplemented!("Toml"), 197 + MdxTextExpression(_) => unimplemented!("MdxTextExpression"), 198 + ImageReference(_) => unimplemented!("ImageReference"), 199 + MdxJsxTextElement(_) => unimplemented!("MdxJsxTextElement"), 200 + LinkReference(_) => unimplemented!("LinkReference"), 201 + MdxFlowExpression(_) => unimplemented!("MdxFlowExpression"), 202 + Definition(_) => unimplemented!("Definition"), 203 + } 204 + } 205 + write!(writer, "<section class=\"footnotes\">\n<ol>\n")?; 206 + for def in footnote_defs.into_iter() { 207 + match def { 208 + None => { 209 + write!(writer, "<li>???</li>\n")?; 210 + } 211 + Some((identifier, children)) => { 212 + write!(writer, "<li id=\"fn-{identifier}\">")?; 213 + for n in children { 214 + write_md_ast(writer, n)?; 215 + } 216 + write!(writer, "</li>\n")?; 217 + } 218 + } 219 + } 220 + write!(writer, "</ol>\n</section>")?; 221 + write!(writer, "\n")?; 222 + Ok(()) 223 + } 224 + 225 + pub fn find_yaml_frontmatter<'root>(ast: &'root mdast::Node) -> Option<&'root str> { 226 + let mut q = vec![ast]; 227 + while let Some(n) = q.pop() { 228 + use mdast::Node::*; 229 + match n { 230 + Root(n) => { 231 + q.extend(n.children.iter()); 232 + } 233 + Yaml(n) => return Some(&n.value), 234 + _ => {} 235 + } 236 + } 237 + None 238 + }
+179
src/sitemap.rs
··· 1 + use crate::{ 2 + frontmatter::FrontMatter, 3 + markdown::{find_yaml_frontmatter, make_mdast}, 4 + }; 5 + use anyhow::anyhow; 6 + use std::{ 7 + borrow::Cow, 8 + cmp::Reverse, 9 + collections::HashMap, 10 + ffi::OsStr, 11 + fs, 12 + path::{Path, PathBuf}, 13 + }; 14 + 15 + const STATIC_EXTENSIONS: [&str; 2] = ["png", "jpg"]; 16 + 17 + fn is_static_extension(e: &OsStr) -> bool { 18 + STATIC_EXTENSIONS.iter().any(|&x| x == e) 19 + } 20 + 21 + /// Translate a path, moving it from having in_path as parent, to out_path. 22 + fn translate(in_path: &Path, out_path: &Path, path: &Path) -> anyhow::Result<PathBuf> { 23 + Ok(out_path.join(path.strip_prefix(in_path)?)) 24 + } 25 + 26 + fn read_front_matter(path: &Path) -> anyhow::Result<FrontMatter> { 27 + let contents = fs::read_to_string(path)?; 28 + let ast = make_mdast(&contents)?; 29 + let yaml = find_yaml_frontmatter(&ast); 30 + let fm = FrontMatter::try_from_yaml(&path, yaml)?; 31 + Ok(fm) 32 + } 33 + 34 + /// A Static file, like an image. 35 + /// 36 + /// This is still contained inside of the content folder. 37 + #[derive(Debug)] 38 + pub struct Static { 39 + pub in_path: PathBuf, 40 + pub out_path: PathBuf, 41 + } 42 + 43 + /// A page with actual markdown content. 44 + #[derive(Debug)] 45 + pub struct Page { 46 + pub name: String, 47 + pub link: String, 48 + pub front_matter: FrontMatter, 49 + pub in_path: PathBuf, 50 + pub out_path: PathBuf, 51 + } 52 + 53 + impl Page { 54 + pub fn folder(&self, base: &Path) -> anyhow::Result<Option<PathBuf>> { 55 + let Some(parent) = self.in_path.parent() else { 56 + return Ok(None); 57 + }; 58 + Ok(Some(parent.strip_prefix(base)?.to_path_buf())) 59 + } 60 + } 61 + 62 + type PageIndex = usize; 63 + 64 + fn sort_page_indices(pages: &[Page], indices: &mut [PageIndex]) { 65 + indices.sort_by_key(|&i| { 66 + ( 67 + Reverse(&pages[i].front_matter.date), 68 + Reverse(&pages[i].front_matter.title), 69 + ) 70 + }); 71 + } 72 + 73 + #[derive(Debug)] 74 + pub struct SiteMap { 75 + statics: Vec<Static>, 76 + pages: Vec<Page>, 77 + pages_by_name: HashMap<String, Vec<usize>>, 78 + folders: HashMap<PathBuf, Vec<usize>>, 79 + } 80 + 81 + impl SiteMap { 82 + pub fn build(in_path: &Path, out_path: &Path) -> anyhow::Result<Self> { 83 + let mut statics: Vec<Static> = Vec::with_capacity(128); 84 + let mut pages: Vec<Page> = Vec::with_capacity(1024); 85 + let mut q = vec![Cow::Borrowed(in_path)]; 86 + while let Some(dir) = q.pop() { 87 + for entry in fs::read_dir(dir)? { 88 + let entry = entry?; 89 + let file_type = entry.file_type()?; 90 + if file_type.is_dir() { 91 + q.push(Cow::Owned(entry.path())); 92 + continue; 93 + } 94 + if !file_type.is_file() { 95 + continue; 96 + } 97 + let path = entry.path(); 98 + if path.to_str().is_none() { 99 + continue; 100 + } 101 + let Some(extension) = path.extension() else { 102 + continue; 103 + }; 104 + if is_static_extension(extension) { 105 + statics.push(Static { 106 + out_path: translate(in_path, out_path, &path)?, 107 + in_path: path, 108 + }); 109 + continue; 110 + } 111 + if extension != "md" { 112 + continue; 113 + } 114 + let front_matter = read_front_matter(&path)?; 115 + let name = path 116 + .file_stem() 117 + .and_then(|x| x.to_str()) 118 + .ok_or_else(|| anyhow!("failed to get file stem"))? 119 + .to_string(); 120 + let link = { 121 + let rel_path = path.strip_prefix(in_path)?.with_extension("html"); 122 + let out_segment = rel_path.to_str().unwrap(); 123 + let mut out = String::with_capacity(1 + out_segment.len()); 124 + out.push('/'); 125 + out.push_str(out_segment); 126 + out 127 + }; 128 + pages.push(Page { 129 + name, 130 + link, 131 + front_matter, 132 + out_path: translate(in_path, out_path, &path.with_extension("html"))?, 133 + in_path: path, 134 + }); 135 + } 136 + } 137 + let pages_by_name = { 138 + let mut out = HashMap::<_, Vec<_>>::new(); 139 + for (i, page) in pages.iter().enumerate() { 140 + out.entry(page.name.clone()).or_default().push(i); 141 + } 142 + out 143 + }; 144 + // Generate warnings for duplicate names 145 + for (name, indices) in &pages_by_name { 146 + if indices.len() > 1 { 147 + eprintln!("WARNING: `{name}` has conflicts"); 148 + for &i in indices { 149 + eprintln!("\t{}", pages[i].in_path.to_string_lossy()); 150 + } 151 + } 152 + } 153 + let folders = { 154 + let mut out = HashMap::<_, Vec<_>>::new(); 155 + for (i, page) in pages.iter().enumerate() { 156 + if let Some(folder) = page.folder(in_path)? { 157 + out.entry(folder).or_default().push(i); 158 + } 159 + } 160 + out 161 + }; 162 + Ok(Self { 163 + statics, 164 + pages, 165 + pages_by_name, 166 + folders, 167 + }) 168 + } 169 + 170 + /// Iterate over all the static files in the content directory. 171 + pub fn statics(&self) -> impl Iterator<Item = &Static> { 172 + self.statics.iter() 173 + } 174 + 175 + /// Iterate over all of the pages. 176 + pub fn pages(&self) -> impl Iterator<Item = &Page> { 177 + self.pages.iter() 178 + } 179 + }