From d4c2fd1fca871751f92e2553035d6d3985566bdb Mon Sep 17 00:00:00 2001 From: Quentin <1771956812@qq.com> Date: Thu, 15 Oct 2026 01:01:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(omml2latex):=20=E4=BF=AE=E5=A4=8D=E5=A4=9A?= =?UTF-8?q?=E5=AD=97=E6=AF=8D=E6=A0=87=E8=AF=86=E7=AC=A6=E8=A2=AB=E6=8B=86?= =?UTF-8?q?=E5=BC=80=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OMML2MML 会把 Word 里连写的多字母标识符(线段名 BC、三角形 △ABC、 角度 45° 等)拆成相邻的 / 元素,原转换器用空格 join 导致 'BC' 被渲染成 'B C'(B×C),'45°' 变成 '4 5°'。 三处修复: - mrow_visitor: 连续单字母 拼接 - legacy_adapter: 根节点多子元素时连续单字母 拼接 - math_strategy: Unicode+字母混合内容按'LaTeX命令后跟字母才加空格'规则 join 回归验证:test_mixed.docx 130 个 OMML 公式,16 个发生变化且全部为 正确改善(线段名/角度/三角形/隐式乘法拼接),零误伤。 --- .gitignore | 4 +++ .../compat/legacy_adapter.py | 31 +++++++++++++++++++ .../strategies/math_strategy.py | 14 ++++++++- .../visitors/mrow_visitor.py | 27 ++++++++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) 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 = []