Spaces:
Running
Running
Download post_nougat_parser.py from ArielKes/DeepDigest: direct link, hf CLI and curl.
- Browser
- Download file 11.5 kB
-
https://huggingface.co/spaces/ArielKes/DeepDigest/resolve/main/post_nougat_parser.py
- Command line
-
hf download hf://spaces/ArielKes/DeepDigest/post_nougat_parser.py
-
curl -L -o post_nougat_parser.py https://huggingface.co/spaces/ArielKes/DeepDigest/resolve/main/post_nougat_parser.py
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") |