@airmark/quiver 0.33.0 → 0.34.0

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 (44) hide show
  1. package/README.md +16 -10
  2. package/package.json +7 -7
  3. package/quills/af4141/0.1.0/Quill.yaml +3 -12
  4. package/quills/af4141/0.1.0/example.md +53 -0
  5. package/quills/afmc_moa/0.0.1/Quill.yaml +29 -45
  6. package/quills/afmc_moa/0.0.1/example.md +114 -0
  7. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/config.typ +1 -1
  8. package/quills/classic_resume/0.0.1/Quill.yaml +83 -163
  9. package/quills/classic_resume/0.0.1/example.md +111 -0
  10. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/components.typ +67 -34
  11. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/config.typ +18 -10
  12. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/layout.typ +36 -7
  13. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/lib.typ +1 -0
  14. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/util.typ +16 -11
  15. package/quills/classic_resume/0.0.1/plate.typ +54 -54
  16. package/quills/cyber_ribbon_chart/0.0.1/Quill.yaml +120 -113
  17. package/quills/cyber_ribbon_chart/0.0.1/example.md +55 -0
  18. package/quills/cyber_ribbon_chart/0.0.1/plate.typ +506 -325
  19. package/quills/daf1206/0.1.0/Quill.yaml +14 -16
  20. package/quills/daf1206/0.1.0/example.md +55 -0
  21. package/quills/daf4392/0.1.0/Quill.yaml +1 -6
  22. package/quills/daf4392/0.1.0/example.md +51 -0
  23. package/quills/usaf_letter/0.1.0/Quill.yaml +7 -42
  24. package/quills/usaf_letter/0.1.0/example.md +35 -0
  25. package/quills/{usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE → usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/OFL.txt} +2 -2
  26. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/SpectralSC-Italic.ttf +0 -0
  27. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/SpectralSC-Regular.ttf +0 -0
  28. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/frontmatter.typ +1 -1
  29. package/quills/usaf_letter/0.1.0/plate.typ +1 -4
  30. package/quills/usaf_memo/0.2.0/Quill.yaml +7 -40
  31. package/quills/usaf_memo/0.2.0/example.md +45 -0
  32. package/quills/usaf_memo/0.3.0/Quill.yaml +13 -50
  33. package/quills/usaf_memo/0.3.0/example.md +48 -0
  34. package/quills/{usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/Cinzel/LICENSE → usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/OFL.txt} +2 -2
  35. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/SpectralSC-Italic.ttf +0 -0
  36. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/SpectralSC-Regular.ttf +0 -0
  37. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +1 -1
  38. package/quills/usaf_memo/0.3.0/plate.typ +1 -4
  39. package/templates/deros_extension.md +40 -0
  40. package/templates/templates.json +7 -0
  41. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
  42. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
  43. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
  44. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
@@ -11,6 +11,7 @@ typst:
11
11
  # heading, optional prose under it, and the rows that section holds. The rows
12
12
  # are the section's own, so an entry cannot land under the wrong heading.
13
13
  main:
14
+ description: "Your name and contacts head the page. Each section under them is a card, printed in the order the cards stand: add a card for every section you want."
14
15
  ui:
15
16
  groups: [identity, page]
16
17
  # Everything prints under a section heading, so main carries no body: a
@@ -20,32 +21,17 @@ main:
20
21
  fields:
21
22
  name:
22
23
  type: string
23
- example: John Doe
24
24
  ui:
25
25
  group: identity
26
- description: "Your name, set large at the top. It is the document's level 1 heading, and becomes the exported PDF's title and author."
26
+ description: "Your name as you want it read, set large at the top. It also titles the exported PDF."
27
27
 
28
28
  contacts:
29
29
  type: array
30
30
  items:
31
31
  type: string
32
- example:
33
- - john.doe@example.com
34
- - (555) 123-4567
35
- - github.com/johndoe
36
- - linkedin.com/in/johndoe
37
- - Pittsburgh, PA
38
32
  ui:
39
33
  group: identity
