|
|
""" |
|
|
Equation formatting checker. |
|
|
|
|
|
Validates: |
|
|
- Punctuation after equations (based on grammar) |
|
|
- Equation numbering consistency |
|
|
- Variable definitions |
|
|
""" |
|
|
import re |
|
|
from typing import List, Set |
|
|
|
|
|
from .base import BaseChecker, CheckResult, CheckSeverity |
|
|
|
|
|
|
|
|
class EquationChecker(BaseChecker): |
|
|
"""Check equation formatting and consistency.""" |
|
|
|
|
|
name = "equation" |
|
|
display_name = "Equations" |
|
|
description = "Check equation formatting and punctuation" |
|
|
|
|
|
|
|
|
EQUATION_ENVS = [ |
|
|
'equation', 'align', 'gather', 'multline', 'eqnarray', |
|
|
'equation*', 'align*', 'gather*', 'multline*', 'eqnarray*' |
|
|
] |
|
|
|
|
|
def check(self, tex_content: str, config: dict = None) -> List[CheckResult]: |
|
|
results = [] |
|
|
|
|
|
|
|
|
punct_results = self._check_equation_punctuation(tex_content) |
|
|
results.extend(punct_results) |
|
|
|
|
|
|
|
|
numbering_results = self._check_numbering_consistency(tex_content) |
|
|
results.extend(numbering_results) |
|
|
|
|
|
|
|
|
inline_results = self._check_inline_math_consistency(tex_content) |
|
|
results.extend(inline_results) |
|
|
|
|
|
return results |
|
|
|
|
|
def _check_equation_punctuation(self, content: str) -> List[CheckResult]: |
|
|
"""Check if equations end with appropriate punctuation.""" |
|
|
results = [] |
|
|
|
|
|
for env in self.EQUATION_ENVS: |
|
|
if '*' in env: |
|
|
env_escaped = env.replace('*', r'\*') |
|
|
else: |
|
|
env_escaped = env |
|
|
|
|
|
|
|
|
pattern = re.compile( |
|
|
rf'\\begin\{{{env_escaped}\}}(.*?)\\end\{{{env_escaped}\}}', |
|
|
re.DOTALL |
|
|
) |
|
|
|
|
|
for match in pattern.finditer(content): |
|
|
eq_content = match.group(1).strip() |
|
|
|
|
|
|
|
|
after_pos = match.end() |
|
|
after_text = content[after_pos:after_pos + 50].strip() |
|
|
|
|
|
|
|
|
|
|
|
eq_content_clean = re.sub(r'\\label\{[^}]+\}', '', eq_content).strip() |
|
|
|
|
|
if eq_content_clean and not re.search(r'[.,;]$', eq_content_clean): |
|
|
|
|
|
if after_text and after_text[0].islower(): |
|
|
line_num = self._find_line_number(content, match.end()) |
|
|
results.append(self._create_result( |
|
|
passed=False, |
|
|
severity=CheckSeverity.INFO, |
|
|
message="Equation may need punctuation (sentence continues after)", |
|
|
line_number=line_num, |
|
|
suggestion="Add comma or period inside equation if it ends a clause" |
|
|
)) |
|
|
|
|
|
return results |
|
|
|
|
|
def _check_numbering_consistency(self, content: str) -> List[CheckResult]: |
|
|
"""Check for mixed numbered and unnumbered equations.""" |
|
|
results = [] |
|
|
|
|
|
|
|
|
numbered = 0 |
|
|
unnumbered = 0 |
|
|
|
|
|
for env in self.EQUATION_ENVS: |
|
|
count = len(re.findall(rf'\\begin\{{{env}\}}', content)) |
|
|
if '*' in env or 'nonumber' in content: |
|
|
unnumbered += count |
|
|
else: |
|
|
numbered += count |
|
|
|
|
|
|
|
|
unnumbered += len(re.findall(r'\\nonumber|\\notag', content)) |
|
|
|
|
|
|
|
|
total = numbered + unnumbered |
|
|
if total > 3 and numbered > 0 and unnumbered > 0: |
|
|
ratio = min(numbered, unnumbered) / total |
|
|
if ratio > 0.2: |
|
|
results.append(self._create_result( |
|
|
passed=False, |
|
|
severity=CheckSeverity.INFO, |
|
|
message=f"Mixed equation numbering: {numbered} numbered, {unnumbered} unnumbered", |
|
|
suggestion="Consider consistent numbering strategy" |
|
|
)) |
|
|
|
|
|
return results |
|
|
|
|
|
def _check_inline_math_consistency(self, content: str) -> List[CheckResult]: |
|
|
"""Check for mixed inline math delimiters.""" |
|
|
results = [] |
|
|
|
|
|
|
|
|
dollar_count = len(re.findall(r'(?<!\$)\$(?!\$)[^$]+\$(?!\$)', content)) |
|
|
paren_count = len(re.findall(r'\\\(.*?\\\)', content)) |
|
|
|
|
|
if dollar_count > 0 and paren_count > 0: |
|
|
results.append(self._create_result( |
|
|
passed=False, |
|
|
severity=CheckSeverity.INFO, |
|
|
message=f"Mixed inline math: ${dollar_count} \\$...\\$ and {paren_count} \\(...\\)", |
|
|
suggestion="Use consistent inline math delimiters throughout" |
|
|
)) |
|
|
|
|
|
return results |
|
|
|