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

1from dataclasses import dataclass 

2from typing import Literal 

3 

4import phonenumbers 

5 

6from rendercv.exception import RenderCVInternalError 

7from rendercv.schema.models.rendercv_model import RenderCVModel 

8 

9from .markdown_parser import markdown_to_typst 

10from .string_processor import clean_url 

11 

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} 

35 

36 

37def compute_connections( 

38 rendercv_model: RenderCVModel, file_type: Literal["typst", "markdown"] 

39) -> list[str]: 

40 """Route to format-specific connection generator. 

41 

42 Args: 

43 rendercv_model: CV model with contact information. 

44 file_type: Target format for connections. 

45 

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) 

53 

54 

55@dataclass 

56class Connection: 

57 fontawesome_icon: str 

58 url: str | None 

59 body: str 

60 

61 

62def parse_connections(rendercv_model: RenderCVModel) -> list[Connection]: 

63 """Extract contact information from CV model into normalized connection format. 

64 

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. 

69 

70 Args: 

71 rendercv_model: CV model with contact information. 

72 

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] 

83 

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 ) 

92 

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] 

99 

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 ) 

114 

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] 

121 

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 ) 

130 

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 ) 

139 

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 ) 

162 

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 ) 

182 

183 return connections 

184 

185 

186def compute_connections_for_typst(rendercv_model: RenderCVModel) -> list[str]: 

187 """Format connections with Typst markup, Font Awesome icons, and conditional hyperlinks. 

188 

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. 

193 

194 Args: 

195 rendercv_model: CV model with contact information and design settings. 

196 

197 Returns: 

198 List of Typst-formatted connection strings ready for template insertion. 

199 """ 

200 connections = parse_connections(rendercv_model) 

201 

202 show_icon = rendercv_model.design.header.connections.show_icons 

203 hyperlink = rendercv_model.design.header.connections.hyperlink 

204 

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 ] 

214 

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 ] 

224 

225 

226def compute_connections_for_markdown(rendercv_model: RenderCVModel) -> list[str]: 

227 """Format connections as Markdown links without icons. 

228 

229 Args: 

230 rendercv_model: CV model with contact information. 

231 

232 Returns: 

233 List of Markdown-formatted connection strings. 

234 """ 

235 connections = parse_connections(rendercv_model) 

236 

237 return [ 

238 ( 

239 f"[{connection.body}]({connection.url})" 

240 if connection.url 

241 else connection.body 

242 ) 

243 for connection in connections 

244 ]