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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,3 +13,7 @@ logs/
|
|||||||
__MACOSX/
|
__MACOSX/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|||||||
@@ -326,6 +326,19 @@ class LegacyMathMLToTeXConverter:
|
|||||||
# Found fragmented function! Add it as \funcname
|
# Found fragmented function! Add it as \funcname
|
||||||
parts.append(f"\\{function_name}")
|
parts.append(f"\\{function_name}")
|
||||||
i += consumed # Skip the consumed elements
|
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:
|
else:
|
||||||
# Normal element - convert and add
|
# Normal element - convert and add
|
||||||
part = self._convert_node(children[i])
|
part = self._convert_node(children[i])
|
||||||
@@ -804,6 +817,24 @@ class LegacyMathMLToTeXConverter:
|
|||||||
chemical_indicators = mathrm_count + number_count
|
chemical_indicators = mathrm_count + number_count
|
||||||
return chemical_indicators > len(parts) / 2
|
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:
|
def _get_local_name(self, tag: str) -> str:
|
||||||
"""
|
"""
|
||||||
Extract local element name from tag (strip namespace).
|
Extract local element name from tag (strip namespace).
|
||||||
|
|||||||
@@ -141,7 +141,19 @@ class MathStrategy(Strategy):
|
|||||||
converted_parts.append(' ')
|
converted_parts.append(' ')
|
||||||
else:
|
else:
|
||||||
converted_parts.append(char)
|
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"
|
# USER REQUIREMENT: "use the mathvariant to determine whether it's mathrm"
|
||||||
# REMOVED: Chemistry detection heuristics (lines 139-162)
|
# REMOVED: Chemistry detection heuristics (lines 139-162)
|
||||||
|
|||||||
@@ -1633,6 +1633,18 @@ class MrowVisitor(Visitor):
|
|||||||
# Found a fragmented function - add it to parts
|
# Found a fragmented function - add it to parts
|
||||||
parts.append(function_name)
|
parts.append(function_name)
|
||||||
i += consumed_count
|
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 <mi> 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:
|
else:
|
||||||
# No fragmented function - convert child normally
|
# No fragmented function - convert child normally
|
||||||
part = self._convert_child(children[i])
|
part = self._convert_child(children[i])
|
||||||
@@ -1642,6 +1654,21 @@ class MrowVisitor(Visitor):
|
|||||||
|
|
||||||
return ' '.join(parts).strip()
|
return ' '.join(parts).strip()
|
||||||
|
|
||||||
|
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 split into adjacent <mi> 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:
|
def _convert_children_no_space(self, node: ET.Element) -> str:
|
||||||
"""Convert all children and join without spaces."""
|
"""Convert all children and join without spaces."""
|
||||||
parts = []
|
parts = []
|
||||||
|
|||||||
Reference in New Issue
Block a user