@airmark/quiver 0.35.6 → 0.35.8
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/package.json +4 -4
- package/quills/af4141/0.1.0/Quill.yaml +39 -49
- package/quills/af4141/0.1.0/plate.typ +7 -7
- package/quills/afmc_moa/0.0.1/Quill.yaml +126 -153
- package/quills/afmc_moa/0.0.1/plate.typ +11 -11
- package/quills/classic_resume/0.1.0/Quill.yaml +34 -40
- package/quills/classic_resume/0.1.0/example.md +53 -59
- package/quills/classic_resume/0.1.0/plate.typ +11 -11
- package/quills/cyber_career_plan/0.1.0/Quill.yaml +258 -287
- package/quills/cyber_career_plan/0.1.0/plate.typ +92 -70
- package/quills/daf1206/0.1.0/plate.typ +18 -18
- package/quills/daf4392/0.1.0/Quill.yaml +33 -38
- package/quills/daf4392/0.1.0/plate.typ +16 -16
- package/quills/usaf_letter/0.1.0/Quill.yaml +143 -158
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/frontmatter.typ +34 -19
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/mainmatter.typ +1 -1
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/primitives.typ +27 -9
- package/quills/usaf_letter/0.1.0/plate.typ +26 -28
- package/quills/usaf_memo/0.2.0/Quill.yaml +217 -247
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +15 -1
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +34 -19
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +5 -1
- package/quills/usaf_memo/0.2.0/plate.typ +28 -28
- package/quills/usaf_memo/0.3.0/Quill.yaml +219 -245
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +47 -15
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +34 -19
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +27 -9
- package/quills/usaf_memo/0.3.0/plate.typ +35 -36
- package/templates/classic_resume.md +53 -59
- package/templates/cyber_career_plan.md +56 -0
- package/templates/templates.json +7 -0
|
@@ -9,171 +9,156 @@ typst:
|
|
|
9
9
|
|
|
10
10
|
main:
|
|
11
11
|
description: "Write the letter as plain paragraphs; they aren't numbered. Use a block quote for lines that should stay as written, like an address. You can close with your phone number and email."
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
description: "Your rank and full name, then your duty title and mailing address, one per line. Don't put a comma between city and state."
|
|
24
|
-
|
|
25
|
-
letter_for:
|
|
26
|
-
type: array
|
|
27
|
-
items:
|
|
28
|
-
type: plaintext
|
|
29
|
-
inline: true
|
|
30
|
-
ui:
|
|
31
|
-
group: addressing
|
|
32
|
-
compact: true
|
|
33
|
-
description: "The recipient's rank and full name, then their duty title and mailing address, one per line. Don't put a comma between city and state."
|
|
34
|
-
|
|
35
|
-
salutation:
|
|
36
|
-
type: plaintext
|
|
37
|
-
inline: true
|
|
38
|
-
ui:
|
|
39
|
-
group: addressing
|
|
40
|
-
compact: true
|
|
41
|
-
description: "The greeting, for example 'Dear Major Smith', with no punctuation after the name."
|
|
42
|
-
|
|
43
|
-
date:
|
|
44
|
-
type: date
|
|
45
|
-
default: ""
|
|
46
|
-
ui:
|
|
47
|
-
group: addressing
|
|
48
|
-
compact: true
|
|
49
|
-
description: "The date you sign. Leave blank to add a fillable date field to the PDF instead."
|
|
50
|
-
|
|
51
|
-
complimentary_close:
|
|
52
|
-
type: plaintext
|
|
53
|
-
inline: true
|
|
54
|
-
default: Sincerely
|
|
55
|
-
ui:
|
|
56
|
-
group: addressing
|
|
57
|
-
compact: true
|
|
58
|
-
description: "The closing above your signature. 'Sincerely' suits most letters."
|
|
59
|
-
|
|
60
|
-
signature_block:
|
|
61
|
-
type: array
|
|
62
|
-
items:
|
|
63
|
-
type: plaintext
|
|
64
|
-
inline: true
|
|
65
|
-
ui:
|
|
66
|
-
group: addressing
|
|
67
|
-
compact: true
|
|
68
|
-
description: "Your name in uppercase, grade, and service, for example 'FIRST M. LAST, Col, USAF'. A personal letter leaves out the duty title."
|
|
69
|
-
|
|
70
|
-
letterhead_title:
|
|
71
|
-
type: array
|
|
72
|
-
items:
|
|
73
|
-
type: plaintext
|
|
74
|
-
inline: true
|
|
75
|
-
default:
|
|
76
|
-
- DEPARTMENT OF THE AIR FORCE
|
|
77
|
-
ui:
|
|
78
|
-
group: letterhead
|
|
79
|
-
description: "The letterhead, in uppercase. Keep the first line unless you work for a joint command or a DoW agency, and add your unit's full name on the lines below it."
|
|
80
|
-
|
|
81
|
-
letterhead_seal:
|
|
82
|
-
type: enum
|
|
83
|
-
values:
|
|
84
|
-
- dow
|
|
85
|
-
- dod
|
|
86
|
-
default: dow
|
|
87
|
-
ui:
|
|
88
|
-
group: letterhead
|
|
89
|
-
compact: true
|
|
90
|
-
description: "The seal at the top left of the letterhead: Department of War or Department of Defense."
|
|
91
|
-
|
|
92
|
-
letterhead_seal_subtitle:
|
|
93
|
-
type: plaintext
|
|
94
|
-
inline: true
|
|
95
|
-
default: ""
|
|
96
|
-
ui:
|
|
97
|
-
group: letterhead
|
|
98
|
-
compact: true
|
|
99
|
-
description: "An optional line in bold capitals under the seal."
|
|
100
|
-
|
|
101
|
-
tag_line:
|
|
102
|
-
type: richtext
|
|
103
|
-
inline: true
|
|
104
|
-
default: ""
|
|
105
|
-
ui:
|
|
106
|
-
group: letterhead
|
|
107
|
-
compact: true
|
|
108
|
-
description: "Your unit's motto, centered at the bottom of every page."
|
|
12
|
+
sections:
|
|
13
|
+
addressing:
|
|
14
|
+
fields:
|
|
15
|
+
letter_from:
|
|
16
|
+
type: array
|
|
17
|
+
items:
|
|
18
|
+
type: plaintext
|
|
19
|
+
inline: true
|
|
20
|
+
ui:
|
|
21
|
+
compact: true
|
|
22
|
+
description: "Your rank and full name, then your duty title and mailing address, one per line. Don't put a comma between city and state."
|
|
109
23
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
- UNCLASSIFIED
|
|
114
|
-
- CUI
|
|
115
|
-
- CONFIDENTIAL
|
|
116
|
-
- SECRET
|
|
117
|
-
- TOP SECRET
|
|
118
|
-
default: ""
|
|
119
|
-
ui:
|
|
120
|
-
group: classification
|
|
121
|
-
compact: true
|
|
122
|
-
description: "The classification banner at the top and bottom of every page. Leave blank for an unclassified document."
|
|
123
|
-
variants:
|
|
124
|
-
CUI:
|
|
125
|
-
controlled_by:
|
|
24
|
+
letter_for:
|
|
25
|
+
type: array
|
|
26
|
+
items:
|
|
126
27
|
type: plaintext
|
|
127
28
|
inline: true
|
|
128
|
-
|
|
129
|
-
|
|
29
|
+
ui:
|
|
30
|
+
compact: true
|
|
31
|
+
description: "The recipient's rank and full name, then their duty title and mailing address, one per line. Don't put a comma between city and state."
|
|
32
|
+
|
|
33
|
+
salutation:
|
|
34
|
+
type: plaintext
|
|
35
|
+
inline: true
|
|
36
|
+
ui:
|
|
37
|
+
compact: true
|
|
38
|
+
description: "The greeting, for example 'Dear Major Smith', with no punctuation after the name."
|
|
39
|
+
|
|
40
|
+
date:
|
|
41
|
+
type: date
|
|
42
|
+
default: ""
|
|
43
|
+
ui:
|
|
44
|
+
compact: true
|
|
45
|
+
description: "The date you sign. Leave blank to add a fillable date field to the PDF instead."
|
|
46
|
+
|
|
47
|
+
complimentary_close:
|
|
48
|
+
type: plaintext
|
|
49
|
+
inline: true
|
|
50
|
+
default: Sincerely
|
|
51
|
+
ui:
|
|
52
|
+
compact: true
|
|
53
|
+
description: "The closing above your signature. 'Sincerely' suits most letters."
|
|
54
|
+
|
|
55
|
+
signature_block:
|
|
56
|
+
type: array
|
|
57
|
+
items:
|
|
130
58
|
type: plaintext
|
|
131
59
|
inline: true
|
|
132
|
-
|
|
133
|
-
|
|
60
|
+
ui:
|
|
61
|
+
compact: true
|
|
62
|
+
description: "Your name in uppercase, grade, and service, for example 'FIRST M. LAST, Col, USAF'. A personal letter leaves out the duty title."
|
|
63
|
+
letterhead:
|
|
64
|
+
fields:
|
|
65
|
+
letterhead_title:
|
|
66
|
+
type: array
|
|
67
|
+
items:
|
|
134
68
|
type: plaintext
|
|
135
69
|
inline: true
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
70
|
+
default:
|
|
71
|
+
- DEPARTMENT OF THE AIR FORCE
|
|
72
|
+
description: "The letterhead, in uppercase. Keep the first line unless you work for a joint command or a DoW agency, and add your unit's full name on the lines below it."
|
|
73
|
+
|
|
74
|
+
letterhead_seal:
|
|
75
|
+
type: enum
|
|
76
|
+
values:
|
|
77
|
+
- dow
|
|
78
|
+
- dod
|
|
79
|
+
default: dow
|
|
80
|
+
ui:
|
|
81
|
+
compact: true
|
|
82
|
+
description: "The seal at the top left of the letterhead: Department of War or Department of Defense."
|
|
83
|
+
|
|
84
|
+
letterhead_seal_subtitle:
|
|
85
|
+
type: plaintext
|
|
86
|
+
inline: true
|
|
87
|
+
default: ""
|
|
88
|
+
ui:
|
|
89
|
+
compact: true
|
|
90
|
+
description: "An optional line in bold capitals under the seal."
|
|
91
|
+
|
|
92
|
+
tag_line:
|
|
93
|
+
type: richtext
|
|
94
|
+
inline: true
|
|
95
|
+
default: ""
|
|
96
|
+
ui:
|
|
97
|
+
compact: true
|
|
98
|
+
description: "Your unit's motto, centered at the bottom of every page."
|
|
99
|
+
classification:
|
|
100
|
+
fields:
|
|
101
|
+
classification:
|
|
102
|
+
type: enum
|
|
103
|
+
values:
|
|
104
|
+
- UNCLASSIFIED
|
|
105
|
+
- CUI
|
|
106
|
+
- CONFIDENTIAL
|
|
107
|
+
- SECRET
|
|
108
|
+
- TOP SECRET
|
|
109
|
+
default: ""
|
|
110
|
+
ui:
|
|
111
|
+
compact: true
|
|
112
|
+
description: "The classification banner at the top and bottom of every page. Leave blank for an unclassified document."
|
|
113
|
+
variants:
|
|
114
|
+
CUI:
|
|
115
|
+
controlled_by:
|
|
116
|
+
type: plaintext
|
|
117
|
+
inline: true
|
|
118
|
+
description: "The office that designated this information as CUI, for example 'SAF/AA'."
|
|
119
|
+
poc:
|
|
120
|
+
type: plaintext
|
|
121
|
+
inline: true
|
|
122
|
+
description: "Who to contact with questions about this CUI, with phone and email, for example 'Capt J. Smith, DSN 555-1234, john.smith@us.af.mil'."
|
|
123
|
+
category:
|
|
124
|
+
type: plaintext
|
|
125
|
+
inline: true
|
|
126
|
+
default: ""
|
|
127
|
+
description: "The category from the DoD CUI Registry, for example 'CTI' or 'PRVCY'."
|
|
128
|
+
limited_dissemination:
|
|
129
|
+
type: plaintext
|
|
130
|
+
inline: true
|
|
131
|
+
default: ""
|
|
132
|
+
description: "A dissemination control for the CUI block, for example 'FEDONLY' or 'NOFORN'. To add one to the banner, use Dissemination."
|
|
133
|
+
|
|
134
|
+
dissemination:
|
|
135
|
+
type: plaintext
|
|
136
|
+
inline: true
|
|
137
|
+
default: ""
|
|
138
|
+
ui:
|
|
139
|
+
compact: true
|
|
140
|
+
description: "A control added to the end of the banner. For example, 'NF' prints as CUI//NF."
|
|
141
|
+
additional:
|
|
142
|
+
fields:
|
|
143
|
+
cc:
|
|
144
|
+
type: array
|
|
145
|
+
items:
|
|
139
146
|
type: plaintext
|
|
140
147
|
inline: true
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
default: []
|
|
149
|
+
description: Offices or people receiving a courtesy copy, one per line.
|
|
143
150
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
default: []
|
|
159
|
-
ui:
|
|
160
|
-
group: additional
|
|
161
|
-
description: Offices or people receiving a courtesy copy, one per line.
|
|
162
|
-
|
|
163
|
-
attachments:
|
|
164
|
-
type: array
|
|
165
|
-
items:
|
|
166
|
-
type: richtext
|
|
167
|
-
inline: true
|
|
168
|
-
default: []
|
|
169
|
-
ui:
|
|
170
|
-
group: additional
|
|
171
|
-
description: "Attachments in the order the letter mentions them. Describe each briefly and include its date. Don't write 'as stated'."
|
|
172
|
-
|
|
173
|
-
font_size:
|
|
174
|
-
type: number
|
|
175
|
-
default: 12
|
|
176
|
-
ui:
|
|
177
|
-
group: additional
|
|
178
|
-
compact: true
|
|
179
|
-
description: "Body text size in points. The standard is 12."
|
|
151
|
+
attachments:
|
|
152
|
+
type: array
|
|
153
|
+
items:
|
|
154
|
+
type: richtext
|
|
155
|
+
inline: true
|
|
156
|
+
default: []
|
|
157
|
+
description: "Attachments in the order the letter mentions them. Describe each briefly and include its date. Don't write 'as stated'."
|
|
158
|
+
|
|
159
|
+
font_size:
|
|
160
|
+
type: number
|
|
161
|
+
default: 12
|
|
162
|
+
ui:
|
|
163
|
+
compact: true
|
|
164
|
+
description: "Body text size in points. The standard is 12."
|
|
@@ -80,6 +80,12 @@
|
|
|
80
80
|
none
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
let tag-line = if falsey(footer-tag-line) {
|
|
84
|
+
none
|
|
85
|
+
} else {
|
|
86
|
+
align(center, text(fill: LETTERHEAD_COLOR, font: "Spectral SC", size: 15pt)[#footer-tag-line])
|
|
87
|
+
}
|
|
88
|
+
|
|
83
89
|
set par(leading: spacing.line, spacing: spacing.line, justify: false)
|
|
84
90
|
set block(above: spacing.line, below: 0em, spacing: 0em)
|
|
85
91
|
set text(font: body-font, size: font-size, fallback: true)
|
|
@@ -126,14 +132,11 @@
|
|
|
126
132
|
)
|
|
127
133
|
}
|
|
128
134
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
dy: -0.625in,
|
|
133
|
-
|
|
134
|
-
#text(fill: LETTERHEAD_COLOR, font: "Spectral SC", size: 15pt)[#footer-tag-line]
|
|
135
|
-
],
|
|
136
|
-
)
|
|
135
|
+
// Page 1's tag line rides the indicator block's float when there is one.
|
|
136
|
+
if tag-line != none {
|
|
137
|
+
context if cui-indicator == none or here().page() > 1 {
|
|
138
|
+
place(bottom + center, dy: -0.625in, tag-line)
|
|
139
|
+
}
|
|
137
140
|
}
|
|
138
141
|
},
|
|
139
142
|
)
|
|
@@ -142,7 +145,8 @@
|
|
|
142
145
|
// corner, dropped into the 0.5in page-edge band. Emitted as a bottom float so
|
|
143
146
|
// it (1) reserves flow space, raising page 1's effective bottom margin so body
|
|
144
147
|
// text never overlaps it, and (2) stays pinned to page 1 — as the first flow
|
|
145
|
-
// content it can never be bumped to page 2.
|
|
148
|
+
// content it can never be bumped to page 2. Page 1's tag line stands above the
|
|
149
|
+
// block in the same float, since both claim the band.
|
|
146
150
|
if cui-indicator != none {
|
|
147
151
|
context {
|
|
148
152
|
// The box shrink-wraps to its widest line; `set align(left)` keeps the
|
|
@@ -154,24 +158,35 @@
|
|
|
154
158
|
set align(left)
|
|
155
159
|
cui-indicator
|
|
156
160
|
})
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
let block-height = measure(indicator-box).height
|
|
162
|
+
let tag-gap = 0.125in
|
|
163
|
+
let stack-height = if tag-line == none {
|
|
164
|
+
block-height
|
|
165
|
+
} else {
|
|
166
|
+
block-height + tag-gap + measure(tag-line, width: page.width - 2 * spacing.margin).height
|
|
167
|
+
}
|
|
168
|
+
// Reserve only the part of the stack inside the text area (`reserved`):
|
|
169
|
+
// float a box of that height, then `place` the stack inside it pushed down
|
|
170
|
+
// by `overhang` so the surplus overflows into the edge band. (A bare
|
|
160
171
|
// `box(height: reserved, indicator-box)` overflows *upward* into the body
|
|
161
|
-
// instead.) The inner `place`
|
|
172
|
+
// instead.) The inner `place`s add no size, so the box stays `reserved`
|
|
162
173
|
// tall and the block's bottom lands 0.5in from the page edge.
|
|
163
174
|
let overhang = spacing.margin - 0.5in
|
|
164
|
-
let reserved =
|
|
175
|
+
let reserved = stack-height - overhang
|
|
165
176
|
place(
|
|
166
|
-
bottom
|
|
177
|
+
bottom,
|
|
167
178
|
float: true,
|
|
168
|
-
// Slide the right edge into the page-edge band, 0.5in from the border;
|
|
169
|
-
// the inner place right-aligns the block to that edge.
|
|
170
|
-
dx: spacing.margin - 0.5in,
|
|
171
179
|
// Minimum gap to the body's last line; the actual gap is larger when the
|
|
172
180
|
// next paragraph can't fit above the block and breaks to the next page.
|
|
173
181
|
clearance: spacing.line,
|
|
174
|
-
box(
|
|
182
|
+
box(width: 100%, height: reserved, {
|
|
183
|
+
// Slide the block's right edge into the page-edge band, 0.5in from
|
|
184
|
+
// the border.
|
|
185
|
+
place(bottom + right, dx: spacing.margin - 0.5in, dy: overhang, indicator-box)
|
|
186
|
+
if tag-line != none {
|
|
187
|
+
place(bottom + center, dy: overhang - block-height - tag-gap, tag-line)
|
|
188
|
+
}
|
|
189
|
+
}),
|
|
175
190
|
)
|
|
176
191
|
}
|
|
177
192
|
}
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
NESTED.update(n => n + 1)
|
|
77
77
|
context {
|
|
78
78
|
let last = BLOCKS.get() == BLOCKS.final()
|
|
79
|
-
block(above: par.spacing, sticky: last and keeps-signature(it), it)
|
|
79
|
+
block(above: par.spacing, below: par.spacing, sticky: last and keeps-signature(it), it)
|
|
80
80
|
}
|
|
81
81
|
NESTED.update(n => n - 1)
|
|
82
82
|
}
|
|
@@ -197,7 +197,10 @@
|
|
|
197
197
|
// spaces to the right of page center"
|
|
198
198
|
// AFH 33-337 "Do not place the signature element on a continuation page by itself"
|
|
199
199
|
// AFH 33-337 long-name example: "Signature block adjusted to the left" when a
|
|
200
|
-
// long name would otherwise exceed the right margin.
|
|
200
|
+
// long name would otherwise exceed the right margin. Only the name line moves
|
|
201
|
+
// the block; any other line that runs long wraps, its overflow beginning "under
|
|
202
|
+
// the third character of the line above". The handbook gives no measurement,
|
|
203
|
+
// so the indent is the width of the line's own first two characters.
|
|
201
204
|
//
|
|
202
205
|
// A closing line may open the section above the block, and AFH 33-337 gives it
|
|
203
206
|
// one geometry across the two documents that have one: on the second line below
|
|
@@ -215,6 +218,17 @@
|
|
|
215
218
|
/// -> content | none
|
|
216
219
|
#let format-authority-line(value) = if falsey(value) { none } else { upper(value) }
|
|
217
220
|
|
|
221
|
+
// A signature line arrives as `str` or as content; the indent measures its text.
|
|
222
|
+
#let plain-text(it) = {
|
|
223
|
+
if type(it) == str { return it }
|
|
224
|
+
if type(it) != content { return "" }
|
|
225
|
+
if it.has("text") { return plain-text(it.text) }
|
|
226
|
+
if it.has("children") { return it.children.map(plain-text).join(default: "") }
|
|
227
|
+
if it.has("body") { return plain-text(it.body) }
|
|
228
|
+
if it.has("child") { return plain-text(it.child) }
|
|
229
|
+
""
|
|
230
|
+
}
|
|
231
|
+
|
|
218
232
|
#let render-signature-block(
|
|
219
233
|
signature-lines,
|
|
220
234
|
closing-line: none,
|
|
@@ -234,11 +248,11 @@
|
|
|
234
248
|
// anchor. pad() is relative to the text area, hence (4.5in - margin).
|
|
235
249
|
let default-pad = 4.5in - spacing.margin
|
|
236
250
|
context {
|
|
237
|
-
// Measure
|
|
238
|
-
// The closing line shares the anchor, so it joins the
|
|
239
|
-
// wider of the two decides the shift and they stay aligned.
|
|
251
|
+
// Measure the name line at its rendered settings to detect long-name
|
|
252
|
+
// overflow. The closing line shares the anchor, so it joins the
|
|
253
|
+
// measurement: the wider of the two decides the shift and they stay aligned.
|
|
240
254
|
let body-width = page.width - 2 * spacing.margin
|
|
241
|
-
let anchored-lines = signature-lines
|
|
255
|
+
let anchored-lines = signature-lines.slice(0, calc.min(1, signature-lines.len()))
|
|
242
256
|
if closing-line != none { anchored-lines.push(closing-line) }
|
|
243
257
|
let widest = 0pt
|
|
244
258
|
for line in anchored-lines {
|
|
@@ -305,9 +319,9 @@
|
|
|
305
319
|
#pad(left: left-pad)[
|
|
306
320
|
#text(hyphenate: false)[
|
|
307
321
|
#for line in signature-lines {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
par(hanging-indent: .
|
|
322
|
+
let clusters = plain-text(line).clusters()
|
|
323
|
+
let lead = clusters.slice(0, calc.min(2, clusters.len())).join()
|
|
324
|
+
par(hanging-indent: measure(lead).width, line)
|
|
311
325
|
}
|
|
312
326
|
// The page the backmatter compares its own against. Must stay
|
|
313
327
|
// inside the unbreakable block: a marker outside one travels with
|
|
@@ -393,7 +407,11 @@
|
|
|
393
407
|
/// - it (content): The table element to style and render
|
|
394
408
|
/// -> content
|
|
395
409
|
#let render-memo-table(it) = {
|
|
396
|
-
|
|
410
|
+
// A headless table's first row is a body row, which Typst also numbers 0.
|
|
411
|
+
show table: t => if t.children.any(c => c.func() == table.header) {
|
|
412
|
+
show table.cell.where(y: 0): set text(weight: "bold")
|
|
413
|
+
t
|
|
414
|
+
} else { t }
|
|
397
415
|
set table(
|
|
398
416
|
stroke: 0.5pt + black,
|
|
399
417
|
inset: (x: 0.5em, y: 0.4em),
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
#import "@local/quillmark-helper:0.
|
|
2
|
-
data, display, form-field, signature-field,
|
|
3
|
-
)
|
|
1
|
+
#import "@local/quillmark-helper:0.2.0" as qm
|
|
4
2
|
#import "@local/tonguetoquill-usaf-letter:0.1.0": (
|
|
5
3
|
backmatter, date-pattern, frontmatter, mainmatter,
|
|
6
4
|
)
|
|
@@ -10,10 +8,10 @@
|
|
|
10
8
|
// the title alone — the package skips a falsey caption. An empty array renders
|
|
11
9
|
// no letterhead text at all, silently: the block is `place`d, so an empty title
|
|
12
10
|
// leaves no glyphs and takes no space from the flow, and the seal stands alone.
|
|
13
|
-
#let letterhead_lines = data.letterhead_title
|
|
11
|
+
#let letterhead_lines = qm.data.letterhead_title
|
|
14
12
|
|
|
15
13
|
// Body text size, in points. Also the height of a blank date's fill-in widget.
|
|
16
|
-
#let body_font_size = data.font_size * 1pt
|
|
14
|
+
#let body_font_size = qm.data.font_size * 1pt
|
|
17
15
|
|
|
18
16
|
// A blank date's fill-in slot: an empty AcroForm text box the signer types the
|
|
19
17
|
// date into, set in the body face and flush right, as the printed date would
|
|
@@ -27,7 +25,7 @@
|
|
|
27
25
|
#let date_slot(name, field) = context box(
|
|
28
26
|
width: date_slot_width,
|
|
29
27
|
height: measure[0].height,
|
|
30
|
-
place(horizon + left, form-field(
|
|
28
|
+
place(horizon + left, qm.form-field(
|
|
31
29
|
name,
|
|
32
30
|
type: "text",
|
|
33
31
|
width: date_slot_width,
|
|
@@ -42,10 +40,10 @@
|
|
|
42
40
|
#show: frontmatter.with(
|
|
43
41
|
letterhead-title: letterhead_lines.at(0, default: ""),
|
|
44
42
|
letterhead-caption: if letterhead_lines.len() > 1 { letterhead_lines.slice(1) } else { () },
|
|
45
|
-
letterhead-seal-subtitle: data.letterhead_seal_subtitle,
|
|
43
|
+
letterhead-seal-subtitle: qm.data.letterhead_seal_subtitle,
|
|
46
44
|
// A letter has no seal-less state, so the blank takes the default, DoW.
|
|
47
45
|
letterhead-seal: image(
|
|
48
|
-
if data.letterhead_seal == "dod" {
|
|
46
|
+
if qm.data.letterhead_seal == "dod" {
|
|
49
47
|
"assets/dod_seal.png"
|
|
50
48
|
} else {
|
|
51
49
|
"assets/dow_seal.png"
|
|
@@ -54,7 +52,7 @@
|
|
|
54
52
|
|
|
55
53
|
// Date. `data.date` is the native `datetime` and would render identically,
|
|
56
54
|
// but its ink would be born inside the package and carry no schema address.
|
|
57
|
-
// `
|
|
55
|
+
// `qm.ink` places the field's *content* projection instead: the glyphs are
|
|
58
56
|
// born in the generated helper, so the letter's date stays click-to-edit
|
|
59
57
|
// however deep the package formats it.
|
|
60
58
|
//
|
|
@@ -63,33 +61,33 @@
|
|
|
63
61
|
// than stamped with the compile date. Passing it as the date also keeps it
|
|
64
62
|
// from `frontmatter`'s own `datetime.today()` fallback.
|
|
65
63
|
date: {
|
|
66
|
-
let authored =
|
|
64
|
+
let authored = qm.ink(qm.data, "date", date-pattern())
|
|
67
65
|
if authored != none { authored } else { date_slot("Date", "date") }
|
|
68
66
|
},
|
|
69
67
|
|
|
70
|
-
letter-from: data.letter_from,
|
|
71
|
-
letter-for: data.letter_for,
|
|
72
|
-
salutation: data.salutation,
|
|
68
|
+
letter-from: qm.data.letter_from,
|
|
69
|
+
letter-for: qm.data.letter_for,
|
|
70
|
+
salutation: qm.data.salutation,
|
|
73
71
|
|
|
74
|
-
footer-tag-line: data.tag_line,
|
|
72
|
+
footer-tag-line: qm.data.tag_line,
|
|
75
73
|
|
|
76
74
|
// The blank reads as no banner, which is what the package's own
|
|
77
75
|
// `classification-level: none` default means.
|
|
78
|
-
classification-level: data.classification.value,
|
|
76
|
+
classification-level: qm.data.classification.value,
|
|
79
77
|
|
|
80
|
-
dissemination: data.dissemination,
|
|
78
|
+
dissemination: qm.data.dissemination,
|
|
81
79
|
|
|
82
80
|
// CUI designation indicator block fields (DoDM 5200.48). `classification`
|
|
83
81
|
// declares a `CUI` variant, so these four exist only where the discriminant
|
|
84
82
|
// reads CUI, and the branch is what makes reading them total: inside it every
|
|
85
83
|
// declared field of that world is present, outside it none is. The package's
|
|
86
84
|
// own `cui_*: none` defaults cover the worlds that omit them.
|
|
87
|
-
..if data.classification.value == "CUI" {
|
|
85
|
+
..if qm.data.classification.value == "CUI" {
|
|
88
86
|
(
|
|
89
|
-
cui-controlled-by: data.classification.controlled_by,
|
|
90
|
-
cui-category: data.classification.category,
|
|
91
|
-
cui-limited-dissemination: data.classification.limited_dissemination,
|
|
92
|
-
cui-poc: data.classification.poc,
|
|
87
|
+
cui-controlled-by: qm.data.classification.controlled_by,
|
|
88
|
+
cui-category: qm.data.classification.category,
|
|
89
|
+
cui-limited-dissemination: qm.data.classification.limited_dissemination,
|
|
90
|
+
cui-poc: qm.data.classification.poc,
|
|
93
91
|
)
|
|
94
92
|
},
|
|
95
93
|
|
|
@@ -98,24 +96,24 @@
|
|
|
98
96
|
|
|
99
97
|
// An empty body reaches the plate as `""`, and `mainmatter` would still set a
|
|
100
98
|
// blank paragraph for it, dropping the close two lines too far.
|
|
101
|
-
#if data.at("$body") != "" {
|
|
102
|
-
mainmatter[#data.at("$body")]
|
|
99
|
+
#if qm.data.at("$body") != "" {
|
|
100
|
+
mainmatter[#qm.data.at("$body")]
|
|
103
101
|
}
|
|
104
102
|
|
|
105
103
|
#backmatter(
|
|
106
|
-
complimentary-close: data.complimentary_close,
|
|
107
|
-
signature-block: data.signature_block,
|
|
104
|
+
complimentary-close: qm.data.complimentary_close,
|
|
105
|
+
signature-block: qm.data.signature_block,
|
|
108
106
|
// The widget sits at the bottom of AFH 33-337's four blank lines and is
|
|
109
107
|
// sized to two and a half of them, so the line and a half above it stays
|
|
110
108
|
// clear of the complimentary close without moving the block off the fifth
|
|
111
109
|
// line.
|
|
112
|
-
signing-field: signature-field(
|
|
110
|
+
signing-field: qm.signature-field(
|
|
113
111
|
"Signature",
|
|
114
112
|
field: "signature_block",
|
|
115
113
|
height: body_font_size * 2.5,
|
|
116
114
|
),
|
|
117
115
|
|
|
118
|
-
..if data.attachments.len() > 0 { (attachments: data.attachments) },
|
|
116
|
+
..if qm.data.attachments.len() > 0 { (attachments: qm.data.attachments) },
|
|
119
117
|
|
|
120
|
-
..if data.cc.len() > 0 { (cc: data.cc) },
|
|
118
|
+
..if qm.data.cc.len() > 0 { (cc: qm.data.cc) },
|
|
121
119
|
)
|