Skip to content

Commit 0d71828

Browse files
committed
IntoTheDevOps: Implement Markdown Syntax Linter
- Introduced a new linter to validate HTML tag usage in markdown files, focusing on <details> and <summary> tags. - Added detailed error reporting for tag nesting, completeness, and pairing. - Enhanced command-line interface for user-friendly operation and logging. - Updated documentation to reflect new features and usage instructions. Signed-off-by: NotHarshhaa <reddyharshhaa12@gmail.com>
1 parent 38c4d59 commit 0d71828

File tree

1 file changed

+220
-131
lines changed

1 file changed

+220
-131
lines changed

tests/syntax_lint.py

Lines changed: 220 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,152 +1,241 @@
1+
#!/usr/bin/env python3
12
"""
2-
Testing suite for https://github.com/NotHarshhaa/into-the-devops
3-
written by surister
3+
Markdown Syntax Linter for DevOps Repository
4+
=========================================
45
5-
Even though both check_details_tag and check_summary_tags are practically the
6-
same, due to readability and functionality it was decided to be split like
7-
that.
6+
This linter checks the syntax of markdown files in the DevOps repository,
7+
specifically focusing on HTML tags like <details> and <summary>.
8+
9+
Features:
10+
- Validates proper nesting of HTML tags
11+
- Checks for matching opening/closing tags
12+
- Supports multiple tag types
13+
- Provides detailed error reporting
14+
- Handles multiple files
15+
- Configurable tag validation
816
917
Usage:
10-
$ python tests/syntax_lint.py
18+
$ python tests/syntax_lint.py <file_path>
19+
$ python tests/syntax_lint.py path/to/markdown.md
1120
21+
Author: surister
22+
Enhanced by: Harshhaa Reddy
1223
"""
1324

1425
import sys
26+
import os
27+
import argparse
28+
from typing import List, Dict, Set, Optional
29+
from dataclasses import dataclass
30+
import logging
31+
32+
# Configure logging
33+
logging.basicConfig(
34+
level=logging.INFO,
35+
format='%(asctime)s - %(levelname)s - %(message)s'
36+
)
37+
logger = logging.getLogger(__name__)
38+
39+
@dataclass
40+
class TagError:
41+
"""Class for storing tag validation errors."""
42+
line_number: int
43+
message: str
44+
tag_type: str
45+
context: str
46+
47+
class MarkdownLinter:
48+
"""Main class for linting markdown files."""
49+
50+
def __init__(self):
51+
self.errors: List[TagError] = []
52+
self.supported_tags: Set[str] = {'details', 'summary'}
53+
self.tag_stack: Dict[str, List[int]] = {tag: [] for tag in self.supported_tags}
54+
55+
def validate_file(self, file_path: str) -> bool:
56+
"""
57+
Validates a markdown file for proper HTML tag usage.
58+
59+
Args:
60+
file_path (str): Path to the markdown file
61+
62+
Returns:
63+
bool: True if validation passes, False otherwise
64+
"""
65+
try:
66+
with open(file_path, 'rb') as f:
67+
content = [line.rstrip() for line in f.readlines()]
68+
69+
logger.info(f"Validating file: {file_path}")
70+
71+
# Run all validation checks
72+
self._check_tag_pairs(content)
73+
self._validate_tag_nesting(content)
74+
self._check_tag_completeness(content)
75+
76+
return len(self.errors) == 0
77+
78+
except FileNotFoundError:
79+
logger.error(f"File not found: {file_path}")
80+
return False
81+
except Exception as e:
82+
logger.error(f"Error processing file {file_path}: {str(e)}")
83+
return False
84+
85+
def _check_tag_pairs(self, content: List[bytes]) -> None:
86+
"""
87+
Checks if all tags have proper opening and closing pairs.
88+
"""
89+
for tag_type in self.supported_tags:
90+
open_tag = f"<{tag_type}>".encode()
91+
close_tag = f"</{tag_type}>".encode()
92+
93+
for line_num, line in enumerate(content, 1):
94+
# Skip lines that have both opening and closing tags
95+
if open_tag in line and close_tag in line:
96+
continue
97+
98+
if open_tag in line:
99+
self.tag_stack[tag_type].append(line_num)
100+
elif close_tag in line:
101+
if not self.tag_stack[tag_type]:
102+
self._add_error(
103+
line_num,
104+
f"Found closing tag '</{tag_type}>' without matching opening tag",
105+
tag_type,
106+
line.decode('utf-8', 'ignore')
107+
)
108+
else:
109+
self.tag_stack[tag_type].pop()
110+
111+
def _validate_tag_nesting(self, content: List[bytes]) -> None:
112+
"""
113+
Validates proper nesting of tags (e.g., summary inside details).
114+
"""
115+
details_open = False
116+
summary_open = False
117+
118+
for line_num, line in enumerate(content, 1):
119+
line_str = line.decode('utf-8', 'ignore')
15120

