Markdown-Checker 1.2.1__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.
Files changed (123) hide show
  1. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/PKG-INFO +6 -6
  2. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/README.md +3 -4
  3. markdown_checker-1.3.0/docs/api/index.rst +20 -0
  4. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/explanation/link-detection.rst +10 -7
  5. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/howto/github-actions.rst +3 -32
  6. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/howto/pyproject-config.rst +1 -0
  7. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/howto/skip-links.rst +4 -3
  8. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/index.rst +1 -2
  9. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/reference/checks.rst +14 -9
  10. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/reference/cli.rst +24 -4
  11. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/reference/configuration.rst +4 -0
  12. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/reference/report-formats.rst +1 -1
  13. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/tutorial.rst +1 -1
  14. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/pyproject.toml +1 -1
  15. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/__init__.py +1 -1
  16. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/checks/broken_paths.py +2 -1
  17. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/checks/broken_urls.py +2 -5
  18. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/checks/locale.py +1 -4
  19. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/checks/tracking.py +12 -12
  20. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/cli.py +36 -9
  21. markdown_checker-1.3.0/src/markdown_checker/models/base.py +84 -0
  22. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/models/config.py +16 -5
  23. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/models/path.py +27 -53
  24. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/models/url.py +14 -0
  25. markdown_checker-1.3.0/src/markdown_checker/patterns.py +57 -0
  26. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/reports/model.py +8 -0
  27. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/reports/renderers/markdown.py +9 -3
  28. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/checks/test_base.py +1 -1
  29. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/checks/test_broken_paths.py +2 -1
  30. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/checks/test_tracking.py +20 -0
  31. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/models/test_base.py +4 -0
  32. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/models/test_path.py +52 -16
  33. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/models/test_url.py +17 -0
  34. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/reports/conftest.py +1 -1
  35. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/reports/renderers/test_json.py +1 -1
  36. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/reports/renderers/test_markdown.py +31 -0
  37. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/reports/test_model.py +22 -0
  38. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/test_cli.py +12 -1
  39. markdown_checker-1.3.0/uv.lock +1835 -0
  40. markdown_checker-1.2.1/src/markdown_checker/models/base.py +0 -59
  41. markdown_checker-1.2.1/src/markdown_checker/patterns.py +0 -36
  42. markdown_checker-1.2.1/uv.lock +0 -1561
  43. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/LICENSE +0 -0
  44. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/CODE_OF_CONDUCT.rst +0 -0
  45. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/CONTRIBUTING.rst +0 -0
  46. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/api/checker.rst +0 -0
  47. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/api/checks.rst +0 -0
  48. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/api/cli.rst +0 -0
  49. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/api/main.rst +0 -0
  50. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/api/models.rst +0 -0
  51. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/api/patterns.rst +0 -0
  52. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/api/reports.rst +0 -0
  53. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/api/utils.rst +0 -0
  54. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/changes.rst +0 -0
  55. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/conf.py +0 -0
  56. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/explanation/index.rst +0 -0
  57. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/explanation/url-checking.rst +0 -0
  58. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/howto/index.rst +0 -0
  59. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/howto/rate-limits.rst +0 -0
  60. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/howto/report-formats.rst +0 -0
  61. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/howto/select-files.rst +0 -0
  62. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/howto/shell-completions.rst +0 -0
  63. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/license.rst +0 -0
  64. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/reference/exit-codes.rst +0 -0
  65. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/docs/reference/index.rst +0 -0
  66. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/checker.py +0 -0
  67. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/checks/__init__.py +0 -0
  68. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/checks/base.py +0 -0
  69. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/models/__init__.py +0 -0
  70. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/py.typed +0 -0
  71. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/reports/__init__.py +0 -0
  72. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/reports/base.py +0 -0
  73. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/reports/registry.py +0 -0
  74. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/reports/renderers/__init__.py +0 -0
  75. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/reports/renderers/annotations.py +0 -0
  76. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/reports/renderers/console.py +0 -0
  77. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/reports/renderers/json.py +0 -0
  78. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/reports/templates/paths/broken.md +0 -0
  79. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/reports/templates/paths/tracking.md +0 -0
  80. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/reports/templates/urls/broken.md +0 -0
  81. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/reports/templates/urls/locale.md +0 -0
  82. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/reports/templates/urls/tracking.md +0 -0
  83. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/reports/writer.py +0 -0
  84. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/utils/__init__.py +0 -0
  85. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/utils/extract_links.py +0 -0
  86. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/utils/github_env.py +0 -0
  87. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/utils/list_files.py +0 -0
  88. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/utils/pyproject_config.py +0 -0
  89. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/utils/spinner.py +0 -0
  90. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/src/markdown_checker/utils/url_pipeline.py +0 -0
  91. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/checks/__init__.py +0 -0
  92. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/checks/test_broken_urls.py +0 -0
  93. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/checks/test_checks.py +0 -0
  94. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/checks/test_locale.py +0 -0
  95. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/conftest.py +0 -0
  96. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/models/__init__.py +0 -0
  97. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/models/test_config.py +0 -0
  98. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/models/test_models.py +0 -0
  99. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/reports/__init__.py +0 -0
  100. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/reports/renderers/__init__.py +0 -0
  101. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/reports/renderers/test_annotations.py +0 -0
  102. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/reports/renderers/test_console.py +0 -0
  103. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/reports/renderers/test_renderers.py +0 -0
  104. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/reports/test_base.py +0 -0
  105. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/reports/test_registry.py +0 -0
  106. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/reports/test_reports.py +0 -0
  107. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/reports/test_writer.py +0 -0
  108. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/resources/sample1.md +0 -0
  109. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/resources/sample2.md +0 -0
  110. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/resources/sample3.md +0 -0
  111. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/resources/sample4.md +0 -0
  112. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/smoke/test_cli_smoke.py +0 -0
  113. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/test_checker.py +0 -0
  114. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/test_markdown_checker.py +0 -0
  115. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/test_patterns.py +0 -0
  116. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/utils/__init__.py +0 -0
  117. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/utils/test_extract_links.py +0 -0
  118. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/utils/test_github_env.py +0 -0
  119. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/utils/test_list_files.py +0 -0
  120. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/utils/test_pyproject_config.py +0 -0
  121. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/utils/test_spinner.py +0 -0
  122. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/utils/test_url_pipeline.py +0 -0
  123. {markdown_checker-1.2.1 → markdown_checker-1.3.0}/tests/utils/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: markdown-checker
