Markdown-Checker 0.1.5__tar.gz → 0.2.1__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-0.1.5/src/markdown_checker.egg-info → markdown_checker-0.2.1}/PKG-INFO +23 -11
- {markdown_checker-0.1.5 → markdown_checker-0.2.1}/README.md +3 -1
- {markdown_checker-0.1.5 → markdown_checker-0.2.1}/pyproject.toml +37 -24
- markdown_checker-0.2.1/src/markdown_checker/__init__.py +373 -0
- markdown_checker-0.2.1/src/markdown_checker/markdown_link_base.py +42 -0
- markdown_checker-0.2.1/src/markdown_checker/paths.py +92 -0
- markdown_checker-0.2.1/src/markdown_checker/reports/generator_base.py +35 -0
- markdown_checker-0.2.1/src/markdown_checker/reports/md_reports/generator.py +60 -0
- markdown_checker-0.2.1/src/markdown_checker/reports/md_reports/templates/paths/broken.md +6 -0
- markdown_checker-0.2.1/src/markdown_checker/reports/md_reports/templates/paths/tracking.md +6 -0
- markdown_checker-0.2.1/src/markdown_checker/reports/md_reports/templates/urls/broken.md +5 -0
- markdown_checker-0.2.1/src/markdown_checker/reports/md_reports/templates/urls/locale.md +6 -0
- markdown_checker-0.2.1/src/markdown_checker/reports/md_reports/templates/urls/tracking.md +6 -0
- markdown_checker-0.2.1/src/markdown_checker/urls.py +52 -0
- markdown_checker-0.2.1/src/markdown_checker/utils/extract_links.py +61 -0
- markdown_checker-0.2.1/src/markdown_checker/utils/format_output.py +42 -0
- markdown_checker-0.2.1/src/markdown_checker/utils/list_files.py +36 -0
- markdown_checker-0.2.1/src/markdown_checker/utils/spinner.py +82 -0
- {markdown_checker-0.1.5 → markdown_checker-0.2.1/src/markdown_checker.egg-info}/PKG-INFO +23 -11
- markdown_checker-0.2.1/src/markdown_checker.egg-info/SOURCES.txt +29 -0
- markdown_checker-0.2.1/src/markdown_checker.egg-info/entry_points.txt +2 -0
- markdown_checker-0.2.1/src/markdown_checker.egg-info/requires.txt +20 -0
- markdown_checker-0.1.5/src/markdown_checker/check_markdown.py +0 -84
- markdown_checker-0.1.5/src/markdown_checker/generators/md_generator.py +0 -64
- markdown_checker-0.1.5/src/markdown_checker/inputs/input_arguments.py +0 -61
- markdown_checker-0.1.5/src/markdown_checker/links/__init__.py +0 -0
- markdown_checker-0.1.5/src/markdown_checker/links/link_operations.py +0 -138
- markdown_checker-0.1.5/src/markdown_checker/paths/__init__.py +0 -0
- markdown_checker-0.1.5/src/markdown_checker/paths/files_paths_reader.py +0 -45
- markdown_checker-0.1.5/src/markdown_checker/validate_markdown.py +0 -60
- markdown_checker-0.1.5/src/markdown_checker.egg-info/SOURCES.txt +0 -22
- markdown_checker-0.1.5/src/markdown_checker.egg-info/entry_points.txt +0 -2
- markdown_checker-0.1.5/src/markdown_checker.egg-info/requires.txt +0 -10
- {markdown_checker-0.1.5 → markdown_checker-0.2.1}/LICENSE +0 -0
- {markdown_checker-0.1.5 → markdown_checker-0.2.1}/setup.cfg +0 -0
- {markdown_checker-0.1.5 → markdown_checker-0.2.1}/src/markdown_checker/py.typed +0 -0
- {markdown_checker-0.1.5/src/markdown_checker → markdown_checker-0.2.1/src/markdown_checker/reports}/__init__.py +0 -0
- {markdown_checker-0.1.5/src/markdown_checker/generators → markdown_checker-0.2.1/src/markdown_checker/reports/md_reports}/__init__.py +0 -0
- {markdown_checker-0.1.5/src/markdown_checker/inputs → markdown_checker-0.2.1/src/markdown_checker/utils}/__init__.py +0 -0
- {markdown_checker-0.1.5 → markdown_checker-0.2.1}/src/markdown_checker.egg-info/dependency_links.txt +0 -0
- {markdown_checker-0.1.5 → markdown_checker-0.2.1}/src/markdown_checker.egg-info/top_level.txt +0 -0
- {markdown_checker-0.1.5 → markdown_checker-0.2.1}/tests/test_markdown_checker.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: markdown-checker
|
|
3
|
-
Version: 0.1
|
|
4
|
-
Summary: A markdown link validation reporting tool
|
|
3
|
+
Version: 0.2.1
|
|
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>
|
|
7
7
|
License: MIT License
|
|
@@ -32,26 +32,36 @@ Project-URL: Issues, https://github.com/john0isaac/markdown-checker/issues
|
|
|
32
32
|
Project-URL: Contributing, https://github.com/john0isaac/markdown-checker/pulls
|
|
33
33
|
Project-URL: Releases, https://github.com/john0isaac/markdown-checker/releases
|
|
34
34
|
Classifier: Development Status :: 5 - Production/Stable
|
|
35
|
+
Classifier: Environment :: Console
|
|
35
36
|
Classifier: License :: OSI Approved :: MIT License
|
|
36
37
|
Classifier: Operating System :: OS Independent
|
|
37
|
-
Classifier: Programming Language :: Python
|
|
38
|
-
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python
|
|
39
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
39
40
|
Classifier: Programming Language :: Python :: 3.9
|
|
40
41
|
Classifier: Programming Language :: Python :: 3.10
|
|
41
42
|
Classifier: Programming Language :: Python :: 3.11
|
|
42
43
|
Classifier: Programming Language :: Python :: 3.12
|
|
43
|
-
|
|
44
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
45
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
46
|
+
Requires-Python: >=3.9
|
|
44
47
|
Description-Content-Type: text/markdown
|
|
45
48
|
License-File: LICENSE
|
|
49
|
+
Requires-Dist: click
|
|
46
50
|
Requires-Dist: requests
|
|
47
|
-
Requires-Dist: types-requests
|
|
48
51
|
Provides-Extra: dev
|
|
49
|
-
Requires-Dist: pytest; extra == "dev"
|
|
50
|
-
Requires-Dist: pytest-cov; extra == "dev"
|
|
51
52
|
Requires-Dist: pre-commit; extra == "dev"
|
|
52
|
-
Requires-Dist: ruff; extra == "dev"
|
|
53
|
-
Requires-Dist: black; extra == "dev"
|
|
54
53
|
Requires-Dist: mypy; extra == "dev"
|
|
54
|
+
Requires-Dist: types-requests; extra == "dev"
|
|
55
|
+
Provides-Extra: lint
|
|
56
|
+
Requires-Dist: ruff; extra == "lint"
|
|
57
|
+
Provides-Extra: test
|
|
58
|
+
Requires-Dist: pytest; extra == "test"
|
|
59
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
60
|
+
Provides-Extra: docs
|
|
61
|
+
Requires-Dist: mkdocs; extra == "docs"
|
|
62
|
+
Requires-Dist: mkdocs-material; extra == "docs"
|
|
63
|
+
Requires-Dist: mkdocstrings[python]; extra == "docs"
|
|
64
|
+
Requires-Dist: pymdown-extensions; extra == "docs"
|
|
55
65
|
|
|
56
66
|
[](https://pypi.org/project/markdown-checker/)
|
|
57
67
|
[](https://markdown-checker.readthedocs.io/en/latest/?badge=latest)
|
|
@@ -78,7 +88,9 @@ pip install markdown-checker
|
|
|
78
88
|
|
|
79
89
|
1. Run `pip install markdown-checker`.
|
|
80
90
|
2. Run `markdown-checker -d {src} -f {func} -gu {url}`. Replace `{src}` with the directory you want to analyze, `{func}` with the available functions like `check_broken_paths`, `{gu}` with your contribution guidance full URL.
|
|
81
|
-
3. The output will be displayed in the terminal and in a `
|
|
91
|
+
3. The output will be displayed in the terminal and in a `comment.md` file.
|
|
92
|
+
|
|
93
|
+
For more customizations read the docs.
|
|
82
94
|
|
|
83
95
|
## Using `markdown-checker` in GitHub Actions
|
|
84
96
|
|
|
@@ -23,7 +23,9 @@ pip install markdown-checker
|
|
|
23
23
|
|
|
24
24
|
1. Run `pip install markdown-checker`.
|
|
25
25
|
2. Run `markdown-checker -d {src} -f {func} -gu {url}`. Replace `{src}` with the directory you want to analyze, `{func}` with the available functions like `check_broken_paths`, `{gu}` with your contribution guidance full URL.
|
|
26
|
-
3. The output will be displayed in the terminal and in a `
|
|
26
|
+
3. The output will be displayed in the terminal and in a `comment.md` file.
|
|
27
|
+
|
|
28
|
+
For more customizations read the docs.
|
|
27
29
|
|
|
28
30
|
## Using `markdown-checker` in GitHub Actions
|
|
29
31
|
|
|
@@ -1,32 +1,31 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools", "wheel"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
1
|
[project]
|
|
6
2
|
name = "markdown-checker"
|
|
7
|
-
description= "A markdown link validation reporting tool"
|
|
8
|
-
version = "0.1
|
|
3
|
+
description= "A markdown link validation reporting tool."
|
|
4
|
+
version = "0.2.1"
|
|
9
5
|
authors = [{ name = "John Aziz", email = "johnaziz269@gmail.com" }]
|
|
10
6
|
maintainers = [{ name = "John Aziz", email = "johnaziz269@gmail.com" }]
|
|
11
7
|
license = {file = "LICENSE"}
|
|
12
8
|
readme = "README.md"
|
|
13
9
|
|
|
14
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.9"
|
|
15
11
|
dependencies = [
|
|
12
|
+
"click",
|
|
16
13
|
"requests",
|
|
17
|
-
|
|
18
|
-
]
|
|
14
|
+
]
|
|
19
15
|
classifiers = [
|
|
20
16
|
"Development Status :: 5 - Production/Stable",
|
|
17
|
+
"Environment :: Console",
|
|
21
18
|
"License :: OSI Approved :: MIT License",
|
|
22
19
|
"Operating System :: OS Independent",
|
|
23
|
-
"Programming Language :: Python
|
|
24
|
-
"Programming Language :: Python :: 3
|
|
20
|
+
"Programming Language :: Python",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
22
|
"Programming Language :: Python :: 3.9",
|
|
26
23
|
"Programming Language :: Python :: 3.10",
|
|
27
24
|
"Programming Language :: Python :: 3.11",
|
|
28
25
|
"Programming Language :: Python :: 3.12",
|
|
29
|
-
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
28
|
+
]
|
|
30
29
|
|
|
31
30
|
[project.urls]
|
|
32
31
|
Homepage = "https://github.com/john0isaac/markdown-checker"
|
|
@@ -40,26 +39,40 @@ Releases = "https://github.com/john0isaac/markdown-checker/releases"
|
|
|
40
39
|
|
|
41
40
|
[project.optional-dependencies]
|
|
42
41
|
dev = [
|
|
43
|
-
"pytest",
|
|
44
|
-
"pytest-cov",
|
|
45
42
|
"pre-commit",
|
|
46
|
-
"ruff",
|
|
47
|
-
"black",
|
|
48
43
|
"mypy",
|
|
44
|
+
"types-requests",
|
|
45
|
+
]
|
|
46
|
+
lint = [
|
|
47
|
+
"ruff"
|
|
48
|
+
]
|
|
49
|
+
test = [
|
|
50
|
+
"pytest",
|
|
51
|
+
"pytest-cov"
|
|
52
|
+
]
|
|
53
|
+
docs = [
|
|
54
|
+
"mkdocs",
|
|
55
|
+
"mkdocs-material",
|
|
56
|
+
"mkdocstrings[python]",
|
|
57
|
+
"pymdown-extensions"
|
|
49
58
|
]
|
|
50
59
|
|
|
51
60
|
[tool.ruff]
|
|
61
|
+
lint.select = ["E", "F", "I", "UP"]
|
|
52
62
|
line-length = 120
|
|
53
|
-
target-version = "
|
|
63
|
+
target-version = "py39"
|
|
54
64
|
src = ["src"]
|
|
65
|
+
lint.isort.known-first-party = ["markdown_checker"]
|
|
55
66
|
output-format = "full"
|
|
56
67
|
|
|
57
|
-
[tool.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
[
|
|
61
|
-
line-length = 120
|
|
62
|
-
target-version = ["py38"]
|
|
68
|
+
[tool.mypy]
|
|
69
|
+
check_untyped_defs = true
|
|
70
|
+
python_version = 3.9
|
|
71
|
+
exclude = [".venv/*"]
|
|
63
72
|
|
|
64
73
|
[project.scripts]
|
|
65
|
-
markdown-checker = "markdown_checker
|
|
74
|
+
markdown-checker = "markdown_checker:main_with_spinner"
|
|
75
|
+
|
|
76
|
+
[build-system]
|
|
77
|
+
requires = ["setuptools", "wheel"]
|
|
78
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Module providing automatic checks functionality to markdown files
|
|
4
|
+
following some Guidelines
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import concurrent.futures
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Union
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from markdown_checker.paths import MarkdownPath
|
|
17
|
+
from markdown_checker.reports.md_reports.generator import MarkdownGenerator
|
|
18
|
+
from markdown_checker.urls import MarkdownURL
|
|
19
|
+
from markdown_checker.utils.extract_links import get_links_from_md_file
|
|
20
|
+
from markdown_checker.utils.format_output import format_links
|
|
21
|
+
from markdown_checker.utils.list_files import get_files_paths_list
|
|
22
|
+
from markdown_checker.utils.spinner import spinner
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def check_url(
|
|
26
|
+
url: MarkdownURL, skip_domains: list[str], skip_urls_containing: list[str], timeout: int, retries: int
|
|
27
|
+
) -> Union[None, MarkdownURL]:
|
|
28
|
+
if any(url.host_name().lower() in domain.lower() for domain in skip_domains) or any(
|
|
29
|
+
url.link in substring for substring in skip_urls_containing
|
|
30
|
+
):
|
|
31
|
+
return None
|
|
32
|
+
if not url.is_alive(timeout=timeout, retries=retries):
|
|
33
|
+
url.issue = "is broken"
|
|
34
|
+
return url
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def detect_issues(
|
|
39
|
+
func: str,
|
|
40
|
+
file_path: Path,
|
|
41
|
+
skip_urls_containing: list[str],
|
|
42
|
+
skip_domains: list[str],
|
|
43
|
+
tracking_domains: list[str],
|
|
44
|
+
timeout: int,
|
|
45
|
+
retries: int,
|
|
46
|
+
) -> tuple[list[Union[MarkdownPath, MarkdownURL]], int]:
|
|
47
|
+
"""
|
|
48
|
+
Function to detect issues in the markdown file based on the function
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
func (str): Function to be executed
|
|
52
|
+
file_path (Path): Path to the markdown file
|
|
53
|
+
skip_urls_containing (list[str]): List of urls to skip check
|
|
54
|
+
skip_domains (list[str]): List of domains to skip check
|
|
55
|
+
tracking_domains (list[str]): List of tracking domains to check
|
|
56
|
+
timeout (int): Timeout in seconds for the requests
|
|
57
|
+
retries (int): Number of retries for the requests
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Detected issues and links count.
|
|
61
|
+
"""
|
|
62
|
+
detected_issues: list[Union[MarkdownPath, MarkdownURL]] = []
|
|
63
|
+
links_count = 0
|
|
64
|
+
# Step 1: Extract all Links from file path
|
|
65
|
+
all_links = get_links_from_md_file(file_path)
|
|
66
|
+
if len(all_links.paths) == 0 and len(all_links.urls) == 0:
|
|
67
|
+
return detected_issues, links_count
|
|
68
|
+
# Step 2: Check for issues based on the function
|
|
69
|
+
if func == "check_broken_paths":
|
|
70
|
+
for path in all_links.paths:
|
|
71
|
+
if not path.exists():
|
|
72
|
+
path.issue = "is broken"
|
|
73
|
+
detected_issues.append(path)
|
|
74
|
+
links_count += len(all_links.paths)
|
|
75
|
+
elif func == "check_broken_urls":
|
|
76
|
+
# currently these domains are known to have restrictions on the requests
|
|
77
|
+
skip_domains.extend(
|
|
78
|
+
[
|
|
79
|
+
"platform.openai.com",
|
|
80
|
+
"help.openai.com",
|
|
81
|
+
"beta.openai.com",
|
|
82
|
+
"marketplace.visualstudio.com",
|
|
83
|
+
"huggingface.co",
|
|
84
|
+
"en.wikipedia.org",
|
|
85
|
+
"twitter.com",
|
|
86
|
+
"www.linkedin.com",
|
|
87
|
+
"make.powerautomate.com",
|
|
88
|
+
"make.powerapps.com",
|
|
89
|
+
"www.midjourney.com",
|
|
90
|
+
"vscode.dev",
|
|
91
|
+
"rodtrent.substack.com",
|
|
92
|
+
"example.com",
|
|
93
|
+
"www.nuget.org",
|
|
94
|
+
"www.docker.com",
|
|
95
|
+
"build.nvidia.com",
|
|
96
|
+
"dotnet.microsoft.com",
|
|
97
|
+
"www.gemini.com",
|
|
98
|
+
"upload.wikimedia.org",
|
|
99
|
+
"medium.com",
|
|
100
|
+
"blogs.nvidia.com",
|
|
101
|
+
"blog.gopenai.com",
|
|
102
|
+
"towardsdatascience.com",
|
|
103
|
+
"code.visualstudio.com",
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
with concurrent.futures.ProcessPoolExecutor() as executor:
|
|
107
|
+
results = list(
|
|
108
|
+
executor.map(
|
|
109
|
+
check_url,
|
|
110
|
+
all_links.urls,
|
|
111
|
+
[skip_domains] * len(all_links.urls),
|
|
112
|
+
[skip_urls_containing] * len(all_links.urls),
|
|
113
|
+
[timeout] * len(all_links.urls),
|
|
114
|
+
[retries] * len(all_links.urls),
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
detected_issues.extend(filter(None, results))
|
|
118
|
+
links_count += len(all_links.urls)
|
|
119
|
+
elif func == "check_urls_tracking":
|
|
120
|
+
for url in all_links.urls:
|
|
121
|
+
if any(url.host_name().lower() in domain.lower() for domain in skip_domains) or any(
|
|
122
|
+
url.link in substring for substring in skip_urls_containing
|
|
123
|
+
):
|
|
124
|
+
continue
|
|
125
|
+
if any(url.host_name().lower() in domain.lower() for domain in tracking_domains) and not url.has_tracking():
|
|
126
|
+
url.issue = "is missing tracking id"
|
|
127
|
+
detected_issues.append(url)
|
|
128
|
+
links_count += len(all_links.urls)
|
|
129
|
+
elif func == "check_paths_tracking":
|
|
130
|
+
for path in all_links.paths:
|
|
131
|
+
if not path.has_tracking():
|
|
132
|
+
path.issue = "is missing tracking id"
|
|
133
|
+
detected_issues.append(path)
|
|
134
|
+
links_count += len(all_links.paths)
|
|
135
|
+
elif func == "check_urls_locale":
|
|
136
|
+
# if you remove locale from these domains, it will fail
|
|
137
|
+
skip_domains.extend(
|
|
138
|
+
[
|
|
139
|
+
"www.nvidia.com",
|
|
140
|
+
]
|
|
141
|
+
)
|
|
142
|
+
for url in all_links.urls:
|
|
143
|
+
if any(url.host_name().lower() in domain.lower() for domain in skip_domains) or any(
|
|
144
|
+
url.link in substring for substring in skip_urls_containing
|
|
145
|
+
):
|
|
146
|
+
continue
|
|
147
|
+
if url.has_locale():
|
|
148
|
+
url.issue = "has locale"
|
|
149
|
+
detected_issues.append(url)
|
|
150
|
+
links_count += len(all_links.urls)
|
|
151
|
+
elif func == "check_paths_locale":
|
|
152
|
+
for path in all_links.paths:
|
|
153
|
+
if path.has_locale():
|
|
154
|
+
path.issue = "has locale"
|
|
155
|
+
detected_issues.append(path)
|
|
156
|
+
links_count += len(all_links.paths)
|
|
157
|
+
return detected_issues, links_count
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class ListOfStrings(click.Option):
|
|
161
|
+
"""
|
|
162
|
+
Helper class to parse a list of strings from the command line.
|
|
163
|
+
|
|
164
|
+
Ref: https://stackoverflow.com/questions/47631914/how-to-pass-several-list-of-arguments-to-click-option
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def type_cast_value(self, ctx, value):
|
|
168
|
+
try:
|
|
169
|
+
if isinstance(value, str):
|
|
170
|
+
# split the string by comma and remove empty strings
|
|
171
|
+
return list(filter(None, value.split(",")))
|
|
172
|
+
elif isinstance(value, list):
|
|
173
|
+
return value
|
|
174
|
+
else:
|
|
175
|
+
raise Exception
|
|
176
|
+
except Exception:
|
|
177
|
+
raise click.BadParameter(value)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@click.command()
|
|
181
|
+
@click.option(
|
|
182
|
+
"-d",
|
|
183
|
+
"--dir",
|
|
184
|
+
type=click.Path(path_type=Path, exists=True, file_okay=False, dir_okay=True, readable=True),
|
|
185
|
+
help="Path to the root directory to check.",
|
|
186
|
+
required=True,
|
|
187
|
+
)
|
|
188
|
+
@click.option(
|
|
189
|
+
"-f",
|
|
190
|
+
"--func",
|
|
191
|
+
type=click.Choice(
|
|
192
|
+
[
|
|
193
|
+
"check_broken_paths",
|
|
194
|
+
"check_broken_urls",
|
|
195
|
+
"check_paths_tracking",
|
|
196
|
+
"check_urls_tracking",
|
|
197
|
+
"check_urls_locale",
|
|
198
|
+
]
|
|
199
|
+
),
|
|
200
|
+
help="Function to be executed.",
|
|
201
|
+
required=True,
|
|
202
|
+
)
|
|
203
|
+
@click.option(
|
|
204
|
+
"-ext",
|
|
205
|
+
"--extensions",
|
|
206
|
+
cls=ListOfStrings,
|
|
207
|
+
type=list[str],
|
|
208
|
+
default=[".md", ".ipynb"],
|
|
209
|
+
help="File extensions to filter the files.",
|
|
210
|
+
required=False,
|
|
211
|
+
)
|
|
212
|
+
@click.option(
|
|
213
|
+
"-td",
|
|
214
|
+
"--tracking-domains",
|
|
215
|
+
cls=ListOfStrings,
|
|
216
|
+
type=list[str],
|
|
217
|
+
default=["github.com", "microsoft.com", "visualstudio.com", "aka.ms", "azure.com"],
|
|
218
|
+
help="List of tracking domains to check.",
|
|
219
|
+
required=False,
|
|
220
|
+
)
|
|
221
|
+
@click.option(
|
|
222
|
+
"-sf",
|
|
223
|
+
"--skip-files",
|
|
224
|
+
cls=ListOfStrings,
|
|
225
|
+
type=list[str],
|
|
226
|
+
default=[
|
|
227
|
+
"CODE_OF_CONDUCT.md",
|
|
228
|
+
"SECURITY.md",
|
|
229
|
+
],
|
|
230
|
+
help="List of file names to skip check.",
|
|
231
|
+
required=False,
|
|
232
|
+
)
|
|
233
|
+
@click.option(
|
|
234
|
+
"-sd",
|
|
235
|
+
"--skip-domains",
|
|
236
|
+
cls=ListOfStrings,
|
|
237
|
+
type=list[str],
|
|
238
|
+
default=[],
|
|
239
|
+
help="List of domains to skip check.",
|
|
240
|
+
required=False,
|
|
241
|
+
)
|
|
242
|
+
@click.option(
|
|
243
|
+
"-suc",
|
|
244
|
+
"--skip-urls-containing",
|
|
245
|
+
cls=ListOfStrings,
|
|
246
|
+
type=list[str],
|
|
247
|
+
default=["https://www.microsoft.com/en-us/security/blog", "video-embed.html"],
|
|
248
|
+
help="List of urls to skip check.",
|
|
249
|
+
required=False,
|
|
250
|
+
)
|
|
251
|
+
@click.option(
|
|
252
|
+
"-gu",
|
|
253
|
+
"--guide-url",
|
|
254
|
+
type=str,
|
|
255
|
+
help="Full url of your contributing guide.",
|
|
256
|
+
required=False,
|
|
257
|
+
)
|
|
258
|
+
@click.option(
|
|
259
|
+
"-to",
|
|
260
|
+
"--timeout",
|
|
261
|
+
type=click.IntRange(0, 50),
|
|
262
|
+
default=10,
|
|
263
|
+
help="Timeout in seconds for the requests.",
|
|
264
|
+
required=False,
|
|
265
|
+
)
|
|
266
|
+
@click.option(
|
|
267
|
+
"-rt",
|
|
268
|
+
"--retries",
|
|
269
|
+
type=click.IntRange(0, 10),
|
|
270
|
+
default=3,
|
|
271
|
+
help="Number of retries for the requests.",
|
|
272
|
+
required=False,
|
|
273
|
+
)
|
|
274
|
+
@click.option(
|
|
275
|
+
"-o",
|
|
276
|
+
"--output-file-name",
|
|
277
|
+
type=str,
|
|
278
|
+
default="comment",
|
|
279
|
+
help="Name of the output file.",
|
|
280
|
+
required=False,
|
|
281
|
+
)
|
|
282
|
+
@click.argument(
|
|
283
|
+
"src",
|
|
284
|
+
nargs=-1,
|
|
285
|
+
type=click.Path(path_type=Path, exists=True, file_okay=True, dir_okay=True, readable=True),
|
|
286
|
+
is_eager=True,
|
|
287
|
+
metavar="SRC ...",
|
|
288
|
+
required=False,
|
|
289
|
+
)
|
|
290
|
+
@click.version_option(
|
|
291
|
+
message=(f"%(prog)s, %(version)s\n" f"Python ({platform.python_implementation()}) {platform.python_version()}"),
|
|
292
|
+
)
|
|
293
|
+
def main(
|
|
294
|
+
src: tuple[Path, ...],
|
|
295
|
+
dir: Path,
|
|
296
|
+
func: str,
|
|
297
|
+
guide_url: Union[str, None],
|
|
298
|
+
extensions: list[str],
|
|
299
|
+
skip_files: list[str],
|
|
300
|
+
skip_domains: list[str],
|
|
301
|
+
skip_urls_containing: list[str],
|
|
302
|
+
tracking_domains: list[str],
|
|
303
|
+
timeout: int,
|
|
304
|
+
retries: int,
|
|
305
|
+
output_file_name: str,
|
|
306
|
+
) -> None:
|
|
307
|
+
"""A markdown link validation reporting tool."""
|
|
308
|
+
_ = src or (Path("./"),) # default to current directory
|
|
309
|
+
_, files_paths = get_files_paths_list(dir, extensions)
|
|
310
|
+
|
|
311
|
+
# remove files from skip_files list
|
|
312
|
+
files_paths = [file_path for file_path in files_paths if file_path.name not in skip_files]
|
|
313
|
+
|
|
314
|
+
formatted_output = ""
|
|
315
|
+
all_files_issues: list[Union[MarkdownPath, MarkdownURL]] = []
|
|
316
|
+
links_checked_count = 0
|
|
317
|
+
github_ci = os.getenv("CI", "false")
|
|
318
|
+
|
|
319
|
+
# iterate over the files to validate the content
|
|
320
|
+
for file_path in files_paths:
|
|
321
|
+
detected_issues, links_count = detect_issues(
|
|
322
|
+
func=func,
|
|
323
|
+
file_path=file_path,
|
|
324
|
+
skip_urls_containing=skip_urls_containing,
|
|
325
|
+
skip_domains=skip_domains,
|
|
326
|
+
tracking_domains=tracking_domains,
|
|
327
|
+
timeout=timeout,
|
|
328
|
+
retries=retries,
|
|
329
|
+
)
|
|
330
|
+
links_checked_count += links_count
|
|
331
|
+
if len(detected_issues) > 0:
|
|
332
|
+
if github_ci == "true":
|
|
333
|
+
formatted_output += f"| `{file_path}` |" + format_links(detected_issues)
|
|
334
|
+
else:
|
|
335
|
+
formatted_output += f"| [`{file_path}`]({file_path}) |" + format_links(detected_issues)
|
|
336
|
+
all_files_issues.extend(detected_issues)
|
|
337
|
+
click.echo(
|
|
338
|
+
click.style(f"\n🔍 Checked {links_checked_count} links in {len(files_paths)} files.", fg="blue"), err=False
|
|
339
|
+
)
|
|
340
|
+
if len(all_files_issues) > 0:
|
|
341
|
+
formatted_output = "| File Full Path | Issues |\n|--------|--------|\n" + formatted_output
|
|
342
|
+
generator = MarkdownGenerator(contributing_guide_url=guide_url, output_file_name=output_file_name)
|
|
343
|
+
generator.generate(func, formatted_output)
|
|
344
|
+
click.echo(click.style(f"😭 Found {len(all_files_issues)} issues in the following files:", fg="red"), err=True)
|
|
345
|
+
for markdown_path in all_files_issues:
|
|
346
|
+
if github_ci == "true":
|
|
347
|
+
# Ref: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-a-warning-message
|
|
348
|
+
click.echo(
|
|
349
|
+
click.style(
|
|
350
|
+
f"::error file={markdown_path.file_path},line={markdown_path.line_number}::"
|
|
351
|
+
f"File {markdown_path.file_path}, line {markdown_path.line_number}, "
|
|
352
|
+
f"Link {markdown_path} {markdown_path.issue}.",
|
|
353
|
+
fg="red",
|
|
354
|
+
),
|
|
355
|
+
err=True,
|
|
356
|
+
)
|
|
357
|
+
else:
|
|
358
|
+
click.echo(
|
|
359
|
+
click.style(
|
|
360
|
+
f"\tFile '{markdown_path.file_path.resolve()}', line {markdown_path.line_number}"
|
|
361
|
+
f"\n{markdown_path} {markdown_path.issue}.\n",
|
|
362
|
+
fg="red",
|
|
363
|
+
),
|
|
364
|
+
err=True,
|
|
365
|
+
)
|
|
366
|
+
sys.exit(1)
|
|
367
|
+
click.echo(click.style("All files are compliant with the guidelines. 🎉", fg="green"), err=False)
|
|
368
|
+
sys.exit(0)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def main_with_spinner() -> None:
|
|
372
|
+
with spinner():
|
|
373
|
+
main()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class MarkdownLinkBase(ABC):
|
|
9
|
+
"""Base class for markdown links"""
|
|
10
|
+
|
|
11
|
+
link: str
|
|
12
|
+
line_number: int
|
|
13
|
+
file_path: Path
|
|
14
|
+
issue: str = ""
|
|
15
|
+
|
|
16
|
+
def has_locale(self) -> bool:
|
|
17
|
+
"""
|
|
18
|
+
Check if the link has a locale
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
True if the link has a locale, False otherwise
|
|
22
|
+
"""
|
|
23
|
+
locale_pattern = re.compile(r"\/[a-z]{2}-[a-z]{2}\/")
|
|
24
|
+
matches = re.findall(locale_pattern, self.link)
|
|
25
|
+
return bool(matches)
|
|
26
|
+
|
|
27
|
+
def has_tracking(self) -> bool:
|
|
28
|
+
"""
|
|
29
|
+
Check if the link has a tracking ID
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
True if the link has a tracking ID, False otherwise
|
|
33
|
+
"""
|
|
34
|
+
tracking_pattern = re.compile(r"(\?|\&)(WT|wt)\.mc_id=")
|
|
35
|
+
matches = re.findall(tracking_pattern, self.link)
|
|
36
|
+
return bool(matches)
|
|
37
|
+
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
return self.link
|
|
40
|
+
|
|
41
|
+
def __repr__(self) -> str:
|
|
42
|
+
return self.link
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from markdown_checker.markdown_link_base import MarkdownLinkBase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class MarkdownPath(MarkdownLinkBase):
|
|
11
|
+
"""Dataclass to store info about a path"""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def path_without_fragments(self) -> Path:
|
|
15
|
+
"""
|
|
16
|
+
Get the path without the fragment
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
The path without the fragment
|
|
20
|
+
"""
|
|
21
|
+
return Path(self.remove_fragments())
|
|
22
|
+
|
|
23
|
+
def remove_fragments(self) -> str:
|
|
24
|
+
"""
|
|
25
|
+
Remove the fragments from a path
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
The path without the fragment
|
|
29
|
+
"""
|
|
30
|
+
# Find the last occurrence of the dot
|
|
31
|
+
dot_index = self.link.rfind(".")
|
|
32
|
+
self.link = re.sub(r"(\?|\&)(WT|wt)\.mc_id=.*", "", self.link)
|
|
33
|
+
|
|
34
|
+
# If a dot is found, slice the string up to the end of the extension
|
|
35
|
+
if dot_index != -1:
|
|
36
|
+
# Find the next slash or fragment after the dot
|
|
37
|
+
slash_index = self.link.find("/", dot_index)
|
|
38
|
+
fragment_index = self.link.find("#", dot_index)
|
|
39
|
+
|
|
40
|
+
# Determine the earliest occurrence of either slash or fragment or query
|
|
41
|
+
end_index = min(
|
|
42
|
+
slash_index if slash_index != -1 else len(self.link),
|
|
43
|
+
fragment_index if fragment_index != -1 else len(self.link),
|
|
44
|
+
)
|
|
45
|
+
return self.link[:end_index]
|
|
46
|
+
else:
|
|
47
|
+
return self.link
|
|
48
|
+
|
|
49
|
+
def get_full_path(self) -> Path:
|
|
50
|
+
"""
|
|
51
|
+
Get the full path of the file by resolving the path without fragments
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The full path of the file
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
return self.path_without_fragments.resolve()
|
|
58
|
+
except (FileNotFoundError, RuntimeError):
|
|
59
|
+
return Path(self.get_full_path_relative())
|
|
60
|
+
|
|
61
|
+
def get_full_path_relative(self) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Get the full path of the file by resolving the path without fragments
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The full path of the file
|
|
67
|
+
"""
|
|
68
|
+
return os.path.normpath(os.path.join(os.path.dirname(self.file_path), self.remove_fragments()))
|
|
69
|
+
|
|
70
|
+
def exists(self) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Check if the path exists
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True if the path exists, False otherwise
|
|
76
|
+
"""
|
|
77
|
+
# Paths starting with / are considered absolute and not resolved
|
|
78
|
+
# so we need to remove the / and check if the path exists
|
|
79
|
+
changed = False
|
|
80
|
+
if self.link.startswith("/"):
|
|
81
|
+
self.link = self.link[1:]
|
|
82
|
+
changed = True
|
|
83
|
+
# Check if the path exists using the relative path resolution
|
|
84
|
+
if os.path.exists(self.get_full_path_relative()):
|
|
85
|
+
return True
|
|
86
|
+
# Check if the path exists using the full path resolution recursively
|
|
87
|
+
if self.get_full_path().exists():
|
|
88
|
+
return True
|
|
89
|
+
# Change the link back to the original value
|
|
90
|
+
if changed:
|
|
91
|
+
self.link = "/" + self.link
|
|
92
|
+
return False
|