Markdown-Checker 0.1.5__tar.gz → 0.2.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 (42) hide show
  1. {markdown_checker-0.1.5/src/markdown_checker.egg-info → markdown_checker-0.2.0}/PKG-INFO +23 -11
  2. {markdown_checker-0.1.5 → markdown_checker-0.2.0}/README.md +3 -1
  3. {markdown_checker-0.1.5 → markdown_checker-0.2.0}/pyproject.toml +37 -24
  4. markdown_checker-0.2.0/src/markdown_checker/__init__.py +342 -0
  5. markdown_checker-0.2.0/src/markdown_checker/markdown_link_base.py +42 -0
  6. markdown_checker-0.2.0/src/markdown_checker/paths.py +92 -0
  7. markdown_checker-0.2.0/src/markdown_checker/reports/generator_base.py +35 -0
  8. markdown_checker-0.2.0/src/markdown_checker/reports/md_reports/generator.py +67 -0
  9. markdown_checker-0.2.0/src/markdown_checker/reports/md_reports/templates/paths/broken.md +6 -0
  10. markdown_checker-0.2.0/src/markdown_checker/reports/md_reports/templates/paths/tracking.md +6 -0
  11. markdown_checker-0.2.0/src/markdown_checker/reports/md_reports/templates/urls/broken.md +5 -0
  12. markdown_checker-0.2.0/src/markdown_checker/reports/md_reports/templates/urls/locale.md +6 -0
  13. markdown_checker-0.2.0/src/markdown_checker/reports/md_reports/templates/urls/tracking.md +6 -0
  14. markdown_checker-0.2.0/src/markdown_checker/urls.py +52 -0
  15. markdown_checker-0.2.0/src/markdown_checker/utils/extract_links.py +61 -0
  16. markdown_checker-0.2.0/src/markdown_checker/utils/format_output.py +42 -0
  17. markdown_checker-0.2.0/src/markdown_checker/utils/list_files.py +36 -0
  18. markdown_checker-0.2.0/src/markdown_checker/utils/spinner.py +82 -0
  19. {markdown_checker-0.1.5 → markdown_checker-0.2.0/src/markdown_checker.egg-info}/PKG-INFO +23 -11
  20. markdown_checker-0.2.0/src/markdown_checker.egg-info/SOURCES.txt +29 -0
  21. markdown_checker-0.2.0/src/markdown_checker.egg-info/entry_points.txt +2 -0
  22. markdown_checker-0.2.0/src/markdown_checker.egg-info/requires.txt +20 -0
  23. markdown_checker-0.1.5/src/markdown_checker/check_markdown.py +0 -84
  24. markdown_checker-0.1.5/src/markdown_checker/generators/md_generator.py +0 -64
  25. markdown_checker-0.1.5/src/markdown_checker/inputs/input_arguments.py +0 -61
  26. markdown_checker-0.1.5/src/markdown_checker/links/__init__.py +0 -0
  27. markdown_checker-0.1.5/src/markdown_checker/links/link_operations.py +0 -138
  28. markdown_checker-0.1.5/src/markdown_checker/paths/__init__.py +0 -0
  29. markdown_checker-0.1.5/src/markdown_checker/paths/files_paths_reader.py +0 -45
  30. markdown_checker-0.1.5/src/markdown_checker/validate_markdown.py +0 -60
  31. markdown_checker-0.1.5/src/markdown_checker.egg-info/SOURCES.txt +0 -22
  32. markdown_checker-0.1.5/src/markdown_checker.egg-info/entry_points.txt +0 -2
  33. markdown_checker-0.1.5/src/markdown_checker.egg-info/requires.txt +0 -10
  34. {markdown_checker-0.1.5 → markdown_checker-0.2.0}/LICENSE +0 -0
  35. {markdown_checker-0.1.5 → markdown_checker-0.2.0}/setup.cfg +0 -0
  36. {markdown_checker-0.1.5 → markdown_checker-0.2.0}/src/markdown_checker/py.typed +0 -0
  37. {markdown_checker-0.1.5/src/markdown_checker → markdown_checker-0.2.0/src/markdown_checker/reports}/__init__.py +0 -0
  38. {markdown_checker-0.1.5/src/markdown_checker/generators → markdown_checker-0.2.0/src/markdown_checker/reports/md_reports}/__init__.py +0 -0
  39. {markdown_checker-0.1.5/src/markdown_checker/inputs → markdown_checker-0.2.0/src/markdown_checker/utils}/__init__.py +0 -0
  40. {markdown_checker-0.1.5 → markdown_checker-0.2.0}/src/markdown_checker.egg-info/dependency_links.txt +0 -0
  41. {markdown_checker-0.1.5 → markdown_checker-0.2.0}/src/markdown_checker.egg-info/top_level.txt +0 -0
  42. {markdown_checker-0.1.5 → markdown_checker-0.2.0}/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.5
