From cd4e08b72dacc4a53a27f7324a062cd4a8ff236f Mon Sep 17 00:00:00 2001 From: Nipunn Koorapati Date: Mon, 23 Aug 2021 21:02:02 -0700 Subject: [PATCH] Leverage readme_path to support relative paths from readme's not at root Added unit tests to confirm things work. Manually tested end to end and confirmed that the rendered page has a properly working relative link as long as the Cargo.toml is in the root of the repo. We'll need to update cargo as mentioned in #3484 in order to support Cargo.toml that aren't in the root of the repo --- src/render.rs | 132 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 89 insertions(+), 43 deletions(-) diff --git a/src/render.rs b/src/render.rs index 94a21c6bd8..f5383605cb 100644 --- a/src/render.rs +++ b/src/render.rs @@ -21,7 +21,7 @@ impl<'a> MarkdownRenderer<'a> { /// /// Per `readme_to_html`, `base_url` is the base URL prepended to any /// relative links in the input document. See that function for more detail. - fn new(base_url: Option<&'a str>) -> MarkdownRenderer<'a> { + fn new(base_url: Option<&'a str>, base_dir: &'a str) -> MarkdownRenderer<'a> { let allowed_classes = hashmap(&[( "code", hashset(&[ @@ -42,7 +42,7 @@ impl<'a> MarkdownRenderer<'a> { "language-yaml", ]), )]); - let sanitize_url = UrlRelative::Custom(Box::new(SanitizeUrl::new(base_url))); + let sanitize_url = UrlRelative::Custom(Box::new(SanitizeUrl::new(base_url, base_dir))); let mut html_sanitizer = Builder::default(); html_sanitizer @@ -131,10 +131,11 @@ fn canon_base_url(mut base_url: String) -> String { /// Sanitize relative URLs in README files. struct SanitizeUrl { base_url: Option, + base_dir: String, } impl SanitizeUrl { - fn new(base_url: Option<&str>) -> Self { + fn new(base_url: Option<&str>, base_dir: &str) -> Self { let base_url = base_url .and_then(|base_url| Url::parse(base_url).ok()) .and_then(|url| match url.host_str() { @@ -143,7 +144,10 @@ impl SanitizeUrl { } _ => None, }); - Self { base_url } + Self { + base_url, + base_dir: base_dir.to_owned(), + } } } @@ -197,6 +201,10 @@ impl UrlRelativeEvaluate for SanitizeUrl { add_sanitize_query, } = is_media_url(url); new_url += if is_media { "raw/HEAD" } else { "blob/HEAD" }; + if !self.base_dir.is_empty() { + new_url += "/"; + new_url += &self.base_dir; + } if !url.starts_with('/') { new_url.push('/'); } @@ -214,22 +222,15 @@ impl UrlRelativeEvaluate for SanitizeUrl { /// Renders Markdown text to sanitized HTML with a given `base_url`. /// See `readme_to_html` for the interpretation of `base_url`. -fn markdown_to_html(text: &str, base_url: Option<&str>) -> String { - let renderer = MarkdownRenderer::new(base_url); +fn markdown_to_html(text: &str, base_url: Option<&str>, base_dir: &str) -> String { + let renderer = MarkdownRenderer::new(base_url, base_dir); renderer.to_html(text) } /// Any readme with a filename ending in one of these extensions will be rendered as Markdown. /// Note we also render a readme as Markdown if _no_ extension is on the filename. -static MARKDOWN_EXTENSIONS: [&str; 7] = [ - ".md", - ".markdown", - ".mdown", - ".mdwn", - ".mkd", - ".mkdn", - ".mkdown", -]; +static MARKDOWN_EXTENSIONS: [&str; 7] = + ["md", "markdown", "mdown", "mdwn", "mkd", "mkdn", "mkdown"]; /// Renders a readme to sanitized HTML. An appropriate rendering method is chosen depending /// on the extension of the supplied `filename`. @@ -250,11 +251,18 @@ static MARKDOWN_EXTENSIONS: [&str; 7] = [ /// let text = "[Rust](https://rust-lang.org/) is an awesome *systems programming* language!"; /// let rendered = readme_to_html(text, "README.md", None)?; /// ``` -pub fn readme_to_html(text: &str, filename: &str, base_url: Option<&str>) -> String { - let filename = filename.to_lowercase(); +pub fn readme_to_html(text: &str, readme_path: &str, base_url: Option<&str>) -> String { + let readme_path = Path::new(readme_path); + let readme_dir = readme_path.parent().and_then(|p| p.to_str()).unwrap_or(""); - if !filename.contains('.') || MARKDOWN_EXTENSIONS.iter().any(|e| filename.ends_with(e)) { - return markdown_to_html(text, base_url); + if readme_path.extension().is_none() { + return markdown_to_html(text, base_url, readme_dir); + } + + if let Some(ext) = readme_path.extension().and_then(|ext| ext.to_str()) { + if MARKDOWN_EXTENSIONS.contains(&ext.to_lowercase().as_str()) { + return markdown_to_html(text, base_url, readme_dir); + } } encode_minimal(text).replace("\n", "
\n") @@ -266,13 +274,13 @@ pub fn render_and_upload_readme( env: &Environment, version_id: i32, text: String, - file_name: String, + readme_path: String, base_url: Option, ) -> Result<(), PerformError> { use crate::schema::*; use diesel::prelude::*; - let rendered = readme_to_html(&text, &file_name, base_url.as_deref()); + let rendered = readme_to_html(&text, &readme_path, base_url.as_deref()); conn.transaction(|| { Version::record_readme_rendering(version_id, conn)?; @@ -311,14 +319,14 @@ mod tests { #[test] fn empty_text() { let text = ""; - let result = markdown_to_html(text, None); + let result = markdown_to_html(text, None, ""); assert_eq!(result, ""); } #[test] fn text_with_script_tag() { let text = "foo_readme\n\n"; - let result = markdown_to_html(text, None); + let result = markdown_to_html(text, None, ""); assert_eq!( result, "