40
- description: "The line under your name, one contact per line here, printed across the page separated by a diamond. Order them by how much you want them used."
41
-
42
- link_contacts:
43
- type: boolean
44
- default: true
45
- ui:
46
- group: identity
47
- compact: true
48
- description: "Turn contacts that look like an email address, a web address or a phone number into live links in the PDF. Anything else — a city, a clearance level — is printed as written."
34
+ description: "How to reach you, one per line and the one you want used first: an email address, a phone number, a profile such as linkedin.com/in/you, a city. They print in that order across the page under your name, a diamond between each. An email address, a web address or a phone number becomes a live link in the PDF."
49
35
 
50
36
  paper:
51
37
  type: enum
@@ -64,7 +50,7 @@ main:
64
50
  ui:
65
51
  group: page
66
52
  compact: true
67
- description: "Body text size in points; decimals allowed, such as 11.5. Along with the margin, this is the lever for fitting a resume onto one page — drop a half point before you drop a bullet."
53
+ description: "Body text size in points, such as 11 or 11.5. Along with the margin, this is the lever for fitting a resume onto one page — drop a half point before you drop a bullet."
68
54
 
69
55
  margin:
70
56
  type: number
@@ -75,51 +61,39 @@ main:
75
61
  description: "Page margin in inches, the same on all four sides. The layout is dense by design; below about 0.4 a printer starts clipping."
76
62
 
77
63
 
78
- # Each kind carries the same heading pair and optional prose under it. A blank
79
- # title prints the kind's usual heading, which the plate holds.
64
+ # Each kind opens with the heading it prints. Its `default:` is the usual one,
65
+ # so a new card shows the heading it will print, and an authored "" prints
66
+ # none.
80
67
  card_kinds:
81
68
  summary:
82
69
  title: Summary
83
- description: "A heading and prose under it. With a title of its own it is any section that is only prose — an objective, a statement of interest."
84
- ui:
85
- groups: [heading]
86
- body:
87
- example: |
88
- Security engineer of eight years, most of it on detection pipelines other people have to keep running.
70
+ description: "Prose under a heading: a few lines on who you are and the work you want next. Retitled, it is any section that is only prose, such as an objective."
71
+ seed:
72
+ $body: An engineer. Likes climbing rocks and making useful things.
89
73
  fields:
90
74
  title:
91
75
  type: plaintext
92
76
  inline: true
93
- default: ""
94
- ui:
95
- group: heading
96
- description: &title_description "Printed in uppercase over a rule. Leave blank for the usual heading: Summary, Work Experience, Education, Projects, Skills or Certifications."
97
- extra: &extra
98
- type: richtext
99
- inline: true
100
- default: ""
101
- ui:
102
- group: heading
103
- compact: true
104
- description: "Qualifier set after the title in the same style, such as a date range or a clarifier. Italicize with *asterisks*. Leave blank for the usual case."
77
+ default: Summary
78
+ description: "Printed in uppercase over a rule. An empty title prints no heading, which sets the summary straight under your name."
105
79
 
106
80
  experience:
107
81
  title: Experience
108
82
  description: "Jobs, most recent first: employer and dates on the first line, role and location on the second, details under them."
109
- ui:
110
- groups: [heading]
111
- body: &section_body
112
- # Empty is a heading straight over its rows, which is the usual section.
113
- example: ""
83
+ seed:
84
+ jobs:
85
+ - company: Employer
86
+ dates: Month YYYY – Present
87
+ role: Job Title
88
+ location: City, ST
89
+ details: |
90
+ - What you did, and what it changed.
114
91
  fields:
115
92
  title:
116
93
  type: plaintext
117
94
  inline: true
118
- default: ""
119
- ui:
120
- group: heading
121
- description: *title_description
122
- extra: *extra
95
+ default: Work Experience
96
+ description: &title_description "Printed in uppercase over a rule. An empty title prints no heading."
123
97
  jobs:
124
98
  type: array
125
99
  items:
@@ -128,56 +102,43 @@ card_kinds:
128
102
  company:
129
103
  type: plaintext
130
104
  inline: true
131
- example: Templar Archives Research Division
132
105
  description: "Set in bold at the left of the first line."
133
106
  dates: &dates
134
107
  type: plaintext
135
108
  inline: true
136
109
  default: ""