4
- Summary: A markdown link validation reporting tool
3
+ Version: 0.2.0
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 :: 3
38
- Classifier: Programming Language :: Python :: 3.8
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
- Requires-Python: >=3.8
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
  [![PyPi](https://img.shields.io/pypi/v/markdown-checker)](https://pypi.org/project/markdown-checker/)
57
67
  [![Documentation Status](https://readthedocs.org/projects/markdown-checker/badge/?version=latest)](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 `comments.md` file.
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 `comments.md` file.
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.5"
3
+ description= "A markdown link validation reporting tool."
4
+ version = "0.2.0"
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.8"
10
+ requires-python = ">=3.9"
15
11
  dependencies = [
12
+ "click",
16
13
  "requests",
17
- "types-requests",
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 :: 3",
24
- "Programming Language :: Python :: 3.8",
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 = "py38"
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.ruff.lint]
58
- select = ["E", "F", "I", "UP"]
59
-
60
- [tool.black]
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.validate_markdown:main"
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,342 @@
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(url: MarkdownURL, skip_domains: list[str], skip_urls_containing: list[str], timeout: int, retries: int):
26
+ if any(url.host_name().lower() in domain.lower() for domain in skip_domains) or any(
27
+ url.link in substring for substring in skip_urls_containing
28
+ ):
29
+ return None
30
+ if not url.is_alive(timeout=timeout, retries=retries):
31
+ url.issue = "is broken"
32
+ return url
33
+ return None
34
+
35
+
36
+ def detect_issues(
37
+ func: str,
38
+ file_path: Path,
39
+ skip_urls_containing: list[str],
40
+ skip_domains: list[str],
41
+ tracking_domains: list[str],
42
+ timeout: int,
43
+ retries: int,
44
+ ) -> tuple[list[Union[MarkdownPath, MarkdownURL]], int]:
45
+ """
46
+ Function to detect issues in the markdown file based on the function
47
+
48
+ Args:
49
+ func (str): Function to be executed
50
+ file_path (Path): Path to the markdown file
51
+ skip_urls_containing (list[str]): List of urls to skip check
52
+ skip_domains (list[str]): List of domains to skip check
53
+ tracking_domains (list[str]): List of tracking domains to check
54
+ timeout (int): Timeout in seconds for the requests
55
+ retries (int): Number of retries for the requests
56
+
57
+ Returns:
58
+ Detected issues and links count.
59
+ """
60
+ detected_issues: list[Union[MarkdownPath, MarkdownURL]] = []
61
+ links_count = 0
62
+ # Step 1: Extract all Links from file path
63
+ all_links = get_links_from_md_file(file_path)
64
+ if len(all_links.paths) == 0 and len(all_links.urls) == 0:
65
+ return detected_issues, links_count
66
+ # Step 2: Check for issues based on the function
67
+ if func == "check_broken_paths":
68
+ for path in all_links.paths:
69
+ if not path.exists():
70
+ path.issue = "is broken"
71
+ detected_issues.append(path)
72
+ links_count += len(all_links.paths)
73
+ elif func == "check_broken_urls":
74
+ # currently these domains are known to have restrictions on the requests
75
+ skip_domains.extend(
76
+ [
77
+ "platform.openai.com",
78
+ "help.openai.com",
79
+ "beta.openai.com",
80
+ "marketplace.visualstudio.com",
81
+ "huggingface.co",
82
+ "en.wikipedia.org",
83
+ "twitter.com",
84
+ "www.linkedin.com",
85
+ "make.powerautomate.com",
86
+ "make.powerapps.com",
87
+ "www.midjourney.com",
88
+ "vscode.dev",
89
+ "rodtrent.substack.com",
90
+ "example.com",
91
+ "www.nuget.org",
92
+ "www.docker.com",
93
+ "build.nvidia.com",
94
+ "dotnet.microsoft.com",
95
+ "www.gemini.com",
96
+ "upload.wikimedia.org",
97
+ ]
98
+ )
99
+ with concurrent.futures.ProcessPoolExecutor() as executor:
100
+ results = list(
101
+ executor.map(
102
+ check_url,
103
+ all_links.urls,
104
+ [skip_domains] * len(all_links.urls),
105
+ [skip_urls_containing] * len(all_links.urls),
106
+ [timeout] * len(all_links.urls),
107
+ [retries] * len(all_links.urls),
108
+ )
109
+ )
110
+ detected_issues.extend(filter(None, results))
111
+ links_count += len(all_links.urls)
112
+ elif func == "check_urls_tracking":
113
+ for url in all_links.urls:
114
+ if any(url.host_name().lower() in domain.lower() for domain in skip_domains) or any(
115
+ url.link in substring for substring in skip_urls_containing
116
+ ):
117
+ continue
118
+ if any(url.host_name().lower() in domain.lower() for domain in tracking_domains) and not url.has_tracking():
119
+ url.issue = "is missing tracking id"
120
+ detected_issues.append(url)
121
+ links_count += len(all_links.urls)
122
+ elif func == "check_paths_tracking":
123
+ for path in all_links.paths:
124
+ if not path.has_tracking():
125
+ path.issue = "is missing tracking id"
126
+ detected_issues.append(path)
127
+ links_count += len(all_links.paths)
128
+ elif func == "check_urls_locale":
129
+ # if you remove locale from these domains, it will fail
130
+ skip_domains.extend(
131
+ [
132
+ "www.nvidia.com",
133
+ ]
134
+ )
135
+ for url in all_links.urls:
136
+ if any(url.host_name().lower() in domain.lower() for domain in skip_domains) or any(
137
+ url.link in substring for substring in skip_urls_containing
138
+ ):
139
+ continue
140
+ if url.has_locale():
141
+ url.issue = "has locale"
142
+ detected_issues.append(url)
143
+ links_count += len(all_links.urls)
144
+ elif func == "check_paths_locale":
145
+ for path in all_links.paths:
146
+ if path.has_locale():
147
+ path.issue = "has locale"
148
+ detected_issues.append(path)
149
+ links_count += len(all_links.paths)
150
+ return detected_issues, links_count
151
+
152
+
153
+ @click.command()
154
+ @click.option(
155
+ "-d",
156
+ "--dir",
157
+ type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True),
158
+ help="Path to the root directory to check.",
159
+ required=True,
160
+ )
161
+ @click.option(
162
+ "-ext",
163
+ "--extensions",
164
+ type=list[str],
165
+ default=[".md", ".ipynb"],
166
+ help="File extensions to filter the files.",
167
+ required=True,
168
+ )
169
+ @click.option(
170
+ "-td",
171
+ "--tracking-domains",
172
+ type=list[str],
173
+ default=["github.com", "microsoft.com", "visualstudio.com", "aka.ms", "azure.com"],
174
+ help="List of tracking domains to check.",
175
+ required=True,
176
+ )
177
+ @click.option(
178
+ "-sf",
179
+ "--skip-files",
180
+ type=list[str],
181
+ default=[
182
+ "CODE_OF_CONDUCT.md",
183
+ "SECURITY.md",
184
+ ],
185
+ help="List of file names to skip check.",
186
+ required=True,
187
+ )
188
+ @click.option(
189
+ "-sd",
190
+ "--skip-domains",
191
+ type=list[str],
192
+ default=[],
193
+ help="List of domains to skip check.",
194
+ required=False,
195
+ )
196
+ @click.option(
197
+ "-suc",
198
+ "--skip-urls-containing",
199
+ type=list[str],
200
+ default=["https://www.microsoft.com/en-us/security/blog", "video-embed.html"],
201
+ help="List of urls to skip check.",
202
+ required=False,
203
+ )
204
+ @click.option(
205
+ "-f",
206
+ "--func",
207
+ type=click.Choice(
208
+ [
209
+ "check_broken_paths",
210
+ "check_broken_urls",
211
+ "check_paths_tracking",
212
+ "check_urls_tracking",
213
+ "check_urls_locale",
214
+ ]
215
+ ),
216
+ help="Function to be executed.",
217
+ required=True,
218
+ )
219
+ @click.option(
220
+ "-gu",
221
+ "--guide-url",
222
+ type=str,
223
+ help="Full url of your contributing guide.",
224
+ required=True,
225
+ )
226
+ @click.option(
227
+ "-to",
228
+ "--timeout",
229
+ type=int,
230
+ default=10,
231
+ help="Timeout in seconds for the requests.",
232
+ required=False,
233
+ )
234
+ @click.option(
235
+ "-rt",
236
+ "--retries",
237
+ type=int,
238
+ default=3,
239
+ help="Number of retries for the requests.",
240
+ required=False,
241
+ )
242
+ @click.option(
243
+ "-o",
244
+ "--output-file-name",
245
+ type=str,
246
+ default="comment",
247
+ help="Name of the output file.",
248
+ required=True,
249
+ )
250
+ @click.argument(
251
+ "src",
252
+ nargs=-1,
253
+ type=click.Path(exists=True, file_okay=True, dir_okay=True, readable=True),
254
+ is_eager=True,
255
+ metavar="SRC ...",
256
+ required=False,
257
+ )
258
+ @click.version_option(
259
+ message=(f"%(prog)s, %(version)s\n" f"Python ({platform.python_implementation()}) {platform.python_version()}"),
260
+ )
261
+ def main(
262
+ src: tuple[str, ...],
263
+ dir: str,
264
+ func: str,
265
+ guide_url: str,
266
+ extensions: list[str],
267
+ skip_files: list[str],
268
+ skip_domains: list[str],
269
+ skip_urls_containing: list[str],
270
+ timeout: int,
271
+ retries: int,
272
+ tracking_domains: list[str],
273
+ output_file_name: str,
274
+ ) -> None:
275
+ """A markdown link validation reporting tool."""
276
+ _ = tuple(Path(item) for item in src) or (Path("./"),) # default to current directory
277
+
278
+ _, files_paths = get_files_paths_list(Path(dir), extensions)
279
+
280
+ # remove files from skip_files list
281
+ files_paths = [file_path for file_path in files_paths if file_path.name not in skip_files]
282
+
283
+ formatted_output = ""
284
+ all_files_issues: list[Union[MarkdownPath, MarkdownURL]] = []
285
+ links_checked_count = 0
286
+ github_ci = os.getenv("CI", "false")
287
+
288
+ # iterate over the files to validate the content
289
+ for file_path in files_paths:
290
+ detected_issues, links_count = detect_issues(
291
+ func=func,
292
+ file_path=file_path,
293
+ skip_urls_containing=skip_urls_containing,
294
+ skip_domains=skip_domains,
295
+ tracking_domains=tracking_domains,
296
+ timeout=timeout,
297
+ retries=retries,
298
+ )
299
+ links_checked_count += links_count
300
+ if len(detected_issues) > 0:
301
+ if github_ci == "true":
302
+ formatted_output += f"| `{file_path}` |" + format_links(detected_issues)
303
+ else:
304
+ formatted_output += f"| [`{file_path}`]({file_path}) |" + format_links(detected_issues)
305
+ all_files_issues.extend(detected_issues)
306
+ click.echo(
307
+ click.style(f"\n🔍 Checked {links_checked_count} links in {len(files_paths)} files.", fg="blue"), err=False
308
+ )
309
+ if len(all_files_issues) > 0:
310
+ formatted_output = "| File Full Path | Issues |\n|--------|--------|\n" + formatted_output
311
+ generator = MarkdownGenerator(contributing_guide_url=guide_url, output_file_name=output_file_name)
312
+ generator.generate(func, formatted_output)
313
+ click.echo(click.style(f"😭 Found {len(all_files_issues)} issues in the following files:", fg="red"), err=True)
314
+ for markdown_path in all_files_issues:
315
+ if github_ci == "true":
316
+ # Ref: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-a-warning-message
317
+ click.echo(
318
+ click.style(
319
+ f"::error file={markdown_path.file_path},line={markdown_path.line_number}::"
320
+ f"File {markdown_path.file_path}, line {markdown_path.line_number}, "
321
+ f"Link {markdown_path} {markdown_path.issue}.",
322
+ fg="red",
323
+ ),
324
+ err=True,
325
+ )
326
+ else:
327
+ click.echo(
328
+ click.style(
329
+ f"\tFile '{markdown_path.file_path.resolve()}', line {markdown_path.line_number}"
330
+ f"\n{markdown_path} {markdown_path.issue}.\n",
331
+ fg="red",
332
+ ),
333
+ err=True,
334
+ )
335
+ sys.exit(1)
336
+ click.echo(click.style("All files are compliant with the guidelines. 🎉", fg="green"), err=False)
337
+ sys.exit(0)
338
+
339
+
340
+ def main_with_spinner() -> None:
341
+ with spinner():
342
+ 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
@@ -0,0 +1,35 @@
1
+ from abc import ABC, abstractmethod
2
+ from pathlib import Path
3
+
4
+
5
+ class GeneratorBase(ABC):
6
+ """Base class for all report generators."""
7
+
8
+ current_dir: Path
9
+ check_broken_paths_template: str
10
+ check_broken_urls_template: str
11
+ check_paths_tracking_template: str
12
+ check_urls_tracking_template: str
13
+ check_urls_locale_template: str
14
+ templates: dict[str, str]
15
+
16
+ @abstractmethod
17
+ def _write_file(self, generated_text: str) -> None:
18
+ """Write the formatted output to a file."""
19
+ raise NotImplementedError
20
+
21
+ @abstractmethod
22
+ def _generate_text(self, function_name: str, formatted_output: str) -> str:
23
+ """
24
+ Generate text based on the formatted output, function name, and contributing guide URL.
25
+
26
+ Args:
27
+ formatted_output (str): The formatted output to be written to the file.
28
+ function_name (str): The name of the function to determine the header for the file.
29
+ """
30
+ raise NotImplementedError
31
+
32
+ @abstractmethod
33
+ def generate(self, function_name: str, formatted_output: str) -> None:
34
+ """Generate a report based on the formatted output."""
35
+ raise NotImplementedError