DeepDigest / post_nougat_parser.py
ArielKes's picture
app and server code. app sends pdf to server that returns mmd
64cf084
Raw History Blame Contribute Delete
11.5 kB
import re
import sys
def convert_inline_math(text):
# This pattern looks for a literal '\(' and then non-greedily
# matches everything '.*?' until it finds a literal '\)'.
pattern = r'\\\((.*?)\\\)'
# The replacement adds spaces around the math expression for better rendering.
return re.sub(pattern, r' $\1$ ', text, flags=re.DOTALL)
def convert_display_math(text):
pattern = r'\\\[(.*?)\\\]'
return re.sub(pattern, r'$$\1$$', text, flags=re.DOTALL)
def clean_nested_tabular(content):
"""Extract content from nested tabular environments"""
def extract_nested_tabular(match):
inner_content = match.group(1)
# Split by & and \\ to get cells, join with line breaks
lines = inner_content.split('\\\\')
cleaned_lines = []
for line in lines:
cells = [cell.strip() for cell in line.split('&') if cell.strip()]
if cells:
cleaned_lines.extend(cells)
return ' \\\\ '.join(cleaned_lines) if cleaned_lines else inner_content
# Handle nested tabular environments
while re.search(r'\\begin\{tabular\}\{[^}]*\}(.*?)\\end\{tabular\}', content, re.DOTALL):
content = re.sub(r'\\begin\{tabular\}\{[^}]*\}(.*?)\\end\{tabular\}', extract_nested_tabular, content,
flags=re.DOTALL)
return content.strip()
def parse_latex_table(table_content):
"""Convert LaTeX tabular to markdown table with support for multicolumn/multirow."""
lines = []
header_separator_added = False
# Extract just the tabular content
table_content = re.sub(r'\\begin\{tabular\}\{[^}]+\}', '', table_content)
table_content = re.sub(r'\\end\{tabular\}', '', table_content)
# Clean nested tabular environments first
table_content = clean_nested_tabular(table_content)
# Split rows by '\\' - use the original simple approach for most cases
rows = re.split(r'\\\\', table_content)
# Storage for multirow spans: {col_index: (remaining_span, content)}
active_multirows = {}
for row_idx, row in enumerate(rows):
row = row.strip()
if not row:
continue
# Remove hline/cline
row = re.sub(r'\\hline', '', row)
row = re.sub(r'\\cline\{[^}]+\}', '', row)
row = row.strip()
if not row:
continue
# Handle multicolumn: expand into multiple identical cells
def expand_multicolumn(match):
cols = int(match.group(1))
content = match.group(2).strip()
return ' & '.join([content] * cols)
row = re.sub(r'\\multicolumn\{(\d+)\}\{[^}]+\}\{([^}]*)\}', expand_multicolumn, row)
# Handle multirow: capture and store for future rows
multirow_positions = {}
def capture_multirow(match):
span = int(match.group(1))
content = match.group(2).strip()
# We'll determine the position after splitting by &
return f"MULTIROW:{span}:{content}"
row = re.sub(r'\\multirow\{(\d+)\}\{[^}]*\}\{([^}]*)\}', capture_multirow, row)
# Split into cells
cells = [cell.strip() for cell in row.split('&')]
# Process multirow markers and build final cells
final_cells = []
for col_idx, cell in enumerate(cells):
if cell.startswith("MULTIROW:"):
parts = cell.split(":", 2)
span = int(parts[1])
content = parts[2] if len(parts) > 2 else ""
final_cells.append(content)
# Store for future rows (span-1 because current row counts)
active_multirows[col_idx] = (span - 1, content)
elif col_idx in active_multirows:
# This column has an active multirow from a previous row
span, content = active_multirows[col_idx]
final_cells.append(content)
# Decrease span
active_multirows[col_idx] = (span - 1, content)
if span - 1 <= 0:
del active_multirows[col_idx]
# Also include the original cell content if it's not empty
if cell and cell != content:
final_cells[-1] = cell # Override with current content
else:
final_cells.append(cell)
# Fill any remaining active multirows for columns beyond our current cells
max_col = max(len(final_cells), max(active_multirows.keys(), default=-1) + 1)
while len(final_cells) < max_col:
col_idx = len(final_cells)
if col_idx in active_multirows:
span, content = active_multirows[col_idx]
final_cells.append(content)
active_multirows[col_idx] = (span - 1, content)
if span - 1 <= 0:
del active_multirows[col_idx]
else:
final_cells.append('')
# Decrement remaining active multirows
to_delete = []
for col_idx in active_multirows:
span, content = active_multirows[col_idx]
active_multirows[col_idx] = (span - 1, content)
if span - 1 <= 0:
to_delete.append(col_idx)
for col_idx in to_delete:
del active_multirows[col_idx]
# Build markdown row
md_row = '| ' + ' | '.join(final_cells) + ' |'
lines.append(md_row)
# Add header separator after first row
if not header_separator_added:
separator = '|' + '---|' * len(final_cells)
lines.append(separator)
header_separator_added = True
return '\n'.join(lines)
def parse_pipe_table(content):
"""
Parses table content that uses pipes for columns and newlines for rows,
which is not standard LaTeX but appears in some Mathpix outputs.
"""
lines = content.strip().split('\n')
md_lines = []
header_separator_added = False
table_data_lines = []
caption_text_line = None
# Separate caption from table data. Heuristic: assumes caption is the
# last non-empty line starting with "Table X:".
potential_caption_lines = [
(i, line.strip()) for i, line in enumerate(lines)
if re.match(r'^\s*Table \d+:', line.strip(), re.IGNORECASE)
]
if potential_caption_lines:
caption_index, caption_full_line = potential_caption_lines[-1]
caption_text_line = re.sub(r'^\s*Table \d+:\s*', '', caption_full_line)
table_data_lines = lines[:caption_index]
else:
table_data_lines = lines
for line in table_data_lines:
line = line.strip()
# Skip empty lines and pre-existing markdown separators
if not line or line.startswith('|---'):
continue
# Handle multirow/multicolumn before splitting (simplistic approach)
line = re.sub(r'\\multirow\{[^}]+\}\{[^}]+\}\{([^}]+)\}', r'\1', line)
line = re.sub(r'\\multicolumn\{[^}]+\}\{[^}]+\}\{([^}]+)\}', r'\1', line)
cells = [cell.strip() for cell in line.split('|')]
# Clean up empty cells from leading/trailing pipes
if cells and not cells[0]:
cells.pop(0)
if cells and not cells[-1]:
cells.pop()
if not cells:
continue
md_row = '| ' + ' | '.join(cells) + ' |'
md_lines.append(md_row)
if not header_separator_added:
separator = '|' + '---|' * len(cells)
md_lines.append(separator)
header_separator_added = True
result = '\n'.join(md_lines)
if caption_text_line:
return f"**Table:** {caption_text_line}\n\n{result}"
return result
def convert_tables(text):
"""Convert all LaTeX tables to markdown tables"""
table_pattern = r'\\begin\{table\}.*?\\end\{table\}'
tabular_pattern = r'\\begin\{tabular\}.*?\\end\{tabular\}'
# First handle tables wrapped in the 'table' environment
def replace_table(match):
table_content = match.group(0)
# Check for standard LaTeX tabular environment first
tabular_match = re.search(tabular_pattern, table_content, re.DOTALL)
if tabular_match:
md_table = parse_latex_table(tabular_match.group(0))
# Extract caption from \caption{} command
caption_match = re.search(r'\\caption\{([^}]+)\}', table_content)
if caption_match:
caption = caption_match.group(1)
return f"**Table:** {caption}\n\n{md_table}"
return md_table
else:
# Fallback for non-standard, pipe-based tables
# Clean the inner content before parsing
inner_content = re.sub(r'\\begin\{table\}|\\end\{table\}', '', table_content)
inner_content = re.sub(r'\\caption\{[^}]+\}|\\label\{[^}]+\}', '', inner_content)
inner_content = re.sub(r'\\centering', '', inner_content)
return parse_pipe_table(inner_content)
text = re.sub(table_pattern, replace_table, text, flags=re.DOTALL)
# Then handle any standalone 'tabular' environments
def replace_tabular(match):
return parse_latex_table(match.group(0))
text = re.sub(tabular_pattern, replace_tabular, text, flags=re.DOTALL)
return text
def remove_latex_commands(text):
"""Remove various LaTeX commands that don't have markdown equivalents"""
# Remove footnotes
text = re.sub(r'\\footnote\{[^}]+\}', '', text)
# Remove some common, simple environments
text = re.sub(r'\\begin\{(?:figure|center)\}', '', text)
text = re.sub(r'\\end\{(?:figure|center)\}', '', text)
# Remove smallmatrix environments (convert to simple text)
text = re.sub(r'\\begin\{smallmatrix\}(.*?)\\end\{smallmatrix\}', r'[\1]', text, flags=re.DOTALL)
# Remove \left and \right which are not needed in standard Markdown math
text = re.sub(r'\\left|\\right', '', text)
# Remove centering command
text = re.sub(r'\\centering', '', text)
# Clean up excessive newlines
text = re.sub(r'\n{3,}', '\n\n', text)
return text
def mathpix_to_markdown(text):
"""Main conversion function applying all transformations in order."""
# The order of operations is important.
text = convert_display_math(text)
text = convert_inline_math(text)
text = convert_tables(text)
text = remove_latex_commands(text)
return text.strip()
def process_file(input_file, output_file):
"""Reads an input file, converts it, and saves to the output file."""
try:
print(f"Reading from '{input_file}'...")
with open(input_file, 'r', encoding='utf-8') as f:
content = f.read()
converted_content = mathpix_to_markdown(content)
print(f"Writing to '{output_file}'...")
with open(output_file, 'w', encoding='utf-8') as f:
f.write(converted_content)
print("Conversion complete.")
except FileNotFoundError:
print(f"Error: The file '{input_file}' was not found.", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"An error occurred during processing: {e}", file=sys.stderr)
sys.exit(1)
def main(in_path, out_path):
process_file(in_path, out_path)
if __name__ == "__main__":
main(r"/Users/arielkeslassy/PycharmProjects/PaperParser/Deep Residual Learning for Image Recognition.md",
r"/Users/arielkeslassy/PycharmProjects/PaperParser/output.md")