diff --git a/fixtures/fragments/file.html b/fixtures/fragments/file.html index 5ff181f737..8e0f62d5d8 100644 --- a/fixtures/fragments/file.html +++ b/fixtures/fragments/file.html @@ -22,6 +22,8 @@ back to Upper-ÄÖö
back to öüä encoded
doesn't exist
+ To the top
+ To the top alt
diff --git a/fixtures/fragments/file1.md b/fixtures/fragments/file1.md index 69af62f4f6..4e315ec804 100644 --- a/fixtures/fragments/file1.md +++ b/fixtures/fragments/file1.md @@ -54,4 +54,12 @@ Therefore we put the test into a code block for now to prevent false positives. [Link to umlauts wrong case](#fünf-sÜße-Äpfel) [Link to umlauts with percent encoding](#f%C3%BCnf-s%C3%BC%C3%9Fe-%C3%A4pfel) +# To top fragments + +The empty "#" and "#top" fragments are always valid +without related HTML element. Browser will scroll to the top of the page. + +[Link to top of file2](file2.md#) +[Alternative link to top of file2](file2.md#top) + ##### Lets wear a hat: être diff --git a/lychee-bin/tests/cli.rs b/lychee-bin/tests/cli.rs index 5479d37ae8..55fc6dfa8e 100644 --- a/lychee-bin/tests/cli.rs +++ b/lychee-bin/tests/cli.rs @@ -1834,8 +1834,10 @@ mod cli { .stderr(contains( "fixtures/fragments/file1.md#kebab-case-fragment-1", )) - .stdout(contains("21 Total")) - .stdout(contains("17 OK")) + .stderr(contains("fixtures/fragments/file.html#top")) + .stderr(contains("fixtures/fragments/file2.md#top")) + .stdout(contains("25 Total")) + .stdout(contains("21 OK")) // 4 failures because of missing fragments .stdout(contains("4 Errors")); } diff --git a/lychee-lib/src/utils/fragment_checker.rs b/lychee-lib/src/utils/fragment_checker.rs index 1cc6c772c8..789b52bb84 100644 --- a/lychee-lib/src/utils/fragment_checker.rs +++ b/lychee-lib/src/utils/fragment_checker.rs @@ -39,14 +39,18 @@ impl FragmentChecker { /// Checks if the given path contains the given fragment. /// - /// Returns false, if there is a fragment in the link and the path is to a - /// Markdown file, which doesn't contain the given fragment. + /// Returns false, if there is a fragment in the link which is not empty or "top" + /// and the path is to a Markdown file, which doesn't contain the given fragment. + /// (Empty # and #top fragments are always valid, triggering the browser to scroll to top.) /// /// In all other cases, returns true. pub(crate) async fn check(&self, path: &Path, url: &Url) -> Result { let Some(fragment) = url.fragment() else { return Ok(true); }; + if fragment.is_empty() || fragment.eq_ignore_ascii_case("top") { + return Ok(true); + }; let mut fragment_decoded = percent_decode_str(fragment).decode_utf8()?; let url_without_frag = Self::remove_fragment(url.clone());