utils.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import re
  2. LEFT_PATTERN = re.compile(r'(\\left)(\S*)')
  3. RIGHT_PATTERN = re.compile(r'(\\right)(\S*)')
  4. LEFT_COUNT_PATTERN = re.compile(r'\\left(?![a-zA-Z])')
  5. RIGHT_COUNT_PATTERN = re.compile(r'\\right(?![a-zA-Z])')
  6. LEFT_RIGHT_REMOVE_PATTERN = re.compile(r'\\left\.?|\\right\.?')
  7. def fix_latex_left_right(s, fix_delimiter=True):
  8. """
  9. 修复LaTeX中的\\left和\\right命令
  10. 1. 确保它们后面跟有效分隔符
  11. 2. 平衡\\left和\\right的数量
  12. """
  13. # 白名单分隔符
  14. valid_delims_list = [r'(', r')', r'[', r']', r'{', r'}', r'/', r'|',
  15. r'\{', r'\}', r'\lceil', r'\rceil', r'\lfloor',
  16. r'\rfloor', r'\backslash', r'\uparrow', r'\downarrow',
  17. r'\Uparrow', r'\Downarrow', r'\|', r'\.']
  18. # 为\left后缺失有效分隔符的情况添加点
  19. def fix_delim(match, is_left=True):
  20. cmd = match.group(1) # \left 或 \right
  21. rest = match.group(2) if len(match.groups()) > 1 else ""
  22. if not rest or rest not in valid_delims_list:
  23. return cmd + "."
  24. return match.group(0)
  25. # 使用更精确的模式匹配\left和\right命令
  26. # 确保它们是独立的命令,不是其他命令的一部分
  27. # 使用预编译正则和统一回调函数
  28. if fix_delimiter:
  29. s = LEFT_PATTERN.sub(lambda m: fix_delim(m, True), s)
  30. s = RIGHT_PATTERN.sub(lambda m: fix_delim(m, False), s)
  31. # 更精确地计算\left和\right的数量
  32. left_count = len(LEFT_COUNT_PATTERN.findall(s)) # 不匹配\lefteqn等
  33. right_count = len(RIGHT_COUNT_PATTERN.findall(s)) # 不匹配\rightarrow等
  34. if left_count == right_count:
  35. # 如果数量相等,检查是否在同一组
  36. return fix_left_right_pairs(s)
  37. # return s
  38. else:
  39. # 如果数量不等,移除所有\left和\right
  40. # logger.debug(f"latex:{s}")
  41. # logger.warning(f"left_count: {left_count}, right_count: {right_count}")
  42. return LEFT_RIGHT_REMOVE_PATTERN.sub('', s)
  43. def fix_left_right_pairs(latex_formula):
  44. """
  45. 检测并修复LaTeX公式中\\left和\\right不在同一组的情况
  46. Args:
  47. latex_formula (str): 输入的LaTeX公式
  48. Returns:
  49. str: 修复后的LaTeX公式
  50. """
  51. # 用于跟踪花括号嵌套层级
  52. brace_stack = []
  53. # 用于存储\left信息: (位置, 深度, 分隔符)
  54. left_stack = []
  55. # 存储需要调整的\right信息: (开始位置, 结束位置, 目标位置)
  56. adjustments = []
  57. i = 0
  58. while i < len(latex_formula):
  59. # 检查是否是转义字符
  60. if i > 0 and latex_formula[i - 1] == '\\':
  61. backslash_count = 0
  62. j = i - 1
  63. while j >= 0 and latex_formula[j] == '\\':
  64. backslash_count += 1
  65. j -= 1
  66. if backslash_count % 2 == 1:
  67. i += 1
  68. continue
  69. # 检测\left命令
  70. if i + 5 < len(latex_formula) and latex_formula[i:i + 5] == "\\left" and i + 5 < len(latex_formula):
  71. delimiter = latex_formula[i + 5]
  72. left_stack.append((i, len(brace_stack), delimiter))
  73. i += 6 # 跳过\left和分隔符
  74. continue
  75. # 检测\right命令
  76. elif i + 6 < len(latex_formula) and latex_formula[i:i + 6] == "\\right" and i + 6 < len(latex_formula):
  77. delimiter = latex_formula[i + 6]
  78. if left_stack:
  79. left_pos, left_depth, left_delim = left_stack.pop()
  80. # 如果\left和\right不在同一花括号深度
  81. if left_depth != len(brace_stack):
  82. # 找到\left所在花括号组的结束位置
  83. target_pos = find_group_end(latex_formula, left_pos, left_depth)
  84. if target_pos != -1:
  85. # 记录需要移动的\right
  86. adjustments.append((i, i + 7, target_pos))
  87. i += 7 # 跳过\right和分隔符
  88. continue
  89. # 处理花括号
  90. if latex_formula[i] == '{':
  91. brace_stack.append(i)
  92. elif latex_formula[i] == '}':
  93. if brace_stack:
  94. brace_stack.pop()
  95. i += 1
  96. # 应用调整,从后向前处理以避免索引变化
  97. if not adjustments:
  98. return latex_formula
  99. result = list(latex_formula)
  100. adjustments.sort(reverse=True, key=lambda x: x[0])
  101. for start, end, target in adjustments:
  102. # 提取\right部分
  103. right_part = result[start:end]
  104. # 从原位置删除
  105. del result[start:end]
  106. # 在目标位置插入
  107. result.insert(target, ''.join(right_part))
  108. return ''.join(result)
  109. def find_group_end(text, pos, depth):
  110. """查找特定深度的花括号组的结束位置"""
  111. current_depth = depth
  112. i = pos
  113. while i < len(text):
  114. if text[i] == '{' and (i == 0 or not is_escaped(text, i)):
  115. current_depth += 1
  116. elif text[i] == '}' and (i == 0 or not is_escaped(text, i)):
  117. current_depth -= 1
  118. if current_depth < depth:
  119. return i
  120. i += 1
  121. return -1 # 未找到对应结束位置
  122. def is_escaped(text, pos):
  123. """检查字符是否被转义"""
  124. backslash_count = 0
  125. j = pos - 1
  126. while j >= 0 and text[j] == '\\':
  127. backslash_count += 1
  128. j -= 1
  129. return backslash_count % 2 == 1
  130. def fix_unbalanced_braces(latex_formula):
  131. """
  132. 检测LaTeX公式中的花括号是否闭合,并删除无法配对的花括号
  133. Args:
  134. latex_formula (str): 输入的LaTeX公式
  135. Returns:
  136. str: 删除无法配对的花括号后的LaTeX公式
  137. """
  138. stack = [] # 存储左括号的索引
  139. unmatched = set() # 存储不匹配括号的索引
  140. i = 0
  141. while i < len(latex_formula):
  142. # 检查是否是转义的花括号
  143. if latex_formula[i] in ['{', '}']:
  144. # 计算前面连续的反斜杠数量
  145. backslash_count = 0
  146. j = i - 1
  147. while j >= 0 and latex_formula[j] == '\\':
  148. backslash_count += 1
  149. j -= 1
  150. # 如果前面有奇数个反斜杠,则该花括号是转义的,不参与匹配
  151. if backslash_count % 2 == 1:
  152. i += 1
  153. continue
  154. # 否则,该花括号参与匹配
  155. if latex_formula[i] == '{':
  156. stack.append(i)
  157. else: # latex_formula[i] == '}'
  158. if stack: # 有对应的左括号
  159. stack.pop()
  160. else: # 没有对应的左括号
  161. unmatched.add(i)
  162. i += 1
  163. # 所有未匹配的左括号
  164. unmatched.update(stack)
  165. # 构建新字符串,删除不匹配的括号
  166. return ''.join(char for i, char in enumerate(latex_formula) if i not in unmatched)
  167. def process_latex(input_string):
  168. """
  169. 处理LaTeX公式中的反斜杠:
  170. 1. 如果\后跟特殊字符(#$%&~_^\\{})或空格,保持不变
  171. 2. 如果\后跟两个小写字母,保持不变
  172. 3. 其他情况,在\后添加空格
  173. Args:
  174. input_string (str): 输入的LaTeX公式
  175. Returns:
  176. str: 处理后的LaTeX公式
  177. """
  178. def replace_func(match):
  179. # 获取\后面的字符
  180. next_char = match.group(1)
  181. # 如果是特殊字符或空格,保持不变
  182. if next_char in "#$%&~_^|\\{} \t\n\r\v\f":
  183. return match.group(0)
  184. # 如果是字母,检查下一个字符
  185. if 'a' <= next_char <= 'z' or 'A' <= next_char <= 'Z':
  186. pos = match.start() + 2 # \x后的位置
  187. if pos < len(input_string) and ('a' <= input_string[pos] <= 'z' or 'A' <= input_string[pos] <= 'Z'):
  188. # 下一个字符也是字母,保持不变
  189. return match.group(0)
  190. # 其他情况,在\后添加空格
  191. return '\\' + ' ' + next_char
  192. # 匹配\后面跟一个字符的情况
  193. pattern = r'\\(.)'
  194. return re.sub(pattern, replace_func, input_string)
  195. # 常见的在KaTeX/MathJax中可用的数学环境
  196. ENV_TYPES = ['array', 'matrix', 'pmatrix', 'bmatrix', 'vmatrix',
  197. 'Bmatrix', 'Vmatrix', 'cases', 'aligned', 'gathered', 'align', 'align*',]
  198. ENV_BEGIN_PATTERNS = {env: re.compile(r'\\begin\{' + env + r'\}') for env in ENV_TYPES}
  199. ENV_END_PATTERNS = {env: re.compile(r'\\end\{' + env + r'\}') for env in ENV_TYPES}
  200. ENV_FORMAT_PATTERNS = {env: re.compile(r'\\begin\{' + env + r'\}\{([^}]*)\}') for env in ENV_TYPES}
  201. def fix_latex_environments(s):
  202. """
  203. 检测LaTeX中环境(如array)的\\begin和\\end是否匹配
  204. 1. 如果缺少\\begin标签则在开头添加
  205. 2. 如果缺少\\end标签则在末尾添加
  206. """
  207. for env in ENV_TYPES:
  208. begin_count = len(ENV_BEGIN_PATTERNS[env].findall(s))
  209. end_count = len(ENV_END_PATTERNS[env].findall(s))
  210. if begin_count != end_count:
  211. if end_count > begin_count:
  212. format_match = ENV_FORMAT_PATTERNS[env].search(s)
  213. default_format = '{c}' if env == 'array' else ''
  214. format_str = '{' + format_match.group(1) + '}' if format_match else default_format
  215. missing_count = end_count - begin_count
  216. begin_command = '\\begin{' + env + '}' + format_str + ' '
  217. s = begin_command * missing_count + s
  218. else:
  219. missing_count = begin_count - end_count
  220. s = s + (' \\end{' + env + '}') * missing_count
  221. return s
  222. REPLACEMENTS_PATTERNS = {
  223. re.compile(r'\\underbar'): r'\\underline',
  224. re.compile(r'\\Bar'): r'\\hat',
  225. re.compile(r'\\Hat'): r'\\hat',
  226. re.compile(r'\\Tilde'): r'\\tilde',
  227. re.compile(r'\\slash'): r'/',
  228. re.compile(r'\\textperthousand'): r'‰',
  229. re.compile(r'\\sun'): r'☉',
  230. re.compile(r'\\textunderscore'): r'\\_',
  231. re.compile(r'\\fint'): r'⨏',
  232. re.compile(r'\\up '): r'\\ ',
  233. re.compile(r'\\vline = '): r'\\models ',
  234. re.compile(r'\\vDash '): r'\\models ',
  235. re.compile(r'\\sq \\sqcup '): r'\\square ',
  236. re.compile(r'\\copyright'): r'©',
  237. }
  238. QQUAD_PATTERN = re.compile(r'\\qquad(?!\s)')
  239. def remove_up_commands(s: str):
  240. """Remove unnecessary up commands from LaTeX code."""
  241. UP_PATTERN = re.compile(r'\\up([a-zA-Z]+)')
  242. s = UP_PATTERN.sub(
  243. lambda m: m.group(0) if m.group(1) in ["arrow", "downarrow", "lus", "silon"] else f"\\{m.group(1)}", s
  244. )
  245. return s
  246. def remove_unsupported_commands(s: str):
  247. """Remove unsupported LaTeX commands."""
  248. COMMANDS_TO_REMOVE_PATTERN = re.compile(
  249. r'\\(?:lefteqn|boldmath|ensuremath|centering|textsubscript|sides|textsl|textcent|emph|protect|null)')
  250. s = COMMANDS_TO_REMOVE_PATTERN.sub('', s)
  251. return s
  252. def latex_rm_whitespace(s: str):
  253. """Remove unnecessary whitespace from LaTeX code."""
  254. s = fix_unbalanced_braces(s)
  255. s = fix_latex_left_right(s)
  256. s = fix_latex_environments(s)
  257. s = remove_up_commands(s)
  258. s = remove_unsupported_commands(s)
  259. # 应用所有替换
  260. for pattern, replacement in REPLACEMENTS_PATTERNS.items():
  261. s = pattern.sub(replacement, s)
  262. # 处理LaTeX中的反斜杠和空格
  263. s = process_latex(s)
  264. # \qquad后补空格
  265. s = QQUAD_PATTERN.sub(r'\\qquad ', s)
  266. # 如果字符串以反斜杠结尾,去掉最后的反斜杠
  267. while s.endswith('\\'):
  268. s = s[:-1]
  269. return s