r""" Visitor for (tensor notation) MathML elements. The element is used to attach prescripts and postscripts to a base element. It's commonly used in physics and chemistry for tensor notation, nuclear notation, etc. Structure: This visitor handles both prescripts (left side) and postscripts (right side) with \sideset. """ from lxml import etree as ET from ..core.interfaces import Visitor, Strategy from ..utils import local_name as _local class MmultiscriptsVisitor(Visitor): r""" Handles (tensor notation) MathML elements. Converts prescripts and postscripts to LaTeX using \sideset for bidirectional scripts. Examples: Only postscripts: R 12 → R_{1}^{2} Only prescripts: R 12 → _{1}^{2}R Both prescripts and postscripts: R 34 12 → \sideset{_{1}^{2}}{_{3}^{4}}{R} Multiple postscripts: R ij kl → R_{i}^{j}_{k}^{l} None elements (empty script positions): R 2 1 → \sideset{_{1}}{^{2}}{R} """ def __init__(self, strategy: Strategy): """Initialize the Mmultiscripts visitor.""" super().__init__(strategy) def visit(self, node: ET.Element) -> str: r""" Convert element to LaTeX with tensor notation. Args: node: The element to convert Returns: LaTeX string with prescripts and postscripts Raises: ValueError: If node is not a element """ if not self.can_handle(node): raise ValueError(f"MmultiscriptsVisitor cannot handle element '{node.tag}'") if len(node) == 0: return "" # First child is the base element base = self._convert_child(node[0]) scripts = list(node)[1:] # Skip base element # Find mprescripts separator prescript_idx = None for i, child in enumerate(scripts): if _local(child.tag) == "mprescripts": prescript_idx = i break if prescript_idx is None: # Only postscripts postscripts = scripts prescripts = [] else: postscripts = scripts[:prescript_idx] prescripts = scripts[prescript_idx + 1:] # Process postscripts (pairs of sub/sup) postscript_parts = self._process_script_pairs(postscripts) # Process prescripts (pairs of sub/sup) prescript_parts = self._process_script_pairs(prescripts) # Use \sideset when there are scripts on both sides if prescript_parts and postscript_parts: left_scripts = "".join(prescript_parts) right_scripts = "".join(postscript_parts) return f"\\sideset{{{left_scripts}}}{{{right_scripts}}}{{{base}}}" elif prescript_parts: # Only left scripts - direct positioning return f"{''.join(prescript_parts)}{base}" elif postscript_parts: # Only right scripts return f"{base}{''.join(postscript_parts)}" else: return base def _process_script_pairs(self, scripts: list) -> list: """ Process script pairs (sub/sup) from the scripts list. Scripts come in pairs: subscript, superscript, subscript, superscript, ... elements represent empty positions. Args: scripts: List of script elements (pairs of sub/sup) Returns: List of LaTeX script strings """ parts = [] for i in range(0, len(scripts), 2): sub = self._convert_script(scripts[i]) if i < len(scripts) else "" sup = self._convert_script(scripts[i + 1]) if i + 1 < len(scripts) else "" if sub and sup: parts.append(f"_{{{sub}}}^{{{sup}}}") elif sub: parts.append(f"_{{{sub}}}") elif sup: parts.append(f"^{{{sup}}}") return parts def _convert_script(self, child: ET.Element) -> str: """ Convert a script element to LaTeX. Handles elements (empty script positions). Args: child: Script element to convert Returns: LaTeX string for the script (empty for ) """ if _local(child.tag) == "none": return "" return self._convert_child(child) 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).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).visit(child) elif tag == 'msup': from .msup_visitor import MsupVisitor return MsupVisitor(self.strategy).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).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 mmultiscripts, using text fallback", file=sys.stderr) return _text_content(child) def can_handle(self, node: ET.Element) -> bool: """Check if this visitor can handle the given node.""" return _local(node.tag) == 'mmultiscripts' @property def element_name(self) -> str: """Get the element name this visitor handles.""" return 'mmultiscripts' def __repr__(self) -> str: """String representation for debugging.""" return f"MmultiscriptsVisitor(strategy={self.strategy.name})"