mirror of https://github.com/eyhc1/rendercv.git
implement custom theme feature
This commit is contained in:
parent
a742e03c30
commit
2f300144ec
|
@ -13,7 +13,8 @@ has provided a valid RenderCV input. This is achieved through the use of
|
|||
|
||||
from datetime import date as Date
|
||||
from typing import Literal, Any, Type
|
||||
from typing_extensions import Annotated, Optional
|
||||
from typing_extensions import Annotated, Optional, get_args
|
||||
import importlib
|
||||
import functools
|
||||
from urllib.request import urlopen, HTTPError
|
||||
import json
|
||||
|
@ -927,7 +928,9 @@ class CurriculumVitae(RenderCVBaseModel):
|
|||
# the theme field, thanks Pydantic's discriminator feature.
|
||||
# See https://docs.pydantic.dev/2.5/concepts/fields/#discriminator for more information
|
||||
# about discriminators.
|
||||
Design = ClassicThemeOptions | ModerncvThemeOptions
|
||||
RenderCVDesign = Annotated[
|
||||
ClassicThemeOptions | ModerncvThemeOptions, pydantic.Field(discriminator="theme")
|
||||
]
|
||||
|
||||
|
||||
class RenderCVDataModel(RenderCVBaseModel):
|
||||
|
@ -937,13 +940,74 @@ class RenderCVDataModel(RenderCVBaseModel):
|
|||
title="Curriculum Vitae",
|
||||
description="The data of the CV.",
|
||||
)
|
||||
design: Design = pydantic.Field(
|
||||
design: RenderCVDesign | Any = pydantic.Field(
|
||||
default=ClassicThemeOptions(theme="classic"),
|
||||
title="Design",
|
||||
description="The design information of the CV.",
|
||||
discriminator="theme",
|
||||
)
|
||||
|
||||
@pydantic.field_validator("design")
|
||||
@classmethod
|
||||
def initialize_if_custom_theme_is_used(
|
||||
cls, design: RenderCVDesign | Any
|
||||
) -> RenderCVDesign | Any:
|
||||
"""Initialize the custom theme if it is used and validate it. Otherwise, return
|
||||
the built-in theme."""
|
||||
# `get_args` for an Annotated object returns the arguments when Annotated is
|
||||
# used. The first argument is actually the union of the types, so we need to
|
||||
# access the first argument to use isinstance function.
|
||||
theme_data_model_types = get_args(RenderCVDesign)[0]
|
||||
if isinstance(design, theme_data_model_types):
|
||||
# it is a built-in theme
|
||||
return design
|
||||
else:
|
||||
# check if the theme name is valid:
|
||||
if not design["theme"].isalpha(): # type: ignore
|
||||
raise ValueError(
|
||||
"The custom theme name should contain only letters.",
|
||||
"theme", # this is the location of the error
|
||||
design["theme"], # this is value of the error # type: ignore
|
||||
)
|
||||
|
||||
# then it is a custom theme
|
||||
custom_theme_folder = pathlib.Path(design["theme"]) # type: ignore
|
||||
|
||||
# check if all the necessary files are provided in the custom theme folder:
|
||||
required_files = [
|
||||
"__init__.py", # design's data model
|
||||
"EducationEntry.j2.tex", # education entry template
|
||||
"ExperienceEntry.j2.tex", # experience entry template
|
||||
"NormalEntry.j2.tex", # normal entry template
|
||||
"OneLineEntry.j2.tex", # one line entry template
|
||||
"PublicationEntry.j2.tex", # publication entry template
|
||||
"TextEntry.j2.tex", # text entry template
|
||||
"SectionTitle.j2.tex", # section title template
|
||||
"Preamble.j2.tex", # preamble template
|
||||
"Header.j2.tex", # header template
|
||||
]
|
||||
|
||||
for file in required_files:
|
||||
file_path = custom_theme_folder / file
|
||||
if not file_path.exists():
|
||||
raise ValueError(
|
||||
f"You provided a custom theme, but the file `{file}` is not"
|
||||
f" found in the folder `{custom_theme_folder}`.",
|
||||
"", # this is the location of the error
|
||||
design["theme"], # this is value of the error # type: ignore
|
||||
)
|
||||
|
||||
# import __init__.py file from the custom theme folder:
|
||||
theme_module = importlib.import_module(design["theme"]) # type: ignore
|
||||
|
||||
ThemeDataModel = getattr(
|
||||
theme_module, f"{design['theme'].title()}ThemeOptions" # type: ignore
|
||||
)
|
||||
|
||||
# initialize and validate the custom theme data model:
|
||||
theme_data_model = ThemeDataModel(**design)
|
||||
|
||||
return theme_data_model
|
||||
|
||||
|
||||
def escape_latex_characters(string: str) -> str:
|
||||
"""Escape $\\LaTeX$ characters in a string.
|
||||
|
|
|
@ -10,6 +10,7 @@ distribution.
|
|||
|
||||
import subprocess
|
||||
import re
|
||||
import os
|
||||
import pathlib
|
||||
import importlib.resources
|
||||
import shutil
|
||||
|
@ -111,16 +112,27 @@ class LaTeXFile:
|
|||
f"{self.design.theme}/{template_name}.j2.tex"
|
||||
)
|
||||
|
||||
# Loop through the entry attributes and make them "" if they are None:
|
||||
# This is necessary because otherwise Jinja2 will template them as "None".
|
||||
if entry is not None and not isinstance(entry, str):
|
||||
for key, value in entry.model_dump().items():
|
||||
if value is None:
|
||||
try:
|
||||
entry.__setattr__(key, "")
|
||||
except ValueError:
|
||||
# Then it means it's a computed_field, so it can be ignored.
|
||||
pass
|
||||
# # Loop through the entry attributes and make them "" if they are None:
|
||||
# # This is necessary because otherwise Jinja2 will template them as "None".
|
||||
# if entry is not None and not isinstance(entry, str):
|
||||
# for key, value in entry.model_dump().items():
|
||||
# if value is None:
|
||||
# if "date" not in key:
|
||||
# # don't touch the date fields, because only date_string is
|
||||
# # called and setting dates to "" will cause problems.
|
||||
# try:
|
||||
# entry.__setattr__(key, "")
|
||||
# except ValueError:
|
||||
# # Then it means it's a computed_field, so it can be ignored.
|
||||
# pass
|
||||
|
||||
# do the below with a loop:
|
||||
if hasattr(entry, "highlights") and entry.highlights is None: # type: ignore
|
||||
entry.highlights = "" # type: ignore
|
||||
if hasattr(entry, "location") and entry.location is None: # type: ignore
|
||||
entry.location = "" # type: ignore
|
||||
if hasattr(entry, "degree") and entry.degree is None: # type: ignore
|
||||
entry.degree = "" # type: ignore
|
||||
|
||||
# The arguments of the template can be used in the template file:
|
||||
latex_code = template.render(
|
||||
|
@ -380,8 +392,10 @@ def setup_jinja2_environment() -> jinja2.Environment:
|
|||
jinja2.Environment: The theme environment.
|
||||
"""
|
||||
# create a Jinja2 environment:
|
||||
# we need to add the current working directory because custom themes might be used.
|
||||
themes_directory = pathlib.Path(__file__).parent / "themes"
|
||||
environment = jinja2.Environment(
|
||||
loader=jinja2.PackageLoader("rendercv", "themes"),
|
||||
loader=jinja2.FileSystemLoader([os.getcwd(), themes_directory]),
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
|
@ -450,7 +464,12 @@ def copy_theme_files_to_output_directory(
|
|||
theme_name (str): The name of the theme.
|
||||
output_directory (pathlib.Path): Path to the output directory.
|
||||
"""
|
||||
theme_directory = importlib.resources.files(f"rendercv.themes.{theme_name}")
|
||||
try:
|
||||
theme_directory = importlib.resources.files(f"rendercv.themes.{theme_name}")
|
||||
except ModuleNotFoundError:
|
||||
# Then it means the theme is a custom theme:
|
||||
theme_directory = pathlib.Path(os.getcwd()) / theme_name
|
||||
|
||||
for theme_file in theme_directory.iterdir():
|
||||
if not ("j2.tex" in theme_file.name or "py" in theme_file.name):
|
||||
if theme_file.is_dir():
|
||||
|
@ -508,12 +527,13 @@ def latex_to_pdf(latex_file_path: pathlib.Path) -> pathlib.Path:
|
|||
raise OSError(f"TinyTeX doesn't support the platform {sys.platform}!")
|
||||
|
||||
# Run TinyTeX:
|
||||
command = [
|
||||
executables[sys.platform],
|
||||
str(latex_file_path.absolute()),
|
||||
"-lualatex",
|
||||
]
|
||||
with subprocess.Popen(
|
||||
[
|
||||
executables[sys.platform],
|
||||
str(latex_file_path.name),
|
||||
"-lualatex",
|
||||
],
|
||||
command,
|
||||
cwd=latex_file_path.parent,
|
||||
stdout=subprocess.DEVNULL, # don't capture the output
|
||||
stderr=subprocess.DEVNULL, # don't capture the error
|
||||
|
@ -523,8 +543,10 @@ def latex_to_pdf(latex_file_path: pathlib.Path) -> pathlib.Path:
|
|||
if latex_process.returncode != 0:
|
||||
raise RuntimeError(
|
||||
"Running TinyTeX has failed! For debugging, we suggest running the"
|
||||
" LaTeX file manually in overleaf.com or another LaTeX editor. If you"
|
||||
" can't solve the problem, please open an issue on GitHub.",
|
||||
" LaTeX file manually in https://overleaf.com.",
|
||||
"If you want to run it locally, run the command below in the terminal:",
|
||||
" ".join([str(command_part) for command_part in command]),
|
||||
"If you can't solve the problem, please open an issue on GitHub.",
|
||||
)
|
||||
|
||||
# check if the PDF file is generated:
|
||||
|
|
Loading…
Reference in New Issue