Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/mdbook-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,9 @@ pub struct HtmlConfig {
/// If enabled, the sidebar includes navigation for headers on the current
/// page. Default is `true`.
pub sidebar_header_nav: bool,
/// If enabled, HTML files will not have the `.html` extension in URLs.
/// Defaults to `false`.
pub no_html_extension: bool,
}

impl Default for HtmlConfig {
Expand Down Expand Up @@ -548,6 +551,7 @@ impl Default for HtmlConfig {
redirect: HashMap::new(),
hash_files: true,
sidebar_header_nav: true,
no_html_extension: false,
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/mdbook-html/front-end/templates/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
<!-- populated by js -->
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
<noscript>
<iframe class="sidebar-iframe-outer" src="{{ path_to_root }}toc.html"></iframe>
{{#if no_html_extension}}<iframe class="sidebar-iframe-outer" src="{{ path_to_root }}_toc/"></iframe>{{else}}<iframe class="sidebar-iframe-outer" src="{{ path_to_root }}toc.html"></iframe>{{/if}}
</noscript>
<div id="mdbook-sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
Expand Down Expand Up @@ -168,7 +168,7 @@

<div class="right-buttons">
{{#if print_enable}}
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
{{#if no_html_extension}}<a href="{{ path_to_root }}_print/" title="Print this book" aria-label="Print this book">{{else}}<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">{{/if}}
{{fa "solid" "print" "print-button"}}
</a>
{{/if}}
Expand Down
16 changes: 16 additions & 0 deletions crates/mdbook-html/front-end/templates/toc.js.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ class MDBookSidebarScrollbox extends HTMLElement {
this.innerHTML = '{{#toc}}{{/toc}}';
// Set the current, active page, and reveal it if it's hidden
let current_page = document.location.href.toString().split('#')[0].split('?')[0];
{{#if no_html_extension}}
if (current_page.endsWith('index.html')) {
current_page = current_page.slice(0, -'index.html'.length);
}
{{else}}
if (current_page.endsWith('/')) {
current_page += 'index.html';
}
{{/if}}
const links = Array.prototype.slice.call(this.querySelectorAll('a'));
const l = links.length;
for (let i = 0; i < l; ++i) {
Expand All @@ -23,10 +29,20 @@ class MDBookSidebarScrollbox extends HTMLElement {
link.href = path_to_root + href;
}
// The 'index' page is supposed to alias the first chapter in the book.
{{#if no_html_extension}}
let link_href = link.href;
if (link_href.endsWith('index.html')) {
link_href = link_href.slice(0, -'index.html'.length);
}
if (link_href === current_page
|| i === 0
&& path_to_root === '') {
{{else}}
if (link.href === current_page
|| i === 0
&& path_to_root === ''
&& current_page.endsWith('/index.html')) {
{{/if}}
link.classList.add('active');
let parent = link.parentElement;
while (parent) {
Expand Down
77 changes: 67 additions & 10 deletions crates/mdbook-html/src/html/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ where
let href: StrTendril = if matches!(link_type, LinkType::Email) {
format!("mailto:{dest_url}").into()
} else {
fix_link(dest_url).into_tendril()
fix_link(dest_url, self.options.config.no_html_extension).into_tendril()
};
let mut a = Element::new("a");
a.insert_attr("href", href);
Expand All @@ -558,7 +558,7 @@ where
id: _,
} => {
let mut img = Element::new("img");
let src = fix_link(dest_url).into_tendril();
let src = fix_link(dest_url, self.options.config.no_html_extension).into_tendril();
img.insert_attr("src", src);
if !title.is_empty() {
img.insert_attr("title", title.into_tendril());
Expand Down Expand Up @@ -675,7 +675,7 @@ where
self_closing: tag.self_closing,
was_raw: true,
};
fix_html_link(&mut el);
fix_html_link(&mut el, self.options.config.no_html_extension);
self.push(Node::Element(el));
if is_closed {
// No end element.
Expand Down Expand Up @@ -1089,10 +1089,11 @@ fn text_in_node(node: NodeRef<'_, Node>, output: &mut String) {

/// Modifies links to work with HTML.
///
/// For local paths, this changes the `.md` extension to `.html`.
fn fix_link<'a>(link: CowStr<'a>) -> CowStr<'a> {
/// For local paths, this changes the `.md` extension to `.html` when
/// `clean_urls` is `false`, or to a trailing-slash clean URL when `true`.
fn fix_link<'a>(link: CowStr<'a>, clean_urls: bool) -> CowStr<'a> {
static_regex!(SCHEME_LINK, r"^[a-z][a-z0-9+.-]*:");
static_regex!(MD_LINK, r"(?P<link>.*)\.md(?P<anchor>#.*)?");
static_regex!(MD_LINK, r"(?P<link>.*)\.md(?P<anchor>#[^?]*)?");

if link.starts_with('#') {
// Fragment-only link.
Expand All @@ -1105,8 +1106,19 @@ fn fix_link<'a>(link: CowStr<'a>) -> CowStr<'a> {

// This is a relative link, adjust it as necessary.
if let Some(caps) = MD_LINK.captures(&link) {
let mut fixed_link = String::from(&caps["link"]);
fixed_link.push_str(".html");
let link_stem = &caps["link"];
let mut fixed_link = if clean_urls {
// Convert .md links to clean URL format (trailing slash).
if link_stem == "index" {
"./".to_string()
} else if let Some(parent) = link_stem.strip_suffix("/index") {
format!("{parent}/")
} else {
format!("{link_stem}/")
}
} else {
format!("{link_stem}.html")
};
if let Some(anchor) = caps.name("anchor") {
fixed_link.push_str(anchor.as_str());
}
Expand All @@ -1117,13 +1129,13 @@ fn fix_link<'a>(link: CowStr<'a>) -> CowStr<'a> {
}

/// Calls [`fix_link`] for HTML elements.
fn fix_html_link(el: &mut Element) {
fn fix_html_link(el: &mut Element, clean_urls: bool) {
if el.name() != "a" {
return;
}
for attr in ["href", "xlink:href"] {
if let Some(value) = el.attr(attr) {
let fixed = fix_link(value.into());
let fixed = fix_link(value.into(), clean_urls);
el.insert_attr(attr, fixed.into_tendril());
}
}
Expand Down Expand Up @@ -1153,3 +1165,48 @@ pub(crate) fn is_void_element(name: &str) -> bool {
| "wbr"
)
}

#[cfg(test)]
mod tests {
use super::fix_link;
use pulldown_cmark::CowStr;

fn fl(s: &str, clean: bool) -> String {
fix_link(CowStr::Borrowed(s), clean).into_string()
}

#[test]
fn fix_link_html_default() {
assert_eq!(fl("chapter-b.md", false), "chapter-b.html");
}

#[test]
fn fix_link_clean_url_basic() {
assert_eq!(fl("chapter-b.md", true), "chapter-b/");
}

#[test]
fn fix_link_clean_url_index() {
assert_eq!(fl("index.md", true), "./");
}

#[test]
fn fix_link_clean_url_nested_index() {
assert_eq!(fl("foo/index.md", true), "foo/");
}

#[test]
fn fix_link_clean_url_with_anchor() {
assert_eq!(fl("foo/bar.md#section", true), "foo/bar/#section");
}

#[test]
fn fix_link_clean_url_external_unchanged() {
assert_eq!(fl("https://example.com", true), "https://example.com");
}

#[test]
fn fix_link_clean_url_anchor_only_unchanged() {
assert_eq!(fl("#anchor", true), "#anchor");
}
}
Loading