""" 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