r""" Visitor for (box/cancel notation) MathML elements. The element is used to enclose mathematical content with various notations including boxes, circles, strike-throughs, and partial frames. This visitor handles notation attribute values like: - box: Full box around content - circle: Circle around content - horizontalstrike, verticalstrike: Strike-through lines - updiagonalstrike, downdiagonalstrike: Diagonal strike-through - top, bottom, left, right: Partial box sides - actuarial, longdiv, radical: Specialized notations """ from lxml import etree as ET from ..core.interfaces import Visitor, Strategy from ..utils import local_name as _local class MencloseVisitor(Visitor): r""" Handles (box/cancel notation) MathML elements. Converts various notation attributes to appropriate LaTeX commands. Examples: Box notation: x → \boxed{x} Horizontal strike: x → \cancel{x} Diagonal strike (up): x → \cancel{x} Diagonal strike (down): x → \bcancel{x} Combined diagonal strikes: x → \xcancel{x} Top line only: x → \overline{x} Bottom line only: x → \underline{x} Left delimiter: x → \left| x \right. Right delimiter: x → \left. x \right| Circle notation: x → \circled{x} Actuarial notation: x → \overline{\underline{x}} Long division: x → \enclose{longdiv}{x} Radical notation: x → \sqrt{x} """ def __init__(self, strategy: Strategy): """Initialize the Menclose visitor.""" super().__init__(strategy) def visit(self, node: ET.Element) -> str: r""" Convert element to LaTeX with appropriate notation. Args: node: The element to convert Returns: LaTeX string with notation applied to content Raises: ValueError: If node is not a element """ if not self.can_handle(node): raise ValueError(f"MencloseVisitor cannot handle element '{node.tag}'") notation = node.get("notation", "box") # Special case: box containing mfrac with underlined/overlined numerator (like chemical reaction conditions) # Pattern: text‾ # Remove the box but preserve the fraction structure: \dfrac{\underline{text}}{} # Also handles: ... mfrac_node = None if notation == "box" and len(node) == 1: child = node[0] child_tag = _local(child.tag) # Direct mfrac child if child_tag == "mfrac": mfrac_node = child # mfrac wrapped in mrow (Word often does this) elif child_tag == "mrow" and len(child) == 1 and _local(child[0].tag) == "mfrac": mfrac_node = child[0] if mfrac_node is not None and len(mfrac_node) >= 2: numerator_node = mfrac_node[0] # Check if numerator contains underlined text (munder with line character) if self._contains_underlined_text(numerator_node): # Convert the fraction directly without the box # This preserves the \dfrac structure with underlined numerator return self._convert_child(mfrac_node) content = self._convert_children(node) # Skip boxing for assignment expressions if notation == "box" and self._contains_assignment(content): return content # Parse notation (could be space-separated multi-value) notations = notation.split() # === COMBINED STRIKE NOTATIONS (check these FIRST) === # Combined diagonal strikes → \xcancel if "updiagonalstrike" in notations and "downdiagonalstrike" in notations: return f"\\xcancel{{{content}}}" # Combined horizontal and vertical strikes → \cancel if "horizontalstrike" in notations and "verticalstrike" in notations: return f"\\cancel{{{content}}}" # === SIDE-SPECIFIC NOTATIONS FOR PARTIAL BOXES === sides = [n for n in notations if n in ["top", "bottom", "left", "right"]] side_count = len(sides) # Single side visible if side_count == 1: side = sides[0] if side == "top": return f"\\overline{{{content}}}" elif side == "bottom": return f"\\underline{{{content}}}" elif side == "left": return f"\\left| {content} \\right." elif side == "right": return f"\\left. {content} \\right|" # Full box (4 sides or explicit "box" notation) elif "box" in notations or side_count == 4: return f"\\boxed{{{content}}}" # Partial box (2-3 sides) elif side_count >= 2: notation_str = " ".join(sides) return f"\\enclose{{{notation_str}}}{{{content}}}" # === INDIVIDUAL STRIKE NOTATIONS === if notation == "horizontalstrike": return f"\\cancel{{{content}}}" if notation == "verticalstrike": return f"\\bcancel{{{content}}}" if notation == "updiagonalstrike": return f"\\cancel{{{content}}}" if notation == "downdiagonalstrike": return f"\\bcancel{{{content}}}" # === OTHER NOTATIONS === if notation == "circle": return f"\\circled{{{content}}}" if notation == "roundedbox": return f"\\boxed{{{content}}}" if notation == "actuarial": return f"\\overline{{\\underline{{{content}}}}}" if notation == "longdiv": return f"\\enclose{{longdiv}}{{{content}}}" if notation == "radical": return f"\\sqrt{{{content}}}" # Default: just return content without notation return content def _convert_children(self, node: ET.Element) -> str: """ Convert all child elements to LaTeX. Args: node: The element Returns: LaTeX string with all children converted """ parts = [] for child in node: parts.append(self._convert_child(child)) return ''.join(parts).strip() 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 menclose, using text fallback", file=sys.stderr) return _text_content(child) def _is_empty_denominator(self, node: ET.Element) -> bool: """ Check if denominator node is empty (no content or only whitespace). Args: node: MathML denominator element to check Returns: True if denominator is empty """ if node is None: return True tag = _local(node.tag) # Empty tag if tag == "mo": text = node.text or "" return text.strip() == "" # Empty tag if tag == "mrow": # No children if len(node) == 0: return True # Only whitespace child if len(node) == 1 and _local(node[0].tag) == "mo": mo_text = node[0].text or "" return mo_text.strip() == "" return False def _contains_assignment(self, content: str) -> bool: """ Check if content contains assignment operators. Assignment expressions should not be boxed. Note: We only skip boxing for explicit assignment operators like := NOT for regular equality = which is often used in boxed theorems. Args: content: LaTeX content to check Returns: True if content contains assignment operators """ # Only specific assignment operators - NOT plain = which appears in boxed theorems assignment_ops = [":=", ": =", "∶=", "∶ =", "≔", "\\coloneqq"] return any(op in content for op in assignment_ops) def _contains_underlined_text(self, node: ET.Element) -> bool: """ Check if node contains underlined text (munder with line character) OR overlined text (mover with line character). This is used to detect chemical reaction conditions and malformed equals. Args: node: MathML element to check Returns: True if node contains underlined or overlined text """ if node is None: return False tag = _local(node.tag) # Check if this node itself is an underlined element (munder with line) if tag == "munder" and len(node) > 1: under_node = node[1] if _local(under_node.tag) == "mo": # Get text content text = under_node.text or "" if "‾" in text: # Line character return True # Check if this node is an overlined element (mover with line) # This handles the "malformed equal" pattern where users put overline in fraction if tag == "mover" and len(node) > 1: over_node = node[1] if _local(over_node.tag) == "mo": # Get text content text = over_node.text or "" # Check for overline/bar characters if text.strip() in ["¯", "‾", "―", "−", "⎺", "⎻", "-", "_"]: return True # Recursively check children for child in node: if self._contains_underlined_text(child): return True return False def can_handle(self, node: ET.Element) -> bool: """Check if this visitor can handle the given node.""" return _local(node.tag) == 'menclose' @property def element_name(self) -> str: """Get the element name this visitor handles.""" return 'menclose' def __repr__(self) -> str: """String representation for debugging.""" return f"MencloseVisitor(strategy={self.strategy.name})"