@airmark/quiver 0.34.0 → 0.35.1
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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/quills/classic_resume/{0.0.1 → 0.1.0}/Quill.yaml +11 -11
- package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/src/components.typ +21 -12
- package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/src/config.typ +6 -5
- package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/src/layout.typ +3 -2
- package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/src/util.typ +17 -4
- package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/typst.toml +0 -5
- package/quills/classic_resume/{0.0.1 → 0.1.0}/plate.typ +16 -9
- package/quills/{cyber_ribbon_chart/0.0.1 → cyber_career_plan/0.1.0}/Quill.yaml +8 -7
- package/quills/{cyber_ribbon_chart/0.0.1 → cyber_career_plan/0.1.0}/example.md +1 -1
- package/quills/{cyber_ribbon_chart/0.0.1 → cyber_career_plan/0.1.0}/plate.typ +127 -55
- package/templates/templates.json +0 -7
- package/templates/deros_extension.md +0 -40
- /package/quills/classic_resume/{0.0.1 → 0.1.0}/example.md +0 -0
- /package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/LICENSE +0 -0
- /package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Bold.ttf +0 -0
- /package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-BoldItalic.ttf +0 -0
- /package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Italic.ttf +0 -0
- /package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Regular.ttf +0 -0
- /package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/fonts/EBGaramond/OFL.txt +0 -0
- /package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/src/lib.typ +0 -0
package/README.md
CHANGED
|
@@ -16,8 +16,8 @@ and aligned to the current Quillmark spec.
|
|
|
16
16
|
| `daf1206` | 0.1.0 | DAF Form 1206 — Nomination for Award |
|
|
17
17
|
| `daf4392` | 0.1.0 | DAF Form 4392 — Pre-Departure Safety Briefing (Page 2) |
|
|
18
18
|
| `afmc_moa` | 0.0.1 | DoD Memorandum of Agreement (DoDI 4000.19) |
|
|
19
|
-
| `classic_resume` | 0.0
|
|
20
|
-
| `
|
|
19
|
+
| `classic_resume` | 0.1.0 | Classic single-page resume |
|
|
20
|
+
| `cyber_career_plan` | 0.1.0 | 17X Cyber Operations Officer career plan: current assignment, vectors and record |
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
quill:
|
|
2
2
|
name: classic_resume
|
|
3
|
-
version: 0.0
|
|
3
|
+
version: 0.1.0
|
|
4
4
|
backend: typst
|
|
5
5
|
description: Classic single-page resume
|
|
6
6
|
|
|
@@ -75,11 +75,11 @@ card_kinds:
|
|
|
75
75
|
type: plaintext
|
|
76
76
|
inline: true
|
|
77
77
|
default: Summary
|
|
78
|
-
description: "Printed in uppercase over a rule. An empty title prints no heading,
|
|
78
|
+
description: "Printed in uppercase over a rule. An empty title prints no heading, so a summary that is the first card sits straight under your name."
|
|
79
79
|
|
|
80
80
|
experience:
|
|
81
81
|
title: Experience
|
|
82
|
-
description: "Jobs, most recent first: employer and dates on the first line, role and location on the second, details under them."
|
|
82
|
+
description: "Jobs, most recent first: employer and dates on the first line, role and location on the second, details under them. Prose written under the card prints under the heading, ahead of the jobs."
|
|
83
83
|
seed:
|
|
84
84
|
jobs:
|
|
85
85
|
- company: Employer
|
|
@@ -122,11 +122,11 @@ card_kinds:
|
|
|
122
122
|
type: richtext
|
|
123
123
|
default: ""
|
|
124
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."
|
|
125
|
-
description: "One row per job. A row is
|
|
125
|
+
description: "One row per job. A row is kept to one page, unless it is taller than a page."
|
|
126
126
|
|
|
127
127
|
education:
|
|
128
128
|
title: Education
|
|
129
|
-
description: "Schools, most recent first: school and date on the first line, degree and location on the second."
|
|
129
|
+
description: "Schools, most recent first: school and date on the first line, degree and location on the second. Prose written under the card prints under the heading, ahead of the schools."
|
|
130
130
|
seed:
|
|
131
131
|
schools:
|
|
132
132
|
- school: School
|
|
@@ -162,7 +162,7 @@ card_kinds:
|
|
|
162
162
|
|
|
163
163
|
skills:
|
|
164
164
|
title: Skills
|
|
165
|
-
description: "Skills in labelled groups, filled across the columns and then down."
|
|
165
|
+
description: "Skills in labelled groups, filled across the columns and then down. Prose written under the card prints under the heading, ahead of the groups."
|
|
166
166
|
seed:
|
|
167
167
|
skills:
|
|
168
168
|
- label: Category
|
|
@@ -183,7 +183,7 @@ card_kinds:
|
|
|
183
183
|
label:
|
|
184
184
|
type: plaintext
|
|
185
185
|
inline: true
|
|
186
|
-
description: "Set in bold, with its items on the line under it."
|
|
186
|
+
description: "Set in bold, with its items on the line under it. Leave blank to print the items alone."
|
|
187
187
|
items:
|
|
188
188
|
type: plaintext
|
|
189
189
|
inline: true
|
|
@@ -198,7 +198,7 @@ card_kinds:
|
|
|
198
198
|
|
|
199
199
|
projects:
|
|
200
200
|
title: Projects
|
|
201
|
-
description: "Projects: name and link on the first line, details under them."
|
|
201
|
+
description: "Projects: name and link on the first line, details under them. Prose written under the card prints under the heading, ahead of the projects."
|
|
202
202
|
seed:
|
|
203
203
|
projects:
|
|
204
204
|
- name: Project
|
|
@@ -223,13 +223,13 @@ card_kinds:
|
|
|
223
223
|
link:
|
|
224
224
|
type: string
|
|
225
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."
|
|
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, an email address or a phone number becomes a live link; anything else, such as 'closed source', prints as written. Leave blank to omit."
|
|
227
227
|
details: *details
|
|
228
228
|
description: "One row per project."
|
|
229
229
|
|
|
230
230
|
certifications:
|
|
231
231
|
title: Certifications
|
|
232
|
-
description: "A list filled across the columns and then down. Retitled, it holds any short items: awards, languages, clearances."
|
|
232
|
+
description: "A list filled across the columns and then down. Retitled, it holds any short items: awards, languages, clearances. Prose written under the card prints under the heading, ahead of the list."
|
|
233
233
|
seed:
|
|
234
234
|
items:
|
|
235
235
|
- Certification (ABBREVIATION)
|
|
@@ -249,7 +249,7 @@ card_kinds:
|
|
|
249
249
|
|
|
250
250
|
other:
|
|
251
251
|
title: Other section
|
|
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."
|
|
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. Prose written under the card prints under the heading, ahead of the rows."
|
|
253
253
|
seed:
|
|
254
254
|
title: Section Title
|
|
255
255
|
entries:
|
package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/src/components.typ
RENAMED
|
@@ -23,18 +23,25 @@
|
|
|
23
23
|
|
|
24
24
|
// Shared skeleton of `entry`: a grid of left/right aligned header cells
|
|
25
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{
|
|
26
|
+
// keeps a left cell that wraps off the right one. An entry taller than a page
|
|
27
|
+
// cannot be kept together, so it breaks where it must rather than running off
|
|
28
|
+
// the foot of one.
|
|
29
|
+
#let _entry(cfg, cells, body) = {
|
|
30
|
+
let inner = {
|
|
32
31
|
grid(columns: (1fr, auto), column-gutter: 1em, row-gutter: cfg.leading, ..cells)
|
|
33
32
|
if body != none {
|
|
34
33
|
_bulleted(cfg, body)
|
|
35
34
|
}
|
|
36
|
-
}
|
|
37
|
-
|
|
35
|
+
}
|
|
36
|
+
block(
|
|
37
|
+
above: cfg.leading + cfg.rule-spacing,
|
|
38
|
+
below: cfg.leading + cfg.entry-spacing,
|
|
39
|
+
layout(region => block(
|
|
40
|
+
breakable: measure(block(width: region.width, inner)).height > region.height,
|
|
41
|
+
inner,
|
|
42
|
+
)),
|
|
43
|
+
)
|
|
44
|
+
}
|
|
38
45
|
|
|
39
46
|
// Sets the contacts on as few lines as hold them, a separator between two on
|
|
40
47
|
// one line. A line therefore breaks only between contacts, and never starts or
|
|
@@ -92,7 +99,7 @@
|
|
|
92
99
|
if named or contacts.len() > 0 {
|
|
93
100
|
with-config(cfg => block(below: cfg.leading + cfg.header-spacing, {
|
|
94
101
|
if named {
|
|
95
|
-
heading(level: 1, name)
|
|
102
|
+
[#heading(level: 1, name) <ttq-classic-resume-name>]
|
|
96
103
|
}
|
|
97
104
|
if contacts.len() > 0 {
|
|
98
105
|
block(
|
|
@@ -164,7 +171,7 @@
|
|
|
164
171
|
///
|
|
165
172
|
/// Two shapes are accepted and told apart by the first item: a flat array of
|
|
166
173
|
/// content, or an array of `(label: .., text: ..)` dictionaries, which puts
|
|
167
|
-
/// the label in bold above its text.
|
|
174
|
+
/// the label in bold above its text. A `none` or empty label prints no line.
|
|
168
175
|
#let item-grid(items: (), columns: 2) = {
|
|
169
176
|
if items.len() == 0 {
|
|
170
177
|
return
|
|
@@ -183,8 +190,10 @@
|
|
|
183
190
|
+ "when the first one is, found " + repr(item),
|
|
184
191
|
)
|
|
185
192
|
block({
|
|
186
|
-
|
|
187
|
-
|
|
193
|
+
if item.label not in (none, "", []) {
|
|
194
|
+
text(weight: "bold", item.label)
|
|
195
|
+
linebreak()
|
|
196
|
+
}
|
|
188
197
|
item.text
|
|
189
198
|
})
|
|
190
199
|
} else {
|
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
|
|
29
29
|
// Resume header. The separator stands between two contacts on one line,
|
|
30
30
|
// never at either end of one, with a word space and this padding on each
|
|
31
|
-
// side of it.
|
|
32
|
-
|
|
31
|
+
// side of it. Sizes in `em` follow the body size: at 12pt, the name is set
|
|
32
|
+
// at 18pt and the separator at 7pt.
|
|
33
|
+
name-size: 1.5em,
|
|
33
34
|
contact-separator: "❖",
|
|
34
|
-
contact-separator-size:
|
|
35
|
+
contact-separator-size: 7em / 12,
|
|
35
36
|
contact-separator-padding: 0.25em,
|
|
36
37
|
|
|
37
38
|
// Rule drawn underneath a section header.
|
|
@@ -39,8 +40,8 @@
|
|
|
39
40
|
|
|
40
41
|
// Square bullet used by lists inside an entry. The negative baseline lifts
|
|
41
42
|
// the square off the baseline so that it lines up with the middle of the
|
|
42
|
-
// x-height instead of hanging below the line.
|
|
43
|
-
marker-size: 3.
|
|
43
|
+
// x-height instead of hanging below the line. At 12pt the square is 3.5pt.
|
|
44
|
+
marker-size: 3.5em / 12,
|
|
44
45
|
marker-baseline: -0.07em,
|
|
45
46
|
marker-indent: 0.8em,
|
|
46
47
|
|
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
// The name at the left and the page count at the right, over every page after
|
|
7
7
|
// the first, so a page read apart from the first still says whose it is. The
|
|
8
|
-
// name is read back off the
|
|
8
|
+
// name is read back off the heading `resume-header` labels, so a level 1
|
|
9
|
+
// heading written in the body never stands in for it. Small and italic, it
|
|
9
10
|
// reads as a running head: a 0.5in margin leaves no room to set it further off
|
|
10
11
|
// the page.
|
|
11
12
|
#let _continuation-header(cfg) = context {
|
|
12
13
|
if counter(page).get().first() > 1 {
|
|
13
14
|
set text(size: cfg.annotation-size, style: "italic")
|
|
14
|
-
let names = query(
|
|
15
|
+
let names = query(<ttq-classic-resume-name>)
|
|
15
16
|
if names.len() > 0 { names.first().body }
|
|
16
17
|
h(1fr)
|
|
17
18
|
counter(page).display("1 of 1", both: true)
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
// A contact line usually holds an email address, a profile link, a phone
|
|
5
5
|
// number and a city. Only the first three are worth turning into a link, and
|
|
6
6
|
// they are distinguishable by shape.
|
|
7
|
+
#let _mailto = regex("^(?i:mailto):\\S+$")
|
|
8
|
+
#let _tel = regex("^(?i:tel):[+(\\d][\\d\\s()./+-]*\\d$")
|
|
7
9
|
#let _email = regex("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")
|
|
8
|
-
#let _scheme = regex("^(https?://|www\\.)\\S+$")
|
|
10
|
+
#let _scheme = regex("^(?i:https?://|www\\.)\\S+$")
|
|
9
11
|
#let _domain = regex("^[\\w-]+(\\.[\\w-]+)*\\.[A-Za-z]{2,24}(/\\S*)?$")
|
|
10
12
|
#let _phone = regex("^[+(\\d][\\d\\s()./+-]*\\d$")
|
|
11
13
|
#let _digit = regex("\\d")
|
|
@@ -13,7 +15,9 @@
|
|
|
13
15
|
/// Turns a contact into a link when it looks like an email address, a web
|
|
14
16
|
/// address or a phone number, and returns it unchanged otherwise. A city or a
|
|
15
17
|
/// clearance level is therefore left alone, and so is anything that is already
|
|
16
|
-
/// content, so callers can always pass their own `link(..)` instead.
|
|
18
|
+
/// content, so callers can always pass their own `link(..)` instead. One
|
|
19
|
+
/// already written as a `mailto:`, `tel:` or `http(s)://` link keeps its
|
|
20
|
+
/// target, whatever case its scheme is written in.
|
|
17
21
|
///
|
|
18
22
|
/// `body` is what prints, the contact itself unless given: a caller holding the
|
|
19
23
|
/// contact as text and as content of its own reads the one and prints the
|
|
@@ -22,11 +26,20 @@
|
|
|
22
26
|
let body = if body == auto { contact } else { body }
|
|
23
27
|
if type(contact) != str {
|
|
24
28
|
body
|
|
29
|
+
} else if contact.match(_mailto) != none {
|
|
30
|
+
link("mailto:" + contact.slice(7), body)
|
|
31
|
+
} else if contact.match(_tel) != none {
|
|
32
|
+
link("tel:" + contact.slice(4).replace(regex("[^\\d+]"), ""), body)
|
|
25
33
|
} else if contact.match(_email) != none {
|
|
26
34
|
link("mailto:" + contact, body)
|
|
27
35
|
} else if contact.match(_scheme) != none {
|
|
28
|
-
let
|
|
29
|
-
|
|
36
|
+
let (scheme, rest) = if lower(contact).starts-with("www.") {
|
|
37
|
+
("https://", contact)
|
|
38
|
+
} else {
|
|
39
|
+
let split = contact.position("://") + 3
|
|
40
|
+
(lower(contact.slice(0, split)), contact.slice(split))
|
|
41
|
+
}
|
|
42
|
+
link(scheme + rest, body)
|
|
30
43
|
} else if contact.match(_domain) != none {
|
|
31
44
|
link("https://" + contact, body)
|
|
32
45
|
} else if contact.match(_phone) != none and contact.matches(_digit).len() >= 7 {
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
// no content. What prints is the field's ink twin, `printed`, which keeps the
|
|
38
38
|
// click target a value computed with loses.
|
|
39
39
|
#let linked(value, printed) = {
|
|
40
|
-
|
|
40
|
+
let value = value.trim()
|
|
41
|
+
if value == "" { none } else { auto-link(value, body: printed) }
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
#show: resume.with(
|
|
@@ -53,9 +54,9 @@
|
|
|
53
54
|
// EB Garamond has no ❖ (U+2756), the package's default separator: it prints
|
|
54
55
|
// only where a second family is installed to lend the glyph, and draws from
|
|
55
56
|
// that family rather than this one. ◆ is the nearest diamond EB Garamond
|
|
56
|
-
// carries itself, and wants a point less to weigh the same.
|
|
57
|
+
// carries itself, and wants a point less to weigh the same: 6pt at 12pt.
|
|
57
58
|
contact-separator: "◆",
|
|
58
|
-
contact-separator-size:
|
|
59
|
+
contact-separator-size: 0.5em,
|
|
59
60
|
)
|
|
60
61
|
|
|
61
62
|
// The package restyles lists to its square bullet inside an entry only. A
|
|
@@ -100,22 +101,28 @@
|
|
|
100
101
|
body: under-entry(details),
|
|
101
102
|
)
|
|
102
103
|
|
|
104
|
+
// The rows an author filled in. A row left wholly blank, as a new one is, would
|
|
105
|
+
// print as an empty gap.
|
|
106
|
+
#let filled(rows) = rows.filter(row => row.values().any(value => {
|
|
107
|
+
or-none(if type(value) == str { value.trim() } else { value }) != none
|
|
108
|
+
}))
|
|
109
|
+
|
|
103
110
|
// What each declared kind sets under its heading and body. A card of a kind
|
|
104
111
|
// the quill does not declare is warned on and left off the page: its fields
|
|
105
112
|
// are not this quill's to read.
|
|
106
113
|
#let rows = (
|
|
107
114
|
summary: card => none,
|
|
108
|
-
experience: card => for job in card.jobs {
|
|
115
|
+
experience: card => for job in filled(card.jobs) {
|
|
109
116
|
dated(job.company, job.dates, job.role, job.location, job.details)
|
|
110
117
|
},
|
|
111
|
-
education: card => for school in card.schools {
|
|
118
|
+
education: card => for school in filled(card.schools) {
|
|
112
119
|
dated(school.school, school.dates, school.degree, school.location, school.details)
|
|
113
120
|
},
|
|
114
121
|
skills: card => item-grid(
|
|
115
|
-
items: card.skills.map(row => (label:
|
|
122
|
+
items: filled(card.skills).map(row => (label: or-none(row.label), text: trim-inline(row.items))),
|
|
116
123
|
columns: calc.max(1, card.columns),
|
|
117
124
|
),
|
|
118
|
-
projects: card => for project in card.projects {
|
|
125
|
+
projects: card => for project in filled(card.projects) {
|
|
119
126
|
entry(
|
|
120
127
|
heading: trim-inline(project.name),
|
|
121
128
|
form: "linked",
|
|
@@ -124,10 +131,10 @@
|
|
|
124
131
|
)
|
|
125
132
|
},
|
|
126
133
|
certifications: card => item-grid(
|
|
127
|
-
items: card.items.map(
|
|
134
|
+
items: card.items.map(or-none).filter(item => item != none),
|
|
128
135
|
columns: calc.max(1, card.columns),
|
|
129
136
|
),
|
|
130
|
-
other: card => for row in card.entries {
|
|
137
|
+
other: card => for row in filled(card.entries) {
|
|
131
138
|
dated(row.heading, row.dates, row.subtitle, row.location, row.details)
|
|
132
139
|
},
|
|
133
140
|
)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
quill:
|
|
2
|
-
name:
|
|
3
|
-
version: 0.0
|
|
2
|
+
name: cyber_career_plan
|
|
3
|
+
version: 0.1.0
|
|
4
4
|
backend: typst
|
|
5
|
-
description: 17X Cyber Operations Officer
|
|
5
|
+
description: "17X Cyber Operations Officer career plan: current assignment, vectors and record"
|
|
6
6
|
|
|
7
7
|
typst:
|
|
8
8
|
plate_file: plate.typ
|
|
@@ -112,7 +112,7 @@ main:
|
|
|
112
112
|
ui:
|
|
113
113
|
group: plan
|
|
114
114
|
compact: true
|
|
115
|
-
description: "How many years wide the timeline is. Twelve carries a Captain from today past the Lt Col board and is the comfortable reading width. The full milestone ladder runs to YG+17, so widening it toward eighteen brings the SDE looks into view at the cost of narrower columns
|
|
115
|
+
description: "How many years wide the timeline is, at most eighteen; a wider window draws at eighteen. Twelve carries a Captain from today past the Lt Col board and is the comfortable reading width. The full milestone ladder runs to YG+17, so widening it toward eighteen brings the SDE looks into view at the cost of narrower columns. A milestone past the window's far end is counted in a note rather than dropped; one before it is history, except a course passed unticked, which is listed as overdue."
|
|
116
116
|
|
|
117
117
|
move_year:
|
|
118
118
|
type: integer?
|
|
@@ -180,7 +180,7 @@ main:
|
|
|
180
180
|
type: number
|
|
181
181
|
default: 2
|
|
182
182
|
title: Years
|
|
183
|
-
description: "Length in years, in halves: 0.5 to 4. The timeline is ruled in half-years, one per VML cycle, so a length is rounded to the nearest half, and one outside that range draws at the nearer end, marked with a dagger. A whole number of years keeps you on the cycle you moved in on; a half switches Summer to Winter or back."
|
|
183
|
+
description: "Length in years, in halves: 0.5 to 4. The timeline is ruled in half-years, one per VML cycle, so a length is rounded to the nearest half first, and one that rounds to outside that range draws at the nearer end, marked with a dagger. A whole number of years keeps you on the cycle you moved in on; a half switches Summer to Winter or back."
|
|
184
184
|
school:
|
|
185
185
|
type: boolean
|
|
186
186
|
default: false
|
|
@@ -258,11 +258,12 @@ main:
|
|
|
258
258
|
type: object
|
|
259
259
|
ui:
|
|
260
260
|
group: qualifications
|
|
261
|
+
layout: flat
|
|
261
262
|
description: "Tick what you hold. The page prints the whole 17X vocabulary and marks these: a rater reads the empty boxes as readily as the filled ones, so what you have not done yet is on the page whether you tick it or not. A ticked course or school also marks its milestone on the timeline done. Written out, a tick is `flight_cc: true`, and a tick with its detail `flight_cc: {detail: UMS Flt/CC}`; an unticked member is left out."
|
|
262
263
|
properties:
|
|
263
264
|
command:
|
|
264
265
|
type: matrix
|
|
265
|
-
title: Leadership &
|
|
266
|
+
title: Leadership & command
|
|
266
267
|
ui:
|
|
267
268
|
compact: true
|
|
268
269
|
members:
|
|
@@ -295,7 +296,7 @@ main:
|
|
|
295
296
|
properties: *detail
|
|
296
297
|
staff:
|
|
297
298
|
type: matrix
|
|
298
|
-
title: Staff &
|
|
299
|
+
title: Staff & functional
|
|
299
300
|
ui:
|
|
300
301
|
compact: true
|
|
301
302
|
members:
|
|
@@ -29,9 +29,10 @@
|
|
|
29
29
|
#set block(spacing: 0pt)
|
|
30
30
|
#set par(spacing: 0pt)
|
|
31
31
|
|
|
32
|
+
#let officer = data.at("name", default: "").trim()
|
|
32
33
|
#set document(
|
|
33
|
-
title: "
|
|
34
|
-
author:
|
|
34
|
+
title: if officer == "" { "Career Plan" } else { "Career Plan — " + officer },
|
|
35
|
+
author: officer,
|
|
35
36
|
)
|
|
36
37
|
|
|
37
38
|
// A `plaintext` field lowers to content padded with a space on each side, which
|
|
@@ -75,13 +76,19 @@
|
|
|
75
76
|
#let board-yg = if adjusted { adj-yg } else { comm-yg }
|
|
76
77
|
// A required year not yet filled in arrives as 0. With no start year the window
|
|
77
78
|
// opens on the year group; with neither, as on a new chart, the axis counts
|
|
78
|
-
// from YG+0 and prints no calendar years rather than the years 0 to 11.
|
|
79
|
+
// from YG+0 and prints no calendar years rather than the years 0 to 11. With a
|
|
80
|
+
// start year but no year group there is nothing to count the ladder from, so
|
|
81
|
+
// the axis prints calendar years alone and no milestone is drawn.
|
|
79
82
|
#let start-year = if data.timeline_start_year == 0 { board-yg } else { data.timeline_start_year }
|
|
80
83
|
#let dated = start-year != 0
|
|
81
|
-
#let
|
|
84
|
+
#let laddered = board-yg != 0 or not dated
|
|
85
|
+
#let yg-offset(k) = if k < 0 [YG#k] else [YG+#k]
|
|
86
|
+
// Eighteen is the widest the marks hold their columns at, and the schema cannot
|
|
87
|
+
// bound an integer, so a wider window draws at eighteen.
|
|
88
|
+
#let years = calc.clamp(data.timeline_years, 1, 18)
|
|
82
89
|
#let cols = years * 2
|
|
83
90
|
#let last-year = start-year + years - 1
|
|
84
|
-
#let window-end = if dated [#last-year] else
|
|
91
|
+
#let window-end = if dated [#last-year] else { yg-offset(last-year - board-yg) }
|
|
85
92
|
#let in-window(y) = y >= start-year and y <= last-year
|
|
86
93
|
#let band-height = 0.46in
|
|
87
94
|
// Marks fit their year column: past twelve years they step off the scale.
|
|
@@ -124,12 +131,16 @@
|
|
|
124
131
|
// A course whose year has passed unticked is what a rater most needs to see, so
|
|
125
132
|
// it stands before the window, in the rail, saying when it was due. A passed
|
|
126
133
|
// board is history.
|
|
127
|
-
#let gates =
|
|
134
|
+
#let gates = if laddered {
|
|
135
|
+
gates.map(g => (..g, overdue: not g.board and not g.done and g.year < start-year))
|
|
136
|
+
} else { () }
|
|
128
137
|
#let looks = ("1st", "2nd", "3rd", "final")
|
|
129
|
-
#let windows =
|
|
130
|
-
(
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
#let windows = if laddered {
|
|
139
|
+
(
|
|
140
|
+
(yg: 8, name: "IDE", by: ("ide_candidate", "ide_graduate")),
|
|
141
|
+
(yg: 14, name: "SDE", by: ("sde_candidate", "sde_graduate")),
|
|
142
|
+
).map(w => (..w, done: held(w.by), years: looks.enumerate().map(((k, _)) => board-yg + w.yg + k)))
|
|
143
|
+
} else { () }
|
|
133
144
|
#let ahead = (
|
|
134
145
|
gates.filter(g => not g.done and g.year > last-year).len()
|
|
135
146
|
+ windows.filter(w => not w.done).map(w => w.years.filter(y => y > last-year).len()).sum(default: 0)
|
|
@@ -216,14 +227,18 @@
|
|
|
216
227
|
#let vectors = data.vectors.enumerate().map(((i, v)) => (
|
|
217
228
|
rank: ranks.at(i, default: "Vector " + str(i + 1)),
|
|
218
229
|
track: or-none(v.track),
|
|
219
|
-
// A length
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
230
|
+
// A length is rounded to the half-year first; one that rounds outside 0.5 to
|
|
231
|
+
// 4 draws at the nearer end under a dagger the legend explains, since the
|
|
232
|
+
// schema cannot bound a number.
|
|
233
|
+
placed: lay(v.tours.map(t => {
|
|
234
|
+
let halves = int(calc.round(float(t.years) * 2))
|
|
235
|
+
(
|
|
236
|
+
title: trim(t.title),
|
|
237
|
+
span: calc.clamp(halves, 1, 8),
|
|
238
|
+
out-of-range: halves < 1 or halves > 8,
|
|
239
|
+
school: t.school,
|
|
240
|
+
)
|
|
241
|
+
})),
|
|
227
242
|
))
|
|
228
243
|
|
|
229
244
|
// The band is ruled to an even height, so a block cannot grow to fit its text:
|
|
@@ -259,6 +274,24 @@
|
|
|
259
274
|
)).flatten().find(s => s != none))
|
|
260
275
|
})
|
|
261
276
|
|
|
277
|
+
// A vector's label holds to its row the way a block's text does: it steps down
|
|
278
|
+
// the scale until it fits, every word within the rail. One too long at the
|
|
279
|
+
// smallest drops its rank, which the row's place already says, and keeps the
|
|
280
|
+
// whole lines the row holds.
|
|
281
|
+
#let track-label(name, sub) = layout(size => {
|
|
282
|
+
let (w, h) = (size.width, size.height)
|
|
283
|
+
let words = plain(name).split(" ").filter(x => x != "")
|
|
284
|
+
let face(s, body) = text(size: s, weight: 700, body)
|
|
285
|
+
let typeset(s) = [#face(s, name)#if sub != none [#linebreak()#text(size: micro, fill: mute, sub)]]
|
|
286
|
+
let fits(s) = words.all(x => measure(face(s, x)).width <= w) and measure(block(width: w, typeset(s))).height <= h
|
|
287
|
+
let s = (lead, base, small).find(fits)
|
|
288
|
+
if s != none { return block(width: w, height: h, align(left + horizon, typeset(s))) }
|
|
289
|
+
let one = measure(face(small, [A])).height
|
|
290
|
+
let step = measure(face(small, [A#linebreak()A])).height - one
|
|
291
|
+
let kept = one + calc.floor((h - one) / step) * step + 2pt
|
|
292
|
+
block(width: w, height: h, align(left + horizon, block(width: w, height: kept, clip: true, face(small, name))))
|
|
293
|
+
})
|
|
294
|
+
|
|
262
295
|
#let span-label(span) = {
|
|
263
296
|
let years = span / 2
|
|
264
297
|
if calc.rem(span, 2) == 0 { [#int(years) yr] } else { [#years yr] }
|
|
@@ -297,7 +330,16 @@
|
|
|
297
330
|
("Year group", if board-yg != 0 [#board-yg], if adjusted [adjusted from #comm-yg]),
|
|
298
331
|
("Date of rank", show-date(data.date_of_rank), none),
|
|
299
332
|
("Arrived station", show-date(data.date_arrived_station), none),
|
|
300
|
-
|
|
333
|
+
// A degree marked "(in progress)" says so in a note under it, as the year
|
|
334
|
+
// group's adjustment does, rather than breaking inside the parenthesis.
|
|
335
|
+
{
|
|
336
|
+
let degree = or-none(data.advanced_degree)
|
|
337
|
+
let flat = plain(degree).trim()
|
|
338
|
+
let mark = "(in progress)"
|
|
339
|
+
if degree != none and flat.ends-with(mark) and flat.len() > mark.len() {
|
|
340
|
+
("Advanced degree", flat.slice(0, -mark.len()).trim(), [in progress])
|
|
341
|
+
} else { ("Advanced degree", degree, none) }
|
|
342
|
+
},
|
|
301
343
|
)
|
|
302
344
|
#grid(
|
|
303
345
|
columns: (name-width,) + (1fr,) * facts.len(),
|
|
@@ -305,7 +347,7 @@
|
|
|
305
347
|
row-gutter: 3pt,
|
|
306
348
|
align: (_, y) => if y == 0 { bottom } else { top },
|
|
307
349
|
// Raised clear of the line under it by the depth of its descenders.
|
|
308
|
-
pad(bottom: 3pt, text(size: display, weight: 800, tracking: -0.3pt,
|
|
350
|
+
pad(bottom: 3pt, text(size: display, weight: 800, tracking: -0.3pt, officer)),
|
|
309
351
|
..facts.map(((name, ..)) => eyebrow(name)),
|
|
310
352
|
identity,
|
|
311
353
|
..facts.map(((_, value, note)) => stack(
|
|
@@ -325,20 +367,21 @@
|
|
|
325
367
|
// the axis
|
|
326
368
|
#rows.push(table.cell[])
|
|
327
369
|
#for i in range(years) {
|
|
328
|
-
let offset =
|
|
370
|
+
let offset = yg-offset(start-year + i - board-yg)
|
|
329
371
|
rows.push(table.cell(colspan: 2, stroke: (bottom: 0.7pt + ink), inset: (x: 3pt, bottom: 3pt))[
|
|
330
|
-
#if dated [
|
|
372
|
+
#if not dated { text(weight: 700, offset) } else if laddered [
|
|
331
373
|
#text(weight: 700)[#(start-year + i)]
|
|
332
374
|
#linebreak()
|
|
333
375
|
#text(size: micro, fill: mute, offset)
|
|
334
|
-
] else { text(weight: 700
|
|
376
|
+
] else { text(weight: 700)[#(start-year + i)] }
|
|
335
377
|
])
|
|
336
378
|
}
|
|
337
379
|
|
|
338
380
|
// One row per constraint, under the axis: a bar from today to the end of the
|
|
339
381
|
// last year it holds, ruled under and closed at its end, so it reads as "until
|
|
340
382
|
// here". The band is not ruled by year, so a label too long for its bar runs
|
|
341
|
-
// on past its end, on one line, over nothing
|
|
383
|
+
// on past its end, on one line, over nothing; one too long for that as well is
|
|
384
|
+
// cut at whichever edge leaves more of it, the bar's end or the page's.
|
|
342
385
|
#for (i, c) in constraints.enumerate() {
|
|
343
386
|
rows.push(if i == 0 { row-label[Constraints] } else { table.cell[] })
|
|
344
387
|
let note = [#text(size: micro, weight: 600)[#c.note]#text(size: micro, fill: mute)[#(" · through " + str(c.through))]]
|
|
@@ -348,9 +391,14 @@
|
|
|
348
391
|
// A bar that runs past the window is left open at its end, since the
|
|
349
392
|
// window's edge is not where it stops.
|
|
350
393
|
let open = c.through > last-year
|
|
351
|
-
let
|
|
394
|
+
let wide = measure(note).width
|
|
395
|
+
let inside = size.width - 8pt
|
|
396
|
+
// The half-years right of the bar, less this cell's own inset.
|
|
397
|
+
let after = (size.width + 2pt) / c.end * (cols - c.end) - 4pt
|
|
398
|
+
let (dx, room) = if wide <= inside or (wide > after and inside >= after) { (3pt, inside) } else { (size.width + 3pt, after) }
|
|
352
399
|
box(width: 100%, height: 8.5pt, stroke: (bottom: constraint-stroke, right: if open { none } else { constraint-stroke }))
|
|
353
|
-
|
|
400
|
+
// Clipped across only: the inset keeps the ascenders and descenders.
|
|
401
|
+
place(left + bottom, dx: dx, box(width: calc.max(0pt, room), inset: (y: 2pt), clip: true, box(width: 10in, note)))
|
|
354
402
|
})
|
|
355
403
|
])
|
|
356
404
|
rows += empty(cols - c.end)
|
|
@@ -383,11 +431,17 @@
|
|
|
383
431
|
// The assignment held now, drawn once across every vector row. It reads like a
|
|
384
432
|
// tour block, title over a line of small print, and sheds what its space cannot
|
|
385
433
|
// hold the way one does; with nothing left, the header still names the job. The
|
|
386
|
-
// unit is left to the header throughout.
|
|
434
|
+
// unit is left to the header throughout. A move past the window leaves the
|
|
435
|
+
// block open at the window's edge, as a tour the window cuts is.
|
|
387
436
|
#let current-block = block(
|
|
388
437
|
width: 100%,
|
|
389
438
|
height: 100%,
|
|
390
|
-
stroke:
|
|
439
|
+
stroke: (
|
|
440
|
+
left: current-stroke,
|
|
441
|
+
top: current-stroke,
|
|
442
|
+
bottom: current-stroke,
|
|
443
|
+
right: if move-col > cols { none } else { current-stroke },
|
|
444
|
+
),
|
|
391
445
|
inset: (x: 3pt, y: 2pt),
|
|
392
446
|
block-text(
|
|
393
447
|
if duty != none { duty } else { [Current assignment] },
|
|
@@ -405,12 +459,11 @@
|
|
|
405
459
|
if v.track != none { sub.push(v.rank) }
|
|
406
460
|
if beyond.len() > 0 { sub.push([+#beyond.len() past #window-end]) }
|
|
407
461
|
|
|
408
|
-
rows.push(table.cell(
|
|
409
|
-
#
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
]
|
|
462
|
+
rows.push(table.cell(stroke: top-rule, inset: (right: 5pt, y: 3pt))[
|
|
463
|
+
#block(width: 100%, height: 100%, track-label(
|
|
464
|
+
if v.track != none { v.track } else { [#v.rank] },
|
|
465
|
+
if sub.len() > 0 { sub.join[ · ] },
|
|
466
|
+
))
|
|
414
467
|
])
|
|
415
468
|
|
|
416
469
|
if rank == 0 and current-span > 0 {
|
|
@@ -520,7 +573,9 @@
|
|
|
520
573
|
}
|
|
521
574
|
|
|
522
575
|
// A table, so each rater's column reads down the years. With no Higher Level
|
|
523
|
-
// Reviewer strat on any row, that column goes rather than printing empty.
|
|
576
|
+
// Reviewer strat on any row, that column goes rather than printing empty. The
|
|
577
|
+
// rows are set at the record's size, so the table reads as one of its rows. A
|
|
578
|
+
// year not yet filled in arrives as 0, sorts last and prints a dash.
|
|
524
579
|
#let strats = data.stratifications.sorted(key: s => -s.year)
|
|
525
580
|
#let any-hlr = strats.any(s => not blank(s.hlr))
|
|
526
581
|
#let strat-cell(v) = if blank(v) { dash } else { trim(v) }
|
|
@@ -531,9 +586,9 @@
|
|
|
531
586
|
stroke: none,
|
|
532
587
|
table.header([], eyebrow[Rater], ..if any-hlr { (eyebrow[HLR],) }),
|
|
533
588
|
..strats.map(s => (
|
|
534
|
-
text(
|
|
535
|
-
|
|
536
|
-
..if any-hlr { (
|
|
589
|
+
if s.year == 0 { dash } else { text(weight: 700)[#s.year] },
|
|
590
|
+
strat-cell(s.rater),
|
|
591
|
+
..if any-hlr { (strat-cell(s.hlr),) },
|
|
537
592
|
)).flatten(),
|
|
538
593
|
)
|
|
539
594
|
|
|
@@ -584,26 +639,43 @@
|
|
|
584
639
|
body
|
|
585
640
|
}
|
|
586
641
|
|
|
587
|
-
// What was typed
|
|
588
|
-
// height
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}),
|
|
599
|
-
)
|
|
600
|
-
#block(width: 100%, height: 1fr, pad(left: rail, layout(size => ruling(calc.floor((size.height - rule-drop) / pitch) + 1))))
|
|
601
|
-
|
|
602
|
-
#block(above: 10pt, grid(
|
|
642
|
+
// What was typed, on at least the first rule, is the minimum and holds its
|
|
643
|
+
// height; the blank rules under it are whatever the page has left. The notes
|
|
644
|
+
// and the line they are signed on are one unit, so typed notes too long for the
|
|
645
|
+
// page carry the signature with them rather than leaving it alone overleaf.
|
|
646
|
+
//
|
|
647
|
+
// The section is one block sized to the rest of its page, which it reads off a
|
|
648
|
+
// mark left where it starts: the page cannot otherwise tell a block what is left
|
|
649
|
+
// of it, and the mark, being empty, stays put when the block does not fit. Notes
|
|
650
|
+
// too long for what is left make the block taller than that, and it goes to the
|
|
651
|
+
// next page whole.
|
|
652
|
+
#let signature = grid(
|
|
603
653
|
columns: (rail, 1fr),
|
|
604
654
|
rail-label[Discussed with],
|
|
605
655
|
text(size: micro, fill: mute)[
|
|
606
656
|
#box(width: 2.5in, stroke: (bottom: 0.4pt + mute))[]
|
|
607
657
|
#h(12pt) on #box(width: 1.2in, stroke: (bottom: 0.4pt + mute))[]
|
|
608
658
|
],
|
|
609
|
-
)
|
|
659
|
+
)
|
|
660
|
+
#let signature-gap = 8pt
|
|
661
|
+
#block(height: 0pt)<notes-start>
|
|
662
|
+
#context {
|
|
663
|
+
let width = 11in - 2 * margin - rail
|
|
664
|
+
let notes = if type(typed) != str { on-ruling(typed) } else { [] }
|
|
665
|
+
let used = int(calc.round(measure(block(width: width, notes)).height / pitch))
|
|
666
|
+
let below = signature-gap + measure(signature).height
|
|
667
|
+
let room = 8.5in - margin - locate(<notes-start>).position().y
|
|
668
|
+
// The typed lines need only reach their last rule; the blank rules run on to
|
|
669
|
+
// the signature's gap.
|
|
670
|
+
let lines = calc.max(1, used)
|
|
671
|
+
let need = (lines - 1) * pitch + rule-drop + below
|
|
672
|
+
let rules = calc.max(lines, int(calc.floor((room - below - rule-drop) / pitch)) + 1)
|
|
673
|
+
block(breakable: false, width: 100%, height: calc.max(room, need), {
|
|
674
|
+
grid(
|
|
675
|
+
columns: (rail, 1fr),
|
|
676
|
+
on-ruling(rail-label[Notes]),
|
|
677
|
+
block(width: 100%, { ruling(rules); notes }),
|
|
678
|
+
)
|
|
679
|
+
place(bottom + left, signature)
|
|
680
|
+
})
|
|
681
|
+
}
|
package/templates/templates.json
CHANGED
|
@@ -54,12 +54,5 @@
|
|
|
54
54
|
"description": "Appoint primary and alternate program managers with an appointee table",
|
|
55
55
|
"file": "appointment_letter.md",
|
|
56
56
|
"tags": ["appointment", "additional-duty", "air-force"]
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
"id": "deros-extension",
|
|
60
|
-
"name": "DEROS Extension Request",
|
|
61
|
-
"description": "Exception to policy DEROS extension request memorandum with commander indorsement",
|
|
62
|
-
"file": "deros_extension.md",
|
|
63
|
-
"tags": ["deros", "pcs", "assignments"]
|
|
64
57
|
}
|
|
65
58
|
]
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
~~~
|
|
2
|
-
$quill: usaf_memo@0.3
|
|
3
|
-
$kind: main
|
|
4
|
-
letterhead_title:
|
|
5
|
-
- DEPARTMENT OF THE AIR FORCE
|
|
6
|
-
- YOUR SQUADRON HERE
|
|
7
|
-
memo_for:
|
|
8
|
-
- SQ/CC
|
|
9
|
-
- ORG FSS/FSPD
|
|
10
|
-
- AFPC ENLISTED DEROS MANAGEMENT
|
|
11
|
-
memo_from:
|
|
12
|
-
- ORG/SYMBOL
|
|
13
|
-
subject: Exception to Policy DEROS Extension Request (JOHN Q. SMITH, SSN)
|
|
14
|
-
signature_block:
|
|
15
|
-
- FIRST M. LAST, Rank, USAF
|
|
16
|
-
- Duty Title
|
|
17
|
-
tag_line: ""
|
|
18
|
-
~~~
|
|
19
|
-
|
|
20
|
-
I am requesting an extension of my current DEROS from DD MMM YYYY to DD MMM YYYY. Why do you need this DEROS extension? What has caused the delay?
|
|
21
|
-
|
|
22
|
-
My original DEROS from insert base to insert base is *DD MMM YYYY* with a RNLTD of *DD MMM YYYY*
|
|
23
|
-
|
|
24
|
-
My personal information is as follows:
|
|
25
|
-
|
|
26
|
-
- Current DEROS: DD MMM YYYY
|
|
27
|
-
|
|
28
|
-
- Requested DEROS: DD MMM YYYY
|
|
29
|
-
|
|
30
|
-
For any questions or concerns you may contact me at *email* or DSN xxx-xxxx
|
|
31
|
-
|
|
32
|
-
~~~
|
|
33
|
-
$kind: indorsement
|
|
34
|
-
from: ORG/CC
|
|
35
|
-
for: ORG FSS/FSPD
|
|
36
|
-
signature_block:
|
|
37
|
-
- FIRST M. LAST, Rank, USAF
|
|
38
|
-
- Commander
|
|
39
|
-
action: undecided
|
|
40
|
-
~~~
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/quills/classic_resume/{0.0.1 → 0.1.0}/packages/ttq-classic-resume/fonts/EBGaramond/OFL.txt
RENAMED
|
File without changes
|
|
File without changes
|