137
- example: August 2024 – Present
138
- description: "Set in bold at the right, opposite the name. Free text rather than a date, so a range reads as you write it: 'August 2024 – Present', 'December 2025'. Keep the form the same down the page."
110
+ description: "Set in bold at the right of the first line. Free text rather than a date, so a range reads as you write it: 'August 2024 – Present', 'December 2025'. Keep the form the same down the page."
139
111
  role:
140
112
  type: plaintext
141
113
  inline: true
142
114
  default: ""
143
- example: Psionic Research Analyst
144
115
  description: "Your title, set in italics under the employer. Leave blank to omit."
145
116
  location: &location
146
117
  type: plaintext
147
118
  inline: true
148
119
  default: ""
149
- example: Pittsburgh, PA
150
120
  description: "Set in italics at the right of the second line. 'Pittsburgh, PA', 'Remote'. Leave blank to omit; with the line's other half blank too, the second line is not printed."
151
121
  details: &details
152
122
  type: richtext
153
123
  default: ""
154
- example: |
155
- - What you did, and what came of it.
156
124
  description: "Set under the entry. Usually a bulleted list, one line each and strongest first; a sentence, *italics* and **bold** set as well, and a heading sets as a bold line. Leave blank for nothing under the entry."
157
- example:
158
- - company: Templar Archives Research Division
159
- dates: August 2024 – Present
160
- role: Psionic Research Analyst
161
- location: Aiur
162
- details: |
163
- - Analyzed Khala disruption patterns, developing countermeasures to protect neural link infrastructure.
164
125
  description: "One row per job. A row is never split across a page break."
165
126
 
166
127
  education:
167
128
  title: Education
168
129
  description: "Schools, most recent first: school and date on the first line, degree and location on the second."
169
- ui:
170
- groups: [heading]
171
- body: *section_body
130
+ seed:
131
+ schools:
132
+ - school: School
133
+ dates: Month YYYY
134
+ degree: Degree
135
+ location: City, ST
172
136
  fields:
173
137
  title:
174
138
  type: plaintext
175
139
  inline: true
176
- default: ""
177
- ui:
178
- group: heading
140
+ default: Education
179
141
  description: *title_description
180
- extra: *extra
181
142
  schools:
182
143
  type: array
183
144
  items:
@@ -186,83 +147,32 @@ card_kinds:
186
147
  school:
187
148
  type: plaintext
188
149
  inline: true
189
- example: Carnegie Mellon University
190
150
  description: "Set in bold at the left of the first line."
191
- dates:
192
- <<: *dates
193
- example: May 2024
151
+ dates: *dates
194
152
  degree:
195
153
  type: plaintext
196
154
  inline: true
197
155
  default: ""
198
- example: BS, Data Science
199
156
  description: "Degree and field, set in italics under the school. Leave blank to omit."
200
157
  location: *location
201
158
  details:
202
159
  <<: *details
203
- example: |
204
- - Distinguished Graduate (top 10%).
205
160
  description: "Honors, activities, coursework, set under the entry. Most degrees need none."
206
- example:
207
- - school: Carnegie Mellon University
208
- dates: December 2025
209
- degree: Master of Information Technology Strategy
210
- location: Pittsburgh, PA
211
161
  description: "One row per school or degree."
212
162
 
213
- projects:
214
- title: Projects
215
- description: "Projects: name and link on the first line, details under them."
216
- ui:
217
- groups: [heading]
218
- body: *section_body
219
- fields:
220
- title:
221
- type: plaintext
222
- inline: true
223
- default: ""
224
- ui:
225
- group: heading
226
- description: *title_description
227
- extra: *extra
228
- projects:
229
- type: array
230
- items:
231
- type: object
232
- properties:
233
- name:
234
- type: plaintext
235
- inline: true
236
- example: Quillmark
237
- description: "Set in bold at the left."
238
- url:
239
- type: string
240
- default: ""
241
- example: https://github.com/nibsbin/quillmark
242
- description: "Set small and italic at the right, and made a live link when it starts with http. Anything else is printed as written, so '<closed source>' stands in for a project with nothing to link. Leave blank to omit."
243
- details: *details
244
- example:
245
- - name: Quillmark
246
- url: https://github.com/nibsbin/quillmark
247
- details: |
248
- - Parameterization engine for typesetting documents from markdown.
249
- description: "One row per project."
250
-
251
163
  skills:
