summaryrefslogtreecommitdiff
path: root/src/generate_html.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/generate_html.rs')
-rw-r--r--src/generate_html.rs167
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} &mdash; {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} &mdash; {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;
-}