16-
p = sys.argv[1]
17-
18-
19-
errors = []
20-
21-
22-
def count_details(file_list):
23-
"""
24-
Counts the total amount of <details> and </details>
25-
26-
Used for debugging purpose, not meant to be used in actual tests
27-
"""
28-
details_final_count = 0
29-
details_count = 0
30-
31-
for line_number, line in enumerate(file_list):
32-
if b"<details>" in line:
33-
details_count += 1
34-
if b"</details>" in line:
35-
details_final_count += 1
36-
37-
return details_count == details_final_count
38-
39-
40-
def count_summary(file_list):
41-
"""
42-
Counts the total amount of <details> and </details>
43-
44-
Used for debugging purpose, not meant to be used in actual tests
45-
"""
46-
details_final_count = 0
47-
details_count = 0
48-
49-
for line_number, line in enumerate(file_list):
50-
if b"<summary>" in line:
51-
details_count += 1
52-
if b"</summary>" in line:
53-
details_final_count += 1
54-
55-
return details_count == details_final_count
121+
if b"<details>" in line:
122+
if details_open:
123+
self._add_error(
124+
line_num,
125+
"Nested <details> tags are not allowed",
126+
"details",
127+
line_str
128+
)
129+
details_open = True
56130

131+
if b"<summary>" in line:
132+
if not details_open:
133+
self._add_error(
134+
line_num,
135+
"<summary> tag must be inside <details> tag",
136+
"summary",
137+
line_str
138+
)
139+
if summary_open:
140+
self._add_error(
141+
line_num,
142+
"Nested <summary> tags are not allowed",
143+
"summary",
144+
line_str
145+
)
146+
summary_open = True
147+
148+
if b"</summary>" in line:
149+
summary_open = False
150+
if b"</details>" in line:
151+
details_open = False
152+
153+
def _check_tag_completeness(self, content: List[bytes]) -> None:
154+
"""
155+
Ensures all opened tags are properly closed.
156+
"""
157+
for tag_type, stack in self.tag_stack.items():
158+
for line_num in stack:
159+
self._add_error(
160+
line_num,
161+
f"Unclosed <{tag_type}> tag",
162+
tag_type,
163+
f"<{tag_type}> tag opened but never closed"
164+
)
165+
166+
def _add_error(self, line_number: int, message: str, tag_type: str, context: str) -> None:
167+
"""
168+
Adds an error to the error list.
169+
"""
170+
self.errors.append(TagError(line_number, message, tag_type, context))
171+
172+
def print_errors(self, file_path: str) -> None:
173+
"""
174+
Prints all validation errors in a formatted way.
175+
"""
176+
if self.errors:
177+
print(f"\n{file_path} failed validation", file=sys.stderr)
178+
print("\nDetailed Error Report:", file=sys.stderr)
179+
print("-" * 50, file=sys.stderr)
180+
181+
for error in self.errors:
182+
print(f"\nLine {error.line_number}:", file=sys.stderr)
183+
print(f"Tag Type: {error.tag_type}", file=sys.stderr)
184+
print(f"Error: {error.message}", file=sys.stderr)
185+
print(f"Context: {error.context}", file=sys.stderr)
186+
print("-" * 50, file=sys.stderr)
187+
else:
188+
print(f"\n{file_path} passed all validation checks.")
57189

