diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index 3a78094d..b6267bee 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -392,7 +392,7 @@ This is a tree of configs, and for each one the value can be one of the three: ` > absolute_links: info > links: > not_found: warn -> anchors: ignore +> anchors: info > absolute_links: info > unrecognized_links: info > ``` @@ -407,6 +407,7 @@ The defaults of some of the behaviors already differ from MkDocs 1.4 and below - > validation: > absolute_links: ignore > unrecognized_links: ignore +> anchors: ignore > ``` >! EXAMPLE: **Recommended settings for most sites (maximal strictness):** diff --git a/mkdocs/config/defaults.py b/mkdocs/config/defaults.py index ccf1604b..6f1da3db 100644 --- a/mkdocs/config/defaults.py +++ b/mkdocs/config/defaults.py @@ -169,7 +169,7 @@ class MkDocsConfig(base.Config): """Warning level for when a Markdown doc links to a relative path that doesn't look like it could be a valid internal link. For example, if the link ends with `/`.""" - anchors = c._LogLevel(default='ignore') + anchors = c._LogLevel(default='info') """Warning level for when a Markdown doc links to an anchor that's not present on the target page.""" links = c.SubConfig(LinksValidation) diff --git a/mkdocs/structure/pages.py b/mkdocs/structure/pages.py index 121af109..317892ac 100644 --- a/mkdocs/structure/pages.py +++ b/mkdocs/structure/pages.py @@ -9,6 +9,7 @@ from urllib.parse import unquote as urlunquote from urllib.parse import urljoin, urlsplit, urlunsplit import markdown +import markdown.htmlparser # type: ignore import markdown.postprocessors import markdown.treeprocessors from markdown.util import AMP_SUBSTITUTE @@ -262,6 +263,9 @@ class Page(StructureItem): extension_configs=config['mdx_configs'] or {}, ) + raw_html_ext = _RawHTMLPreprocessor() + raw_html_ext._register(md) + relative_path_ext = _RelativePathTreeprocessor(self.file, files, config) relative_path_ext._register(md) @@ -271,7 +275,9 @@ class Page(StructureItem): self.content = md.convert(self.markdown) self.toc = get_toc(getattr(md, 'toc_tokens', [])) self._title_from_render = extract_title_ext.title - self.present_anchor_ids = relative_path_ext.present_anchor_ids + self.present_anchor_ids = ( + relative_path_ext.present_anchor_ids | raw_html_ext.present_anchor_ids + ) if log.getEffectiveLevel() > logging.DEBUG: self.links_to_anchors = relative_path_ext.links_to_anchors @@ -282,7 +288,8 @@ class Page(StructureItem): """Links to anchors in other files that this page contains. The structure is: `{file_that_is_linked_to: {'anchor': 'original_link/to/some_file.md#anchor'}}`. - Populated after `.render()`. Populated only if `validation: {anchors: info}` (or greater) is set""" + Populated after `.render()`. Populated only if `validation: {anchors: info}` (or greater) is set. + """ def validate_anchor_links(self, *, files: Files, log_level: int) -> None: if not self.links_to_anchors: @@ -328,6 +335,8 @@ class _RelativePathTreeprocessor(markdown.treeprocessors.Treeprocessor): if anchor := element.get('id'): self.present_anchor_ids.add(anchor) if element.tag == 'a': + if anchor := element.get('name'): + self.present_anchor_ids.add(anchor) key = 'href' elif element.tag == 'img': key = 'src' @@ -483,6 +492,36 @@ class _RelativePathTreeprocessor(markdown.treeprocessors.Treeprocessor): md.treeprocessors.register(self, "relpath", 0) +class _RawHTMLPreprocessor(markdown.preprocessors.Preprocessor): + def __init__(self) -> None: + super().__init__() + self.present_anchor_ids: set[str] = set() + + def run(self, lines: list[str]) -> list[str]: + parser = _HTMLHandler() + parser.feed('\n'.join(lines)) + parser.close() + self.present_anchor_ids = parser.present_anchor_ids + return lines + + def _register(self, md: markdown.Markdown) -> None: + md.preprocessors.register( + self, "mkdocs_raw_html", priority=21 # Right before 'html_block'. + ) + + +class _HTMLHandler(markdown.htmlparser.htmlparser.HTMLParser): # type: ignore[name-defined] + def __init__(self) -> None: + super().__init__() + self.present_anchor_ids: set[str] = set() + + def handle_starttag(self, tag: str, attrs: Sequence[tuple[str, str]]) -> None: + for k, v in attrs: + if k == 'id' or (k == 'name' and tag == 'a'): + self.present_anchor_ids.add(v) + return super().handle_starttag(tag, attrs) + + class _ExtractTitleTreeprocessor(markdown.treeprocessors.Treeprocessor): title: str | None = None postprocessors: Sequence[markdown.postprocessors.Postprocessor] = () @@ -505,8 +544,4 @@ class _ExtractTitleTreeprocessor(markdown.treeprocessors.Treeprocessor): def _register(self, md: markdown.Markdown) -> None: self.postprocessors = tuple(md.postprocessors) - md.treeprocessors.register( - self, - "mkdocs_extract_title", - priority=-1, # After the end. - ) + md.treeprocessors.register(self, "mkdocs_extract_title", priority=-1) # After the end. diff --git a/mkdocs/tests/build_tests.py b/mkdocs/tests/build_tests.py index 00cb6320..9e9d6c0a 100644 --- a/mkdocs/tests/build_tests.py +++ b/mkdocs/tests/build_tests.py @@ -691,7 +691,6 @@ class BuildTests(PathAssertionMixin, unittest.TestCase): cfg = load_config(docs_dir=docs_dir, site_dir=site_dir, validation={'anchors': 'warn'}) build.build(cfg) - @unittest.skip("The implementation is not good enough to understand this yet.") # TODO @tempdir( files={ 'test/foo.md': '[bar](bar.md#heading2)', diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py index 9d8592f9..119cd726 100644 --- a/mkdocs/tests/config/config_options_tests.py +++ b/mkdocs/tests/config/config_options_tests.py @@ -1586,7 +1586,7 @@ class NestedSubConfigTest(TestCase): 'not_found': logging.WARNING, 'absolute_links': logging.INFO, 'unrecognized_links': logging.INFO, - 'anchors': logging.DEBUG, + 'anchors': logging.INFO, }, } diff --git a/pyproject.toml b/pyproject.toml index f1fe5a41..66fc56c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,12 +36,12 @@ dependencies = [ "click >=7.0", "Jinja2 >=2.11.1", "markupsafe >=2.0.1", - "Markdown >=3.2.1", + "Markdown >=3.3.6", "PyYAML >=5.1", "watchdog >=2.0", "ghp-import >=1.0", "pyyaml_env_tag >=0.1", - "importlib-metadata >=4.3; python_version < '3.10'", + "importlib-metadata >=4.4; python_version < '3.10'", "packaging >=20.5", "mergedeep >=1.3.4", "pathspec >=0.11.1", @@ -57,12 +57,12 @@ min-versions = [ "click ==7.0", "Jinja2 ==2.11.1", "markupsafe ==2.0.1", - "Markdown ==3.2.1", + "Markdown ==3.3.6", "PyYAML ==5.1", "watchdog ==2.0", "ghp-import ==1.0", "pyyaml_env_tag ==0.1", - "importlib-metadata ==4.3; python_version < '3.10'", + "importlib-metadata ==4.4; python_version < '3.10'", "packaging ==20.5", "mergedeep ==1.3.4", "pathspec ==0.11.1",