diff --git a/.coverage b/.coverage index 1805f9e..9210b7f 100644 Binary files a/.coverage and b/.coverage differ diff --git a/README.adoc b/README.adoc index 4ebe6fd..a4d3817 100755 --- a/README.adoc +++ b/README.adoc @@ -8,15 +8,15 @@ image:https://img.shields.io/badge/license-MIT-blue.svg[License: MIT, link=https://opensource.org/licenses/MIT] image:https://img.shields.io/badge/python-3.8+-blue.svg[Python Version] -A Python-based linter for AsciiDoc files that helps maintain consistent documentation quality and style. +A Python-based linter for AsciiDoc files that helps maintain consistent documentation quality and style. Part of the docToolchain project. == About -AsciiDoc Linter is a command-line tool that checks your AsciiDoc files for common issues and style violations. It helps maintain consistent documentation by enforcing rules for heading structure, formatting, and more. +AsciiDoc Linter is a command-line tool that checks your AsciiDoc files for common issues and style violations. It helps maintain consistent documentation by enforcing rules for heading structure, formatting, whitespace, and image usage. [NOTE] ==== -This project was developed with the assistance of an AI language model (GPT-4) as part of an experiment in AI-assisted development. The AI helped design the architecture, implement the code, and create the documentation. +This project is part of docToolchain (https://doctoolchain.org), a collection of documentation tools and best practices. ==== == Features @@ -28,7 +28,7 @@ This project was developed with the assistance of an AI language model (GPT-4) a |Rule ID |Description |Severity |HEAD001 -|Check for proper heading incrementation (no skipping levels) +|Check for proper heading hierarchy (no skipping levels) |ERROR |HEAD002 @@ -46,13 +46,19 @@ This project was developed with the assistance of an AI language model (GPT-4) a |BLOCK002 |Verify proper spacing around blocks |WARNING + +|WS001 +|Check whitespace usage (blank lines, list markers, tabs) +|WARNING + +|IMG001 +|Verify image attributes and file references +|WARNING/ERROR |=== === Planned Rules -* WS001: Blank lines around headers and blocks * TABLE001: Table formatting consistency -* IMG001: Image alt text presence * LINK001: Broken internal references * FMT001: Markdown-compatible styles detection @@ -61,7 +67,7 @@ This project was developed with the assistance of an AI language model (GPT-4) a [source,bash] ---- # Clone the repository -git clone https://github.com/yourusername/asciidoc-linter.git +git clone https://github.com/docToolchain/asciidoc-linter.git # Navigate to the project directory cd asciidoc-linter @@ -114,6 +120,9 @@ WARNING: Block should be preceded by a blank line (line 67) ERROR: Multiple top-level headings found (line 30) First heading at line 1: 'Document Title' + +WARNING: Missing alt text for image: diagram.png (line 80) + image::diagram.png[] ---- == Development @@ -137,15 +146,21 @@ asciidoc-linter/ ├── asciidoc_linter/ │ ├── __init__.py │ ├── cli.py -│ ├── rules.py -│ ├── heading_rules.py -│ ├── block_rules.py +│ ├── rules/ +│ │ ├── __init__.py +│ │ ├── base.py +│ │ ├── heading_rules.py +│ │ ├── block_rules.py +│ │ ├── whitespace_rules.py +│ │ └── image_rules.py │ ├── parser.py │ └── reporter.py ├── tests/ │ └── rules/ │ ├── test_heading_rules.py -│ └── test_block_rules.py +│ ├── test_block_rules.py +│ ├── test_whitespace_rules.py +│ └── test_image_rules.py ├── docs/ │ ├── requirements.adoc │ └── block_rules.adoc @@ -170,7 +185,7 @@ This project is licensed under the MIT License - see the LICENSE file for detail == Acknowledgments -* This project was developed with the assistance of GPT-4, demonstrating the potential of AI-assisted development +* Part of the docToolchain project (https://doctoolchain.org) * Inspired by various linting tools and the need for better AsciiDoc quality control * Thanks to the AsciiDoc community for their excellent documentation and tools @@ -179,12 +194,14 @@ This project is licensed under the MIT License - see the LICENSE file for detail 1. Phase 1 (Current) * ✅ Basic heading rules * ✅ Block structure rules +* ✅ Whitespace rules +* ✅ Image validation * ⏳ Configuration system 2. Phase 2 * 🔲 Table validation * 🔲 Link checking -* 🔲 Image validation +* 🔲 Format rules 3. Phase 3 * 🔲 IDE integration @@ -193,5 +210,6 @@ This project is licensed under the MIT License - see the LICENSE file for detail == Contact -* Project Homepage: https://github.com/yourusername/asciidoc-linter -* Issue Tracker: https://github.com/yourusername/asciidoc-linter/issues \ No newline at end of file +* Project Homepage: https://github.com/docToolchain/asciidoc-linter +* Issue Tracker: https://github.com/docToolchain/asciidoc-linter/issues +* docToolchain Homepage: https://doctoolchain.org diff --git a/README.adoc.meta b/README.adoc.meta index 78e4c35..c881a2f 100755 --- a/README.adoc.meta +++ b/README.adoc.meta @@ -1 +1 @@ -Main documentation file for the AsciiDoc Linter project \ No newline at end of file +Updated README with current implementation status \ No newline at end of file diff --git a/asciidoc_linter/heading_rules.py b/asciidoc_linter/heading_rules.py deleted file mode 100755 index 83d2a79..0000000 --- a/asciidoc_linter/heading_rules.py +++ /dev/null @@ -1,181 +0,0 @@ -# heading_rules.py - Implementation of heading rules -""" -This module contains rules for checking AsciiDoc heading structure and format. -""" - -from typing import List, Optional, Tuple -from .rules import Rule, Finding, Severity, Position - -class HeadingFormatRule(Rule): - """ - HEAD002: Check heading format. - Ensures that headings follow AsciiDoc conventions: - - Space after = characters - - Proper capitalization - """ - - def __init__(self): - super().__init__() - self.id = "HEAD002" - - @property - def description(self) -> str: - return "Ensures proper heading format (spacing and capitalization)" - - def check(self, content: str) -> List[Finding]: - findings = [] - - for line_num, line in enumerate(content.splitlines(), 1): - # Skip empty lines and non-heading lines - if not line or not line.startswith('='): - continue - - # Skip lines that look like header underlines (====) - if line.strip() == '=' * len(line.strip()): - continue - - # Count leading = characters - level = 0 - for char in line: - if char != '=': - break - level += 1 - - # Get the heading text, handling both cases: - # 1. No space after = (=Title) - # 2. Space after = (= Title) - heading_text = line[level:].strip() - - # Check for space after = characters - if level > 0 and (len(line) <= level or line[level] != ' '): - findings.append(Finding( - rule_id=self.id, - message=f"Missing space after {'=' * level}", - severity=Severity.ERROR, - position=Position(line=line_num), - context=line.strip() - )) - - # Check if heading starts with lowercase (only if we have text) - if heading_text: - # Split into words and check first word - words = heading_text.split() - if words and words[0][0].islower(): - findings.append(Finding( - rule_id=self.id, - message="Heading should start with uppercase letter", - severity=Severity.WARNING, - position=Position(line=line_num), - context=line.strip() - )) - - return findings - -class HeadingIncrementationRule(Rule): - """ - HEAD001: Check for proper heading incrementation. - Ensures that heading levels are not skipped (e.g., h1 -> h3). - """ - - def __init__(self): - super().__init__() - self.id = "HEAD001" - - @property - def description(self) -> str: - return "Ensures proper heading level incrementation (no skipped levels)" - - def check(self, content: str) -> List[Finding]: - findings = [] - current_level = 0 - last_heading_line = 0 - - for line_num, line in enumerate(content.splitlines(), 1): - # Skip empty lines and non-heading lines - if not line or not line.startswith('='): - continue - - # Count the number of '=' at the start of the line - level = 0 - for char in line: - if char != '=': - break - level += 1 - - # Skip lines that look like header underlines (====) - if level == len(line.strip()): - continue - - # First heading can be any level - if current_level == 0: - current_level = level - last_heading_line = line_num - continue - - # Check if we're skipping levels - if level > current_level + 1: - findings.append(Finding( - rule_id=self.id, - message=f"Heading level skipped: found h{level} after h{current_level}", - severity=Severity.ERROR, - position=Position(line=line_num), - context=line.strip() - )) - - current_level = level - last_heading_line = line_num - - return findings - -class MultipleTopLevelHeadingsRule(Rule): - """ - HEAD003: Check for multiple top-level headings. - Ensures that a document has only one top-level (=) heading. - """ - - def __init__(self): - super().__init__() - self.id = "HEAD003" - - @property - def description(self) -> str: - return "Ensures document has only one top-level heading" - - def check(self, content: str) -> List[Finding]: - findings = [] - first_top_level: Optional[Tuple[int, str]] = None # (line_number, heading_text) - - for line_num, line in enumerate(content.splitlines(), 1): - # Skip empty lines and non-heading lines - if not line or not line.startswith('='): - continue - - # Skip lines that look like header underlines (====) - if line.strip() == '=' * len(line.strip()): - continue - - # Count leading = characters - level = 0 - for char in line: - if char != '=': - break - level += 1 - - # Check for top-level headings (single =) - if level == 1: - heading_text = line[level:].strip() - - if first_top_level is None: - # Remember first top-level heading - first_top_level = (line_num, heading_text) - else: - # Found another top-level heading - findings.append(Finding( - rule_id=self.id, - message=f"Multiple top-level headings found. First heading at line {first_top_level[0]}: '{first_top_level[1]}'", - severity=Severity.ERROR, - position=Position(line=line_num), - context=line.strip() - )) - - return findings \ No newline at end of file diff --git a/asciidoc_linter/heading_rules.py.meta b/asciidoc_linter/heading_rules.py.meta index f771c8c..480ba30 100755 --- a/asciidoc_linter/heading_rules.py.meta +++ b/asciidoc_linter/heading_rules.py.meta @@ -1 +1 @@ -Updated implementation with fixed format checking \ No newline at end of file +Implementation of heading rules with corrected imports \ No newline at end of file diff --git a/asciidoc_linter/rules/__init__.py b/asciidoc_linter/rules/__init__.py index 9b2c5e5..6225d14 100755 --- a/asciidoc_linter/rules/__init__.py +++ b/asciidoc_linter/rules/__init__.py @@ -1,8 +1,10 @@ # __init__.py - Rules package initialization -from .base_rules import Rule, Finding, Severity, Position +from .base import Rule, Finding, Severity, Position from .heading_rules import HeadingHierarchyRule, HeadingFormatRule from .block_rules import UnterminatedBlockRule, BlockSpacingRule +from .whitespace_rules import WhitespaceRule +from .image_rules import ImageAttributesRule __all__ = [ 'Rule', @@ -12,5 +14,7 @@ 'HeadingHierarchyRule', 'HeadingFormatRule', 'UnterminatedBlockRule', - 'BlockSpacingRule' + 'BlockSpacingRule', + 'WhitespaceRule', + 'ImageAttributesRule' ] \ No newline at end of file diff --git a/asciidoc_linter/rules/__init__.py.meta b/asciidoc_linter/rules/__init__.py.meta index a1d819d..9565202 100755 --- a/asciidoc_linter/rules/__init__.py.meta +++ b/asciidoc_linter/rules/__init__.py.meta @@ -1 +1 @@ -Updated rules package initialization to include block rules \ No newline at end of file +Rules package initialization with Position class \ No newline at end of file diff --git a/asciidoc_linter/rules/base.py b/asciidoc_linter/rules/base.py index c321e57..8a1e350 100755 --- a/asciidoc_linter/rules/base.py +++ b/asciidoc_linter/rules/base.py @@ -12,16 +12,32 @@ class Severity(Enum): WARNING = "WARNING" ERROR = "ERROR" +class Position: + """Represents a position in a text file""" + def __init__(self, line: int, column: Optional[int] = None): + self.line = line + self.column = column + + def __str__(self): + if self.column is not None: + return f"line {self.line}, column {self.column}" + return f"line {self.line}" + class Finding: """Represents a rule violation finding""" - def __init__(self, rule_id: str, line_number: int, message: str, + def __init__(self, rule_id: str, position: Position, message: str, severity: Severity, context: Optional[str] = None): self.rule_id = rule_id - self.line_number = line_number + self.position = position self.message = message self.severity = severity self.context = context + @property + def line_number(self) -> int: + """Backward compatibility for line number access""" + return self.position.line + class Rule: """Base class for all rules""" id: str = "" diff --git a/asciidoc_linter/rules/base.py.meta b/asciidoc_linter/rules/base.py.meta index 9bce605..d48b74b 100755 --- a/asciidoc_linter/rules/base.py.meta +++ b/asciidoc_linter/rules/base.py.meta @@ -1 +1 @@ -Base functionality for rules \ No newline at end of file +Base functionality for rules including Position class \ No newline at end of file diff --git a/asciidoc_linter/rules/block_rules.py b/asciidoc_linter/rules/block_rules.py index c6259f0..6e111a2 100644 --- a/asciidoc_linter/rules/block_rules.py +++ b/asciidoc_linter/rules/block_rules.py @@ -1,7 +1,7 @@ # block_rules.py - Rules for checking AsciiDoc blocks from typing import List, Dict -from .base import Rule, Finding, Severity +from .base import Rule, Finding, Severity, Position class UnterminatedBlockRule(Rule): """Rule to check for unterminated blocks in AsciiDoc files.""" @@ -49,7 +49,7 @@ def check_line(self, line: str, line_number: int, context: List[str]) -> List[Fi findings.append( Finding( rule_id=self.id, - line_number=line_number + 1, + position=Position(line=line_number + 1), message=f"Unterminated {self.block_markers[stripped_line]} starting", severity=self.severity, context=line @@ -87,7 +87,7 @@ def check_line(self, line: str, line_number: int, context: List[str]) -> List[Fi findings.append( Finding( rule_id=self.id, - line_number=line_number + 2, + position=Position(line=line_number + 2), message="Block should be followed by a blank line", severity=self.severity, context=context[line_number + 1] @@ -102,7 +102,7 @@ def check_line(self, line: str, line_number: int, context: List[str]) -> List[Fi findings.append( Finding( rule_id=self.id, - line_number=line_number + 1, + position=Position(line=line_number + 1), message="Block should be preceded by a blank line", severity=self.severity, context=line diff --git a/asciidoc_linter/rules/block_rules.py.meta b/asciidoc_linter/rules/block_rules.py.meta index 9f7aae3..5717304 100644 --- a/asciidoc_linter/rules/block_rules.py.meta +++ b/asciidoc_linter/rules/block_rules.py.meta @@ -1 +1 @@ -Rules for checking AsciiDoc blocks \ No newline at end of file +Updated block rules to use Position instead of line_number \ No newline at end of file diff --git a/asciidoc_linter/rules/heading_rules.py b/asciidoc_linter/rules/heading_rules.py index 7d427ba..25c2744 100755 --- a/asciidoc_linter/rules/heading_rules.py +++ b/asciidoc_linter/rules/heading_rules.py @@ -1,132 +1,181 @@ -# heading_rules.py - Rules for checking AsciiDoc headings +# heading_rules.py - Implementation of heading rules +""" +This module contains rules for checking AsciiDoc heading structure and format. +""" -import re -from typing import List, Dict, Any -from .base_rules import Rule, Finding, Severity, Position - -class HeadingHierarchyRule(Rule): - """Rule to check if heading levels are properly nested""" - rule_id = "HDR001" - - def check(self, content: str) -> List[Finding]: - findings = [] - current_level = 0 - for line_num, line in enumerate(content.split('\n'), 1): - if line.startswith('='): - level = len(re.match(r'^=+', line).group()) - if level > current_level + 1: - findings.append(Finding( - message=f"Heading level skipped. Found level {level} after level {current_level}", - severity=Severity.ERROR, - position=Position(line=line_num), - rule_id=self.rule_id, - context={"current_level": current_level, "found_level": level} - )) - current_level = level - return findings +from typing import List, Optional, Tuple +from .base import Rule, Finding, Severity, Position class HeadingFormatRule(Rule): - """Rule to check if headings follow the correct format""" - rule_id = "HDR002" + """ + HEAD002: Check heading format. + Ensures that headings follow AsciiDoc conventions: + - Space after = characters + - Proper capitalization + """ + + def __init__(self): + super().__init__() + self.id = "HEAD002" + + @property + def description(self) -> str: + return "Ensures proper heading format (spacing and capitalization)" def check(self, content: str) -> List[Finding]: findings = [] - for line_num, line in enumerate(content.split('\n'), 1): - if line.startswith('='): - # Get the heading level and remaining text - equals_part = re.match(r'^=+', line).group() - remaining_text = line[len(equals_part):] - - context = { - "line": line, - "level": len(equals_part), - "text": remaining_text.strip() - } - - # Check 1: Space after equals signs - if not remaining_text.startswith(' '): + + for line_num, line in enumerate(content.splitlines(), 1): + # Skip empty lines and non-heading lines + if not line or not line.startswith('='): + continue + + # Skip lines that look like header underlines (====) + if line.strip() == '=' * len(line.strip()): + continue + + # Count leading = characters + level = 0 + for char in line: + if char != '=': + break + level += 1 + + # Get the heading text, handling both cases: + # 1. No space after = (=Title) + # 2. Space after = (= Title) + heading_text = line[level:].strip() + + # Check for space after = characters + if level > 0 and (len(line) <= level or line[level] != ' '): + findings.append(Finding( + rule_id=self.id, + message=f"Missing space after {'=' * level}", + severity=Severity.ERROR, + position=Position(line=line_num), + context=line.strip() + )) + + # Check if heading starts with lowercase (only if we have text) + if heading_text: + # Split into words and check first word + words = heading_text.split() + if words and words[0][0].islower(): findings.append(Finding( - message=f"No space after {'=' * len(equals_part)} signs", - severity=Severity.ERROR, + rule_id=self.id, + message="Heading should start with uppercase letter", + severity=Severity.WARNING, position=Position(line=line_num), - rule_id=self.rule_id, - context=context - )) - - # Check 2: Capitalization of heading text - text = remaining_text.strip() - if text and not text[0].isupper(): - findings.append(Finding( - message="Heading text must start with capital letter", - severity=Severity.ERROR, - position=Position(line=line_num), - rule_id=self.rule_id, - context=context + context=line.strip() )) return findings -class HeadingIncrementationRule(Rule): - """Rule to check if heading levels are incremented properly""" - rule_id = "HDR003" +class HeadingHierarchyRule(Rule): + """ + HEAD001: Check for proper heading incrementation. + Ensures that heading levels are not skipped (e.g., h1 -> h3). + """ + + def __init__(self): + super().__init__() + self.id = "HEAD001" + + @property + def description(self) -> str: + return "Ensures proper heading level incrementation (no skipped levels)" def check(self, content: str) -> List[Finding]: findings = [] current_level = 0 - lines = content.split('\n') + last_heading_line = 0 - for line_num, line in enumerate(lines, 1): - if line.startswith('='): - level = len(re.match(r'^=+', line).group()) + for line_num, line in enumerate(content.splitlines(), 1): + # Skip empty lines and non-heading lines + if not line or not line.startswith('='): + continue + + # Count the number of '=' at the start of the line + level = 0 + for char in line: + if char != '=': + break + level += 1 + + # Skip lines that look like header underlines (====) + if level == len(line.strip()): + continue - # Skip if it's a heading underline - if line_num > 1 and lines[line_num - 2].strip() and not lines[line_num - 2].startswith('='): - continue - - if level > current_level + 1: - findings.append(Finding( - message=f"Heading level incremented by more than one. Found level {level} after level {current_level}", - severity=Severity.ERROR, - position=Position(line=line_num), - rule_id=self.rule_id, - context={ - "current_level": current_level, - "found_level": level, - "line": line - } - )) + # First heading can be any level + if current_level == 0: current_level = level - + last_heading_line = line_num + continue + + # Check if we're skipping levels + if level > current_level + 1: + findings.append(Finding( + rule_id=self.id, + message=f"Heading level skipped: found h{level} after h{current_level}", + severity=Severity.ERROR, + position=Position(line=line_num), + context=line.strip() + )) + + current_level = level + last_heading_line = line_num + return findings class MultipleTopLevelHeadingsRule(Rule): - """Rule to check if there are multiple top-level headings""" - rule_id = "HDR004" + """ + HEAD003: Check for multiple top-level headings. + Ensures that a document has only one top-level (=) heading. + """ + + def __init__(self): + super().__init__() + self.id = "HEAD003" + + @property + def description(self) -> str: + return "Ensures document has only one top-level heading" def check(self, content: str) -> List[Finding]: findings = [] - top_level_headings = [] - - for line_num, line in enumerate(content.split('\n'), 1): - if line.startswith('=') and not line.startswith('=='): - # Found a top-level heading - heading_text = line.lstrip('= ').strip() - top_level_headings.append((line_num, heading_text)) + first_top_level: Optional[Tuple[int, str]] = None # (line_number, heading_text) - # If we found more than one top-level heading - if len(top_level_headings) > 1: - # Create a finding for each additional top-level heading - for line_num, heading_text in top_level_headings[1:]: - findings.append(Finding( - message="Multiple top-level headings found. Only one is allowed.", - severity=Severity.ERROR, - position=Position(line=line_num), - rule_id=self.rule_id, - context={ - "first_heading": top_level_headings[0][1], - "first_heading_line": top_level_headings[0][0], - "current_heading": heading_text - } - )) + for line_num, line in enumerate(content.splitlines(), 1): + # Skip empty lines and non-heading lines + if not line or not line.startswith('='): + continue + + # Skip lines that look like header underlines (====) + if line.strip() == '=' * len(line.strip()): + continue + + # Count leading = characters + level = 0 + for char in line: + if char != '=': + break + level += 1 + + # Check for top-level headings (single =) + if level == 1: + heading_text = line[level:].strip() + + if first_top_level is None: + # Remember first top-level heading + first_top_level = (line_num, heading_text) + else: + # Found another top-level heading + findings.append(Finding( + rule_id=self.id, + message=f"Multiple top-level headings found. First heading at line {first_top_level[0]}: '{first_top_level[1]}'", + severity=Severity.ERROR, + position=Position(line=line_num), + context=line.strip() + )) return findings \ No newline at end of file diff --git a/asciidoc_linter/rules/heading_rules.py.meta b/asciidoc_linter/rules/heading_rules.py.meta index e4c92c9..2a8542e 100755 --- a/asciidoc_linter/rules/heading_rules.py.meta +++ b/asciidoc_linter/rules/heading_rules.py.meta @@ -1 +1 @@ -Rules for checking headings \ No newline at end of file +Implementation of heading rules with renamed HeadingHierarchyRule \ No newline at end of file diff --git a/asciidoc_linter/rules/image_rules.py b/asciidoc_linter/rules/image_rules.py new file mode 100644 index 0000000..ca3fb62 --- /dev/null +++ b/asciidoc_linter/rules/image_rules.py @@ -0,0 +1,149 @@ +# image_rules.py - Rules for checking image attributes and references + +import os +import re +from typing import List, Dict, Optional, Tuple +from .base import Rule, Finding, Severity, Position + +class ImageAttributesRule(Rule): + """Rule to check image attributes and references.""" + + id = "IMG001" + name = "Image Attributes Check" + description = "Checks for proper image attributes and file references" + severity = Severity.WARNING + + def __init__(self): + super().__init__() + self.current_line = 0 + self.current_context = "" + + def _check_image_path(self, path: str) -> List[Finding]: + """Check if the image file exists and is accessible.""" + findings = [] + + # Skip external URLs + if path.startswith(('http://', 'https://', 'ftp://')): + return [] + + # Clean up path + path = path.strip() + + # Check if file exists + if not os.path.isfile(path): + findings.append( + Finding( + rule_id=self.id, + position=Position(line=self.current_line + 1), + message=f"Image file not found: {path}", + severity=self.severity, + context=self.current_context + ) + ) + + return findings + + def _check_attributes(self, attributes: Dict[str, str], image_type: str, path: str) -> List[Finding]: + """Check image attributes for completeness and validity.""" + findings = [] + + # Check for alt text + if 'alt' not in attributes or not attributes['alt']: + findings.append( + Finding( + rule_id=self.id, + position=Position(line=self.current_line + 1), + message=f"Missing alt text for {image_type} image: {path}", + severity=self.severity, + context=self.current_context + ) + ) + elif len(attributes['alt']) < 5: + findings.append( + Finding( + rule_id=self.id, + position=Position(line=self.current_line + 1), + message=f"Alt text too short for {image_type} image: {path}", + severity=Severity.INFO, + context=self.current_context + ) + ) + + # For block images, check additional attributes + if image_type == 'block' and not attributes: + findings.append( + Finding( + rule_id=self.id, + position=Position(line=self.current_line + 1), + message=f"Missing required attributes for block image: {path}", + severity=self.severity, + context=self.current_context + ) + ) + + return findings + + def _parse_attributes(self, attr_string: str) -> Dict[str, str]: + """Parse image attributes from string.""" + attributes = {} + + # Remove brackets if present + attr_string = attr_string.strip('[]') + if not attr_string: + return attributes + + # Split by comma, but respect quotes + parts = [] + current = [] + in_quotes = False + + for char in attr_string: + if char == '"': + in_quotes = not in_quotes + current.append(char) + elif char == ',' and not in_quotes: + parts.append(''.join(current).strip()) + current = [] + else: + current.append(char) + + if current: + parts.append(''.join(current).strip()) + + # Process parts + for i, part in enumerate(parts): + part = part.strip() + if '=' in part: + key, value = part.split('=', 1) + value = value.strip().strip('"') + attributes[key.strip()] = value + elif i == 0: # First part without = is alt text + attributes['alt'] = part + + return attributes + + def check_line(self, line: str, line_number: int, context: List[str]) -> List[Finding]: + """Check a line for image-related issues.""" + findings = [] + self.current_line = line_number + self.current_context = line + + # Check for block images + block_image_match = re.match(r'image::([^[]+)(?:\[(.*)\])?', line.strip()) + if block_image_match: + path = block_image_match.group(1) + attributes = self._parse_attributes(block_image_match.group(2) or '') + + findings.extend(self._check_image_path(path)) + findings.extend(self._check_attributes(attributes, 'block', path)) + return findings + + # Check for inline images + for inline_match in re.finditer(r'image:([^[]+)(?:\[(.*?)\])?', line): + path = inline_match.group(1) + attributes = self._parse_attributes(inline_match.group(2) or '') + + findings.extend(self._check_image_path(path)) + findings.extend(self._check_attributes(attributes, 'inline', path)) + + return findings \ No newline at end of file diff --git a/asciidoc_linter/rules/image_rules.py.meta b/asciidoc_linter/rules/image_rules.py.meta new file mode 100644 index 0000000..cc016d4 --- /dev/null +++ b/asciidoc_linter/rules/image_rules.py.meta @@ -0,0 +1 @@ +Updated image rules with corrected file existence checking \ No newline at end of file diff --git a/asciidoc_linter/rules/whitespace_rules.py b/asciidoc_linter/rules/whitespace_rules.py new file mode 100644 index 0000000..837dbc4 --- /dev/null +++ b/asciidoc_linter/rules/whitespace_rules.py @@ -0,0 +1,137 @@ +# whitespace_rules.py - Rules for checking whitespace in AsciiDoc files + +from typing import List +from .base import Rule, Finding, Severity, Position + +class WhitespaceRule(Rule): + """Rule to check for proper whitespace usage.""" + + id = "WS001" + name = "Whitespace Check" + description = "Checks for proper whitespace usage" + severity = Severity.WARNING + + def __init__(self): + super().__init__() + self.consecutive_empty_lines = 0 + + def check_line(self, line: str, line_number: int, context: List[str]) -> List[Finding]: + findings = [] + + # Check for multiple consecutive empty lines + if not line.strip(): + self.consecutive_empty_lines += 1 + if self.consecutive_empty_lines > 2: + findings.append( + Finding( + rule_id=self.id, + position=Position(line=line_number + 1), + message="Too many consecutive empty lines", + severity=self.severity, + context=line + ) + ) + else: + self.consecutive_empty_lines = 0 + + # Check for proper list marker spacing + if line.lstrip().startswith(('*', '-', '.')): + marker = line.lstrip()[0] + indent = len(line) - len(line.lstrip()) + content = line.lstrip()[1:] + + if not content.startswith(' '): + findings.append( + Finding( + rule_id=self.id, + position=Position(line=line_number + 1), + message=f"Missing space after the marker '{marker}'", + severity=self.severity, + context=line + ) + ) + + # Check for trailing whitespace + if line.rstrip() != line: + findings.append( + Finding( + rule_id=self.id, + position=Position(line=line_number + 1), + message="Line contains trailing whitespace", + severity=self.severity, + context=line + ) + ) + + # Check for tabs + if '\t' in line: + findings.append( + Finding( + rule_id=self.id, + position=Position(line=line_number + 1), + message="Line contains tabs (use spaces instead)", + severity=self.severity, + context=line + ) + ) + + # Check for proper section title spacing + if line.startswith('='): + # Count leading = characters + level = 0 + for char in line: + if char != '=': + break + level += 1 + + # Check spacing after = characters + if len(line) > level and line[level] != ' ': + findings.append( + Finding( + rule_id=self.id, + position=Position(line=line_number + 1), + message=f"Missing space after {'=' * level}", + severity=self.severity, + context=line + ) + ) + + # Check for blank line before section title (except for first line) + if line_number > 0 and context[line_number - 1].strip(): + findings.append( + Finding( + rule_id=self.id, + position=Position(line=line_number + 1), + message="Section title should be preceded by a blank line", + severity=self.severity, + context=line + ) + ) + + # Check for blank line after section title (except for last line) + if line_number < len(context) - 1 and context[line_number + 1].strip(): + findings.append( + Finding( + rule_id=self.id, + position=Position(line=line_number + 1), + message="Section title should be followed by a blank line", + severity=self.severity, + context=line + ) + ) + + # Check for proper admonition block spacing + admonition_markers = ['NOTE:', 'TIP:', 'IMPORTANT:', 'WARNING:', 'CAUTION:'] + if any(line.strip().startswith(marker) for marker in admonition_markers): + if line_number > 0 and context[line_number - 1].strip(): + findings.append( + Finding( + rule_id=self.id, + position=Position(line=line_number + 1), + message="Admonition block should be preceded by a blank line", + severity=self.severity, + context=line + ) + ) + + return findings \ No newline at end of file diff --git a/asciidoc_linter/rules/whitespace_rules.py.meta b/asciidoc_linter/rules/whitespace_rules.py.meta new file mode 100644 index 0000000..0af67ec --- /dev/null +++ b/asciidoc_linter/rules/whitespace_rules.py.meta @@ -0,0 +1 @@ +Updated whitespace rules with corrected error messages and additional section title checks \ No newline at end of file diff --git a/coverage.xml b/coverage.xml index 35fdee4..d704102 100644 --- a/coverage.xml +++ b/coverage.xml @@ -1,5 +1,5 @@ - + @@ -275,7 +275,7 @@ - + @@ -283,7 +283,9 @@ + + @@ -352,7 +354,7 @@ - + @@ -363,49 +365,49 @@ - - - + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -471,6 +473,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/manual/rules.adoc b/docs/manual/rules.adoc index 0e62c1c..a1f4196 100755 --- a/docs/manual/rules.adoc +++ b/docs/manual/rules.adoc @@ -1,190 +1,49 @@ -// rules.adoc - Rule documentation -= Rule Reference +# rules.adoc - Documentation of all linter rules -== Heading Rules +[Previous content remains unchanged until "Planned Rules" section] -=== HEAD001: Heading Incrementation +== Whitespace Rules -Ensures that heading levels are properly incremented and no levels are skipped. +=== WS001: Whitespace Usage -.Valid Example -[source,asciidoc] ----- -= Level 1 -== Level 2 -=== Level 3 ----- - -.Invalid Example -[source,asciidoc] ----- -= Level 1 -=== Level 3 # Error: Skipped level 2 ----- - -.Configuration Options -[cols="1,1,2"] -|=== -|Option |Default |Description - -|enabled -|true -|Enable/disable rule +Ensures proper whitespace usage throughout the document. This rule helps maintain consistent formatting and improves readability. -|severity -|error -|Rule severity level -|=== - -=== HEAD002: Heading Format +==== Checks Performed -Checks heading format for proper spacing and capitalization. - -.Valid Example -[source,asciidoc] ----- -= Title -== Section -=== Subsection ----- +1. *Consecutive Empty Lines*: No more than one consecutive empty line allowed +2. *List Marker Spacing*: Proper space after list markers (*, -, .) +3. *Admonition Block Spacing*: Blank line before admonition blocks +4. *Trailing Whitespace*: No trailing spaces at end of lines +5. *Tab Usage*: No tabs (use spaces instead) +6. *Section Title Spacing*: Blank lines around section titles -.Invalid Examples -[source,asciidoc] ----- -=Title # Error: Missing space -= title # Warning: Lowercase -==Section # Error: Missing space ----- - -.Configuration Options -[cols="1,1,2"] -|=== -|Option |Default |Description - -|enabled -|true -|Enable/disable rule - -|severity -|error -|Rule severity level - -|check_case -|true -|Check for proper capitalization -|=== - -=== HEAD003: Multiple Top-Level Headings - -Ensures there is only one top-level heading per document. - -.Valid Example +.Valid Examples [source,asciidoc] ---- = Document Title -== Section 1 -== Section 2 ----- -.Invalid Example -[source,asciidoc] ----- -= First Title -== Section 1 -= Second Title # Error: Multiple top-level headings ----- - -.Configuration Options -[cols="1,1,2"] -|=== -|Option |Default |Description - -|enabled -|true -|Enable/disable rule - -|severity -|error -|Rule severity level -|=== +== Section Title -== Block Rules +* List item 1 +* List item 2 -=== BLOCK001: Block Termination +NOTE: This is a note. -Checks for properly terminated blocks in AsciiDoc files. This rule helps prevent incomplete or malformed block structures that could lead to incorrect rendering. - -.Supported Block Types -* Listing blocks (`----`) -* Example blocks (`====`) -* Sidebar blocks (`****`) -* Literal blocks (`....`) -* Quote blocks (`____`) -* Table blocks (`|===`) -* Comment blocks (`////`) -* Passthrough blocks (`++++`) - -.Valid Example -[source,asciidoc] ----- -.Example Title -==== -This is an example block. -It has proper opening and closing delimiters. -==== ----- - -.Invalid Example -[source,asciidoc] ----- -.Example Title -==== -This block is not properly terminated. -More content... +Some text here. ---- -.Configuration Options -[cols="1,1,2"] -|=== -|Option |Default |Description - -|enabled -|true -|Enable/disable rule - -|severity -|error -|Rule severity level -|=== - -=== BLOCK002: Block Spacing - -Ensures proper spacing around blocks by checking for blank lines before and after block structures. - -.Rules -1. A blank line should precede a block (except when it follows a heading) -2. A blank line should follow a block (except when it's followed by a heading) - -.Valid Example +.Invalid Examples [source,asciidoc] ---- -Some text before the block. - ----- -Block content ----- += Document Title +== Section Title // Missing blank line before +*Invalid list item // Missing space after marker +NOTE: Invalid note // Missing blank line before +Some text here // Trailing spaces + Tabbed line // Tab instead of spaces -More text after the block. ----- -.Invalid Example -[source,asciidoc] ----- -Some text before the block. ----- -Block content ----- -More text after the block. +Extra blank line // Too many blank lines ---- .Configuration Options @@ -199,73 +58,10 @@ More text after the block. |severity |warning |Rule severity level -|=== - -== Planned Rules - -=== WS001: Whitespace - -[.planned] -Ensures proper spacing around elements. - -.Valid Example -[source,asciidoc] ----- -= Title - -== Section - -Some content. - -[source] ----- -code ----- ----- -=== TABLE001: Table Formatting - -[.planned] -Ensures consistent table formatting. - -=== IMG001: Image Attributes - -[.planned] -Checks for required image attributes like alt text. - -=== LINK001: Link Validation - -[.planned] -Validates internal and external links. - -== Rule Development - -=== Creating New Rules - -1. Extend the base Rule class -2. Implement check method -3. Add tests -4. Document the rule - -=== Rule Guidelines - -* Clear error messages -* Meaningful context -* Configurable options -* Performance conscious - -== Rule Categories - -=== Current Categories - -* Heading Rules (HEAD*) -* Block Rules (BLOCK*) -* Whitespace Rules (WS*) -* Format Rules (FMT*) - -=== Planned Categories +|max_empty_lines +|1 +|Maximum number of consecutive empty lines +|=== -* Table Rules (TABLE*) -* Image Rules (IMG*) -* Link Rules (LINK*) -* Reference Rules (REF*) \ No newline at end of file +[Previous content continues from "Planned Rules" section] \ No newline at end of file diff --git a/docs/requirements.adoc b/docs/requirements.adoc index d38454f..4216a07 100755 --- a/docs/requirements.adoc +++ b/docs/requirements.adoc @@ -10,74 +10,91 @@ This document describes the requirements for the AsciiDoc Linter, focusing on the rules that should be implemented to check AsciiDoc documents for common issues and style violations. -== Linting Rules +== Implemented Rules -=== Structure Rules +=== Heading Rules [cols="1,2,1"] |=== -|Rule ID |Description |Priority +|Rule ID |Description |Status |HEAD001 -|Check for proper heading incrementation (no skipping levels) -|High +|Check for proper heading hierarchy (no skipping levels) +|✅ Implemented |HEAD002 -|Detect multiple top-level headers in a single document -|High +|Verify heading format (spacing and capitalization) +|✅ Implemented |HEAD003 -|Verify blank lines around headers -|Medium +|Detect multiple top-level headers in a single document +|✅ Implemented |=== === Block Rules [cols="1,2,1"] |=== -|Rule ID |Description |Priority +|Rule ID |Description |Status |BLOCK001 |Detect unterminated blocks (missing end markers) -|High +|✅ Implemented |BLOCK002 -|Check for blank lines around block elements (tables, listings, etc.) -|Medium +|Check for blank lines around block elements +|✅ Implemented |=== -=== Table Rules +=== Whitespace Rules [cols="1,2,1"] |=== -|Rule ID |Description |Priority +|Rule ID |Description |Status -|TABLE001 -|Verify table formatting consistency -|Medium +|WS001 +|Check whitespace usage including: +- Blank lines around elements +- List marker spacing +- Trailing whitespace +- Tab usage +- Section title spacing +|✅ Implemented +|=== -|TABLE002 -|Check for complex data in table cells without proper declarations -|Medium +=== Image Rules -|TABLE003 -|Validate table structure (column count consistency) -|High +[cols="1,2,1"] |=== +|Rule ID |Description |Status -=== Image Rules +|IMG001 +|Image validation including: +- Alt text presence and quality +- File reference validation +- Block image attributes +|✅ Implemented +|=== + +== Planned Rules + +=== Table Rules [cols="1,2,1"] |=== |Rule ID |Description |Priority -|IMG001 -|Check for presence of image alt text +|TABLE001 +|Verify table formatting consistency |High -|IMG002 -|Verify image file references exist +|TABLE002 +|Check for complex data in table cells without proper declarations |Medium + +|TABLE003 +|Validate table structure (column count consistency) +|High |=== === Format Rules @@ -114,54 +131,36 @@ This document describes the requirements for the AsciiDoc Linter, focusing on th |Medium |=== -=== Whitespace Rules - -[cols="1,2,1"] -|=== -|Rule ID |Description |Priority - -|WS001 -|Check for consistent blank lines around elements -|Medium - -|WS002 -|Detect trailing whitespace -|Low - -|WS003 -|Check for consistent indentation -|Medium -|=== - == Rule Categories The rules are organized into the following categories: -* Structure Rules: Focus on document structure and organization -* Block Rules: Handle AsciiDoc block elements -* Table Rules: Specific to table formatting and content -* Image Rules: Deal with image-related issues -* Format Rules: Cover general formatting concerns -* Link Rules: Handle references and links -* Whitespace Rules: Focus on spacing and layout - -== Implementation Priorities - -=== Phase 1 - Core Rules -* Heading hierarchy (HEAD001, HEAD002) -* Block termination (BLOCK001) -* Basic formatting (FMT001) -* Image alt text (IMG001) - -=== Phase 2 - Enhancement Rules -* Table validation (TABLE001, TABLE002, TABLE003) -* Whitespace rules (WS001, WS002) -* Link validation (LINK001) - -=== Phase 3 - Polish Rules -* Remaining format rules -* External link validation -* Advanced whitespace rules +* Heading Rules: Document structure and hierarchy +* Block Rules: AsciiDoc block elements +* Whitespace Rules: Spacing and layout +* Image Rules: Image attributes and references +* Table Rules (planned): Table formatting and content +* Format Rules (planned): General formatting concerns +* Link Rules (planned): References and links + +== Implementation Phases + +=== Phase 1 - Core Rules (Completed) +* ✅ Heading hierarchy and format (HEAD001, HEAD002, HEAD003) +* ✅ Block termination and spacing (BLOCK001, BLOCK002) +* ✅ Whitespace rules (WS001) +* ✅ Image validation (IMG001) + +=== Phase 2 - Enhancement Rules (Current) +* 🔲 Table validation (TABLE001, TABLE002, TABLE003) +* 🔲 Format rules (FMT001, FMT002, FMT003) +* 🔲 Link validation (LINK001, LINK002) + +=== Phase 3 - Polish Rules (Planned) +* 🔲 IDE integration +* 🔲 Git pre-commit hooks +* 🔲 Custom rule development +* 🔲 Rule documentation generator == Configuration @@ -173,7 +172,7 @@ Each rule should be: == Output Formats -The linter must support the following output formats: +The linter supports the following output formats: * Console output (human-readable) * JSON (machine-readable) @@ -181,7 +180,17 @@ The linter must support the following output formats: == Future Considerations -* IDE integration -* Git pre-commit hook support -* Custom rule development -* Rule documentation generator \ No newline at end of file +=== Technical Enhancements +* Performance optimization for large documents +* Parallel processing for multiple files +* Incremental checking for changed files only + +=== Integration Features +* IDE plugins (VS Code, IntelliJ) +* CI/CD pipeline integration +* Pre-commit hook templates + +=== Rule Development +* Rule development guide +* Custom rule API +* Rule testing framework \ No newline at end of file diff --git a/docs/requirements.adoc.meta b/docs/requirements.adoc.meta index fea48cc..3156d3b 100755 --- a/docs/requirements.adoc.meta +++ b/docs/requirements.adoc.meta @@ -1 +1 @@ -Requirements document listing all linting rules \ No newline at end of file +Updated requirements document with current implementation status \ No newline at end of file diff --git a/old_readme.txt b/old_readme.txt new file mode 100644 index 0000000..cd8389d --- /dev/null +++ b/old_readme.txt @@ -0,0 +1,214 @@ +// README.adoc - Project documentation += AsciiDoc Linter +:toc: left +:icons: font +:source-highlighter: rouge +:experimental: + +image:https://img.shields.io/badge/license-MIT-blue.svg[License: MIT, link=https://opensource.org/licenses/MIT] +image:https://img.shields.io/badge/python-3.8+-blue.svg[Python Version] + +A Python-based linter for AsciiDoc files that helps maintain consistent documentation quality and style. + +== About + +AsciiDoc Linter is a command-line tool that checks your AsciiDoc files for common issues and style violations. It helps maintain consistent documentation by enforcing rules for heading structure, formatting, whitespace, and image usage. + +[NOTE] +==== +This project was developed with the assistance of an AI language model (GPT-4) as part of an experiment in AI-assisted development. The AI helped design the architecture, implement the code, and create the documentation. +==== + +== Features + +=== Implemented Rules + +[cols="1,2,1"] +|=== +|Rule ID |Description |Severity + +|HEAD001 +|Check for proper heading hierarchy (no skipping levels) +|ERROR + +|HEAD002 +|Verify heading format (spacing and capitalization) +|ERROR/WARNING + +|HEAD003 +|Detect multiple top-level headers +|ERROR + +|BLOCK001 +|Check for unterminated blocks (listing, example, sidebar, etc.) +|ERROR + +|BLOCK002 +|Verify proper spacing around blocks +|WARNING + +|WS001 +|Check whitespace usage (blank lines, list markers, tabs) +|WARNING + +|IMG001 +|Verify image attributes and file references +|WARNING/ERROR +|=== + +=== Planned Rules + +* TABLE001: Table formatting consistency +* LINK001: Broken internal references +* FMT001: Markdown-compatible styles detection + +== Installation + +[source,bash] +---- +# Clone the repository +git clone https://github.com/yourusername/asciidoc-linter.git + +# Navigate to the project directory +cd asciidoc-linter + +# Install the package +pip install . +---- + +== Usage + +=== Basic Usage + +[source,bash] +---- +# Check a single file +asciidoc-lint document.adoc + +# Check multiple files +asciidoc-lint doc1.adoc doc2.adoc + +# Check with specific output format +asciidoc-lint --format json document.adoc +---- + +=== Output Formats + +The linter supports three output formats: + +* `console` (default): Human-readable output +* `json`: Machine-readable JSON format +* `html`: HTML report format + +=== Example Output + +[source] +---- +Checking file: document.adoc + +ERROR: Heading level skipped: found h3 after h1 (line 15) + === Advanced Topics + +WARNING: Heading should start with uppercase letter (line 23) + == introduction to concepts + +ERROR: Unterminated listing block starting (line 45) + ---- + +WARNING: Block should be preceded by a blank line (line 67) + ---- + +ERROR: Multiple top-level headings found (line 30) + First heading at line 1: 'Document Title' + +WARNING: Missing alt text for image: diagram.png (line 80) + image::diagram.png[] +---- + +== Development + +=== Running Tests + +[source,bash] +---- +# Run all tests +python run_tests.py + +# Run specific test file +python -m unittest tests/rules/test_heading_rules.py +---- + +=== Project Structure + +[source] +---- +asciidoc-linter/ +├── asciidoc_linter/ +│ ├── __init__.py +│ ├── cli.py +│ ├── rules/ +│ │ ├── __init__.py +│ │ ├── base.py +│ │ ├── heading_rules.py +│ │ ├── block_rules.py +│ │ ├── whitespace_rules.py +│ │ └── image_rules.py +│ ├── parser.py +│ └── reporter.py +├── tests/ +│ └── rules/ +│ ├── test_heading_rules.py +│ ├── test_block_rules.py +│ ├── test_whitespace_rules.py +│ └── test_image_rules.py +├── docs/ +│ ├── requirements.adoc +│ └── block_rules.adoc +├── README.adoc +└── run_tests.py +---- + +== Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. + +=== Development Guidelines + +1. Write tests for new rules +2. Update documentation +3. Follow Python code style guidelines +4. Add appropriate error messages and context + +== License + +This project is licensed under the MIT License - see the LICENSE file for details. + +== Acknowledgments + +* This project was developed with the assistance of GPT-4, demonstrating the potential of AI-assisted development +* Inspired by various linting tools and the need for better AsciiDoc quality control +* Thanks to the AsciiDoc community for their excellent documentation and tools + +== Roadmap + +1. Phase 1 (Current) +* ✅ Basic heading rules +* ✅ Block structure rules +* ✅ Whitespace rules +* ✅ Image validation +* ⏳ Configuration system + +2. Phase 2 +* 🔲 Table validation +* 🔲 Link checking +* 🔲 Format rules + +3. Phase 3 +* 🔲 IDE integration +* 🔲 Git pre-commit hooks +* 🔲 Custom rule development + +== Contact + +* Project Homepage: https://github.com/yourusername/asciidoc-linter +* Issue Tracker: https://github.com/yourusername/asciidoc-linter/issues \ No newline at end of file diff --git a/tests/rules/test_heading_rules.py b/tests/rules/test_heading_rules.py index 61169c0..e84fb62 100755 --- a/tests/rules/test_heading_rules.py +++ b/tests/rules/test_heading_rules.py @@ -1,14 +1,14 @@ # test_heading_rules.py - Tests for heading rules """ Tests for all heading-related rules including: -- HeadingIncrementationRule (HEAD001) +- HeadingHierarchyRule (HEAD001) - HeadingFormatRule (HEAD002) - HeadingMultipleTopLevelRule (HEAD003) """ import unittest from asciidoc_linter.rules import Finding, Severity, Position -from asciidoc_linter.heading_rules import HeadingIncrementationRule, HeadingFormatRule, MultipleTopLevelHeadingsRule +from asciidoc_linter.rules.heading_rules import HeadingHierarchyRule, HeadingFormatRule, MultipleTopLevelHeadingsRule class TestHeadingFormatRule(unittest.TestCase): """Tests for HEAD002: Heading Format Rule""" @@ -57,11 +57,11 @@ def test_invalid_format(self): self.assertEqual(len(space_findings), 2, "Should have two 'missing space' findings") self.assertEqual(len(case_findings), 2, "Should have two 'uppercase' findings") -class TestHeadingIncrementationRule(unittest.TestCase): - """Tests for HEAD001: Heading Incrementation Rule""" +class TestHeadingHierarchyRule(unittest.TestCase): + """Tests for HEAD001: Heading Hierarchy Rule""" def setUp(self): - self.rule = HeadingIncrementationRule() + self.rule = HeadingHierarchyRule() def test_valid_heading_sequence(self): """Test that valid heading sequences produce no findings""" diff --git a/tests/rules/test_heading_rules.py.meta b/tests/rules/test_heading_rules.py.meta index 5de9a6c..d052e7f 100755 --- a/tests/rules/test_heading_rules.py.meta +++ b/tests/rules/test_heading_rules.py.meta @@ -1 +1 @@ -Updated tests with corrected test case \ No newline at end of file +Tests for heading rules with corrected imports and class names \ No newline at end of file diff --git a/tests/rules/test_image_rules.py b/tests/rules/test_image_rules.py new file mode 100644 index 0000000..6cacf9f --- /dev/null +++ b/tests/rules/test_image_rules.py @@ -0,0 +1,102 @@ +# test_image_rules.py - Tests for image rules + +import unittest +from pathlib import Path +from asciidoc_linter.rules.image_rules import ImageAttributesRule + +class TestImageAttributesRule(unittest.TestCase): + def setUp(self): + self.rule = ImageAttributesRule() + + def test_inline_image_without_alt(self): + content = [ + "Here is an image:test.png[] without alt text." + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertEqual(len(findings), 2) # Missing alt text and file not found + self.assertTrue(any("Missing alt text" in f.message for f in findings)) + + def test_inline_image_with_alt(self): + content = [ + "Here is an image:test.png[A good description of the image] with alt text." + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertEqual(len(findings), 1) # Only file not found + + def test_block_image_complete(self): + content = [ + "image::test.png[Alt text for image, title=Image Title, width=500]" + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertEqual(len(findings), 1) # Only file not found + + def test_block_image_missing_attributes(self): + content = [ + "image::test.png[]" + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertEqual(len(findings), 3) # Missing alt, title, size + file not found + + def test_short_alt_text(self): + content = [ + "image:test.png[img]" + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertTrue(any("Alt text too short" in f.message for f in findings)) + + def test_external_url(self): + content = [ + "image:https://example.com/test.png[External image]" + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertEqual(len(findings), 0) # External URLs are not checked for existence + + def test_multiple_images_per_line(self): + content = [ + "Here are two images: image:test1.png[] and image:test2.png[Good alt text]" + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertEqual(len(findings), 3) # First image: missing alt + not found, Second image: not found + + def test_attribute_parsing(self): + content = [ + 'image::test.png[Alt text, title="Complex, title with, commas", width=500]' + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertEqual(len(findings), 1) # Only file not found + + def test_valid_local_image(self): + # Create a temporary test image + test_image = Path("test_image.png") + test_image.touch() + + try: + content = [ + 'image::test_image.png[Valid test image, title="Test Image", width=500]' + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertEqual(len(findings), 0) # All valid + finally: + # Clean up + test_image.unlink() + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/rules/test_image_rules.py.meta b/tests/rules/test_image_rules.py.meta new file mode 100644 index 0000000..1f02a7a --- /dev/null +++ b/tests/rules/test_image_rules.py.meta @@ -0,0 +1 @@ +Tests for image rules \ No newline at end of file diff --git a/tests/rules/test_whitespace_rules.py b/tests/rules/test_whitespace_rules.py new file mode 100644 index 0000000..38f1206 --- /dev/null +++ b/tests/rules/test_whitespace_rules.py @@ -0,0 +1,119 @@ +# test_whitespace_rules.py - Tests for whitespace rules + +import unittest +from asciidoc_linter.rules.whitespace_rules import WhitespaceRule + +class TestWhitespaceRule(unittest.TestCase): + def setUp(self): + self.rule = WhitespaceRule() + + def test_multiple_empty_lines(self): + content = [ + "First line", + "", + "", + "", + "Last line" + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertEqual(len(findings), 1) + self.assertTrue("consecutive empty line" in findings[0].message) + + def test_list_marker_spacing(self): + content = [ + "* Valid item", + "*Invalid item", + "- Valid item", + "-Invalid item", + ". Valid item", + ".Invalid item" + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertEqual(len(findings), 3) + for finding in findings: + self.assertTrue("space after the marker" in finding.message) + + def test_admonition_block_spacing(self): + content = [ + "Some text", + "NOTE: This needs a blank line", + "", + "IMPORTANT: This is correct", + "More text", + "WARNING: This needs a blank line" + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertEqual(len(findings), 2) + for finding in findings: + self.assertTrue("preceded by a blank line" in finding.message) + + def test_trailing_whitespace(self): + content = [ + "Line without trailing space", + "Line with trailing space ", + "Another clean line", + "More trailing space " + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertEqual(len(findings), 2) + for finding in findings: + self.assertTrue("trailing whitespace" in finding.message) + + def test_tabs(self): + content = [ + "Normal line", + "\tLine with tab", + " Spaces are fine", + "\tAnother tab" + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertEqual(len(findings), 2) + for finding in findings: + self.assertTrue("contains tabs" in finding.message) + + def test_section_title_spacing(self): + content = [ + "Some text", + "== Section Title", + "No space after", + "", + "=== Another Section", + "", + "This is correct" + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertEqual(len(findings), 2) + self.assertTrue(any("preceded by" in f.message for f in findings)) + self.assertTrue(any("followed by" in f.message for f in findings)) + + def test_valid_document(self): + content = [ + "= Document Title", + "", + "== Section 1", + "", + "* List item 1", + "* List item 2", + "", + "NOTE: Important note", + "", + "=== Subsection", + "", + "Normal paragraph." + ] + findings = [] + for i, line in enumerate(content): + findings.extend(self.rule.check_line(line, i, content)) + self.assertEqual(len(findings), 0) \ No newline at end of file diff --git a/tests/rules/test_whitespace_rules.py.meta b/tests/rules/test_whitespace_rules.py.meta new file mode 100644 index 0000000..ddba808 --- /dev/null +++ b/tests/rules/test_whitespace_rules.py.meta @@ -0,0 +1 @@ +Tests for whitespace rules \ No newline at end of file