252
164
  title: Skills
253
- description: "Labelled lists of skills, filled across the columns and then down."
254
- ui:
255
- groups: [heading]
256
- body: *section_body
165
+ description: "Skills in labelled groups, filled across the columns and then down."
166
+ seed:
167
+ skills:
168
+ - label: Category
169
+ items: Skill, Skill, Skill
257
170
  fields:
258
171
  title:
259
172
  type: plaintext
260
173
  inline: true
261
- default: ""
262
- ui:
263
- group: heading
174
+ default: Skills
264
175
  description: *title_description
265
- extra: *extra
266
176
  skills:
267
177
  type: array
268
178
  ui:
@@ -273,19 +183,11 @@ card_kinds:
273
183
  label:
274
184
  type: plaintext
275
185
  inline: true
276
- example: Programming
277
- description: "Set in bold on its own line above the text."
278
- text:
186
+ description: "Set in bold, with its items on the line under it."
187
+ items:
279
188
  type: plaintext
280
189
  inline: true
281
- example: Python, R, JS, Rust, PowerShell, CI/CD
282
- title: Items
283
- description: "The items under that label, usually comma separated. Printed as written — markup is the one thing a labeled row does not carry."
284
- example:
285
- - label: Programming
286
- text: "Python, R, JS, C#, Rust, PowerShell, CI/CD"
287
- - label: Cloud
288
- text: "AWS EC2/S3, Helm, Docker, Serverless"
190
+ description: "Usually comma separated. Printed as written — markup is the one thing a labeled row does not carry."
289
191
  description: "One label and its items per row."
290
192
  columns: &columns
291
193
  type: integer
@@ -294,57 +196,79 @@ card_kinds:
294
196
  compact: true
295
197
  description: "Number of columns. Two suits items that run most of a line; three suits single words."
296
198
 
199
+ projects:
200
+ title: Projects
201
+ description: "Projects: name and link on the first line, details under them."
202
+ seed:
203
+ projects:
204
+ - name: Project
205
+ link: github.com/you/project
206
+ details: |
207
+ - What it does, and your part in it.
208
+ fields:
209
+ title:
210
+ type: plaintext
211
+ inline: true
212
+ default: Projects
213
+ description: *title_description
214
+ projects:
215
+ type: array
216
+ items:
217
+ type: object
218
+ properties:
219
+ name:
220
+ type: plaintext
221
+ inline: true
222
+ description: "Set in bold at the left."
223
+ link:
224
+ type: string
225
+ default: ""
226
+ description: "Where to see it, set small and italic at the right as written: github.com/you/project reads better than its https:// form. A web address becomes a live link; anything else, such as 'closed source', prints as written. Leave blank to omit."
227
+ details: *details
228
+ description: "One row per project."
229
+
297
230
  certifications:
298
231
  title: Certifications
299
- description: "A list of certifications, filled across the columns and then down."
300
- ui:
301
- groups: [heading]
302
- body: *section_body
232
+ description: "A list filled across the columns and then down. Retitled, it holds any short items: awards, languages, clearances."
233
+ seed:
234
+ items:
235
+ - Certification (ABBREVIATION)
303
236
  fields:
304
237
  title:
305
238
  type: plaintext
306
239
  inline: true
307
- default: ""
308
- ui:
309
- group: heading
240
+ default: Certifications
310
241
  description: *title_description
311
- extra: *extra
312
242
  items:
313
243
  type: array
314
244
  items:
315
245
  type: richtext
316
246
  inline: true
317
- example:
318
- - Offensive Security Certified Professional (OSCP)
319
- - GIAC Cyber Threat Intelligence (GCTI)
320
247
  description: "One item per line. Keep each to a line at the column width. Italicize with *asterisks*."
321
248
  columns: *columns
322
249
 
323
250
  other:
324
251
  title: Other section
325
252
  description: "A section the kinds above do not name — volunteer work, military service, publications, a competition. Rows take the Experience shape with neutral names."
326
- ui:
327
- groups: [heading]
328
- body: *section_body
253
+ seed:
254
+ title: Section Title
255
+ entries:
256
+ - heading: Entry
257
+ details: |
258
+ - What it was, and what came of it.
329
259
  fields:
