resumeforge 0.1.0__tar.gz
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.
- resumeforge-0.1.0/LICENSE +21 -0
- resumeforge-0.1.0/PKG-INFO +330 -0
- resumeforge-0.1.0/README.md +307 -0
- resumeforge-0.1.0/pyproject.toml +32 -0
- resumeforge-0.1.0/setup.cfg +4 -0
- resumeforge-0.1.0/src/resumeforge/__init__.py +0 -0
- resumeforge-0.1.0/src/resumeforge/adapters/__init__.py +0 -0
- resumeforge-0.1.0/src/resumeforge/adapters/fpdf_adapter.py +63 -0
- resumeforge-0.1.0/src/resumeforge/adapters/heading_adapter.py +44 -0
- resumeforge-0.1.0/src/resumeforge/adapters/layout_adapter.py +33 -0
- resumeforge-0.1.0/src/resumeforge/cli.py +109 -0
- resumeforge-0.1.0/src/resumeforge/constants.py +13 -0
- resumeforge-0.1.0/src/resumeforge/engines/__init__.py +0 -0
- resumeforge-0.1.0/src/resumeforge/engines/fpdf_engine.py +152 -0
- resumeforge-0.1.0/src/resumeforge/mappers/__init__.py +0 -0
- resumeforge-0.1.0/src/resumeforge/mappers/heading_mapper.py +23 -0
- resumeforge-0.1.0/src/resumeforge/mappers/section_mapper.py +92 -0
- resumeforge-0.1.0/src/resumeforge/models.py +89 -0
- resumeforge-0.1.0/src/resumeforge/parser.py +39 -0
- resumeforge-0.1.0/src/resumeforge/renderer.py +80 -0
- resumeforge-0.1.0/src/resumeforge/transformer.py +158 -0
- resumeforge-0.1.0/src/resumeforge/validator.py +12 -0
- resumeforge-0.1.0/src/resumeforge.egg-info/PKG-INFO +330 -0
- resumeforge-0.1.0/src/resumeforge.egg-info/SOURCES.txt +31 -0
- resumeforge-0.1.0/src/resumeforge.egg-info/dependency_links.txt +1 -0
- resumeforge-0.1.0/src/resumeforge.egg-info/entry_points.txt +2 -0
- resumeforge-0.1.0/src/resumeforge.egg-info/requires.txt +7 -0
- resumeforge-0.1.0/src/resumeforge.egg-info/top_level.txt +1 -0
- resumeforge-0.1.0/tests/test_cli.py +66 -0
- resumeforge-0.1.0/tests/test_parser.py +331 -0
- resumeforge-0.1.0/tests/test_renderer.py +204 -0
- resumeforge-0.1.0/tests/test_transformer.py +435 -0
- resumeforge-0.1.0/tests/test_validator.py +55 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 John Joseph Strong
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: resumeforge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert plain-text CVs into styled A4 PDFs using RCSS
|
|
5
|
+
Author: John Strong
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://resume-forge-cli.web.app/
|
|
8
|
+
Project-URL: Repository, https://github.com/JohnStrong/ResumeForge
|
|
9
|
+
Project-URL: Issues, https://github.com/JohnStrong/ResumeForge/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: lark
|
|
17
|
+
Requires-Dist: fpdf2
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: pytest; extra == "test"
|
|
20
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
21
|
+
Requires-Dist: pypdf; extra == "test"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# ResumeForge — README
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+

|
|
28
|
+

|
|
29
|
+

