#!/usr/bin/env python3 """ CLI entry point for refactored MathML to LaTeX converter. Usage: python -m refactor --docx file.docx uv run python -m refactor --docx file.docx --subject chemistry """ import argparse import sys import os from pathlib import Path import time # Use relative imports for standalone module from .compat import LegacyMathMLToTeXConverter from .parsers import InputParserFactory from .utils.timer import PerformanceTimer def handle_json_batch(args, timer): """ Handle JSON batch conversion mode. Args: args: Parsed command-line arguments timer: PerformanceTimer instance Returns: Exit code (0 for success, non-zero for error) """ import json from .batch import BatchJSONConverter try: # Read input JSON with timer.measure("json_read"): if args.verbose and not args.quiet: print(f"Reading JSON from {args.json}...", file=sys.stderr) with open(args.json, 'r', encoding='utf-8') as f: input_data = json.load(f) # Validate input format if not isinstance(input_data, dict): print("Error: Input JSON must be a dictionary (key -> equation mapping)", file=sys.stderr) return 1 # Support both flat dict and nested "mathml_dict" format if "mathml_dict" in input_data: equations_dict = input_data["mathml_dict"] if args.verbose and not args.quiet: print("Detected nested 'mathml_dict' format", file=sys.stderr) else: equations_dict = input_data if not equations_dict: print("Error: No equations found in input JSON", file=sys.stderr) return 1 if not args.quiet: print(f"INFO: Found {len(equations_dict)} equations in JSON file", file=sys.stderr) # Create batch converter with timer.measure("converter_init"): converter = BatchJSONConverter( subject_mode=args.subject, wrap_identifiers=args.wrap_mathrm, prefer_xlongequal=not args.no_xlongequal, universal_delimiter_scaling=not args.no_universal_delimiters, verbose=args.verbose and not args.quiet, ) # Convert (parallel or sequential) if args.parallel and len(equations_dict) >= 10: if not args.quiet: print(f"INFO: Using parallel processing with {args.workers or 'auto'} workers", file=sys.stderr) with timer.measure("parallel_batch_conversion", {"equations": len(equations_dict)}): results = converter.convert_batch_parallel( equations_dict, workers=args.workers, quiet=args.quiet ) else: if args.parallel and not args.quiet: print("INFO: Sequential mode (< 10 equations, parallel not beneficial)", file=sys.stderr) with timer.measure("sequential_batch_conversion", {"equations": len(equations_dict)}): results = converter.convert_batch(equations_dict, quiet=args.quiet) # Format output JSON with timer.measure("json_format"): output_json = json.dumps(results, ensure_ascii=False, indent=2) # Write output with timer.measure("file_output"): if args.output: with open(args.output, 'w', encoding='utf-8') as f: f.write(output_json) if not args.quiet: print(f"Wrote {len(results)} results to {args.output}", file=sys.stderr) else: print(output_json) # Show timing report if requested if args.show_timing and not args.quiet: print("\n", file=sys.stderr) print(timer.format_report(), file=sys.stderr) return 0 except FileNotFoundError: print(f"Error: Input file not found: {args.json}", file=sys.stderr) return 1 except json.JSONDecodeError as e: print(f"Error: Invalid JSON in input file: {e}", file=sys.stderr) return 1 except Exception as e: print(f"Error: {e}", file=sys.stderr) if args.verbose: import traceback traceback.print_exc() return 1 def main(): """Main CLI entry point.""" parser = argparse.ArgumentParser( description="Refactored OMML/MathML → LaTeX converter with design patterns", epilog="Examples:\n" " python -m omml2tex --docx document.docx --output output.tex\n" " python -m omml2tex --mathml input.xml --subject chemistry\n" " python -m omml2tex --json equations.json -o results.json\n" " python -m omml2tex --json equations.json --parallel --workers 4\n" " uv run python -m omml2tex --docx file.docx\n", formatter_class=argparse.RawDescriptionHelpFormatter ) # Input options (mutually exclusive) input_group = parser.add_mutually_exclusive_group(required=True) input_group.add_argument("--omml", help="Path to input OMML XML file") input_group.add_argument("--docx", help="Path to input DOCX file") input_group.add_argument("--mathml", help="Path to input MathML XML file (use '-' for stdin)") input_group.add_argument("--json", help="Path to input JSON file (key -> equation mapping for batch conversion)") # Output options parser.add_argument("--output", "-o", help="Output file path (default: stdout)") parser.add_argument("--output-format", choices=["latex", "json"], default="latex", help="Output format (default: latex)") # Conversion options parser.add_argument("--wrap-mathrm", action="store_true", help="Wrap multi-character identifiers in \\mathrm{...} (disabled by default)") parser.add_argument("--no-xlongequal", action="store_true", help="Do not prefer \\xlongequal for overset '='") parser.add_argument("--subject", choices=["auto", "math", "physics", "chemistry", "statistics"], default="math", help="Subject mode for specialized formatting (default: math for MathML, auto for DOCX)") parser.add_argument("--no-universal-delimiters", action="store_true", help="Disable universal \\left\\right scaling for all delimiters (enabled by default)") # Advanced options parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") parser.add_argument("--quiet", "-q", action="store_true", help="Quiet mode (errors only)") parser.add_argument("--numbered", action="store_true", help="Number equations in output") # Performance options parser.add_argument("--parallel", action="store_true", help="Enable parallel processing for faster conversion (recommended for 5+ equations)") parser.add_argument("--workers", type=int, default=None, help="Number of worker processes (default: CPU count, max 8)") parser.add_argument("--show-timing", action="store_true", help="Show detailed performance timing report at the end") args = parser.parse_args() # Initialize performance timer timer = PerformanceTimer(enabled=args.show_timing) try: # Start total timing with timer.measure("total_execution"): # JSON batch mode has different I/O semantics - handle separately if args.json: return handle_json_batch(args, timer) # Determine input source and type (single-file modes) with timer.measure("input_detection"): if args.docx: input_path = args.docx input_type = "docx" # Auto-detect subject mode for DOCX if not explicitly set if args.subject == "math": args.subject = "auto" elif args.omml: input_path = args.omml input_type = "omml" elif args.mathml: input_path = args.mathml input_type = "mathml" else: print("Error: No input file specified", file=sys.stderr) return 1 # Handle stdin ("-") if input_path == "-": if args.verbose and not args.quiet: print("Reading from stdin...", file=sys.stderr) input_path = sys.stdin.read() # Parse input file to get MathML equations if args.verbose and not args.quiet: print(f"Parsing input: {input_path if input_path != '-' else 'stdin'}", file=sys.stderr) try: with timer.measure("input_parsing", {"type": input_type}): parser_obj = InputParserFactory.create_parser(input_path, parser_type=input_type) mathml_equations = parser_obj.parse(input_path) except Exception as e: print(f"Error parsing input: {e}", file=sys.stderr) return 1 if args.verbose and not args.quiet: print(f"Found {len(mathml_equations)} equation(s)", file=sys.stderr) # Print info messages to stderr (like original converter) if not args.quiet: print(f"INFO: Processing {'DOCX' if args.docx else 'MathML' if args.mathml else 'OMML'} file: {input_path}", file=sys.stderr) print(f"INFO: Found {len(mathml_equations)} equations in {input_path}", file=sys.stderr) print(f"INFO: Auto-detected subject mode: {args.subject}", file=sys.stderr) # Convert equations - use parallel processing if enabled if args.parallel and len(mathml_equations) >= 5: # PARALLEL MODE: Use multiprocessing for faster conversion if not args.quiet: print(f"INFO: Using parallel processing with {args.workers or 'auto'} workers", file=sys.stderr) from .parallel import ParallelConverter with timer.measure("parallel_conversion", {"equations": len(mathml_equations)}): parallel_converter = ParallelConverter( workers=args.workers, subject_mode=args.subject, wrap_identifiers=args.wrap_mathrm, prefer_xlongequal=not args.no_xlongequal, universal_delimiter_scaling=not args.no_universal_delimiters, show_progress=not args.quiet, ) # Convert in parallel conversion_results = parallel_converter.convert_batch(mathml_equations, quiet=args.quiet) # Format results with timer.measure("format_results"): results = [] for i, result in enumerate(conversion_results, 1): if result['success']: latex = result['latex'] # Wrap in $ ... $ for inline math if '\\begin{' in latex or latex.startswith('\\['): wrapped = f"${latex}$" else: wrapped = f"${latex}$" if args.numbered: results.append(f"% Equation {i}\n{wrapped}") else: results.append(wrapped) else: error_msg = result.get('error', 'Unknown error') if not args.quiet: print(f"Warning: Failed to convert equation {i}: {error_msg}", file=sys.stderr) results.append(f"% Error: {error_msg}") else: # SEQUENTIAL MODE: Original single-threaded conversion if args.parallel and not args.quiet: print("INFO: Sequential mode (< 5 equations, parallel not beneficial)", file=sys.stderr) with timer.measure("converter_init"): converter = LegacyMathMLToTeXConverter( wrap_identifiers_in_mathrm=args.wrap_mathrm, prefer_xlongequal=not args.no_xlongequal, subject_mode=args.subject, universal_delimiter_scaling=not args.no_universal_delimiters, ) # Convert equations results = [] for i, mathml in enumerate(mathml_equations, 1): try: with timer.measure("equation_conversion"): latex = converter.convert(mathml) # Wrap in $ ... $ for inline math (match original behavior) # Use $$ ... $$ if equation contains \begin{...} or is display-style if '\\begin{' in latex or latex.startswith('\\['): wrapped = f"${latex}$" else: wrapped = f"${latex}$" if args.numbered: results.append(f"% Equation {i}\n{wrapped}") else: results.append(wrapped) except Exception as e: if not args.quiet: print(f"Warning: Failed to convert equation {i}: {e}", file=sys.stderr) results.append(f"% Error: {e}") # Calculate success stats with timer.measure("statistics_calculation"): successful = sum(1 for r in results if not r.startswith("% Error")) failed = len(results) - successful success_rate = (successful / len(results) * 100) if results else 0 # Format output with timer.measure("output_formatting"): if args.output_format == "json": import json output = json.dumps({ "equations": results, "count": len(results), "subject_mode": args.subject, "successful": successful, "failed": failed, }, indent=2) else: # latex # Add summary header (matching original format) if not args.quiet: summary = [ "", "Conversion Results:", f" Total equations: {len(results)}", f" Successful: {successful}", f" Failed: {failed}", f" Success rate: {success_rate:.1f}%", "", "=" * 50, "CONVERTED EQUATIONS:", "=" * 50, ] output = "\n".join(summary) + "\n" + "\n\n".join(results) else: output = "\n\n".join(results) # Write output with timer.measure("file_output"): if args.output: with open(args.output, 'w', encoding='utf-8') as f: f.write(output) if not args.quiet: print(f"Wrote {len(results)} equation(s) to {args.output}", file=sys.stderr) else: print(output) # Show timing report if requested if args.show_timing and not args.quiet: print("\n", file=sys.stderr) print(timer.format_report(), file=sys.stderr) return 0 except KeyboardInterrupt: print("\nInterrupted", file=sys.stderr) return 130 except Exception as e: print(f"Error: {e}", file=sys.stderr) if args.verbose: import traceback traceback.print_exc() return 1 if __name__ == '__main__': sys.exit(main())