Skip to content

Commit cd4e08b

Browse files
committed
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
1 parent d3fde3b commit cd4e08b

File tree

1 file changed

+89
-43
lines changed

1 file changed

+89
-43
lines changed

src/render.rs

Lines changed: 89 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ impl<'a> MarkdownRenderer<'a> {
2121
///
2222
/// Per `readme_to_html`, `base_url` is the base URL prepended to any
2323
/// relative links in the input document. See that function for more detail.
24-
fn new(base_url: Option<&'a str>) -> MarkdownRenderer<'a> {
24+
fn new(base_url: Option<&'a str>, base_dir: &'a str) -> MarkdownRenderer<'a> {
2525
let allowed_classes = hashmap(&[(
2626
"code",
2727
hashset(&[
@@ -42,7 +42,7 @@ impl<'a> MarkdownRenderer<'a> {
4242
"language-yaml",
4343
]),
4444
)]);
45-
let sanitize_url = UrlRelative::Custom(Box::new(SanitizeUrl::new(base_url)));
45+
let sanitize_url = UrlRelative::Custom(Box::new(SanitizeUrl::new(base_url, base_dir)));
4646

4747
let mut html_sanitizer = Builder::default();
4848
html_sanitizer
@@ -131,10 +131,11 @@ fn canon_base_url(mut base_url: String) -> String {
131131
/// Sanitize relative URLs in README files.
132132
struct SanitizeUrl {
133133
base_url: Option<String>,
134+
base_dir: String,
134135
}
135136

136137
impl SanitizeUrl {
137-
fn new(base_url: Option<&str>) -> Self {
138+
fn new(base_url: Option<&str>, base_dir: &str) -> Self {
138139
let base_url = base_url
139140
.and_then(|base_url| Url::parse(base_url).ok())
140141
.and_then(|url| match url.host_str() {
@@ -143,7 +144,10 @@ impl SanitizeUrl {
143144
}
144145
_ => None,
145146
});
146-
Self { base_url }
147+
Self {
148+
base_url,
149+
base_dir: base_dir.to_owned(),
150+
}
147151
}
148152
}
149153

@@ -197,6 +201,10 @@ impl UrlRelativeEvaluate for SanitizeUrl {
197201
add_sanitize_query,
198202
} = is_media_url(url);
199203
new_url += if is_media { "raw/HEAD" } else { "blob/HEAD" };
204+
if !self.base_dir.is_empty() {
205+
new_url += "/";
206+
new_url += &self.base_dir;
207+
}
200208
if !url.starts_with('/') {
201209
new_url.push('/');
202210
}
@@ -214,22 +222,15 @@ impl UrlRelativeEvaluate for SanitizeUrl {
214222

215223
/// Renders Markdown text to sanitized HTML with a given `base_url`.
216224
/// See `readme_to_html` for the interpretation of `base_url`.
217-
fn markdown_to_html(text: &str, base_url: Option<&str>) -> String {
218-
let renderer = MarkdownRenderer::new(base_url);
225+
fn markdown_to_html(text: &str, base_url: Option<&str>, base_dir: &str) -> String {
226+
let renderer = MarkdownRenderer::new(base_url, base_dir);
219227
renderer.to_html(text)
220228
}
221229

222230
/// Any readme with a filename ending in one of these extensions will be rendered as Markdown.
223231
/// Note we also render a readme as Markdown if _no_ extension is on the filename.
224-
static MARKDOWN_EXTENSIONS: [&str; 7] = [
225-
".md",
226-
".markdown",
227-
".mdown",
228-
".mdwn",
229-
".mkd",
230-
".mkdn",
231-
".mkdown",
232-
];
232+
static MARKDOWN_EXTENSIONS: [&str; 7] =
233+
["md", "markdown", "mdown", "mdwn", "mkd", "mkdn", "mkdown"];
233234

234235
/// Renders a readme to sanitized HTML. An appropriate rendering method is chosen depending
235236
/// on the extension of the supplied `filename`.
@@ -250,11 +251,18 @@ static MARKDOWN_EXTENSIONS: [&str; 7] = [
250251
/// let text = "[Rust](https://rust-lang.org/) is an awesome *systems programming* language!";
251252
/// let rendered = readme_to_html(text, "README.md", None)?;
252253
/// ```
253-
pub fn readme_to_html(text: &str, filename: &str, base_url: Option<&str>) -> String {
254-
let filename = filename.to_lowercase();
254+
pub fn readme_to_html(text: &str, readme_path: &str, base_url: Option<&str>) -> String {
255+
let readme_path = Path::new(readme_path);
256+
let readme_dir = readme_path.parent().and_then(|p| p.to_str()).unwrap_or("");
255257

256-
if !filename.contains('.') || MARKDOWN_EXTENSIONS.iter().any(|e| filename.ends_with(e)) {
257-
return markdown_to_html(text, base_url);
258+
if readme_path.extension().is_none() {
259+
return markdown_to_html(text, base_url, readme_dir);
260+
}
261+
262+
if let Some(ext) = readme_path.extension().and_then(|ext| ext.to_str()) {
263+
if MARKDOWN_EXTENSIONS.contains(&ext.to_lowercase().as_str()) {
264+
return markdown_to_html(text, base_url, readme_dir);
265+
}
258266
}
259267

260268
encode_minimal(text).replace("\n", "<br>\n")
@@ -266,13 +274,13 @@ pub fn render_and_upload_readme(
266274
env: &Environment,
267275
version_id: i32,
268276
text: String,
269-
file_name: String,
277+
readme_path: String,
270278
base_url: Option<String>,
271279
) -> Result<(), PerformError> {
272280
use crate::schema::*;
273281
use diesel::prelude::*;
274282

275-
let rendered = readme_to_html(&text, &file_name, base_url.as_deref());
283+
let rendered = readme_to_html(&text, &readme_path, base_url.as_deref());
276284

277285
conn.transaction(|| {
278286
Version::record_readme_rendering(version_id, conn)?;
@@ -311,14 +319,14 @@ mod tests {
311319
#[test]
312320
fn empty_text() {
313321
let text = "";
314-
let result = markdown_to_html(text, None);
322+
let result = markdown_to_html(text, None, "");
315323
assert_eq!(result, "");
316324
}
317325

318326
#[test]
319327
fn text_with_script_tag() {
320328
let text = "foo_readme\n\n<script>alert('Hello World')</script>";
321-
let result = markdown_to_html(text, None);
329+
let result = markdown_to_html(text, None, "");
322330
assert_eq!(
323331
result,
324332
"<p>foo_readme</p>\n&lt;script&gt;alert(\'Hello World\')&lt;/script&gt;\n"
@@ -328,7 +336,7 @@ mod tests {
328336
#[test]
329337
fn text_with_iframe_tag() {
330338
let text = "foo_readme\n\n<iframe>alert('Hello World')</iframe>";
331-
let result = markdown_to_html(text, None);
339+
let result = markdown_to_html(text, None, "");
332340
assert_eq!(
333341
result,
334342
"<p>foo_readme</p>\n&lt;iframe&gt;alert(\'Hello World\')&lt;/iframe&gt;\n"
@@ -338,14 +346,14 @@ mod tests {
338346
#[test]
339347
fn text_with_unknown_tag() {
340348
let text = "foo_readme\n\n<unknown>alert('Hello World')</unknown>";
341-
let result = markdown_to_html(text, None);
349+
let result = markdown_to_html(text, None, "");
342350
assert_eq!(result, "<p>foo_readme</p>\n<p>alert(\'Hello World\')</p>\n");
343351
}
344352

345353
#[test]
346354
fn text_with_inline_javascript() {
347355
let text = r#"foo_readme\n\n<a href="https://crates.io/crates/cargo-registry" onclick="window.alert('Got you')">Crate page</a>"#;
348-
let result = markdown_to_html(text, None);
356+
let result = markdown_to_html(text, None, "");
349357
assert_eq!(
350358
result,
351359
"<p>foo_readme\\n\\n<a href=\"https://crates.io/crates/cargo-registry\" rel=\"nofollow noopener noreferrer\">Crate page</a></p>\n"
@@ -357,7 +365,7 @@ mod tests {
357365
#[test]
358366
fn text_with_fancy_single_quotes() {
359367
let text = "wb’";
360-
let result = markdown_to_html(text, None);
368+
let result = markdown_to_html(text, None, "");
361369
assert_eq!(result, "<p>wb’</p>\n");
362370
}
363371

@@ -366,7 +374,7 @@ mod tests {
366374
let code_block = r#"```rust \
367375
println!("Hello World"); \
368376
```"#;
369-
let result = markdown_to_html(code_block, None);
377+
let result = markdown_to_html(code_block, None, "");
370378
assert!(result.contains("<code class=\"language-rust\">"));
371379
}
372380

@@ -375,14 +383,14 @@ mod tests {
375383
let code_block = r#"```rust , no_run \
376384
println!("Hello World"); \
377385
```"#;
378-
let result = markdown_to_html(code_block, None);
386+
let result = markdown_to_html(code_block, None, "");
379387
assert!(result.contains("<code class=\"language-rust\">"));
380388
}
381389

382390
#[test]
383391
fn text_with_forbidden_class_attribute() {
384392
let text = "<p class='bad-class'>Hello World!</p>";
385-
let result = markdown_to_html(text, None);
393+
let result = markdown_to_html(text, None, "");
386394
assert_eq!(result, "<p>Hello World!</p>\n");
387395
}
388396

@@ -403,7 +411,7 @@ mod tests {
403411
if extra_slash { "/" } else { "" },
404412
);
405413

406-
let result = markdown_to_html(absolute, Some(&url));
414+
let result = markdown_to_html(absolute, Some(&url), "");
407415
assert_eq!(
408416
result,
409417
format!(
@@ -412,7 +420,7 @@ mod tests {
412420
)
413421
);
414422

415-
let result = markdown_to_html(relative, Some(&url));
423+
let result = markdown_to_html(relative, Some(&url), "");
416424
assert_eq!(
417425
result,
418426
format!(
@@ -421,7 +429,7 @@ mod tests {
421429
)
422430
);
423431

424-
let result = markdown_to_html(image, Some(&url));
432+
let result = markdown_to_html(image, Some(&url), "");
425433
assert_eq!(
426434
result,
427435
format!(
@@ -430,7 +438,7 @@ mod tests {
430438
)
431439
);
432440

433-
let result = markdown_to_html(html_image, Some(&url));
441+
let result = markdown_to_html(html_image, Some(&url), "");
434442
assert_eq!(
435443
result,
436444
format!(
@@ -439,18 +447,36 @@ mod tests {
439447
)
440448
);
441449

442-
let result = markdown_to_html(svg, Some(&url));
450+
let result = markdown_to_html(svg, Some(&url), "");
443451
assert_eq!(
444452
result,
445453
format!(
446454
"<p><img src=\"https://{}/rust-lang/test/raw/HEAD/sanitize.svg?sanitize=true\" alt=\"alt\"></p>\n",
447455
host
448456
)
449457
);
458+
459+
let result = markdown_to_html(svg, Some(&url), "subdir");
460+
assert_eq!(
461+
result,
462+
format!(
463+
"<p><img src=\"https://{}/rust-lang/test/raw/HEAD/subdir/sanitize.svg?sanitize=true\" alt=\"alt\"></p>\n",
464+
host
465+
)
466+
);
467+
468+
let result = markdown_to_html(svg, Some(&url), "subdir1/subdir2");
469+
assert_eq!(
470+
result,
471+
format!(
472+
"<p><img src=\"https://{}/rust-lang/test/raw/HEAD/subdir1/subdir2/sanitize.svg?sanitize=true\" alt=\"alt\"></p>\n",
473+
host
474+
)
475+
);
450476
}
451477
}
452478

453-
let result = markdown_to_html(absolute, Some("https://google.com/"));
479+
let result = markdown_to_html(absolute, Some("https://google.com/"), "");
454480
assert_eq!(
455481
result,
456482
"<p><a rel=\"nofollow noopener noreferrer\">hi</a></p>\n"
@@ -462,7 +488,7 @@ mod tests {
462488
let readme_text =
463489
"[![Crates.io](https://img.shields.io/crates/v/clap.svg)](https://crates.io/crates/clap)";
464490
let repository = "https://github.com/kbknapp/clap-rs/";
465-
let result = markdown_to_html(readme_text, Some(repository));
491+
let result = markdown_to_html(readme_text, Some(repository), "");
466492

467493
assert_eq!(
468494
result,
@@ -472,12 +498,32 @@ mod tests {
472498

473499
#[test]
474500
fn readme_to_html_renders_markdown() {
475-
for f in &["README", "readme.md", "README.MARKDOWN", "whatever.mkd"] {
501+
for f in &[
502+
"README",
503+
"readme.md",
504+
"README.MARKDOWN",
505+
"whatever.mkd",
506+
"s/readme.md",
507+
"s1/s2/readme.md",
508+
] {
476509
assert_eq!(
477510
readme_to_html("*lobster*", f, None),
478511
"<p><em>lobster</em></p>\n"
479512
);
480513
}
514+
515+
assert_eq!(
516+
readme_to_html("*[lobster](docs/lobster)*", "readme.md", Some("https://github.com/rust-lang/test")),
517+
"<p><em><a href=\"https://github.com/rust-lang/test/blob/HEAD/docs/lobster\" rel=\"nofollow noopener noreferrer\">lobster</a></em></p>\n"
518+
);
519+
assert_eq!(
520+
readme_to_html("*[lobster](docs/lobster)*", "s/readme.md", Some("https://github.com/rust-lang/test")),
521+
"<p><em><a href=\"https://github.com/rust-lang/test/blob/HEAD/s/docs/lobster\" rel=\"nofollow noopener noreferrer\">lobster</a></em></p>\n"
522+
);
523+
assert_eq!(
524+
readme_to_html("*[lobster](docs/lobster)*", "s1/s2/readme.md", Some("https://github.com/rust-lang/test")),
525+
"<p><em><a href=\"https://github.com/rust-lang/test/blob/HEAD/s1/s2/docs/lobster\" rel=\"nofollow noopener noreferrer\">lobster</a></em></p>\n"
526+
);
481527
}
482528

483529
#[test]
@@ -493,7 +539,7 @@ mod tests {
493539
#[test]
494540
fn header_has_tags() {
495541
let text = "# My crate\n\nHello, world!\n";
496-
let result = markdown_to_html(text, None);
542+
let result = markdown_to_html(text, None, "");
497543
assert_eq!(
498544
result,
499545
"<h1><a href=\"#my-crate\" id=\"user-content-my-crate\" rel=\"nofollow noopener noreferrer\"></a>My crate</h1>\n<p>Hello, world!</p>\n"
@@ -504,7 +550,7 @@ mod tests {
504550
fn manual_anchor_is_sanitized() {
505551
let text =
506552
"<h1><a href=\"#my-crate\" id=\"my-crate\"></a>My crate</h1>\n<p>Hello, world!</p>\n";
507-
let result = markdown_to_html(text, None);
553+
let result = markdown_to_html(text, None, "");
508554
assert_eq!(
509555
result,
510556
"<h1><a href=\"#my-crate\" id=\"user-content-my-crate\" rel=\"nofollow noopener noreferrer\"></a>My crate</h1>\n<p>Hello, world!</p>\n"
@@ -514,7 +560,7 @@ mod tests {
514560
#[test]
515561
fn tables_with_rowspan_and_colspan() {
516562
let text = "<table><tr><th rowspan=\"1\" colspan=\"2\">Target</th></tr></table>\n";
517-
let result = markdown_to_html(text, None);
563+
let result = markdown_to_html(text, None, "");
518564
assert_eq!(
519565
result,
520566
"<table><tbody><tr><th rowspan=\"1\" colspan=\"2\">Target</th></tr></tbody></table>\n"
@@ -524,7 +570,7 @@ mod tests {
524570
#[test]
525571
fn text_alignment() {
526572
let text = "<h1 align=\"center\">foo-bar</h1>\n<h5 align=\"center\">Hello World!</h5>\n";
527-
let result = markdown_to_html(text, None);
573+
let result = markdown_to_html(text, None, "");
528574
assert_eq!(
529575
result,
530576
"<h1 align=\"center\">foo-bar</h1>\n<h5 align=\"center\">Hello World!</h5>\n"
@@ -535,7 +581,7 @@ mod tests {
535581
fn image_alignment() {
536582
let text =
537583
"<p align=\"center\"><img src=\"https://img.shields.io/crates/v/clap.svg\" alt=\"\"></p>\n";
538-
let result = markdown_to_html(text, None);
584+
let result = markdown_to_html(text, None, "");
539585
assert_eq!(
540586
result,
541587
"<p align=\"center\"><img src=\"https://img.shields.io/crates/v/clap.svg\" alt=\"\"></p>\n"

0 commit comments

Comments
 (0)