mirror of
https://github.com/rendercv/rendercv.git
synced 2026-10-24 21:28:47 -04:00
341 lines
9.8 KiB
Python
341 lines
9.8 KiB
Python
"""This module implements LaTeX file generation and LaTeX runner utilities for RenderCV.
|
|
"""
|
|
import subprocess
|
|
import os
|
|
import re
|
|
import shutil
|
|
|
|
from jinja2 import Environment, PackageLoader
|
|
|
|
|
|
def markdown_to_latex(markdown_string: str) -> str:
|
|
"""Convert a markdown string to LaTeX.
|
|
|
|
This function is used as a Jinja2 filter.
|
|
|
|
Example:
|
|
```python
|
|
markdown_to_latex("This is a **bold** text with an [*italic link*](https://google.com).")
|
|
```
|
|
|
|
will return:
|
|
|
|
`#!pytjon "This is a \\textbf{bold} text with a \\hrefExternal{https://google.com}{\\textit{link}}."`
|
|
|
|
Args:
|
|
value (str): The markdown string to convert.
|
|
|
|
Returns:
|
|
str: The LaTeX string.
|
|
"""
|
|
if not isinstance(markdown_string, str):
|
|
raise ValueError("markdown_to_latex should only be used on strings!")
|
|
|
|
# convert links
|
|
links = re.findall(r"\[([^\]\[]*)\]\((.*?)\)", markdown_string)
|
|
if links is not None:
|
|
for link in links:
|
|
link_text = link[0]
|
|
link_url = link[1]
|
|
|
|
old_link_string = f"[{link_text}]({link_url})"
|
|
new_link_string = "\\hrefExternal{" + link_url + "}{" + link_text + "}"
|
|
|
|
markdown_string = markdown_string.replace(old_link_string, new_link_string)
|
|
|
|
# convert bold
|
|
bolds = re.findall(r"\*\*([^\*]*)\*\*", markdown_string)
|
|
if bolds is not None:
|
|
for bold_text in bolds:
|
|
old_bold_text = f"**{bold_text}**"
|
|
new_bold_text = "\\textbf{" + bold_text + "}"
|
|
|
|
markdown_string = markdown_string.replace(old_bold_text, new_bold_text)
|
|
|
|
# convert italic
|
|
italics = re.findall(r"\*([^\*]*)\*", markdown_string)
|
|
if italics is not None:
|
|
for italic_text in italics:
|
|
old_italic_text = f"*{italic_text}*"
|
|
new_italic_text = "\\textit{" + italic_text + "}"
|
|
|
|
markdown_string = markdown_string.replace(old_italic_text, new_italic_text)
|
|
|
|
latex_string = markdown_string
|
|
|
|
return latex_string
|
|
|
|
|
|
def markdown_url_to_url(value: str) -> bool:
|
|
"""Convert a markdown link to a normal string URL.
|
|
|
|
This function is used as a Jinja2 filter.
|
|
|
|
Example:
|
|
```python
|
|
markdown_url_to_url("[Google](https://google.com)")
|
|
```
|
|
|
|
will return:
|
|
|
|
`#!python "https://google.com"`
|
|
|
|
Args:
|
|
value (str): The markdown link to convert.
|
|
|
|
Returns:
|
|
str: The URL as a string.
|
|
"""
|
|
if not isinstance(value, str):
|
|
raise ValueError("markdown_to_latex should only be used on strings!")
|
|
|
|
link = re.search(r"\[(.*)\]\((.*?)\)", value)
|
|
if link is not None:
|
|
url = link.groups()[1]
|
|
return url
|
|
else:
|
|
raise ValueError("markdown_url_to_url should only be used on markdown links!")
|
|
|
|
|
|
def make_it_bold(value: str, match_str: str) -> str:
|
|
"""Make the matched parts of the string bold.
|
|
|
|
This function is used as a Jinja2 filter.
|
|
|
|
Example:
|
|
```python
|
|
make_it_bold_if("Hello World!", "Hello")
|
|
```
|
|
|
|
will return:
|
|
|
|
`#!python "\\textbf{Hello} World!"`
|
|
|
|
Args:
|
|
value (str): The string to make bold.
|
|
match_str (str): The string to match.
|
|
"""
|
|
if not isinstance(value, str):
|
|
raise ValueError("make_it_bold_if should only be used on strings!")
|
|
|
|
if not isinstance(match_str, str):
|
|
raise ValueError("The string to match should be a string!")
|
|
|
|
if match_str in value:
|
|
value = value.replace(match_str, "\\textbf{" + match_str + "}")
|
|
return value
|
|
else:
|
|
return value
|
|
|
|
|
|
def make_it_underlined(value: str, match_str: str) -> str:
|
|
"""Make the matched parts of the string underlined.
|
|
|
|
This function is used as a Jinja2 filter.
|
|
|
|
Example:
|
|
```python
|
|
make_it_underlined_if("Hello World!", "Hello")
|
|
```
|
|
|
|
will return:
|
|
|
|
`#!python "\\underline{Hello} World!"`
|
|
|
|
Args:
|
|
value (str): The string to make underlined.
|
|
match_str (str): The string to match.
|
|
"""
|
|
if not isinstance(value, str):
|
|
raise ValueError("make_it_underlined_if should only be used on strings!")
|
|
|
|
if not isinstance(match_str, str):
|
|
raise ValueError("The string to match should be a string!")
|
|
|
|
if match_str in value:
|
|
value = value.replace(match_str, "\\underline{" + match_str + "}")
|
|
return value
|
|
else:
|
|
return value
|
|
|
|
|
|
def make_it_italic(value: str, match_str: str) -> str:
|
|
"""Make the matched parts of the string italic.
|
|
|
|
This function is used as a Jinja2 filter.
|
|
|
|
Example:
|
|
```python
|
|
make_it_italic_if("Hello World!", "Hello")
|
|
```
|
|
|
|
will return:
|
|
|
|
`#!python "\\textit{Hello} World!"`
|
|
|
|
Args:
|
|
value (str): The string to make italic.
|
|
match_str (str): The string to match.
|
|
"""
|
|
if not isinstance(value, str):
|
|
raise ValueError("make_it_italic_if should only be used on strings!")
|
|
|
|
if not isinstance(match_str, str):
|
|
raise ValueError("The string to match should be a string!")
|
|
|
|
if match_str in value:
|
|
value = value.replace(match_str, "\\textit{" + match_str + "}")
|
|
return value
|
|
else:
|
|
return value
|
|
|
|
|
|
def divide_length_by(length: str, divider: float) -> str:
|
|
# r"""Divide a length by a number.
|
|
|
|
# Length is a string with the following regex pattern: `\d+\.?\d* *(cm|in|pt|mm|ex|em)`
|
|
# """
|
|
# Get the value as a float and the unit as a string:
|
|
value = re.search(r"\d+\.?\d*", length).group()
|
|
unit = re.findall(r"[^\d\.\s]+", length)[0]
|
|
|
|
return str(float(value) / divider) + " " + unit
|
|
|
|
|
|
def get_today() -> str:
|
|
"""Return today's date.
|
|
|
|
Returns:
|
|
str: Today's date.
|
|
"""
|
|
from datetime import date
|
|
|
|
today = date.today()
|
|
return today.strftime("%B %d, %Y")
|
|
|
|
|
|
def get_path_to_font_directory(font_name: str) -> str:
|
|
"""Return the path to the fonts directory.
|
|
|
|
Returns:
|
|
str: The path to the fonts directory.
|
|
"""
|
|
return os.path.join(os.path.dirname(__file__), "templates", "fonts", font_name)
|
|
|
|
|
|
def render_template(data):
|
|
"""Render the template using the given data.
|
|
|
|
Args:
|
|
data (RenderCVDataModel): The data to use to render the template.
|
|
|
|
Returns:
|
|
str: The path to the rendered LaTeX file.
|
|
"""
|
|
# templates_directory = os.path.dirname(os.path.dirname())
|
|
|
|
# create a Jinja2 environment:
|
|
environment = Environment(
|
|
loader=PackageLoader("rendercv", "templates"),
|
|
trim_blocks=True,
|
|
lstrip_blocks=True,
|
|
)
|
|
|
|
# add new functions to the environment:
|
|
environment.globals.update(str=str)
|
|
|
|
# set custom delimiters for LaTeX templating:
|
|
environment.block_start_string = "((*"
|
|
environment.block_end_string = "*))"
|
|
environment.variable_start_string = "<<"
|
|
environment.variable_end_string = ">>"
|
|
environment.comment_start_string = "((#"
|
|
environment.comment_end_string = "#))"
|
|
|
|
# add custom filters:
|
|
environment.filters["markdown_to_latex"] = markdown_to_latex
|
|
environment.filters["markdown_url_to_url"] = markdown_url_to_url
|
|
environment.filters["make_it_bold"] = make_it_bold
|
|
environment.filters["make_it_underlined"] = make_it_underlined
|
|
environment.filters["make_it_italic"] = make_it_italic
|
|
environment.filters["divide_length_by"] = divide_length_by
|
|
|
|
# load the template:
|
|
theme = data.design.theme
|
|
template = environment.get_template(f"{theme}.tex.j2")
|
|
|
|
output_latex_file = template.render(
|
|
cv=data.cv,
|
|
design=data.design,
|
|
theme_options=data.design.options,
|
|
today=get_today(),
|
|
)
|
|
|
|
# Create an output file and write the rendered LaTeX code to it:
|
|
output_file_path = os.path.join(os.getcwd(), "tests", "outputs", "test.tex")
|
|
os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
|
|
with open(output_file_path, "w") as file:
|
|
file.write(output_latex_file)
|
|
|
|
# Copy the fonts directory to the output directory:
|
|
# Remove the old fonts directory if it exists:
|
|
if os.path.exists(os.path.join(os.path.dirname(output_file_path), "fonts")):
|
|
shutil.rmtree(os.path.join(os.path.dirname(output_file_path), "fonts"))
|
|
|
|
font_directory = get_path_to_font_directory(data.design.font)
|
|
output_fonts_directory = os.path.join(os.path.dirname(output_file_path), "fonts")
|
|
shutil.copytree(
|
|
font_directory,
|
|
output_fonts_directory,
|
|
dirs_exist_ok=True,
|
|
)
|
|
|
|
return output_file_path
|
|
|
|
|
|
def run_latex(latex_file_path):
|
|
"""
|
|
Run TinyTeX with the given LaTeX file and generate a PDF.
|
|
|
|
Args:
|
|
latexFilePath (str): The path to the LaTeX file to compile.
|
|
"""
|
|
latex_file_path = os.path.normpath(latex_file_path)
|
|
latex_file = os.path.basename(latex_file_path)
|
|
|
|
if os.name == "nt":
|
|
# remove all files except the .tex file
|
|
for file in os.listdir(os.path.dirname(latex_file_path)):
|
|
if file.endswith(".tex") or file == "fonts":
|
|
continue
|
|
os.remove(os.path.join(os.path.dirname(latex_file_path), file))
|
|
|
|
tinytexPath = os.path.join(
|
|
os.path.dirname(__file__),
|
|
"vendor",
|
|
"TinyTeX",
|
|
"bin",
|
|
"windows",
|
|
)
|
|
print("PDF generatation started!")
|
|
subprocess.run(
|
|
[
|
|
f"{tinytexPath}\\latexmk.exe",
|
|
"-lualatex",
|
|
# "-c",
|
|
f"{latex_file}",
|
|
"-synctex=1",
|
|
"-interaction=nonstopmode",
|
|
"-file-line-error",
|
|
],
|
|
cwd=os.path.dirname(latex_file_path),
|
|
stdout=subprocess.DEVNULL,
|
|
)
|
|
print("PDF generated successfully!")
|
|
else:
|
|
print(
|
|
"Only Windows is supported for now. But you can still use the generated"
|
|
" .tex file to generate the PDF. Go to overleaf.com and upload the .tex"
|
|
" file there to generate the PDF."
|
|
)
|