init: WordFileParse docx→HTML 解析服务(含 Word EQ 域公式修复)
服务:Spring Boot 3.2 + docx4j 8.3.9,POST /api/v3/server_office_file/file/parse
接受 .docx,返回 HTML(公式 → LaTeX,图片 → RustFS 外链)
公式支持三类来源:
- OMML(Word 2007+ 公式) → Python omml_mathml2latex
- MathType OLE 对象 → Ruby mathtype2mathml → MathML → LaTeX
- Word EQ 域(老教辅主力) → EqToLatexUtil(本次新增)
本次改动:
1. 新增 java-svc/.../core/EqToLatexUtil.java:
移植自 jby backend-services office-core,EQ 语法 → LaTeX
(\f→\dfrac、\r→\sqrt、\i→\int/\sum/\prod、\s→^{}/_{}、\b→\left\right、
\a→array、\x→\boxed/\overline、\o\al 堆叠、圈码、化学符号、极限、连等式)
品牌标记 JBYU* → WPU*,与采集端一致
2. CtoMathFinder:接上 EQ 分支(原为 not-ported 桩),域代码经
EqToLatexUtil.convert 转 LaTeX 写入 eqLatexMap(WPEQNUM 占位符)
3. WordParseUtil.dealPlaceholder:EQ 的 LaTeX 同样加 \( \) 包裹
(原先因 WPEQNUM 例外不加包裹,前端/MathJax 不渲染)
修前症状:教辅 Word 里分式/方程组/上下标整段丢失(如 {−1/3, 1} 变成 {-,1})
修后实测:46 份教辅 2908 处 EQ 域全部还原,公式总数 2954 处、零未包裹残留
安全:application.yml(含 RustFS 密钥)不入库,提供 application.yml.example;
OssService 的 @Value 默认密钥已清空
This commit is contained in:
333
scripts/omml_mathml2latex/converters/brace_marker_resolver.py
Normal file
333
scripts/omml_mathml2latex/converters/brace_marker_resolver.py
Normal file
@@ -0,0 +1,333 @@
|
||||
"""
|
||||
Brace marker resolver for context-aware delimiter pairing.
|
||||
|
||||
This module converts __LBRACE__ and __RBRACE__ markers to proper LaTeX
|
||||
delimiters based on pairing context and existing \\left...\\right delimiters.
|
||||
|
||||
The markers distinguish source braces (from DOCX) from generated braces
|
||||
(in LaTeX commands like \\mathrm{...}).
|
||||
|
||||
Author: omml2tex project
|
||||
Date: 2026-01-14
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import List, Dict, Optional
|
||||
from ..utils.debug import debug_print
|
||||
|
||||
|
||||
class BraceMarkerResolver:
|
||||
"""
|
||||
Resolves __LBRACE__ and __RBRACE__ markers to LaTeX delimiters.
|
||||
|
||||
Pairing rules:
|
||||
1. __LBRACE__...__RBRACE__ → \\left\\{...\\right\\}
|
||||
2. __LBRACE__...\\right. → \\left\\{...\\right.
|
||||
3. \\left....__RBRACE__ → \\left....\\right\\}
|
||||
4. Standalone __LBRACE__ → \\{
|
||||
5. Standalone __RBRACE__ → \\}
|
||||
"""
|
||||
|
||||
def __init__(self, debug: bool = False):
|
||||
"""
|
||||
Initialize the brace marker resolver.
|
||||
|
||||
Args:
|
||||
debug: Enable debug output to stderr
|
||||
"""
|
||||
self.debug = debug
|
||||
|
||||
def resolve(self, latex: str) -> str:
|
||||
"""
|
||||
Convert __LBRACE__ and __RBRACE__ markers to LaTeX delimiters.
|
||||
|
||||
Args:
|
||||
latex: LaTeX string with markers
|
||||
|
||||
Returns:
|
||||
LaTeX string with markers resolved to proper delimiters
|
||||
|
||||
Examples:
|
||||
"__LBRACE__ x __RBRACE__" → "\\left\\{ x \\right\\}"
|
||||
"__LBRACE__ x \\right." → "\\left\\{ x \\right."
|
||||
"x __LBRACE__" → "x \\{"
|
||||
"""
|
||||
if not latex or '__LBRACE__' not in latex and '__RBRACE__' not in latex:
|
||||
return latex
|
||||
|
||||
if self.debug:
|
||||
debug_print(f"[BRACE RESOLVE] Input: {latex[:100]}...")
|
||||
|
||||
# Step 1: Find all markers and existing \\left...\\right delimiters
|
||||
markers = self._find_markers(latex)
|
||||
existing_delims = self._find_existing_delimiters(latex)
|
||||
|
||||
if not markers:
|
||||
if self.debug:
|
||||
debug_print("[BRACE RESOLVE] No markers found")
|
||||
return latex
|
||||
|
||||
if self.debug:
|
||||
debug_print(f"[BRACE RESOLVE] Found {len(markers)} markers")
|
||||
|
||||
# Step 2: Pair markers using stack-based algorithm
|
||||
pairs = self._pair_markers(markers, existing_delims, latex)
|
||||
|
||||
if self.debug:
|
||||
debug_print(f"[BRACE RESOLVE] Created {len(pairs)} pairs")
|
||||
|
||||
# Step 3: Build replacements
|
||||
replacements = self._build_replacements(pairs)
|
||||
|
||||
# Step 4: Apply replacements in reverse order
|
||||
result = self._apply_replacements(latex, replacements)
|
||||
|
||||
if self.debug:
|
||||
debug_print(f"[BRACE RESOLVE] Output: {result[:100]}...")
|
||||
|
||||
return result
|
||||
|
||||
def _find_markers(self, latex: str) -> List[Dict]:
|
||||
"""
|
||||
Find all __LBRACE__ and __RBRACE__ markers.
|
||||
|
||||
Args:
|
||||
latex: LaTeX string
|
||||
|
||||
Returns:
|
||||
List of marker dicts with 'text', 'pos', 'end' keys
|
||||
"""
|
||||
markers = []
|
||||
|
||||
# Find __LBRACE__
|
||||
for match in re.finditer(r'__LBRACE__', latex):
|
||||
markers.append({
|
||||
'kind': 'marker',
|
||||
'text': '__LBRACE__',
|
||||
'pos': match.start(),
|
||||
'end': match.end(),
|
||||
})
|
||||
|
||||
# Find __RBRACE__
|
||||
for match in re.finditer(r'__RBRACE__', latex):
|
||||
markers.append({
|
||||
'kind': 'marker',
|
||||
'text': '__RBRACE__',
|
||||
'pos': match.start(),
|
||||
'end': match.end(),
|
||||
})
|
||||
|
||||
# Sort by position
|
||||
markers.sort(key=lambda x: x['pos'])
|
||||
|
||||
return markers
|
||||
|
||||
def _find_existing_delimiters(self, latex: str) -> List[Dict]:
|
||||
"""
|
||||
Find existing \\left and \\right delimiters.
|
||||
|
||||
Args:
|
||||
latex: LaTeX string
|
||||
|
||||
Returns:
|
||||
List of delimiter dicts with 'text', 'pos', 'end', 'type' keys
|
||||
"""
|
||||
delimiters = []
|
||||
|
||||
# Pattern for \\left followed by delimiter
|
||||
# \\left(, \\left[, \\left\\{, \\left\\langle, \\left., etc.
|
||||
left_pattern = r'\\left(?:\\{|\\langle|\\lfloor|\\lceil|\\||[(\[|.])'
|
||||
for match in re.finditer(left_pattern, latex):
|
||||
delimiters.append({
|
||||
'kind': 'existing',
|
||||
'text': match.group(),
|
||||
'pos': match.start(),
|
||||
'end': match.end(),
|
||||
'type': 'left',
|
||||
})
|
||||
|
||||
# Pattern for \\right followed by delimiter
|
||||
right_pattern = r'\\right(?:\\}|\\rangle|\\rfloor|\\rceil|\\||[)\]|.])'
|
||||
for match in re.finditer(right_pattern, latex):
|
||||
delimiters.append({
|
||||
'kind': 'existing',
|
||||
'text': match.group(),
|
||||
'pos': match.start(),
|
||||
'end': match.end(),
|
||||
'type': 'right',
|
||||
})
|
||||
|
||||
return delimiters
|
||||
|
||||
def _pair_markers(self, markers: List[Dict], existing_delims: List[Dict], latex: str) -> List[Dict]:
|
||||
"""
|
||||
Pair markers using stack-based algorithm.
|
||||
|
||||
Args:
|
||||
markers: List of marker dicts
|
||||
existing_delims: List of existing delimiter dicts
|
||||
latex: Original LaTeX string (for context)
|
||||
|
||||
Returns:
|
||||
List of pair dicts describing how to resolve each marker
|
||||
"""
|
||||
stack = []
|
||||
pairs = []
|
||||
|
||||
# Combine and sort by position
|
||||
all_items = sorted(markers + existing_delims, key=lambda x: x['pos'])
|
||||
|
||||
for item in all_items:
|
||||
if item['kind'] == 'marker' and item['text'] == '__LBRACE__':
|
||||
# Push opening marker
|
||||
stack.append({'type': 'marker_open', 'item': item})
|
||||
|
||||
elif item['kind'] == 'marker' and item['text'] == '__RBRACE__':
|
||||
# Try to pair with stack top
|
||||
if stack:
|
||||
top = stack[-1]
|
||||
if top['type'] == 'marker_open':
|
||||
# Pair: __LBRACE__...__RBRACE__
|
||||
opener = stack.pop()
|
||||
pairs.append({
|
||||
'type': 'symmetric',
|
||||
'left_pos': opener['item']['pos'],
|
||||
'left_end': opener['item']['end'],
|
||||
'right_pos': item['pos'],
|
||||
'right_end': item['end'],
|
||||
})
|
||||
elif top['type'] == 'existing_left':
|
||||
# Pair: \\left....__RBRACE__
|
||||
stack.pop()
|
||||
pairs.append({
|
||||
'type': 'right_with_existing_left',
|
||||
'right_pos': item['pos'],
|
||||
'right_end': item['end'],
|
||||
})
|
||||
else:
|
||||
# Standalone closing marker
|
||||
pairs.append({
|
||||
'type': 'standalone',
|
||||
'marker': '__RBRACE__',
|
||||
'pos': item['pos'],
|
||||
'end': item['end'],
|
||||
})
|
||||
else:
|
||||
# Standalone closing marker
|
||||
pairs.append({
|
||||
'type': 'standalone',
|
||||
'marker': '__RBRACE__',
|
||||
'pos': item['pos'],
|
||||
'end': item['end'],
|
||||
})
|
||||
|
||||
elif item['kind'] == 'existing' and item['type'] == 'left':
|
||||
# Push existing \\left delimiter
|
||||
stack.append({'type': 'existing_left', 'item': item})
|
||||
|
||||
elif item['kind'] == 'existing' and item['type'] == 'right':
|
||||
# Try to pair with marker on stack
|
||||
if stack:
|
||||
top = stack[-1]
|
||||
if top['type'] == 'marker_open':
|
||||
# Pair: __LBRACE__...\\right
|
||||
opener = stack.pop()
|
||||
pairs.append({
|
||||
'type': 'left_with_existing_right',
|
||||
'left_pos': opener['item']['pos'],
|
||||
'left_end': opener['item']['end'],
|
||||
})
|
||||
elif top['type'] == 'existing_left':
|
||||
# Paired: \\left...\\right (no action needed)
|
||||
stack.pop()
|
||||
|
||||
# Handle remaining items on stack (standalone opening markers)
|
||||
for item in stack:
|
||||
if item['type'] == 'marker_open':
|
||||
pairs.append({
|
||||
'type': 'standalone',
|
||||
'marker': '__LBRACE__',
|
||||
'pos': item['item']['pos'],
|
||||
'end': item['item']['end'],
|
||||
})
|
||||
|
||||
return pairs
|
||||
|
||||
def _build_replacements(self, pairs: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
Build replacement list from pairs.
|
||||
|
||||
Args:
|
||||
pairs: List of pair dicts
|
||||
|
||||
Returns:
|
||||
List of replacement dicts with 'pos', 'end', 'old', 'new' keys
|
||||
"""
|
||||
replacements = []
|
||||
|
||||
for pair in pairs:
|
||||
if pair['type'] == 'symmetric':
|
||||
# __LBRACE__...__RBRACE__ → \\left\\{...\\right\\}
|
||||
replacements.append({
|
||||
'pos': pair['left_pos'],
|
||||
'end': pair['left_end'],
|
||||
'old': '__LBRACE__',
|
||||
'new': '\\left\\{',
|
||||
})
|
||||
replacements.append({
|
||||
'pos': pair['right_pos'],
|
||||
'end': pair['right_end'],
|
||||
'old': '__RBRACE__',
|
||||
'new': '\\right\\}',
|
||||
})
|
||||
|
||||
elif pair['type'] == 'left_with_existing_right':
|
||||
# __LBRACE__...\\right → \\left\\{...\\right
|
||||
replacements.append({
|
||||
'pos': pair['left_pos'],
|
||||
'end': pair['left_end'],
|
||||
'old': '__LBRACE__',
|
||||
'new': '\\left\\{',
|
||||
})
|
||||
|
||||
elif pair['type'] == 'right_with_existing_left':
|
||||
# \\left....__RBRACE__ → \\left....\\right\\}
|
||||
replacements.append({
|
||||
'pos': pair['right_pos'],
|
||||
'end': pair['right_end'],
|
||||
'old': '__RBRACE__',
|
||||
'new': '\\right\\}',
|
||||
})
|
||||
|
||||
elif pair['type'] == 'standalone':
|
||||
# Standalone marker → escaped brace
|
||||
replacements.append({
|
||||
'pos': pair['pos'],
|
||||
'end': pair['end'],
|
||||
'old': pair['marker'],
|
||||
'new': '\\{' if pair['marker'] == '__LBRACE__' else '\\}',
|
||||
})
|
||||
|
||||
# Sort by position in reverse order for safe replacement
|
||||
replacements.sort(key=lambda x: x['pos'], reverse=True)
|
||||
|
||||
return replacements
|
||||
|
||||
def _apply_replacements(self, latex: str, replacements: List[Dict]) -> str:
|
||||
"""
|
||||
Apply replacements to LaTeX string.
|
||||
|
||||
Args:
|
||||
latex: Original LaTeX string
|
||||
replacements: List of replacement dicts (must be sorted by pos descending)
|
||||
|
||||
Returns:
|
||||
LaTeX string with replacements applied
|
||||
"""
|
||||
result = latex
|
||||
|
||||
for rep in replacements:
|
||||
# Replace from back to front to preserve positions
|
||||
result = result[:rep['pos']] + rep['new'] + result[rep['end']:]
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,846 @@
|
||||
"""
|
||||
Universal delimiter pairing post-processor.
|
||||
|
||||
This module provides functionality to add \\left and \\right commands to ALL
|
||||
delimiter pairs in LaTeX output, ensuring uniform scaling regardless of content.
|
||||
|
||||
The processor uses a smart stack-based pairing algorithm that:
|
||||
1. Handles symmetric pairs: (), [], {}, ||
|
||||
2. Handles mismatched pairs for interval notation: [), (], etc.
|
||||
3. Skips delimiters already in \\left...\\right or cases environment
|
||||
|
||||
Author: omml2tex project
|
||||
Date: 2025-11-12
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from typing import List, Tuple, Dict, Optional
|
||||
|
||||
from ..utils.debug import debug_print
|
||||
|
||||
|
||||
class DelimiterPairingProcessor:
|
||||
"""
|
||||
Post-processor that adds \\left and \\right to all delimiter pairs.
|
||||
|
||||
This ensures uniform scaling of all parentheses, brackets, and braces,
|
||||
including interval notation with mismatched delimiters.
|
||||
|
||||
Rules:
|
||||
1. Add \\left\\right to symmetric pairs: (), [], {}, ||, ⟨⟩
|
||||
2. Add \\left\\right to mismatched pairs (intervals): [), (], etc.
|
||||
3. Add \\left\\right to quantum mechanics notation: ⟨φ|, |ψ⟩, ⟨φ|ψ⟩
|
||||
4. Skip delimiters in: \\left...\\right, \\begin{array}...\\end{array}
|
||||
"""
|
||||
|
||||
# Delimiter mappings
|
||||
# BUG-010 FIX: Added '.' to OPENING_DELIMS for invisible left delimiter (\left.)
|
||||
OPENING_DELIMS = {'(', '[', '{', '|', '\\langle', '\\{', '\\|', '\\lfloor', '\\lceil', '.'}
|
||||
CLOSING_DELIMS = {')', ']', '}', '|', '\\rangle', '\\}', '\\|', '.', '\\rfloor', '\\rceil'} # '.' for invisible delimiter
|
||||
ALL_DELIMS = OPENING_DELIMS | CLOSING_DELIMS
|
||||
|
||||
# Compatible delimiter pairings (for interval notation and quantum mechanics)
|
||||
# Any opening can pair with any closing
|
||||
COMPATIBLE_PAIRS = {
|
||||
'(': {')', ']', '}', '\\rangle', '\\}', '.'}, # ( can close with ), ], }, ⟩, or . (invisible)
|
||||
'[': {')', ']', '}', '\\rangle', '\\}', '.'}, # [ can close with ), ], }, ⟩, or . (invisible)
|
||||
'{': {')', ']', '}', '\\rangle', '\\}', '.'}, # { can close with ), ], }, ⟩, or . (invisible)
|
||||
'\\{': {')', ']', '}', '\\rangle', '\\}', '.'}, # \{ can close with ), ], }, \}, or . (invisible)
|
||||
'|': {'|', '\\rangle', '.'}, # | can pair with |, ⟩, or . (invisible)
|
||||
'\\|': {'\\|', '.'}, # \| pairs with \| or . (invisible)
|
||||
'\\langle': {')', ']', '}', '\\rangle', '|', '\\}', '.'}, # ⟨ can pair with ⟩, |, or . (invisible)
|
||||
'\\lfloor': {'\\rfloor', '.'}, # ⌊ pairs with ⌋ or . (invisible)
|
||||
'\\lceil': {'\\rceil', '.'}, # ⌈ pairs with ⌉ or . (invisible)
|
||||
# BUG-010 FIX: '.' (invisible delimiter) can pair with any closing delimiter
|
||||
'.': {')', ']', '}', '\\rangle', '\\}', '\\|', '|', '\\rfloor', '\\rceil', '.'},
|
||||
}
|
||||
|
||||
def __init__(self, debug: bool = False):
|
||||
"""
|
||||
Initialize the delimiter pairing processor.
|
||||
|
||||
Args:
|
||||
debug: Enable debug output to stderr
|
||||
"""
|
||||
self.debug = debug
|
||||
|
||||
def process(self, latex: str) -> str:
|
||||
"""
|
||||
Add \\left and \\right to all delimiter pairs in LaTeX.
|
||||
|
||||
This is a post-processing step that runs AFTER the visitor pattern,
|
||||
ensuring ALL delimiters get scaled, including interval notation.
|
||||
|
||||
Args:
|
||||
latex: LaTeX string from conversion
|
||||
|
||||
Returns:
|
||||
LaTeX string with \\left...\\right added to all delimiter pairs
|
||||
|
||||
Examples:
|
||||
"(x + y)" → "\\left(x + y\\right)"
|
||||
"[a, b]" → "\\left[a, b\\right]"
|
||||
"[0, 1)" → "\\left[0, 1\\right)" (interval notation)
|
||||
"(a, b]" → "\\left(a, b\\right]" (interval notation)
|
||||
"\\langle \\phi | \\psi \\rangle" → "\\left\\langle \\phi | \\psi \\right\\rangle" (quantum mechanics)
|
||||
"|\\psi\\rangle" → "\\left|\\psi\\right\\rangle" (ket notation)
|
||||
"\\left\\{ \\begin{array}...\\right." → unchanged (skip)
|
||||
"""
|
||||
if not latex or not latex.strip():
|
||||
return latex
|
||||
|
||||
# Step 1: Find all plain delimiters (skip those in protected contexts)
|
||||
delimiters = self._find_plain_delimiters(latex)
|
||||
|
||||
if not delimiters:
|
||||
if self.debug:
|
||||
debug_print("[DELIM PAIR] No plain delimiters found")
|
||||
return latex
|
||||
|
||||
if self.debug:
|
||||
debug_print(f"[DELIM PAIR] Found {len(delimiters)} plain delimiters")
|
||||
|
||||
# Step 2: Pair delimiters using smart algorithm (handles intervals)
|
||||
# Also identify pipes that should be \mid based on context
|
||||
pairs, skip_indices = self._pair_delimiters(delimiters, latex)
|
||||
|
||||
if self.debug:
|
||||
debug_print(f"[DELIM PAIR] Paired {len(pairs)} delimiter pairs")
|
||||
for opening, closing in pairs:
|
||||
debug_print(f" - {opening['char']} at {opening['pos']} ... {closing['char']} at {closing['pos']}")
|
||||
if skip_indices:
|
||||
debug_print(f"[DELIM PAIR] Skipped {len(skip_indices)} pipes (will convert to \\mid)")
|
||||
|
||||
# Step 3: Build replacement list (includes both paired delimiters and skipped pipes)
|
||||
replacements = self._build_replacements(pairs, delimiters, skip_indices)
|
||||
|
||||
# Step 4: Apply replacements in reverse order
|
||||
result = self._apply_replacements(latex, replacements)
|
||||
|
||||
# Step 5: Cleanup - Remove any remaining __DELIM__ markers
|
||||
# These are delimiters that couldn't be paired (unknown types, unmatched, etc.)
|
||||
# This is a fail-safe to ensure no markers leak into the final output
|
||||
if '__DELIM__' in result:
|
||||
if self.debug:
|
||||
debug_print("[DELIM PAIR] Cleaning up remaining __DELIM__ markers")
|
||||
result = result.replace('__DELIM__', '')
|
||||
|
||||
return result
|
||||
|
||||
def _find_plain_delimiters(self, latex: str) -> List[Dict]:
|
||||
"""
|
||||
Find all delimiters marked with __DELIM__ markers.
|
||||
|
||||
Phase 3 Fix: Only delimiters with __DELIM__ markers are paired.
|
||||
This distinguishes fence delimiters (with fence="true" in MathML) from
|
||||
plain operators that happen to use the same character (e.g., | in P(A|B)).
|
||||
|
||||
Args:
|
||||
latex: LaTeX string with __DELIM__ markers
|
||||
|
||||
Returns:
|
||||
List of delimiter dictionaries with 'char', 'pos', 'end', 'marker_start', 'marker_end' keys
|
||||
|
||||
Examples:
|
||||
Input: "P __DELIM__(__DELIM__ A | B __DELIM__)__DELIM__"
|
||||
Output: [
|
||||
{'char': '(', 'pos': 11, 'end': 12, 'marker_start': 2, 'marker_end': 22},
|
||||
{'char': ')', 'pos': 32, 'end': 33, 'marker_start': 23, 'marker_end': 43}
|
||||
]
|
||||
Note: Plain '|' at position 17 is NOT in the list (no __DELIM__ markers)
|
||||
"""
|
||||
delimiters = []
|
||||
marker = '__DELIM__'
|
||||
marker_len = len(marker)
|
||||
|
||||
# Scan for __DELIM__ markers
|
||||
i = 0
|
||||
while i < len(latex):
|
||||
if latex[i:i+marker_len] == marker:
|
||||
# Found opening marker
|
||||
# FIX BUG-007: Check if there's a leading backslash before __DELIM__
|
||||
# Pattern: \__DELIM__\{__DELIM__ should be treated as a unit
|
||||
marker_start = i
|
||||
if i > 0 and latex[i-1] == '\\':
|
||||
# Include the leading backslash in the marker range
|
||||
marker_start = i - 1
|
||||
i += marker_len
|
||||
|
||||
# Extract delimiter character(s) between markers
|
||||
delim_start = i
|
||||
|
||||
# Look for closing marker
|
||||
closing_marker_pos = latex.find(marker, i)
|
||||
if closing_marker_pos == -1:
|
||||
# No closing marker - skip this opening marker
|
||||
continue
|
||||
|
||||
# Extract delimiter between markers
|
||||
delim_str = latex[delim_start:closing_marker_pos]
|
||||
|
||||
# Handle STRETCHY:: prefix (from MathStrategy)
|
||||
if delim_str.startswith('STRETCHY::'):
|
||||
delim_str = delim_str[10:] # Strip prefix
|
||||
|
||||
marker_end = closing_marker_pos + marker_len
|
||||
|
||||
# Skip empty delimiters
|
||||
if not delim_str.strip():
|
||||
i = marker_end
|
||||
continue
|
||||
|
||||
# Add delimiter to list
|
||||
delimiters.append({
|
||||
'char': delim_str,
|
||||
'pos': delim_start,
|
||||
'end': closing_marker_pos,
|
||||
'marker_start': marker_start,
|
||||
'marker_end': marker_end,
|
||||
'escaped': delim_str.startswith('\\'),
|
||||
})
|
||||
|
||||
# Move past closing marker
|
||||
i = marker_end
|
||||
else:
|
||||
i += 1
|
||||
|
||||
return delimiters
|
||||
|
||||
def _find_brace_argument_regions(self, latex: str) -> List[Tuple[int, int]]:
|
||||
"""
|
||||
Find regions where braces are used as command arguments.
|
||||
|
||||
This includes:
|
||||
- Command arguments: \\frac{num}{denom}, \\sqrt{x}, etc.
|
||||
- Subscripts: x_{sub}
|
||||
- Superscripts: x^{sup}
|
||||
|
||||
Args:
|
||||
latex: LaTeX string
|
||||
|
||||
Returns:
|
||||
List of (start, end) tuples for brace regions
|
||||
"""
|
||||
regions = []
|
||||
|
||||
# Simple approach: Find all {...} pairs
|
||||
# These are almost always command arguments, not delimiters
|
||||
stack = []
|
||||
for i, char in enumerate(latex):
|
||||
if char == '{' and (i == 0 or latex[i-1] != '\\'):
|
||||
stack.append(i)
|
||||
elif char == '}' and (i == 0 or latex[i-1] != '\\'):
|
||||
if stack:
|
||||
start = stack.pop()
|
||||
regions.append((start, i+1))
|
||||
|
||||
return regions
|
||||
|
||||
def _find_protected_regions(self, latex: str) -> List[Tuple[int, int]]:
|
||||
"""
|
||||
Find protected regions that should not have delimiters modified.
|
||||
|
||||
Protected regions:
|
||||
1. \\left...\\right pairs
|
||||
2. \\begin{array}...\\end{array} (cases environment)
|
||||
|
||||
Args:
|
||||
latex: LaTeX string
|
||||
|
||||
Returns:
|
||||
List of (start, end) position tuples for protected regions
|
||||
"""
|
||||
protected = []
|
||||
|
||||
# Pattern 1: Find \\left...\\right pairs
|
||||
# Match \\left followed by delimiter, content, \\right followed by delimiter
|
||||
# Include \langle and \rangle as possible delimiters
|
||||
left_pattern = r'\\left(?:[(\[{\|]|\\langle)'
|
||||
right_pattern = r'\\right(?:[)\]}\|.]|\\rangle)'
|
||||
|
||||
# Find all \\left positions
|
||||
left_positions = []
|
||||
for match in re.finditer(left_pattern, latex):
|
||||
left_positions.append(match.start())
|
||||
|
||||
# Find all \\right positions
|
||||
right_positions = []
|
||||
for match in re.finditer(right_pattern, latex):
|
||||
right_positions.append(match.end())
|
||||
|
||||
# Pair them (simple heuristic: match nearest \\right after each \\left)
|
||||
for left_start in left_positions:
|
||||
# Find the nearest \\right after this \\left
|
||||
for right_end in right_positions:
|
||||
if right_end > left_start:
|
||||
protected.append((left_start, right_end))
|
||||
break
|
||||
|
||||
# Pattern 2: Find \\begin{array}...\\end{array}
|
||||
array_pattern = r'\\begin\{array\}.*?\\end\{array\}'
|
||||
for match in re.finditer(array_pattern, latex, re.DOTALL):
|
||||
protected.append((match.start(), match.end()))
|
||||
|
||||
return protected
|
||||
|
||||
def _is_in_protected_region(self, pos: int, protected_regions: List[Tuple[int, int]]) -> bool:
|
||||
"""Check if position is within any protected region."""
|
||||
for start, end in protected_regions:
|
||||
if start <= pos < end:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _pair_delimiters(self, delimiters: List[Dict], latex: str) -> Tuple[List[Tuple[Dict, Dict]], set]:
|
||||
r"""
|
||||
Pair opening and closing delimiters using smart algorithm.
|
||||
|
||||
Algorithm supports:
|
||||
1. Symmetric pairs: (), [], {}, ||, ⟨⟩
|
||||
2. Mismatched pairs for intervals: [), (], etc.
|
||||
3. Quantum mechanics: |ψ⟩ (ket), ⟨φ| (bra), ⟨φ|ψ⟩ (inner product)
|
||||
4. Conditional probability: P(A|B) - pipe inside parentheses becomes \mid
|
||||
5. Set notation: {x|x>0} - pipe inside braces becomes \mid
|
||||
|
||||
Key insight: For pipes, use proximity-based pairing (pair closest pipes first)
|
||||
to correctly handle cases like {x | x + |x-2| < 0} where the inner pipes
|
||||
should be paired before the outer pipe.
|
||||
|
||||
Args:
|
||||
delimiters: List of delimiter dictionaries
|
||||
latex: Full LaTeX string for context checking
|
||||
|
||||
Returns:
|
||||
Tuple of (pairs, skip_indices) where:
|
||||
- pairs: List of (opening, closing) delimiter pairs
|
||||
- skip_indices: Set of indices for pipes that should become \mid
|
||||
"""
|
||||
pairs = []
|
||||
|
||||
# Phase 1: Identify operator pipes (set separators, conditional probability) and pair remaining pipes
|
||||
# Operator pipes should NOT be paired - they become \mid
|
||||
# Examples:
|
||||
# - Set notation: {x | condition} - first pipe is separator
|
||||
# - Conditional probability: P(A|B) - pipe is operator
|
||||
pipe_indices = [i for i, d in enumerate(delimiters) if d['char'] == '|']
|
||||
operator_pipe_indices = set() # Pipes that should become \mid
|
||||
paired_pipe_indices = set()
|
||||
|
||||
if pipe_indices:
|
||||
# First, identify pipes that are operators (not delimiters)
|
||||
for pipe_idx in pipe_indices:
|
||||
pipe_delim = delimiters[pipe_idx]
|
||||
pipe_start = pipe_delim['marker_start']
|
||||
|
||||
# Look backwards in LaTeX string
|
||||
search_start = max(0, pipe_start - 50)
|
||||
latex_before = latex[search_start:pipe_start]
|
||||
|
||||
# Check 1: Set notation separator {x | ...}
|
||||
# Pattern: brace shortly before pipe, no other pipes between them
|
||||
# NOTE: Check for both raw braces and __LBRACE__ markers
|
||||
if '{' in latex_before or '\\{' in latex_before or '__LBRACE__' in latex_before:
|
||||
last_brace_pos = max(
|
||||
latex_before.rfind('{'),
|
||||
latex_before.rfind('\\{'),
|
||||
latex_before.rfind('__LBRACE__')
|
||||
)
|
||||
content_between = latex_before[last_brace_pos:]
|
||||
if len(content_between) < 30:
|
||||
# Check no other pipes between brace and this pipe
|
||||
has_pipes_between = any(
|
||||
delimiters[other_idx]['marker_start'] > search_start + last_brace_pos and
|
||||
delimiters[other_idx]['marker_start'] < pipe_start
|
||||
for other_idx in pipe_indices if other_idx != pipe_idx
|
||||
)
|
||||
if not has_pipes_between:
|
||||
# Check if there's actual content between brace and pipe
|
||||
# Pattern: {x | condition} → content exists → separator
|
||||
# Pattern: {|x|} → no content → absolute value
|
||||
# Strip the brace marker itself to get content after it
|
||||
content_after_brace = content_between
|
||||
for marker in ('__LBRACE__', '\\{', '{'):
|
||||
if content_after_brace.startswith(marker):
|
||||
content_after_brace = content_after_brace[len(marker):]
|
||||
break
|
||||
content_after_brace = content_after_brace.strip()
|
||||
|
||||
if content_after_brace:
|
||||
# Has content between brace and pipe → set notation separator
|
||||
operator_pipe_indices.add(pipe_idx)
|
||||
continue # Skip other checks
|
||||
|
||||
# Check 2: Conditional probability operator P(A|B)
|
||||
# Pattern: parenthesis shortly before pipe
|
||||
if '(' in latex_before or '\\left(' in latex_before:
|
||||
last_paren_pos = max(
|
||||
latex_before.rfind('('),
|
||||
latex_before.rfind('\\left(')
|
||||
)
|
||||
content_between = latex_before[last_paren_pos:]
|
||||
if len(content_between) < 50:
|
||||
# Check no other pipes between parenthesis and this pipe
|
||||
has_pipes_between = any(
|
||||
delimiters[other_idx]['marker_start'] > search_start + last_paren_pos and
|
||||
delimiters[other_idx]['marker_start'] < pipe_start
|
||||
for other_idx in pipe_indices if other_idx != pipe_idx
|
||||
)
|
||||
if not has_pipes_between:
|
||||
# This is likely a conditional probability operator
|
||||
operator_pipe_indices.add(pipe_idx)
|
||||
continue # Skip other checks
|
||||
|
||||
# Remove operator pipes from pairing consideration
|
||||
pipe_indices_for_pairing = [idx for idx in pipe_indices if idx not in operator_pipe_indices]
|
||||
|
||||
# Now pair the remaining pipes using proximity-based algorithm
|
||||
# This pairs absolute values like |x-2| correctly
|
||||
while len(pipe_indices_for_pairing) >= 2:
|
||||
# Find the pair of pipes with minimum distance
|
||||
min_distance = float('inf')
|
||||
best_pair = None
|
||||
|
||||
for i in range(len(pipe_indices_for_pairing)):
|
||||
for j in range(i + 1, len(pipe_indices_for_pairing)):
|
||||
idx1, idx2 = pipe_indices_for_pairing[i], pipe_indices_for_pairing[j]
|
||||
distance = idx2 - idx1
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
best_pair = (i, j, idx1, idx2)
|
||||
|
||||
if best_pair:
|
||||
i, j, idx1, idx2 = best_pair
|
||||
# Pair these pipes
|
||||
pairs.append((delimiters[idx1], delimiters[idx2]))
|
||||
paired_pipe_indices.add(idx1)
|
||||
paired_pipe_indices.add(idx2)
|
||||
# Remove from consideration
|
||||
pipe_indices_for_pairing.pop(j) # Remove j first (higher index)
|
||||
pipe_indices_for_pairing.pop(i) # Then remove i
|
||||
else:
|
||||
break
|
||||
|
||||
# Phase 2: Pair other delimiters using stack-based algorithm
|
||||
stack = []
|
||||
for idx, delim in enumerate(delimiters):
|
||||
char = delim['char']
|
||||
|
||||
# Skip pipes that were already paired in Phase 1
|
||||
if char == '|' and idx in paired_pipe_indices:
|
||||
continue
|
||||
|
||||
# Skip pipes entirely - they'll be handled in Phase 3
|
||||
if char == '|':
|
||||
continue
|
||||
|
||||
# Special handling for ambiguous delimiters that can be both opening AND closing
|
||||
# Examples: '.' (invisible delimiter), '|' (pipe)
|
||||
# Strategy: If there's a compatible opening on the stack, treat as closing.
|
||||
# Otherwise, treat as opening.
|
||||
is_ambiguous = char in self.OPENING_DELIMS and char in self.CLOSING_DELIMS
|
||||
|
||||
if is_ambiguous:
|
||||
# Check if there's a compatible opening on the stack
|
||||
paired = False
|
||||
if stack:
|
||||
for i in range(len(stack) - 1, -1, -1):
|
||||
opening = stack[i]
|
||||
opening_char = opening['char']
|
||||
|
||||
# Check if this ambiguous delimiter can close this opening
|
||||
if char in self.COMPATIBLE_PAIRS.get(opening_char, set()):
|
||||
# Found a compatible match - treat as CLOSING
|
||||
paired_opening = stack.pop(i)
|
||||
pairs.append((paired_opening, delim))
|
||||
paired = True
|
||||
break
|
||||
|
||||
if not paired:
|
||||
# No compatible opening found - treat as OPENING
|
||||
stack.append(delim)
|
||||
|
||||
elif char in self.OPENING_DELIMS:
|
||||
# Unambiguous opening delimiter - push to stack
|
||||
stack.append(delim)
|
||||
|
||||
elif char in self.CLOSING_DELIMS:
|
||||
# Unambiguous closing delimiter - find compatible opening
|
||||
if stack:
|
||||
# Strategy: Find the MOST RECENT opening that's compatible
|
||||
# This handles both symmetric pairs and interval notation
|
||||
paired = False
|
||||
for i in range(len(stack) - 1, -1, -1):
|
||||
opening = stack[i]
|
||||
opening_char = opening['char']
|
||||
|
||||
# Check if this opening is compatible with closing
|
||||
if char in self.COMPATIBLE_PAIRS.get(opening_char, set()):
|
||||
# Found a compatible match!
|
||||
paired_opening = stack.pop(i)
|
||||
pairs.append((paired_opening, delim))
|
||||
paired = True
|
||||
break
|
||||
|
||||
if not paired and self.debug:
|
||||
debug_print(f"[DELIM PAIR] Warning: Unpaired closing delimiter '{char}' at {delim['pos']}")
|
||||
else:
|
||||
# Stack is empty - unpaired closing delimiter
|
||||
if self.debug:
|
||||
debug_print(f"[DELIM PAIR] Warning: Unpaired closing delimiter '{char}' at {delim['pos']}")
|
||||
|
||||
# Phase 3: Post-process to identify UNPAIRED pipes that should be \mid operators
|
||||
# Pattern 1: \langle ... | ... \rangle → quantum mechanics inner product (unpaired pipe)
|
||||
# Pattern 2: ( ... | ... ) → conditional probability P(A|B) (unpaired pipe)
|
||||
# Pattern 3: [ ... | ... ] → set builder notation [x|x>0] (unpaired pipe)
|
||||
# Pattern 4: { ... | ... } → set notation {x|x>0} (unpaired pipe)
|
||||
# We mark these UNPAIRED pipes to convert to \mid
|
||||
skip_indices = set()
|
||||
for i, delim in enumerate(delimiters):
|
||||
if delim['char'] == '|' and i not in paired_pipe_indices:
|
||||
# This pipe is unpaired - check if it's inside other delimiters
|
||||
# Check if there's a \langle before and \rangle after this pipe
|
||||
has_langle_before = False
|
||||
has_rangle_after = False
|
||||
|
||||
# Check if there's a ( or [ before and ) or ] after this pipe
|
||||
has_paren_or_bracket_before = False
|
||||
has_paren_or_bracket_after = False
|
||||
|
||||
# Check if there's a { before and } after this pipe in the LaTeX string
|
||||
# This handles set notation where braces might not have fence="true"
|
||||
has_brace_before = False
|
||||
has_brace_after = False
|
||||
|
||||
# Check in delimiters list first
|
||||
for j in range(i-1, -1, -1):
|
||||
if delimiters[j]['char'] == '\\langle':
|
||||
has_langle_before = True
|
||||
break
|
||||
elif delimiters[j]['char'] in ('(', '['):
|
||||
has_paren_or_bracket_before = True
|
||||
break
|
||||
elif delimiters[j]['char'] in ('{', '\\{'):
|
||||
has_brace_before = True
|
||||
break
|
||||
elif delimiters[j]['char'] in (')', ']', '}', '\\}'): # Other closing delims
|
||||
break # Stop searching
|
||||
|
||||
for j in range(i+1, len(delimiters)):
|
||||
if delimiters[j]['char'] == '\\rangle':
|
||||
has_rangle_after = True
|
||||
break
|
||||
elif delimiters[j]['char'] in (')', ']'):
|
||||
has_paren_or_bracket_after = True
|
||||
break
|
||||
elif delimiters[j]['char'] in ('}', '\\}'):
|
||||
has_brace_after = True
|
||||
break
|
||||
elif delimiters[j]['char'] in ('(', '[', '{', '\\{', '\\langle'): # Other opening delims
|
||||
break # Stop searching
|
||||
|
||||
# Also check the LaTeX string for plain braces (not in delimiters list)
|
||||
# Look backwards from the pipe position for a plain { or \{ or \left\{
|
||||
if not has_brace_before:
|
||||
pipe_start = delim['marker_start']
|
||||
# Search backwards in LaTeX for { or \{ or \left\{
|
||||
for k in range(pipe_start - 1, max(0, pipe_start - 200), -1):
|
||||
# Check for \left\{
|
||||
if k >= 6 and latex[k-6:k+1] == '\\left\\{':
|
||||
has_brace_before = True
|
||||
break
|
||||
# Check for plain { (not \{)
|
||||
elif latex[k] == '{' and (k == 0 or latex[k-1] != '\\'):
|
||||
has_brace_before = True
|
||||
break
|
||||
# Check for \{ (escaped brace)
|
||||
elif k >= 1 and latex[k-1:k+1] == '\\{':
|
||||
has_brace_before = True
|
||||
break
|
||||
# Stop at other closing delimiters
|
||||
elif latex[k] in (')', ']') and (k == 0 or latex[k-1] != '\\'):
|
||||
break
|
||||
# Stop at \right) or \right]
|
||||
elif k >= 7 and latex[k-7:k+1] in ('\\right)', '\\right]'):
|
||||
break
|
||||
|
||||
# Look forwards from the pipe position for a plain } or \} or \right\}
|
||||
if not has_brace_after:
|
||||
pipe_end = delim['marker_end']
|
||||
# Search forwards in LaTeX for } or \} or \right\}
|
||||
for k in range(pipe_end, min(len(latex), pipe_end + 200)):
|
||||
# Check for \right\}
|
||||
if k + 8 <= len(latex) and latex[k:k+8] == '\\right\\}':
|
||||
has_brace_after = True
|
||||
break
|
||||
# Check for plain } (not \})
|
||||
elif latex[k] == '}' and (k == 0 or latex[k-1] != '\\'):
|
||||
has_brace_after = True
|
||||
break
|
||||
# Check for \} (escaped brace)
|
||||
elif k + 2 <= len(latex) and latex[k:k+2] == '\\}':
|
||||
has_brace_after = True
|
||||
break
|
||||
# Stop at other opening delimiters
|
||||
elif latex[k] in ('(', '[') and (k == 0 or latex[k-1] != '\\'):
|
||||
break
|
||||
# Stop at \left( or \left[
|
||||
elif k + 6 <= len(latex) and latex[k:k+6] in ('\\left(', '\\left['):
|
||||
break
|
||||
|
||||
# If unpaired pipe is between \langle and \rangle, it's an inner product operator
|
||||
if has_langle_before and has_rangle_after:
|
||||
skip_indices.add(i)
|
||||
|
||||
# If unpaired pipe is between ( and ) or [ and ], it's a conditional probability or set builder operator
|
||||
# This handles P(A|B), P(A|B,C), [x|x>0], etc.
|
||||
elif has_paren_or_bracket_before and has_paren_or_bracket_after:
|
||||
skip_indices.add(i)
|
||||
|
||||
# If unpaired pipe is between { and }, it's set notation {x|x>0}
|
||||
elif has_brace_before and has_brace_after:
|
||||
skip_indices.add(i)
|
||||
# Check if there's a \langle before and \rangle after this pipe
|
||||
has_langle_before = False
|
||||
has_rangle_after = False
|
||||
|
||||
# Check if there's a ( or [ before and ) or ] after this pipe
|
||||
has_paren_or_bracket_before = False
|
||||
has_paren_or_bracket_after = False
|
||||
|
||||
# NEW: Check if there's a { before and } after this pipe in the LaTeX string
|
||||
# This handles set notation where braces might not have fence="true"
|
||||
has_brace_before = False
|
||||
has_brace_after = False
|
||||
|
||||
# Check in delimiters list first
|
||||
for j in range(i-1, -1, -1):
|
||||
if delimiters[j]['char'] == '\\langle':
|
||||
has_langle_before = True
|
||||
break
|
||||
elif delimiters[j]['char'] in ('(', '['):
|
||||
has_paren_or_bracket_before = True
|
||||
break
|
||||
elif delimiters[j]['char'] in ('{', '\\{'):
|
||||
has_brace_before = True
|
||||
break
|
||||
elif delimiters[j]['char'] in (')', ']', '}', '\\}'): # Other closing delims
|
||||
break # Stop searching
|
||||
|
||||
for j in range(i+1, len(delimiters)):
|
||||
if delimiters[j]['char'] == '\\rangle':
|
||||
has_rangle_after = True
|
||||
break
|
||||
elif delimiters[j]['char'] in (')', ']'):
|
||||
has_paren_or_bracket_after = True
|
||||
break
|
||||
elif delimiters[j]['char'] in ('}', '\\}'):
|
||||
has_brace_after = True
|
||||
break
|
||||
elif delimiters[j]['char'] in ('(', '[', '{', '\\{', '\\langle'): # Other opening delims
|
||||
break # Stop searching
|
||||
|
||||
# NEW: Also check the LaTeX string for plain braces (not in delimiters list)
|
||||
# Look backwards from the pipe position for a plain { or \{ or \left\{
|
||||
if not has_brace_before:
|
||||
pipe_start = delim['marker_start']
|
||||
# Search backwards in LaTeX for { or \{ or \left\{
|
||||
for k in range(pipe_start - 1, max(0, pipe_start - 200), -1):
|
||||
# Check for \left\{
|
||||
if k >= 6 and latex[k-6:k+1] == '\\left\\{':
|
||||
has_brace_before = True
|
||||
break
|
||||
# Check for plain { (not \{)
|
||||
elif latex[k] == '{' and (k == 0 or latex[k-1] != '\\'):
|
||||
has_brace_before = True
|
||||
break
|
||||
# Check for \{ (escaped brace)
|
||||
elif k >= 1 and latex[k-1:k+1] == '\\{':
|
||||
has_brace_before = True
|
||||
break
|
||||
# Stop at other closing delimiters
|
||||
elif latex[k] in (')', ']') and (k == 0 or latex[k-1] != '\\'):
|
||||
break
|
||||
# Stop at \right) or \right]
|
||||
elif k >= 7 and latex[k-7:k+1] in ('\\right)', '\\right]'):
|
||||
break
|
||||
|
||||
# Look forwards from the pipe position for a plain } or \} or \right\}
|
||||
if not has_brace_after:
|
||||
pipe_end = delim['marker_end']
|
||||
# Search forwards in LaTeX for } or \} or \right\}
|
||||
for k in range(pipe_end, min(len(latex), pipe_end + 200)):
|
||||
# Check for \right\}
|
||||
if k + 8 <= len(latex) and latex[k:k+8] == '\\right\\}':
|
||||
has_brace_after = True
|
||||
break
|
||||
# Check for plain } (not \})
|
||||
elif latex[k] == '}' and (k == 0 or latex[k-1] != '\\'):
|
||||
has_brace_after = True
|
||||
break
|
||||
# Check for \} (escaped brace)
|
||||
elif k + 2 <= len(latex) and latex[k:k+2] == '\\}':
|
||||
has_brace_after = True
|
||||
break
|
||||
# Stop at other opening delimiters
|
||||
elif latex[k] in ('(', '[') and (k == 0 or latex[k-1] != '\\'):
|
||||
break
|
||||
# Stop at \left( or \left[
|
||||
elif k + 6 <= len(latex) and latex[k:k+6] in ('\\left(', '\\left['):
|
||||
break
|
||||
|
||||
# If pipe is between \langle and \rangle, it's an inner product operator
|
||||
if has_langle_before and has_rangle_after:
|
||||
skip_indices.add(i)
|
||||
|
||||
# If pipe is between ( and ) or [ and ], it's a conditional probability or set builder operator
|
||||
# This handles P(A|B), P(A|B,C), [x|x>0], etc.
|
||||
elif has_paren_or_bracket_before and has_paren_or_bracket_after:
|
||||
skip_indices.add(i)
|
||||
|
||||
# If unpaired pipe is between { and }, it's set notation {x|x>0}
|
||||
elif has_brace_before and has_brace_after:
|
||||
skip_indices.add(i)
|
||||
|
||||
# Report unpaired opening delimiters
|
||||
if stack and self.debug:
|
||||
for unpaired in stack:
|
||||
debug_print(f"[DELIM PAIR] Warning: Unpaired opening delimiter '{unpaired['char']}' at {unpaired['pos']}")
|
||||
|
||||
# Merge operator pipes into skip_indices so they get converted to \mid
|
||||
skip_indices.update(operator_pipe_indices)
|
||||
|
||||
return pairs, skip_indices
|
||||
|
||||
def _build_replacements(self, pairs: List[Tuple[Dict, Dict]], delimiters: List[Dict], skip_indices: set) -> List[Tuple[int, int, str]]:
|
||||
r"""
|
||||
Build replacement list for paired delimiters and skipped pipes.
|
||||
|
||||
Phase 3 Fix: Replace __DELIM__char__DELIM__ with \left/\right commands.
|
||||
Also converts skipped pipes (conditional probability) to \mid.
|
||||
|
||||
Args:
|
||||
pairs: List of (opening, closing) delimiter pairs with marker positions
|
||||
delimiters: List of all delimiter dictionaries
|
||||
skip_indices: Set of indices for pipes that should become \mid
|
||||
|
||||
Returns:
|
||||
List of (start_pos, end_pos, replacement_text) tuples
|
||||
|
||||
Examples:
|
||||
Input pair: {'char': '(', 'marker_start': 2, 'marker_end': 22}
|
||||
Output: (2, 22, '\\left(') # Replace __DELIM__(__DELIM__ with \left(
|
||||
|
||||
Input skipped pipe: {'char': '|', 'marker_start': 10, 'marker_end': 30}
|
||||
Output: (10, 30, ' \\mid ') # Replace __DELIM__|__DELIM__ with \mid
|
||||
"""
|
||||
replacements = []
|
||||
|
||||
# Handle paired delimiters
|
||||
for opening, closing in pairs:
|
||||
opening_char = opening['char']
|
||||
closing_char = closing['char']
|
||||
|
||||
# Build \\left command for opening delimiter
|
||||
left_cmd = self._get_left_command(opening_char)
|
||||
right_cmd = self._get_right_command(closing_char)
|
||||
|
||||
if left_cmd and right_cmd:
|
||||
# Replace entire marker range: __DELIM__char__DELIM__ → \left/\right command
|
||||
replacements.append((opening['marker_start'], opening['marker_end'], left_cmd))
|
||||
replacements.append((closing['marker_start'], closing['marker_end'], right_cmd))
|
||||
|
||||
# Handle skipped pipes (convert to \mid)
|
||||
for idx in skip_indices:
|
||||
delim = delimiters[idx]
|
||||
# Replace __DELIM__|__DELIM__ with \mid (with spaces for proper LaTeX spacing)
|
||||
replacements.append((delim['marker_start'], delim['marker_end'], ' \\mid '))
|
||||
|
||||
return replacements
|
||||
|
||||
def _get_left_command(self, char: str) -> Optional[str]:
|
||||
"""Get \\left command for opening delimiter."""
|
||||
left_map = {
|
||||
'(': '\\left(',
|
||||
'[': '\\left[',
|
||||
'{': '\\left\\{',
|
||||
'\\{': '\\left\\{', # Escaped brace → \left\{
|
||||
'|': '\\left|',
|
||||
'\\|': '\\left\\|', # Double vertical line → \left\|
|
||||
'\\langle': '\\left\\langle',
|
||||
'\\lfloor': '\\left\\lfloor', # Floor function
|
||||
'\\lceil': '\\left\\lceil', # Ceiling function
|
||||
'.': '\\left.', # BUG-010 FIX: Invisible left delimiter
|
||||
}
|
||||
return left_map.get(char)
|
||||
|
||||
def _get_right_command(self, char: str) -> Optional[str]:
|
||||
"""Get \\right command for closing delimiter."""
|
||||
right_map = {
|
||||
')': '\\right)',
|
||||
']': '\\right]',
|
||||
'}': '\\right\\}',
|
||||
'\\}': '\\right\\}', # Escaped brace → \right\}
|
||||
'|': '\\right|',
|
||||
'\\|': '\\right\\|', # Double vertical line → \right\|
|
||||
'\\rangle': '\\right\\rangle',
|
||||
'.': '\\right.', # Invisible delimiter → \right.
|
||||
'\\rfloor': '\\right\\rfloor', # Floor function
|
||||
'\\rceil': '\\right\\rceil', # Ceiling function
|
||||
}
|
||||
return right_map.get(char)
|
||||
|
||||
def _apply_replacements(self, latex: str, replacements: List[Tuple[int, int, str]]) -> str:
|
||||
"""
|
||||
Apply replacements to LaTeX string.
|
||||
|
||||
Replacements are applied in reverse order to maintain position indices.
|
||||
|
||||
Args:
|
||||
latex: Original LaTeX string
|
||||
replacements: List of (start_pos, end_pos, replacement_text) tuples
|
||||
|
||||
Returns:
|
||||
Modified LaTeX string
|
||||
"""
|
||||
# Sort by position (reverse order) to maintain indices
|
||||
replacements.sort(key=lambda x: x[0], reverse=True)
|
||||
|
||||
result = latex
|
||||
for start, end, replacement in replacements:
|
||||
result = result[:start] + replacement + result[end:]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Convenience function for direct use
|
||||
def add_universal_left_right(latex: str, debug: bool = False) -> str:
|
||||
"""
|
||||
Add \\left and \\right to all delimiter pairs in LaTeX.
|
||||
|
||||
Convenience function that creates a DelimiterPairingProcessor and
|
||||
processes the LaTeX string.
|
||||
|
||||
This function handles:
|
||||
- Symmetric pairs: (), [], {}, ||
|
||||
- Interval notation: [), (], etc.
|
||||
- Skips protected contexts (\\left...\\right, cases environment)
|
||||
|
||||
Args:
|
||||
latex: LaTeX string from conversion
|
||||
debug: Enable debug output to stderr
|
||||
|
||||
Returns:
|
||||
LaTeX string with \\left...\\right added to all delimiter pairs
|
||||
|
||||
Examples:
|
||||
>>> add_universal_left_right("(x + y)")
|
||||
"\\\\left(x + y\\\\right)"
|
||||
|
||||
>>> add_universal_left_right("[0, 1)")
|
||||
"\\\\left[0, 1\\\\right)"
|
||||
|
||||
>>> add_universal_left_right("\\\\left\\\\{ \\\\begin{array}...\\\\right.")
|
||||
"\\\\left\\\\{ \\\\begin{array}...\\\\right." (unchanged)
|
||||
"""
|
||||
processor = DelimiterPairingProcessor(debug=debug)
|
||||
return processor.process(latex)
|
||||
2094
scripts/omml_mathml2latex/converters/post_processor.py
Normal file
2094
scripts/omml_mathml2latex/converters/post_processor.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user