diff --git a/properdocs/structure/pages.py b/properdocs/structure/pages.py index 8e141a0c..66ca78a9 100644 --- a/properdocs/structure/pages.py +++ b/properdocs/structure/pages.py @@ -315,7 +315,10 @@ def validate_anchor_links(self, *, files: Files, log_level: int) -> None: continue context = "" if to_file == self.file: - problem = "there is no such anchor on this page" + if original_link.endswith('#' + anchor): + problem = "there is no such anchor on this page" + else: + problem = f"there is no anchor '#{anchor}' on this page" if anchor.startswith('fnref:'): context = " This seems to be a footnote that is never referenced." else: @@ -416,6 +419,16 @@ def _possible_target_uris( yield guess tried.add(guess) + def register_anchor(self, *, file: File, anchor: str, url: str) -> None: + if not anchor: + return + # Detect https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment/Text_fragments#syntax + if (index := anchor.find(':~:')) > -1: + if index == 0: + return # This is entirely just a directive, no anchor. + anchor = anchor[:index] + self.links_to_anchors.setdefault(file, {}).setdefault(anchor, url) + def path_to_url(self, url: str) -> str: scheme, netloc, path, query, anchor = urlsplit(url) @@ -433,9 +446,8 @@ def path_to_url(self, url: str) -> str: elif AMP_SUBSTITUTE in url: # AMP_SUBSTITUTE is used internally by Markdown only for email. return url elif not path: # Self-link containing only query or anchor. - if anchor: - # Register that the page links to itself with an anchor. - self.links_to_anchors.setdefault(self.file, {}).setdefault(anchor, url) + # Register that the page links to itself with an anchor. + self.register_anchor(file=self.file, anchor=anchor, url=url) return url path = urlunquote(path) @@ -498,9 +510,8 @@ def path_to_url(self, url: str) -> str: assert target_uri is not None assert target_file is not None - if anchor: - # Register that this page links to the target file with an anchor. - self.links_to_anchors.setdefault(target_file, {}).setdefault(anchor, url) + # Register that this page links to the target file with an anchor. + self.register_anchor(file=target_file, anchor=anchor, url=url) if target_file.inclusion.is_excluded(): if self.file.inclusion.is_excluded(): diff --git a/properdocs/tests/build_tests.py b/properdocs/tests/build_tests.py index 9058b951..05902995 100644 --- a/properdocs/tests/build_tests.py +++ b/properdocs/tests/build_tests.py @@ -776,7 +776,7 @@ def test_anchor_no_warning_with_html(self, site_dir, docs_dir): } ) @tempdir() - def test_anchor_warning_and_query(self, site_dir, docs_dir): + def test_anchor_and_query_warning(self, site_dir, docs_dir): cfg = load_config(docs_dir=docs_dir, site_dir=site_dir, validation={'anchors': 'info'}) expected_logs = ''' @@ -806,6 +806,23 @@ def test_anchor_warning_for_footnote(self, site_dir, docs_dir): with self._assert_build_logs(expected_logs): build.build(cfg) + @tempdir( + files={ + 'test/foo.md': '# page1 heading\n\n[bar](bar.md#page1-heading:~:text=a)\n\n[just text](#:~:text=text)', + 'test/bar.md': '# page2 heading\n\n[aaa](#a:~:text=a)\n\n[bbb](#page2-heading:~:text=a)', + } + ) + @tempdir() + def test_anchor_with_directive_warnings(self, site_dir, docs_dir): + cfg = load_config(docs_dir=docs_dir, site_dir=site_dir, validation={'anchors': 'warn'}) + + expected_logs = ''' + WARNING:Doc file 'test/bar.md' contains a link '#a:~:text=a', but there is no anchor '#a' on this page. + WARNING:Doc file 'test/foo.md' contains a link 'bar.md#page1-heading:~:text=a', but the doc 'test/bar.md' does not contain an anchor '#page1-heading'. + ''' + with self._assert_build_logs(expected_logs): + build.build(cfg) + @tempdir( files={ 'foo.md': 'page1 content',