""" Text processing utilities for file I/O and LaTeX formatting. This module provides functions for reading/writing text files and converting various space characters to appropriate LaTeX spacing commands. """ import os def read_text(path: str, encoding: str = "utf-8") -> str: """Read text from a file. Args: path: Path to the file to read encoding: Text encoding (default: utf-8) Returns: File contents as string Example: >>> content = read_text('input.tex') """ with open(path, "r", encoding=encoding) as f: return f.read() def write_text(path: str, content: str, encoding: str = "utf-8") -> None: """Write text to a file, creating parent directories if needed. Args: path: Path to the file to write content: Text content to write encoding: Text encoding (default: utf-8) Example: >>> write_text('output.tex', '\\\\sin x') """ dirpath = os.path.dirname(path) if dirpath: os.makedirs(dirpath, exist_ok=True) with open(path, "w", encoding=encoding) as f: f.write(content) def convert_spaces_to_latex(text: str) -> str: """Convert space characters in text to appropriate LaTeX spacing commands. Handles: - U+00A0 (NBSP, \\xa0) → ~ (tilde, non-breaking space in LaTeX) - U+0020 (regular space) → \\\\ (backslash-space) for each space Args: text: Input text possibly containing various space characters Returns: Text with spaces converted to LaTeX commands Example: >>> convert_spaces_to_latex('sin x') 'sin\\\\ x' >>> convert_spaces_to_latex('sin\\xa0x') # NBSP 'sin~x' """ if not text: return text # Quick check: if no spaces present, return as-is if '\xa0' not in text and ' ' not in text: return text result = [] i = 0 while i < len(text): char = text[i] if char == '\xa0': # U+00A0: Non-breaking space # Always use tilde for NBSP in LaTeX result.append('~') i += 1 elif char == ' ': # U+0020: Regular space # Count consecutive spaces space_count = 1 while i + space_count < len(text) and text[i + space_count] == ' ': space_count += 1 # Convert each space to backslash-space (backslash followed by space) # For N spaces, produce "\\ " repeated N times: "\\ \\ \\ " # Each '\\ ' is already a complete unit, so use empty join (not space join) result.append('\\ ' * space_count) i += space_count # Skip all processed spaces else: # Regular character result.append(char) i += 1 # Join results but preserve trailing spaces that are part of LaTeX commands result_str = ''.join(result) # Don't strip if the string ends with LaTeX space command '\\ ' # The trailing space in '\\ ' is semantically significant if result_str.endswith('\\ '): return result_str else: return result_str.rstrip() # Remove trailing space from last element def escape_latex_special_chars(text: str, preserve_nbsp: bool = True) -> str: """Escape special characters that have meaning in LaTeX. LaTeX special characters that need escaping: - $ (math mode delimiter) - ^ (superscript) - _ (subscript) - # (parameter token) - @ (letter category code) - ! (negative thin space) - % (comment) - & (alignment tab) - * (discretionary multiplication) - ~ (tilde) → \\sim in math mode Args: text: Input text possibly containing special characters preserve_nbsp: If True, preserve NBSP (\\xa0) for later conversion to ~ If False, convert NBSP to ~ before escaping Returns: Text with special characters escaped Example: >>> escape_latex_special_chars('$100 ^ 2') '\\$100 \\^ 2' >>> escape_latex_special_chars('H_2O') 'H\\_2O' >>> escape_latex_special_chars('a ~ b') 'a \\sim b' """ if not text: return text # CRITICAL: Preserve marker strings used for brace resolution # These markers distinguish source braces from generated LaTeX braces if text in ('__LBRACE__', '__RBRACE__', '__DELIM__'): return text # Use a temporary placeholder for NBSP to preserve it during escaping # This allows NBSP to be converted to ~ later for LaTeX spacing NBSP_PLACEHOLDER = '\x00NBSP\x00' # Using null bytes as unlikely to appear in text if preserve_nbsp and '\xa0' in text: # Temporarily replace NBSP with placeholder text = text.replace('\xa0', NBSP_PLACEHOLDER) # Mapping of special characters to their escaped versions # NOTE: In math mode, * is a valid operator and doesn't need escaping # NOTE: { and } are NOT escaped here - they're handled via __LBRACE__/__RBRACE__ markers # to distinguish source braces from generated LaTeX braces (e.g., \mathrm{...}) ESCAPE_MAP = { '$': r'\$', '%': r'\%', '#': r'\#', '&': r'\&', '_': r'\_', '^': r'\^', # NOTE: '~' is NOT escaped here - it's handled via UNICODE_TO_LATEX mapping to \sim '@': r'\@', '!': r'\!', # NOTE: '*' is NOT escaped - it's a valid multiplication operator in math mode '{': r'\{', '}': r'\}', } result = text for char, escaped in ESCAPE_MAP.items(): result = result.replace(char, escaped) # Restore NBSP placeholder back to NBSP for later processing if preserve_nbsp and NBSP_PLACEHOLDER in result: result = result.replace(NBSP_PLACEHOLDER, '\xa0') return result