330
260
  title:
331
261
  type: plaintext
332
262
  inline: true
333
- example: Cyber Competition
334
- ui:
335
- group: heading
336
- description: "Printed in uppercase over a rule. Other has no usual heading, so this one is yours to write."
337
- extra: *extra
263
+ description: "Printed in uppercase over a rule. This section has no usual heading, so this one is yours to write."
338
264
  entries:
339
265
  type: array
340
- default: []
341
266
  items:
342
267
  type: object
343
268
  properties:
344
269
  heading:
345
270
  type: plaintext
346
271
  inline: true
347
- example: 1st in NCX 2023
348
272
  description: "Set in bold at the left of the first line."
349
273
  dates: *dates
350
274
  subtitle:
@@ -354,8 +278,4 @@ card_kinds:
354
278
  description: "Set in italics under the heading. Leave blank to omit."
355
279
  location: *location
356
280
  details: *details
357
- example:
358
- - heading: 1st in NCX 2023
359
- details: |
360
- - Developed strategies, defensive scripts, and exploits for the Cyber Combat event.
361
281
  description: "One row per item. A section that is only prose is a Summary with a title of its own."
@@ -0,0 +1,111 @@
1
+ ~~~
2
+ $quill: classic_resume
3
+ $kind: main
4
+ name: John Doe
5
+ contacts:
6
+ - john.doe@example.com
7
+ - (555) 123-4567
8
+ - github.com/johndoe
9
+ - linkedin.com/in/johndoe
10
+ - Pittsburgh, PA
11
+ paper: us-letter
12
+ font_size: 11.5
13
+ margin: 0.5
14
+ ~~~
15
+
16
+ ~~~
17
+ $kind: summary
18
+ ~~~
19
+
20
+ An engineer. Likes climbing rocks and making useful things.
21
+
22
+ ~~~
23
+ $kind: certifications
24
+ title: Active Certifications
25
+ columns: 2
26
+ items:
27
+ - Offensive Security Certified Professional (OSCP)
28
+ - GIAC Cyber Threat Intelligence (GCTI)
29
+ - CompTIA CASP+, CySA+, Sec+, Net+, A+, Proj+
30
+ - GIAC Machine Learning Engineer (GMLE)
31
+ ~~~
32
+
33
+ ~~~
34
+ $kind: skills
35
+ columns: 2
36
+ skills:
37
+ - label: Programming
38
+ items: Python, R, JS, C#, Rust, PowerShell, CI/CD
39
+ - label: Data Science
40
+ items: ML/statistics, TensorFlow, AI Engineering
41
+ - label: IT & Cybersecurity
42
+ items: AD DS, Splunk, Metasploit, Wireshark, Nessus
43
+ - label: Cloud
44
+ items: AWS EC2/S3, Helm, Docker, Serverless
45
+ ~~~
46
+
47
+ ~~~
48
+ $kind: experience
49
+ jobs:
50
+ - company: Templar Archives Research Division
51
+ dates: August 2024 – Present
52
+ role: Psionic Research Analyst
53
+ location: Aiur
54
+ details: |
55
+ - Analyzed Khala disruption patterns following Amon's corruption, developing countermeasures to protect remaining neural link infrastructure.
56
+ - Building automated threat detection pipelines using Khaydarin crystal arrays to monitor Void energy signatures across the sector.
57
+ - company: Terran Dominion Ghost Academy
58
+ dates: May 2025 – July 2025
59
+ role: Covert Ops Trainee
60
+ location: Tarsonis (Remote)
61
+ details: |
62
+ - Developed tactical HUD displays for Ghost operatives integrating real-time Zerg hive cluster intelligence.
63
+ - company: Raynor's Raiders
64
+ dates: January 2018 – June 2020
65
+ role: Combat Engineer
66
+ location: Mar Sara
67
+ details: |
68
+ - Administered Hyperion shipboard systems, SCV maintenance protocols, and bunker defense automation for 30,000+ colonists.
69
+ ~~~
70
+
71
+ ~~~
72
+ $kind: education
73
+ schools:
74
+ - school: Carnegie Mellon University
75
+ dates: December 2025
76
+ degree: Master of Information Technology Strategy
77
+ location: Pittsburgh, PA
78
+ - school: United States Air Force Academy
79
+ dates: May 2024
80
+ degree: BS, Data Science
81
+ location: Colorado Springs, CO
82
+ details: |
83
+ - Distinguished Graduate (top 10%); Chinese language minor (L2+/R1 on DLPT).
84
+ ~~~
85
+
86
+ ~~~
87
+ $kind: other
88
+ title: Cyber Competition
89
+ entries:
90
+ - heading: 1st in SANS Academy Cup 2024
91
+ details: |
92
+ - Competed as the Delogrand Web Exploit SME, solving SQLi, API, and HTTP packet crafting problems.
93
+ - heading: 1st in NCX 2023
94
+ details: |
95
+ - Developed strategies, defensive scripts, and exploits for the Cyber Combat event.
96
+ - Analyzed logs with Bash and Python for the Data Analysis event.
97
+ ~~~
98
+
99
+ ~~~
100
+ $kind: projects
101
+ title: Selected Projects
102
+ projects:
103
+ - name: TongueToQuill
104
+ link: tonguetoquill.com
105
+ details: |
106
+ - Rich markdown editor for perfectly formatted USAF and USSF documents with Claude MCP integration.
107
+ - name: Quillmark
108
+ link: github.com/nibsbin/quillmark
109
+ details: |
110
+ - Parameterization engine for generating arbitrarily typesetted documents from markdown content.
111
+ ~~~
@@ -22,24 +22,56 @@
22
22
  }
