Coverage for src / rendercv / schema / models / design / classic_theme.py: 100%
157 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 typing import Literal
3import pydantic
5from rendercv.schema.models.base import BaseModelWithoutExtraKeys
6from rendercv.schema.models.design.color import Color
7from rendercv.schema.models.design.font_family import FontFamily as FontFamilyType
8from rendercv.schema.models.design.typst_dimension import (
9 TypstDimension,
10 length_common_description,
11)
13# Page
15type PageSize = Literal["a4", "a5", "us-letter", "us-executive"]
18class Page(BaseModelWithoutExtraKeys):
19 size: PageSize = pydantic.Field(
20 default="us-letter",
21 description=(
22 "The page size. Use 'a4' (international standard) or 'us-letter' (US"
23 " standard). The default value is `us-letter`."
24 ),
25 )
26 top_margin: TypstDimension = pydantic.Field(
27 default="0.7in",
28 description=length_common_description + " The default value is `0.7in`.",
29 )
30 bottom_margin: TypstDimension = pydantic.Field(
31 default="0.7in",
32 description=length_common_description + " The default value is `0.7in`.",
33 )
34 left_margin: TypstDimension = pydantic.Field(
35 default="0.7in",
36 description=length_common_description + " The default value is `0.7in`.",
37 )
38 right_margin: TypstDimension = pydantic.Field(
39 default="0.7in",
40 description=length_common_description + " The default value is `0.7in`.",
41 )
42 show_footer: bool = pydantic.Field(
43 default=True,
44 description=(
45 "Show the footer at the bottom of pages. The default value is `true`."
46 ),
47 )
48 show_top_note: bool = pydantic.Field(
49 default=True,
50 description=(
51 "Show the top note at the top of the first page. The default value is"
52 " `true`."
53 ),
54 )
57# Colors
59color_common_description = (
60 "The color can be specified either with their name"
61 " (https://www.w3.org/TR/SVG11/types.html#ColorKeywords), hexadecimal value, RGB"
62 " value, or HSL value."
63)
64color_common_examples = ["Black", "7fffd4", "rgb(0,79,144)", "hsl(270, 60%, 70%)"]
67class Colors(BaseModelWithoutExtraKeys):
68 body: Color = pydantic.Field(
69 default=Color("rgb(0, 0, 0)"),
70 description=(
71 color_common_description + " The default value is `rgb(0, 0, 0)`."
72 ),
73 examples=color_common_examples,
74 )
75 name: Color = pydantic.Field(
76 default=Color("rgb(0, 79, 144)"),
77 description=color_common_description
78 + " The default value is `rgb(0, 79, 144)`.",
79 examples=color_common_examples,
80 )
81 headline: Color = pydantic.Field(
82 default=Color("rgb(0, 79, 144)"),
83 description=color_common_description
84 + " The default value is `rgb(0, 79, 144)`.",
85 examples=color_common_examples,
86 )
87 connections: Color = pydantic.Field(
88 default=Color("rgb(0, 79, 144)"),
89 description=color_common_description
90 + " The default value is `rgb(0, 79, 144)`.",
91 examples=color_common_examples,
92 )
93 section_titles: Color = pydantic.Field(
94 default=Color("rgb(0, 79, 144)"),
95 description=color_common_description
96 + " The default value is `rgb(0, 79, 144)`.",
97 examples=color_common_examples,
98 )
99 links: Color = pydantic.Field(
100 default=Color("rgb(0, 79, 144)"),
101 description=color_common_description
102 + " The default value is `rgb(0, 79, 144)`.",
103 examples=color_common_examples,
104 )
105 footer: Color = pydantic.Field(
106 default=Color("rgb(128, 128, 128)"),
107 description=color_common_description
108 + " The default value is `rgb(128, 128, 128)`.",
109 examples=color_common_examples,
110 )
111 top_note: Color = pydantic.Field(
112 default=Color("rgb(128, 128, 128)"),
113 description=color_common_description
114 + " The default value is `rgb(128, 128, 128)`.",
115 examples=color_common_examples,
116 )
119# Typography
121type BodyAlignment = Literal["left", "justified", "justified-with-no-hyphenation"]
122type Alignment = Literal["left", "center", "right"]
125class FontFamily(BaseModelWithoutExtraKeys):
126 body: FontFamilyType = pydantic.Field(
127 default="Source Sans 3",
128 description=(
129 "The font family for body text. The default value is `Source Sans 3`."
130 ),
131 )
132 name: FontFamilyType = pydantic.Field(
133 default="Source Sans 3",
134 description=(
135 "The font family for the name. The default value is `Source Sans 3`."
136 ),
137 )
138 headline: FontFamilyType = pydantic.Field(
139 default="Source Sans 3",
140 description=(
141 "The font family for the headline. The default value is `Source Sans 3`."
142 ),
143 )
144 connections: FontFamilyType = pydantic.Field(
145 default="Source Sans 3",
146 description=(
147 "The font family for connections. The default value is `Source Sans 3`."
148 ),
149 )
150 section_titles: FontFamilyType = pydantic.Field(
151 default="Source Sans 3",
152 description=(
153 "The font family for section titles. The default value is `Source Sans 3`."
154 ),
155 )
158class FontSize(BaseModelWithoutExtraKeys):
159 body: TypstDimension = pydantic.Field(
160 default="10pt",
161 description="The font size for body text. The default value is `10pt`.",
162 )
163 name: TypstDimension = pydantic.Field(
164 default="30pt",
165 description="The font size for the name. The default value is `30pt`.",
166 )
167 headline: TypstDimension = pydantic.Field(
168 default="10pt",
169 description="The font size for the headline. The default value is `10pt`.",
170 )
171 connections: TypstDimension = pydantic.Field(
172 default="10pt",
173 description="The font size for connections. The default value is `10pt`.",
174 )
175 section_titles: TypstDimension = pydantic.Field(
176 default="1.4em",
177 description="The font size for section titles. The default value is `1.4em`.",
178 )
181class SmallCaps(BaseModelWithoutExtraKeys):
182 name: bool = pydantic.Field(
183 default=False,
184 description=(
185 "Whether to use small caps for the name. The default value is `false`."
186 ),
187 )
188 headline: bool = pydantic.Field(
189 default=False,
190 description=(
191 "Whether to use small caps for the headline. The default value is `false`."
192 ),
193 )
194 connections: bool = pydantic.Field(
195 default=False,
196 description=(
197 "Whether to use small caps for connections. The default value is `false`."
198 ),
199 )
200 section_titles: bool = pydantic.Field(
201 default=False,
202 description=(
203 "Whether to use small caps for section titles. The default value is"
204 " `false`."
205 ),
206 )
209class Bold(BaseModelWithoutExtraKeys):
210 name: bool = pydantic.Field(
211 default=True,
212 description="Whether to make the name bold. The default value is `true`.",
213 )
214 headline: bool = pydantic.Field(
215 default=False,
216 description="Whether to make the headline bold. The default value is `false`.",
217 )
218 connections: bool = pydantic.Field(
219 default=False,
220 description="Whether to make connections bold. The default value is `false`.",
221 )
222 section_titles: bool = pydantic.Field(
223 default=True,
224 description="Whether to make section titles bold. The default value is `true`.",
225 )
228class Typography(BaseModelWithoutExtraKeys):
229 line_spacing: TypstDimension = pydantic.Field(
230 default="0.6em",
231 description=(
232 "Space between lines of text. Larger values create more vertical space. The"
233 " default value is `0.6em`."
234 ),
235 )
236 alignment: Literal["left", "justified", "justified-with-no-hyphenation"] = (
237 pydantic.Field(
238 default="justified",
239 description=(
240 "Text alignment. Options: 'left', 'justified' (spreads text across full"
241 " width), 'justified-with-no-hyphenation' (justified without word"
242 " breaks). The default value is `justified`."
243 ),
244 )
245 )
246 date_and_location_column_alignment: Alignment = pydantic.Field(
247 default="right",
248 description=(
249 "Alignment for dates and locations in entries. Options: 'left', 'center',"
250 " 'right'. The default value is `right`."
251 ),
252 )
253 font_family: FontFamily | FontFamilyType = pydantic.Field(
254 default_factory=FontFamily,
255 description=(
256 "The font family. You can provide a single font name as a string (applies"
257 " to all elements), or a dictionary with keys 'body', 'name', 'headline',"
258 " 'connections', and 'section_titles' to customize each element. Any system"
259 " font can be used."
260 ),
261 )
262 font_size: FontSize = pydantic.Field(
263 default_factory=FontSize,
264 description="Font sizes for different elements.",
265 )
266 small_caps: SmallCaps = pydantic.Field(
267 default_factory=SmallCaps,
268 description="Small caps styling for different elements.",
269 )
270 bold: Bold = pydantic.Field(
271 default_factory=Bold,
272 description="Bold styling for different elements.",
273 )
275 @pydantic.field_validator(
276 "font_family", mode="plain", json_schema_input_type=FontFamily | FontFamilyType
277 )
278 @classmethod
279 def validate_font_family(
280 cls, font_family: FontFamily | FontFamilyType
281 ) -> FontFamily:
282 """Convert string font to FontFamily object with uniform styling.
284 Why:
285 Users can provide simple string "Latin Modern Roman" for all text,
286 or specify per-element fonts via FontFamily dict. Validator accepts
287 both, expanding strings to full FontFamily objects.
289 Args:
290 font_family: String font name or FontFamily object.
292 Returns:
293 FontFamily object with all fields populated.
294 """
295 if isinstance(font_family, str):
296 return FontFamily(
297 body=font_family,
298 name=font_family,
299 headline=font_family,
300 connections=font_family,
301 section_titles=font_family,
302 )
304 return FontFamily.model_validate(font_family)
307# Links
310class Links(BaseModelWithoutExtraKeys):
311 underline: bool = pydantic.Field(
312 default=False,
313 description="Underline hyperlinks. The default value is `false`.",
314 )
315 show_external_link_icon: bool = pydantic.Field(
316 default=False,
317 description=(
318 "Show an external link icon next to URLs. The default value is `false`."
319 ),
320 )
323# Header
325type PhoneNumberFormatType = Literal["national", "international", "E164"]
328class Connections(BaseModelWithoutExtraKeys):
329 phone_number_format: PhoneNumberFormatType = pydantic.Field(
330 default="national",
331 description="Phone number format. The default value is `national`.",
332 )
333 hyperlink: bool = pydantic.Field(
334 default=True,
335 description=(
336 "Make contact information clickable in the PDF. The default value is"
337 " `true`."
338 ),
339 )
340 show_icons: bool = pydantic.Field(
341 default=True,
342 description=(
343 "Show icons next to contact information. The default value is `true`."
344 ),
345 )
346 display_urls_instead_of_usernames: bool = pydantic.Field(
347 default=False,
348 description=(
349 "Display full URLs instead of labels. The default value is `false`."
350 ),
351 )
352 separator: str = pydantic.Field(
353 default="",
354 description=(
355 "Character(s) to separate contact items (e.g., '|' or '•'). Leave empty for"
356 " no separator. The default value is `''`."
357 ),
358 )
359 space_between_connections: TypstDimension = pydantic.Field(
360 default="0.5cm",
361 description=(
362 "Horizontal space between contact items. "
363 + length_common_description
364 + " The default value is `0.5cm`."
365 ),
366 )
369class Header(BaseModelWithoutExtraKeys):
370 alignment: Alignment = pydantic.Field(
371 default="center",
372 description=(
373 "Header alignment. Options: 'left', 'center', 'right'. The default value is"
374 " `center`."
375 ),
376 )
377 photo_width: TypstDimension = pydantic.Field(
378 default="3.5cm",
379 description="Photo width. "
380 + length_common_description
381 + " The default value is `3.5cm`.",
382 )
383 photo_position: Literal["left", "right"] = pydantic.Field(
384 default="left",
385 description="Photo position (left or right). The default value is `left`.",
386 )
387 photo_space_left: TypstDimension = pydantic.Field(
388 default="0.4cm",
389 description=(
390 "Space to the left of the photo. "
391 + length_common_description
392 + " The default value is `0.4cm`."
393 ),
394 )
395 photo_space_right: TypstDimension = pydantic.Field(
396 default="0.4cm",
397 description=(
398 "Space to the right of the photo. "
399 + length_common_description
400 + " The default value is `0.4cm`."
401 ),
402 )
403 space_below_name: TypstDimension = pydantic.Field(
404 default="0.7cm",
405 description="Space below your name. "
406 + length_common_description
407 + " The default value is `0.7cm`.",
408 )
409 space_below_headline: TypstDimension = pydantic.Field(
410 default="0.7cm",
411 description="Space below the headline. "
412 + length_common_description
413 + " The default value is `0.7cm`.",
414 )
415 space_below_connections: TypstDimension = pydantic.Field(
416 default="0.7cm",
417 description="Space below contact information. "
418 + length_common_description
419 + " The default value is `0.7cm`.",
420 )
421 connections: Connections = pydantic.Field(
422 default_factory=Connections,
423 description="Contact information settings.",
424 )
427# Section Titles
429type SectionTitleType = Literal[
430 "with_partial_line",
431 "with_full_line",
432 "without_line",
433 "moderncv",
434 "centered_without_line",
435 "centered_with_partial_line",
436 "centered_with_centered_partial_line",
437 "centered_with_full_line",
438]
441class SectionTitles(BaseModelWithoutExtraKeys):
442 type: SectionTitleType = pydantic.Field(
443 default="with_partial_line",
444 description=(
445 "Section title visual style. Use 'with_partial_line' for a line next to the"
446 " title, 'with_full_line' for a line across the page, 'without_line' for no"
447 " line, 'moderncv' for the ModernCV style, 'centered_without_line' for a"
448 " centered title with no line, 'centered_with_partial_line' for a centered"
449 " title with baseline partial lines on both sides,"
450 " 'centered_with_centered_partial_line' for a centered title with"
451 " middle-aligned lines on both sides, or 'centered_with_full_line' for a"
452 " full-width line underneath. The default value is `with_partial_line`."
453 ),
454 )
455 line_thickness: TypstDimension = pydantic.Field(
456 default="0.5pt",
457 description=length_common_description + " The default value is `0.5pt`.",
458 )
459 space_above: TypstDimension = pydantic.Field(
460 default="0.5cm",
461 description=length_common_description + " The default value is `0.5cm`.",
462 )
463 space_below: TypstDimension = pydantic.Field(
464 default="0.3cm",
465 description=length_common_description + " The default value is `0.3cm`.",
466 )
469# Sections
472class Sections(BaseModelWithoutExtraKeys):
473 allow_page_break: bool = pydantic.Field(
474 default=True,
475 description=(
476 "Allow page breaks within sections. If false, sections that don't fit will"
477 " start on a new page. The default value is `true`."
478 ),
479 )
480 space_between_regular_entries: TypstDimension = pydantic.Field(
481 default="1.2em",
482 description=(
483 "Vertical space between entries. "
484 + length_common_description
485 + " The default value is `1.2em`."
486 ),
487 )
488 space_between_text_based_entries: TypstDimension = pydantic.Field(
489 default="0.3em",
490 description=(
491 "Vertical space between text-based entries. "
492 + length_common_description
493 + " The default value is `0.3em`."
494 ),
495 )
496 show_time_spans_in: list[str] = pydantic.Field(
497 default=["experience"],
498 description=(
499 "Section titles where time spans (e.g., '2 years 3 months') should be"
500 " displayed. The default value is `['experience']`."
501 ),
502 examples=[["Experience"], ["Experience", "Education"]],
503 )
505 @pydantic.field_validator("show_time_spans_in", mode="after")
506 @classmethod
507 def convert_section_titles_to_snake_case(cls, value: list[str]) -> list[str]:
508 return [section_title.lower().replace(" ", "_") for section_title in value]
511# Entries
513type Bullet = Literal["●", "•", "◦", "-", "◆", "★", "■", "—", "○"]
516class Summary(BaseModelWithoutExtraKeys):
517 space_above: TypstDimension = pydantic.Field(
518 default="0cm",
519 description=(
520 "Space above summary text. "
521 + length_common_description
522 + " The default value is `0cm`."
523 ),
524 )
525 space_left: TypstDimension = pydantic.Field(
526 default="0cm",
527 description=(
528 "Left margin for summary text. "
529 + length_common_description
530 + " The default value is `0cm`."
531 ),
532 )
535class Highlights(BaseModelWithoutExtraKeys):
536 bullet: Bullet = pydantic.Field(
537 default="•",
538 description="Bullet character for highlights. The default value is `•`.",
539 )
540 nested_bullet: Bullet = pydantic.Field(
541 default="•",
542 description="Bullet character for nested highlights. The default value is `•`.",
543 )
544 space_left: TypstDimension = pydantic.Field(
545 default="0.15cm",
546 description=(
547 "Left indentation. "
548 + length_common_description
549 + " The default value is `0.15cm`."
550 ),
551 )
552 space_above: TypstDimension = pydantic.Field(
553 default="0cm",
554 description=(
555 "Space above highlights. "
556 + length_common_description
557 + " The default value is `0cm`."
558 ),
559 )
560 space_between_items: TypstDimension = pydantic.Field(
561 default="0cm",
562 description=(
563 "Space between highlight items. "
564 + length_common_description
565 + " The default value is `0cm`."
566 ),
567 )
568 space_between_bullet_and_text: TypstDimension = pydantic.Field(
569 default="0.5em",
570 description=(
571 "Space between bullet and text. "
572 + length_common_description
573 + " The default value is `0.5em`."
574 ),
575 )
578class Entries(BaseModelWithoutExtraKeys):
579 date_and_location_width: TypstDimension = pydantic.Field(
580 default="4.15cm",
581 description=(
582 "Width of the date/location column. "
583 + length_common_description
584 + " The default value is `4.15cm`."
585 ),
586 )
587 side_space: TypstDimension = pydantic.Field(
588 default="0.2cm",
589 description=(
590 "Left and right margins. "
591 + length_common_description
592 + " The default value is `0.2cm`."
593 ),
594 )
595 space_between_columns: TypstDimension = pydantic.Field(
596 default="0.1cm",
597 description=(
598 "Space between main content and date/location columns. "
599 + length_common_description
600 + " The default value is `0.1cm`."
601 ),
602 )
603 allow_page_break: bool = pydantic.Field(
604 default=False,
605 description=(
606 "Allow page breaks within entries. If false, entries that don't fit will"
607 " move to a new page. The default value is `false`."
608 ),
609 )
610 short_second_row: bool = pydantic.Field(
611 default=True,
612 description=(
613 "Shorten the second row to align with the date/location column. The default"
614 " value is `true`."
615 ),
616 )
617 degree_width: TypstDimension = pydantic.Field(
618 default="1cm",
619 description=(
620 "Width of the degree column. "
621 + length_common_description
622 + " The default value is `1cm`."
623 ),
624 )
625 summary: Summary = pydantic.Field(
626 default_factory=Summary,
627 description="Summary text settings.",
628 )
629 highlights: Highlights = pydantic.Field(
630 default_factory=Highlights,
631 description="Highlights settings.",
632 )
635# Templates
638class OneLineEntryTemplate(BaseModelWithoutExtraKeys):
639 main_column: str = pydantic.Field(
640 default="**LABEL:** DETAILS",
641 description=(
642 "Template for one-line entries. Available placeholders:\n- `LABEL`: The"
643 ' label text (e.g., "Languages", "Citizenship")\n- `DETAILS`: The details'
644 ' text (e.g., "English (native), Spanish (fluent)")\n\nYou can also add'
645 " arbitrary keys to entries and use them as UPPERCASE placeholders.\n\nThe"
646 " default value is `**LABEL:** DETAILS`."
647 ),
648 )
651class EducationEntryTemplate(BaseModelWithoutExtraKeys):
652 main_column: str = pydantic.Field(
653 default="**INSTITUTION**, AREA\nSUMMARY\nHIGHLIGHTS",
654 description=(
655 "Template for education entry main column. Available placeholders:\n-"
656 " `INSTITUTION`: Institution name\n- `AREA`: Field of study/major\n-"
657 " `DEGREE`: Degree type (e.g., BS, PhD)\n- `DEGREE_WITH_AREA`: Locale-aware"
658 " phrase combining degree and area (e.g., 'BS in Computer Science')\n-"
659 " `SUMMARY`: Summary text\n-"
660 " `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location text\n- `DATE`:"
661 " Formatted date or date range\n\nYou can also add arbitrary keys to"
662 " entries and use them as UPPERCASE placeholders.\n\nThe default value is"
663 " `**INSTITUTION**, AREA\\nSUMMARY\\nHIGHLIGHTS`."
664 ),
665 )
666 degree_column: str | None = pydantic.Field(
667 default="**DEGREE**",
668 description=(
669 "Optional degree column template. If provided, displays degree in separate"
670 " column. If `null`, no degree column is shown. Available placeholders:\n-"
671 " `INSTITUTION`: Institution name\n- `AREA`: Field of study/major\n-"
672 " `DEGREE`: Degree type (e.g., BS, PhD)\n- `SUMMARY`: Summary text\n-"
673 " `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location text\n- `DATE`:"
674 " Formatted date or date range\n\nYou can also add arbitrary keys to"
675 " entries and use them as UPPERCASE placeholders.\n\nThe default value is"
676 " `**DEGREE**`."
677 ),
678 )
679 date_and_location_column: str = pydantic.Field(
680 default="LOCATION\nDATE",
681 description=(
682 "Template for education entry date/location column. Available"
683 " placeholders:\n- `INSTITUTION`: Institution name\n- `AREA`: Field of"
684 " study/major\n- `DEGREE`: Degree type (e.g., BS, PhD)\n- `SUMMARY`:"
685 " Summary text\n- `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location"
686 " text\n- `DATE`: Formatted date or date range\n\nYou can also add"
687 " arbitrary keys to entries and use them as UPPERCASE placeholders.\n\nThe"
688 " default value is `LOCATION\\nDATE`."
689 ),
690 )
693class NormalEntryTemplate(BaseModelWithoutExtraKeys):
694 main_column: str = pydantic.Field(
695 default="**NAME**\nSUMMARY\nHIGHLIGHTS",
696 description=(
697 "Template for normal entry main column. Available placeholders:\n- `NAME`:"
698 " Entry name/title\n- `SUMMARY`: Summary text\n- `HIGHLIGHTS`: Bullet"
699 " points list\n- `LOCATION`: Location text\n- `DATE`: Formatted date or"
700 " date range\n\nYou can also add arbitrary keys to entries and use them as"
701 " UPPERCASE placeholders.\n\nThe default value is"
702 " `**NAME**\\nSUMMARY\\nHIGHLIGHTS`."
703 ),
704 )
705 date_and_location_column: str = pydantic.Field(
706 default="LOCATION\nDATE",
707 description=(
708 "Template for normal entry date/location column. Available placeholders:\n-"
709 " `NAME`: Entry name/title\n- `SUMMARY`: Summary text\n- `HIGHLIGHTS`:"
710 " Bullet points list\n- `LOCATION`: Location text\n- `DATE`: Formatted date"
711 " or date range\n\nYou can also add arbitrary keys to entries and use them"
712 " as UPPERCASE placeholders.\n\nThe default value is `LOCATION\\nDATE`."
713 ),
714 )
717class ExperienceEntryTemplate(BaseModelWithoutExtraKeys):
718 main_column: str = pydantic.Field(
719 default="**COMPANY**, POSITION\nSUMMARY\nHIGHLIGHTS",
720 description=(
721 "Template for experience entry main column. Available placeholders:\n-"
722 " `COMPANY`: Company name\n- `POSITION`: Job title/position\n- `SUMMARY`:"
723 " Summary text\n- `HIGHLIGHTS`: Bullet points list\n- `LOCATION`: Location"
724 " text\n- `DATE`: Formatted date or date range\n\nYou can also add"
725 " arbitrary keys to entries and use them as UPPERCASE placeholders.\n\nThe"
726 " default value is `**COMPANY**, POSITION\\nSUMMARY\\nHIGHLIGHTS`."
727 ),
728 )
729 date_and_location_column: str = pydantic.Field(
730 default="LOCATION\nDATE",
731 description=(
732 "Template for experience entry date/location column. Available"
733 " placeholders:\n- `COMPANY`: Company name\n- `POSITION`: Job"
734 " title/position\n- `SUMMARY`: Summary text\n- `HIGHLIGHTS`: Bullet points"
735 " list\n- `LOCATION`: Location text\n- `DATE`: Formatted date or date"
736 " range\n\nYou can also add arbitrary keys to entries and use them as"
737 " UPPERCASE placeholders.\n\nThe default value is `LOCATION\\nDATE`."
738 ),
739 )
742class PublicationEntryTemplate(BaseModelWithoutExtraKeys):
743 main_column: str = pydantic.Field(
744 default="**TITLE**\nSUMMARY\nAUTHORS\nURL (JOURNAL)",
745 description=(
746 "Template for publication entry main column. Available placeholders:\n-"
747 " `TITLE`: Publication title\n- `AUTHORS`: List of authors (formatted as"
748 " comma-separated string)\n- `SUMMARY`: Summary/abstract text\n- `DOI`:"
749 " Digital Object Identifier\n- `URL`: Publication URL (if DOI not"
750 " provided)\n- `JOURNAL`: Journal/conference/venue name\n- `DATE`:"
751 " Formatted date\n\nYou can also add arbitrary keys to entries and use them"
752 " as UPPERCASE placeholders.\n\nThe default value is"
753 " `**TITLE**\\nSUMMARY\\nAUTHORS\\nURL (JOURNAL)`."
754 ),
755 )
756 date_and_location_column: str = pydantic.Field(
757 default="DATE",
758 description=(
759 "Template for publication entry date column. Available placeholders:\n-"
760 " `TITLE`: Publication title\n- `AUTHORS`: List of authors (formatted as"
761 " comma-separated string)\n- `SUMMARY`: Summary/abstract text\n- `DOI`:"
762 " Digital Object Identifier\n- `URL`: Publication URL (if DOI not"
763 " provided)\n- `JOURNAL`: Journal/conference/venue name\n- `DATE`:"
764 " Formatted date\n\nYou can also add arbitrary keys to entries and use them"
765 " as UPPERCASE placeholders.\n\nThe default value is `DATE`."
766 ),
767 )
770class Templates(BaseModelWithoutExtraKeys):
771 footer: str = pydantic.Field(
772 default="*NAME -- PAGE_NUMBER/TOTAL_PAGES*",
773 description=(
774 "Template for the footer. Available placeholders:\n"
775 "- `NAME`: The CV owner's name from `cv.name`\n"
776 "- `PAGE_NUMBER`: Current page number\n"
777 "- `TOTAL_PAGES`: Total number of pages\n"
778 "- `CURRENT_DATE`: Formatted date based on `design.templates.single_date`\n"
779 "- `MONTH_NAME`: Full month name (e.g., January)\n"
780 "- `MONTH_ABBREVIATION`: Abbreviated month name (e.g., Jan)\n"
781 "- `MONTH`: Month number (e.g., 1)\n"
782 "- `MONTH_IN_TWO_DIGITS`: Zero-padded month (e.g., 01)\n"
783 "- `DAY`: Day of the month (e.g., 5)\n"
784 "- `DAY_IN_TWO_DIGITS`: Zero-padded day (e.g., 05)\n"
785 "- `YEAR`: Full year (e.g., 2025)\n"
786 "- `YEAR_IN_TWO_DIGITS`: Two-digit year (e.g., 25)\n\n"
787 "The default value is `*NAME -- PAGE_NUMBER/TOTAL_PAGES*`."
788 ),
789 )
790 top_note: str = pydantic.Field(
791 default="*LAST_UPDATED CURRENT_DATE*",
792 description=(
793 "Template for the top note. Available placeholders:\n- `LAST_UPDATED`:"
794 ' Localized "last updated" text from `locale.last_updated`\n-'
795 " `CURRENT_DATE`: Formatted date based on `design.templates.single_date`\n-"
796 " `NAME`: The CV owner's name from `cv.name`\n- `MONTH_NAME`: Full month"
797 " name (e.g., January)\n- `MONTH_ABBREVIATION`: Abbreviated month name"
798 " (e.g., Jan)\n- `MONTH`: Month number (e.g., 1)\n- `MONTH_IN_TWO_DIGITS`:"
799 " Zero-padded month (e.g., 01)\n- `DAY`: Day of the month (e.g., 5)\n-"
800 " `DAY_IN_TWO_DIGITS`: Zero-padded day (e.g., 05)\n- `YEAR`: Full year"
801 " (e.g., 2025)\n- `YEAR_IN_TWO_DIGITS`: Two-digit year (e.g., 25)\n\n"
802 "The default value is `*LAST_UPDATED CURRENT_DATE*`."
803 ),
804 )
805 single_date: str = pydantic.Field(
806 default="MONTH_ABBREVIATION YEAR",
807 description=(
808 "Template for single dates. Available placeholders:\n"
809 "- `MONTH_NAME`: Full month name (e.g., January)\n"
810 "- `MONTH_ABBREVIATION`: Abbreviated month name (e.g., Jan)\n"
811 "- `MONTH`: Month number (e.g., 1)\n"
812 "- `MONTH_IN_TWO_DIGITS`: Zero-padded month (e.g., 01)\n"
813 "- `DAY`: Day of the month (e.g., 5)\n"
814 "- `DAY_IN_TWO_DIGITS`: Zero-padded day (e.g., 05)\n"
815 "- `YEAR`: Full year (e.g., 2025)\n"
816 "- `YEAR_IN_TWO_DIGITS`: Two-digit year (e.g., 25)\n\n"
817 "The default value is `MONTH_ABBREVIATION YEAR`."
818 ),
819 )
820 date_range: str = pydantic.Field(
821 default="START_DATE – END_DATE",
822 description=(
823 "Template for date ranges. Available placeholders:\n- `START_DATE`:"
824 " Formatted start date based on `design.templates.single_date`\n-"
825 " `END_DATE`: Formatted end date based on `design.templates.single_date`"
826 ' (or "present"/"ongoing" for current positions)\n\nThe default value is'
827 " `START_DATE – END_DATE`."
828 ),
829 )
830 time_span: str = pydantic.Field(
831 default="HOW_MANY_YEARS YEARS HOW_MANY_MONTHS MONTHS",
832 description=(
833 "Template for time spans (duration calculations). Available"
834 " placeholders:\n- `HOW_MANY_YEARS`: Number of years (e.g., 2)\n- `YEARS`:"
835 ' Localized word for "years" from `locale.years` (or singular "year")\n-'
836 " `HOW_MANY_MONTHS`: Number of months (e.g., 3)\n- `MONTHS`: Localized word"
837 ' for "months" from `locale.months` (or singular "month")\n\nThe default'
838 " value is `HOW_MANY_YEARS YEARS HOW_MANY_MONTHS MONTHS`."
839 ),
840 )
841 one_line_entry: OneLineEntryTemplate = pydantic.Field(
842 default_factory=OneLineEntryTemplate,
843 description="Template for one-line entries.",
844 )
845 education_entry: EducationEntryTemplate = pydantic.Field(
846 default_factory=EducationEntryTemplate,
847 description="Template for education entries.",
848 )
849 normal_entry: NormalEntryTemplate = pydantic.Field(
850 default_factory=NormalEntryTemplate,
851 description="Template for normal entries.",
852 )
853 experience_entry: ExperienceEntryTemplate = pydantic.Field(
854 default_factory=ExperienceEntryTemplate,
855 description="Template for experience entries.",
856 )
857 publication_entry: PublicationEntryTemplate = pydantic.Field(
858 default_factory=PublicationEntryTemplate,
859 description="Template for publication entries.",
860 )
863# ClassicTheme
866class ClassicTheme(BaseModelWithoutExtraKeys):
867 theme: Literal["classic"] = "classic"
868 page: Page = pydantic.Field(default_factory=Page)
869 colors: Colors = pydantic.Field(default_factory=Colors)
870 typography: Typography = pydantic.Field(default_factory=Typography)
871 links: Links = pydantic.Field(default_factory=Links)
872 header: Header = pydantic.Field(default_factory=Header)
873 section_titles: SectionTitles = pydantic.Field(default_factory=SectionTitles)
874 sections: Sections = pydantic.Field(default_factory=Sections)
875 entries: Entries = pydantic.Field(default_factory=Entries)
876 templates: Templates = pydantic.Field(default_factory=Templates)