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.
Files changed (32) hide show
  1. prismcv-0.1.0/.gitignore +13 -0
  2. prismcv-0.1.0/LICENSE +21 -0
  3. prismcv-0.1.0/PKG-INFO +140 -0
  4. prismcv-0.1.0/README.md +128 -0
  5. prismcv-0.1.0/README.zh-CN.md +128 -0
  6. prismcv-0.1.0/docs/themes.en.jpg +0 -0
  7. prismcv-0.1.0/docs/themes.zh.jpg +0 -0
  8. prismcv-0.1.0/pyproject.toml +19 -0
  9. prismcv-0.1.0/src/prismcv/__init__.py +3 -0
  10. prismcv-0.1.0/src/prismcv/build.py +353 -0
  11. prismcv-0.1.0/src/prismcv/cli.py +79 -0
  12. prismcv-0.1.0/src/prismcv/example/content/education.yaml +15 -0
  13. prismcv-0.1.0/src/prismcv/example/content/experience.yaml +60 -0
  14. prismcv-0.1.0/src/prismcv/example/content/profile.yaml +9 -0
  15. prismcv-0.1.0/src/prismcv/example/content/projects.yaml +15 -0
  16. prismcv-0.1.0/src/prismcv/example/content/publications.yaml +6 -0
  17. prismcv-0.1.0/src/prismcv/example/content/skills.yaml +6 -0
  18. prismcv-0.1.0/src/prismcv/example/cv.yaml +19 -0
  19. prismcv-0.1.0/src/prismcv/example/design.yaml +25 -0
  20. prismcv-0.1.0/src/prismcv/example/targets/academic.yaml +11 -0
  21. prismcv-0.1.0/src/prismcv/example/targets/full.yaml +11 -0
  22. prismcv-0.1.0/src/prismcv/example/targets/industry.yaml +9 -0
  23. prismcv-0.1.0/src/prismcv/example/targets/jobs/2026-10-15_example-data-engineer.yaml +20 -0
  24. prismcv-0.1.0/src/prismcv/init.py +36 -0
  25. prismcv-0.1.0/src/prismcv/template/content/education.yaml +6 -0
  26. prismcv-0.1.0/src/prismcv/template/content/experience.yaml +10 -0
  27. prismcv-0.1.0/src/prismcv/template/content/profile.yaml +2 -0
  28. prismcv-0.1.0/src/prismcv/template/content/projects.yaml +6 -0
  29. prismcv-0.1.0/src/prismcv/template/content/skills.yaml +6 -0
  30. prismcv-0.1.0/src/prismcv/template/cv.yaml +14 -0
  31. prismcv-0.1.0/src/prismcv/template/design.yaml +13 -0
  32. prismcv-0.1.0/src/prismcv/template/targets/main.yaml +7 -0
@@ -0,0 +1,13 @@
1
+ # Generated PDFs
2
+ out/
3
+
4
+ # Python
5
+ __pycache__/
6
+ *.pyc
7
+ .venv/
8
+ dist/
9
+
10
+ # OS and editors
11
+ .DS_Store
12
+ .vscode/
13
+ .idea/
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
+ ![The example CV in nine themes: classic, engineeringresumes, sb2nov, moderncv, engineeringclassic, harvard, ink, opal and ember](docs/themes.en.jpg)
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
+ ```
@@ -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
+ ![The example CV in nine themes: classic, engineeringresumes, sb2nov, moderncv, engineeringclassic, harvard, ink, opal and ember](docs/themes.en.jpg)
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
+ ![示例简历在九个主题下的效果:classic、engineeringresumes、sb2nov、moderncv、engineeringclassic、harvard、ink、opal 和 ember](docs/themes.zh.jpg)
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,3 @@
1
+ """PrismCV: one source, every CV."""
2
+
3
+ __version__ = "0.1.0"
@@ -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,6 @@
1
+ # Mark yourself in the author list with *italics*.
2
+ paper-fusion:
3
+ title: Low-cost LiDAR and IMU Fusion for Indoor Mapping
4
+ authors: [Maria Santos, "*Nathan Lee*", Wei Chen]
5
+ journal: Example Robotics Conference 2024
6
+ date: 2024-06
@@ -0,0 +1,6 @@
1
+ # An id can hold a list of entries; a target that uses it gets them all.
2
+ skills-general:
3
+ - label: { en: Languages, zh: 编程语言 }
4
+ details: C++, Python, MATLAB
5
+ - label: { en: Tools, zh: 工具 }
6
+ details: ROS 2, Git, KiCad, Docker
@@ -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,6 @@
1
+ university:
2
+ institution: Example University
3
+ degree: BSc
4
+ area: Computer Science
5
+ start_date: 2022-09
6
+ end_date: 2026-06
@@ -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,2 @@
1
+ profile:
2
+ text: A sentence or two about who you are and what you are looking for.
@@ -0,0 +1,6 @@
1
+ example-project:
2
+ name: Example Project
3
+ start_date: 2024-01
4
+ end_date: present
5
+ highlights:
6
+ - What it does and how you built it.
@@ -0,0 +1,6 @@
1
+ # An id can hold a list of entries; a target that uses it gets them all.
2
+ skills:
3
+ - label: Languages
4
+ details: Python, Java
5
+ - label: Tools
6
+ details: Git, Docker
@@ -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: []
@@ -0,0 +1,7 @@
1
+ # One version of the CV: which entries go in which section, in order.
2
+ sections:
3
+ profile: [profile]
4
+ education: [university]
5
+ experience: [example-job]
6
+ projects: [example-project]
7
+ skills: [skills]