23
23
 
24
24
  // Shared skeleton of `entry`: a grid of left/right aligned header cells
25
- // followed by an optional bulleted body, kept together on one page.
26
- #let _entry(cfg, cells, body) = {
27
- v(cfg.entry-spacing)
28
- block(breakable: false, {
29
- grid(columns: (1fr, auto), row-gutter: cfg.leading, ..cells)
25
+ // followed by an optional bulleted body, kept together on one page. The gutter
26
+ // keeps a left cell that wraps off the right one.
27
+ #let _entry(cfg, cells, body) = block(
28
+ above: cfg.leading + cfg.rule-spacing,
29
+ below: cfg.leading + cfg.entry-spacing,
30
+ breakable: false,
31
+ {
32
+ grid(columns: (1fr, auto), column-gutter: 1em, row-gutter: cfg.leading, ..cells)
30
33
  if body != none {
31
34
  _bulleted(cfg, body)
32
35
  }
33
- })
34
- }
36
+ },
37
+ )
38
+
39
+ // Sets the contacts on as few lines as hold them, a separator between two on
40
+ // one line. A line therefore breaks only between contacts, and never starts or
41
+ // ends on a separator. The spaces around the separator are real ones, so text
42
+ // copied out of the PDF keeps the contacts apart.
43
+ #let _contact-lines(cfg, contacts) = layout(region => {
44
+ // The separator's ink is centred on the x-height, where a middle dot sits,
45
+ // whatever glyph and size it is set in.
46
+ let glyph = text(size: cfg.contact-separator-size, cfg.contact-separator)
47
+ let extent(top, bottom, body) = measure(text(top-edge: top, bottom-edge: bottom, body)).height
48
+ let centre = extent("bounds", "baseline", glyph) - extent("bounds", "bounds", glyph) / 2
49
+ let separator = [ #box(
50
+ baseline: centre - extent("x-height", "baseline", [x]) / 2,
51
+ inset: (x: cfg.contact-separator-padding),
52
+ glyph,
53
+ ) ]
54
+ let lines = ()
55
+ let current = ()
56
+ for contact in contacts {
57
+ if current.len() > 0 and measure((..current, contact).join(separator)).width > region.width {
58
+ lines.push(current)
59
+ current = ()
60
+ }
61
+ current.push(contact)
62
+ }
63
+ lines.push(current)
64
+ lines.map(it => it.join(separator)).join(linebreak())
65
+ })
35
66
 
36
67
  /// Name and contact line at the top of the resume.
37
68
  ///
38
69
  /// The name becomes the level 1 heading of the document, and — unless `title`
