prismcv 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.
- prismcv-0.1.0/.gitignore +13 -0
- prismcv-0.1.0/LICENSE +21 -0
- prismcv-0.1.0/PKG-INFO +140 -0
- prismcv-0.1.0/README.md +128 -0
- prismcv-0.1.0/README.zh-CN.md +128 -0
- prismcv-0.1.0/docs/themes.en.jpg +0 -0
- prismcv-0.1.0/docs/themes.zh.jpg +0 -0
- prismcv-0.1.0/pyproject.toml +19 -0
- prismcv-0.1.0/src/prismcv/__init__.py +3 -0
- prismcv-0.1.0/src/prismcv/build.py +353 -0
- prismcv-0.1.0/src/prismcv/cli.py +79 -0
- prismcv-0.1.0/src/prismcv/example/content/education.yaml +15 -0
- prismcv-0.1.0/src/prismcv/example/content/experience.yaml +60 -0
- prismcv-0.1.0/src/prismcv/example/content/profile.yaml +9 -0
- prismcv-0.1.0/src/prismcv/example/content/projects.yaml +15 -0
- prismcv-0.1.0/src/prismcv/example/content/publications.yaml +6 -0
- prismcv-0.1.0/src/prismcv/example/content/skills.yaml +6 -0
- prismcv-0.1.0/src/prismcv/example/cv.yaml +19 -0
- prismcv-0.1.0/src/prismcv/example/design.yaml +25 -0
- prismcv-0.1.0/src/prismcv/example/targets/academic.yaml +11 -0
- prismcv-0.1.0/src/prismcv/example/targets/full.yaml +11 -0
- prismcv-0.1.0/src/prismcv/example/targets/industry.yaml +9 -0
- prismcv-0.1.0/src/prismcv/example/targets/jobs/2026-10-15_example-data-engineer.yaml +20 -0
- prismcv-0.1.0/src/prismcv/init.py +36 -0
- prismcv-0.1.0/src/prismcv/template/content/education.yaml +6 -0
- prismcv-0.1.0/src/prismcv/template/content/experience.yaml +10 -0
- prismcv-0.1.0/src/prismcv/template/content/profile.yaml +2 -0
- prismcv-0.1.0/src/prismcv/template/content/projects.yaml +6 -0
- prismcv-0.1.0/src/prismcv/template/content/skills.yaml +6 -0
- prismcv-0.1.0/src/prismcv/template/cv.yaml +14 -0
- prismcv-0.1.0/src/prismcv/template/design.yaml +13 -0
- prismcv-0.1.0/src/prismcv/template/targets/main.yaml +7 -0
prismcv-0.1.0/.gitignore
ADDED
prismcv-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andy Zhang
|
|
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.
|
prismcv-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: prismcv
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One source, every CV: build multilingual, tailored CVs with RenderCV.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: pypdf
|
|
9
|
+
Requires-Dist: pyyaml
|
|
10
|
+
Requires-Dist: rendercv[full]==2.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
<!-- markdownlint-disable MD033 MD041 -->
|
|
14
|
+
|
|
15
|
+
<div align="center">
|
|
16
|
+
|
|
17
|
+
<h1>Prism CV</h1>
|
|
18
|
+
|
|
19
|
+
<p><strong>One source, every CV.</strong></p>
|
|
20
|
+
|
|
21
|
+
<p>
|
|
22
|
+
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
|
|
23
|
+
<img alt="Python 3.12+" src="https://img.shields.io/badge/python-3.12%2B-3776AB?logo=python&logoColor=white">
|
|
24
|
+
<a href="https://github.com/rendercv/rendercv"><img alt="RenderCV 2.8" src="https://img.shields.io/badge/RenderCV-2.8-blue"></a>
|
|
25
|
+
<a href="README.zh-CN.md"><img alt="中文" src="https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87-blue"></a>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
Keep your whole career in one place and build every version of your CV from it: each language, academic and industry, and one tailored to each job. Edit an entry once and every version picks it up.
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
```mermaid
|
|
35
|
+
flowchart LR
|
|
36
|
+
C["content/<br/>every entry, written once,<br/>languages side by side"]
|
|
37
|
+
T["targets/x.yaml<br/>which entries, in what order,<br/>what is reworded"]
|
|
38
|
+
B["PrismCV<br/>assembles one YAML per language"]
|
|
39
|
+
R["RenderCV<br/>typesets it into PDF"]
|
|
40
|
+
P["out/x/*.pdf"]
|
|
41
|
+
|
|
42
|
+
C --> B
|
|
43
|
+
T --> B
|
|
44
|
+
B --> R --> P
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
[RenderCV](https://github.com/rendercv/rendercv) typesets one YAML file into a PDF. PrismCV assembles that file from your content and a target; all layout is RenderCV's.
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
Requires Python 3.12+.
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
pip install -e .
|
|
55
|
+
prismcv init my-cv # create a CV folder from a minimal template
|
|
56
|
+
cd my-cv
|
|
57
|
+
prismcv build --all # every target
|
|
58
|
+
prismcv build main --lang en # one target, one language
|
|
59
|
+
prismcv build main --debug # keep the generated RenderCV input
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A CV folder holds `cv.yaml`, `design.yaml`, `content/` and `targets/`. Run `prismcv build` inside one, or point to one with `--root`. PDFs go to `out/<target>/`.
|
|
63
|
+
|
|
64
|
+
`prismcv init --example` creates a full bilingual example instead, using every feature below. Its Chinese version needs the [Noto Sans SC](https://fonts.google.com/noto/specimen/Noto+Sans+SC) font.
|
|
65
|
+
|
|
66
|
+
## Themes
|
|
67
|
+
|
|
68
|
+
Set `theme` in `design.yaml`.
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
## Content
|
|
73
|
+
|
|
74
|
+
`cv.yaml` holds your name and contact details and the section titles. Every file in `content/` holds entries keyed by an id; how you split them into files is up to you. Fields use [RenderCV's names](https://docs.rendercv.com).
|
|
75
|
+
|
|
76
|
+
Text that differs between languages maps language codes to text; anything else is written once. An excerpt from the example's [`content/experience.yaml`](src/prismcv/example/content/experience.yaml):
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
brightwave-intern:
|
|
80
|
+
company: { en: Brightwave Robotics, zh: 明波机器人 }
|
|
81
|
+
location: { en: "Montreal, Canada", zh: 蒙特利尔 }
|
|
82
|
+
start_date: 2025-05
|
|
83
|
+
end_date: present
|
|
84
|
+
highlights:
|
|
85
|
+
- en: Wrote C++ drivers for an IMU
|
|
86
|
+
zh: 编写惯性测量单元的 C++ 驱动
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Use `zh: null` to leave a bullet out of one language. An id can also hold a list of entries (a group of skills), and an entry with only `text` is a paragraph (a profile).
|
|
90
|
+
|
|
91
|
+
## Targets
|
|
92
|
+
|
|
93
|
+
Each file in `targets/` is one version of the CV. `langs` defaults to every language, and `max_pages` warns when a PDF is longer. An excerpt from the example's [target tailored to one job](src/prismcv/example/targets/jobs/2026-10-15_example-data-engineer.yaml):
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
langs: [en]
|
|
97
|
+
max_pages: 1
|
|
98
|
+
sections:
|
|
99
|
+
profile:
|
|
100
|
+
- text: Engineer who enjoys turning messy sensor data into reliable pipelines.
|
|
101
|
+
work:
|
|
102
|
+
title: Experience
|
|
103
|
+
entries:
|
|
104
|
+
- brightwave-intern@data
|
|
105
|
+
- greenleaf-analyst:
|
|
106
|
+
highlights:
|
|
107
|
+
- Rewritten bullet for this role only
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
An item is an id, an id with fields to override, or an entry written inline. A variant such as `brightwave-intern@data` lives in `content/` and changes only the fields it lists, so it suits rewording you will reuse. `title` renames a section for this target.
|
|
111
|
+
|
|
112
|
+
## Languages
|
|
113
|
+
|
|
114
|
+
`languages` in `design.yaml` maps each code to a [RenderCV locale](https://docs.rendercv.com). To add one, add it there, put any settings it needs under its code (a font, a date format), and add its text to the content you use; the build lists anything missing.
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
MIT License
|
|
120
|
+
|
|
121
|
+
Copyright (c) 2026 Andy Zhang
|
|
122
|
+
|
|
123
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
124
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
125
|
+
in the Software without restriction, including without limitation the rights
|
|
126
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
127
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
128
|
+
furnished to do so, subject to the following conditions:
|
|
129
|
+
|
|
130
|
+
The above copyright notice and this permission notice shall be included in all
|
|
131
|
+
copies or substantial portions of the Software.
|
|
132
|
+
|
|
133
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
134
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
135
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
136
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
137
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
138
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
139
|
+
SOFTWARE.
|
|
140
|
+
```
|
prismcv-0.1.0/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<!-- markdownlint-disable MD033 MD041 -->
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h1>Prism CV</h1>
|
|
6
|
+
|
|
7
|
+
<p><strong>One source, every CV.</strong></p>
|
|
8
|
+
|
|
9
|
+
<p>
|
|
10
|
+
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
|
|
11
|
+
<img alt="Python 3.12+" src="https://img.shields.io/badge/python-3.12%2B-3776AB?logo=python&logoColor=white">
|
|
12
|
+
<a href="https://github.com/rendercv/rendercv"><img alt="RenderCV 2.8" src="https://img.shields.io/badge/RenderCV-2.8-blue"></a>
|
|
13
|
+
<a href="README.zh-CN.md"><img alt="中文" src="https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87-blue"></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
Keep your whole career in one place and build every version of your CV from it: each language, academic and industry, and one tailored to each job. Edit an entry once and every version picks it up.
|
|
19
|
+
|
|
20
|
+
## How it works
|
|
21
|
+
|
|
22
|
+
```mermaid
|
|
23
|
+
flowchart LR
|
|
24
|
+
C["content/<br/>every entry, written once,<br/>languages side by side"]
|
|
25
|
+
T["targets/x.yaml<br/>which entries, in what order,<br/>what is reworded"]
|
|
26
|
+
B["PrismCV<br/>assembles one YAML per language"]
|
|
27
|
+
R["RenderCV<br/>typesets it into PDF"]
|
|
28
|
+
P["out/x/*.pdf"]
|
|
29
|
+
|
|
30
|
+
C --> B
|
|
31
|
+
T --> B
|
|
32
|
+
B --> R --> P
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
[RenderCV](https://github.com/rendercv/rendercv) typesets one YAML file into a PDF. PrismCV assembles that file from your content and a target; all layout is RenderCV's.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
Requires Python 3.12+.
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
pip install -e .
|
|
43
|
+
prismcv init my-cv # create a CV folder from a minimal template
|
|
44
|
+
cd my-cv
|
|
45
|
+
prismcv build --all # every target
|
|
46
|
+
prismcv build main --lang en # one target, one language
|
|
47
|
+
prismcv build main --debug # keep the generated RenderCV input
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A CV folder holds `cv.yaml`, `design.yaml`, `content/` and `targets/`. Run `prismcv build` inside one, or point to one with `--root`. PDFs go to `out/<target>/`.
|
|
51
|
+
|
|
52
|
+
`prismcv init --example` creates a full bilingual example instead, using every feature below. Its Chinese version needs the [Noto Sans SC](https://fonts.google.com/noto/specimen/Noto+Sans+SC) font.
|
|
53
|
+
|
|
54
|
+
## Themes
|
|
55
|
+
|
|
56
|
+
Set `theme` in `design.yaml`.
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
## Content
|
|
61
|
+
|
|
62
|
+
`cv.yaml` holds your name and contact details and the section titles. Every file in `content/` holds entries keyed by an id; how you split them into files is up to you. Fields use [RenderCV's names](https://docs.rendercv.com).
|
|
63
|
+
|
|
64
|
+
Text that differs between languages maps language codes to text; anything else is written once. An excerpt from the example's [`content/experience.yaml`](src/prismcv/example/content/experience.yaml):
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
brightwave-intern:
|
|
68
|
+
company: { en: Brightwave Robotics, zh: 明波机器人 }
|
|
69
|
+
location: { en: "Montreal, Canada", zh: 蒙特利尔 }
|
|
70
|
+
start_date: 2025-05
|
|
71
|
+
end_date: present
|
|
72
|
+
highlights:
|
|
73
|
+
- en: Wrote C++ drivers for an IMU
|
|
74
|
+
zh: 编写惯性测量单元的 C++ 驱动
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Use `zh: null` to leave a bullet out of one language. An id can also hold a list of entries (a group of skills), and an entry with only `text` is a paragraph (a profile).
|
|
78
|
+
|
|
79
|
+
## Targets
|
|
80
|
+
|
|
81
|
+
Each file in `targets/` is one version of the CV. `langs` defaults to every language, and `max_pages` warns when a PDF is longer. An excerpt from the example's [target tailored to one job](src/prismcv/example/targets/jobs/2026-10-15_example-data-engineer.yaml):
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
langs: [en]
|
|
85
|
+
max_pages: 1
|
|
86
|
+
sections:
|
|
87
|
+
profile:
|
|
88
|
+
- text: Engineer who enjoys turning messy sensor data into reliable pipelines.
|
|
89
|
+
work:
|
|
90
|
+
title: Experience
|
|
91
|
+
entries:
|
|
92
|
+
- brightwave-intern@data
|
|
93
|
+
- greenleaf-analyst:
|
|
94
|
+
highlights:
|
|
95
|
+
- Rewritten bullet for this role only
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
An item is an id, an id with fields to override, or an entry written inline. A variant such as `brightwave-intern@data` lives in `content/` and changes only the fields it lists, so it suits rewording you will reuse. `title` renames a section for this target.
|
|
99
|
+
|
|
100
|
+
## Languages
|
|
101
|
+
|
|
102
|
+
`languages` in `design.yaml` maps each code to a [RenderCV locale](https://docs.rendercv.com). To add one, add it there, put any settings it needs under its code (a font, a date format), and add its text to the content you use; the build lists anything missing.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
MIT License
|
|
108
|
+
|
|
109
|
+
Copyright (c) 2026 Andy Zhang
|
|
110
|
+
|
|
111
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
112
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
113
|
+
in the Software without restriction, including without limitation the rights
|
|
114
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
115
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
116
|
+
furnished to do so, subject to the following conditions:
|
|
117
|
+
|
|
118
|
+
The above copyright notice and this permission notice shall be included in all
|
|
119
|
+
copies or substantial portions of the Software.
|
|
120
|
+
|
|
121
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
122
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
123
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
124
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
125
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
126
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
127
|
+
SOFTWARE.
|
|
128
|
+
```
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<!-- markdownlint-disable MD033 MD041 -->
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h1>Prism CV</h1>
|
|
6
|
+
|
|
7
|
+
<p><strong>一份内容,生成所有简历。</strong></p>
|
|
8
|
+
|
|
9
|
+
<p>
|
|
10
|
+
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
|
|
11
|
+
<img alt="Python 3.12+" src="https://img.shields.io/badge/python-3.12%2B-3776AB?logo=python&logoColor=white">
|
|
12
|
+
<a href="https://github.com/rendercv/rendercv"><img alt="RenderCV 2.8" src="https://img.shields.io/badge/RenderCV-2.8-blue"></a>
|
|
13
|
+
<a href="README.md"><img alt="English" src="https://img.shields.io/badge/README-English-blue"></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
把所有经历集中放在一处,由它生成每个版本的简历:各种语言、学术版和工业版,以及为每个岗位定制的版本。一段经历只改一次,所有版本都会更新。
|
|
19
|
+
|
|
20
|
+
## 原理
|
|
21
|
+
|
|
22
|
+
```mermaid
|
|
23
|
+
flowchart LR
|
|
24
|
+
C["content/<br/>所有经历,只写一次,<br/>各种语言写在一起"]
|
|
25
|
+
T["targets/x.yaml<br/>用哪些条目、什么顺序、<br/>改写哪些地方"]
|
|
26
|
+
B["PrismCV<br/>拼出每种语言一份 YAML"]
|
|
27
|
+
R["RenderCV<br/>排版成 PDF"]
|
|
28
|
+
P["out/x/*.pdf"]
|
|
29
|
+
|
|
30
|
+
C --> B
|
|
31
|
+
T --> B
|
|
32
|
+
B --> R --> P
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
[RenderCV](https://github.com/rendercv/rendercv) 把一份 YAML 排版成 PDF。Prism CV 根据内容和版本文件拼出这份 YAML,排版全部由 RenderCV 完成。
|
|
36
|
+
|
|
37
|
+
## 快速开始
|
|
38
|
+
|
|
39
|
+
需要 Python 3.12 以上。
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
pip install -e .
|
|
43
|
+
prismcv init my-cv # 用最小模板新建一个简历文件夹
|
|
44
|
+
cd my-cv
|
|
45
|
+
prismcv build --all # 所有版本
|
|
46
|
+
prismcv build main --lang en # 一个版本、一种语言
|
|
47
|
+
prismcv build main --debug # 保留生成的 RenderCV 输入文件
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
简历文件夹包含 `cv.yaml`、`design.yaml`、`content/` 和 `targets/`。在其中运行 `prismcv build`,或者用 `--root` 指定这样的文件夹。PDF 输出到 `out/<版本>/`。
|
|
51
|
+
|
|
52
|
+
`prismcv init --example` 会改为生成一份完整的中英双语示例,用到下面介绍的所有功能。其中文版需要安装 [Noto Sans SC](https://fonts.google.com/noto/specimen/Noto+Sans+SC) 字体。
|
|
53
|
+
|
|
54
|
+
## 主题
|
|
55
|
+
|
|
56
|
+
在 `design.yaml` 中修改 `theme`。
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
## 内容
|
|
61
|
+
|
|
62
|
+
`cv.yaml` 是姓名、联系方式和板块标题。`content/` 里的每个文件按 id 存放条目,怎么分文件由你决定。字段名沿用 [RenderCV 的字段](https://docs.rendercv.com)。
|
|
63
|
+
|
|
64
|
+
各语言不同的文字写成“语言代码 → 文字”,其余只写一次。下面节选自示例的 [`content/experience.yaml`](src/prismcv/example/content/experience.yaml):
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
brightwave-intern:
|
|
68
|
+
company: { en: Brightwave Robotics, zh: 明波机器人 }
|
|
69
|
+
location: { en: "Montreal, Canada", zh: 蒙特利尔 }
|
|
70
|
+
start_date: 2025-05
|
|
71
|
+
end_date: present
|
|
72
|
+
highlights:
|
|
73
|
+
- en: Wrote C++ drivers for an IMU
|
|
74
|
+
zh: 编写惯性测量单元的 C++ 驱动
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
某条 bullet 在某种语言中不需要,写 `zh: null`。一个 id 也可以对应一组条目(比如一组技能),只有 `text` 的条目是一段文字(比如个人简介)。
|
|
78
|
+
|
|
79
|
+
## 版本
|
|
80
|
+
|
|
81
|
+
`targets/` 里每个文件是一个版本的简历。`langs` 默认生成所有语言,`max_pages` 在 PDF 超出页数时给出警告。下面节选自示例中[一个针对岗位定制的版本](src/prismcv/example/targets/jobs/2026-10-15_example-data-engineer.yaml):
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
langs: [en]
|
|
85
|
+
max_pages: 1
|
|
86
|
+
sections:
|
|
87
|
+
profile:
|
|
88
|
+
- text: Engineer who enjoys turning messy sensor data into reliable pipelines.
|
|
89
|
+
work:
|
|
90
|
+
title: Experience
|
|
91
|
+
entries:
|
|
92
|
+
- brightwave-intern@data
|
|
93
|
+
- greenleaf-analyst:
|
|
94
|
+
highlights:
|
|
95
|
+
- Rewritten bullet for this role only
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
条目可以是一个 id、带覆盖字段的 id,或者直接写出的条目。`brightwave-intern@data` 这样的变体写在 `content/` 中,只改写它列出的字段,适合会重复使用的改写。`title` 为这个版本修改板块标题。
|
|
99
|
+
|
|
100
|
+
## 语言
|
|
101
|
+
|
|
102
|
+
`design.yaml` 中的 `languages` 把每个语言代码对应到一个 [RenderCV 语言](https://docs.rendercv.com)。添加语言时,在这里加上它,把它需要的设置(字体、日期格式)写在它的代码下,再为用到的内容补上译文;缺少的地方构建时会列出来。
|
|
103
|
+
|
|
104
|
+
## 许可证
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
MIT License
|
|
108
|
+
|
|
109
|
+
Copyright (c) 2026 Andy Zhang
|
|
110
|
+
|
|
111
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
112
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
113
|
+
in the Software without restriction, including without limitation the rights
|
|
114
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
115
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
116
|
+
furnished to do so, subject to the following conditions:
|
|
117
|
+
|
|
118
|
+
The above copyright notice and this permission notice shall be included in all
|
|
119
|
+
copies or substantial portions of the Software.
|
|
120
|
+
|
|
121
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
122
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
123
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
124
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
125
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
126
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
127
|
+
SOFTWARE.
|
|
128
|
+
```
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "prismcv"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "One source, every CV: build multilingual, tailored CVs with RenderCV."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"rendercv[full]==2.8",
|
|
14
|
+
"pypdf",
|
|
15
|
+
"pyyaml",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
prismcv = "prismcv.cli:main"
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Build CV PDFs from a CV folder (cv.yaml, design.yaml, content/, targets/) with RenderCV."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import functools
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
from pypdf import PdfReader
|
|
16
|
+
|
|
17
|
+
ROOT = Path.cwd()
|
|
18
|
+
CV_FILE = ROOT / "cv.yaml"
|
|
19
|
+
CONTENT = ROOT / "content"
|
|
20
|
+
TARGETS = ROOT / "targets"
|
|
21
|
+
OUT = ROOT / "out"
|
|
22
|
+
DESIGN = ROOT / "design.yaml"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def use_root(root: Path) -> None:
|
|
26
|
+
"""Point every path at the CV folder to build."""
|
|
27
|
+
global ROOT, CV_FILE, CONTENT, TARGETS, OUT, DESIGN
|
|
28
|
+
ROOT = root.resolve()
|
|
29
|
+
CV_FILE, DESIGN = ROOT / "cv.yaml", ROOT / "design.yaml"
|
|
30
|
+
CONTENT, TARGETS, OUT = ROOT / "content", ROOT / "targets", ROOT / "out"
|
|
31
|
+
load_design.cache_clear()
|
|
32
|
+
if not all(p.is_file() for p in (CV_FILE, DESIGN)) or not all(p.is_dir() for p in (CONTENT, TARGETS)):
|
|
33
|
+
raise BuildError(f"{ROOT} is not a CV folder: it needs cv.yaml, design.yaml, content/ and targets/ "
|
|
34
|
+
"(create one with prismcv init)")
|
|
35
|
+
|
|
36
|
+
SCHEMA = "https://raw.githubusercontent.com/rendercv/rendercv/refs/tags/v2.8/schema.json"
|
|
37
|
+
|
|
38
|
+
# Fields a RenderCV entry can have. A one-key item in a target whose key is
|
|
39
|
+
# neither an id nor one of these is reported as an unknown id.
|
|
40
|
+
ENTRY_FIELDS = {
|
|
41
|
+
"text", "bullet", "label", "details", "number", "reversed_number",
|
|
42
|
+
"company", "position", "institution", "area", "degree", "name",
|
|
43
|
+
"title", "authors", "journal", "doi", "url", "summary", "highlights",
|
|
44
|
+
"date", "start_date", "end_date", "location",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Marks text explicitly set to null for one language, so it is left out.
|
|
48
|
+
OMIT = object()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BuildError(Exception):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_yaml(path: Path):
|
|
56
|
+
with open(path, encoding="utf-8") as f:
|
|
57
|
+
return yaml.safe_load(f)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def rel(path: Path) -> str:
|
|
61
|
+
return str(path.relative_to(ROOT))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@functools.cache
|
|
65
|
+
def load_design() -> tuple[dict, dict, dict]:
|
|
66
|
+
"""Read design.yaml: languages, the shared settings, and per-language settings."""
|
|
67
|
+
data = load_yaml(DESIGN) or {}
|
|
68
|
+
langs = data.pop("languages", None) or {"en": "english"}
|
|
69
|
+
if not isinstance(langs, dict) or not all(isinstance(v, str) for v in langs.values()):
|
|
70
|
+
raise BuildError("design.yaml: languages must map language codes to RenderCV locales, e.g. en: english")
|
|
71
|
+
per_lang = {code: data.pop(code) or {} for code in list(langs) if code in data}
|
|
72
|
+
return langs, data, per_lang
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def languages() -> tuple[str, ...]:
|
|
76
|
+
return tuple(load_design()[0])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def merge(base: dict, extra: dict) -> dict:
|
|
80
|
+
"""Merge extra into base, recursing into nested mappings."""
|
|
81
|
+
for k, v in extra.items():
|
|
82
|
+
if isinstance(v, dict) and isinstance(base.get(k), dict):
|
|
83
|
+
merge(base[k], v)
|
|
84
|
+
else:
|
|
85
|
+
base[k] = v
|
|
86
|
+
return base
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def design_for(lang: str) -> dict:
|
|
90
|
+
"""RenderCV design, locale and settings for one language."""
|
|
91
|
+
langs, shared, per_lang = load_design()
|
|
92
|
+
doc = copy.deepcopy(shared)
|
|
93
|
+
merge(doc.setdefault("design", {}), copy.deepcopy(per_lang.get(lang, {})))
|
|
94
|
+
doc.setdefault("locale", {})["language"] = langs[lang]
|
|
95
|
+
return localize(doc, lang, "design.yaml")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def load_pool() -> dict:
|
|
99
|
+
"""Merge every file in content/ into one id -> entry pool."""
|
|
100
|
+
pool, where = {}, {}
|
|
101
|
+
for path in sorted(CONTENT.rglob("*.yaml")):
|
|
102
|
+
data = load_yaml(path) or {}
|
|
103
|
+
if not isinstance(data, dict):
|
|
104
|
+
raise BuildError(f"{rel(path)}: top level must map ids to entries")
|
|
105
|
+
for eid, entry in data.items():
|
|
106
|
+
if eid in pool:
|
|
107
|
+
raise BuildError(f"duplicate id '{eid}' in {rel(where[eid])} and {rel(path)}")
|
|
108
|
+
pool[eid], where[eid] = entry, path
|
|
109
|
+
return pool
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def load_cv() -> tuple[dict, dict]:
|
|
113
|
+
"""Read cv.yaml: the header (name and contact details) and the section titles."""
|
|
114
|
+
data = load_yaml(CV_FILE) or {}
|
|
115
|
+
if not isinstance(data, dict):
|
|
116
|
+
raise BuildError("cv.yaml: top level must be a mapping with header and sections")
|
|
117
|
+
return dict(data.get("header") or {}), dict(data.get("sections") or {})
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Languages
|
|
121
|
+
|
|
122
|
+
def is_localized(value) -> bool:
|
|
123
|
+
return isinstance(value, dict) and bool(value) and set(value) <= set(languages())
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def localize(value, lang: str, where: str):
|
|
127
|
+
"""Replace every {en: ..., zh: ...} mapping inside value with the text for lang."""
|
|
128
|
+
if is_localized(value):
|
|
129
|
+
if lang not in value:
|
|
130
|
+
raise BuildError(f"{where}: missing '{lang}' text")
|
|
131
|
+
if value[lang] is None:
|
|
132
|
+
return OMIT
|
|
133
|
+
return localize(value[lang], lang, where)
|
|
134
|
+
if isinstance(value, dict):
|
|
135
|
+
if set(value) & set(languages()):
|
|
136
|
+
extra = sorted(set(value) - set(languages()))
|
|
137
|
+
raise BuildError(f"{where}: unexpected keys {extra} next to language keys "
|
|
138
|
+
"- put text containing commas in quotes, e.g. en: \"London, UK\"")
|
|
139
|
+
out = {}
|
|
140
|
+
for k, v in value.items():
|
|
141
|
+
v = localize(v, lang, f"{where}.{k}")
|
|
142
|
+
if v is not OMIT:
|
|
143
|
+
out[k] = v
|
|
144
|
+
return out
|
|
145
|
+
if isinstance(value, list):
|
|
146
|
+
out = []
|
|
147
|
+
for i, v in enumerate(value):
|
|
148
|
+
v = localize(v, lang, f"{where}[{i}]")
|
|
149
|
+
if v is not OMIT:
|
|
150
|
+
out.append(v)
|
|
151
|
+
return out
|
|
152
|
+
return value
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# Entries
|
|
156
|
+
|
|
157
|
+
def resolve_id(eid: str, pool: dict, stack: tuple = ()):
|
|
158
|
+
"""Return a copy of an entry with its base entry (for variants) applied."""
|
|
159
|
+
if eid not in pool:
|
|
160
|
+
raise BuildError(f"unknown id '{eid}'")
|
|
161
|
+
if eid in stack:
|
|
162
|
+
raise BuildError(f"circular base: {' -> '.join(stack + (eid,))}")
|
|
163
|
+
entry = copy.deepcopy(pool[eid])
|
|
164
|
+
if not isinstance(entry, dict):
|
|
165
|
+
return entry
|
|
166
|
+
base = entry.pop("base", None)
|
|
167
|
+
if base is None and "@" in eid:
|
|
168
|
+
base = eid.split("@", 1)[0]
|
|
169
|
+
if base is None:
|
|
170
|
+
return entry
|
|
171
|
+
parent = resolve_id(base, pool, stack + (eid,))
|
|
172
|
+
if not isinstance(parent, dict):
|
|
173
|
+
raise BuildError(f"'{eid}': base '{base}' is not a single entry")
|
|
174
|
+
parent.update(entry)
|
|
175
|
+
return parent
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def resolve_item(item, pool: dict) -> list:
|
|
179
|
+
"""Turn one item of a target section into a list of entries.
|
|
180
|
+
|
|
181
|
+
An item is an id, an id with fields to override, or an entry written inline.
|
|
182
|
+
An id may stand for a list of entries, such as a group of skills.
|
|
183
|
+
"""
|
|
184
|
+
if isinstance(item, str):
|
|
185
|
+
entry = resolve_id(item, pool)
|
|
186
|
+
elif isinstance(item, dict) and len(item) == 1 and next(iter(item)) in pool:
|
|
187
|
+
eid, override = next(iter(item.items()))
|
|
188
|
+
entry = resolve_id(eid, pool)
|
|
189
|
+
if override:
|
|
190
|
+
if not isinstance(entry, dict):
|
|
191
|
+
raise BuildError(f"'{eid}' is a list and cannot be overridden field by field")
|
|
192
|
+
entry.update(copy.deepcopy(override))
|
|
193
|
+
elif isinstance(item, dict):
|
|
194
|
+
if len(item) == 1 and next(iter(item)) not in ENTRY_FIELDS:
|
|
195
|
+
raise BuildError(f"unknown id '{next(iter(item))}'")
|
|
196
|
+
entry = copy.deepcopy(item)
|
|
197
|
+
else:
|
|
198
|
+
raise BuildError(f"cannot understand item {item!r}")
|
|
199
|
+
entries = entry if isinstance(entry, list) else [entry]
|
|
200
|
+
return [simplify(e) for e in entries]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def simplify(entry):
|
|
204
|
+
"""An entry with only a text field becomes a plain paragraph."""
|
|
205
|
+
if isinstance(entry, dict) and set(entry) == {"text"}:
|
|
206
|
+
return entry["text"]
|
|
207
|
+
return entry
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def entry_kind(entry) -> str:
|
|
211
|
+
if isinstance(entry, str):
|
|
212
|
+
return "text"
|
|
213
|
+
keys = set(entry)
|
|
214
|
+
for kind, marker in (
|
|
215
|
+
("experience", {"company", "position"}), ("education", {"institution"}),
|
|
216
|
+
("publication", {"authors"}), ("one-line", {"label"}), ("bullet", {"bullet"}),
|
|
217
|
+
("numbered", {"number"}), ("reversed-numbered", {"reversed_number"}),
|
|
218
|
+
("normal", {"name"}),
|
|
219
|
+
):
|
|
220
|
+
if marker <= keys:
|
|
221
|
+
return kind
|
|
222
|
+
return "unknown"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# Assembling
|
|
226
|
+
|
|
227
|
+
def assemble(target_name: str, target: dict, lang: str, pool: dict) -> dict:
|
|
228
|
+
"""Build the complete RenderCV input for one target in one language."""
|
|
229
|
+
header, titles = load_cv()
|
|
230
|
+
header.update(target.get("header") or {})
|
|
231
|
+
|
|
232
|
+
cv = localize(header, lang, "header")
|
|
233
|
+
cv["sections"] = {}
|
|
234
|
+
for key, spec in (target.get("sections") or {}).items():
|
|
235
|
+
where = f"targets/{target_name}.yaml: {key}"
|
|
236
|
+
if isinstance(spec, dict):
|
|
237
|
+
items, title = spec.get("entries") or [], spec.get("title")
|
|
238
|
+
else:
|
|
239
|
+
items, title = spec or [], None
|
|
240
|
+
title = title or titles.get(key)
|
|
241
|
+
if title is None:
|
|
242
|
+
raise BuildError(f"{where}: no title (add '{key}' to sections in cv.yaml)")
|
|
243
|
+
title = localize(title, lang, f"{where}.title")
|
|
244
|
+
|
|
245
|
+
entries = []
|
|
246
|
+
for i, item in enumerate(items):
|
|
247
|
+
try:
|
|
248
|
+
resolved = resolve_item(item, pool)
|
|
249
|
+
except BuildError as e:
|
|
250
|
+
raise BuildError(f"{where}[{i}]: {e}") from None
|
|
251
|
+
for e in resolved:
|
|
252
|
+
e = localize(e, lang, f"{where}[{i}]")
|
|
253
|
+
if e is not OMIT:
|
|
254
|
+
entries.append(e)
|
|
255
|
+
if not entries:
|
|
256
|
+
continue
|
|
257
|
+
kinds = {entry_kind(e) for e in entries}
|
|
258
|
+
if len(kinds) > 1:
|
|
259
|
+
raise BuildError(f"{where}: mixed entry types {sorted(kinds)} in one section")
|
|
260
|
+
cv["sections"][title] = entries
|
|
261
|
+
|
|
262
|
+
doc = {"cv": cv}
|
|
263
|
+
doc.update(design_for(lang))
|
|
264
|
+
return doc
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def write_input(doc: dict, folder: Path, stem: str) -> Path:
|
|
268
|
+
settings = doc.setdefault("settings", {})
|
|
269
|
+
settings.setdefault("render_command", {}).update({
|
|
270
|
+
"output_folder": ".",
|
|
271
|
+
"pdf_path": f"{stem}.pdf",
|
|
272
|
+
"typst_path": f"{stem}.typ",
|
|
273
|
+
"dont_generate_markdown": True,
|
|
274
|
+
"dont_generate_html": True,
|
|
275
|
+
"dont_generate_png": True,
|
|
276
|
+
})
|
|
277
|
+
path = folder / f"{stem}.yaml"
|
|
278
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
279
|
+
f.write(f"# yaml-language-server: $schema={SCHEMA}\n")
|
|
280
|
+
f.write("# Generated by build.py. Edit content/ and targets/ instead.\n")
|
|
281
|
+
yaml.safe_dump(doc, f, allow_unicode=True, sort_keys=False, width=1000)
|
|
282
|
+
return path
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def render(yaml_path: Path) -> None:
|
|
286
|
+
exe = shutil.which("rendercv", path=str(Path(sys.executable).parent)) or shutil.which("rendercv")
|
|
287
|
+
if not exe:
|
|
288
|
+
raise BuildError("rendercv not found - install the requirements: pip install -r requirements.txt")
|
|
289
|
+
proc = subprocess.run([exe, "render", yaml_path.name], cwd=yaml_path.parent,
|
|
290
|
+
capture_output=True, text=True)
|
|
291
|
+
if proc.returncode != 0:
|
|
292
|
+
raise BuildError(f"RenderCV failed (run with --debug to keep its input):\n{proc.stdout}{proc.stderr}")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# Targets
|
|
296
|
+
|
|
297
|
+
def all_targets() -> list[str]:
|
|
298
|
+
return sorted(str(p.relative_to(TARGETS).with_suffix("")) for p in TARGETS.rglob("*.yaml"))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def normalise(name: str) -> str:
|
|
302
|
+
name = name.removeprefix("targets/").removesuffix(".yaml")
|
|
303
|
+
if not (TARGETS / f"{name}.yaml").exists():
|
|
304
|
+
raise BuildError(f"no such target: targets/{name}.yaml")
|
|
305
|
+
return name
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def file_stem(lang: str) -> str:
|
|
309
|
+
"""File name for a PDF, from the name in the first configured language."""
|
|
310
|
+
name = load_cv()[0].get("name", "CV")
|
|
311
|
+
name = localize(name, languages()[0], "header.name")
|
|
312
|
+
stem = re.sub(r"\W+", "_", str(name)).strip("_") or "CV"
|
|
313
|
+
return f"{stem}_CV_{lang}"
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def build(name: str, pool: dict, only_lang: str | None, debug: bool) -> list[str]:
|
|
317
|
+
"""Build one target and return any warnings."""
|
|
318
|
+
target = load_yaml(TARGETS / f"{name}.yaml") or {}
|
|
319
|
+
langs = target.get("langs") or list(languages())
|
|
320
|
+
limit = target.get("max_pages")
|
|
321
|
+
if limit is not None and (not isinstance(limit, int) or isinstance(limit, bool) or limit < 1):
|
|
322
|
+
raise BuildError(f"targets/{name}.yaml: max_pages must be a positive whole number (or left out for no limit)")
|
|
323
|
+
if only_lang:
|
|
324
|
+
langs = [l for l in langs if l == only_lang]
|
|
325
|
+
if not langs:
|
|
326
|
+
print(f" - skipped: this target is not built in '{only_lang}'")
|
|
327
|
+
|
|
328
|
+
warnings = []
|
|
329
|
+
for lang in langs:
|
|
330
|
+
if lang not in languages():
|
|
331
|
+
raise BuildError(f"targets/{name}.yaml: language '{lang}' is not listed in design.yaml")
|
|
332
|
+
doc = assemble(name, target, lang, pool)
|
|
333
|
+
out_folder = OUT / name
|
|
334
|
+
out_folder.mkdir(parents=True, exist_ok=True)
|
|
335
|
+
stem = file_stem(lang)
|
|
336
|
+
pdf = out_folder / f"{stem}.pdf"
|
|
337
|
+
with tempfile.TemporaryDirectory(prefix="cv-") as tmp:
|
|
338
|
+
work = Path(tmp)
|
|
339
|
+
path = write_input(doc, work, stem)
|
|
340
|
+
try:
|
|
341
|
+
render(path)
|
|
342
|
+
finally:
|
|
343
|
+
if debug:
|
|
344
|
+
for suffix in (".yaml", ".typ"):
|
|
345
|
+
if (work / f"{stem}{suffix}").exists():
|
|
346
|
+
shutil.copy2(work / f"{stem}{suffix}", out_folder / f"{stem}{suffix}")
|
|
347
|
+
shutil.copy2(work / f"{stem}.pdf", pdf)
|
|
348
|
+
|
|
349
|
+
pages = len(PdfReader(pdf).pages)
|
|
350
|
+
print(f" ✓ {rel(pdf)} ({pages} page{'s' if pages != 1 else ''})")
|
|
351
|
+
if limit and pages > limit:
|
|
352
|
+
warnings.append(f"{name} ({lang}): {pages} pages, exceeds max_pages: {limit}")
|
|
353
|
+
return warnings
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""The prismcv command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from . import build as b
|
|
10
|
+
from .init import InitError, create
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_build(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
|
|
14
|
+
try:
|
|
15
|
+
b.use_root(args.root)
|
|
16
|
+
if not args.targets and not args.all:
|
|
17
|
+
parser.print_help()
|
|
18
|
+
print("\navailable targets:\n " + "\n ".join(b.all_targets()))
|
|
19
|
+
return 1
|
|
20
|
+
if args.lang and args.lang not in b.languages():
|
|
21
|
+
raise b.BuildError(f"unknown language '{args.lang}' (configured: {', '.join(b.languages())})")
|
|
22
|
+
pool = b.load_pool()
|
|
23
|
+
names = b.all_targets() if args.all else [b.normalise(t) for t in args.targets]
|
|
24
|
+
except b.BuildError as e:
|
|
25
|
+
print(f"✗ {e}", file=sys.stderr)
|
|
26
|
+
return 1
|
|
27
|
+
|
|
28
|
+
failed, warnings = [], []
|
|
29
|
+
for name in names:
|
|
30
|
+
print(name)
|
|
31
|
+
try:
|
|
32
|
+
warnings += b.build(name, pool, args.lang, args.debug)
|
|
33
|
+
except b.BuildError as e:
|
|
34
|
+
print(f" ✗ {e}", file=sys.stderr)
|
|
35
|
+
failed.append(name)
|
|
36
|
+
for w in warnings:
|
|
37
|
+
print(f"⚠ {w}")
|
|
38
|
+
if failed:
|
|
39
|
+
print(f"✗ failed: {', '.join(failed)}", file=sys.stderr)
|
|
40
|
+
return 1
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run_init(args: argparse.Namespace) -> int:
|
|
45
|
+
try:
|
|
46
|
+
created = create(args.path, example=args.example)
|
|
47
|
+
except InitError as e:
|
|
48
|
+
print(f"✗ {e}", file=sys.stderr)
|
|
49
|
+
return 1
|
|
50
|
+
root = args.path.resolve()
|
|
51
|
+
for path in created:
|
|
52
|
+
print(f" + {path.relative_to(root)}")
|
|
53
|
+
print(f"Created a CV in {root}. Edit cv.yaml and content/, then run: prismcv build --all")
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main() -> int:
|
|
58
|
+
ap = argparse.ArgumentParser(prog="prismcv", description="One source, every CV.")
|
|
59
|
+
commands = ap.add_subparsers(dest="command", required=True)
|
|
60
|
+
|
|
61
|
+
bp = commands.add_parser("build", help="build CV PDFs with RenderCV")
|
|
62
|
+
bp.add_argument("targets", nargs="*", help="target names, e.g. industry or jobs/2026-10-15_acme")
|
|
63
|
+
bp.add_argument("--all", action="store_true", help="build every target")
|
|
64
|
+
bp.add_argument("--lang", help="only build this language")
|
|
65
|
+
bp.add_argument("--debug", action="store_true", help="keep the generated RenderCV input next to the PDF")
|
|
66
|
+
bp.add_argument("--root", type=Path, default=Path.cwd(), help="CV folder to build (default: current folder)")
|
|
67
|
+
|
|
68
|
+
ip = commands.add_parser("init", help="create a new CV folder")
|
|
69
|
+
ip.add_argument("path", nargs="?", type=Path, default=Path.cwd(), help="folder to create it in (default: current folder)")
|
|
70
|
+
ip.add_argument("--example", action="store_true", help="start from the full bilingual example instead of the minimal template")
|
|
71
|
+
|
|
72
|
+
args = ap.parse_args()
|
|
73
|
+
if args.command == "init":
|
|
74
|
+
return run_init(args)
|
|
75
|
+
return run_build(args, bp)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
sys.exit(main())
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
meng:
|
|
2
|
+
institution: { en: Northfield Institute of Technology, zh: 北原理工学院 }
|
|
3
|
+
degree: { en: MEng, zh: 硕士 }
|
|
4
|
+
area: { en: Electrical Engineering, zh: 电气工程 }
|
|
5
|
+
location: { en: "Toronto, Canada", zh: 加拿大多伦多 }
|
|
6
|
+
start_date: 2024-09
|
|
7
|
+
end_date: present
|
|
8
|
+
|
|
9
|
+
bsc:
|
|
10
|
+
institution: { en: Lakeside University, zh: 湖畔大学 }
|
|
11
|
+
degree: { en: BSc, zh: 学士 }
|
|
12
|
+
area: { en: Physics, zh: 物理学 }
|
|
13
|
+
location: { en: "Vancouver, Canada", zh: 加拿大温哥华 }
|
|
14
|
+
start_date: 2020-09
|
|
15
|
+
end_date: 2024-05
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Work, research and activities. Each target decides which section an entry goes in.
|
|
2
|
+
|
|
3
|
+
photonics-lab:
|
|
4
|
+
company: { en: "Applied Photonics Lab, Lakeside University", zh: 湖畔大学应用光子学实验室 }
|
|
5
|
+
position: { en: Undergraduate Researcher, zh: 本科生研究员 }
|
|
6
|
+
location: { en: "Vancouver, Canada", zh: 加拿大温哥华 }
|
|
7
|
+
start_date: 2023-05
|
|
8
|
+
end_date: 2024-04
|
|
9
|
+
highlights:
|
|
10
|
+
- en: Calibrated a LiDAR array and wrote Python tools to clean its output
|
|
11
|
+
zh: 校准激光雷达阵列,并编写 Python 工具清洗其输出数据
|
|
12
|
+
- en: Results contributed to a conference paper on sensor fusion
|
|
13
|
+
zh: 研究结果用于一篇多传感器融合方向的会议论文
|
|
14
|
+
|
|
15
|
+
brightwave-intern:
|
|
16
|
+
company: { en: Brightwave Robotics, zh: 明波机器人 }
|
|
17
|
+
position: { en: Embedded Software Intern, zh: 嵌入式软件实习生 }
|
|
18
|
+
location: { en: "Montreal, Canada", zh: 加拿大蒙特利尔 }
|
|
19
|
+
start_date: 2025-05
|
|
20
|
+
end_date: 2025-08
|
|
21
|
+
highlights:
|
|
22
|
+
- en: Wrote C++ drivers for an IMU on a warehouse robot platform
|
|
23
|
+
zh: 为仓储机器人平台的惯性测量单元编写 C++ 驱动
|
|
24
|
+
- en: Cut sensor read latency by 30% by moving polling to DMA
|
|
25
|
+
zh: 将轮询改为 DMA 传输,传感器读取延迟降低 30%
|
|
26
|
+
- en: Added hardware-in-the-loop tests to the CI pipeline
|
|
27
|
+
zh: 在持续集成流程中加入硬件在环测试
|
|
28
|
+
|
|
29
|
+
# A variant: inherits brightwave-intern and replaces the fields written here.
|
|
30
|
+
brightwave-intern@data:
|
|
31
|
+
highlights:
|
|
32
|
+
- en: Built a pipeline to collect and visualise sensor logs from test runs
|
|
33
|
+
zh: 搭建测试运行中传感器日志的采集与可视化流程
|
|
34
|
+
- en: Analysed failure data to find the root cause of drift in navigation
|
|
35
|
+
zh: 分析故障数据,定位导航漂移问题的根本原因
|
|
36
|
+
|
|
37
|
+
greenleaf-analyst:
|
|
38
|
+
company: { en: Greenleaf Energy, zh: 绿叶能源 }
|
|
39
|
+
position: { en: Part-time Data Analyst, zh: 兼职数据分析师 }
|
|
40
|
+
location: { en: "Toronto, Canada", zh: 加拿大多伦多 }
|
|
41
|
+
start_date: 2025-10
|
|
42
|
+
end_date: present
|
|
43
|
+
highlights:
|
|
44
|
+
- en: Build weekly dashboards on solar panel output for the operations team
|
|
45
|
+
zh: 为运营团队制作太阳能板发电量的周报看板
|
|
46
|
+
|
|
47
|
+
robotics-club:
|
|
48
|
+
company: { en: Lakeside University Robotics Club, zh: 湖畔大学机器人社 }
|
|
49
|
+
position: { en: Team Captain, zh: 队长 }
|
|
50
|
+
start_date: 2022-09
|
|
51
|
+
end_date: 2024-05
|
|
52
|
+
highlights:
|
|
53
|
+
- en: Led a 12-person team through two national competitions
|
|
54
|
+
zh: 带领 12 人团队参加两届全国比赛
|
|
55
|
+
|
|
56
|
+
hackathon:
|
|
57
|
+
company: { en: Northfield Hardware Hackathon, zh: 北原硬件黑客松 }
|
|
58
|
+
position: { en: Co-organiser, zh: 联合组织者 }
|
|
59
|
+
start_date: 2025-01
|
|
60
|
+
end_date: 2025-03
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# A target can also write its own paragraph instead.
|
|
2
|
+
profile-industry:
|
|
3
|
+
text:
|
|
4
|
+
en: Electrical engineer with hands-on experience in embedded software and sensor data pipelines.
|
|
5
|
+
zh: 电气工程背景,有嵌入式软件与传感器数据处理的实践经验。
|
|
6
|
+
profile-academic:
|
|
7
|
+
text:
|
|
8
|
+
en: Electrical engineering graduate researching sensor fusion for low-cost robotics.
|
|
9
|
+
zh: 电气工程硕士,研究方向为低成本机器人的多传感器融合。
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
weather-station:
|
|
2
|
+
name: { en: Solar Weather Station, zh: 太阳能气象站 }
|
|
3
|
+
start_date: 2024-01
|
|
4
|
+
end_date: 2024-04
|
|
5
|
+
highlights:
|
|
6
|
+
- en: Off-grid weather station on an ESP32, reporting over LoRa
|
|
7
|
+
zh: 基于 ESP32 的离网气象站,通过 LoRa 上报数据
|
|
8
|
+
|
|
9
|
+
line-follower:
|
|
10
|
+
name: { en: Line-following Robot, zh: 循迹机器人 }
|
|
11
|
+
start_date: 2023-02
|
|
12
|
+
end_date: 2023-03
|
|
13
|
+
highlights:
|
|
14
|
+
- en: PID-controlled robot that placed 2nd in a regional competition
|
|
15
|
+
zh: 采用 PID 控制,在地区比赛中获得第二名
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Plain text is shared by all languages; {en: ..., zh: ...} gives one text per language.
|
|
2
|
+
header:
|
|
3
|
+
name: { en: Nathan Lee, zh: 李南森 }
|
|
4
|
+
location: { en: "Toronto, Canada", zh: 加拿大多伦多 }
|
|
5
|
+
email: nathan.lee@example.com
|
|
6
|
+
phone: { en: "+1 416 555 0123", zh: "+86 138 0013 8000" }
|
|
7
|
+
social_networks:
|
|
8
|
+
- network: GitHub
|
|
9
|
+
username: example
|
|
10
|
+
|
|
11
|
+
sections:
|
|
12
|
+
profile: { en: Profile, zh: 个人简介 }
|
|
13
|
+
education: { en: Education, zh: 教育背景 }
|
|
14
|
+
work: { en: Work Experience, zh: 工作经历 }
|
|
15
|
+
research: { en: Research Experience, zh: 科研经历 }
|
|
16
|
+
projects: { en: Projects, zh: 项目经历 }
|
|
17
|
+
publications: { en: Publications, zh: 发表论文 }
|
|
18
|
+
activities: { en: Leadership & Activities, zh: 校园活动 }
|
|
19
|
+
skills: { en: Skills, zh: 技能 }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Language codes and their RenderCV locales. The first names the PDF files.
|
|
2
|
+
languages:
|
|
3
|
+
en: english
|
|
4
|
+
zh: mandarin_chinese
|
|
5
|
+
|
|
6
|
+
# RenderCV design shared by every language. Options: https://docs.rendercv.com
|
|
7
|
+
design:
|
|
8
|
+
theme: classic
|
|
9
|
+
page:
|
|
10
|
+
size: a4
|
|
11
|
+
show_footer: false
|
|
12
|
+
show_top_note: false
|
|
13
|
+
sections:
|
|
14
|
+
show_time_spans_in: []
|
|
15
|
+
|
|
16
|
+
# Settings for one language, added on top of the shared design.
|
|
17
|
+
zh:
|
|
18
|
+
typography:
|
|
19
|
+
font_family: Noto Sans SC
|
|
20
|
+
templates:
|
|
21
|
+
single_date: YEAR年MONTH月
|
|
22
|
+
experience_entry:
|
|
23
|
+
main_column: "**COMPANY**,POSITION\nSUMMARY\nHIGHLIGHTS"
|
|
24
|
+
education_entry:
|
|
25
|
+
main_column: "**INSTITUTION**,AREA\nSUMMARY\nHIGHLIGHTS"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Academic: research and publications first.
|
|
2
|
+
langs: [en, zh]
|
|
3
|
+
max_pages: 2
|
|
4
|
+
sections:
|
|
5
|
+
profile: [profile-academic]
|
|
6
|
+
education: [meng, bsc]
|
|
7
|
+
research: [photonics-lab]
|
|
8
|
+
publications: [paper-fusion]
|
|
9
|
+
work: [brightwave-intern]
|
|
10
|
+
activities: [robotics-club, hackathon]
|
|
11
|
+
skills: [skills-general]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Full: everything, no page limit.
|
|
2
|
+
langs: [en, zh]
|
|
3
|
+
sections:
|
|
4
|
+
profile: [profile-industry]
|
|
5
|
+
education: [meng, bsc]
|
|
6
|
+
work: [brightwave-intern, greenleaf-analyst]
|
|
7
|
+
research: [photonics-lab]
|
|
8
|
+
projects: [weather-station, line-follower]
|
|
9
|
+
publications: [paper-fusion]
|
|
10
|
+
activities: [robotics-club, hackathon]
|
|
11
|
+
skills: [skills-general]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Industry: work and projects first, no publications.
|
|
2
|
+
langs: [en, zh]
|
|
3
|
+
max_pages: 1
|
|
4
|
+
sections:
|
|
5
|
+
profile: [profile-industry]
|
|
6
|
+
education: [meng, bsc]
|
|
7
|
+
work: [brightwave-intern, greenleaf-analyst]
|
|
8
|
+
projects: [weather-station, line-follower]
|
|
9
|
+
skills: [skills-general]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# A target tailored to one job application, named date_company_role.yaml.
|
|
2
|
+
langs: [en]
|
|
3
|
+
max_pages: 1
|
|
4
|
+
sections:
|
|
5
|
+
# A paragraph written just for this job
|
|
6
|
+
profile:
|
|
7
|
+
- text: Engineer who enjoys turning messy sensor data into reliable pipelines.
|
|
8
|
+
education: [meng, bsc]
|
|
9
|
+
work:
|
|
10
|
+
# A different title for this target only
|
|
11
|
+
title: Experience
|
|
12
|
+
entries:
|
|
13
|
+
# A variant defined in content/
|
|
14
|
+
- brightwave-intern@data
|
|
15
|
+
# Override some fields; the rest come from content/
|
|
16
|
+
- greenleaf-analyst:
|
|
17
|
+
highlights:
|
|
18
|
+
- Rewritten bullet for this role only
|
|
19
|
+
projects: [weather-station]
|
|
20
|
+
skills: [skills-general]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Create a new CV folder from the template or the example."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
HERE = Path(__file__).resolve().parent
|
|
9
|
+
TEMPLATE = HERE / "template"
|
|
10
|
+
EXAMPLE = HERE / "example"
|
|
11
|
+
CV_PARTS = ("cv.yaml", "design.yaml", "content", "targets")
|
|
12
|
+
GITIGNORE = "# Generated PDFs\nout/\n"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InitError(Exception):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create(root: Path, example: bool = False) -> list[Path]:
|
|
20
|
+
"""Copy the template (or the example) into root without overwriting a CV. Returns the files created."""
|
|
21
|
+
root = root.resolve()
|
|
22
|
+
existing = [name for name in CV_PARTS if (root / name).exists()]
|
|
23
|
+
if existing:
|
|
24
|
+
raise InitError(f"{root} already contains {', '.join(existing)}")
|
|
25
|
+
source = EXAMPLE if example else TEMPLATE
|
|
26
|
+
created = []
|
|
27
|
+
for src in sorted(source.rglob("*.yaml")):
|
|
28
|
+
dest = root / src.relative_to(source)
|
|
29
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
shutil.copyfile(src, dest)
|
|
31
|
+
created.append(dest)
|
|
32
|
+
gitignore = root / ".gitignore"
|
|
33
|
+
if not gitignore.exists():
|
|
34
|
+
gitignore.write_text(GITIGNORE, encoding="utf-8")
|
|
35
|
+
created.append(gitignore)
|
|
36
|
+
return created
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Work, research and activities. Each target decides which section an entry goes in.
|
|
2
|
+
example-job:
|
|
3
|
+
company: Example Company
|
|
4
|
+
position: Software Engineer Intern
|
|
5
|
+
location: City, Country
|
|
6
|
+
start_date: 2025-06
|
|
7
|
+
end_date: 2025-09
|
|
8
|
+
highlights:
|
|
9
|
+
- What you did and what came of it.
|
|
10
|
+
- Another achievement, with a number if you have one.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
header:
|
|
2
|
+
name: Your Name
|
|
3
|
+
location: City, Country
|
|
4
|
+
email: you@example.com
|
|
5
|
+
social_networks:
|
|
6
|
+
- network: GitHub
|
|
7
|
+
username: your-username
|
|
8
|
+
|
|
9
|
+
sections:
|
|
10
|
+
profile: Profile
|
|
11
|
+
education: Education
|
|
12
|
+
experience: Experience
|
|
13
|
+
projects: Projects
|
|
14
|
+
skills: Skills
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Language codes and their RenderCV locales. The first names the PDF files.
|
|
2
|
+
languages:
|
|
3
|
+
en: english
|
|
4
|
+
|
|
5
|
+
# RenderCV design shared by every language. Options: https://docs.rendercv.com
|
|
6
|
+
design:
|
|
7
|
+
theme: classic
|
|
8
|
+
page:
|
|
9
|
+
size: a4
|
|
10
|
+
show_footer: false
|
|
11
|
+
show_top_note: false
|
|
12
|
+
sections:
|
|
13
|
+
show_time_spans_in: []
|