File size: 5,289 Bytes
46df5f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
"""
Caption placement checker.

Validates that:
- Table captions appear ABOVE the table content
- Figure captions appear BELOW the figure content
"""
import re
from typing import List

from .base import BaseChecker, CheckResult, CheckSeverity


class CaptionChecker(BaseChecker):
    """Check for correct caption placement in tables and figures."""
    
    name = "caption"
    display_name = "Caption Placement"
    description = "Verify table captions are above and figure captions are below"
    
    # Patterns for environments
    TABLE_ENV_PATTERN = re.compile(
        r'\\begin\{table\*?\}(.*?)\\end\{table\*?\}',
        re.DOTALL | re.IGNORECASE
    )
    FIGURE_ENV_PATTERN = re.compile(
        r'\\begin\{figure\*?\}(.*?)\\end\{figure\*?\}',
        re.DOTALL | re.IGNORECASE
    )
    
    # Content patterns
    CAPTION_PATTERN = re.compile(r'\\caption\s*[\[{]')
    TABULAR_PATTERN = re.compile(r'\\begin\{tabular')
    INCLUDEGRAPHICS_PATTERN = re.compile(r'\\includegraphics')
    TIKZ_PATTERN = re.compile(r'\\begin\{tikzpicture\}')
    
    def check(self, tex_content: str, config: dict = None) -> List[CheckResult]:
        results = []
        
        # Check table environments
        for match in self.TABLE_ENV_PATTERN.finditer(tex_content):
            env_content = match.group(1)
            env_start = match.start()
            
            # Skip if commented
            if self._is_commented(tex_content, env_start):
                continue
            
            result = self._check_table_caption(env_content, tex_content, env_start)
            if result:
                results.append(result)
        
        # Check figure environments
        for match in self.FIGURE_ENV_PATTERN.finditer(tex_content):
            env_content = match.group(1)
            env_start = match.start()
            
            # Skip if commented
            if self._is_commented(tex_content, env_start):
                continue
            
            result = self._check_figure_caption(env_content, tex_content, env_start)
            if result:
                results.append(result)
        
        return results
    
    def _check_table_caption(self, env_content: str, full_content: str, env_start: int) -> CheckResult:
        """Check that table caption is above tabular content."""
        caption_match = self.CAPTION_PATTERN.search(env_content)
        tabular_match = self.TABULAR_PATTERN.search(env_content)
        
        if not caption_match:
            line_num = self._find_line_number(full_content, env_start)
            return self._create_result(
                passed=False,
                severity=CheckSeverity.WARNING,
                message="Table environment missing caption",
                line_number=line_num,
                suggestion="Add \\caption{} before \\begin{tabular}"
            )
        
        if not tabular_match:
            # Table without tabular content - skip
            return None
        
        # Caption should come BEFORE tabular
        if caption_match.start() > tabular_match.start():
            line_num = self._find_line_number(full_content, env_start + caption_match.start())
            return self._create_result(
                passed=False,
                severity=CheckSeverity.ERROR,
                message="Table caption should be placed ABOVE the table content",
                line_number=line_num,
                line_content=self._get_line_content(full_content, line_num),
                suggestion="Move \\caption{} before \\begin{tabular}"
            )
        
        return None
    
    def _check_figure_caption(self, env_content: str, full_content: str, env_start: int) -> CheckResult:
        """Check that figure caption is below image content."""
        caption_match = self.CAPTION_PATTERN.search(env_content)
        graphics_match = self.INCLUDEGRAPHICS_PATTERN.search(env_content)
        tikz_match = self.TIKZ_PATTERN.search(env_content)
        
        # Find the actual content (either graphics or tikz)
        content_match = graphics_match or tikz_match
        
        if not caption_match:
            line_num = self._find_line_number(full_content, env_start)
            return self._create_result(
                passed=False,
                severity=CheckSeverity.WARNING,
                message="Figure environment missing caption",
                line_number=line_num,
                suggestion="Add \\caption{} after \\includegraphics"
            )
        
        if not content_match:
            # Figure without graphics/tikz - could be custom content, skip
            return None
        
        # Caption should come AFTER content
        if caption_match.start() < content_match.start():
            line_num = self._find_line_number(full_content, env_start + caption_match.start())
            return self._create_result(
                passed=False,
                severity=CheckSeverity.ERROR,
                message="Figure caption should be placed BELOW the figure content",
                line_number=line_num,
                line_content=self._get_line_content(full_content, line_num),
                suggestion="Move \\caption{} after \\includegraphics"
            )
        
        return None