3
- Version: 1.2.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
  [![PyPi](https://img.shields.io/pypi/v/markdown-checker)](https://pypi.org/project/markdown-checker/)
31
32
  [![Documentation Status](https://readthedocs.org/projects/markdown-checker/badge/?version=latest)](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 is broken.
81
+ ./missing.md could not be found at /docs/missing.md.
81
82
  ```
82
83
 
83
84
  ## Using `markdown-checker` in GitHub Actions
@@ -87,7 +88,7 @@ Run this tool within a GitHub workflow using the
87
88
  GitHub Action, which posts the report as a pull request comment:
88
89
 
89
90
  ```yaml
90
- - uses: john0isaac/action-check-markdown@v1.2.0
91
+ - uses: john0isaac/action-check-markdown@v1.3.0
91
92
  with:
92
93
  github-token: ${{ secrets.GITHUB_TOKEN }}
93
94
  command: check_broken_paths
@@ -96,8 +97,7 @@ GitHub Action, which posts the report as a pull request comment:
96
97
  ```
97
98
 
98
99
  See the [GitHub Actions how-to](https://markdown-checker.readthedocs.io/en/latest/howto/github-actions/)
99
- for the full workflow, including running `check_broken_urls` (not yet
100
- supported by the wrapper action) directly as a step.
100
+ for the full workflow.
101
101
 
102
102
  ## Documentation
103
103
 
@@ -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 is broken.
51
+ ./missing.md could not be found at /docs/missing.md.
52
52
  ```
53
53
 
54
54
  ## Using `markdown-checker` in GitHub Actions
@@ -58,7 +58,7 @@ Run this tool within a GitHub workflow using the
58
58
  GitHub Action, which posts the report as a pull request comment:
59
59
 
60
60
  ```yaml
61
- - uses: john0isaac/action-check-markdown@v1.2.0
61
+ - uses: john0isaac/action-check-markdown@v1.3.0
62
62
  with:
63
63
  github-token: ${{ secrets.GITHUB_TOKEN }}
64
64
  command: check_broken_paths
@@ -67,8 +67,7 @@ GitHub Action, which posts the report as a pull request comment:
67
67
  ```
68
68
 
69
69
  See the [GitHub Actions how-to](https://markdown-checker.readthedocs.io/en/latest/howto/github-actions/)
70
- for the full workflow, including running `check_broken_urls` (not yet
71
- supported by the wrapper action) directly as a step.
70
+ for the full workflow.
72
71
 
73
72
  ## Documentation
74
73
 
@@ -0,0 +1,20 @@
1
+ API Reference
2
+ =============
3
+
4
+ If you are looking for information on a specific function, class or
5
+ method, this part of the documentation is for you. These pages are
6
+ generated from the docstrings in ``markdown_checker`` and are meant for
7
+ programmatic/library use. For the command-line interface, see
8
+ :doc:`../reference/cli` instead.
9
+
10
+ .. toctree::
11
+ :maxdepth: 1
12
+
13
+ main
14
+ checker
15
+ cli
16
+ models
17
+ checks
18
+ patterns
19
+ reports
20
+ utils
@@ -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``. Both checks that operate on paths strip
42
- this fragment before resolving the file, so only the ``docs/usage.md``
43
- part needs to exist on disk; the anchor itself is never validated against
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 falls back to resolving it as an
52
- absolute or current-working-directory-relative path. A path is reported as
53
- broken only if neither resolution finds an existing file.
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.
@@ -35,48 +35,19 @@ replacing ``guide-url`` with the URL of your own contribution guide:
35
35
  contents: read
36
36
  steps:
37
37
  - uses: actions/checkout@v7
38
- - uses: john0isaac/action-check-markdown@v1.2.0
38
+ - uses: john0isaac/action-check-markdown@v1.3.0
39
39
  with:
40
40
  github-token: ${{ secrets.GITHUB_TOKEN }}
41
41
  command: check_broken_paths
42
42
  directory: ./
43
43
  guide-url: "https://github.com/<owner>/<repo>/blob/main/CONTRIBUTING.md"
44
44
 
45
- The ``command`` input selects which check to run. As of v1.2.0 the action
45
+ The ``command`` input selects which check to run it
46
46
  supports ``check_broken_paths``, ``check_paths_tracking``,
47
- ``check_urls_tracking``, and ``check_urls_locale`` - **not**
47
+ ``check_urls_tracking``, and ``check_urls_locale``
48
48
  ``check_broken_urls``. Add a separate job (or step) per command if you want
49
49
  to run more than one check in the same workflow.
50
50
 
51
- Run ``check_broken_urls`` directly (no wrapper action)
52
- ------------------------------------------------------------
53
-
54
- Since the wrapper action doesn't support URL checking yet, install and
55
- invoke markdown-checker directly as a workflow step instead. Use
56
- ``--report-format github-annotations`` so failures show up as inline
57
- annotations on the PR diff, the same way the action's output does:
58
-
59
- .. code-block:: yaml
60
-
61
- jobs:
62
- check-broken-urls:
63
- runs-on: ubuntu-latest
64
- permissions:
65
- contents: read
66
- steps:
67
- - uses: actions/checkout@v7
68
- - uses: actions/setup-python@v6
69
- with:
70
- python-version: "3.11"
71
- - run: pip install markdown-checker
72
- - run: >
73
- markdown-checker . -f check_broken_urls
74
- -rf github-annotations -o -
75
-
76
- Because ``$CI=true`` is set automatically in GitHub Actions, warnings
77
- (rate-limited/unverifiable links) are emitted as ``::warning`` annotations
78
- rather than failing the job; only broken links (an error-level issue) fail it.
79
-
80
51
  Verify it worked
81
52
  ------------------
82
53
 
@@ -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
- Matching is a substring match against the URL's hostname, so
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``
@@ -71,9 +71,8 @@ method, this part of the documentation is for you.
71
71
  .. toctree::
72
72
  :maxdepth: 2
73
73
  :caption: API Reference
74
- :glob:
75
74
 
76
- api/*
75
+ api/index
77
76
 
78
77
  Additional Notes
79
78
  ----------------
@@ -26,7 +26,7 @@ Sample finding
26
26
  .. code-block:: text
27
27
 
28
28
  File 'docs/index.md', line 5
29
- ./missing.md is broken.
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 segment" is a
59
- language-country pair matching ``xx-xx`` (case-insensitive).
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 a ``wt.mc_id``
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``, ``--skip-domains``, ``--skip-urls-containing``.
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 a ``wt.mc_id`` query parameter. Unlike
98
- ``check_urls_tracking``, there is no domain filter - all paths are checked.
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`` only.
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 ``wt.mc_id`` tracking parameter.
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``. Substring
114
- match against the URL's hostname.
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.2.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 is broken.
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
@@ -67,7 +67,7 @@ typing = [
67
67
  ]
68
68
 
69
69
  [build-system]
70
- requires = ["flit_core<4"]
70
+ requires = ["flit_core<5"]
71
71
  build-backend = "flit_core.buildapi"
72
72
 
73
73
  [project.scripts]
@@ -2,7 +2,7 @@
2
2
  markdown-checker - a markdown link validation reporting tool.
3
3
  """
4
4
 
5
- __version__ = "1.2.1"
5
+ __version__ = "1.3.0"
6
6
 
7
7
  from markdown_checker.cli import main
8
8
  from markdown_checker.models import Config
@@ -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.issue = "is broken"
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
@@ -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 = [domain.lower() for domain in [*config.skip_domains, *_BUILTIN_SKIP_DOMAINS]]
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
- hostname = url.host_name().lower()
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
- hostname = url.host_name().lower()
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 ``wt.mc_id`` tracking ID."""
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 a ``wt.mc_id`` query
25
- parameter (see :meth:`MarkdownLinkBase.has_tracking
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
- hostname = url.host_name().lower()
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 any(domain.lower() in hostname for domain in tracking_domains) and not url.has_tracking():
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 a ``wt.mc_id`` query
63
- parameter (see :meth:`MarkdownLinkBase.has_tracking
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. ``config`` and ``service`` are unused.
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
@@ -2,6 +2,7 @@
2
2
 
3
3
  import os
4
4
  import platform
5
+ import sys
5
6
  from pathlib import Path
6
7
  from typing import Any
7
8
 
@@ -10,6 +11,7 @@ import click
10
11
  from markdown_checker import __version__
11
12
  from markdown_checker.checker import run_check_on_files
12
13
  from markdown_checker.models.config import Config
14
+ from markdown_checker.patterns import DEFAULT_TRACKING_PARAMS
13
15
  from markdown_checker.reports import build_report
14
16
  from markdown_checker.reports import get_renderer
15
17
  from markdown_checker.reports import RENDERERS
@@ -38,6 +40,23 @@ class PyprojectCommand(click.Command):
38
40
  return super().make_context(info_name, args, parent, **extra)
39
41
 
40
42
 
43
+ def _echo_safe(message: str, *, fg: str | None = None, err: bool = False) -> None:
44
+ """Echo a (optionally styled) message, dropping characters the output stream can't encode.
45
+
46
+ On Windows, piping stdout/stderr to another process (e.g. ``| grep``) can make Python
47
+ fall back to a legacy code page (e.g. cp1252) that can't represent emoji. Retry without
48
+ those characters instead of crashing the whole command.
49
+ """
50
+ styled = click.style(message, fg=fg) if fg else message
51
+ try:
52
+ click.echo(styled, err=err)
53
+ except UnicodeEncodeError:
54
+ stream = sys.stderr if err else sys.stdout
55
+ encoding = getattr(stream, "encoding", None) or "ascii"
56
+ safe_message = styled.encode(encoding, errors="ignore").decode(encoding)
57
+ click.echo(safe_message, err=err)
58
+
59
+
41
60
  class ListOfStrings(click.Option):
42
61
  """
43
62
  Helper class to parse a comma-separated list of strings from the command line.
@@ -105,6 +124,15 @@ class ListOfStrings(click.Option):
105
124
  help="List of tracking domains to check.",
106
125
  required=False,
107
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
+ )
108
136
  @click.option(
109
137
  "-sf",
110
138
  "--skip-files",
@@ -240,6 +268,7 @@ def main(
240
268
  skip_domains: list[str],
241
269
  skip_urls_containing: list[str],
242
270
  tracking_domains: list[str],
271
+ tracking_param: list[str],
243
272
  timeout: int,
244
273
  retries: int,
245
274
  retry_on_429: bool,
@@ -291,6 +320,7 @@ def main(
291
320
  skip_domains=skip_domains,
292
321
  skip_urls_containing=skip_urls_containing,
293
322
  tracking_domains=tracking_domains,
323
+ tracking_param=tracking_param,
294
324
  timeout=timeout,
295
325
  retries=retries,
296
326
  retry_on_429=retry_on_429,
@@ -309,9 +339,9 @@ def main(
309
339
  progress_callback=lambda: bar.update(1),
310
340
  )
311
341
 
312
- click.echo(
313
- click.style(f"\n🔍 Checked {check_result.links_checked} links in {len(files_paths)} files.", fg="blue"),
314
- err=False,
342
+ _echo_safe(
343
+ f"\n🔍 Checked {check_result.links_checked} links in {len(files_paths)} files.",
344
+ fg="blue",
315
345
  )
316
346
 
317
347
  if check_result.issues:
@@ -328,13 +358,10 @@ def main(
328
358
  renderer = get_renderer(report_format)
329
359
  out_path = None if output_file_name == "-" else Path(f"{output_file_name}{renderer.file_extension}")
330
360
  write_report(renderer.render(report), output_path=out_path)
331
- click.echo(click.style(f"😭 Found {report.error_count} issues in the following files:", fg="red"), err=True)
361
+ _echo_safe(f"😭 Found {report.error_count} issues in the following files:", fg="red", err=True)
332
362
 
333
363
  if report.has_warnings:
334
- click.echo(
335
- click.style(f"⚠ {report.warning_count} links had warnings:", fg="yellow"),
336
- err=True,
337
- )
364
+ _echo_safe(f"⚠ {report.warning_count} links had warnings:", fg="yellow", err=True)
338
365
 
339
366
  terminal_renderer = GitHubAnnotationsRenderer() if config.output_mode == "ci" else ConsoleRenderer()
340
367
  for message, level in terminal_renderer.render_lines(report):
@@ -342,5 +369,5 @@ def main(
342
369
 
343
370
  ctx.exit(1 if report.has_errors else 0)
344
371
 
345
- click.echo(click.style("All files are compliant with the guidelines. 🎉", fg="green"), err=False)
372
+ _echo_safe("All files are compliant with the guidelines. 🎉", fg="green")
346
373
  ctx.exit(0)