"""Visitor for (subscript) MathML elements.""" from lxml import etree as ET from ..core.interfaces import Visitor, Strategy from ..core.context import ConversionContext from ..utils import text_content as _text_content, local_name as _local class MsubVisitor(Visitor): """Handles (subscript) MathML elements. Examples: Simple: x1 → x_{1} Index: ai → a_{i} """ def __init__(self, strategy: Strategy, context: ConversionContext = None): """ Initialize the Msub visitor. Args: strategy: The formatting strategy to use context: Shared conversion context for state tracking """ super().__init__(strategy) self.context = context def visit(self, node: ET.Element) -> str: if not self.can_handle(node): raise ValueError(f"MsubVisitor cannot handle element '{node.tag}'") children = list(node) if len(children) < 2: raise ValueError(f"msub element must have 2 children, got {len(children)}") # Convert base (not in script context) base = self._convert_child(children[0]) # Push script context before converting subscript if self.context: self.context.push_script_context('msub') try: # Convert subscript (in script context) subscript = self._convert_child(children[1]) finally: # Always pop context, even if conversion fails if self.context: self.context.pop_script_context() # Special case: Empty subscript - just return the base if not subscript or not subscript.strip(): base = self._wrap_if_needed(base) return base base = self._wrap_if_needed(base) # Special case: If base contains a superscript (like f^{\prime}), # wrap it in braces to prevent syntax errors # Example: f^{\prime} should become {f^{\prime}}_{x}, not f^{\prime}_{x} if self._contains_superscript(base): base = f"{{{base}}}" return f"{base}_{self._brace(subscript)}" def _convert_child(self, child: ET.Element) -> str: """Convert a child element to LaTeX.""" tag = _local(child.tag) # Import visitors as needed to avoid circular imports if tag == 'mi': from .mi_visitor import MiVisitor return MiVisitor(self.strategy, self.context).visit(child) elif tag == 'mo': from .mo_visitor import MoVisitor return MoVisitor(self.strategy).visit(child) elif tag == 'mn': from .mn_visitor import MnVisitor return MnVisitor(self.strategy).visit(child) elif tag == 'mfrac': from .mfrac_visitor import MfracVisitor return MfracVisitor(self.strategy, self.context).visit(child) elif tag == 'msup': from .msup_visitor import MsupVisitor return MsupVisitor(self.strategy).visit(child) elif tag == 'msub': # Recursive: msub can contain other msub elements return self.visit(child) elif tag == 'mrow': from .mrow_visitor import MrowVisitor return MrowVisitor(self.strategy, self.context).visit(child) elif tag == 'msqrt': from .msqrt_visitor import MsqrtVisitor return MsqrtVisitor(self.strategy).visit(child) elif tag == 'mroot': from .mroot_visitor import MrootVisitor return MrootVisitor(self.strategy).visit(child) elif tag == 'msubsup': from .msubsup_visitor import MsubsupVisitor return MsubsupVisitor(self.strategy).visit(child) elif tag == 'munder': from .munder_visitor import MunderVisitor return MunderVisitor(self.strategy).visit(child) elif tag == 'mover': from .mover_visitor import MoverVisitor return MoverVisitor(self.strategy).visit(child) elif tag == 'munderover': from .munderover_visitor import MunderoverVisitor return MunderoverVisitor(self.strategy).visit(child) elif tag == 'mfenced': from .mfenced_visitor import MfencedVisitor return MfencedVisitor(self.strategy).visit(child) elif tag == 'mtable': from .mtable_visitor import MtableVisitor return MtableVisitor(self.strategy).visit(child) elif tag == 'mtext': from .mtext_visitor import MtextVisitor return MtextVisitor(self.strategy).visit(child) elif tag == 'mstyle': from .mstyle_visitor import MstyleVisitor return MstyleVisitor(self.strategy).visit(child) elif tag == 'mspace': from .mspace_visitor import MspaceVisitor return MspaceVisitor(self.strategy).visit(child) elif tag == 'menclose': from .menclose_visitor import MencloseVisitor return MencloseVisitor(self.strategy).visit(child) elif tag == 'mmultiscripts': from .mmultiscripts_visitor import MmultiscriptsVisitor return MmultiscriptsVisitor(self.strategy).visit(child) elif tag == 'maction': from .maction_visitor import MactionVisitor return MactionVisitor(self.strategy).visit(child) elif tag == 'merror': from .merror_visitor import MerrorVisitor return MerrorVisitor(self.strategy).visit(child) elif tag == 'mpadded': from .mpadded_visitor import MpaddedVisitor return MpaddedVisitor(self.strategy).visit(child) elif tag == 'mphantom': from .mphantom_visitor import MphantomVisitor return MphantomVisitor(self.strategy).visit(child) elif tag == 'ms': from .ms_visitor import MsVisitor return MsVisitor(self.strategy).visit(child) elif tag == 'mglyph': from .mglyph_visitor import MglyphVisitor return MglyphVisitor(self.strategy).visit(child) elif tag == 'maligngroup': from .maligngroup_visitor import MaligngroupVisitor return MaligngroupVisitor(self.strategy).visit(child) elif tag == 'malignmark': from .malignmark_visitor import MalignmarkVisitor return MalignmarkVisitor(self.strategy).visit(child) elif tag == 'mlabeledtr': from .mlabeledtr_visitor import MlabeledtrVisitor return MlabeledtrVisitor(self.strategy).visit(child) else: # Fallback: return text content for unknown elements import sys print(f"[WARNING] Unknown element <{tag}> in msub, using text fallback", file=sys.stderr) return _text_content(child) def _wrap_if_needed(self, base: str) -> str: """Wrap base in parentheses if needed, avoiding double-wrapping.""" # Check if base already has delimiters (starts and ends with matching delimiters) base_stripped = base.strip() if base_stripped: # Check for matching delimiters delimiter_pairs = { '(': ')', '[': ']', '{': '}', '\\left(': '\\right)', '\\left[': '\\right]', '\\left\\{': '\\right\\}', } for open_delim, close_delim in delimiter_pairs.items(): if base_stripped.startswith(open_delim) and base_stripped.endswith(close_delim): # Already has delimiters, no need to wrap return base # Check if base needs grouping for proper subscript application # In LaTeX, x_2 applies subscript to x, but "a + b_2" applies only to b # We need to brace multi-element bases: {a + b}_2 # # Use braces {} for grouping (invisible) not parentheses () (visible) # Only wrap if base contains operators that indicate a true multi-element expression # Don't wrap for spaces alone (e.g., "\Delta H" has space but is semantically single) math_operators = {'=', '+', '-', '/', '*', '\\times', '\\cdot', '\\pm', '\\to'} if ' ' in base and any(op in base for op in math_operators): return f"{{{base}}}" return base def _brace(self, content: str) -> str: return f"{{{content}}}" def _contains_superscript(self, base: str) -> bool: """ Check if base contains a LaTeX superscript (^{...} or ^X). This is important for proper grouping when adding subscripts. For example: f^{\\prime} must be wrapped as {f^{\\prime}} before adding subscripts to avoid invalid syntax. Args: base: The base LaTeX string Returns: True if base contains an unbraced superscript """ if not base or '^' not in base: return False # If the entire base is already wrapped in braces, it's safe base_stripped = base.strip() if base_stripped.startswith('{') and base_stripped.endswith('}'): # Check if these braces wrap the entire expression depth = 0 for i, char in enumerate(base_stripped): if char == '{': depth += 1 elif char == '}': depth -= 1 # If depth hits 0 before the end, the braces don't wrap everything if depth == 0 and i < len(base_stripped) - 1: break # If we maintained depth > 0 until the last character, it's wrapped if i == len(base_stripped) - 1: return False # Base contains ^ and is not fully wrapped return True def can_handle(self, node: ET.Element) -> bool: return _local(node.tag) == 'msub' @property def element_name(self) -> str: return 'msub' def __repr__(self) -> str: return f"MsubVisitor(strategy={self.strategy.name})"