@airmark/quiver 0.32.2 → 0.33.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 (76) hide show
  1. package/README.md +54 -18
  2. package/package.json +8 -6
  3. package/quills/af4141/0.1.0/Quill.yaml +1 -1
  4. package/quills/afmc_moa/0.0.1/Quill.yaml +10 -17
  5. package/quills/classic_resume/0.0.1/Quill.yaml +361 -0
  6. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/LICENSE +21 -0
  7. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Bold.ttf +0 -0
  8. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-BoldItalic.ttf +0 -0
  9. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Italic.ttf +0 -0
  10. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Regular.ttf +0 -0
  11. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/OFL.txt +94 -0
  12. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/components.typ +181 -0
  13. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/config.typ +53 -0
  14. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/layout.typ +57 -0
  15. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/lib.typ +6 -0
  16. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/util.typ +32 -0
  17. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/typst.toml +26 -0
  18. package/quills/classic_resume/0.0.1/plate.typ +143 -0
  19. package/quills/cyber_ribbon_chart/0.0.1/Quill.yaml +320 -0
  20. package/quills/cyber_ribbon_chart/0.0.1/plate.typ +428 -0
  21. package/quills/daf1206/0.1.0/Quill.yaml +1 -2
  22. package/quills/daf4392/0.1.0/Quill.yaml +15 -1
  23. package/quills/daf4392/0.1.0/plate.typ +2 -2
  24. package/quills/usaf_letter/0.1.0/Quill.yaml +214 -0
  25. package/quills/usaf_letter/0.1.0/assets/dod_seal.png +0 -0
  26. package/quills/usaf_letter/0.1.0/assets/dow_seal.png +0 -0
  27. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/.gitignore +7 -0
  28. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/LICENSE +201 -0
  29. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
  30. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/Cinzel/LICENSE +93 -0
  31. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/CopperplateCC-Bold.otf +0 -0
  32. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
  33. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/LICENSE.md +79 -0
  34. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LICENSE +102 -0
  35. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LiberationMono-Regular.ttf +0 -0
  36. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
  37. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
  38. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
  39. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
  40. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
  41. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/backmatter.typ +32 -0
  42. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/config.typ +45 -0
  43. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/frontmatter.typ +226 -0
  44. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/lib.typ +35 -0
  45. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/mainmatter.typ +140 -0
  46. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/primitives.typ +544 -0
  47. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/utils.typ +334 -0
  48. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/typst.toml +22 -0
  49. package/quills/usaf_letter/0.1.0/plate.typ +124 -0
  50. package/quills/usaf_memo/0.2.0/Quill.yaml +1 -1
  51. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
  52. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +1 -1
  53. package/quills/usaf_memo/0.3.0/Quill.yaml +62 -43
  54. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
  55. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
  56. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +29 -17
  57. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +97 -126
  58. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/config.typ +12 -45
  59. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +81 -87
  60. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +58 -66
  61. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/lib.typ +14 -36
  62. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +47 -19
  63. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +158 -111
  64. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +41 -141
  65. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/typst.toml +3 -8
  66. package/quills/usaf_memo/0.3.0/plate.typ +81 -85
  67. package/templates/af4141.md +23 -0
  68. package/templates/appointment_letter.md +58 -0
  69. package/templates/daf1206.md +23 -0
  70. package/templates/daf4392.md +34 -0
  71. package/templates/pass_request.md +48 -0
  72. package/templates/rebuttal.md +63 -0
  73. package/templates/templates.json +58 -0
  74. package/templates/usaf_template.md +30 -0
  75. package/templates/ussf_template.md +30 -0
  76. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel[wght].ttf +0 -0
