diff options
author | Ben Bridle <ben@derelict.engineering> | 2025-01-09 22:15:55 +1300 |
---|---|---|
committer | Ben Bridle <ben@derelict.engineering> | 2025-01-09 22:16:12 +1300 |
commit | ee28abce78ffa3b53ff039ff8640a3b37dc5348b (patch) | |
tree | 2cec62a7930874d1164a1d74e24778a9d4b5c286 /src/generate_html.rs | |
parent | ec6ef10964fd605d7a911fee47bc3cc0a031bdaa (diff) | |
download | toaster-ee28abce78ffa3b53ff039ff8640a3b37dc5348b.zip |
Rewrite link handling and add navigation features to generated HTML
Diffstat (limited to 'src/generate_html.rs')
-rw-r--r-- | src/generate_html.rs | 105 |
1 files changed, 81 insertions, 24 deletions
diff --git a/src/generate_html.rs b/src/generate_html.rs index 84c3bdb..dd08885 100644 --- a/src/generate_html.rs +++ b/src/generate_html.rs @@ -4,44 +4,94 @@ use markdown::*; pub fn generate_html(document: &MarkdownDocument, page: &Page, website: &Website) -> String { + let root = page.root(); + let page_name = &page.name; + let site_name = &website.name; + let mut parent_url = String::new(); + for segment in &page.parents { + parent_url.push_str(&make_url_safe(segment)); parent_url.push('/'); + } + parent_url.pop(); + let parent_name = match page.parents.get(page.parents.len()-1) { + Some(parent) => parent.to_string(), + None => String::new(), + }; + + let head = get_html_head(document, page); let head = head.trim(); + let mut home = format!("<a id='home' href='{root}index.html'>{site_name}</a>"); + let mut parent = format!("<a id='parent' href='../{parent_url}.html'>{parent_name}</a>"); + let mut title = format!("<h1 id='title'>{page_name}</h1>"); + let mut toc = get_table_of_contents(page); + let main = document_to_html(document, page, website); let main = main.trim(); + + if page.parents.is_empty() { + parent.clear(); + if page.name_url == "index" { + home.clear(); + title.clear(); + toc.clear(); + } + } + format!("\ <!DOCTYPE html> <head> -<title>{} — {}</title> +<title>{page_name} — {site_name}</title> <meta charset='UTF-8'> <meta name='viewport' content='width=device-width, initial-scale=1'> -{} +{head} </head> <body> +<header> +<nav id='up'> +{home} +{parent} +</nav> +{title} +{toc} +</header> <main> -{} +{main} </main> </body> -</html> \ -", - page.name, website.name, - get_html_head(document, page).trim(), - document_to_html(document, page, website).trim() - ) +</html>") } - pub fn get_html_head(document: &MarkdownDocument, page: &Page) -> String { if let Some(Block::Fragment { language, content }) = document.blocks.first() { if language == "embed-html-head" { return content.to_string(); } } - let back = page.back_string(); + let root = page.root(); format!("\ -<link rel='stylesheet' type='text/css' media='screen' href='{back}static/screen.css'> -<link rel='stylesheet' type='text/css' media='print' href='{back}static/print.css'> -<script src='{back}static/render_math.js' defer></script> \ +<link rel='stylesheet' type='text/css' media='screen' href='{root}static/screen.css'> +<link rel='stylesheet' type='text/css' media='print' href='{root}static/print.css'> +<script src='{root}static/render_math.js' defer></script> \ ") } +pub fn get_table_of_contents(page: &Page) -> String { + if page.headings.len() < 3 { + return String::new(); + } + let mut toc = String::from("<nav id='toc'><details><summary></summary><ul>\n"); + for heading in &page.headings { + let name = &heading.name; + let url = &heading.url; + let class = match heading.level { + Level::Heading1 => "l1", + Level::Heading2 => "l2", + Level::Heading3 => "l3", + }; + toc.push_str(&format!("<li><a href='#{url}' class='{class}'>{name}</a></li>\n")); + } + toc.push_str("</ul></details></nav>\n"); + return toc; +} + pub fn document_to_html(document: &MarkdownDocument, page: &Page, website: &Website) -> String { let mut html = String::new(); @@ -54,9 +104,10 @@ pub fn document_to_html(document: &MarkdownDocument, page: &Page, website: &Webs ($t:expr,$l:expr,$c:expr) => { html!("<{} {}>{}</{}>", $t, $c, line_to_html!($l), $t) }; ($t:expr,$l:expr) => { html!("<{}>{}</{}>", $t, line_to_html!($l), $t) }; } macro_rules! wrap { - ($t:expr,$f:expr) => {{ html!("<{}>", $t); $f; html!("</{}>", $t); }}; - } + ($t:expr,$c:expr,$f:expr) => {{ html!("<{} {}>", $t, $c); $f; html!("</{}>", $t); }}; + ($t:expr,$f:expr) => {{ html!("<{}>", $t); $f; html!("</{}>", $t); }}; } + let root = page.root(); for block in &document.blocks { match block { Block::Heading { level, line } => match level { @@ -96,11 +147,17 @@ pub fn document_to_html(document: &MarkdownDocument, page: &Page, website: &Webs }), Block::Note(lines) => wrap!("aside", for line in lines { tag!("p", line) }), Block::Embedded { label, path } => match path.rsplit_once('.') { - Some((_, extension)) => match extension.to_lowercase().as_str() { - "jpg"|"jpeg"|"png"|"webp"|"gif"|"tiff" => html!( - "<figure><a href='{path}'><img src='{path}' alt='{label}' title='{label}'></a></figure>"), - "mp3"|"wav"|"m4a" => html!("<audio src='{path}' controls>"), - ext @ _ => warn!("Unrecognised extension for embedded file {path:?} with extension {ext:?} in page {:?}", page.name), + Some((_, extension)) => { + let path = match path.strip_prefix('/') { + Some(stripped) => format!("{root}{stripped}"), + None => path.to_string(), + }; + match extension.to_lowercase().as_str() { + "jpg"|"jpeg"|"png"|"webp"|"gif"|"tiff" => html!( + "<figure><a href='{path}'><img src='{path}' alt='{label}' title='{label}'></a></figure>"), + "mp3"|"wav"|"m4a" => html!("<audio src='{path}' controls>"), + ext @ _ => warn!("Unrecognised extension for embedded file {path:?} with extension {ext:?} in page {:?}", page.name), + } } _ => warn!("Cannot embed file {path:?} with no file extension in page {:?}", page.name), } @@ -119,7 +176,7 @@ pub fn document_to_html(document: &MarkdownDocument, page: &Page, website: &Webs } } Block::Break => html!("<hr>"), - Block::Table(table) => wrap!("table", { + Block::Table(table) => wrap!("div", "class='table'", wrap!("table", { wrap!("thead", wrap!("tr", for column in &table.columns { tag!("th", column.name); @@ -150,7 +207,7 @@ pub fn document_to_html(document: &MarkdownDocument, page: &Page, website: &Webs }) }) }; - }) + })) } } return html; @@ -194,7 +251,7 @@ fn line_to_html(line: &Line, page: &Page, website: &Website) -> String { // Check that the heading exists. if class == "heading" { let heading = path.strip_prefix('#').unwrap().to_string(); - if !page.headings.contains(&heading) { + if !page.headings.iter().any(|h| h.url == heading) { warn!("Page {:?} contains link to nonexistent internal heading {heading:?}", page.name); } } |