diff options
| author | Ben Bridle <ben@derelict.engineering> | 2025-12-12 19:59:27 +1300 |
|---|---|---|
| committer | Ben Bridle <ben@derelict.engineering> | 2025-12-12 19:59:27 +1300 |
| commit | 50df287852367d3e50779155c6e92b6e2a388c9d (patch) | |
| tree | fc3d5322dcbe44eb235f40996a533b265b030fef | |
| parent | e83e426f7e01a05086418fe8246aef95e5e13dfb (diff) | |
| download | toaster-50df287852367d3e50779155c6e92b6e2a388c9d.zip | |
Allow pages to contain duplicate headings under different h1 headings
This kind of works, but the whole system will have to be rewritten
from the ground up so that every heading knows its own canonical name.
| -rw-r--r-- | src/collect_files.rs | 30 | ||||
| -rw-r--r-- | src/generate_html.rs | 26 |
2 files changed, 40 insertions, 16 deletions
diff --git a/src/collect_files.rs b/src/collect_files.rs index cb785fc..30dcb98 100644 --- a/src/collect_files.rs +++ b/src/collect_files.rs @@ -33,6 +33,7 @@ pub struct Heading { pub name: String, pub url: String, pub level: Level, + pub block_id: usize, } pub struct StaticItem { @@ -199,23 +200,44 @@ impl Website { "md" => { let markdown = std::fs::read_to_string(&source_path).unwrap(); let document = MarkdownDocument::from_str(&markdown); + // Collect headings, check for duplicates. let mut heading_set = HashSet::new(); let mut duplicates = HashSet::new(); - let headings = document.blocks.iter() - .filter_map(|block| if let Block::Heading { line, level } = block { + let mut headings: Vec<_> = document.blocks.iter().enumerate() + .filter_map(|(block_id, block)| if let Block::Heading { line, level } = block { let name = line.to_string(); let url = make_url_safe(strip_appendix(&name)); let level = level.to_owned(); if !heading_set.insert(url.clone()) { duplicates.insert(url.clone()); } - Some(Heading { name, url, level }) + Some(Heading { name, url, level, block_id }) } else { None }).collect(); + + // Namespace any duplicate headings to the parent h1 heading. + let mut parent_url = String::new(); + for heading in &mut headings { + if let Level::Heading1 = heading.level { + parent_url = heading.url.clone(); + } + if duplicates.contains(&heading.url) { + heading.url = format!("{parent_url}-{}", heading.url); + } + } + // Check for duplicates again, and warn if any. + heading_set.clear(); + duplicates.clear(); + for heading in &headings { + if !heading_set.insert(heading.url.clone()) { + duplicates.insert(heading.url.clone()); + } + } for url in duplicates { warn!("Page {full_name:?} contains multiple headings with ID \"#{url}\""); } + if name_url == "+index" { if parents.is_empty() { // This is the index file for the whole site. @@ -351,7 +373,7 @@ impl Website { if !path.starts_with('/') { path = format!("{}{path}", from.parent_url()); } - let path = make_url_safe(&collapse_path(&path)); + path = make_url_safe(&collapse_path(&path)); // Find page with this path in website. for page in &self.pages { diff --git a/src/generate_html.rs b/src/generate_html.rs index 33158a5..306892b 100644 --- a/src/generate_html.rs +++ b/src/generate_html.rs @@ -28,12 +28,10 @@ pub fn generate_html(page: &Page, website: &Website) -> String { None => String::new(), }; let toc = get_table_of_contents(page); - let toc_main = if page.headings.len() > 3 { + let toc_main = if page.headings.len() >= 3 { format!("<details><summary></summary>\n{toc}</details>\n") } else { String::new() }; - let toc_side = if page.headings.len() > 3 { - format!("<div>{toc}</div>\n") - } else { String::new() }; + let toc_side = format!("<div>{toc}</div>\n"); let main = document_to_html(page, website); let main = main.trim(); format!("\ @@ -45,7 +43,8 @@ pub fn generate_html(page: &Page, website: &Website) -> String { {head} </head> <body> -<nav id='toc-side'> +<nav id='outline' class='hidden'> +<h1></h1> {toc_side} </nav> <div id='page'> @@ -107,9 +106,6 @@ pub fn get_html_head(page: &Page, website: &Website) -> String { pub fn get_table_of_contents(page: &Page) -> String { - if page.headings.len() < 3 { - return String::new(); - } let mut toc = String::from("<ul>\n"); let site_name = sanitize_text(&page.name, true); toc.push_str(&format!("<li class='l1'><a href='#title'>{site_name}</a></li>\n")); @@ -146,21 +142,27 @@ pub fn document_to_html(page: &Page, website: &Website) -> String { ($t:expr,$f:expr) => {{ html!("<{}>", $t); $f; html!("</{}>", $t); }}; } wrap!("article", - for block in &page.document.blocks { + for (i, block) in page.document.blocks.iter().enumerate() { match block { Block::Heading { level, line } => { if let Level::Heading1 = level { html!("</article>"); html!("<article>"); + // html!("<nav class='return'><a href='#'></a></nav>"); + }; + // Find namespaced heading ID from headings list. + let url = match page.headings.iter().find(|h| h.block_id == i) { + Some(heading) => heading.url.clone(), + None => unreachable!("Couldn't find heading in headings list"), }; - let id = make_url_safe(strip_appendix(&line.to_string())); + // let url = make_url_safe(strip_appendix(&line.to_string())); let heading_tag = match level { Level::Heading1 => "h1", Level::Heading2 => "h2", Level::Heading3 => "h3", }; - wrap!(heading_tag, format!("id='{id}'"), { - tag!("a", line, format!("href='#{id}'")); + wrap!(heading_tag, format!("id='{url}'"), { + tag!("a", line, format!("href='#{url}'")); }); } Block::Paragraph(line) => tag!("p", line), |