39
- /// and `author` say otherwise — the title and author of the exported PDF.
40
- /// Contacts are separated by a diamond and, with `link-contacts` left on, email
41
- /// addresses, web addresses and phone numbers among them become links. Pass
42
- /// ready-made content instead of a string to opt a single contact out.
70
+ /// and `author` say otherwise — the title and author of the exported PDF. A
71
+ /// `none` or empty name prints no heading and leaves both unset. With
72
+ /// `link-contacts` left on, email addresses, web addresses and phone numbers
73
+ /// among the contacts become links. Pass ready-made content instead of a
74
+ /// string to opt a single contact out.
43
75
  #let resume-header(
44
76
  name: "",
45
77
  contacts: (),
@@ -48,28 +80,28 @@
48
80
  author: auto,
49
81
  ) = {
50
82
  // Only the metadata that resolves to something is set, so that passing
51
- // `none` leaves Typst's own default in place.
83
+ // `none` leaves Typst's own default in place. An author is text only.
52
84
  let metadata = (:)
53
85
  let title = if title == auto { name } else { title }
54
86
  let author = if author == auto { name } else { author }
55
87
  if title not in (none, "") { metadata.title = title }
56
- if author not in (none, "") { metadata.author = author }
88
+ if type(author) == str and author != "" { metadata.author = author }
57
89
  set document(..metadata)
58
90
 
59
- with-config(cfg => {
60
- heading(level: 1, name)
61
- v(cfg.entry-spacing)
62
-
63
- // A box rather than spaces, so that the padding is a real length and the
64
- // separator still cannot be split from the contacts on either side.
65
- let separator = box(
66
- inset: (x: cfg.contact-separator-padding),
67
- text(size: cfg.contact-separator-size, cfg.contact-separator),
68
- )
69
- contacts
70
- .map(contact => if link-contacts { auto-link(contact) } else { contact })
71
- .join(separator)
72
- })
91
+ let named = name not in (none, "")
92
+ if named or contacts.len() > 0 {
93
+ with-config(cfg => block(below: cfg.leading + cfg.header-spacing, {
94
+ if named {
95
+ heading(level: 1, name)
96
+ }
97
+ if contacts.len() > 0 {
98
+ block(
99
+ above: cfg.leading + cfg.entry-spacing,
100
+ _contact-lines(cfg, contacts.map(contact => if link-contacts { auto-link(contact) } else { contact })),
101
+ )
102
+ }
103
+ }))
104
+ }
73
105
  }
74
106
 
75
107
  /// Section title with a rule underneath it, such as `Work Experience`.
@@ -85,8 +117,9 @@
85
117
  ///
86
118
  /// `dated` puts `dates` opposite `heading` and an optional italic second line
87
119
  /// (`subtitle` / `location`). `linked` puts `url` opposite `heading`, small
88
- /// and italic, and drops the second line. `body` is ordinary Typst markup —
89
- /// most often a `- item` list, which picks up the square bullet of the template.
120
+ /// and italic, and drops the second line; a `url` given as a string becomes a
121
+ /// link where a contact would. `body` is ordinary Typst markup — most often a
122
+ /// `- item` list, which picks up the square bullet of the template.
90
123
  #let entry(
91
124
  heading: "",
92
125
  form: "dated",
@@ -109,7 +142,7 @@
109
142
  style: "italic",
110
143
  url,
111
144
  )
112
- if type(url) == str and url.starts-with("http") { link(url, label) } else { label }
145
+ auto-link(url, body: label)
113
146
  }
114
147
  (align(left, text(weight: "bold", heading)), align(right, annotation))
115
148
  } else {
@@ -163,8 +196,8 @@
163
196
  item
164
197
  }
165
198
 
166
- with-config(cfg => {
167
- v(cfg.entry-spacing)
199
+ with-config(cfg => block(
200
+ above: cfg.leading + cfg.rule-spacing,
168
201
  pad(
169
202
  // Line the items up with the bullets of the entries around them.
170
203
  left: cfg.marker-size + cfg.marker-indent,
@@ -176,6 +209,6 @@
176
209
  column-gutter: 1em,
177
210
  ..items.map(cell),
178
211
  ),
179
- )
180
- })
212
+ ),
213
+ ))
181
214
  }