|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
> 🌐 **Website:** https://resume-forge-cli.web.app/
|
|
33
|
+
|
|
34
|
+
## Table of Contents
|
|
35
|
+
- [About](#about)
|
|
36
|
+
- [Setup](#setup)
|
|
37
|
+
- [Usage](#usage)
|
|
38
|
+
- [Troubleshooting](#troubleshooting)
|
|
39
|
+
- [Testing](#testing)
|
|
40
|
+
- [RCSS DSL](#rcss-dsl)
|
|
41
|
+
- [Section identification](#section-identification)
|
|
42
|
+
- [.rcss basics (MVP)](#rcss-basics-mvp)
|
|
43
|
+
- [Example .rcss snippets](#example-rcss-snippets)
|
|
44
|
+
- [Contributing](#contributing)
|
|
45
|
+
- [Examples](#examples)
|
|
46
|
+
|
|
47
|
+
## About
|
|
48
|
+
ResumeForge converts a plain UTF-8 text CV into a styled multi-page A4 PDF using a small CSS-like DSL (.rcss). Supports two layout modes: standard (single-column) and grid (2-column). MVP excludes font-face loading and decorative assets.
|
|
49
|
+
|
|
50
|
+
## Setup
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
python3 -m venv .venv
|
|
54
|
+
source .venv/bin/activate
|
|
55
|
+
pip install -e .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Render a plain-text CV to styled PDF
|
|
62
|
+
resumeforge render --input examples/resume.txt --style examples/valid.rcss --output resume.pdf
|
|
63
|
+
|
|
64
|
+
# Validate an RCSS style file for syntax errors
|
|
65
|
+
resumeforge validate --style examples/valid.rcss
|
|
66
|
+
|
|
67
|
+
# Print CLI version
|
|
68
|
+
resumeforge version
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Troubleshooting
|
|
72
|
+
|
|
73
|
+
**"Unexpected token ... Expected one of: HEADING, LAYOUT, @font-face, SECTION"**
|
|
74
|
+
Your `.rcss` file has an invalid selector. Only `layout { ... }`, `heading { ... }`, `@font-face { ... }`, and `section[name="..."] { ... }` are valid. Check for typos in the selector keyword.
|
|
75
|
+
|
|
76
|
+
**"Unexpected token ... Expected one of: SEMICOLON"**
|
|
77
|
+
A property declaration is missing its trailing semicolon. Every declaration must end with `;`.
|
|
78
|
+
|
|
79
|
+
**"RCSS must contain a layout { ... } rule"**
|
|
80
|
+
The transformer could not find a `layout` block in your `.rcss` file. Every stylesheet requires one.
|
|
81
|
+
|
|
82
|
+
**"RCSS must contain at least one section[name=...] rule"**
|
|
83
|
+
Your `.rcss` defines a layout but no section rules. Add at least one `section[name="..."] { ... }` block.
|
|
84
|
+
|
|
85
|
+
**"No sections found in CV text matching the stylesheet"**
|
|
86
|
+
The headings in your `.txt` file don't match any `section[name="..."]` values in the stylesheet. Headings must match exactly (case-sensitive, full line).
|
|
87
|
+
|
|
88
|
+
**"CV text is missing one or more sections defined in the stylesheet"**
|
|
89
|
+
Your `.txt` file is missing a heading that the stylesheet expects. Ensure every `section[name="..."]` in the `.rcss` has a corresponding heading line in the CV text.
|
|
90
|
+
|
|
91
|
+
**"No raw sections to apply rules to"**
|
|
92
|
+
The section mapper received no parsed sections to style. This typically means your CV text was empty or contained no lines matching any stylesheet section names.
|
|
93
|
+
|
|
94
|
+
**"No matching stylesheet rule for one or more sections"**
|
|
95
|
+
A section was parsed from the CV text but has no corresponding `section[name="..."]` rule in the stylesheet. Ensure every heading in your `.txt` file has a matching rule in the `.rcss`.
|
|
96
|
+
|
|
97
|
+
**"column-widths must sum to 100%"**
|
|
98
|
+
The percentage values in `column-widths` do not add up to 100. For example, `column-widths: 30% 60%;` totals 90%. Adjust so they equal 100%.
|
|
99
|
+
|
|
100
|
+
**"column-widths values must be whole numbers with %"**
|
|
101
|
+
Each value in `column-widths` must be an integer followed by `%`. Decimal values like `33.3%` and bare numbers like `35` are not allowed.
|
|
102
|
+
|
|
103
|
+
**"layout property '...' is not valid"**
|
|
104
|
+
The layout adapter encountered an unrecognised property in `layout { ... }`. Check for typos. Valid properties: `mode`, `columns`, `column-widths`, `column-gap`, `margins`, `font-family`.
|
|
105
|
+
|
|
106
|
+
**"CV text must have heading content (name/contact) before the first section"**
|
|
107
|
+
Your `.txt` file begins immediately with a section heading (e.g. `Skills` or `Experience`) with no name or contact information above it. Every CV must have at least one line of text before the first section — typically your full name, job title, and contact details (email, phone, LinkedIn). This heading block is rendered at the top of the PDF before any sections.
|
|
108
|
+
|
|
109
|
+
## Testing
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pip install pytest
|
|
113
|
+
pytest
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## RCSS DSL
|
|
117
|
+
|
|
118
|
+
> Grammar definition: [`src/resumeforge/grammar/rcss.lark`](src/resumeforge/grammar/rcss.lark)
|
|
119
|
+
|
|
120
|
+
### Section identification
|
|
121
|
+
- A section begins at a heading line that matches the pattern ^{HEADING} (a full-line header like: LINKS, WORK EXPERIENCE, EDUCATION).
|
|
122
|
+
- A section contains all text from that heading line up to the next heading line or EOF.
|
|
123
|
+
- Section selectors in .rcss match the heading text exactly (e.g., section[name="WORK EXPERIENCE"]).
|
|
124
|
+
|
|
125
|
+
### .rcss basics (MVP)
|
|
126
|
+
- File extension: .rcss
|
|
127
|
+
- Grid mode supports exactly 2 columns. grid-column must be 1 or 2 for each section in grid mode.
|
|
128
|
+
|
|
129
|
+
#### Layout properties (in `layout { ... }`)
|
|
130
|
+
| Property | Values | Description |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `mode` | `single`, `grid` | Page layout mode |
|
|
133
|
+
| `columns` | `2` | Number of columns (grid mode) |
|
|
134
|
+
| `column-widths` | e.g. `35% 65%` | Width of each column as percentages (grid mode, must sum to 100%) |
|
|
135
|
+
| `column-gap` | e.g. `6mm` | Gap between columns |
|
|
136
|
+
| `margins` | e.g. `20mm 18mm 20mm 18mm` | Page margins (top right bottom left) |
|
|
137
|
+
| `font-family` | e.g. `"Helvetica"` | Default font (overridden by @font-face) |
|
|
138
|
+
|
|
139
|
+
#### Font face properties (in `@font-face { ... }`) — optional
|
|
140
|
+
| Property | Values | Description |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `font-family` | e.g. `"Carlito"` | Font family name to register |
|
|
143
|
+
| `src` | e.g. `"fonts/Carlito-Regular.ttf"` | Path to regular weight TTF |
|
|
144
|
+
| `src-bold` | e.g. `"fonts/Carlito-Bold.ttf"` | Path to bold weight TTF |
|
|
145
|
+
|
|
146
|
+
#### Heading properties (in `heading { ... }`) — optional
|
|
147
|
+
The `heading` block styles the resume header (name, title, contact info) that appears before the first section. If omitted, ATS-friendly defaults are applied automatically.
|
|
148
|
+
|
|
149
|
+
| Property | Values | Default | Description |
|
|
150
|
+
|---|---|---|---|
|
|
151
|
+
| `font-size` | e.g. `20pt` | `20pt` | Name/first line font size. Subsequent lines (contact info, title) are scaled down proportionally |
|
|
152
|
+
| `align` | `left`, `center`, `right` | `center` | Text alignment |
|
|
153
|
+
| `line-height` | e.g. `7` | `7` | Line height in mm |
|
|
154
|
+
| `color` | e.g. `#333333` | black | Text color (hex) |
|
|
155
|
+
|
|
156
|
+
#### Section properties (in `section[name="..."] { ... }`)
|
|
157
|
+
|
|
158
|
+
**Style properties** (PDF render mode — applied as PDF state before writing):
|
|
159
|
+
| Property | Values | Default | Description |
|
|
160
|
+
|---|---|---|---|
|
|
161
|
+
| `font-size` | e.g. `12pt` | `11pt` | Text size |
|
|
162
|
+
| `color` | e.g. `#333333` | black | Text color (hex) |
|
|
163
|
+
| `background-color` | e.g. `#f0f0f0` | none | Section fill color (hex) |
|
|
164
|
+
|
|
165
|
+
**Write properties** (PDF render mode — control how content is rendered):
|
|
166
|
+
| Property | Values | Default | Description |
|
|
167
|
+
|---|---|---|---|
|
|
168
|
+
| `align` | `left`, `center`, `right` | `left` | Text alignment |
|
|
169
|
+
| `line-height` | e.g. `7` | `5` | Line height in mm |
|
|
170
|
+
| `display` | `block`, `inline` | `block` | Block wraps text (multi-line), inline flows horizontally |
|
|
171
|
+
|
|
172
|
+
**Layout positioning** (grid mode only):
|
|
173
|
+
| Property | Values | Default | Description |
|
|
174
|
+
|---|---|---|---|
|
|
175
|
+
| `grid-column` | `1`, `2` | — | Which column to place the section in |
|
|
176
|
+
| `padding` | e.g. `8mm` | `0` | Inner spacing |
|
|
177
|
+
| `width` | e.g. `1fr` | `1fr` | Proportional column width |
|
|
178
|
+
|
|
179
|
+
### Example .rcss snippets
|
|
180
|
+
Single-column (resume-single.rcss)
|
|
181
|
+
```css
|
|
182
|
+
layout { mode: single; margins: 20mm 18mm 20mm 18mm; }
|
|
183
|
+
|
|
184
|
+
section[name="HEADER"] {
|
|
185
|
+
padding: 8mm;
|
|
186
|
+
align: center;
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Two-column grid (resume-grid.rcss)
|
|
191
|
+
```css
|
|
192
|
+
layout { mode: grid; columns: 2; column-widths: 35% 65%; column-gap: 6mm; margins: 20mm 18mm 20mm 18mm; }
|
|
193
|
+
|
|
194
|
+
/* Place by heading text and explicit column (1 or 2) */
|
|
195
|
+
section[name="SIDEBAR"] {
|
|
196
|
+
grid-column: 1;
|
|
197
|
+
padding: 6mm;
|
|
198
|
+
width: 1fr;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
section[name="MAIN"] {
|
|
202
|
+
grid-column: 2;
|
|
203
|
+
padding: 6mm;
|
|
204
|
+
width: 1fr;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
section[name="HEADER"] {
|
|
208
|
+
grid-column: 1;
|
|
209
|
+
padding: 8mm;
|
|
210
|
+
align: center;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Contributing
|
|
215
|
+
|
|
216
|
+
1. Create a feature branch from `main`: `git checkout -b feature/your-feature`
|
|
217
|
+
2. Make changes, commit using [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`)
|
|
218
|
+
3. Ensure all tests pass: `pytest`
|
|
219
|
+
4. Open a pull request for code review before merging
|
|
220
|
+
5. Merge to `main` with `--no-ff` to preserve branch history
|
|
221
|
+
|
|
222
|
+
## Examples
|
|
223
|
+
|
|
224
|
+
### Step 1: Write your CV as plain text (`examples/resume.txt`)
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
Lorem Ipsum
|
|
228
|
+
Senior Software Engineer
|
|
229
|
+
lorem.ipsum@fakeemail.xyz | +44 0000 000000
|
|
230
|
+
|
|
231
|
+
Links
|
|
232
|
+
github.com/loremipsum
|
|
233
|
+
linkedin.com/in/loremipsum
|
|
234
|
+
loremipsum.dev
|
|
235
|
+
|
|
236
|
+
Skills
|
|
237
|
+
- Python,
|
|
238
|
+
- TypeScript
|
|
239
|
+
- Go
|
|
240
|
+
- Rust
|
|
241
|
+
- AWS (Lambda, DynamoDB, ECS, CDK)
|
|
242
|
+
- Terraform
|
|
243
|
+
- PostgreSQL,
|
|
244
|
+
- Redis,
|
|
245
|
+
- Kafka
|
|
246
|
+
- System Design
|
|
247
|
+
- CI/CD
|
|
248
|
+
- Kubernetes
|
|
249
|
+
- Observability
|
|
250
|
+
|
|
251
|
+
Work Experience
|
|
252
|
+
Senior Software Engineer - Acme Widget Corp
|
|
253
|
+
Jan 2021 - Present
|
|
254
|
+
- Architected event-driven microservices processing 2M+ events/day
|
|
255
|
+
- Led monolith-to-ECS migration reducing deploy times by 70%
|
|
256
|
+
- Designed real-time analytics pipeline using Kafka and Flink
|
|
257
|
+
- Mentored 4 junior engineers through pairing and code review
|
|
258
|
+
|
|
259
|
+
Software Engineer - Placeholder Technologies Inc
|
|
260
|
+
Mar 2018 - Dec 2020
|
|
261
|
+
- Built REST and gRPC APIs serving 500K daily active users
|
|
262
|
+
- Implemented canary deployments reducing rollback incidents by 85%
|
|
263
|
+
- Developed internal CLI tooling adopted by 3 engineering teams
|
|
264
|
+
|
|
265
|
+
Software Engineer - Foobar Systems Ltd
|
|
266
|
+
Sep 2015 - Feb 2018
|
|
267
|
+
- Developed customer-facing dashboard using React and TypeScript
|
|
268
|
+
- Designed multi-tenant SaaS schema in PostgreSQL
|
|
269
|
+
- Reduced API response times by 40% through caching
|
|
270
|
+
|
|
271
|
+
Education
|
|
272
|
+
MSc Computer Science - University of Nowhere, 2015
|
|
273
|
+
BSc Mathematics - University of Somewhere, 2013
|
|
274
|
+
|
|
275
|
+
References
|
|
276
|
+
Dolor Sit Amet
|
|
277
|
+
Engineering Director, Acme Widget Corp
|
|
278
|
+
dolor.sit@fakecorp.xyz
|
|
279
|
+
|
|
280
|
+
Consectetur Adipiscing
|
|
281
|
+
CTO, Placeholder Technologies Inc
|
|
282
|
+
consectetur@faketech.xyz
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Step 2: Style it with RCSS (`examples/valid.rcss`)
|
|
286
|
+
|
|
287
|
+
```css
|
|
288
|
+
@font-face { font-family: "Carlito"; src: "examples/fonts/Carlito-Regular.ttf"; src-bold: "examples/fonts/Carlito-Bold.ttf"; }
|
|
289
|
+
|
|
290
|
+
layout { mode: grid; columns: 2; column-widths: 30% 70%; column-gap: 6mm; margins: 20mm 18mm 20mm 18mm; font-family: "Carlito"; }
|
|
291
|
+
|
|
292
|
+
heading { font-size: 20pt; align: center; line-height: 7; color: #555555; }
|
|
293
|
+
|
|
294
|
+
section[name="Links"] {
|
|
295
|
+
color: #336699;
|
|
296
|
+
line-height: 5;
|
|
297
|
+
grid-column: 1;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
section[name="Skills"] {
|
|
301
|
+
line-height: 5;
|
|
302
|
+
grid-column: 1;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
section[name="Work Experience"] {
|
|
306
|
+
line-height: 5;
|
|
307
|
+
grid-column: 2;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
section[name="Education"] {
|
|
311
|
+
line-height: 5;
|
|
312
|
+
grid-column: 2;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
section[name="References"] {
|
|
316
|
+
color: #555555;
|
|
317
|
+
line-height: 5;
|
|
318
|
+
grid-column: 2;
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Step 3: Render to PDF
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
resumeforge render --input examples/resume.txt --style examples/valid.rcss --output examples/resume.pdf
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Result
|
|
329
|
+
|
|
330
|
+

|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# ResumeForge — README
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
> 🌐 **Website:** https://resume-forge-cli.web.app/
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
- [About](#about)
|
|
13
|
+
- [Setup](#setup)
|
|
14
|
+
- [Usage](#usage)
|
|
15
|
+
- [Troubleshooting](#troubleshooting)
|
|
16
|
+
- [Testing](#testing)
|
|
17
|
+
- [RCSS DSL](#rcss-dsl)
|
|
18
|
+
- [Section identification](#section-identification)
|
|
19
|
+
- [.rcss basics (MVP)](#rcss-basics-mvp)
|
|
20
|
+
- [Example .rcss snippets](#example-rcss-snippets)
|
|
21
|
+
- [Contributing](#contributing)
|
|
22
|
+
- [Examples](#examples)
|
|
23
|
+
|
|
24
|
+
## About
|
|
25
|
+
ResumeForge converts a plain UTF-8 text CV into a styled multi-page A4 PDF using a small CSS-like DSL (.rcss). Supports two layout modes: standard (single-column) and grid (2-column). MVP excludes font-face loading and decorative assets.
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python3 -m venv .venv
|
|
31
|
+
source .venv/bin/activate
|
|
32
|
+
pip install -e .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Render a plain-text CV to styled PDF
|
|
39
|
+
resumeforge render --input examples/resume.txt --style examples/valid.rcss --output resume.pdf
|
|
40
|
+
|
|
41
|
+
# Validate an RCSS style file for syntax errors
|
|
42
|
+
resumeforge validate --style examples/valid.rcss
|
|
43
|
+
|
|
44
|
+
# Print CLI version
|
|
45
|
+
resumeforge version
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Troubleshooting
|
|
49
|
+
|
|
50
|
+
**"Unexpected token ... Expected one of: HEADING, LAYOUT, @font-face, SECTION"**
|
|
51
|
+
Your `.rcss` file has an invalid selector. Only `layout { ... }`, `heading { ... }`, `@font-face { ... }`, and `section[name="..."] { ... }` are valid. Check for typos in the selector keyword.
|
|
52
|
+
|
|
53
|
+
**"Unexpected token ... Expected one of: SEMICOLON"**
|
|
54
|
+
A property declaration is missing its trailing semicolon. Every declaration must end with `;`.
|
|
55
|
+
|
|
56
|
+
**"RCSS must contain a layout { ... } rule"**
|
|
57
|
+
The transformer could not find a `layout` block in your `.rcss` file. Every stylesheet requires one.
|
|
58
|
+
|
|
59
|
+
**"RCSS must contain at least one section[name=...] rule"**
|
|
60
|
+
Your `.rcss` defines a layout but no section rules. Add at least one `section[name="..."] { ... }` block.
|
|
61
|
+
|
|
62
|
+
**"No sections found in CV text matching the stylesheet"**
|
|
63
|
+
The headings in your `.txt` file don't match any `section[name="..."]` values in the stylesheet. Headings must match exactly (case-sensitive, full line).
|
|
64
|
+
|
|
65
|
+
**"CV text is missing one or more sections defined in the stylesheet"**
|
|
66
|
+
Your `.txt` file is missing a heading that the stylesheet expects. Ensure every `section[name="..."]` in the `.rcss` has a corresponding heading line in the CV text.
|
|
67
|
+
|
|
68
|
+
**"No raw sections to apply rules to"**
|
|
69
|
+
The section mapper received no parsed sections to style. This typically means your CV text was empty or contained no lines matching any stylesheet section names.
|
|
70
|
+
|
|
71
|
+
**"No matching stylesheet rule for one or more sections"**
|
|
72
|
+
A section was parsed from the CV text but has no corresponding `section[name="..."]` rule in the stylesheet. Ensure every heading in your `.txt` file has a matching rule in the `.rcss`.
|
|
73
|
+
|
|
74
|
+
**"column-widths must sum to 100%"**
|
|
75
|
+
The percentage values in `column-widths` do not add up to 100. For example, `column-widths: 30% 60%;` totals 90%. Adjust so they equal 100%.
|
|
76
|
+
|
|
77
|
+
**"column-widths values must be whole numbers with %"**
|
|
78
|
+
Each value in `column-widths` must be an integer followed by `%`. Decimal values like `33.3%` and bare numbers like `35` are not allowed.
|
|
79
|
+
|
|
80
|
+
**"layout property '...' is not valid"**
|
|
81
|
+
The layout adapter encountered an unrecognised property in `layout { ... }`. Check for typos. Valid properties: `mode`, `columns`, `column-widths`, `column-gap`, `margins`, `font-family`.
|
|
82
|
+
|
|
83
|
+
**"CV text must have heading content (name/contact) before the first section"**
|
|
84
|
+
Your `.txt` file begins immediately with a section heading (e.g. `Skills` or `Experience`) with no name or contact information above it. Every CV must have at least one line of text before the first section — typically your full name, job title, and contact details (email, phone, LinkedIn). This heading block is rendered at the top of the PDF before any sections.
|
|
85
|
+
|
|
86
|
+
## Testing
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install pytest
|
|
90
|
+
pytest
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## RCSS DSL
|
|
94
|
+
|
|
95
|
+
> Grammar definition: [`src/resumeforge/grammar/rcss.lark`](src/resumeforge/grammar/rcss.lark)
|
|
96
|
+
|
|
97
|
+
### Section identification
|
|
98
|
+
- A section begins at a heading line that matches the pattern ^{HEADING} (a full-line header like: LINKS, WORK EXPERIENCE, EDUCATION).
|
|
99
|
+
- A section contains all text from that heading line up to the next heading line or EOF.
|
|
100
|
+
- Section selectors in .rcss match the heading text exactly (e.g., section[name="WORK EXPERIENCE"]).
|
|
101
|
+
|
|
102
|
+
### .rcss basics (MVP)
|
|
103
|
+
- File extension: .rcss
|
|
104
|
+
- Grid mode supports exactly 2 columns. grid-column must be 1 or 2 for each section in grid mode.
|
|
105
|
+
|
|
106
|
+
#### Layout properties (in `layout { ... }`)
|
|
107
|
+
| Property | Values | Description |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `mode` | `single`, `grid` | Page layout mode |
|
|
110
|
+
| `columns` | `2` | Number of columns (grid mode) |
|
|
111
|
+
| `column-widths` | e.g. `35% 65%` | Width of each column as percentages (grid mode, must sum to 100%) |
|
|
112
|
+
| `column-gap` | e.g. `6mm` | Gap between columns |
|
|
113
|
+
| `margins` | e.g. `20mm 18mm 20mm 18mm` | Page margins (top right bottom left) |
|
|
114
|
+
| `font-family` | e.g. `"Helvetica"` | Default font (overridden by @font-face) |
|
|
115
|
+
|
|
116
|
+
#### Font face properties (in `@font-face { ... }`) — optional
|
|
117
|
+
| Property | Values | Description |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `font-family` | e.g. `"Carlito"` | Font family name to register |
|
|
120
|
+
| `src` | e.g. `"fonts/Carlito-Regular.ttf"` | Path to regular weight TTF |
|
|
121
|
+
| `src-bold` | e.g. `"fonts/Carlito-Bold.ttf"` | Path to bold weight TTF |
|
|
122
|
+
|
|
123
|
+
#### Heading properties (in `heading { ... }`) — optional
|
|
124
|
+
The `heading` block styles the resume header (name, title, contact info) that appears before the first section. If omitted, ATS-friendly defaults are applied automatically.
|
|
125
|
+
|
|
126
|
+
| Property | Values | Default | Description |
|
|
127
|
+
|---|---|---|---|
|
|
128
|
+
| `font-size` | e.g. `20pt` | `20pt` | Name/first line font size. Subsequent lines (contact info, title) are scaled down proportionally |
|
|
129
|
+
| `align` | `left`, `center`, `right` | `center` | Text alignment |
|
|
130
|
+
| `line-height` | e.g. `7` | `7` | Line height in mm |
|
|
131
|
+
| `color` | e.g. `#333333` | black | Text color (hex) |
|
|
132
|
+
|
|
133
|
+
#### Section properties (in `section[name="..."] { ... }`)
|
|
134
|
+
|
|
135
|
+
**Style properties** (PDF render mode — applied as PDF state before writing):
|
|
136
|
+
| Property | Values | Default | Description |
|
|
137
|
+
|---|---|---|---|
|
|
138
|
+
| `font-size` | e.g. `12pt` | `11pt` | Text size |
|
|
139
|
+
| `color` | e.g. `#333333` | black | Text color (hex) |
|
|
140
|
+
| `background-color` | e.g. `#f0f0f0` | none | Section fill color (hex) |
|
|
141
|
+
|
|
142
|
+
**Write properties** (PDF render mode — control how content is rendered):
|
|
143
|
+
| Property | Values | Default | Description |
|
|
144
|
+
|---|---|---|---|
|
|
145
|
+
| `align` | `left`, `center`, `right` | `left` | Text alignment |
|
|
146
|
+
| `line-height` | e.g. `7` | `5` | Line height in mm |
|
|
147
|
+
| `display` | `block`, `inline` | `block` | Block wraps text (multi-line), inline flows horizontally |
|
|
148
|
+
|
|
149
|
+
**Layout positioning** (grid mode only):
|
|
150
|
+
| Property | Values | Default | Description |
|
|
151
|
+
|---|---|---|---|
|
|
152
|
+
| `grid-column` | `1`, `2` | — | Which column to place the section in |
|
|
153
|
+
| `padding` | e.g. `8mm` | `0` | Inner spacing |
|
|
154
|
+
| `width` | e.g. `1fr` | `1fr` | Proportional column width |
|
|
155
|
+
|
|
156
|
+
### Example .rcss snippets
|
|
157
|
+
Single-column (resume-single.rcss)
|
|
158
|
+
```css
|
|
159
|
+
layout { mode: single; margins: 20mm 18mm 20mm 18mm; }
|
|
160
|
+
|
|
161
|
+
section[name="HEADER"] {
|
|
162
|
+
padding: 8mm;
|
|
163
|
+
align: center;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Two-column grid (resume-grid.rcss)
|
|
168
|
+
```css
|
|
169
|
+
layout { mode: grid; columns: 2; column-widths: 35% 65%; column-gap: 6mm; margins: 20mm 18mm 20mm 18mm; }
|
|
170
|
+
|
|
171
|
+
/* Place by heading text and explicit column (1 or 2) */
|
|
172
|
+
section[name="SIDEBAR"] {
|
|
173
|
+
grid-column: 1;
|
|
174
|
+
padding: 6mm;
|
|
175
|
+
width: 1fr;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
section[name="MAIN"] {
|
|
179
|
+
grid-column: 2;
|
|
180
|
+
padding: 6mm;
|
|
181
|
+
width: 1fr;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
section[name="HEADER"] {
|
|
185
|
+
grid-column: 1;
|
|
186
|
+
padding: 8mm;
|
|
187
|
+
align: center;
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Contributing
|
|
192
|
+
|
|
193
|
+
1. Create a feature branch from `main`: `git checkout -b feature/your-feature`
|
|
194
|
+
2. Make changes, commit using [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`)
|
|
195
|
+
3. Ensure all tests pass: `pytest`
|
|
196
|
+
4. Open a pull request for code review before merging
|
|
197
|
+
5. Merge to `main` with `--no-ff` to preserve branch history
|
|
198
|
+
|
|
199
|
+
## Examples
|
|
200
|
+
|
|
201
|
+
### Step 1: Write your CV as plain text (`examples/resume.txt`)
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
Lorem Ipsum
|
|
205
|
+
Senior Software Engineer
|
|
206
|
+
lorem.ipsum@fakeemail.xyz | +44 0000 000000
|
|
207
|
+
|
|
208
|
+
Links
|
|
209
|
+
github.com/loremipsum
|
|
210
|
+
linkedin.com/in/loremipsum
|
|
211
|
+
loremipsum.dev
|
|
212
|
+
|
|
213
|
+
Skills
|
|
214
|
+
- Python,
|
|
215
|
+
- TypeScript
|
|
216
|
+
- Go
|
|
217
|
+
- Rust
|
|
218
|
+
- AWS (Lambda, DynamoDB, ECS, CDK)
|
|
219
|
+
- Terraform
|
|
220
|
+
- PostgreSQL,
|
|
221
|
+
- Redis,
|
|
222
|
+
- Kafka
|
|
223
|
+
- System Design
|
|
224
|
+
- CI/CD
|
|
225
|
+
- Kubernetes
|
|
226
|
+
- Observability
|
|
227
|
+
|
|
228
|
+
Work Experience
|
|
229
|
+
Senior Software Engineer - Acme Widget Corp
|
|
230
|
+
Jan 2021 - Present
|
|
231
|
+
- Architected event-driven microservices processing 2M+ events/day
|
|
232
|
+
- Led monolith-to-ECS migration reducing deploy times by 70%
|
|
233
|
+
- Designed real-time analytics pipeline using Kafka and Flink
|
|
234
|
+
- Mentored 4 junior engineers through pairing and code review
|
|
235
|
+
|
|
236
|
+
Software Engineer - Placeholder Technologies Inc
|
|
237
|
+
Mar 2018 - Dec 2020
|
|
238
|
+
- Built REST and gRPC APIs serving 500K daily active users
|
|
239
|
+
- Implemented canary deployments reducing rollback incidents by 85%
|
|
240
|
+
- Developed internal CLI tooling adopted by 3 engineering teams
|
|
241
|
+
|
|
242
|
+
Software Engineer - Foobar Systems Ltd
|
|
243
|
+
Sep 2015 - Feb 2018
|
|
244
|
+
- Developed customer-facing dashboard using React and TypeScript
|
|
245
|
+
- Designed multi-tenant SaaS schema in PostgreSQL
|
|
246
|
+
- Reduced API response times by 40% through caching
|
|
247
|
+
|
|
248
|
+
Education
|
|
249
|
+
MSc Computer Science - University of Nowhere, 2015
|
|
250
|
+
BSc Mathematics - University of Somewhere, 2013
|
|
251
|
+
|
|
252
|
+
References
|
|
253
|
+
Dolor Sit Amet
|
|
254
|
+
Engineering Director, Acme Widget Corp
|
|
255
|
+
dolor.sit@fakecorp.xyz
|
|
256
|
+
|
|
257
|
+
Consectetur Adipiscing
|
|
258
|
+
CTO, Placeholder Technologies Inc
|
|
259
|
+
consectetur@faketech.xyz
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Step 2: Style it with RCSS (`examples/valid.rcss`)
|
|
263
|
+
|
|
264
|
+
```css
|
|
265
|
+
@font-face { font-family: "Carlito"; src: "examples/fonts/Carlito-Regular.ttf"; src-bold: "examples/fonts/Carlito-Bold.ttf"; }
|
|
266
|
+
|
|
267
|
+
layout { mode: grid; columns: 2; column-widths: 30% 70%; column-gap: 6mm; margins: 20mm 18mm 20mm 18mm; font-family: "Carlito"; }
|
|
268
|
+
|
|
269
|
+
heading { font-size: 20pt; align: center; line-height: 7; color: #555555; }
|
|
270
|
+
|
|
271
|
+
section[name="Links"] {
|
|
272
|
+
color: #336699;
|
|
273
|
+
line-height: 5;
|
|
274
|
+
grid-column: 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
section[name="Skills"] {
|
|
278
|
+
line-height: 5;
|
|
279
|
+
grid-column: 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
section[name="Work Experience"] {
|
|
283
|
+
line-height: 5;
|
|
284
|
+
grid-column: 2;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
section[name="Education"] {
|
|
288
|
+
line-height: 5;
|
|
289
|
+
grid-column: 2;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
section[name="References"] {
|
|
293
|
+
color: #555555;
|
|
294
|
+
line-height: 5;
|
|
295
|
+
grid-column: 2;
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Step 3: Render to PDF
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
resumeforge render --input examples/resume.txt --style examples/valid.rcss --output examples/resume.pdf
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Result
|
|
306
|
+
|
|
307
|
+

|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "resumeforge"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Convert plain-text CVs into styled A4 PDFs using RCSS"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [{name = "John Strong"}]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
dependencies = ["lark", "fpdf2"]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://resume-forge-cli.web.app/"
|
|
22
|
+
Repository = "https://github.com/JohnStrong/ResumeForge"
|
|
23
|
+
Issues = "https://github.com/JohnStrong/ResumeForge/issues"
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
resumeforge = "resumeforge.cli:main"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
test = ["pytest", "pytest-cov", "pypdf"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
testpaths = ["tests"]
|
|
File without changes
|
|
File without changes
|