Markdown-Checker 1.2.2__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/PKG-INFO +4 -3
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/README.md +1 -1
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/explanation/link-detection.rst +10 -7
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/howto/pyproject-config.rst +1 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/howto/skip-links.rst +4 -3
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/reference/checks.rst +14 -9
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/reference/cli.rst +24 -4
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/reference/configuration.rst +4 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/reference/report-formats.rst +1 -1
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/tutorial.rst +1 -1
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/pyproject.toml +1 -1
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/__init__.py +1 -1
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/checks/broken_paths.py +2 -1
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/checks/broken_urls.py +2 -5
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/checks/locale.py +1 -4
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/checks/tracking.py +12 -12
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/cli.py +12 -0
- markdown_checker-1.3.0/src/markdown_checker/models/base.py +84 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/models/config.py +16 -5
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/models/path.py +27 -53
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/models/url.py +14 -0
- markdown_checker-1.3.0/src/markdown_checker/patterns.py +57 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/reports/model.py +8 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/reports/renderers/markdown.py +9 -3
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/checks/test_base.py +1 -1
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/checks/test_broken_paths.py +2 -1
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/checks/test_tracking.py +20 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/models/test_base.py +4 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/models/test_path.py +52 -16
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/models/test_url.py +17 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/reports/conftest.py +1 -1
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/reports/renderers/test_json.py +1 -1
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/reports/renderers/test_markdown.py +31 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/reports/test_model.py +22 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/test_cli.py +12 -1
- markdown_checker-1.3.0/uv.lock +1835 -0
- markdown_checker-1.2.2/src/markdown_checker/models/base.py +0 -59
- markdown_checker-1.2.2/src/markdown_checker/patterns.py +0 -36
- markdown_checker-1.2.2/uv.lock +0 -1561
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/LICENSE +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/CODE_OF_CONDUCT.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/CONTRIBUTING.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/api/checker.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/api/checks.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/api/cli.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/api/index.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/api/main.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/api/models.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/api/patterns.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/api/reports.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/api/utils.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/changes.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/conf.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/explanation/index.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/explanation/url-checking.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/howto/github-actions.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/howto/index.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/howto/rate-limits.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/howto/report-formats.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/howto/select-files.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/howto/shell-completions.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/index.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/license.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/reference/exit-codes.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/docs/reference/index.rst +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/checker.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/checks/__init__.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/checks/base.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/models/__init__.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/py.typed +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/reports/__init__.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/reports/base.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/reports/registry.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/reports/renderers/__init__.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/reports/renderers/annotations.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/reports/renderers/console.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/reports/renderers/json.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/reports/templates/paths/broken.md +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/reports/templates/paths/tracking.md +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/reports/templates/urls/broken.md +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/reports/templates/urls/locale.md +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/reports/templates/urls/tracking.md +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/reports/writer.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/utils/__init__.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/utils/extract_links.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/utils/github_env.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/utils/list_files.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/utils/pyproject_config.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/utils/spinner.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/utils/url_pipeline.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/checks/__init__.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/checks/test_broken_urls.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/checks/test_checks.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/checks/test_locale.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/conftest.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/models/__init__.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/models/test_config.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/models/test_models.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/reports/__init__.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/reports/renderers/__init__.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/reports/renderers/test_annotations.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/reports/renderers/test_console.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/reports/renderers/test_renderers.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/reports/test_base.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/reports/test_registry.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/reports/test_reports.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/reports/test_writer.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/resources/sample1.md +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/resources/sample2.md +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/resources/sample3.md +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/resources/sample4.md +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/smoke/test_cli_smoke.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/test_checker.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/test_markdown_checker.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/test_patterns.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/utils/__init__.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/utils/test_extract_links.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/utils/test_github_env.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/utils/test_list_files.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/utils/test_pyproject_config.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/utils/test_spinner.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/utils/test_url_pipeline.py +0 -0
- {markdown_checker-1.2.2 → markdown_checker-1.3.0}/tests/utils/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: markdown-checker
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: A markdown link validation reporting tool.
|
|
5
5
|
Author-email: John Aziz <johnaziz269@gmail.com>
|
|
6
6
|
Maintainer-email: John Aziz <johnaziz269@gmail.com>
|
|
@@ -26,6 +26,7 @@ Project-URL: Documentation, https://markdown-checker.readthedocs.io/en/latest/
|
|
|
26
26
|
Project-URL: Homepage, https://github.com/john0isaac/markdown-checker
|
|
27
27
|
Project-URL: Issues, https://github.com/john0isaac/markdown-checker/issues
|
|
28
28
|
Project-URL: Releases, https://github.com/john0isaac/markdown-checker/releases
|
|
29
|
+
Import-Name: markdown_checker
|
|
29
30
|
|
|
30
31
|
[](https://pypi.org/project/markdown-checker/)
|
|
31
32
|
[](https://markdown-checker.readthedocs.io/en/latest/?badge=latest)
|
|
@@ -77,7 +78,7 @@ exits with a non-zero status:
|
|
|
77
78
|
🔍 Checked 42 links in 10 files.
|
|
78
79
|
😭 Found 1 issues in the following files:
|
|
79
80
|
File 'docs/index.md', line 5
|
|
80
|
-
./missing.md
|
|
81
|
+
./missing.md could not be found at /docs/missing.md.
|
|
81
82
|
```
|
|
82
83
|
|
|
83
84
|
## Using `markdown-checker` in GitHub Actions
|
|
@@ -48,7 +48,7 @@ exits with a non-zero status:
|
|
|
48
48
|
🔍 Checked 42 links in 10 files.
|
|
49
49
|
😭 Found 1 issues in the following files:
|
|
50
50
|
File 'docs/index.md', line 5
|
|
51
|
-
./missing.md
|
|
51
|
+
./missing.md could not be found at /docs/missing.md.
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
## Using `markdown-checker` in GitHub Actions
|
|
@@ -38,16 +38,19 @@ Fragments on paths
|
|
|
38
38
|
----------------------
|
|
39
39
|
|
|
40
40
|
A path may carry a trailing ``?query`` and/or ``#anchor`` - for example
|
|
41
|
-
``docs/usage.md#configuration``.
|
|
42
|
-
|
|
43
|
-
part needs to exist on disk; the anchor itself is never
|
|
44
|
-
the target file's headings.
|
|
41
|
+
``docs/usage.md#configuration``. As in any URL, everything from the first
|
|
42
|
+
``?`` or ``#`` onward is stripped before resolving the file, so only the
|
|
43
|
+
``docs/usage.md`` part needs to exist on disk; the anchor itself is never
|
|
44
|
+
validated against the target file's headings. The remaining path is then
|
|
45
|
+
percent-decoded, so ``my%20file.md`` refers to ``my file.md``. A file name
|
|
46
|
+
that really contains ``#`` or ``?`` must be written as ``%23``/``%3F``.
|
|
45
47
|
|
|
46
48
|
Resolving a path to a file
|
|
47
49
|
--------------------------------
|
|
48
50
|
|
|
49
51
|
``check_broken_paths`` tries to resolve a path relative to the markdown
|
|
50
52
|
file it was found in first (e.g. ``../img.png`` in ``docs/usage.md``
|
|
51
|
-
resolves against ``docs/``), then
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
resolves against ``docs/``), then relative to the current working
|
|
54
|
+
directory. Root-relative paths (``/docs/usage.md``) are tried the same way
|
|
55
|
+
with the leading ``/`` dropped. A path is reported as broken only if
|
|
56
|
+
neither resolution finds an existing file.
|
|
@@ -18,6 +18,7 @@ Add a ``[tool.markdown-checker]`` table to your project's
|
|
|
18
18
|
extensions = [".md", ".ipynb"]
|
|
19
19
|
skip-files = ["CODE_OF_CONDUCT.md", "SECURITY.md"]
|
|
20
20
|
tracking-domains = ["github.com", "microsoft.com", "visualstudio.com", "aka.ms", "azure.com"]
|
|
21
|
+
tracking-param = ["wt.mc_id"]
|
|
21
22
|
guide-url = "https://github.com/john0isaac/markdown-checker/blob/main/CONTRIBUTING.md"
|
|
22
23
|
max-workers = 10
|
|
23
24
|
report-format = "markdown"
|
|
@@ -17,8 +17,9 @@ check automatically (e.g. it blocks bots) but you still want to keep the link:
|
|
|
17
17
|
|
|
18
18
|
markdown-checker . -f check_broken_urls --skip-domains=example.com,test.com
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
``--skip-domains=example.com`` also matches ``www.example.com
|
|
20
|
+
A domain matches the URL's hostname exactly or as a parent domain, so
|
|
21
|
+
``--skip-domains=example.com`` also matches ``www.example.com`` (but not
|
|
22
|
+
``notexample.com``).
|
|
22
23
|
|
|
23
24
|
Skip URLs containing a substring
|
|
24
25
|
------------------------------------
|
|
@@ -35,7 +36,7 @@ List syntax
|
|
|
35
36
|
-----------
|
|
36
37
|
|
|
37
38
|
Every list option (``--skip-domains``, ``--skip-urls-containing``,
|
|
38
|
-
``--extensions``, ``--skip-files``, ``--tracking-domains``) takes a single
|
|
39
|
+
``--extensions``, ``--skip-files``, ``--tracking-domains``, ``--tracking-param``) takes a single
|
|
39
40
|
comma-separated value:
|
|
40
41
|
|
|
41
42
|
- Correct: ``--skip-domains=example.com,test.com``
|
|
@@ -26,7 +26,7 @@ Sample finding
|
|
|
26
26
|
.. code-block:: text
|
|
27
27
|
|
|
28
28
|
File 'docs/index.md', line 5
|
|
29
|
-
./missing.md
|
|
29
|
+
./missing.md could not be found at /docs/missing.md.
|
|
30
30
|
|
|
31
31
|
``check_broken_urls``
|
|
32
32
|
------------------------
|
|
@@ -55,8 +55,10 @@ Sample finding
|
|
|
55
55
|
|
|
56
56
|
Flags
|
|
57
57
|
URLs whose path contains a locale segment, e.g. ``/en-us/`` in
|
|
58
|
-
``https://example.com/en-us/docs``. A "locale
|
|
59
|
-
language-country pair
|
|
58
|
+
``https://example.com/en-us/docs`` or a trailing ``/en-us``. A "locale
|
|
59
|
+
segment" is a known language-country pair of the form ``xx-xx``
|
|
60
|
+
(case-insensitive), so look-alikes such as ``/ci-cd/`` aren't flagged.
|
|
61
|
+
The host, query string and anchor are not searched.
|
|
60
62
|
Ignores
|
|
61
63
|
``www.nvidia.com`` (built in - its docs always include a locale
|
|
62
64
|
segment), plus any host in ``--skip-domains`` or URL in
|
|
@@ -73,8 +75,8 @@ Sample finding
|
|
|
73
75
|
--------------------------
|
|
74
76
|
|
|
75
77
|
Flags
|
|
76
|
-
URLs on a configured tracking domain that are missing
|
|
77
|
-
query parameter.
|
|
78
|
+
URLs on a configured tracking domain that are missing every
|
|
79
|
+
``--tracking-param`` query parameter (``wt.mc_id`` by default).
|
|
78
80
|
Ignores
|
|
79
81
|
URLs on hosts *not* listed in ``--tracking-domains`` (the check does
|
|
80
82
|
nothing for them), plus any host in ``--skip-domains`` or URL in
|
|
@@ -83,7 +85,8 @@ Default tracking domains
|
|
|
83
85
|
``github.com``, ``microsoft.com``, ``visualstudio.com``, ``aka.ms``,
|
|
84
86
|
``azure.com``.
|
|
85
87
|
Options that affect it
|
|
86
|
-
``--tracking-domains``, ``--
|
|
88
|
+
``--tracking-domains``, ``--tracking-param``, ``--skip-domains``,
|
|
89
|
+
``--skip-urls-containing``.
|
|
87
90
|
Sample finding
|
|
88
91
|
.. code-block:: text
|
|
89
92
|
|
|
@@ -94,10 +97,12 @@ Sample finding
|
|
|
94
97
|
---------------------------
|
|
95
98
|
|
|
96
99
|
Flags
|
|
97
|
-
Every relative path link missing
|
|
98
|
-
``
|
|
100
|
+
Every relative path link missing every ``--tracking-param`` query
|
|
101
|
+
parameter (``wt.mc_id`` by default). Unlike ``check_urls_tracking``,
|
|
102
|
+
there is no domain filter - all paths are checked.
|
|
99
103
|
Options that affect it
|
|
100
|
-
``SRC``/``--dir``, ``--extensions``, ``--skip-files
|
|
104
|
+
``SRC``/``--dir``, ``--extensions``, ``--skip-files``,
|
|
105
|
+
``--tracking-param``.
|
|
101
106
|
Sample finding
|
|
102
107
|
.. code-block:: text
|
|
103
108
|
|
|
@@ -78,14 +78,33 @@ Type
|
|
|
78
78
|
Default
|
|
79
79
|
``github.com,microsoft.com,visualstudio.com,aka.ms,azure.com``.
|
|
80
80
|
Constraints
|
|
81
|
-
Only affects ``check_urls_tracking``.
|
|
81
|
+
Only affects ``check_urls_tracking``. Subdomains are included, e.g.
|
|
82
|
+
``microsoft.com`` also matches ``learn.microsoft.com``.
|
|
82
83
|
Description
|
|
83
|
-
Hostnames that must carry a
|
|
84
|
+
Hostnames that must carry a tracking parameter (see ``--tracking-param``).
|
|
84
85
|
|
|
85
86
|
.. code-block:: bash
|
|
86
87
|
|
|
87
88
|
markdown-checker . -f check_urls_tracking --tracking-domains=github.com,microsoft.com
|
|
88
89
|
|
|
90
|
+
``-tp``, ``--tracking-param``
|
|
91
|
+
---------------------------------
|
|
92
|
+
|
|
93
|
+
Type
|
|
94
|
+
``list[str]``, comma-separated.
|
|
95
|
+
Default
|
|
96
|
+
``wt.mc_id``.
|
|
97
|
+
Constraints
|
|
98
|
+
Only affects ``check_urls_tracking`` and ``check_paths_tracking``.
|
|
99
|
+
Matched case-insensitively as a query parameter (after ``?`` or ``&``).
|
|
100
|
+
Description
|
|
101
|
+
Tracking query parameters a link must carry. A link passes if it has any
|
|
102
|
+
one of them.
|
|
103
|
+
|
|
104
|
+
.. code-block:: bash
|
|
105
|
+
|
|
106
|
+
markdown-checker . -f check_urls_tracking --tracking-param=wt.mc_id,utm_source
|
|
107
|
+
|
|
89
108
|
``-sf``, ``--skip-files``
|
|
90
109
|
-----------------------------
|
|
91
110
|
|
|
@@ -110,8 +129,9 @@ Type
|
|
|
110
129
|
Default
|
|
111
130
|
``[]`` (none).
|
|
112
131
|
Constraints
|
|
113
|
-
Only affects ``check_broken_urls`` and ``check_urls_locale``.
|
|
114
|
-
|
|
132
|
+
Only affects ``check_broken_urls`` and ``check_urls_locale``. Matches the
|
|
133
|
+
URL's hostname exactly or as a parent domain (``example.com`` matches
|
|
134
|
+
``www.example.com`` but not ``notexample.com``).
|
|
115
135
|
Description
|
|
116
136
|
Domains to exclude from URL checks.
|
|
117
137
|
|
|
@@ -47,6 +47,9 @@ Keys
|
|
|
47
47
|
* - ``tracking-domains``
|
|
48
48
|
- ``--tracking-domains``
|
|
49
49
|
- array of strings
|
|
50
|
+
* - ``tracking-param``
|
|
51
|
+
- ``--tracking-param``
|
|
52
|
+
- array of strings
|
|
50
53
|
* - ``skip-files``
|
|
51
54
|
- ``--skip-files``
|
|
52
55
|
- array of strings
|
|
@@ -96,6 +99,7 @@ Example
|
|
|
96
99
|
skip-files = ["CODE_OF_CONDUCT.md", "SECURITY.md"]
|
|
97
100
|
skip-domains = []
|
|
98
101
|
tracking-domains = ["github.com", "microsoft.com", "visualstudio.com", "aka.ms", "azure.com"]
|
|
102
|
+
tracking-param = ["wt.mc_id"]
|
|
99
103
|
guide-url = "https://github.com/john0isaac/markdown-checker/blob/main/CONTRIBUTING.md"
|
|
100
104
|
timeout = 20
|
|
101
105
|
retries = 3
|
|
@@ -46,7 +46,7 @@ A structured document including both errors and warnings:
|
|
|
46
46
|
.. code-block:: json
|
|
47
47
|
|
|
48
48
|
{
|
|
49
|
-
"tool": {"name": "markdown-checker", "version": "1.
|
|
49
|
+
"tool": {"name": "markdown-checker", "version": "1.3.0"},
|
|
50
50
|
"check": "check_broken_urls",
|
|
51
51
|
"summary": {
|
|
52
52
|
"files_checked": 10,
|
|
@@ -63,7 +63,7 @@ You'll see output similar to:
|
|
|
63
63
|
🔍 Checked 2 links in 2 files.
|
|
64
64
|
😭 Found 1 issues in the following files:
|
|
65
65
|
File 'docs-sample/index.md', line 5
|
|
66
|
-
./missing.md
|
|
66
|
+
./missing.md could not be found at /docs/missing.md.
|
|
67
67
|
|
|
68
68
|
Because an error-level issue was found, markdown-checker also writes a
|
|
69
69
|
``comment.md`` file in the current directory with the same information as a
|
{markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/checks/broken_paths.py
RENAMED
|
@@ -28,6 +28,7 @@ class BrokenPathsCheck(BaseCheck[MarkdownPath]):
|
|
|
28
28
|
detected_issues: list[MarkdownPath] = []
|
|
29
29
|
for path in links.paths:
|
|
30
30
|
if not path.exists():
|
|
31
|
-
path.
|
|
31
|
+
path.resolved_path = path.get_full_path()
|
|
32
|
+
path.issue = f"could not be found at {path.resolved_path.as_posix()}"
|
|
32
33
|
detected_issues.append(path)
|
|
33
34
|
return detected_issues
|
{markdown_checker-1.2.2 → markdown_checker-1.3.0}/src/markdown_checker/checks/broken_urls.py
RENAMED
|
@@ -58,7 +58,7 @@ class BrokenURLsCheck(BaseCheck[MarkdownURL]):
|
|
|
58
58
|
:meth:`collect` to resolve later.
|
|
59
59
|
"""
|
|
60
60
|
config = config or Config()
|
|
61
|
-
effective_skip = [
|
|
61
|
+
effective_skip = [*config.skip_domains, *_BUILTIN_SKIP_DOMAINS]
|
|
62
62
|
skip_urls_containing = config.skip_urls_containing
|
|
63
63
|
|
|
64
64
|
# Standalone fallback (direct calls, tests): own a private service for this run only.
|
|
@@ -67,10 +67,7 @@ class BrokenURLsCheck(BaseCheck[MarkdownURL]):
|
|
|
67
67
|
|
|
68
68
|
pending: list[tuple[MarkdownURL, Future[URLCheckResult]]] = []
|
|
69
69
|
for url in links.urls:
|
|
70
|
-
|
|
71
|
-
if any(domain in hostname for domain in effective_skip) or any(
|
|
72
|
-
substring in url.link for substring in skip_urls_containing
|
|
73
|
-
):
|
|
70
|
+
if url.matches_domain(effective_skip) or any(substring in url.link for substring in skip_urls_containing):
|
|
74
71
|
continue
|
|
75
72
|
pending.append((url, _service.submit(url)))
|
|
76
73
|
return (pending, _service if owns_service else None)
|
|
@@ -40,10 +40,7 @@ class URLsLocaleCheck(BaseCheck[MarkdownURL]):
|
|
|
40
40
|
|
|
41
41
|
detected_issues: list[MarkdownURL] = []
|
|
42
42
|
for url in links.urls:
|
|
43
|
-
|
|
44
|
-
if any(domain.lower() in hostname for domain in effective_skip) or any(
|
|
45
|
-
substring in url.link for substring in skip_urls_containing
|
|
46
|
-
):
|
|
43
|
+
if url.matches_domain(effective_skip) or any(substring in url.link for substring in skip_urls_containing):
|
|
47
44
|
continue
|
|
48
45
|
if url.has_locale():
|
|
49
46
|
url.issue = "has locale"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""The ``check_urls_tracking``/``check_paths_tracking`` checks: flag links missing a
|
|
1
|
+
"""The ``check_urls_tracking``/``check_paths_tracking`` checks: flag links missing a tracking ID."""
|
|
2
2
|
|
|
3
3
|
from markdown_checker.checks.base import BaseCheck
|
|
4
4
|
from markdown_checker.models import Config
|
|
@@ -21,8 +21,9 @@ class URLsTrackingCheck(BaseCheck[MarkdownURL]):
|
|
|
21
21
|
service: URLCheckService | None = None,
|
|
22
22
|
) -> list[MarkdownURL]:
|
|
23
23
|
"""Flag every URL in ``links.urls`` whose host matches
|
|
24
|
-
``config.tracking_domains`` but is missing
|
|
25
|
-
|
|
24
|
+
``config.tracking_domains`` but is missing all of the
|
|
25
|
+
``config.tracking_param`` query parameters (see
|
|
26
|
+
:meth:`MarkdownLinkBase.has_tracking
|
|
26
27
|
<markdown_checker.models.base.MarkdownLinkBase.has_tracking>`). URLs
|
|
27
28
|
on other hosts, or matching ``config.skip_domains``/
|
|
28
29
|
``skip_urls_containing``, are ignored. Sets
|
|
@@ -36,12 +37,9 @@ class URLsTrackingCheck(BaseCheck[MarkdownURL]):
|
|
|
36
37
|
|
|
37
38
|
detected_issues: list[MarkdownURL] = []
|
|
38
39
|
for url in links.urls:
|
|
39
|
-
|
|
40
|
-
if any(domain.lower() in hostname for domain in skip_domains) or any(
|
|
41
|
-
substring in url.link for substring in skip_urls_containing
|
|
42
|
-
):
|
|
40
|
+
if url.matches_domain(skip_domains) or any(substring in url.link for substring in skip_urls_containing):
|
|
43
41
|
continue
|
|
44
|
-
if
|
|
42
|
+
if url.matches_domain(tracking_domains) and not url.has_tracking(config.tracking_param):
|
|
45
43
|
url.issue = "is missing tracking id"
|
|
46
44
|
detected_issues.append(url)
|
|
47
45
|
return detected_issues
|
|
@@ -59,16 +57,18 @@ class PathsTrackingCheck(BaseCheck[MarkdownPath]):
|
|
|
59
57
|
config: Config | None = None,
|
|
60
58
|
service: URLCheckService | None = None,
|
|
61
59
|
) -> list[MarkdownPath]:
|
|
62
|
-
"""Flag every path in ``links.paths`` missing
|
|
63
|
-
|
|
60
|
+
"""Flag every path in ``links.paths`` missing all of the
|
|
61
|
+
``config.tracking_param`` query parameters (see
|
|
62
|
+
:meth:`MarkdownLinkBase.has_tracking
|
|
64
63
|
<markdown_checker.models.base.MarkdownLinkBase.has_tracking>`),
|
|
65
64
|
setting ``issue="is missing tracking id"`` (error-level) on each.
|
|
66
65
|
Unlike ``check_urls_tracking``, there is no domain filter: every
|
|
67
|
-
path is checked. ``
|
|
66
|
+
path is checked. ``service`` is unused.
|
|
68
67
|
"""
|
|
68
|
+
config = config or Config()
|
|
69
69
|
detected_issues: list[MarkdownPath] = []
|
|
70
70
|
for path in links.paths:
|
|
71
|
-
if not path.has_tracking():
|
|
71
|
+
if not path.has_tracking(config.tracking_param):
|
|
72
72
|
path.issue = "is missing tracking id"
|
|
73
73
|
detected_issues.append(path)
|
|
74
74
|
return detected_issues
|
|
@@ -11,6 +11,7 @@ import click
|
|
|
11
11
|
from markdown_checker import __version__
|
|
12
12
|
from markdown_checker.checker import run_check_on_files
|
|
13
13
|
from markdown_checker.models.config import Config
|
|
14
|
+
from markdown_checker.patterns import DEFAULT_TRACKING_PARAMS
|
|
14
15
|
from markdown_checker.reports import build_report
|
|
15
16
|
from markdown_checker.reports import get_renderer
|
|
16
17
|
from markdown_checker.reports import RENDERERS
|
|
@@ -123,6 +124,15 @@ class ListOfStrings(click.Option):
|
|
|
123
124
|
help="List of tracking domains to check.",
|
|
124
125
|
required=False,
|
|
125
126
|
)
|
|
127
|
+
@click.option(
|
|
128
|
+
"-tp",
|
|
129
|
+
"--tracking-param",
|
|
130
|
+
cls=ListOfStrings,
|
|
131
|
+
type=list[str],
|
|
132
|
+
default=list(DEFAULT_TRACKING_PARAMS),
|
|
133
|
+
help="List of tracking query parameters; a link passes if it has any of them.",
|
|
134
|
+
required=False,
|
|
135
|
+
)
|
|
126
136
|
@click.option(
|
|
127
137
|
"-sf",
|
|
128
138
|
"--skip-files",
|
|
@@ -258,6 +268,7 @@ def main(
|
|
|
258
268
|
skip_domains: list[str],
|
|
259
269
|
skip_urls_containing: list[str],
|
|
260
270
|
tracking_domains: list[str],
|
|
271
|
+
tracking_param: list[str],
|
|
261
272
|
timeout: int,
|
|
262
273
|
retries: int,
|
|
263
274
|
retry_on_429: bool,
|
|
@@ -309,6 +320,7 @@ def main(
|
|
|
309
320
|
skip_domains=skip_domains,
|
|
310
321
|
skip_urls_containing=skip_urls_containing,
|
|
311
322
|
tracking_domains=tracking_domains,
|
|
323
|
+
tracking_param=tracking_param,
|
|
312
324
|
timeout=timeout,
|
|
313
325
|
retries=retries,
|
|
314
326
|
retry_on_429=retry_on_429,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Shared base dataclass for the two kinds of link markdown-checker extracts: URLs and paths."""
|
|
2
|
+
|
|
3
|
+
import locale
|
|
4
|
+
from abc import ABC
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Literal
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
from markdown_checker.patterns import DEFAULT_TRACKING_PARAMS
|
|
12
|
+
from markdown_checker.patterns import LOCALE_PATTERN
|
|
13
|
+
from markdown_checker.patterns import tracking_pattern
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True)
|
|
17
|
+
class MarkdownLinkBase(ABC):
|
|
18
|
+
"""Base class for a single link found in a markdown file."""
|
|
19
|
+
|
|
20
|
+
link: str
|
|
21
|
+
"""The raw link destination as written in the file (e.g.
|
|
22
|
+
``"https://example.com"`` or ``"../img.png"``).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
line_number: int
|
|
26
|
+
"""1-based line number where the link was found."""
|
|
27
|
+
|
|
28
|
+
file_path: Path
|
|
29
|
+
"""Path to the markdown file the link was found in."""
|
|
30
|
+
|
|
31
|
+
issue: str = ""
|
|
32
|
+
"""Human-readable description set by a check when this link fails it
|
|
33
|
+
(e.g. ``"is broken"``); empty when no issue was found.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
issue_level: Literal["error", "warning"] = "error"
|
|
37
|
+
"""Severity of ``issue`` once set. ``"error"`` fails the CLI run (exit
|
|
38
|
+
code 1); ``"warning"`` is reported but never fails it.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
resolved_path: Path | None = None
|
|
42
|
+
"""The link's resolved on-disk target path, set by
|
|
43
|
+
``check_broken_paths`` (see :meth:`MarkdownPath.get_full_path
|
|
44
|
+
<markdown_checker.models.path.MarkdownPath.get_full_path>`); left unset
|
|
45
|
+
(``None``) by every other check, including URL checks.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def has_locale(self) -> bool:
|
|
49
|
+
"""
|
|
50
|
+
Check if the link's path contains a locale segment, e.g. ``/en-us/``.
|
|
51
|
+
|
|
52
|
+
Only the path is searched (not the host, query or anchor), and a
|
|
53
|
+
candidate ``xx-yy`` segment only counts if it's a known locale, so
|
|
54
|
+
segments like ``/ci-cd/`` aren't reported.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True if the link has a locale, False otherwise
|
|
58
|
+
"""
|
|
59
|
+
path = urlparse(self.link).path
|
|
60
|
+
return any(
|
|
61
|
+
f"{match['language']}_{match['region']}".lower() in locale.locale_alias
|
|
62
|
+
for match in LOCALE_PATTERN.finditer(path)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def has_tracking(self, params: Sequence[str] = DEFAULT_TRACKING_PARAMS) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
Check if the link carries any of the tracking query parameters in
|
|
68
|
+
``params`` (case-insensitive), e.g. ``?wt.mc_id=`` or ``&wt.mc_id=``.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
params: Tracking parameter names; any one of them counts.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if the link has a tracking ID, False otherwise
|
|
75
|
+
"""
|
|
76
|
+
if not params:
|
|
77
|
+
return False
|
|
78
|
+
return bool(tracking_pattern(tuple(params)).search(self.link))
|
|
79
|
+
|
|
80
|
+
def __str__(self) -> str:
|
|
81
|
+
return self.link
|
|
82
|
+
|
|
83
|
+
def __repr__(self) -> str:
|
|
84
|
+
return self.link
|
|
@@ -4,6 +4,8 @@ from typing import Literal
|
|
|
4
4
|
|
|
5
5
|
import httpx2
|
|
6
6
|
|
|
7
|
+
from markdown_checker.patterns import DEFAULT_TRACKING_PARAMS
|
|
8
|
+
|
|
7
9
|
DEFAULT_HEADERS: dict[str, str] = {
|
|
8
10
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
9
11
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
|
|
@@ -33,8 +35,10 @@ class Config:
|
|
|
33
35
|
"""Holds all runtime configuration for a check run."""
|
|
34
36
|
|
|
35
37
|
skip_domains: list[str] = field(default_factory=list)
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
+
"""Domains to exclude from URL checks. A domain matches the URL's
|
|
39
|
+
hostname exactly or as a parent domain (``example.com`` matches
|
|
40
|
+
``www.example.com`` but not ``notexample.com``). Used by
|
|
41
|
+
``check_broken_urls``, ``check_urls_locale`` and ``check_urls_tracking``.
|
|
38
42
|
"""
|
|
39
43
|
|
|
40
44
|
skip_urls_containing: list[str] = field(default_factory=list)
|
|
@@ -43,9 +47,16 @@ class Config:
|
|
|
43
47
|
"""
|
|
44
48
|
|
|
45
49
|
tracking_domains: list[str] = field(default_factory=list)
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
"""Domains whose URLs must carry a tracking parameter (see
|
|
51
|
+
``tracking_param``), matched like ``skip_domains`` (subdomains
|
|
52
|
+
included). Only used by ``check_urls_tracking``; URLs on other hosts are
|
|
53
|
+
ignored by that check.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
tracking_param: list[str] = field(default_factory=lambda: list(DEFAULT_TRACKING_PARAMS))
|
|
57
|
+
"""Tracking query parameter names a link must carry; any one of them
|
|
58
|
+
counts, matched case-insensitively. Used by ``check_urls_tracking`` and
|
|
59
|
+
``check_paths_tracking``.
|
|
49
60
|
"""
|
|
50
61
|
|
|
51
62
|
timeout: int = 20
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from urllib.parse import unquote
|
|
4
5
|
|
|
5
6
|
from markdown_checker.models.base import MarkdownLinkBase
|
|
7
|
+
from markdown_checker.patterns import QUERY_OR_ANCHOR_PATTERN
|
|
6
8
|
from markdown_checker.patterns import TRACKING_QUERY_PATTERN
|
|
7
9
|
|
|
8
10
|
|
|
@@ -26,46 +28,36 @@ class MarkdownPath(MarkdownLinkBase):
|
|
|
26
28
|
def remove_fragments(self) -> str:
|
|
27
29
|
"""
|
|
28
30
|
Strip the tracking query string and any trailing ``?query``/``#anchor``
|
|
29
|
-
fragment from the link,
|
|
31
|
+
fragment from the link, then percent-decode what's left (e.g.
|
|
32
|
+
``my%20file.md`` becomes ``my file.md``), leaving just the file path.
|
|
33
|
+
|
|
34
|
+
As in any URL, the first ``?`` or ``#`` starts the fragment, so a
|
|
35
|
+
literal ``#`` or ``?`` in a file name must be written as ``%23``/``%3F``.
|
|
30
36
|
|
|
31
37
|
Returns:
|
|
32
38
|
The link with its fragment removed, as a string.
|
|
33
39
|
"""
|
|
34
40
|
cleaned = TRACKING_QUERY_PATTERN.sub("", self.link)
|
|
35
|
-
|
|
36
|
-
# Find the last occurrence of the dot
|
|
37
|
-
dot_index = cleaned.rfind(".")
|
|
38
|
-
|
|
39
|
-
# If a dot is found, slice the string up to the end of the extension
|
|
40
|
-
if dot_index != -1:
|
|
41
|
-
# Find the next slash or fragment after the dot
|
|
42
|
-
slash_index = cleaned.find("/", dot_index)
|
|
43
|
-
fragment_index = cleaned.find("#", dot_index)
|
|
44
|
-
|
|
45
|
-
# Determine the earliest occurrence of either slash or fragment or query
|
|
46
|
-
end_index = min(
|
|
47
|
-
slash_index if slash_index != -1 else len(cleaned),
|
|
48
|
-
fragment_index if fragment_index != -1 else len(cleaned),
|
|
49
|
-
)
|
|
50
|
-
return cleaned[:end_index]
|
|
51
|
-
else:
|
|
52
|
-
return cleaned
|
|
41
|
+
return unquote(QUERY_OR_ANCHOR_PATTERN.sub("", cleaned))
|
|
53
42
|
|
|
54
43
|
def get_full_path(self) -> Path:
|
|
55
44
|
"""
|
|
56
|
-
Resolve the link (with its fragment stripped) to an absolute path
|
|
57
|
-
relative to the current working directory.
|
|
45
|
+
Resolve the link (with its fragment stripped) to an absolute path.
|
|
58
46
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
path), falls back to
|
|
47
|
+
Root-relative links (e.g. ``/docs/usage.md``) resolve against the
|
|
48
|
+
current working directory, which is treated as the repository root.
|
|
49
|
+
Other links resolve relative to the current working directory too;
|
|
50
|
+
if that fails (e.g. on an unusual path), this falls back to
|
|
51
|
+
:meth:`get_full_path_relative`.
|
|
63
52
|
|
|
64
53
|
Returns:
|
|
65
54
|
The resolved absolute path.
|
|
66
55
|
"""
|
|
56
|
+
cleaned = self.remove_fragments()
|
|
57
|
+
if cleaned.startswith("/"):
|
|
58
|
+
return Path(os.path.normpath(Path.cwd() / cleaned.lstrip("/")))
|
|
67
59
|
try:
|
|
68
|
-
return
|
|
60
|
+
return Path(cleaned).resolve()
|
|
69
61
|
except (FileNotFoundError, RuntimeError):
|
|
70
62
|
return Path(self.get_full_path_relative())
|
|
71
63
|
|
|
@@ -85,39 +77,21 @@ class MarkdownPath(MarkdownLinkBase):
|
|
|
85
77
|
"""
|
|
86
78
|
Check whether the link's target file exists on disk.
|
|
87
79
|
|
|
88
|
-
Tries resolution relative to the containing markdown file first
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
flag links whose target is
|
|
80
|
+
Tries resolution relative to the containing markdown file first, then
|
|
81
|
+
relative to the current working directory. Root-relative links (e.g.
|
|
82
|
+
``/docs/usage.md``) are tried the same way with the leading ``/``
|
|
83
|
+
dropped. Used by ``check_broken_paths`` to flag links whose target is
|
|
84
|
+
missing.
|
|
92
85
|
|
|
93
86
|
Returns:
|
|
94
87
|
True if the path exists, False otherwise
|
|
95
88
|
"""
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
cleaned = TRACKING_QUERY_PATTERN.sub("", link)
|
|
99
|
-
|
|
100
|
-
# Strip fragments/anchors the same way remove_fragments() does
|
|
101
|
-
dot_index = cleaned.rfind(".")
|
|
102
|
-
if dot_index != -1:
|
|
103
|
-
slash_index = cleaned.find("/", dot_index)
|
|
104
|
-
fragment_index = cleaned.find("#", dot_index)
|
|
105
|
-
end_index = min(
|
|
106
|
-
slash_index if slash_index != -1 else len(cleaned),
|
|
107
|
-
fragment_index if fragment_index != -1 else len(cleaned),
|
|
108
|
-
)
|
|
109
|
-
cleaned = cleaned[:end_index]
|
|
110
|
-
|
|
111
|
-
# Check relative path resolution
|
|
89
|
+
cleaned = self.remove_fragments().lstrip("/")
|
|
90
|
+
|
|
112
91
|
relative_path = os.path.normpath(os.path.join(os.path.dirname(self.file_path), cleaned))
|
|
113
92
|
if os.path.exists(relative_path):
|
|
114
93
|
return True
|
|
115
|
-
# Check absolute path resolution
|
|
116
94
|
try:
|
|
117
|
-
|
|
118
|
-
return True
|
|
95
|
+
return Path(cleaned).resolve().exists()
|
|
119
96
|
except (FileNotFoundError, RuntimeError, OSError):
|
|
120
|
-
|
|
121
|
-
if resolved.exists():
|
|
122
|
-
return True
|
|
123
|
-
return False
|
|
97
|
+
return False
|