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.
- simple_resume/__init__.py +132 -0
- simple_resume/core/__init__.py +47 -0
- simple_resume/core/colors.py +215 -0
- simple_resume/core/config.py +672 -0
- simple_resume/core/constants/__init__.py +207 -0
- simple_resume/core/constants/colors.py +98 -0
- simple_resume/core/constants/files.py +28 -0
- simple_resume/core/constants/layout.py +58 -0
- simple_resume/core/dependencies.py +258 -0
- simple_resume/core/effects.py +154 -0
- simple_resume/core/exceptions.py +261 -0
- simple_resume/core/file_operations.py +68 -0
- simple_resume/core/generate/__init__.py +21 -0
- simple_resume/core/generate/exceptions.py +69 -0
- simple_resume/core/generate/html.py +233 -0
- simple_resume/core/generate/pdf.py +659 -0
- simple_resume/core/generate/plan.py +131 -0
- simple_resume/core/hydration.py +55 -0
- simple_resume/core/importers/__init__.py +3 -0
- simple_resume/core/importers/json_resume.py +284 -0
- simple_resume/core/latex/__init__.py +60 -0
- simple_resume/core/latex/context.py +56 -0
- simple_resume/core/latex/conversion.py +227 -0
- simple_resume/core/latex/escaping.py +68 -0
- simple_resume/core/latex/fonts.py +93 -0
- simple_resume/core/latex/formatting.py +81 -0
- simple_resume/core/latex/sections.py +218 -0
- simple_resume/core/latex/types.py +84 -0
- simple_resume/core/markdown.py +127 -0
- simple_resume/core/models.py +102 -0
- simple_resume/core/palettes/__init__.py +38 -0
- simple_resume/core/palettes/common.py +73 -0
- simple_resume/core/palettes/data/default_palettes.json +58 -0
- simple_resume/core/palettes/exceptions.py +33 -0
- simple_resume/core/palettes/fetch_types.py +52 -0
- simple_resume/core/palettes/generators.py +137 -0
- simple_resume/core/palettes/registry.py +76 -0
- simple_resume/core/palettes/resolution.py +123 -0
- simple_resume/core/palettes/sources.py +162 -0
- simple_resume/core/paths.py +21 -0
- simple_resume/core/protocols.py +134 -0
- simple_resume/core/py.typed +0 -0
- simple_resume/core/render/__init__.py +37 -0
- simple_resume/core/render/manage.py +199 -0
- simple_resume/core/render/plan.py +405 -0
- simple_resume/core/result.py +226 -0
- simple_resume/core/resume.py +609 -0
- simple_resume/core/skills.py +60 -0
- simple_resume/core/validation.py +321 -0
- simple_resume/py.typed +0 -0
- simple_resume/shell/__init__.py +3 -0
- simple_resume/shell/assets/static/css/README.md +213 -0
- simple_resume/shell/assets/static/css/common.css +641 -0
- simple_resume/shell/assets/static/css/fonts.css +42 -0
- simple_resume/shell/assets/static/css/preview.css +82 -0
- simple_resume/shell/assets/static/css/print.css +99 -0
- simple_resume/shell/assets/static/fonts/AvenirLTStd-Book.otf +0 -0
- simple_resume/shell/assets/static/fonts/AvenirLTStd-Light.otf +0 -0
- simple_resume/shell/assets/static/fonts/AvenirLTStd-Medium.otf +0 -0
- simple_resume/shell/assets/static/fonts/AvenirLTStd-Oblique.otf +0 -0
- simple_resume/shell/assets/static/fonts/AvenirLTStd-Roman.otf +0 -0
- simple_resume/shell/assets/static/fonts/fontawesome/Font Awesome 6 Brands-Regular-400.otf +0 -0
- simple_resume/shell/assets/static/fonts/fontawesome/Font Awesome 6 Free-Solid-900.otf +0 -0
- simple_resume/shell/assets/static/images/default_profile_1.jpg +0 -0
- simple_resume/shell/assets/static/images/default_profile_2.png +0 -0
- simple_resume/shell/assets/static/schema.json +236 -0
- simple_resume/shell/assets/static/themes/README.md +208 -0
- simple_resume/shell/assets/static/themes/bold.yaml +64 -0
- simple_resume/shell/assets/static/themes/classic.yaml +64 -0
- simple_resume/shell/assets/static/themes/executive.yaml +64 -0
- simple_resume/shell/assets/static/themes/minimal.yaml +64 -0
- simple_resume/shell/assets/static/themes/modern.yaml +64 -0
- simple_resume/shell/assets/templates/html/cover.html +129 -0
- simple_resume/shell/assets/templates/html/demo.html +13 -0
- simple_resume/shell/assets/templates/html/resume_base.html +453 -0
- simple_resume/shell/assets/templates/html/resume_no_bars.html +316 -0
- simple_resume/shell/assets/templates/html/resume_with_bars.html +362 -0
- simple_resume/shell/cli/__init__.py +35 -0
- simple_resume/shell/cli/main.py +975 -0
- simple_resume/shell/cli/palette.py +75 -0
- simple_resume/shell/cli/random_palette_demo.py +407 -0
- simple_resume/shell/config.py +96 -0
- simple_resume/shell/effect_executor.py +211 -0
- simple_resume/shell/file_opener.py +308 -0
- simple_resume/shell/generate/__init__.py +37 -0
- simple_resume/shell/generate/core.py +650 -0
- simple_resume/shell/generate/lazy.py +284 -0
- simple_resume/shell/io_utils.py +199 -0
- simple_resume/shell/palettes/__init__.py +1 -0
- simple_resume/shell/palettes/fetch.py +63 -0
- simple_resume/shell/palettes/loader.py +321 -0
- simple_resume/shell/palettes/remote.py +179 -0
- simple_resume/shell/pdf_executor.py +52 -0
- simple_resume/shell/py.typed +0 -0
- simple_resume/shell/render/__init__.py +1 -0
- simple_resume/shell/render/latex.py +308 -0
- simple_resume/shell/render/operations.py +240 -0
- simple_resume/shell/resume_extensions.py +737 -0
- simple_resume/shell/runtime/__init__.py +7 -0
- simple_resume/shell/runtime/content.py +190 -0
- simple_resume/shell/runtime/generate.py +497 -0
- simple_resume/shell/runtime/lazy.py +138 -0
- simple_resume/shell/runtime/lazy_import.py +173 -0
- simple_resume/shell/service_locator.py +80 -0
- simple_resume/shell/services.py +256 -0
- simple_resume/shell/session/__init__.py +6 -0
- simple_resume/shell/session/config.py +35 -0
- simple_resume/shell/session/manage.py +386 -0
- simple_resume/shell/strategies.py +181 -0
- simple_resume/shell/themes/__init__.py +35 -0
- simple_resume/shell/themes/loader.py +230 -0
- simple_resume-0.1.9.dist-info/METADATA +201 -0
- simple_resume-0.1.9.dist-info/RECORD +116 -0
- simple_resume-0.1.9.dist-info/WHEEL +4 -0
- simple_resume-0.1.9.dist-info/entry_points.txt +5 -0
- 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,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)
|