foo_readme

\n<script>alert(\'Hello World\')</script>\n" @@ -328,7 +336,7 @@ mod tests { #[test] fn text_with_iframe_tag() { let text = "foo_readme\n\n"; - let result = markdown_to_html(text, None); + let result = markdown_to_html(text, None, ""); assert_eq!( result, "

foo_readme

\n<iframe>alert(\'Hello World\')</iframe>\n" @@ -338,14 +346,14 @@ mod tests { #[test] fn text_with_unknown_tag() { let text = "foo_readme\n\nalert('Hello World')"; - let result = markdown_to_html(text, None); + let result = markdown_to_html(text, None, ""); assert_eq!(result, "

foo_readme

\n

alert(\'Hello World\')

\n"); } #[test] fn text_with_inline_javascript() { let text = r#"foo_readme\n\nCrate page"#; - let result = markdown_to_html(text, None); + let result = markdown_to_html(text, None, ""); assert_eq!( result, "

foo_readme\\n\\nCrate page

\n" @@ -357,7 +365,7 @@ mod tests { #[test] fn text_with_fancy_single_quotes() { let text = "wb’"; - let result = markdown_to_html(text, None); + let result = markdown_to_html(text, None, ""); assert_eq!(result, "

wb’

\n"); } @@ -366,7 +374,7 @@ mod tests { let code_block = r#"```rust \ println!("Hello World"); \ ```"#; - let result = markdown_to_html(code_block, None); + let result = markdown_to_html(code_block, None, ""); assert!(result.contains("")); } @@ -375,14 +383,14 @@ mod tests { let code_block = r#"```rust , no_run \ println!("Hello World"); \ ```"#; - let result = markdown_to_html(code_block, None); + let result = markdown_to_html(code_block, None, ""); assert!(result.contains("")); } #[test] fn text_with_forbidden_class_attribute() { let text = "

Hello World!

"; - let result = markdown_to_html(text, None); + let result = markdown_to_html(text, None, ""); assert_eq!(result, "

Hello World!

\n"); } @@ -403,7 +411,7 @@ mod tests { if extra_slash { "/" } else { "" }, ); - let result = markdown_to_html(absolute, Some(&url)); + let result = markdown_to_html(absolute, Some(&url), ""); assert_eq!( result, format!( @@ -412,7 +420,7 @@ mod tests { ) ); - let result = markdown_to_html(relative, Some(&url)); + let result = markdown_to_html(relative, Some(&url), ""); assert_eq!( result, format!( @@ -421,7 +429,7 @@ mod tests { ) ); - let result = markdown_to_html(image, Some(&url)); + let result = markdown_to_html(image, Some(&url), ""); assert_eq!( result, format!( @@ -430,7 +438,7 @@ mod tests { ) ); - let result = markdown_to_html(html_image, Some(&url)); + let result = markdown_to_html(html_image, Some(&url), ""); assert_eq!( result, format!( @@ -439,7 +447,7 @@ mod tests { ) ); - let result = markdown_to_html(svg, Some(&url)); + let result = markdown_to_html(svg, Some(&url), ""); assert_eq!( result, format!( @@ -447,10 +455,28 @@ mod tests { host ) ); + + let result = markdown_to_html(svg, Some(&url), "subdir"); + assert_eq!( + result, + format!( + "

\"alt\"

\n", + host + ) + ); + + let result = markdown_to_html(svg, Some(&url), "subdir1/subdir2"); + assert_eq!( + result, + format!( + "

\"alt\"

\n", + host + ) + ); } } - let result = markdown_to_html(absolute, Some("https://google.com/")); + let result = markdown_to_html(absolute, Some("https://google.com/"), ""); assert_eq!( result, "

hi

\n" @@ -462,7 +488,7 @@ mod tests { let readme_text = "[![Crates.io](https://img.shields.io/crates/v/clap.svg)](https://crates.io/crates/clap)"; let repository = "https://github.com/kbknapp/clap-rs/"; - let result = markdown_to_html(readme_text, Some(repository)); + let result = markdown_to_html(readme_text, Some(repository), ""); assert_eq!( result, @@ -472,12 +498,32 @@ mod tests { #[test] fn readme_to_html_renders_markdown() { - for f in &["README", "readme.md", "README.MARKDOWN", "whatever.mkd"] { + for f in &[ + "README", + "readme.md", + "README.MARKDOWN", + "whatever.mkd", + "s/readme.md", + "s1/s2/readme.md", + ] { assert_eq!( readme_to_html("*lobster*", f, None), "

lobster

\n" ); } + + assert_eq!( + readme_to_html("*[lobster](docs/lobster)*", "readme.md", Some("https://github.com/rust-lang/test")), + "

lobster

\n" + ); + assert_eq!( + readme_to_html("*[lobster](docs/lobster)*", "s/readme.md", Some("https://github.com/rust-lang/test")), + "

lobster

\n" + ); + assert_eq!( + readme_to_html("*[lobster](docs/lobster)*", "s1/s2/readme.md", Some("https://github.com/rust-lang/test")), + "

lobster

\n" + ); } #[test] @@ -493,7 +539,7 @@ mod tests { #[test] fn header_has_tags() { let text = "# My crate\n\nHello, world!\n"; - let result = markdown_to_html(text, None); + let result = markdown_to_html(text, None, ""); assert_eq!( result, "

My crate

\n

Hello, world!

\n" @@ -504,7 +550,7 @@ mod tests { fn manual_anchor_is_sanitized() { let text = "

My crate

\n

Hello, world!

\n"; - let result = markdown_to_html(text, None); + let result = markdown_to_html(text, None, ""); assert_eq!( result, "

My crate

\n

Hello, world!

\n" @@ -514,7 +560,7 @@ mod tests { #[test] fn tables_with_rowspan_and_colspan() { let text = "
Target
\n"; - let result = markdown_to_html(text, None); + let result = markdown_to_html(text, None, ""); assert_eq!( result, "
Target
\n" @@ -524,7 +570,7 @@ mod tests { #[test] fn text_alignment() { let text = "

foo-bar

\n
Hello World!
\n"; - let result = markdown_to_html(text, None); + let result = markdown_to_html(text, None, ""); assert_eq!( result, "

foo-bar

\n
Hello World!
\n" @@ -535,7 +581,7 @@ mod tests { fn image_alignment() { let text = "

\"\"

\n"; - let result = markdown_to_html(text, None); + let result = markdown_to_html(text, None, ""); assert_eq!( result, "

\"\"

\n"