simple-resume 0.1.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. simple_resume/__init__.py +132 -0
  2. simple_resume/core/__init__.py +47 -0
  3. simple_resume/core/colors.py +215 -0
  4. simple_resume/core/config.py +672 -0
  5. simple_resume/core/constants/__init__.py +207 -0
  6. simple_resume/core/constants/colors.py +98 -0
  7. simple_resume/core/constants/files.py +28 -0
  8. simple_resume/core/constants/layout.py +58 -0
  9. simple_resume/core/dependencies.py +258 -0
  10. simple_resume/core/effects.py +154 -0
  11. simple_resume/core/exceptions.py +261 -0
  12. simple_resume/core/file_operations.py +68 -0
  13. simple_resume/core/generate/__init__.py +21 -0
  14. simple_resume/core/generate/exceptions.py +69 -0
  15. simple_resume/core/generate/html.py +233 -0
  16. simple_resume/core/generate/pdf.py +659 -0
  17. simple_resume/core/generate/plan.py +131 -0
  18. simple_resume/core/hydration.py +55 -0
  19. simple_resume/core/importers/__init__.py +3 -0
  20. simple_resume/core/importers/json_resume.py +284 -0
  21. simple_resume/core/latex/__init__.py +60 -0
  22. simple_resume/core/latex/context.py +56 -0
  23. simple_resume/core/latex/conversion.py +227 -0
  24. simple_resume/core/latex/escaping.py +68 -0
  25. simple_resume/core/latex/fonts.py +93 -0
  26. simple_resume/core/latex/formatting.py +81 -0
  27. simple_resume/core/latex/sections.py +218 -0
  28. simple_resume/core/latex/types.py +84 -0
  29. simple_resume/core/markdown.py +127 -0
  30. simple_resume/core/models.py +102 -0
  31. simple_resume/core/palettes/__init__.py +38 -0
  32. simple_resume/core/palettes/common.py +73 -0
  33. simple_resume/core/palettes/data/default_palettes.json +58 -0
  34. simple_resume/core/palettes/exceptions.py +33 -0
  35. simple_resume/core/palettes/fetch_types.py +52 -0
  36. simple_resume/core/palettes/generators.py +137 -0
  37. simple_resume/core/palettes/registry.py +76 -0
  38. simple_resume/core/palettes/resolution.py +123 -0
  39. simple_resume/core/palettes/sources.py +162 -0
  40. simple_resume/core/paths.py +21 -0
  41. simple_resume/core/protocols.py +134 -0
  42. simple_resume/core/py.typed +0 -0
  43. simple_resume/core/render/__init__.py +37 -0
  44. simple_resume/core/render/manage.py +199 -0
  45. simple_resume/core/render/plan.py +405 -0
  46. simple_resume/core/result.py +226 -0
  47. simple_resume/core/resume.py +609 -0
  48. simple_resume/core/skills.py +60 -0
  49. simple_resume/core/validation.py +321 -0
  50. simple_resume/py.typed +0 -0
  51. simple_resume/shell/__init__.py +3 -0
  52. simple_resume/shell/assets/static/css/README.md +213 -0
  53. simple_resume/shell/assets/static/css/common.css +641 -0
  54. simple_resume/shell/assets/static/css/fonts.css +42 -0
  55. simple_resume/shell/assets/static/css/preview.css +82 -0
  56. simple_resume/shell/assets/static/css/print.css +99 -0
  57. simple_resume/shell/assets/static/fonts/AvenirLTStd-Book.otf +0 -0
  58. simple_resume/shell/assets/static/fonts/AvenirLTStd-Light.otf +0 -0
  59. simple_resume/shell/assets/static/fonts/AvenirLTStd-Medium.otf +0 -0
  60. simple_resume/shell/assets/static/fonts/AvenirLTStd-Oblique.otf +0 -0
  61. simple_resume/shell/assets/static/fonts/AvenirLTStd-Roman.otf +0 -0
  62. simple_resume/shell/assets/static/fonts/fontawesome/Font Awesome 6 Brands-Regular-400.otf +0 -0
  63. simple_resume/shell/assets/static/fonts/fontawesome/Font Awesome 6 Free-Solid-900.otf +0 -0
  64. simple_resume/shell/assets/static/images/default_profile_1.jpg +0 -0
  65. simple_resume/shell/assets/static/images/default_profile_2.png +0 -0
  66. simple_resume/shell/assets/static/schema.json +236 -0
  67. simple_resume/shell/assets/static/themes/README.md +208 -0
  68. simple_resume/shell/assets/static/themes/bold.yaml +64 -0
  69. simple_resume/shell/assets/static/themes/classic.yaml +64 -0
  70. simple_resume/shell/assets/static/themes/executive.yaml +64 -0
  71. simple_resume/shell/assets/static/themes/minimal.yaml +64 -0
  72. simple_resume/shell/assets/static/themes/modern.yaml +64 -0
  73. simple_resume/shell/assets/templates/html/cover.html +129 -0
  74. simple_resume/shell/assets/templates/html/demo.html +13 -0
  75. simple_resume/shell/assets/templates/html/resume_base.html +453 -0
  76. simple_resume/shell/assets/templates/html/resume_no_bars.html +316 -0
  77. simple_resume/shell/assets/templates/html/resume_with_bars.html +362 -0
  78. simple_resume/shell/cli/__init__.py +35 -0
  79. simple_resume/shell/cli/main.py +975 -0
  80. simple_resume/shell/cli/palette.py +75 -0
  81. simple_resume/shell/cli/random_palette_demo.py +407 -0
  82. simple_resume/shell/config.py +96 -0
  83. simple_resume/shell/effect_executor.py +211 -0
  84. simple_resume/shell/file_opener.py +308 -0
  85. simple_resume/shell/generate/__init__.py +37 -0
  86. simple_resume/shell/generate/core.py +650 -0
  87. simple_resume/shell/generate/lazy.py +284 -0
  88. simple_resume/shell/io_utils.py +199 -0
  89. simple_resume/shell/palettes/__init__.py +1 -0
  90. simple_resume/shell/palettes/fetch.py +63 -0
  91. simple_resume/shell/palettes/loader.py +321 -0
  92. simple_resume/shell/palettes/remote.py +179 -0
  93. simple_resume/shell/pdf_executor.py +52 -0
  94. simple_resume/shell/py.typed +0 -0
  95. simple_resume/shell/render/__init__.py +1 -0
  96. simple_resume/shell/render/latex.py +308 -0
  97. simple_resume/shell/render/operations.py +240 -0
  98. simple_resume/shell/resume_extensions.py +737 -0
  99. simple_resume/shell/runtime/__init__.py +7 -0
  100. simple_resume/shell/runtime/content.py +190 -0
  101. simple_resume/shell/runtime/generate.py +497 -0
  102. simple_resume/shell/runtime/lazy.py +138 -0
  103. simple_resume/shell/runtime/lazy_import.py +173 -0
  104. simple_resume/shell/service_locator.py +80 -0
  105. simple_resume/shell/services.py +256 -0
  106. simple_resume/shell/session/__init__.py +6 -0
  107. simple_resume/shell/session/config.py +35 -0
  108. simple_resume/shell/session/manage.py +386 -0
  109. simple_resume/shell/strategies.py +181 -0
  110. simple_resume/shell/themes/__init__.py +35 -0
  111. simple_resume/shell/themes/loader.py +230 -0
  112. simple_resume-0.1.9.dist-info/METADATA +201 -0
  113. simple_resume-0.1.9.dist-info/RECORD +116 -0
  114. simple_resume-0.1.9.dist-info/WHEEL +4 -0
  115. simple_resume-0.1.9.dist-info/entry_points.txt +5 -0
  116. simple_resume-0.1.9.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,321 @@
