import re _SPECIAL_CHARS = { "&": r"\&", "%": r"\%", "$": r"\$", "#": r"\#", "_": r"\_", "{": r"\{", "}": r"\}", "~": r"\textasciitilde{}", "^": r"\textasciicircum{}", "\\": r"\textbackslash{}", } _SPECIAL_CHARS_RE = re.compile("|".join(re.escape(c) for c in _SPECIAL_CHARS)) def escape_latex(value: str) -> str: """Escape LaTeX special characters in plain user-entered text.""" return _SPECIAL_CHARS_RE.sub(lambda m: _SPECIAL_CHARS[m.group(0)], value) _STEP_BOLD_RE = re.compile(r"^\*\*(.+?)\*\*(.*)$") def format_multiline(value: str) -> str: """Escape text and convert user newlines into LaTeX line breaks. Supports a leading "**Label**: rest" line convention (seen in the existing hand-written recipes) which becomes \\textbf{Label}: rest. """ lines = value.strip().splitlines() rendered_lines = [] for line in lines: match = _STEP_BOLD_RE.match(line) if match: label, rest = match.groups() rendered_lines.append(f"\\textbf{{{escape_latex(label)}}}{escape_latex(rest)}") else: rendered_lines.append(escape_latex(line)) return " \\\\\n ".join(rendered_lines)