|
|
""" |
|
|
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" |
|
|
|
|
|
|
|
|
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 |
|
|
) |
|
|
|
|
|
|
|
|
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 = [] |
|
|
|
|
|
|
|
|
for match in self.TABLE_ENV_PATTERN.finditer(tex_content): |
|
|
env_content = match.group(1) |
|
|
env_start = match.start() |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
for match in self.FIGURE_ENV_PATTERN.finditer(tex_content): |
|
|
env_content = match.group(1) |
|
|
env_start = match.start() |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
return None |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
return None |
|
|
|
|
|
|
|
|
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 |
|
|
|