Coverage for src / rendercv / renderer / templater / connections.py: 100%
81 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
1from dataclasses import dataclass
2from typing import Literal
4import phonenumbers
6from rendercv.exception import RenderCVInternalError
7from rendercv.schema.models.rendercv_model import RenderCVModel
9from .markdown_parser import markdown_to_typst
10from .string_processor import clean_url
12fontawesome_icons = {
13 "LinkedIn": "linkedin",
14 "GitHub": "github",
15 "GitLab": "gitlab",
16 "IMDB": "imdb",
17 "Instagram": "instagram",
18 "Mastodon": "mastodon",
19 "ORCID": "orcid",
20 "StackOverflow": "stack-overflow",
21 "ResearchGate": "researchgate",
22 "YouTube": "youtube",
23 "Google Scholar": "graduation-cap",
24 "Telegram": "telegram",
25 "WhatsApp": "whatsapp",
26 "Leetcode": "code",
27 "X": "x-twitter",
28 "Bluesky": "bluesky",
29 "location": "location-dot",
30 "email": "envelope",
31 "phone": "phone",
32 "website": "link",
33 "Reddit": "reddit",
34}
37def compute_connections(
38 rendercv_model: RenderCVModel, file_type: Literal["typst", "markdown"]
39) -> list[str]:
40 """Route to format-specific connection generator.
42 Args:
43 rendercv_model: CV model with contact information.
44 file_type: Target format for connections.
46 Returns:
47 List of formatted connection strings.
48 """
49 return {
50 "typst": compute_connections_for_typst,
51 "markdown": compute_connections_for_markdown,
52 }[file_type](rendercv_model)
55@dataclass
56class Connection:
57 fontawesome_icon: str
58 url: str | None
59 body: str
62def parse_connections(rendercv_model: RenderCVModel) -> list[Connection]:
63 """Extract contact information from CV model into normalized connection format.
65 Why:
66 CV header displays various contact methods in user-defined order. This
67 extracts emails, phones, websites, location, and social networks from
68 the model, preserving the order specified in the input file.
70 Args:
71 rendercv_model: CV model with contact information.
73 Returns:
74 List of connections with icon specifiers, URLs, and display text.
75 """
76 connections: list[Connection] = []
77 for key in rendercv_model.cv._key_order:
78 match key:
79 case "email":
80 emails = rendercv_model.cv.email
81 if not isinstance(emails, list):
82 emails = [emails]
84 for email in emails:
85 url = f"mailto:{email}"
86 body = str(email)
87 connections.append(
88 Connection(
89 fontawesome_icon=fontawesome_icons[key], url=url, body=body
90 )
91 )
93 case "phone":
94 phones = rendercv_model.cv.phone
95 if phones is None:
96 raise RenderCVInternalError("phone key present but value is None")
97 if not isinstance(phones, list):
98 phones = [phones]
100 for phone in phones:
101 url = str(phone)
102 body = phonenumbers.format_number(
103 phonenumbers.parse(phone, None),
104 getattr(
105 phonenumbers.PhoneNumberFormat,
106 rendercv_model.design.header.connections.phone_number_format.upper(),
107 ),
108 )
109 connections.append(
110 Connection(
111 fontawesome_icon=fontawesome_icons[key], url=url, body=body
112 )
113 )
115 case "website":
116 websites = rendercv_model.cv.website
117 if not websites:
118 raise RenderCVInternalError("website key present but value is None")
119 if not isinstance(websites, list):
120 websites = [websites]
122 for website in websites:
123 url = str(website)
124 body = clean_url(url)
125 connections.append(
126 Connection(
127 fontawesome_icon=fontawesome_icons[key], url=url, body=body
128 )
129 )
131 case "location":
132 url = None
133 body = str(rendercv_model.cv.location)
134 connections.append(
135 Connection(
136 fontawesome_icon=fontawesome_icons[key], url=None, body=body
137 )
138 )
140 case "social_networks":
141 if rendercv_model.cv.social_networks is None:
142 raise RenderCVInternalError(
143 "social_networks key present but value is None"
144 )
145 for social_network in rendercv_model.cv.social_networks:
146 url = social_network.url
147 if rendercv_model.design.header.connections.display_urls_instead_of_usernames:
148 body = clean_url(url)
149 else:
150 match social_network.network:
151 case "Google Scholar":
152 body = "Google Scholar"
153 case _:
154 body = social_network.username
155 connections.append(
156 Connection(
157 fontawesome_icon=fontawesome_icons[social_network.network],
158 url=url,
159 body=body,
160 )
161 )
163 case "custom_connections":
164 if rendercv_model.cv.custom_connections is None:
165 raise RenderCVInternalError(
166 "custom_connections key present but value is None"
167 )
168 for custom_connection in rendercv_model.cv.custom_connections:
169 url = (
170 str(custom_connection.url)
171 if custom_connection.url is not None
172 else None
173 )
174 body = custom_connection.placeholder
175 connections.append(
176 Connection(
177 fontawesome_icon=custom_connection.fontawesome_icon,
178 url=url,
179 body=body,
180 )
181 )
183 return connections
186def compute_connections_for_typst(rendercv_model: RenderCVModel) -> list[str]:
187 """Format connections with Typst markup, Font Awesome icons, and conditional hyperlinks.
189 Why:
190 Typst templates need connection strings with icon syntax and link markup.
191 Icon visibility and hyperlink behavior are user-configurable through
192 design settings, requiring conditional formatting at render time.
194 Args:
195 rendercv_model: CV model with contact information and design settings.
197 Returns:
198 List of Typst-formatted connection strings ready for template insertion.
199 """
200 connections = parse_connections(rendercv_model)
202 show_icon = rendercv_model.design.header.connections.show_icons
203 hyperlink = rendercv_model.design.header.connections.hyperlink
205 placeholders = [
206 (
207 f'#connection-with-icon("{connection.fontawesome_icon}")'
208 f"[{markdown_to_typst(connection.body)}]"
209 if show_icon
210 else markdown_to_typst(connection.body)
211 )
212 for connection in connections
213 ]
215 return [
216 (
217 f'#link("{connection.url}", icon: false, if-underline: false, if-color:'
218 f" false)[{placeholder}]"
219 if connection.url and hyperlink
220 else placeholder
221 )
222 for connection, placeholder in zip(connections, placeholders, strict=True)
223 ]
226def compute_connections_for_markdown(rendercv_model: RenderCVModel) -> list[str]:
227 """Format connections as Markdown links without icons.
229 Args:
230 rendercv_model: CV model with contact information.
232 Returns:
233 List of Markdown-formatted connection strings.
234 """
235 connections = parse_connections(rendercv_model)
237 return [
238 (
239 f"[{connection.body}]({connection.url})"
240 if connection.url
241 else connection.body
242 )
243 for connection in connections
244 ]