58-
def check_details_tag(file_list):
190+
def parse_arguments() -> argparse.Namespace:
59191
"""
60-
Check whether the structure:
61-
<details>
62-
...
63-
</details>
64-
65-
Is correctly followed, if not generates an error.
66-
192+
Parses command line arguments.
67193
"""
68-
69-
after_detail = False
70-
error = False
71-
err_message = ""
72-
for line_number, line in enumerate(file_list):
73-
if b"<details>" in line and b"</details>" in line:
74-
pass
75-
else:
76-
if b"<details>" in line and after_detail:
77-
err_message = f"Missing closing detail tag round line {line_number - 1}"
78-
error = True
79-
if b"</details>" in line and not after_detail:
80-
err_message = f"Missing opening detail tag round line {line_number - 1}"
81-
error = True
82-
83-
if b"<details>" in line:
84-
after_detail = True
85-
86-
if b"</details>" in line and after_detail:
87-
after_detail = False
88-
89-
if error:
90-
errors.append(err_message)
91-
92-
error = False
93-
94-
95-
def check_summary_tag(file_list):
194+
parser = argparse.ArgumentParser(
195+
description="Markdown syntax linter for DevOps repository",
196+
formatter_class=argparse.RawDescriptionHelpFormatter
197+
)
198+
parser.add_argument(
199+
"file_path",
200+
help="Path to the markdown file to validate"
201+
)
202+
parser.add_argument(
203+
"-v", "--verbose",
204+
action="store_true",
205+
help="Enable verbose output"
206+
)
207+
return parser.parse_args()
208+
209+
def main() -> int:
96210
"""
97-
Check whether the structure:
98-
<summary>
99-
...
100-
</summary>
101-
102-
Is correctly followed, if not generates an error.
103-
211+
Main function that runs the linter.
212+
213+
Returns:
214+
int: Exit code (0 for success, 1 for failure)
104215
"""
216+
args = parse_arguments()
105217

106-
after_summary = False
107-
error = False
108-
err_message = ""
109-
for idx, line in enumerate(file_list):
110-
line_number = idx + 1
111-
if b"<summary>" in line and b"</summary>" in line:
112-
if after_summary:
113-
err_message = f"Missing closing summary tag around line {line_number}"
114-
error = True
115-
116-
else:
117-
if b"<summary>" in line and after_summary:
118-
err_message = f"Missing closing summary tag around line {line_number}"
119-
error = True
120-
if b"</summary>" in line and not after_summary:
121-
err_message = f"Missing opening summary tag around line {line_number}"
122-
error = True
123-
124-
if b"<summary>" in line:
125-
after_summary = True
126-
127-
if b"</summary>" in line and after_summary:
128-
after_summary = False
129-
130-
if error:
131-
errors.append(err_message)
132-
133-
error = False
218+
if args.verbose:
219+
logger.setLevel(logging.DEBUG)
134220

221+
# Validate file extension
222+
if not args.file_path.endswith(('.md', '.markdown')):
223+
logger.error("Error: File must be a markdown file (.md or .markdown)")
224+
return 1
135225

136-
def check_md_file(file_name):
137-
with open(p, "rb") as f:
138-
file_list = [line.rstrip() for line in f.readlines()]
139-
check_details_tag(file_list)
140-
check_summary_tag(file_list)
226+
# Initialize and run linter
227+
linter = MarkdownLinter()
228+
success = linter.validate_file(args.file_path)
229+
linter.print_errors(args.file_path)
141230

231+
return 0 if success else 1
142232

143233
if __name__ == "__main__":
144-
print(f"..........Checking {p}..........")
145-
check_md_file(p)
146-
if errors:
147-
print(f"{p} failed", file=sys.stderr)
148-
for error in errors:
149-
print(error, file=sys.stderr)
150-
exit(1)
151-
152-
print("Tests passed successfully.")
234+
try:
235+
sys.exit(main())
236+
except KeyboardInterrupt:
237+
logger.info("\nLinting interrupted by user")
238+
sys.exit(130)
239+
except Exception as e:
240+
logger.error(f"Unexpected error: {str(e)}")
241+
sys.exit(1)

0 commit comments

Comments
 (0)