From c1a0af3cf65ae3371e336ca29ea42c02c15ba9fb Mon Sep 17 00:00:00 2001 From: joaofreires Date: Sat, 7 May 2022 16:22:24 -0300 Subject: [PATCH 1/4] add absolute links support --- src/config.rs | 3 ++ src/renderer/html_handlebars/hbs_renderer.rs | 19 +++++++++-- src/utils/mod.rs | 34 +++++++++++++------- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/config.rs b/src/config.rs index 6b8f141461..26b00b4b65 100644 --- a/src/config.rs +++ b/src/config.rs @@ -516,6 +516,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 @@ -562,6 +564,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 9c126feb89..154564ce6f 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -55,10 +55,23 @@ impl HtmlHandlebars { } let content = ch.content.clone(); - let content = utils::render_markdown(&content, ctx.html_config.curly_quotes); + let content = if ctx.html_config.use_site_url_as_root { + utils::render_markdown_with_path( + &content, + ctx.html_config.curly_quotes, + None, + ctx.html_config.site_url.as_ref(), + ) + } else { + utils::render_markdown(&content, ctx.html_config.curly_quotes) + }; - let fixed_content = - utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path)); + let fixed_content = utils::render_markdown_with_path( + &ch.content, + ctx.html_config.curly_quotes, + Some(path), + None, + ); if !ctx.is_index && ctx.html_config.print.page_break { // Add page break between chapters // See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before diff --git a/src/utils/mod.rs b/src/utils/mod.rs index ab1a1bcdc4..13bc07bfec 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -96,13 +96,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> { lazy_static! { static ref SCHEME_LINK: Regex = Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap(); static ref MD_LINK: Regex = 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 { @@ -139,12 +139,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 @@ -160,7 +167,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,19 +176,19 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> { match event { Event::Start(Tag::Link(link_type, dest, title)) => { - Event::Start(Tag::Link(link_type, fix(dest, path), title)) + Event::Start(Tag::Link(link_type, fix(dest, path, abs_url), title)) } Event::Start(Tag::Image(link_type, dest, title)) => { - Event::Start(Tag::Image(link_type, fix(dest, path), title)) + Event::Start(Tag::Image(link_type, fix(dest, path, abs_url), title)) } - Event::Html(html) => Event::Html(fix_html(html, path)), + Event::Html(html) => Event::Html(fix_html(html, path, abs_url)), _ => event, } } /// Wrapper around the pulldown-cmark parser for rendering markdown to HTML. pub fn render_markdown(text: &str, curly_quotes: bool) -> String { - render_markdown_with_path(text, curly_quotes, None) + render_markdown_with_path(text, curly_quotes, None, None) } pub fn new_cmark_parser(text: &str, curly_quotes: bool) -> Parser<'_, '_> { @@ -196,12 +203,17 @@ pub fn new_cmark_parser(text: &str, curly_quotes: bool) -> Parser<'_, '_> { Parser::new_ext(text, opts) } -pub fn render_markdown_with_path(text: &str, curly_quotes: bool, path: Option<&Path>) -> String { +pub fn render_markdown_with_path( + text: &str, + curly_quotes: bool, + path: Option<&Path>, + abs_url: Option<&String>, +) -> String { let mut s = String::with_capacity(text.len() * 3 / 2); let p = new_cmark_parser(text, curly_quotes); let events = p .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 d9c5f105336012d9fbd44e029d222c0779fb5d34 Mon Sep 17 00:00:00 2001 From: joaofreires Date: Sat, 7 May 2022 16:22:46 -0300 Subject: [PATCH 2/4] 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 b9c3086114..2552a5ae24 100644 --- a/guide/src/format/configuration/renderers.md +++ b/guide/src/format/configuration/renderers.md @@ -159,6 +159,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 e2b0b05ef837fbc0b938fae5f72fb7cf357e55b4 Mon Sep 17 00:00:00 2001 From: joaofreires Date: Fri, 11 Nov 2022 13:48:48 -0300 Subject: [PATCH 3/4] requested changes --- guide/src/format/configuration/renderers.md | 1 + src/renderer/html_handlebars/hbs_renderer.rs | 10 +++----- src/utils/mod.rs | 27 ++++++++++++++------ 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/guide/src/format/configuration/renderers.md b/guide/src/format/configuration/renderers.md index 2552a5ae24..814990da4e 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 f2b01063a4..1572e3a512 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -56,7 +56,7 @@ impl HtmlHandlebars { let content = ch.content.clone(); let content = if ctx.html_config.use_site_url_as_root { - utils::render_markdown_with_path( + utils::render_markdown_with_abs_path( &content, ctx.html_config.curly_quotes, None, @@ -66,12 +66,8 @@ impl HtmlHandlebars { utils::render_markdown(&content, ctx.html_config.curly_quotes) }; - let fixed_content = utils::render_markdown_with_path( - &ch.content, - ctx.html_config.curly_quotes, - Some(path), - None, - ); + let fixed_content = + utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path)); if !ctx.is_index && ctx.html_config.print.page_break { // Add page break between chapters // See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 4a3da66108..6636ed5f97 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -126,20 +126,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)); } @@ -181,7 +186,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, curly_quotes: bool) -> String { - render_markdown_with_path(text, curly_quotes, None, None) + render_markdown_with_path(text, curly_quotes, None) } pub fn new_cmark_parser(text: &str, curly_quotes: bool) -> Parser<'_, '_> { @@ -196,12 +201,18 @@ pub fn new_cmark_parser(text: &str, curly_quotes: bool) -> Parser<'_, '_> { Parser::new_ext(text, opts) } -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) +} + +pub fn render_markdown_with_abs_path( text: &str, curly_quotes: bool, path: Option<&Path>, abs_url: Option<&String>, ) -> String { + // This function should be merged with `render_markdown_with_path` + // in the future. Currently, it is used not to break compatibility. let mut s = String::with_capacity(text.len() * 3 / 2); let p = new_cmark_parser(text, curly_quotes); let events = p From 2336e6aa9032ed6c58a40ef71fa7d4566e65e19f Mon Sep 17 00:00:00 2001 From: joaofreires Date: Fri, 11 Nov 2022 13:48:57 -0300 Subject: [PATCH 4/4] 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 6636ed5f97..d4d83e05fa 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -422,6 +422,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;