diff options
Diffstat (limited to 'src/generate_html.rs')
| -rw-r--r-- | src/generate_html.rs | 167 |
1 files changed, 96 insertions, 71 deletions
diff --git a/src/generate_html.rs b/src/generate_html.rs index 5526a06..0e6ce75 100644 --- a/src/generate_html.rs +++ b/src/generate_html.rs @@ -4,82 +4,89 @@ use markdown::*; use recipe::*; +const DEFAULT_TEMPLATE: &str = "\ +<!DOCTYPE html> + <head> + <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_link} + {parent_link} + </nav> + <h1 id='title'>{page_name_html}</h1> + </header> + <main> + {main} + </main> + </body> +</html>"; + + pub fn generate_html(page: &Page, website: &Website) -> String { let root = page.root(); // Get page name as a plain string and as an HTML fragment. - let mut page_name_plain = page.name.clone(); - let mut page_name_html = sanitize_text(&page_name_plain, true); + let mut page_name = page.name.plain(); + let mut page_name_html = sanitize_text(&page_name, true); // Find any override-title fragments. for block in &page.document.blocks { if let Block::Fragment { language, content } = block { if language == "override-title" { let line = Line::from_str(content); - page_name_plain = line.to_string(); - page_name_html = line_to_html(&line, page, website); + page_name = line.to_string(); + page_name_html = line_to_html(&line, page, website, &None); } } } - page_name_plain = sanitize_text(&page_name_plain, true); + page_name = sanitize_text(&page_name, true); // Get the URL of the parent page. - let site_name = sanitize_text(&website.name, true); + let site_name = sanitize_text(&website.name.plain(), true); let mut parent_url = String::new(); for segment in &page.parents { - parent_url.push_str(&make_url_safe(segment)); parent_url.push('/'); + parent_url.push_str(&segment.slug()); parent_url.push('/'); } parent_url.pop(); let head = get_html_head(page, website); let head = head.trim(); let home_link = format!("<a id='home' href='{root}index.html'>{site_name}</a>"); let parent_link = match page.parents.last() { - Some(name) => format!("<a id='parent' href='../{}.html'>{name}</a>", make_url_safe(name)), + Some(name) => format!("<a id='parent' href='../{}.html'>{name}</a>", name.slug()), None => String::new(), }; // Format tables of contents and the main page. let toc = get_table_of_contents(page); - let toc_main = if page.headings.len() >= 3 { + let toc_compact = if page.headings.len() >= 3 { format!("<details><summary></summary>\n{toc}</details>\n") } else { String::new() }; - let toc_side = format!("<div>{toc}</div>\n"); let main = document_to_html(page, website); let main = main.trim(); - format!("\ -<!DOCTYPE html> -<head> -<title>{page_name_plain} — {site_name}</title> -<meta charset='UTF-8'> -<meta name='viewport' content='width=device-width, initial-scale=1'> -{head} -</head> -<body> -<nav id='outline' class='hidden'> -<h1></h1> -{toc_side} -</nav> -<div id='page'> -<header> -<nav id='up'> -{home_link} -{parent_link} -</nav> -<h1 id='title'>{page_name_html}</h1> -<nav id='toc'> -{toc_main} -</nav> -</header> -<main> -{main} -</main> -</div> -</body> -</html>") + let mut template = website.config.get("html.template"); + if template.trim().is_empty() { + template = DEFAULT_TEMPLATE.to_string(); + } + + template + .replace("{site_name}", &site_name ) + .replace("{page_name}", &page_name ) + .replace("{page_name_html}", &page_name_html) + .replace("{home_link}", &home_link ) + .replace("{parent_link}", &parent_link ) + .replace("{head}", &head ) + .replace("{toc_compact}", &toc_compact ) + .replace("{toc}", &toc ) + .replace("{main}", &main ) } pub fn generate_html_redirect(path: &str, website: &Website) -> String { - let head = website.get_config("html.redirect.head"); let head = head.trim(); + let head = website.config.get("html.redirect.head"); let head = head.trim(); let path = sanitize_text(path, false); format!("\ <!DOCTYPE html> @@ -107,7 +114,7 @@ pub fn get_html_head(page: &Page, website: &Website) -> String { } } if include_default_head { - html_head.insert_str(0, &website.get_config("html.head")); + html_head.insert_str(0, &website.config.get("html.head")); } let root = page.root(); html_head @@ -118,12 +125,12 @@ pub fn get_html_head(page: &Page, website: &Website) -> String { pub fn get_table_of_contents(page: &Page) -> String { let mut toc = String::from("<ul>\n"); - let site_name = sanitize_text(&page.name, true); + let site_name = sanitize_text(&page.name.plain(), true); toc.push_str(&format!("<li class='l1'><a href='#title'>{site_name}</a></li>\n")); for heading in &page.headings { let name = &heading.name; - let url = &heading.url; + let url = &heading.slug(); let class = match heading.level { Level::Heading1 => "l1", Level::Heading2 => "l2", @@ -140,9 +147,10 @@ pub fn document_to_html(page: &Page, website: &Website) -> String { let from = &page; let root = page.root(); let mut html = String::new(); + let mut prefix = None; macro_rules! line_to_html { - ($l:expr) => {{ line_to_html(&$l, page, website) }}; } + ($l:expr) => {{ line_to_html(&$l, page, website, &prefix) }}; } macro_rules! html { ($($arg:tt)*) => {{ html.push_str(&format!($($arg)*)); html.push('\n'); }}; } macro_rules! tag { @@ -159,14 +167,15 @@ pub fn document_to_html(page: &Page, website: &Website) -> String { if let Level::Heading1 = level { html!("</article>"); html!("<article>"); + prefix = Some(to_slug(&line.to_string())); // 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(), + Some(heading) => heading.slug(), None => unreachable!("Couldn't find heading in headings list"), }; - // let url = make_url_safe(strip_appendix(&line.to_string())); + // let url = to_slug(strip_appendix(&line.to_string())); let heading_tag = match level { Level::Heading1 => "h1", Level::Heading2 => "h2", @@ -288,7 +297,7 @@ pub fn document_to_html(page: &Page, website: &Website) -> String { if let Some((name, file_name)) = line.split_once("::") { let name = name.trim(); let file_name = file_name.trim(); - let ParsedLink { path, class, label } = parse_internal_link(name, page, website); + let ParsedLink { path, class, label } = parse_internal_link(name, page, website, &prefix); if let Some(image_paths) = website.has_image(file_name, &root) { let thumb = sanitize_text(&image_paths.thumb, false); html!("<a href='{path}' class='{class}'><img src='{thumb}'/><p>{label}</p></a>") @@ -301,9 +310,9 @@ pub fn document_to_html(page: &Page, website: &Website) -> String { } }), _ => wrap!("pre", format!("class='{language}'"), { - if let Some(i) = website.highlighters.languages.get(language) { + if let Some(i) = website.config.languages.get(language) { let mut source = String::new(); - let highlighter = &website.highlighters.highlighters[*i]; + let highlighter = &website.config.highlighters[*i]; for span in highlighter.highlight(content) { if span.tag.is_empty() { source.push_str(&sanitize_text(&span.text, false)); @@ -366,7 +375,7 @@ pub fn document_to_html(page: &Page, website: &Website) -> String { -fn line_to_html(line: &Line, page: &Page, website: &Website) -> String { +fn line_to_html(line: &Line, page: &Page, website: &Website, prefix: &Option<String>) -> String { let mut html = String::new(); for line_element in &line.tokens { match line_element { @@ -381,7 +390,7 @@ fn line_to_html(line: &Line, page: &Page, website: &Website) -> String { Token::Math(text) => { let text = &sanitize_text(text, false); html.push_str(&format!("<span class='math'>{text}</span>")) } Token::InternalLink{ label: link_label, path } => { - let ParsedLink { path, class, mut label } = parse_internal_link(path, page, website); + let ParsedLink { path, class, mut label } = parse_internal_link(path, page, website, prefix); if !link_label.is_empty() { label = link_label.to_string(); } @@ -404,30 +413,56 @@ struct ParsedLink { pub class: &'static str, } -fn parse_internal_link(name: &str, page: &Page, website: &Website) -> ParsedLink { +fn parse_internal_link(name: &str, page: &Page, website: &Website, prefix: &Option<String>) -> ParsedLink { let from = &page; let (class, label, path) = match name.split_once('#') { - Some(("", heading)) => ("heading", heading, format!("#{}", strip_appendix(heading))), - Some((page, heading)) => ("page", heading, format!("{page}.html#{}", strip_appendix(heading))), + Some(("", heading)) => ("heading", heading, format!("#{heading}")), + Some((page, heading)) => ("page", heading, format!("{page}.html#{heading}")), _ => ("page", name, format!("{name}.html")), }; - let mut path = make_url_safe(&path); + let mut path = to_slug(&path); let label = match label.rsplit_once('/') { Some((_, label)) => sanitize_text(label.trim(), true), None => sanitize_text(label.trim(), true), }; - // Check that the linked internal page exists. + // Check that the linked internal page with heading exists. if class == "page" { match website.has_page(page, &path, "html") { Some(resolved) => path = resolved, None => warn!("Page {from:?} contains link to nonexistent page {path:?}"), } } - // Check that the heading exists. + // Check that the heading exists on this page. if class == "heading" { - let heading = path.strip_prefix('#').unwrap(); - if !page.headings.iter().any(|h| h.url == heading) { - warn!("Page {from:?} contains link to nonexistent internal heading {heading:?}"); + let plain_heading = path.strip_prefix('#').unwrap().to_string(); + let prefixed_heading = match prefix { + Some(prefix) => format!("{prefix}-{plain_heading}"), + None => plain_heading.to_string(), + }; + let mut matched = false; + for heading in &page.headings { + if heading.name.slug() == plain_heading { + if heading.prefix.is_some() { + // The matched heading has a prefix, so is one of many. + // The prefix must match, we must disambiguate the path. + if heading.slug() == prefixed_heading { + matched = true; + path = format!("#{prefixed_heading}"); + break; + } + } else { + // The matched heading has no prefix, so is unique on the page. + matched = true; + break + } + } + } + if !matched { + let prefix_note = match prefix { + Some(prefix) => format!(" (under {prefix:?})"), + None => format!(""), + }; + warn!("Page {from:?} contains link to nonexistent internal heading {plain_heading:?}{prefix_note}"); } } let path = url_encode(&path); @@ -466,13 +501,3 @@ fn parse_external_link(label: &str, path: &str, page: &Page, website: &Website) let label = sanitize_text(&label, true); ParsedLink { path, class: "external", label } } - -/// Remove a 'Appendix #: ' prefix from a string. -pub fn strip_appendix(text: &str) -> &str { - if let Some((prefix, name)) = text.split_once(": ") { - if prefix.starts_with("Appendix") { - return name; - } - } - return text; -} |