@@ -0,0 +1,94 @@
1
+ Copyright 2017 The EB Garamond Project Authors (https://github.com/octaviopardo/EBGaramond12)
2
+ Copyright 2025 Deborah Khodanovich (RCS citation glyphs)
3
+
4
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
5
+ This license is copied below, and is also available with a FAQ at:
6
+ http://scripts.sil.org/OFL
7
+
8
+
9
+ -----------------------------------------------------------
10
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
11
+ -----------------------------------------------------------
12
+
13
+ PREAMBLE
14
+ The goals of the Open Font License (OFL) are to stimulate worldwide
15
+ development of collaborative font projects, to support the font creation
16
+ efforts of academic and linguistic communities, and to provide a free and
17
+ open framework in which fonts may be shared and improved in partnership
18
+ with others.
19
+
20
+ The OFL allows the licensed fonts to be used, studied, modified and
21
+ redistributed freely as long as they are not sold by themselves. The
22
+ fonts, including any derivative works, can be bundled, embedded,
23
+ redistributed and/or sold with any software provided that any reserved
24
+ names are not used by derivative works. The fonts and derivatives,
25
+ however, cannot be released under any other type of license. The
26
+ requirement for fonts to remain under this license does not apply
27
+ to any document created using the fonts or their derivatives.
28
+
29
+ DEFINITIONS
30
+ "Font Software" refers to the set of files released by the Copyright
31
+ Holder(s) under this license and clearly marked as such. This may
32
+ include source files, build scripts and documentation.
33
+
34
+ "Reserved Font Name" refers to any names specified as such after the
35
+ copyright statement(s).
36
+
37
+ "Original Version" refers to the collection of Font Software components as
38
+ distributed by the Copyright Holder(s).
39
+
40
+ "Modified Version" refers to any derivative made by adding to, deleting,
41
+ or substituting -- in part or in whole -- any of the components of the
42
+ Original Version, by changing formats or by porting the Font Software to a
43
+ new environment.
44
+
45
+ "Author" refers to any designer, engineer, programmer, technical
46
+ writer or other person who contributed to the Font Software.
47
+
48
+ PERMISSION & CONDITIONS
49
+ Permission is hereby granted, free of charge, to any person obtaining
50
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
51
+ redistribute, and sell modified and unmodified copies of the Font
52
+ Software, subject to the following conditions:
53
+
54
+ 1) Neither the Font Software nor any of its individual components,
55
+ in Original or Modified Versions, may be sold by itself.
56
+
57
+ 2) Original or Modified Versions of the Font Software may be bundled,
58
+ redistributed and/or sold with any software, provided that each copy
59
+ contains the above copyright notice and this license. These can be
60
+ included either as stand-alone text files, human-readable headers or
61
+ in the appropriate machine-readable metadata fields within text or
62
+ binary files as long as those fields can be easily viewed by the user.
63
+
64
+ 3) No Modified Version of the Font Software may use the Reserved Font
65
+ Name(s) unless explicit written permission is granted by the corresponding
66
+ Copyright Holder. This restriction only applies to the primary font name as
67
+ presented to the users.
68
+
69
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
70
+ Software shall not be used to promote, endorse or advertise any
71
+ Modified Version, except to acknowledge the contribution(s) of the
72
+ Copyright Holder(s) and the Author(s) or with their explicit written
73
+ permission.
74
+
75
+ 5) The Font Software, modified or unmodified, in part or in whole,
76
+ must be distributed entirely under this license, and must not be
77
+ distributed under any other license. The requirement for fonts to
78
+ remain under this license does not apply to any document created
79
+ using the Font Software.
80
+
81
+ TERMINATION
82
+ This license becomes null and void if any of the above conditions are
83
+ not met.
84
+
85
+ DISCLAIMER
86
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
87
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
88
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
89
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
90
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
91
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
92
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
93
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
94
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,181 @@
1
+ // components.typ
2
+ // The building blocks a resume is written with.
3
+
4
+ #import "config.typ": with-config
5
+ #import "util.typ": auto-link
6
+
7
+ // Restyles native `- item` lists to the square bullet of the template. It is a
8
+ // show rule rather than a custom list component so that the body of an entry
9
+ // stays ordinary Typst markup.
10
+ #let _bulleted(cfg, body) = {
11
+ show list: set list(
12
+ marker: box(
13
+ fill: black,
14
+ width: cfg.marker-size,
15
+ height: cfg.marker-size,
16
+ baseline: cfg.marker-baseline,
17
+ ),
18
+ body-indent: cfg.marker-indent,
19
+ indent: 0em,
20
+ )
21
+ body
22
+ }
23
+
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)
30
+ if body != none {
31
+ _bulleted(cfg, body)
32
+ }
33
+ })
34
+ }
35
+
36
+ /// Name and contact line at the top of the resume.
37
+ ///
38
+ /// 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.
43
+ #let resume-header(
44
+ name: "",
45
+ contacts: (),
46
+ link-contacts: true,
47
+ title: auto,
48
+ author: auto,
49
+ ) = {
50
+ // Only the metadata that resolves to something is set, so that passing
51
+ // `none` leaves Typst's own default in place.
52
+ let metadata = (:)
53
+ let title = if title == auto { name } else { title }
54
+ let author = if author == auto { name } else { author }
55
+ if title not in (none, "") { metadata.title = title }
56
+ if author not in (none, "") { metadata.author = author }
57
+ set document(..metadata)
58
+
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
+ })
73
+ }
74
+
75
+ /// Section title with a rule underneath it, such as `Work Experience`.
76
+ ///
77
+ /// `extra` is appended to the title in the same weight, for a qualifier like a
78
+ /// date range. The section becomes a level 2 heading, so `== Work Experience`
79
+ /// renders identically.
80
+ #let section-header(title, extra: none) = {
81
+ heading(level: 2, if extra == none { title } else { [#title #extra] })
82
+ }
83
+
84
+ /// A resume row: dated (a job, a degree, an award) or linked (a project).
85
+ ///
86
+ /// `dated` puts `dates` opposite `heading` and an optional italic second line
87
+ /// (`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.
90
+ #let entry(
91
+ heading: "",
92
+ form: "dated",
93
+ dates: none,
94
+ subtitle: none,
95
+ location: none,
96
+ url: none,
97
+ body: none,
98
+ ) = with-config(cfg => {
99
+ assert(
100
+ form in ("dated", "linked"),
101
+ message: "entry: `form` must be \"dated\" or \"linked\", found " + repr(form),
102
+ )
103
+
104
+ let cells = if form == "linked" {
105
+ let annotation = if url != none {
106
+ let label = text(
107
+ size: cfg.annotation-size,
108
+ font: if cfg.annotation-font == auto { cfg.font } else { cfg.annotation-font },
109
+ style: "italic",
110
+ url,
111
+ )
112
+ if type(url) == str and url.starts-with("http") { link(url, label) } else { label }
113
+ }
114
+ (align(left, text(weight: "bold", heading)), align(right, annotation))
115
+ } else {
116
+ let cells = (
117
+ align(left, text(weight: "bold", heading)),
118
+ align(right, text(weight: "bold", dates)),
119
+ )
120
+ if subtitle != none or location != none {
121
+ cells.push(align(left, text(style: "italic", subtitle)))
122
+ cells.push(align(right, text(style: "italic", location)))
123
+ }
124
+ cells
125
+ }
126
+
127
+ _entry(cfg, cells, body)
128
+ })
129
+
130
+ /// Multi-column list of short items, for certifications, skills or awards.
131
+ ///
132
+ /// Two shapes are accepted and told apart by the first item: a flat array of
133
+ /// content, or an array of `(label: .., text: ..)` dictionaries, which puts
134
+ /// the label in bold above its text.
135
+ #let item-grid(items: (), columns: 2) = {
136
+ if items.len() == 0 {
137
+ return
138
+ }
139
+ assert(
140
+ type(columns) == int and columns >= 1,
141
+ message: "item-grid: `columns` must be a positive integer, found " + repr(columns),
142
+ )
143
+
144
+ let labeled = type(items.at(0)) == dictionary and "label" in items.at(0)
145
+
146
+ let cell(item) = if labeled {
147
+ assert(
148
+ type(item) == dictionary and "label" in item and "text" in item,
149
+ message: "item-grid: every item must be a `(label: .., text: ..)` dictionary "
150
+ + "when the first one is, found " + repr(item),
151
+ )
152
+ block({
153
+ text(weight: "bold", item.label)
154
+ linebreak()
155
+ item.text
156
+ })
157
+ } else {
158
+ assert(
159
+ type(item) != dictionary,
160
+ message: "item-grid: a `(label: .., text: ..)` dictionary cannot be mixed with "
161
+ + "plain items, found " + repr(item),
162
+ )
163
+ item
164
+ }
165
+
166
+ with-config(cfg => {
167
+ v(cfg.entry-spacing)
168
+ pad(
169
+ // Line the items up with the bullets of the entries around them.
170
+ left: cfg.marker-size + cfg.marker-indent,
171
+ grid(
172
+ columns: (1fr,) * columns,
173
+ // Labeled items are two lines tall, so they need the extra gap to
174
+ // stay visually separated.
175
+ row-gutter: if labeled { cfg.leading + cfg.entry-spacing } else { cfg.leading },
176
+ column-gutter: 1em,
177
+ ..items.map(cell),
178
+ ),
179
+ )
180
+ })
181
+ }
@@ -0,0 +1,53 @@
1
+ // config.typ
2
+ // Defaults for the template, plus the state that carries them to components.
3
+ //
4
+ // `resume` stores the effective configuration in `config-state`; components
5
+ // read it back through `with-config`. That way a `#show: resume.with(..)`
6
+ // override reaches every component without threading arguments through each
7
+ // individual call.
8
+
9
+ #let default-config = (
10
+ // Body font. The first installed family wins, so the bundled EB Garamond is
11
+ // preferred while Typst's built-in serif faces act as a fallback.
12
+ font: ("EB Garamond", "Libertinus Serif", "New Computer Modern"),
13
+ size: 12pt,
14
+ paper: "us-letter",
15
+ margin: 0.5in,
16
+
17
+ // Vertical rhythm. `leading` is used for the leading inside a paragraph and
18
+ // for the gap between paragraphs, blocks and list items; the two `*-spacing`
19
+ // values are the extra breathing room above a section header and above an
20
+ // entry.
21
+ leading: 0.5em,
22
+ section-spacing: 5pt,
23
+ entry-spacing: 5pt,
24
+
25
+ // Resume header. The separator keeps its own padding so that the space
26
+ // around it does not change when its size does.
27
+ name-size: 18pt,
28
+ contact-separator: "❖",
29
+ contact-separator-size: 7pt,
30
+ contact-separator-padding: 0.5em,
31
+
32
+ // Rule drawn underneath a section header. The gap between the rule and the
33
+ // first entry of the section comes from that entry's `entry-spacing`.
34
+ rule-stroke: 0.75pt,
35
+
36
+ // Square bullet used by lists inside an entry. The negative baseline lifts
37
+ // the square off the baseline so that it lines up with the middle of the
38
+ // x-height instead of hanging below the line.
39
+ marker-size: 3.5pt,
40
+ marker-baseline: -0.07em,
41
+ marker-indent: 0.8em,
42
+
43
+ // Right-aligned annotation of a linked entry, normally a URL. A font of
44
+ // `auto` keeps the body font.
45
+ annotation-font: auto,
46
+ annotation-size: 8pt,
47
+ )
48
+
49
+ #let config-state = state("ttq-classic-resume:config", default-config)
50
+
51
+ // Runs `fn` with the active configuration. Components use this instead of
52
+ // reading `config-state` directly so that the `context` stays out of the way.
53
+ #let with-config(fn) = context fn(config-state.get())
@@ -0,0 +1,57 @@
1
+ // layout.typ
2
+ // The document-wide show rule.
3
+
4
+ #import "config.typ": config-state, default-config
5
+
6
+ /// Applies the resume styling to the whole document.
7
+ ///
8
+ /// Use `#show: resume` for the defaults, or `#show: resume.with(size: 11pt)`
9
+ /// to override any key of `default-config`.
10
+ #let resume(..options, body) = {
11
+ assert(
12
+ options.pos().len() == 0,
13
+ message: "resume: expected named options only, found "
14
+ + str(options.pos().len()) + " positional argument(s)",
15
+ )
16
+ let unknown = options.named().keys().filter(key => key not in default-config)
17
+ assert(
18
+ unknown.len() == 0,
19
+ message: "resume: unknown option(s) " + unknown.map(repr).join(", ")
20
+ + "; expected one of " + default-config.keys().map(repr).join(", "),
21
+ )
22
+
23
+ let cfg = default-config + options.named()
24
+
25
+ set page(paper: cfg.paper, margin: cfg.margin)
26
+ set text(font: cfg.font, size: cfg.size)
27
+ set par(leading: cfg.leading, spacing: cfg.leading, justify: false)
28
+
29
+ // One vertical rhythm for every kind of block-level content.
30
+ set block(above: cfg.leading, below: cfg.leading)
31
+ set list(spacing: cfg.leading)
32
+
33
+ // Hyperlinks are styled like the surrounding text: on paper the colour is
34
+ // noise, and the link is still live in the PDF.
35
+ show link: set text(fill: black)
36
+
37
+ // Headings carry the document structure into the tagged PDF. The resume look
38
+ // is applied here rather than inside the components, so that plain `=` and
39
+ // `==` markup renders the same way: the name is the level 1 heading, a
40
+ // section title the level 2 one. Both are set apart by weight and case
41
+ // rather than by size.
42
+ set heading(numbering: none)
43
+ show heading: set text(size: cfg.size, weight: "bold")
44
+ show heading: set block(above: cfg.leading, below: cfg.leading)
45
+ // The name is enlarged inside the block, not on the heading itself, so that
46
+ // the surrounding `em` spacing keeps resolving against the body size.
47
+ show heading.where(level: 1): it => block(text(size: cfg.name-size, it.body))
48
+ show heading.where(level: 2): it => {
49
+ v(cfg.section-spacing)
50
+ block(upper(it.body))
51
+ block(above: cfg.leading, line(length: 100%, stroke: cfg.rule-stroke))
52
+ }
53
+
54
+ config-state.update(cfg)
55
+
56
+ body
57
+ }
@@ -0,0 +1,6 @@
1
+ // lib.typ
2
+ // Public API of the classic resume template.
3
+
4
+ #import "config.typ": default-config
5
+ #import "layout.typ": resume
6
+ #import "components.typ": entry, item-grid, resume-header, section-header
@@ -0,0 +1,32 @@
1
+ // util.typ
2
+ // Helpers shared by the components.
3
+
4
+ // A contact line usually holds an email address, a profile link, a phone
5
+ // number and a city. Only the first three are worth turning into a link, and
6
+ // they are distinguishable by shape.
7
+ #let _email = regex("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")
8
+ #let _scheme = regex("^(https?://|www\\.)\\S+$")
9
+ #let _domain = regex("^[\\w-]+(\\.[\\w-]+)*\\.[A-Za-z]{2,24}(/\\S*)?$")
10
+ #let _phone = regex("^[+(\\d][\\d\\s()./+-]*\\d$")
11
+ #let _digit = regex("\\d")
12
+
13
+ // Turns a contact into a link when it looks like an email address, a web
14
+ // address or a phone number, and returns it unchanged otherwise. A city or a
15
+ // clearance level is therefore left alone, and so is anything that is already
16
+ // content, so callers can always pass their own `link(..)` instead.
17
+ #let auto-link(contact) = {
18
+ if type(contact) != str {
19
+ contact
20
+ } else if contact.match(_email) != none {
21
+ link("mailto:" + contact, contact)
22
+ } else if contact.match(_scheme) != none {
23
+ let href = if contact.starts-with("www.") { "https://" + contact } else { contact }
24
+ link(href, contact)
25
+ } else if contact.match(_domain) != none {
26
+ link("https://" + contact, contact)
27
+ } else if contact.match(_phone) != none and contact.matches(_digit).len() >= 7 {
28
+ link("tel:" + contact.replace(regex("[^\\d+]"), ""), contact)
29
+ } else {
30
+ contact
31
+ }
32
+ }
@@ -0,0 +1,26 @@
1
+ [package]
2
+ name = "ttq-classic-resume"
3
+ version = "0.1.0"
4
+ compiler = "0.14.0"
5
+ entrypoint = "src/lib.typ"
6
+ repository = "https://github.com/nibsbin/ttq-classic-resume"
7
+ authors = ["Nibs"]
8
+ license = "MIT"
9
+ description = "A clean and modern resume."
10
+
11
+ keywords = [
12
+ "resume",
13
+ "cv",
14
+ "curriculum vitae",
15
+ "job",
16
+ "career",
17
+ "professional",
18
+ "template",
19
+ ]
20
+ categories = ["cv"]
21
+ disciplines = ["business", "computer-science", "engineering", "communication"]
22
+
23
+ [template]
24
+ path = "template"
25
+ entrypoint = "resume.typ"
26
+ thumbnail = "thumbnail.png"
@@ -0,0 +1,143 @@
1
+ #import "@local/quillmark-helper:0.1.0": data
2
+ #import "@local/ttq-classic-resume:0.1.0": (
3
+ default-config, entry, item-grid, resume, resume-header, section-header,
4
+ )
5
+
6
+ // `plaintext`/`richtext` fields lower to content carrying a space element on
7
+ // each side, so a blank one is not `[]` and a filled one would print its
8
+ // padding inside a bold run or an italic one.
9
+ #let trim-inline(v) = {
10
+ if type(v) != content { return v }
11
+ let kids = v.at("children", default: none)
12
+ if kids == none { return v }
13
+ let padding(c) = c == [ ] or c.func() == parbreak
14
+ while kids.len() > 0 and padding(kids.first()) { kids = kids.slice(1) }
15
+ while kids.len() > 0 and padding(kids.last()) { kids = kids.slice(0, -1) }
16
+ kids.sum(default: [])
17
+ }
18
+
19
+ // `none` for a field the author left blank, so the components can drop what it
20
+ // would have occupied: a dated `entry` omits its whole second line when both
21
+ // halves are none, a linked one its annotation, an entry what is under it, and
22
+ // a section its heading.
23
+ #let or-none(v) = {
24
+ let trimmed = trim-inline(v)
25
+ if trimmed == [] or trimmed == "" { none } else { trimmed }
26
+ }
27
+
28
+ // The quillmark helper leaves an unset or whitespace-only markdown body as the
29
+ // empty string; only a non-empty body is eval'd into content.
30
+ #let body-of(card) = {
31
+ let body = card.at("$body", default: "")
32
+ if type(body) == str { none } else { body }
33
+ }
34
+
35
+ #let stock-title = (
36
+ summary: "Summary",
37
+ experience: "Work Experience",
38
+ education: "Education",
39
+ projects: "Projects",
40
+ skills: "Skills",
41
+ certifications: "Certifications",
42
+ )
43
+
44
+ #show: resume.with(
45
+ // An enum's blank is authorable even where the schema declares a default.
46
+ paper: if data.paper != "" { data.paper } else { "us-letter" },
47
+ size: data.font_size * 1pt,
48
+ margin: data.margin * 1in,
49
+
50
+ // The package's stack names two families to fall back to; only the one this
51
+ // quill bundles is in the render's font book, and naming the absent two
52
+ // warns once per render.
53
+ font: "EB Garamond",
54
+ // EB Garamond has no ❖ (U+2756), the package's default separator: it prints
55
+ // only where a second family is installed to lend the glyph, and draws from
56
+ // that family rather than this one. ◆ is the nearest diamond EB Garamond
57
+ // carries itself, and wants a point less to weigh the same.
58
+ contact-separator: "◆",
59
+ contact-separator-size: 6pt,
60
+ )
61
+
62
+ // The package restyles lists to its square bullet inside an entry only. A
63
+ // section body is the other place a resume holds one, and a list that changed
64
+ // marker with its surroundings would read as two templates.
65
+ #show list: set list(
66
+ marker: box(
67
+ fill: black,
68
+ width: default-config.marker-size,
69
+ height: default-config.marker-size,
70
+ baseline: default-config.marker-baseline,
71
+ ),
72
+ body-indent: default-config.marker-indent,
73
+ indent: 0em,
74
+ )
75
+
76
+ // `name` and `contacts` are read as `string` rather than `plaintext`, which
77
+ // lowers to content: the name reaches `set document(author:)`, which takes no
78
+ // content, and `link-contacts` linkifies a str while passing content through.
79
+ #resume-header(
80
+ name: data.name,
81
+ contacts: data.contacts,
82
+ link-contacts: data.link_contacts,
83
+ )
84
+
85
+ // The section titles are the PDF's outline, and a `details` cell cannot decline a
86
+ // heading as a body does, so one written there sets as a bold line outside it.
87
+ #let under-entry(details) = {
88
+ let body = or-none(details)
89
+ if body == none { return none }
90
+ set heading(outlined: false, bookmarked: false)
91
+ show heading: it => block(strong(it.body))
92
+ body
93
+ }
94
+
95
+ #let dated(heading, dates, subtitle, location, details) = entry(
96
+ heading: trim-inline(heading),
97
+ form: "dated",
98
+ dates: trim-inline(dates),
99
+ subtitle: or-none(subtitle),
100
+ location: or-none(location),
101
+ body: under-entry(details),
102
+ )
103
+
104
+ #for card in data.at("$cards") {
105
+ let kind = card.at("$kind", default: none)
106
+ let title = or-none(card.title)
107
+ if title == none { title = stock-title.at(kind, default: none) }
108
+ let extra = or-none(card.extra)
109
+ if title != none or extra != none {
110
+ section-header(if title != none { title } else { [] }, extra: extra)
111
+ }
112
+ body-of(card)
113
+
114
+ if kind == "experience" {
115
+ for job in card.jobs {
116
+ dated(job.company, job.dates, job.role, job.location, job.details)
117
+ }
118
+ } else if kind == "education" {
119
+ for school in card.schools {
120
+ dated(school.school, school.dates, school.degree, school.location, school.details)
121
+ }
122
+ } else if kind == "other" {
123
+ for row in card.entries {
124
+ dated(row.heading, row.dates, row.subtitle, row.location, row.details)
125
+ }
126
+ } else if kind == "projects" {
127
+ for project in card.projects {
128
+ entry(
129
+ heading: trim-inline(project.name),
130
+ form: "linked",
131
+ url: if project.url != "" { project.url } else { none },
132
+ body: under-entry(project.details),
133
+ )
134
+ }
135
+ } else if kind == "skills" {
136
+ item-grid(
137
+ items: card.skills.map(row => (label: trim-inline(row.label), text: trim-inline(row.text))),
138
+ columns: calc.max(1, card.columns),
139
+ )
140
+ } else if kind == "certifications" {
141
+ item-grid(items: card.items.map(trim-inline), columns: calc.max(1, card.columns))
142
+ }
143
+ }