41 lines
1.2 KiB
Python
41 lines
1.2 KiB
Python
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)
|