Coverage for src / rendercv / renderer / templater / templater.py: 100%
48 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-25 13:49 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-25 13:49 +0000
1import contextlib
2import functools
3import pathlib
4from typing import Literal
6import jinja2
8from rendercv.schema.models.rendercv_model import RenderCVModel
10from .markdown_parser import markdown_to_html
11from .model_processor import download_photo_from_url, process_model
12from .string_processor import clean_url
14templates_directory = pathlib.Path(__file__).parent / "templates"
17@functools.lru_cache(maxsize=1)
18def get_jinja2_environment(
19 input_file_path: pathlib.Path | None = None,
20) -> jinja2.Environment:
21 """Create cached Jinja2 environment with custom filters and template loaders.
23 Why:
24 Template rendering is called multiple times per render. Caching environment
25 prevents repeated filesystem scans. Loader hierarchy enables user template
26 overrides by checking input file directory before built-in templates.
28 Args:
29 input_file_path: Path to input file for user template override resolution.
31 Returns:
32 Configured Jinja2 environment with filters and loaders.
33 """
34 env = jinja2.Environment(
35 loader=jinja2.FileSystemLoader(
36 [
37 ( # To allow users to override the templates:
38 input_file_path.parent if input_file_path else pathlib.Path.cwd()
39 ),
40 templates_directory,
41 ]
42 ),
43 trim_blocks=True,
44 lstrip_blocks=True,
45 )
46 env.filters["clean_url"] = clean_url
47 env.filters["strip"] = lambda string: string.strip()
48 return env
51def render_full_template(
52 rendercv_model: RenderCVModel, file_type: Literal["typst", "markdown"]
53) -> str:
54 """Render complete CV document by assembling preamble, header, and sections.
56 Why:
57 CV generation requires consistent structure across formats. This orchestrates
58 model processing, template rendering for each component, and assembly into
59 final document following proper order.
61 Example:
62 ```py
63 typst_document = render_full_template(rendercv_model, "typst")
64 # Returns complete .typ file with preamble, header, and all sections
66 markdown_document = render_full_template(rendercv_model, "markdown")
67 # Returns complete .md file with header and all sections
68 ```
70 Args:
71 rendercv_model: CV model to render.
72 file_type: Output format for template selection and processing.
74 Returns:
75 Complete rendered document as string.
76 """
77 extension = {
78 "typst": "typ",
79 "markdown": "md",
80 }[file_type]
82 download_photo_from_url(rendercv_model)
83 rendercv_model = process_model(rendercv_model, file_type)
85 header = render_single_template(
86 file_type,
87 f"Header.j2.{extension}",
88 rendercv_model,
89 )
90 if file_type == "typst":
91 preamble = render_single_template(
92 file_type,
93 f"Preamble.j2.{extension}",
94 rendercv_model,
95 )
96 code = f"{preamble}\n\n{header}\n"
97 else:
98 code = f"{header}\n"
100 for rendercv_section in rendercv_model.cv.rendercv_sections:
101 section_beginning = render_single_template(
102 file_type,
103 f"SectionBeginning.j2.{extension}",
104 rendercv_model,
105 section_title=rendercv_section.title,
106 snake_case_section_title=rendercv_section.snake_case_title,
107 entry_type=rendercv_section.entry_type,
108 )
109 section_ending = render_single_template(
110 file_type,
111 f"SectionEnding.j2.{extension}",
112 rendercv_model,
113 entry_type=rendercv_section.entry_type,
114 )
115 entry_codes = []
116 for entry in rendercv_section.entries:
117 entry_code = render_single_template(
118 file_type,
119 f"entries/{rendercv_section.entry_type}.j2.{extension}",
120 rendercv_model,
121 entry=entry,
122 )
123 entry_codes.append(entry_code)
124 entries_code = "\n\n".join(entry_codes)
125 section_code = f"{section_beginning}\n{entries_code}\n{section_ending}"
126 code += f"\n{section_code}"
128 return code
131def render_html(rendercv_model: RenderCVModel, markdown: str) -> str:
132 """Convert Markdown to HTML and wrap with full HTML template.
134 Why:
135 HTML output requires both content conversion (Markdown to HTML body) and
136 document structure (head, CSS, metadata). Separate function handles HTML-
137 specific workflow distinct from Typst/Markdown direct generation.
139 Example:
140 ```py
141 markdown_content = render_full_template(rendercv_model, "markdown")
142 html_document = render_html(rendercv_model, markdown_content)
143 # Returns complete HTML with <head>, CSS, and converted Markdown body
144 ```
146 Args:
147 rendercv_model: CV model for template context.
148 markdown: Markdown content to convert.
150 Returns:
151 Complete HTML document.
152 """
153 html_body = markdown_to_html(markdown)
154 return render_single_template(
155 "html", "Full.html", rendercv_model, html_body=html_body
156 )
159def render_single_template(
160 file_type: Literal["markdown", "typst", "html"],
161 relative_template_path: str,
162 rendercv_model: RenderCVModel,
163 **kwargs,
164) -> str:
165 """Render single Jinja2 template with user override support. Arbitrary keyword
166 arguments may be passed to the template as additional template variables.
168 Why:
169 Users can override built-in templates by placing custom templates in
170 theme folder alongside input file. Typst templates check theme-specific
171 location first, falling back to built-in templates if not found.
173 Example:
174 ```py
175 header = render_single_template("typst", "Header.j2.typ", rendercv_model)
176 # First checks for classic/Header.j2.typ in input file directory
177 # Falls back to built-in typst/Header.j2.typ if not found
179 section = render_single_template(
180 "typst",
181 "SectionBeginning.j2.typ",
182 rendercv_model,
183 section_title="Experience",
184 )
185 ```
187 Args:
188 file_type: Format for template directory selection.
189 relative_template_path: Template file path relative to format directory.
190 rendercv_model: CV model providing template context.
192 Returns:
193 Rendered template as string.
194 """
195 jinja2_environment = get_jinja2_environment(rendercv_model._input_file_path)
196 template = None
197 if file_type == "typst":
198 # Try user's own Typst templates first:
199 with contextlib.suppress(jinja2.TemplateNotFound):
200 template = jinja2_environment.get_template(
201 f"{rendercv_model.design.theme}/{relative_template_path}"
202 )
204 if template is None:
205 template = jinja2_environment.get_template(
206 f"{file_type}/{relative_template_path}"
207 )
209 return template.render(
210 cv=rendercv_model.cv,
211 design=rendercv_model.design,
212 locale=rendercv_model.locale,
213 settings=rendercv_model.settings,
214 **kwargs,
215 )