""" Visitor for (superscript) MathML elements. The element represents superscripts in MathML: - Powers: x^2, x^n - Exponents: e^x, 10^{-3} - Indices: a^i, T^{ij} - Prime notation: f', f'', f''' (derivatives) """ 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 MsupVisitor(Visitor): """ Handles (superscript) MathML elements. This visitor converts superscripts to LaTeX format using ^ notation. Special handling for prime notation (derivatives). Examples: Simple power: x2 → x^{2} Exponent: ex → e^{x} Complex base: x+1 2 → (x + 1)^{2} Complex exponent: x2n → x^{2 n} Prime notation (derivatives): f′ → f' f″ → f'' y‴ → y''' """ def __init__(self, strategy: Strategy, context: ConversionContext = None): """ Initialize the Msup 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: """ Convert element to LaTeX. Args: node: The element to convert Returns: LaTeX superscript string Raises: ValueError: If node is not an element ValueError: If msup has fewer than 2 children """ if not self.can_handle(node): raise ValueError(f"MsupVisitor cannot handle element '{node.tag}'") children = list(node) if len(children) < 2: raise ValueError( f"msup element must have 2 children (base, superscript), " f"got {len(children)}" ) # NEW: Check for nested msup and merge if needed # Pattern: base # Should become: base^{sup1sup2} instead of base^{sup1}^{sup2} nested_msup = self._detect_nested_msup(children[0]) if nested_msup is not None: result = self._merge_nested_superscripts(nested_msup, children[1]) if result: base_latex, merged_sup_latex = result base_latex = self._wrap_if_needed(base_latex) return f"{base_latex}^{self._brace(merged_sup_latex)}" # Convert base (not in script context) base = self._convert_child(children[0]) # Push script context before converting superscript if self.context: self.context.push_script_context('msup') try: # Convert superscript (in script context) superscript = self._convert_child(children[1]) finally: # Always pop context, even if conversion fails if self.context: self.context.pop_script_context() # Special case: Empty superscript - just return the base # This can happen when OMML has with an empty if not superscript or not superscript.strip(): base = self._wrap_if_needed(base) return base # Special case: Prime notation (\prime → ^{\prime}, \prime\prime → ^{\prime\prime}, etc.) # If superscript contains only \prime symbols, convert to proper LaTeX prime syntax if self._is_prime_notation(superscript): # Wrap base if needed (complex expressions) base = self._wrap_if_needed(base) # Remove spaces from prime notation clean_primes = superscript.replace(' ', '').replace('\xa0', '') # Convert to proper superscript form: ^{\prime}, ^{\prime\prime}, etc. return f"{base}^{{{clean_primes}}}" # Special case: cancelto pattern (menclose with updiagonalstrike + superscript) # Pattern: contenttarget # Should become: \cancelto{target}{content} instead of \cancel{content}^{target} if self._is_cancel_with_target(children[0], base): # Extract content from \cancel{content} content = self._extract_cancel_content(base) if content: return f"\\cancelto{self._brace(superscript)}{self._brace(content)}" # TODO: More special cases # - Empty base (just return ^{sup}) # - Omit ^{1} for simple variables # - Large operators with \limits # Wrap base if needed (complex expressions) base = self._wrap_if_needed(base) # Return base^{superscript} return f"{base}^{self._brace(superscript)}" 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': # Recursive: msup can contain other msup elements return self.visit(child) elif tag == 'msub': from .msub_visitor import MsubVisitor return MsubVisitor(self.strategy).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 msup, using text fallback", file=sys.stderr) return _text_content(child) def _wrap_if_needed(self, base: str) -> str: """ Wrap base in parentheses if needed. Args: base: The base content Returns: Base, optionally wrapped in parentheses Note: Complex expressions with operators should be wrapped. Simple identifiers and numbers don't need wrapping. """ # Check if base already has delimiters (starts and ends with matching delimiters) base_stripped = base.strip() if base_stripped: # Check for __DELIM__ markers (used by delimiter pairing processor) # Pattern: __DELIM__X__DELIM__ ... __DELIM__Y__DELIM__ # These will be converted to \left...\right by post-processor if base_stripped.startswith('__DELIM__') and '__DELIM__' in base_stripped[9:]: # Already has delimiter markers, no need to wrap return base # 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 superscript application # In LaTeX, x^2 applies superscript 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 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: """Wrap content in braces.""" return f"{{{content}}}" def _is_prime_notation(self, superscript: str) -> bool: """ Check if superscript is prime notation (only \\prime symbols). Prime notation in LaTeX: - x^{\\prime} (single prime) - x^{\\prime\\prime} (double prime) - x^{\\prime\\prime\\prime} (triple prime) - x^{\\prime\\prime\\prime\\prime} (quadruple prime) Args: superscript: The converted superscript LaTeX string Returns: True if superscript contains only \\prime symbols Note: Handles spaced primes from mrow conversion (e.g., "\\prime \\prime") which should become "\\prime\\prime" """ # Remove ALL whitespace no_spaces = superscript.replace(' ', '').replace('\xa0', '') if not no_spaces: return False # Check if content is only \prime symbols (possibly repeated) # Valid patterns: \prime, \prime\prime, \prime\prime\prime, etc. # The string should be exactly \prime repeated N times if not no_spaces.startswith('\\prime'): return False # Remove all \prime occurrences and check if anything remains test = no_spaces.replace('\\prime', '') return len(test) == 0 def _is_cancel_with_target(self, base_node: ET.Element, base_latex: str) -> bool: """ Check if base is a cancel/strike-through that should use cancelto. Args: base_node: The base element (first child of msup) base_latex: The converted LaTeX for the base Returns: True if this is a cancel pattern that should use \\cancelto """ # Check if base_node is menclose with updiagonalstrike notation if _local(base_node.tag) == 'menclose': notation = base_node.get('notation', '') return 'updiagonalstrike' in notation # Check if base_node is mrow containing a single menclose if _local(base_node.tag) == 'mrow': children = list(base_node) # If mrow contains exactly one child and it's a menclose if len(children) == 1 and _local(children[0].tag) == 'menclose': notation = children[0].get('notation', '') return 'updiagonalstrike' in notation return False def _extract_cancel_content(self, base_latex: str) -> str: """ Extract content from \\cancel{content}, \\xcancel{content}, etc. Args: base_latex: LaTeX string containing cancel command Returns: The content inside the cancel braces, or empty string if not a cancel pattern Examples: \\cancel{x} → x \\xcancel{abc} → abc \\bcancel{123} → 123 """ import re # Pattern: \cancel{content}, \xcancel{content}, \bcancel{content} match = re.match(r'\\(?:x|b)?cancel\{(.+?)\}$', base_latex) if match: return match.group(1) return '' def _detect_nested_msup(self, base_node: ET.Element): """ Detect if base contains a nested msup element. Handles two patterns: 1. Direct nested: ...... 2. Wrapped in mrow: ...... Args: base_node: The base element (first child of outer msup) Returns: The nested msup element if found, None otherwise """ # Check direct nested msup if _local(base_node.tag) == 'msup': return base_node # Check msup wrapped in mrow (most common pattern) if _local(base_node.tag) == 'mrow': children = list(base_node) # If mrow contains exactly one child and it's an msup if len(children) == 1 and _local(children[0].tag) == 'msup': return children[0] return None def _merge_nested_superscripts(self, inner_msup: ET.Element, outer_sup_node: ET.Element): """ Merge nested msup with outer superscript. This handles the pattern: Which should become: base^{sup1sup2} instead of base^{sup1}^{sup2} Args: inner_msup: The nested element outer_sup_node: The outer superscript element Returns: Tuple of (base_latex, merged_superscript_latex) or None if invalid Examples: n2+ → ("\\mathrm{n}", "2+") """ # Extract inner msup children inner_children = list(inner_msup) if len(inner_children) < 2: return None # Invalid structure inner_base = inner_children[0] inner_sup = inner_children[1] # Convert base (not in script context) base_latex = self._convert_child(inner_base) # Convert both superscripts in script context if self.context: self.context.push_script_context('msup') try: inner_sup_latex = self._convert_child(inner_sup) outer_sup_latex = self._convert_child(outer_sup_node) finally: if self.context: self.context.pop_script_context() # Merge superscripts: {inner}{outer} # Example: inner="2", outer="+" → "2+" merged_sup = f"{inner_sup_latex}{outer_sup_latex}" return (base_latex, merged_sup) def can_handle(self, node: ET.Element) -> bool: """Check if this visitor can handle the given node.""" return _local(node.tag) == 'msup' @property def element_name(self) -> str: """Get the element name this visitor handles.""" return 'msup' def __repr__(self) -> str: """String representation for debugging.""" return f"MsupVisitor(strategy={self.strategy.name})"