1
+ """Provide validation functions for resume inputs and files."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from simple_resume.core.constants import (
8
+ MAX_FILE_SIZE_MB,
9
+ SUPPORTED_FORMATS,
10
+ OutputFormat,
11
+ )
12
+ from simple_resume.core.constants.files import SUPPORTED_YAML_EXTENSIONS
13
+ from simple_resume.core.exceptions import (
14
+ ConfigurationError,
15
+ FileSystemError,
16
+ ValidationError,
17
+ )
18
+
19
+ EMAIL_REGEX = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
20
+ DATE_REGEX = re.compile(r"^\d{4}(-\d{2})?$")
21
+
22
+
23
+ def validate_format(
24
+ format_str: str | OutputFormat, param_name: str = "format"
25
+ ) -> OutputFormat:
26
+ """Validate and normalize a format string.
27
+
28
+ Args:
29
+ format_str: The format string or enum to validate.
30
+ param_name: The parameter name for error messages.
31
+
32
+ Returns:
33
+ An `OutputFormat` enum value.
34
+
35
+ Raises:
36
+ ValidationError: If the format is not supported.
37
+
38
+ """
39
+ if not format_str:
40
+ raise ValidationError(f"{param_name} cannot be empty")
41
+
42
+ normalized = (
43
+ format_str.value
44
+ if isinstance(format_str, OutputFormat)
45
+ else format_str.lower().strip()
46
+ )
47
+
48
+ if not OutputFormat.is_valid(normalized):
49
+ raise ValidationError(
50
+ f"Unsupported {param_name}: '{format_str}'. "
51
+ f"Supported formats: {', '.join(SUPPORTED_FORMATS)}"
52
+ )
53
+
54
+ return OutputFormat(normalized)
55
+
56
+
57
+ def validate_file_path(
58
+ file_path: str | Path,
59
+ *,
60
+ must_exist: bool = True,
61
+ must_be_file: bool = True,
62
+ allowed_extensions: tuple[str, ...] | None = None,
63
+ ) -> Path:
64
+ """Validate a file path.
65
+
66
+ Args:
67
+ file_path: The path to validate.
68
+ must_exist: If `True`, the path must exist.
69
+ must_be_file: If `True`, the path must be a file.
70
+ allowed_extensions: If provided, the file must have one of these extensions.
71
+
72
+ Returns:
73
+ A validated `Path` object.
74
+
75
+ Raises:
76
+ FileSystemError: If path validation fails.
77
+
78
+ """
79
+ if not file_path:
80
+ raise FileSystemError("File path cannot be empty")
81
+
82
+ path = Path(file_path) if isinstance(file_path, str) else file_path
83
+
84
+ # Resolve to absolute path if not already absolute.
85
+ if not path.is_absolute():
86
+ path = path.resolve()
87
+
88
+ if must_exist and not path.exists():
89
+ raise FileSystemError(f"Path does not exist: {path}")
90
+
91
+ if must_be_file and must_exist and not path.is_file():
92
+ raise FileSystemError(f"Path is not a file: {path}")
93
+
94
+ if allowed_extensions and path.suffix.lower() not in allowed_extensions:
95
+ raise FileSystemError(
96
+ f"Invalid file extension '{path.suffix}'. "
97
+ f"Allowed: {', '.join(allowed_extensions)}"
98
+ )
99
+
100
+ # Check file size if file exists.
101
+ if must_exist and path.is_file():
102
+ size_mb = path.stat().st_size / (1024 * 1024)
103
+ if size_mb > MAX_FILE_SIZE_MB:
104
+ raise FileSystemError(
105
+ f"File too large: {size_mb:.1f}MB (max: {MAX_FILE_SIZE_MB}MB)"
106
+ )
107
+
108
+ return path
109
+
110
+
111
+ def validate_directory_path(
112
+ dir_path: str | Path, *, must_exist: bool = False, create_if_missing: bool = False
113
+ ) -> Path:
114
+ """Validate a directory path.
115
+
116
+ Args:
117
+ dir_path: The directory path to validate.
118
+ must_exist: If `True`, the directory must exist.
119
+ create_if_missing: If `True`, create the directory if it doesn't exist.
120
+
121
+ Returns:
122
+ A validated `Path` object.
123
+
124
+ Raises:
125
+ FileSystemError: If path validation fails.
126
+
127
+ """
128
+ if not dir_path:
129
+ raise FileSystemError("Directory path cannot be empty")
130
+
131
+ path = Path(dir_path) if isinstance(dir_path, str) else dir_path
132
+
133
+ if not path.is_absolute():
134
+ path = path.resolve()
135
+
136
+ if must_exist and not path.exists():
137
+ raise FileSystemError(f"Directory does not exist: {path}")
138
+
139
+ if path.exists() and not path.is_dir():
140
+ raise FileSystemError(f"Path is not a directory: {path}")
141
+
142
+ if create_if_missing and not path.exists():
143
+ # NOTE: Directory creation has been moved to shell layer.
144
+ # Core validation should not perform I/O operations.
145
+ # Callers should create directories in the shell layer if needed.
146
+ raise FileSystemError(
147
+ f"Directory does not exist and create_if_missing is not supported "
148
+ f"in core validation: {path}. Create the directory in the shell layer."
149
+ )
150
+
151
+ return path
152
+
153
+
154
+ def validate_template_name(template: str) -> str:
155
+ """Validate a template name.
156
+
157
+ Args:
158
+ template: The template name to validate.
159
+
160
+ Returns:
161
+ The validated template name.
162
+
163
+ Raises:
164
+ ConfigurationError: If the template name is invalid.
165
+
166
+ """
167
+ if not template:
168
+ raise ConfigurationError("Template name cannot be empty")
169
+
170
+ template = template.strip()
171
+
172
+ # Allow custom templates; ensure reasonable string format.
173
+ if not template.replace("_", "").replace("-", "").isalnum():
174
+ message = (
175
+ f"Invalid template name: '{template}'. "
176
+ "Template names should contain only alphanumeric characters, "
177
+ "hyphens, and underscores."
178
+ )
179
+ raise ConfigurationError(message)
180
+
181
+ return template
182
+
183
+
184
+ def validate_yaml_file(file_path: str | Path) -> Path:
185
+ """Validate a YAML resume file.
186
+
187
+ Args:
188
+ file_path: The path to the YAML file.
189
+
190
+ Returns:
191
+ A validated `Path` object.
192
+
193
+ Raises:
194
+ FileSystemError: If file validation fails.
195
+
196
+ """
197
+ return validate_file_path(
198
+ file_path,
199
+ must_exist=True,
200
+ must_be_file=True,
201
+ allowed_extensions=tuple(SUPPORTED_YAML_EXTENSIONS),
202
+ )
203
+
204
+
205
+ def validate_resume_data(data: dict[str, Any]) -> None:
206
+ """Validate the basic structure of the resume data.
207
+
208
+ Args:
209
+ data: A resume data dictionary.
210
+
211
+ Raises:
212
+ ValidationError: If the data structure is invalid.
213
+
214
+ """
215
+ if not isinstance(data, dict):
216
+ raise ValidationError("Resume data must be a dictionary")
217
+
218
+ if not data:
219
+ raise ValidationError("Resume data cannot be empty")
220
+
221
+ # Check required fields.
222
+ if "full_name" not in data:
223
+ raise ValidationError("Resume data must include 'full_name'")
224
+
225
+ if not data.get("full_name"):
226
+ raise ValidationError("'full_name' cannot be empty")
227
+
228
+ _validate_required_email(data)
229
+
230
+ # Check config if present.
231
+ if "config" in data:
232
+ if not isinstance(data["config"], dict):
233
+ raise ValidationError("'config' must be a dictionary")
234
+
235
+ _validate_date_fields(data)
236
+
237
+
238
+ def validate_output_path(output_path: str | Path, format_type: str) -> Path:
239
+ """Validate the output file path for a generated resume.
240
+
241
+ Args:
242
+ output_path: The output file path.
243
+ format_type: The output format (e.g., "pdf", "html").
244
+
245
+ Returns:
246
+ A validated `Path` object.
247
+
248
+ Raises:
249
+ FileSystemError: If path validation fails.
250
+
251
+ """
252
+ path = Path(output_path) if isinstance(output_path, str) else output_path
253
+
254
+ # Validate parent directory.
255
+ if path.parent and path.parent != Path("."):
256
+ validate_directory_path(path.parent, must_exist=False, create_if_missing=False)
257
+
258
+ # Check file extension matches format.
259
+ expected_ext = f".{format_type.lower()}"
260
+ if path.suffix.lower() != expected_ext:
261
+ message = (
262
+ f"Output path extension '{path.suffix}' doesn't match format "
263
+ f"'{format_type}'. Expected: {expected_ext}"
264
+ )
265
+ raise FileSystemError(message)
266
+
267
+ return path
268
+
269
+
270
+ def _validate_required_email(data: dict[str, Any]) -> None:
271
+ """Validate the required email field."""
272
+ email = data.get("email")
273
+ if email is None:
274
+ raise ValidationError("Resume data must include 'email'")
275
+
276
+ if not isinstance(email, str) or not EMAIL_REGEX.match(email.strip()):
277
+ raise ValidationError(
278
+ "Invalid email format. Expected something like user@example.com"
279
+ )
280
+
281
+
282
+ def _is_date_key(key: str) -> bool:
283
+ """Check if a key is a date field."""
284
+ key_lower = key.lower()
285
+ return key_lower == "date" or key_lower.endswith("_date")
286
+
287
+
288
+ def _validate_date_value(field: str, value: Any) -> None:
289
+ """Validate a date field's value.
290
+
291
+ Args:
292
+ field: The name of the field being validated.
293
+ value: The value of the date field.
294
+
295
+ Raises:
296
+ ValidationError: If the date format is invalid.
297
+
298
+ """
299
+ if value is None or value == "":
300
+ return
301
+ if not isinstance(value, str) or not DATE_REGEX.match(value.strip()):
302
+ raise ValidationError(
303
+ f"Invalid date format for '{field}'. Use 'YYYY' or 'YYYY-MM'."
304
+ )
305
+
306
+
307
+ def _validate_date_fields(node: Any) -> None:
308
+ """Recursively validate date fields within a dictionary or list.
309
+
310
+ Args:
311
+ node: The dictionary or list node to traverse and validate.
312
+
313
+ """
314
+ if isinstance(node, dict):
315
+ for key, value in node.items():
316
+ if isinstance(key, str) and _is_date_key(key):
317
+ _validate_date_value(key, value)
318
+ _validate_date_fields(value)
319
+ elif isinstance(node, list):
320
+ for item in node:
321
+ _validate_date_fields(item)
simple_resume/py.typed ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ """Shell layer for resume generation - orchestrates I/O and external dependencies."""
2
+
3
+ __all__: list[str] = []
@@ -0,0 +1,213 @@
1
+ # CSS Architecture for Simple Resume
2
+
3
+ This directory contains the external CSS files for the resume templates.
4
+
5
+ ## File Structure
6
+
7
+ ```text
8
+ static/css/
9
+ ├── README.md # This file
10
+ ├── fonts.css # @font-face declarations for Avenir fonts
11
+ ├── common.css # Shared styles for PDF and web preview
12
+ ├── print.css # PDF-specific styles and WeasyPrint workarounds
13
+ └── preview.css # Web browser preview enhancements
14
+ ```
15
+
16
+ ## CSS Files
17
+
18
+ ### fonts.css
19
+
20
+ Font-face declarations for the Avenir font family. Uses relative paths
21
+ to load fonts from `../fonts/`.
22
+
23
+ ### common.css
24
+
25
+ All shared layout and component styles. Uses CSS custom properties
26
+ (variables) for theme customization. These variables must be injected
27
+ via an inline `<style>` block in the template.
28
+
29
+ ### print.css
30
+
31
+ PDF/print-specific styles including `@page` rules and WeasyPrint
32
+ workarounds. Applied only for PDF generation.
33
+
34
+ ### preview.css
35
+
36
+ Web browser enhancements for the preview mode. Adds shadows, smooth
37
+ scrolling, and responsive adjustments.
38
+
39
+ ## CSS Custom Properties
40
+
41
+ The template must inject CSS custom properties for theme values.
42
+ Required properties:
43
+
44
+ ```css
45
+ :root {
46
+ /* Colors */
47
+ --sidebar-color: #2c3e50;
48
+ --sidebar-text-color: #ffffff;
49
+ --theme-color: #3498db;
50
+ --date2-color: #7f8c8d;
51
+ --bar-background-color: #bdc3c7;
52
+ --frame-color: #f5f5f5;
53
+ --heading-icon-color: #ffffff;
54
+ --section-icon-color: #2c3e50;
55
+ --section-header-color: #2c3e50;
56
+
57
+ /* Dimensions (use mm for print compatibility) */
58
+ --page-width: 210mm;
59
+ --page-height: 297mm;
60
+ --sidebar-width: 60mm;
61
+ --body-width: 150mm;
62
+ --padding: 5mm;
63
+
64
+ /* Layout spacing */
65
+ --sidebar-padding-top: 5mm;
66
+ --sidebar-padding-bottom: 5mm;
67
+ --sidebar-padding-left: 5mm;
68
+ --sidebar-padding-right: 5mm;
69
+ --pitch-padding-top: 10mm;
70
+ --pitch-padding-bottom: 5mm;
71
+ --pitch-padding-left: 5mm;
72
+ --h2-padding-left-full: 25mm;
73
+ --h2-width: 140mm;
74
+ --h3-padding-top: 3mm;
75
+ --date-container-width: 25mm;
76
+ --description-container-padding-left: 5mm;
77
+ --skill-container-padding-top: 2mm;
78
+ --profile-image-padding-bottom: 5mm;
79
+ --profile-width: 50mm;
80
+ --frame-padding: 10mm;
81
+
82
+ /* Section icon styling */
83
+ --section-icon-circle-size: 10mm;
84
+ --section-icon-circle-x-offset: 0mm;
85
+ --section-icon-design-size: 5mm;
86
+ --section-icon-design-x-offset: 0;
87
+ --section-icon-design-y-offset: 0;
88
+ --section-heading-text-margin: 5mm;
89
+ --section-heading-marker-margin-left: -10mm;
90
+ --section-heading-marker-line-height: 15mm;
91
+ }
92
+ ```
93
+
94
+ ## Usage in Templates
95
+
96
+ ### Loading CSS Files
97
+
98
+ From templates in `templates/html/`:
99
+
100
+ ```html
101
+ <head>
102
+ <!-- Load external CSS -->
103
+ <link rel="stylesheet" href="../static/css/fonts.css">
104
+ <link rel="stylesheet" href="../static/css/common.css">
105
+
106
+ <!-- Print styles (for PDF) -->
107
+ <link rel="stylesheet" href="../static/css/print.css">
108
+
109
+ <!-- Preview styles (browser only) -->
110
+ {% if preview %}
111
+ <link rel="stylesheet" href="../static/css/preview.css">
112
+ {% endif %}
113
+
114
+ <!-- Inject CSS custom properties from YAML config -->
115
+ <style>
116
+ :root {
117
+ --sidebar-color: {{ resume_config["sidebar_color"] }};
118
+ --theme-color: {{ resume_config["theme_color"] }};
119
+ /* ... other properties ... */
120
+ }
121
+ </style>
122
+ </head>
123
+ ```
124
+
125
+ ### Path Resolution
126
+
127
+ CSS files use relative paths. From `static/css/`:
128
+
129
+ - Fonts: `../fonts/AvenirLTStd-Light.otf`
130
+ - Images: `../images/...`
131
+
132
+ Templates link to CSS from `templates/html/`:
133
+
134
+ - CSS: `../static/css/common.css`
135
+
136
+ WeasyPrint's `base_url` is set to the templates directory, so all
137
+ relative paths resolve from there.
138
+
139
+ ## Migration Status
140
+
141
+ ### Phase 1: External CSS with Fallback (Complete)
142
+
143
+ - [x] CSS files created and documented
144
+ - [x] CSS custom properties defined
145
+ - [x] Templates updated to use external CSS (`<link>` tags added)
146
+ - [x] Font paths fixed in templates (`../static/fonts/`)
147
+ - [x] All 1014 tests passing
148
+ - [x] Font embedding verified (Avenir fonts in PDFs)
149
+
150
+ ### Phase 2: Visual Regression Testing (Complete)
151
+
152
+ - [x] All 8 sample PDFs generated successfully
153
+ - [x] Fonts properly embedded in all PDFs (verified with `pdffonts`):
154
+ - Avenir-35-Light
155
+ - Avenir-45
156
+ - Avenir-55-Medium
157
+ - Avenir-65-Bold
158
+ - Avenir-35-Light-Oblique
159
+ - [x] Layout renders correctly across all samples
160
+ - [x] Dark sidebar theme verified
161
+ - [x] Multipage layouts verified
162
+ - [x] Palette variations verified
163
+
164
+ ### Phase 3: Remove Inline Styles (Complete)
165
+
166
+ - [x] Remove inline fallback styles from `resume_base.html`
167
+ - [x] Remove inline fallback font styles from `cover.html`
168
+ - [x] Final visual regression testing (all 8 PDFs generated, fonts verified)
169
+ - [x] All 1014 tests passing
170
+ - [x] Documentation updated
171
+
172
+ **Note:** `cover.html` retains cover-specific inline styles that use `resume_config`
173
+ variables (e.g., `cover_padding_top`). These are not duplicates of external CSS
174
+ but template-specific styles that require Jinja variable injection.
175
+
176
+ ## Testing
177
+
178
+ After making changes:
179
+
180
+ 1. Generate PDF: `uv run simple-resume generate --format pdf`
181
+ 2. Compare output visually with previous version
182
+ 3. Check fonts are embedded: `pdffonts output.pdf`
183
+ 4. Run test suite: `uv run pytest`
184
+
185
+ ## WeasyPrint Notes
186
+
187
+ WeasyPrint has some CSS limitations:
188
+
189
+ - `calc()` with CSS variables may not work in all contexts
190
+ - Some flexbox edge cases render differently
191
+ - Font loading requires proper path resolution via `base_url`
192
+
193
+ The styles in this directory have been tested with WeasyPrint.
194
+
195
+ ## Theme Presets
196
+
197
+ For ready-to-use theme configurations, see the **[Theme Gallery](../themes/README.md)**:
198
+
199
+ | Theme | Description |
200
+ |-------|-------------|
201
+ | `modern.yaml` | Clean, contemporary design |
202
+ | `classic.yaml` | Traditional professional look |
203
+ | `bold.yaml` | High-contrast, vibrant colors |
204
+ | `minimal.yaml` | Light and airy |
205
+ | `executive.yaml` | Sophisticated dark with gold |
206
+
207
+ Each theme provides a complete `config:` block you can copy into your resume YAML.
208
+
209
+ ## Related Documentation
210
+
211
+ - [Theme Gallery](../themes/README.md) - Pre-built themes and customization guide
212
+ - [Path Handling Guide](../../../../wiki/Path-Handling-Guide.md)
213
+ - [PDF Renderer Evaluation](../../../../wiki/PDF-Renderer-Evaluation.md)