From bbc4758bb99c0f1546b418207669644d9a2e919d Mon Sep 17 00:00:00 2001 From: joaofreires Date: Sat, 7 May 2022 16:22:24 -0300 Subject: [PATCH 1/7] add absolute links support --- src/config.rs | 3 ++ src/renderer/html_handlebars/hbs_renderer.rs | 12 +++++++- src/utils/mod.rs | 30 +++++++++++++------- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/config.rs b/src/config.rs index 7ef8bcef12..e33d6adda0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -582,6 +582,8 @@ pub struct HtmlConfig { pub input_404: Option, /// Absolute url to site, used to emit correct paths for the 404 page, which might be accessed in a deeply nested directory pub site_url: Option, + /// Prepend the `site_url` in links with absolute path. + pub use_site_url_as_root: bool, /// The DNS subdomain or apex domain at which your book will be hosted. This /// string will be written to a file named CNAME in the root of your site, /// as required by GitHub Pages (see [*Managing a custom domain for your @@ -632,6 +634,7 @@ impl Default for HtmlConfig { edit_url_template: None, input_404: None, site_url: None, + use_site_url_as_root: false, cname: None, live_reload_endpoint: None, redirect: HashMap::new(), diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index a144b32b57..e4d23611a0 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -57,12 +57,22 @@ impl HtmlHandlebars { .insert("git_repository_edit_url".to_owned(), json!(edit_url)); } - let content = utils::render_markdown(&ch.content, ctx.html_config.smart_punctuation()); + let content = if ctx.html_config.use_site_url_as_root { + utils::render_markdown_with_path( + &ch.content, + ctx.html_config.smart_punctuation(), + None, + ctx.html_config.site_url.as_ref(), + ) + } else { + utils::render_markdown(&ch.content, ctx.html_config.smart_punctuation()) + }; let fixed_content = utils::render_markdown_with_path( &ch.content, ctx.html_config.smart_punctuation(), Some(path), + None, ); if !ctx.is_index && ctx.html_config.print.page_break { // Add page break between chapters diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 597f0ea400..70dd83b8b8 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -92,13 +92,13 @@ pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap(event: Event<'a>, path: Option<&Path>) -> Event<'a> { +fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>, abs_url: Option<&String>) -> Event<'a> { static SCHEME_LINK: LazyLock = LazyLock::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap()); static MD_LINK: LazyLock = LazyLock::new(|| Regex::new(r"(?P.*)\.md(?P#.*)?").unwrap()); - fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> { + fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>, abs_url: Option<&String>) -> CowStr<'a> { if dest.starts_with('#') { // Fragment-only link. if let Some(path) = path { @@ -135,12 +135,19 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> { } else { fixed_link.push_str(&dest); }; - return CowStr::from(fixed_link); + if fixed_link.starts_with('/') { + fixed_link = match abs_url { + Some(abs_url) => format!("{}{}", abs_url.trim_end_matches('/'), &fixed_link), + None => fixed_link, + } + .into(); + } + return CowStr::from(format!("{}", fixed_link)); } dest } - fn fix_html<'a>(html: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> { + fn fix_html<'a>(html: CowStr<'a>, path: Option<&Path>, abs_url: Option<&String>) -> CowStr<'a> { // This is a terrible hack, but should be reasonably reliable. Nobody // should ever parse a tag with a regex. However, there isn't anything // in Rust that I know of that is suitable for handling partial html @@ -154,7 +161,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> { HTML_LINK .replace_all(&html, |caps: ®ex::Captures<'_>| { - let fixed = fix(caps[2].into(), path); + let fixed = fix(caps[2].into(), path, abs_url); format!("{}{}\"", &caps[1], fixed) }) .into_owned() @@ -169,7 +176,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> { id, }) => Event::Start(Tag::Link { link_type, - dest_url: fix(dest_url, path), + dest_url: fix(dest_url, path, abs_url), title, id, }), @@ -180,19 +187,19 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> { id, }) => Event::Start(Tag::Image { link_type, - dest_url: fix(dest_url, path), + dest_url: fix(dest_url, path, abs_url), title, id, }), - Event::Html(html) => Event::Html(fix_html(html, path)), - Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, path)), + Event::Html(html) => Event::Html(fix_html(html, path, abs_url)), + Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, path, abs_url)), _ => event, } } /// Wrapper around the pulldown-cmark parser for rendering markdown to HTML. pub fn render_markdown(text: &str, smart_punctuation: bool) -> String { - render_markdown_with_path(text, smart_punctuation, None) + render_markdown_with_path(text, smart_punctuation, None, None) } /// Creates a new pulldown-cmark parser of the given text. @@ -218,6 +225,7 @@ pub fn render_markdown_with_path( text: &str, smart_punctuation: bool, path: Option<&Path>, + abs_url: Option<&String>, ) -> String { let mut body = String::with_capacity(text.len() * 3 / 2); @@ -250,7 +258,7 @@ pub fn render_markdown_with_path( let events = new_cmark_parser(text, smart_punctuation) .map(clean_codeblock_headers) - .map(|event| adjust_links(event, path)) + .map(|event| adjust_links(event, path, abs_url)) .flat_map(|event| { let (a, b) = wrap_tables(event); a.into_iter().chain(b) From 82a6ba271f57e8c273e3151902a3816c374828aa Mon Sep 17 00:00:00 2001 From: joaofreires Date: Sat, 7 May 2022 16:22:46 -0300 Subject: [PATCH 2/7] update docs --- guide/src/format/configuration/renderers.md | 1 + 1 file changed, 1 insertion(+) diff --git a/guide/src/format/configuration/renderers.md b/guide/src/format/configuration/renderers.md index a827d2936f..4a19355de4 100644 --- a/guide/src/format/configuration/renderers.md +++ b/guide/src/format/configuration/renderers.md @@ -164,6 +164,7 @@ The following configuration options are available: navigation links and script/css imports in the 404 file work correctly, even when accessing urls in subdirectories. Defaults to `/`. If `site-url` is set, make sure to use document relative links for your assets, meaning they should not start with `/`. +- **use-site-url-as-root:** Prepend the `site_url` in links with absolute path. - **cname:** The DNS subdomain or apex domain at which your book will be hosted. This string will be written to a file named CNAME in the root of your site, as required by GitHub Pages (see [*Managing a custom domain for your GitHub Pages From 30b2ca3b1a14e5563f5b242c8d82ed3b8a2e85bc Mon Sep 17 00:00:00 2001 From: joaofreires Date: Fri, 11 Nov 2022 13:48:48 -0300 Subject: [PATCH 3/7] requested changes --- guide/src/format/configuration/renderers.md | 1 + src/renderer/html_handlebars/hbs_renderer.rs | 2 +- src/utils/mod.rs | 17 +++++++++++------ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/guide/src/format/configuration/renderers.md b/guide/src/format/configuration/renderers.md index 4a19355de4..575374cdac 100644 --- a/guide/src/format/configuration/renderers.md +++ b/guide/src/format/configuration/renderers.md @@ -109,6 +109,7 @@ edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path site-url = "/example-book/" cname = "myproject.rs" input-404 = "not-found.md" +use-site-url-as-root = false ``` The following configuration options are available: diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index e4d23611a0..c412104954 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -58,7 +58,7 @@ impl HtmlHandlebars { } let content = if ctx.html_config.use_site_url_as_root { - utils::render_markdown_with_path( + utils::render_markdown_with_abs_path( &ch.content, ctx.html_config.smart_punctuation(), None, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 70dd83b8b8..e8c2307fbb 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -127,20 +127,25 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>, abs_url: Option<&Stri } if let Some(caps) = MD_LINK.captures(&dest) { - fixed_link.push_str(&caps["link"]); + fixed_link.push_str(&caps["link"].trim_start_matches('/')); fixed_link.push_str(".html"); if let Some(anchor) = caps.name("anchor") { fixed_link.push_str(anchor.as_str()); } + } else if !fixed_link.is_empty() { + // prevent links with double slashes + fixed_link.push_str(&dest.trim_start_matches('/')); } else { fixed_link.push_str(&dest); }; - if fixed_link.starts_with('/') { - fixed_link = match abs_url { - Some(abs_url) => format!("{}{}", abs_url.trim_end_matches('/'), &fixed_link), - None => fixed_link, + if dest.starts_with('/') || path.is_some() { + if let Some(abs_url) = abs_url { + fixed_link = format!( + "{}/{}", + abs_url.trim_end_matches('/'), + &fixed_link.trim_start_matches('/') + ); } - .into(); } return CowStr::from(format!("{}", fixed_link)); } From ec2c304b13613b9fe2279a673e1d57cbe804e0e8 Mon Sep 17 00:00:00 2001 From: joaofreires Date: Fri, 11 Nov 2022 13:48:57 -0300 Subject: [PATCH 4/7] add new tests --- src/utils/mod.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/src/utils/mod.rs b/src/utils/mod.rs index e8c2307fbb..0597f960b8 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -613,6 +613,88 @@ more text with spaces } } + mod render_markdown_with_abs_path { + use super::super::render_markdown_with_abs_path; + use std::path::Path; + + #[test] + fn preserves_external_links() { + assert_eq!( + render_markdown_with_abs_path( + "[example](https://www.rust-lang.org/)", + false, + None, + Some(&"ABS_PATH".to_string()) + ), + "

example

\n" + ); + } + + #[test] + fn replace_root_links() { + assert_eq!( + render_markdown_with_abs_path( + "[example](/testing)", + false, + None, + Some(&"ABS_PATH".to_string()) + ), + "

example

\n" + ); + } + + #[test] + fn replace_root_links_using_path() { + assert_eq!( + render_markdown_with_abs_path( + "[example](bar.md)", + false, + Some(Path::new("foo/chapter.md")), + Some(&"ABS_PATH".to_string()) + ), + "

example

\n" + ); + assert_eq!( + render_markdown_with_abs_path( + "[example](/bar.md)", + false, + Some(Path::new("foo/chapter.md")), + Some(&"ABS_PATH".to_string()) + ), + "

example

\n" + ); + assert_eq!( + render_markdown_with_abs_path( + "[example](/bar.html)", + false, + Some(Path::new("foo/chapter.md")), + None + ), + "

example

\n" + ); + } + + #[test] + fn preserves_relative_links() { + assert_eq!( + render_markdown_with_abs_path( + "[example](../testing)", + false, + None, + Some(&"ABS_PATH".to_string()) + ), + "

example

\n" + ); + } + + #[test] + fn preserves_root_links() { + assert_eq!( + render_markdown_with_abs_path("[example](/testing)", false, None, None), + "

example

\n" + ); + } + } #[allow(deprecated)] mod id_from_content { use super::super::id_from_content; From 7c42e4a4fd51b63581e5e5c445c4c783d422e021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Freires?= Date: Tue, 1 Jul 2025 19:52:01 -0300 Subject: [PATCH 5/7] Updates link handling to use `&str` for consistency Refactors multiple functions to replace `Option<&String>` with `Option<&str>` for improved readability and adherence to Rust best practices. Simplifies `render_markdown_with_path` to remove unused `abs_url` parameter and introduces `render_markdown_with_abs_path` for enhanced link rendering flexibility. Fixes test cases to align with the updated parameter types. --- src/renderer/html_handlebars/hbs_renderer.rs | 3 +- src/utils/mod.rs | 40 ++++++++++++++------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index c412104954..fa4f066597 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -62,7 +62,7 @@ impl HtmlHandlebars { &ch.content, ctx.html_config.smart_punctuation(), None, - ctx.html_config.site_url.as_ref(), + ctx.html_config.site_url.as_deref(), ) } else { utils::render_markdown(&ch.content, ctx.html_config.smart_punctuation()) @@ -72,7 +72,6 @@ impl HtmlHandlebars { &ch.content, ctx.html_config.smart_punctuation(), Some(path), - None, ); if !ctx.is_index && ctx.html_config.print.page_break { // Add page break between chapters diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 0597f960b8..9bde9ce7a5 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -92,13 +92,13 @@ pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap(event: Event<'a>, path: Option<&Path>, abs_url: Option<&String>) -> Event<'a> { +fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>, abs_url: Option<&str>) -> Event<'a> { static SCHEME_LINK: LazyLock = LazyLock::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap()); static MD_LINK: LazyLock = LazyLock::new(|| Regex::new(r"(?P.*)\.md(?P#.*)?").unwrap()); - fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>, abs_url: Option<&String>) -> CowStr<'a> { + fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>, abs_url: Option<&str>) -> CowStr<'a> { if dest.starts_with('#') { // Fragment-only link. if let Some(path) = path { @@ -152,7 +152,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>, abs_url: Option<&Stri dest } - fn fix_html<'a>(html: CowStr<'a>, path: Option<&Path>, abs_url: Option<&String>) -> CowStr<'a> { + fn fix_html<'a>(html: CowStr<'a>, path: Option<&Path>, abs_url: Option<&str>) -> CowStr<'a> { // This is a terrible hack, but should be reasonably reliable. Nobody // should ever parse a tag with a regex. However, there isn't anything // in Rust that I know of that is suitable for handling partial html @@ -204,7 +204,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>, abs_url: Option<&Stri /// Wrapper around the pulldown-cmark parser for rendering markdown to HTML. pub fn render_markdown(text: &str, smart_punctuation: bool) -> String { - render_markdown_with_path(text, smart_punctuation, None, None) + render_markdown_with_path(text, smart_punctuation, None) } /// Creates a new pulldown-cmark parser of the given text. @@ -226,11 +226,29 @@ pub fn new_cmark_parser(text: &str, smart_punctuation: bool) -> Parser<'_> { /// `path` should only be set if this is being generated for the consolidated /// print page. It should point to the page being rendered relative to the /// root of the book. -pub fn render_markdown_with_path( +pub fn render_markdown_with_path(text: &str, curly_quotes: bool, path: Option<&Path>) -> String { + render_markdown_with_abs_path(text, curly_quotes, path, None) +} + + +/// Renders markdown to HTML. +/// +/// `path` should only be set if this is being generated for the consolidated +/// print page. It should point to the page being rendered relative to the +/// root of the book. +/// `abs_url` is the absolute URL to use for links that start with `/`. +/// If `abs_url` is `None`, then links that start with `/` will be +/// rendered relative to the current path. +/// If `abs_url` is `Some`, then links that start with `/` will be +/// rendered as absolute links using the provided URL. +////// This is useful for generating links in the print page, where the +/// links should point to the original location of the page, not the +/// print page itself. +pub fn render_markdown_with_abs_path( text: &str, smart_punctuation: bool, path: Option<&Path>, - abs_url: Option<&String>, + abs_url: Option<&str>, ) -> String { let mut body = String::with_capacity(text.len() * 3 / 2); @@ -624,7 +642,7 @@ more text with spaces "[example](https://www.rust-lang.org/)", false, None, - Some(&"ABS_PATH".to_string()) + Some("ABS_PATH") ), "

example

\n" ); @@ -637,7 +655,7 @@ more text with spaces "[example](/testing)", false, None, - Some(&"ABS_PATH".to_string()) + Some("ABS_PATH") ), "

example

\n" ); @@ -650,7 +668,7 @@ more text with spaces "[example](bar.md)", false, Some(Path::new("foo/chapter.md")), - Some(&"ABS_PATH".to_string()) + Some("ABS_PATH") ), "

example

\n" ); @@ -659,7 +677,7 @@ more text with spaces "[example](/bar.md)", false, Some(Path::new("foo/chapter.md")), - Some(&"ABS_PATH".to_string()) + Some("ABS_PATH") ), "

example

\n" ); @@ -681,7 +699,7 @@ more text with spaces "[example](../testing)", false, None, - Some(&"ABS_PATH".to_string()) + Some("ABS_PATH") ), "

example

\n" ); From fe21b1301c447ce8edfb651e9b877dff6adf80e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Freires?= Date: Tue, 1 Jul 2025 19:54:32 -0300 Subject: [PATCH 6/7] refactor: clean up with cargo fmt --- src/utils/mod.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 9bde9ce7a5..75ad1cd486 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -230,7 +230,6 @@ pub fn render_markdown_with_path(text: &str, curly_quotes: bool, path: Option<&P render_markdown_with_abs_path(text, curly_quotes, path, None) } - /// Renders markdown to HTML. /// /// `path` should only be set if this is being generated for the consolidated @@ -651,12 +650,7 @@ more text with spaces #[test] fn replace_root_links() { assert_eq!( - render_markdown_with_abs_path( - "[example](/testing)", - false, - None, - Some("ABS_PATH") - ), + render_markdown_with_abs_path("[example](/testing)", false, None, Some("ABS_PATH")), "

example

\n" ); } From ef33e4bb57b0a46ad55e013ee1734a5ca6b765dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Freires?= Date: Tue, 1 Jul 2025 19:58:39 -0300 Subject: [PATCH 7/7] Simplifies string conversion in link adjustment --- src/utils/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 75ad1cd486..65dfdef6dc 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -147,7 +147,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>, abs_url: Option<&str> ); } } - return CowStr::from(format!("{}", fixed_link)); + return CowStr::from(fixed_link.to_string()); } dest }