Coverage for rendercv/renderer/renderer.py: 89%
108 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-26 00:25 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-26 00:25 +0000
1"""
2The `rendercv.renderer.renderer` module contains the necessary functions for rendering
3Typst, PDF, Markdown, HTML, and PNG files from the `RenderCVDataModel` object.
4"""
6import importlib.resources
7import pathlib
8import re
9import shutil
10import sys
11from typing import Any, Literal, Optional
13from .. import data
14from . import templater
17def copy_theme_files_to_output_directory(
18 theme_name: str,
19 output_directory_path: pathlib.Path,
20):
21 """Copy the auxiliary files (all the files that don't end with `.j2.typ` and `.py`)
22 of the theme to the output directory.
24 Args:
25 theme_name: The name of the theme.
26 output_directory_path: Path to the output directory.
27 """
28 if theme_name in data.available_themes:
29 theme_directory_path = importlib.resources.files(
30 f"rendercv.themes.{theme_name}"
31 )
32 else:
33 # Then it means the theme is a custom theme. If theme_directory is not given
34 # as an argument, then look for the theme in the current working directory.
35 theme_directory_path = pathlib.Path.cwd() / theme_name
37 if not theme_directory_path.is_dir():
38 message = (
39 f"The theme {theme_name} doesn't exist in the available themes and"
40 " the current working directory!"
41 )
42 raise FileNotFoundError(message)
44 dont_copy_files_with_these_extensions = [".py", ".j2.typ"]
45 for theme_file in theme_directory_path.iterdir():
46 # theme_file.suffix returns the latest part of the file name after the last dot.
47 # But we need the latest part of the file name after the first dot:
48 try:
49 suffix = re.search(r"\..*", theme_file.name)[0] # type: ignore
50 except TypeError:
51 suffix = ""
53 if suffix not in dont_copy_files_with_these_extensions:
54 if theme_file.is_dir():
55 shutil.copytree(
56 str(theme_file),
57 output_directory_path / theme_file.name,
58 dirs_exist_ok=True,
59 )
60 else:
61 shutil.copyfile(
62 str(theme_file), output_directory_path / theme_file.name
63 )
66def create_contents_of_a_typst_file(
67 rendercv_data_model: data.RenderCVDataModel,
68) -> str:
69 """Create a Typst file with the given data model and return it as a string.
71 Args:
72 rendercv_data_model: The data model.
74 Returns:
75 The path to the generated Typst file.
76 """
77 jinja2_environment = templater.Jinja2Environment().environment
79 file_object = templater.TypstFile(
80 rendercv_data_model,
81 jinja2_environment,
82 )
84 return file_object.get_full_code()
87def create_a_typst_file(
88 rendercv_data_model: data.RenderCVDataModel,
89 output_directory: pathlib.Path,
90) -> pathlib.Path:
91 """Create a Typst file (depending on the theme) with the given data model and write
92 it to the output directory.
94 Args:
95 rendercv_data_model: The data model.
96 output_directory: Path to the output directory. If not given, the Typst file
97 will be returned as a string.
99 Returns:
100 The path to the generated Typst file.
101 """
103 typst_contents = create_contents_of_a_typst_file(rendercv_data_model)
105 # Create output directory if it doesn't exist:
106 if not output_directory.is_dir():
107 output_directory.mkdir(parents=True)
109 file_name = f"{str(rendercv_data_model.cv.name).replace(' ', '_')}_CV.typ"
110 file_path = output_directory / file_name
111 file_path.write_text(typst_contents, encoding="utf-8")
113 return file_path
116def create_a_markdown_file(
117 rendercv_data_model: data.RenderCVDataModel, output_directory: pathlib.Path
118) -> pathlib.Path:
119 """Render the Markdown file with the given data model and write it to the output
120 directory.
122 Args:
123 rendercv_data_model: The data model.
124 output_directory: Path to the output directory.
126 Returns:
127 The path to the rendered Markdown file.
128 """
129 # create output directory if it doesn't exist:
130 if not output_directory.is_dir():
131 output_directory.mkdir(parents=True)
133 jinja2_environment = templater.Jinja2Environment().environment
134 markdown_file_object = templater.MarkdownFile(
135 rendercv_data_model,
136 jinja2_environment,
137 )
139 markdown_file_name = f"{str(rendercv_data_model.cv.name).replace(' ', '_')}_CV.md"
140 markdown_file_path = output_directory / markdown_file_name
141 markdown_file_object.create_file(markdown_file_path)
143 return markdown_file_path
146def create_a_typst_file_and_copy_theme_files(
147 rendercv_data_model: data.RenderCVDataModel, output_directory: pathlib.Path
148) -> pathlib.Path:
149 """Render the Typst file with the given data model in the output directory and
150 copy the auxiliary theme files to the output directory.
152 Args:
153 rendercv_data_model: The data model.
154 output_directory: Path to the output directory.
156 Returns:
157 The path to the rendered Typst file.
158 """
159 file_path = create_a_typst_file(rendercv_data_model, output_directory)
160 copy_theme_files_to_output_directory(
161 rendercv_data_model.design.theme, output_directory
162 )
164 # Copy the profile picture to the output directory, if it exists:
165 if rendercv_data_model.cv.photo:
166 shutil.copyfile(
167 rendercv_data_model.cv.photo,
168 output_directory / rendercv_data_model.cv.photo.name,
169 )
171 return file_path
174class TypstCompiler:
175 """A singleton class for the Typst compiler."""
177 instance: "TypstCompiler"
178 compiler: Any
179 file_path: pathlib.Path
181 def __new__(cls, file_path: pathlib.Path):
182 if not hasattr(cls, "instance") or cls.instance.file_path != file_path:
183 try:
184 import rendercv_fonts
185 import typst
186 except Exception as e:
187 from .. import _parial_install_error_message
189 raise ImportError(_parial_install_error_message) from e
191 cls.instance = super().__new__(cls)
192 cls.instance.file_path = file_path
193 cls.instance.compiler = typst.Compiler(
194 file_path, font_paths=rendercv_fonts.paths_to_font_folders
195 )
197 return cls.instance
199 def run(
200 self,
201 output: pathlib.Path,
202 format: Literal["png", "pdf"],
203 ppi: Optional[float] = None,
204 ) -> pathlib.Path | list[pathlib.Path]:
205 return self.instance.compiler.compile(format=format, output=output, ppi=ppi)
208def render_a_pdf_from_typst(file_path: pathlib.Path) -> pathlib.Path:
209 """Run TinyTeX with the given Typst file to render the PDF.
211 Args:
212 file_path: The path to the Typst file.
214 Returns:
215 The path to the rendered PDF file.
216 """
217 typst_compiler = TypstCompiler(file_path)
219 # Before running Typst, make sure the PDF file is not open in another program,
220 # that wouldn't allow Typst to write to it. Remove the PDF file if it exists,
221 # if it's not removable, then raise an error:
222 pdf_output_path = file_path.with_suffix(".pdf")
224 if sys.platform == "win32":
225 if pdf_output_path.is_file():
226 try:
227 pdf_output_path.unlink()
228 except PermissionError as e:
229 message = (
230 f"The PDF file {pdf_output_path} is open in another program and"
231 " doesn't allow RenderCV to rewrite it. Please close the PDF file."
232 )
233 raise RuntimeError(message) from e
235 typst_compiler.run(output=pdf_output_path, format="pdf")
237 return pdf_output_path
240def render_pngs_from_typst(
241 file_path: pathlib.Path, ppi: float = 150
242) -> list[pathlib.Path]:
243 """Run Typst with the given Typst file to render the PNG files.
245 Args:
246 file_path: The path to the Typst file.
247 ppi: Pixels per inch for PNG output, defaults to 150.
249 Returns:
250 Paths to the rendered PNG files.
251 """
252 typst_compiler = TypstCompiler(file_path)
253 output_path = file_path.parent / (file_path.stem + "_{p}.png")
254 output = typst_compiler.run(format="png", ppi=ppi, output=output_path)
256 if isinstance(output, list):
257 return [
258 output_path.parent / output_path.name.format(p=i)
259 for i in range(len(output))
260 ]
262 return [output_path.parent / output_path.name.format(p=1)]
265def render_an_html_from_markdown(markdown_file_path: pathlib.Path) -> pathlib.Path:
266 """Render an HTML file from a Markdown file with the same name and in the same
267 directory. It uses `rendercv/themes/main.j2.html` as the Jinja2 template.
269 Args:
270 markdown_file_path: The path to the Markdown file.
272 Returns:
273 The path to the rendered HTML file.
274 """
275 try:
276 import markdown
277 except Exception as e:
278 from .. import _parial_install_error_message
280 raise ImportError(_parial_install_error_message) from e
282 # check if the file exists:
283 if not markdown_file_path.is_file():
284 message = f"The file {markdown_file_path} doesn't exist!"
285 raise FileNotFoundError(message)
287 # Convert the markdown file to HTML:
288 markdown_text = markdown_file_path.read_text(encoding="utf-8")
289 html_body = markdown.markdown(markdown_text)
291 # Get the title of the markdown content:
292 title = re.search(r"# (.*)\n", markdown_text)
293 title = title.group(1) if title else None
295 jinja2_environment = templater.Jinja2Environment().environment
296 html_template = jinja2_environment.get_template("main.j2.html")
297 html = html_template.render(html_body=html_body, title=title)
299 # Write html into a file:
300 html_file_path = markdown_file_path.parent / f"{markdown_file_path.stem}.html"
301 html_file_path.write_text(html, encoding="utf-8")
303 return html_file_path