diff --git a/.gitignore b/.gitignore index d39f6ce..a9d91f4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,7 @@ logs/ __MACOSX/ .idea/ .vscode/ + +# Python +__pycache__/ +*.pyc diff --git a/scripts/omml_mathml2latex/compat/legacy_adapter.py b/scripts/omml_mathml2latex/compat/legacy_adapter.py index 836ca07..0295528 100644 --- a/scripts/omml_mathml2latex/compat/legacy_adapter.py +++ b/scripts/omml_mathml2latex/compat/legacy_adapter.py @@ -326,6 +326,19 @@ class LegacyMathMLToTeXConverter: # Found fragmented function! Add it as \funcname parts.append(f"\\{function_name}") i += consumed # Skip the consumed elements + elif self._is_plain_single_letter_mi(children[i]): + # Consecutive single-letter identifiers are joined without spaces: + # Word often stores a multi-letter identifier (line segment "BC") + # as a single that OMML2MML splits into adjacent runs. + # Re-merge them so "BC" stays "BC" instead of "B C" (B×C). + merged = self._convert_node(children[i]) + j = i + 1 + while j < len(children) and self._is_plain_single_letter_mi(children[j]): + merged += self._convert_node(children[j]) + j += 1 + if merged.strip() not in ['', '\\']: + parts.append(merged) + i = j else: # Normal element - convert and add part = self._convert_node(children[i]) @@ -804,6 +817,24 @@ class LegacyMathMLToTeXConverter: chemical_indicators = mathrm_count + number_count return chemical_indicators > len(parts) / 2 + def _is_plain_single_letter_mi(self, node: ET.Element) -> bool: + """Return True if node is a single plain-letter identifier. + + Used to re-merge multi-letter identifiers (e.g. line segment "BC") that + Word/OMML2MML split into adjacent runs. Only plain italic identifiers + are merged; elements carrying an explicit mathvariant (e.g. normal for + chemical formulas) are left untouched. + """ + if not isinstance(getattr(node, 'tag', None), str): + return False + if self._get_local_name(node.tag) != 'mi': + return False + if node.get('mathvariant') is not None: + return False + from ..utils import text_content + content = text_content(node).strip() + return len(content) == 1 and content.isalpha() + def _get_local_name(self, tag: str) -> str: """ Extract local element name from tag (strip namespace). diff --git a/scripts/omml_mathml2latex/strategies/math_strategy.py b/scripts/omml_mathml2latex/strategies/math_strategy.py index c5b93ab..7215718 100644 --- a/scripts/omml_mathml2latex/strategies/math_strategy.py +++ b/scripts/omml_mathml2latex/strategies/math_strategy.py @@ -141,7 +141,19 @@ class MathStrategy(Strategy): converted_parts.append(' ') else: converted_parts.append(char) - return ' '.join(converted_parts) + # Join with intelligent spacing: a LaTeX command followed by a + # letter/digit needs a space (otherwise "\triangleA" becomes an + # undefined command), but plain letters stay adjacent so that a + # multi-letter identifier like "△ABC" becomes "\triangle ABC", + # not "\triangle A B C". + result_parts = [] + for idx, part in enumerate(converted_parts): + result_parts.append(part) + if idx < len(converted_parts) - 1: + nxt = converted_parts[idx + 1] + if part.startswith('\\') and nxt and (nxt[0].isalpha() or nxt[0].isdigit()): + result_parts.append(' ') + return ''.join(result_parts) # USER REQUIREMENT: "use the mathvariant to determine whether it's mathrm" # REMOVED: Chemistry detection heuristics (lines 139-162) diff --git a/scripts/omml_mathml2latex/visitors/mrow_visitor.py b/scripts/omml_mathml2latex/visitors/mrow_visitor.py index c00f62f..52946eb 100644 --- a/scripts/omml_mathml2latex/visitors/mrow_visitor.py +++ b/scripts/omml_mathml2latex/visitors/mrow_visitor.py @@ -1633,6 +1633,18 @@ class MrowVisitor(Visitor): # Found a fragmented function - add it to parts parts.append(function_name) i += consumed_count + elif self._is_plain_single_letter_mi(children[i]): + # Consecutive single-letter identifiers are joined without spaces: + # Word often splits a multi-letter identifier (e.g. line segment "BC") + # into several runs, which OMML2MML turns into adjacent elements. + # Re-merge them so "BC" stays "BC" instead of "B C" (which renders as B×C). + merged = self._convert_child(children[i]) + j = i + 1 + while j < len(children) and self._is_plain_single_letter_mi(children[j]): + merged += self._convert_child(children[j]) + j += 1 + parts.append(merged) + i = j else: # No fragmented function - convert child normally part = self._convert_child(children[i]) @@ -1642,6 +1654,21 @@ class MrowVisitor(Visitor): return ' '.join(parts).strip() + def _is_plain_single_letter_mi(self, node: ET.Element) -> bool: + """Return True if node is a single plain-letter identifier. + + Used to re-merge multi-letter identifiers (e.g. line segment "BC") that + Word split into adjacent runs. Only plain italic identifiers are + merged; elements carrying an explicit mathvariant (e.g. normal for + chemical formulas, double-struck, etc.) are left untouched. + """ + if _local(node.tag) != 'mi': + return False + if node.get('mathvariant') is not None: + return False + content = _text_content(node).strip() + return len(content) == 1 and content.isalpha() + def _convert_children_no_space(self, node: ET.Element) -> str: """Convert all children and join without spaces.""" parts = []