""" Visitor for (fraction) MathML elements. The element represents fractions in MathML: - Standard fractions: \\frac{numerator}{denominator} - Display fractions: \\dfrac{numerator}{denominator} - Bevelled fractions (line fractions): numerator/denominator """ 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 MfracVisitor(Visitor): """ Handles (fraction) MathML elements. This visitor converts fractions to LaTeX format using \\frac or \\dfrac commands depending on context. Examples: Standard fraction: 12 → \\dfrac{1}{2} Algebraic fraction: xy → \\dfrac{x}{y} Complex fraction: x+1 y-1 → \\dfrac{x + 1}{y - 1} Bevelled fraction (bevelled="true"): 12 → 1/2 No-bar stacked fraction (linethickness="0"): nk → \genfrac{}{}{0pt}{}{n}{k} Note: For full compatibility with the original converter, we need to: 1. Detect xlongequal pattern (special chemistry notation) 2. Handle script context (use \\frac in superscripts/subscripts) 3. Handle display style context (use \\dfrac in display mode) Currently implements basic functionality. Advanced features TODO. """ def __init__(self, strategy: Strategy, context: ConversionContext = None): """ Initialize the Mfrac visitor. Args: strategy: The formatting strategy to use context: Shared conversion context for state tracking """ super().__init__(strategy) self.context = context # We'll need a reference to the visitor registry to convert children # This will be set by the conversion stage self._visitor_registry = None def set_visitor_registry(self, registry): """ Set the visitor registry for converting child elements. Args: registry: The VisitorRegistry instance Note: This is called by the conversion stage to enable child conversion. """ self._visitor_registry = registry def visit(self, node: ET.Element) -> str: """ Convert element to LaTeX. Args: node: The element to convert Returns: LaTeX fraction string Raises: ValueError: If node is not an element ValueError: If mfrac has fewer than 2 children """ if not self.can_handle(node): raise ValueError(f"MfracVisitor cannot handle element '{node.tag}'") # mfrac should have exactly 2 children: numerator and denominator children = list(node) if len(children) < 2: # Malformed mfrac - handle gracefully for MathJax compatibility # \frac{} or \frac{x}{} should work; \frac without braces won't if len(children) == 0: return "\\dfrac{}{}" elif len(children) == 1: # Only numerator provided numerator = self._convert_child(children[0]) return f"\\dfrac{{{numerator}}}{{}}" # Check for xlongequal pattern with === in denominator FIRST (BUG-009 fix) # This MUST come before linethickness check because noBar fractions # with === in denominator should produce \xlongequal, not bevelled fraction # Pattern: 高温=== if self._is_xlongequal_with_equals_denominator(children): # Extract text from numerator (goes in braces) numerator = self._convert_child(children[0]) return f"\\xlongequal[\\quad]{{{numerator}}}" # Check for xlongequal pattern WITH both upper and lower text # Pattern: ‾ # This represents \xlongequal[text2]{text1} (text above and below equals) if self._is_xlongequal_with_both_texts(children): # Extract text from numerator (goes in braces) and denominator (goes in brackets) upper_text = self._extract_reaction_condition_text(children[0]) lower_text = self._convert_child(children[1]) # Strip any \mathrm{} or \text{} wrappers from both texts # Note: The post-processing will handle space removal lower_clean = self._strip_latex_wrappers(lower_text) upper_clean = self._strip_latex_wrappers(upper_text) return f"\\xlongequal[{lower_clean}]{{{upper_clean}}}" # Check for xlongequal pattern (chemistry reaction condition) # Pattern: ‾ # This is Word's way of representing reaction conditions like "点燃" above an arrow if self._is_chemical_reaction_condition(children): # Extract the text from the underlined numerator condition_text = self._extract_reaction_condition_text(children[0]) return f"\\xlongequal[\\quad]{{{condition_text}}}" # Check for bevelled fraction (bevelled="true" attribute triggers inline fraction) # Note: bevelled="true" means inline fraction with slash (a/b) bevelled = node.get('bevelled') if bevelled == 'true': return self._convert_bevelled_fraction(children) # Check for no-bar stacking (linethickness="0") # Note: linethickness="0" means no-bar stacking (like binomial), NOT bevelled # This is for binomial-style stacking without a fraction bar linethickness = node.get('linethickness') if linethickness == '0': return self._convert_nobar_stack(children) # Convert numerator and denominator numerator = self._convert_child(children[0]) denominator = self._convert_child(children[1]) # Context-aware fraction command selection # Priority: Script context > display style context > default if self.context and self.context.in_script_context(): # Use \frac in script contexts (superscript/subscript) for proper sizing frac_command = "\\frac" elif self.context and self.context.display_style_context: # Use \dfrac if in display style context (from mstyle) frac_command = "\\dfrac" else: # Default to \dfrac for main equations frac_command = "\\dfrac" # Wrap in braces and return return f"{frac_command}{self._brace(numerator)}{self._brace(denominator)}" def _convert_bevelled_fraction(self, children: list) -> str: """ Convert bevelled (inline) fraction. Bevelled fractions display as numerator/denominator in a single line. This is triggered by bevelled="true" attribute. Note: This is different from linethickness="0" which produces stacking without a fraction bar (like binomial coefficients). Args: children: List of child elements [numerator, denominator] Returns: LaTeX string with / separator Examples: 1/2, x/y, (x+1)/(y-1) """ numerator = self._convert_child(children[0]) denominator = self._convert_child(children[1]) # For bevelled fractions, use simple / separator # May need parentheses for complex expressions # TODO: Add intelligent parenthesization return f"{numerator}/{denominator}" def _convert_nobar_stack(self, children: list) -> str: """ Convert no-bar stacked fraction (linethickness="0"). This is for stacking like binomial coefficients where numerator is above denominator but without a fraction bar. Args: children: List of child elements [numerator, denominator] Returns: LaTeX string for stacked notation Note: We use \\begin{array}{c}...\\end{array} for proper stacking. This is more user-friendly and widely understood than \\genfrac syntax. Examples: nk → \\begin{array}{c} n \\\\ k \\end{array} """ numerator = self._convert_child(children[0]) denominator = self._convert_child(children[1]) # Use array environment for proper no-bar stacking # Format: \begin{array}{c} top \\ bottom \end{array} # Center-aligned single column with line break between elements # Note: No space after \\ to avoid it being converted to \~ return f"\\begin{{array}}{{c}} {numerator} \\\\\\\\{denominator} \\end{{array}}" def _convert_child(self, child: ET.Element) -> str: """ Convert a child element to LaTeX. Args: child: The child element to convert Returns: LaTeX string Note: For now, we directly instantiate the appropriate visitor. In the future, this should use the visitor registry. """ from ..utils import local_name as _local tag = _local(child.tag) if tag == 'mi': from .mi_visitor import MiVisitor visitor = MiVisitor(self.strategy) return visitor.visit(child) elif tag == 'mo': from .mo_visitor import MoVisitor visitor = MoVisitor(self.strategy) return visitor.visit(child) elif tag == 'mn': from .mn_visitor import MnVisitor visitor = MnVisitor(self.strategy) return visitor.visit(child) elif tag == 'mrow': from .mrow_visitor import MrowVisitor return MrowVisitor(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 == 'msubsup': from .msubsup_visitor import MsubsupVisitor return MsubsupVisitor(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 == '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 for unknown elements: return text content import sys print(f"[WARNING] Unknown element <{tag}> in mfrac, using text fallback", file=sys.stderr) return _text_content(child) def _brace(self, content: str) -> str: """ Wrap content in braces if needed. Args: content: The content to wrap Returns: Content wrapped in braces Note: Always wraps in braces for fractions to ensure proper LaTeX parsing. """ return f"{{{content}}}" def _is_chemical_reaction_condition(self, children: list) -> bool: """ Check if this fraction represents a chemical reaction condition. Pattern: numerator is munder with underline OR mover with overline, denominator is empty/whitespace Args: children: [numerator, denominator] child elements Returns: True if this is a chemical reaction condition pattern """ if len(children) != 2: return False numerator = children[0] denominator = children[1] # Unwrap mrow if present (Word often wraps in mrow) numerator_tag = _local(numerator.tag) if numerator_tag == "mrow" and len(numerator) == 1: numerator = numerator[0] numerator_tag = _local(numerator.tag) # Check if numerator is munder with underline OR mover with overline if numerator_tag in ("munder", "mover") and len(numerator) >= 2: line_node = numerator[1] # Second child is the line (under or over) if _local(line_node.tag) == "mo": text = line_node.text or "" # Check for line character (same character used for both over and under) if "‾" in text or text.strip() in ["¯", "―", "−", "_", "-"]: # Check if denominator is empty denom_tag = _local(denominator.tag) # Empty tag if denom_tag == "mo": denom_text = denominator.text or "" if denom_text.strip() == "": return True # Empty tag elif denom_tag == "mrow": # Check if mrow has no children or only whitespace if len(denominator) == 0: return True # Check if mrow has only whitespace text nodes if len(denominator) == 1 and _local(denominator[0].tag) == "mo": mo_text = denominator[0].text or "" if mo_text.strip() == "": return True return False def _extract_reaction_condition_text(self, numerator_node: ET.Element) -> str: """ Extract text from underlined/overlined numerator for reaction condition. Args: numerator_node: The munder or mover element (or mrow wrapping it) containing the condition text Returns: LaTeX content with user-specified formatting preserved (e.g., \\mathbf{}) but subject-determined formatting removed (e.g., \\mathrm{}) """ numerator_tag = _local(numerator_node.tag) # Unwrap mrow if present if numerator_tag == "mrow" and len(numerator_node) == 1: numerator_node = numerator_node[0] numerator_tag = _local(numerator_node.tag) # Handle both munder and mover if numerator_tag not in ("munder", "mover"): return "" # The first child contains the actual content (same for both munder and mover) if len(numerator_node) > 0: content_node = numerator_node[0] # Convert to LaTeX, preserving user-specified formatting (like \mathbf{}) content = self._convert_child(content_node) # Only strip subject-determined formatting (\mathrm{} for chemistry mode) # Preserve user-specified formatting like \mathbf{}, \textbf{}, \textit{}, etc. if content.startswith("\\mathrm{") and content.endswith("}"): content = content[8:-1] # Remove \mathrm{ and } # Strip \text{} wrapper if present elif content.startswith("\\text{") and content.endswith("}"): content = content[6:-1] # Remove \text{ and } # Clean up extra spaces return content.strip() return "" def _is_xlongequal_with_equals_denominator(self, children: list) -> bool: """ Check if this fraction represents xlongequal with === in denominator. Pattern: numerator has content, denominator contains only consecutive equals signs This pattern is used in chemistry for reaction arrows with conditions above. Args: children: [numerator, denominator] child elements Returns: True if denominator contains only === (or ==, ====, etc.) Examples: 高温 === → \\xlongequal[\\quad]{高温} heat == → \\xlongequal[\\quad]{heat} """ if len(children) != 2: return False numerator = children[0] denominator = children[1] # Get text content from denominator denom_text = _text_content(denominator).strip() # Check if denominator contains only equals signs (2 or more) # Pattern: ==, ===, ====, = = =, etc. (with or without spaces) import re # Remove all spaces to check if it's only equals signs denom_text_no_spaces = denom_text.replace(' ', '') if denom_text_no_spaces and re.match(r'^=+$', denom_text_no_spaces) and len(denom_text_no_spaces) >= 2: # Check if numerator has actual content num_text = _text_content(numerator).strip() if num_text and num_text != "": return True return False def _is_xlongequal_with_both_texts(self, children: list) -> bool: """ Check if this fraction represents xlongequal with both upper and lower text. Pattern: numerator is munder with underline OR mover with overline, denominator has actual content Args: children: [numerator, denominator] child elements Returns: True if this is xlongequal with both texts pattern Examples: abc‾ defg → \\xlongequal[defg]{abc} abc‾ defg → \\xlongequal[defg]{abc} abc‾ defg → \\xlongequal[defg]{abc} """ if len(children) != 2: return False numerator = children[0] denominator = children[1] # Unwrap mrow if present (Word often wraps in mrow) numerator_tag = _local(numerator.tag) if numerator_tag == "mrow" and len(numerator) == 1: numerator = numerator[0] numerator_tag = _local(numerator.tag) # Check if numerator is munder with underline OR mover with overline if numerator_tag in ("munder", "mover") and len(numerator) >= 2: line_node = numerator[1] # Second child is the line (under or over) if _local(line_node.tag) == "mo": text = line_node.text or "" # Check for line character (same character used for both over and under) if "‾" in text or text.strip() in ["¯", "―", "−", "_", "-"]: # Check if denominator is NOT empty (has actual content) denom_text = _text_content(denominator).strip() if denom_text and denom_text != "": return True return False def _strip_latex_wrappers(self, latex: str) -> str: """ Strip LaTeX wrapper commands like \\mathrm{}, \\text{}, etc. Args: latex: LaTeX string that might have wrappers Returns: Clean text without wrappers Examples: \\mathrm{abc} → abc \\text{hello} → hello \\mathrm{d e f g} → d e f g (spaces preserved) """ latex = latex.strip() # Strip \mathrm{} wrapper if latex.startswith("\\mathrm{") and latex.endswith("}"): latex = latex[8:-1] # Strip \text{} wrapper if latex.startswith("\\text{") and latex.endswith("}"): latex = latex[6:-1] # Strip \mathbf{} wrapper if latex.startswith("\\mathbf{") and latex.endswith("}"): latex = latex[8:-1] return latex.strip() def can_handle(self, node: ET.Element) -> bool: """ Check if this visitor can handle the given node. Args: node: The MathML element to check Returns: True if node is an element, False otherwise """ return _local(node.tag) == 'mfrac' @property def element_name(self) -> str: """Get the element name this visitor handles.""" return 'mfrac' def __repr__(self) -> str: """String representation for debugging.""" return f"MfracVisitor(strategy={self.strategy.name})"