fix(omml2latex): 修复多字母标识符被拆开的问题

OMML2MML 会把 Word 里连写的多字母标识符(线段名 BC、三角形 △ABC、
角度 45° 等)拆成相邻的 <mi>/<mn> 元素,原转换器用空格 join 导致
'BC' 被渲染成 'B C'(B×C),'45°' 变成 '4 5°'。

三处修复:
- mrow_visitor: 连续单字母 <mi> 拼接
- legacy_adapter: <math> 根节点多子元素时连续单字母 <mi> 拼接
- math_strategy: Unicode+字母混合内容按'LaTeX命令后跟字母才加空格'规则 join

回归验证:test_mixed.docx 130 个 OMML 公式,16 个发生变化且全部为
正确改善(线段名/角度/三角形/隐式乘法拼接),零误伤。
This commit is contained in:
2026-10-15 01:01:10 +08:00
parent b00f8d7f46
commit d4c2fd1fca
4 changed files with 75 additions and 1 deletions

View File

@@ -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 <m:t> that OMML2MML splits into adjacent <mi> 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 <mi> identifier.
Used to re-merge multi-letter identifiers (e.g. line segment "BC") that
Word/OMML2MML split into adjacent <mi> 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).