@airmark/quiver 0.32.1 → 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.
- package/README.md +54 -18
- package/package.json +8 -6
- package/quills/af4141/0.1.0/Quill.yaml +1 -1
- package/quills/afmc_moa/0.0.1/Quill.yaml +10 -17
- package/quills/classic_resume/0.0.1/Quill.yaml +361 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/LICENSE +21 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Bold.ttf +0 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-BoldItalic.ttf +0 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Italic.ttf +0 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Regular.ttf +0 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/OFL.txt +94 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/components.typ +181 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/config.typ +53 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/layout.typ +57 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/lib.typ +6 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/util.typ +32 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/typst.toml +26 -0
- package/quills/classic_resume/0.0.1/plate.typ +143 -0
- package/quills/cyber_ribbon_chart/0.0.1/Quill.yaml +320 -0
- package/quills/cyber_ribbon_chart/0.0.1/plate.typ +428 -0
- package/quills/daf1206/0.1.0/Quill.yaml +1 -2
- package/quills/daf4392/0.1.0/Quill.yaml +15 -1
- package/quills/daf4392/0.1.0/plate.typ +2 -2
- package/quills/usaf_letter/0.1.0/Quill.yaml +214 -0
- package/quills/usaf_letter/0.1.0/assets/dod_seal.png +0 -0
- package/quills/usaf_letter/0.1.0/assets/dow_seal.png +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/.gitignore +7 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/LICENSE +201 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/Cinzel/LICENSE +93 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/CopperplateCC-Bold.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/LICENSE.md +79 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LICENSE +102 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LiberationMono-Regular.ttf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/backmatter.typ +32 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/config.typ +45 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/frontmatter.typ +226 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/lib.typ +35 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/mainmatter.typ +140 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/primitives.typ +544 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/utils.typ +334 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/typst.toml +22 -0
- package/quills/usaf_letter/0.1.0/plate.typ +124 -0
- package/quills/usaf_memo/0.2.0/Quill.yaml +1 -1
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +1 -1
- package/quills/usaf_memo/0.3.0/Quill.yaml +75 -56
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +29 -17
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +97 -126
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/config.typ +12 -45
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +81 -87
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +58 -66
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/lib.typ +14 -36
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +47 -19
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +178 -121
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +41 -141
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/typst.toml +3 -8
- package/quills/usaf_memo/0.3.0/plate.typ +89 -85
- package/templates/af4141.md +23 -0
- package/templates/appointment_letter.md +58 -0
- package/templates/daf1206.md +23 -0
- package/templates/daf4392.md +34 -0
- package/templates/pass_request.md +48 -0
- package/templates/rebuttal.md +63 -0
- package/templates/templates.json +58 -0
- package/templates/usaf_template.md +30 -0
- package/templates/ussf_template.md +30 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel[wght].ttf +0 -0
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
// One rendering function per section of the memorandum, each placing its
|
|
2
|
+
// section where AFH 33-337 Chapter 14 puts it.
|
|
3
|
+
|
|
4
|
+
#import "config.typ": *
|
|
5
|
+
#import "utils.typ": *
|
|
6
|
+
|
|
7
|
+
// AFH 33-337 does not specify letterhead placement; the geometry below follows
|
|
8
|
+
// standard USAF memo convention.
|
|
9
|
+
#let render-letterhead(
|
|
10
|
+
title,
|
|
11
|
+
caption,
|
|
12
|
+
font,
|
|
13
|
+
letterhead-seal: none,
|
|
14
|
+
letterhead-seal-subtitle: none,
|
|
15
|
+
letterhead-emblem: none, // optional image placed opposite the seal (right side)
|
|
16
|
+
letterhead-emblem-height: 1in, // emblem fit-box height; reduce for shorter emblems
|
|
17
|
+
) = {
|
|
18
|
+
font = ensure-array(font)
|
|
19
|
+
// `upper` takes content as readily as `str`, so the letterhead's uppercasing
|
|
20
|
+
// survives a title or caption handed over as markup.
|
|
21
|
+
title = upper(join-lines(title))
|
|
22
|
+
caption = upper(join-lines(caption))
|
|
23
|
+
|
|
24
|
+
// Letterhead corner geometry. The seal (left) and emblem (right) share one
|
|
25
|
+
// reference band so the corners stay in parity: both bleed `corner-overhang`
|
|
26
|
+
// past the page margin and center on the same axis (`band-center`). The
|
|
27
|
+
// emblem may be shorter than the band but stays centered on that axis.
|
|
28
|
+
let corner-overhang = 0.5in
|
|
29
|
+
let corner-width = 2in
|
|
30
|
+
let band-height = 1in // seal height; also the emblem's reference band
|
|
31
|
+
let band-top = -band-height / 2 // puts the band center at dy 0
|
|
32
|
+
let band-center = band-top + band-height / 2
|
|
33
|
+
|
|
34
|
+
// The centered title/caption is placed content (not width-constrained), so a
|
|
35
|
+
// long caption would otherwise render on one line and run underneath the seal.
|
|
36
|
+
// Bound the caption to the width that stays clear of the seal on both sides
|
|
37
|
+
// and auto-shrink it to fit: the seal reaches `band-height - corner-overhang`
|
|
38
|
+
// into the text area, so reserving that plus a gutter on each side keeps the
|
|
39
|
+
// centered caption from touching the seal. `layout` resolves the text-area
|
|
40
|
+
// width so the bound tracks the page/margins.
|
|
41
|
+
let caption-gutter = 0.25in
|
|
42
|
+
let caption-clear = (band-height - corner-overhang) + caption-gutter
|
|
43
|
+
place(
|
|
44
|
+
dy: 0.625in - spacing.margin,
|
|
45
|
+
box(
|
|
46
|
+
width: 100%,
|
|
47
|
+
fill: none,
|
|
48
|
+
stroke: none,
|
|
49
|
+
layout(size => place(
|
|
50
|
+
center + top,
|
|
51
|
+
align(center)[
|
|
52
|
+
#set text(12pt, font: font, fill: LETTERHEAD_COLOR, weight: "bold")
|
|
53
|
+
#title\
|
|
54
|
+
#v(1pt)
|
|
55
|
+
#if not falsey(caption) {
|
|
56
|
+
fit-to-width(
|
|
57
|
+
size.width - 2 * caption-clear,
|
|
58
|
+
alignment: center,
|
|
59
|
+
box(text(10.5pt)[#caption]),
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
)),
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if letterhead-seal != none {
|
|
68
|
+
let seal-body = if falsey(letterhead-seal-subtitle) {
|
|
69
|
+
block[
|
|
70
|
+
#fit-box(width: corner-width, height: band-height)[#letterhead-seal]
|
|
71
|
+
]
|
|
72
|
+
} else {
|
|
73
|
+
// Isolate seal column from document `font-size`: stack `em` spacing and subtitle
|
|
74
|
+
// must not scale with body text (see frontmatter `set text(size: font-size)`).
|
|
75
|
+
// Subtitle is wrapped in `box` so it stays on one line and may extend past
|
|
76
|
+
// the seal's 2in column rather than wrapping.
|
|
77
|
+
block[
|
|
78
|
+
#set text(9pt, font: font, fill: LETTERHEAD_COLOR, weight: "bold")
|
|
79
|
+
// Spacing applies between positional stack children only, not one `[…]` body.
|
|
80
|
+
#stack(
|
|
81
|
+
spacing: 0.5em,
|
|
82
|
+
fit-box(width: corner-width, height: band-height)[#letterhead-seal],
|
|
83
|
+
box(upper(join-lines(letterhead-seal-subtitle))),
|
|
84
|
+
)
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
place(
|
|
88
|
+
left + top,
|
|
89
|
+
dx: -corner-overhang,
|
|
90
|
+
dy: band-top,
|
|
91
|
+
seal-body,
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if letterhead-emblem != none {
|
|
96
|
+
// Mirror the seal: same overhang and width, centered on the seal's band
|
|
97
|
+
// axis. Placing the emblem's top at `band-center - height/2` keeps its
|
|
98
|
+
// center on `band-center` for any (possibly shorter) emblem height.
|
|
99
|
+
place(
|
|
100
|
+
right + top,
|
|
101
|
+
dx: corner-overhang,
|
|
102
|
+
dy: band-center - letterhead-emblem-height / 2,
|
|
103
|
+
block[
|
|
104
|
+
#fit-box(width: corner-width, height: letterhead-emblem-height, alignment: right + horizon)[#letterhead-emblem]
|
|
105
|
+
],
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// AFH 33-337 "Date": "Place the date 1 inch from the right edge, 1.75 inches from the top"
|
|
111
|
+
#let render-date-section(date, memo-style: "usaf") = {
|
|
112
|
+
align(right)[#display-date(date, memo-style: memo-style)]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// AFH 33-337 "MEMORANDUM FOR": "Place 'MEMORANDUM FOR' on the second line below the date"
|
|
116
|
+
#let render-for-section(recipients, cols) = {
|
|
117
|
+
blank-line()
|
|
118
|
+
grid(
|
|
119
|
+
columns: (auto, auto, 1fr),
|
|
120
|
+
"MEMORANDUM FOR",
|
|
121
|
+
" ",
|
|
122
|
+
align(left)[
|
|
123
|
+
#if type(recipients) == array {
|
|
124
|
+
create-auto-grid(recipients.map(upper), column-gutter: spacing.tab, cols: cols)
|
|
125
|
+
} else {
|
|
126
|
+
upper(recipients)
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// AFH 33-337 "FROM:": "Place 'FROM:' in uppercase, flush with the left margin,
|
|
133
|
+
// on the second line below the last line of the MEMORANDUM FOR element"
|
|
134
|
+
#let render-from-section(from-info) = {
|
|
135
|
+
blank-line()
|
|
136
|
+
from-info = join-lines(from-info)
|
|
137
|
+
|
|
138
|
+
grid(
|
|
139
|
+
columns: (auto, auto, 1fr),
|
|
140
|
+
"FROM:", " ", align(left)[#from-info],
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// Whether a `references` entry carries no citation text.
|
|
145
|
+
///
|
|
146
|
+
/// Blank entries reach the template routinely — a stub entry left under
|
|
147
|
+
/// `references:` for the user to fill in. Such an entry must not count as a
|
|
148
|
+
/// reference: a lone blank one would otherwise satisfy the "exactly one
|
|
149
|
+
/// reference" test below and render as an empty `()` after the subject.
|
|
150
|
+
///
|
|
151
|
+
/// - entry (any): A single `references` element
|
|
152
|
+
/// -> bool
|
|
153
|
+
#let blank-reference(entry) = falsey(entry) or entry == []
|
|
154
|
+
|
|
155
|
+
/// Drops `references` entries that carry no citation text and normalizes the
|
|
156
|
+
/// result to an array, so blank placeholder entries neither render on their
|
|
157
|
+
/// own nor affect the inline-vs-block decision.
|
|
158
|
+
///
|
|
159
|
+
/// - references (array | none): Raw reference entries
|
|
160
|
+
/// -> array
|
|
161
|
+
#let compact-references(references) = ensure-array(references).filter(entry => not blank-reference(entry))
|
|
162
|
+
|
|
163
|
+
// AFH 33-337 "SUBJECT:": "In all uppercase letters place 'SUBJECT:', flush with the
|
|
164
|
+
// left margin, on the second line below the last line of the FROM element"
|
|
165
|
+
#let render-subject-section(subject-text, inline-reference: none) = {
|
|
166
|
+
blank-line()
|
|
167
|
+
let content = if not blank-reference(inline-reference) {
|
|
168
|
+
// `subject-text` is a content field, so it is boxed for the same reason the
|
|
169
|
+
// inline reference is: a markup block's edge newlines would otherwise read
|
|
170
|
+
// as a space, doubling the one before the parenthesis.
|
|
171
|
+
[#box(subject-text) (#box(inline-reference))]
|
|
172
|
+
} else {
|
|
173
|
+
[#subject-text]
|
|
174
|
+
}
|
|
175
|
+
grid(
|
|
176
|
+
columns: (auto, auto, 1fr),
|
|
177
|
+
"SUBJECT:", " ", content,
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// AFH 33-337: only render References block for two or more references.
|
|
182
|
+
// A single reference is rendered inline after the SUBJECT text instead.
|
|
183
|
+
#let render-references-section(references) = {
|
|
184
|
+
let references = compact-references(references)
|
|
185
|
+
if references.len() >= 2 {
|
|
186
|
+
blank-line()
|
|
187
|
+
grid(
|
|
188
|
+
columns: (auto, auto, 1fr),
|
|
189
|
+
// Spread the entries as enum items lettered "(a) (b) (c)" per AFH 33-337.
|
|
190
|
+
"References:", " ", enum(..references, numbering: "(a) ", body-indent: 0pt),
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// AFH 33-337 "Signature Block": "Start the signature block on the fifth line below
|
|
196
|
+
// the last line of text and 4.5 inches from the left edge of the page or three
|
|
197
|
+
// spaces to the right of page center"
|
|
198
|
+
// AFH 33-337 "Do not place the signature element on a continuation page by itself"
|
|
199
|
+
// AFH 33-337 long-name example: "Signature block adjusted to the left" when a
|
|
200
|
+
// long name would otherwise exceed the right margin.
|
|
201
|
+
//
|
|
202
|
+
// A closing line may open the section above the block, and AFH 33-337 gives it
|
|
203
|
+
// one geometry across the two documents that have one: on the second line below
|
|
204
|
+
// the text at the block's own anchor, with the signature then five lines below
|
|
205
|
+
// the line rather than below the body. The memorandum's occupant is the
|
|
206
|
+
// authority line, "FOR THE COMMANDER"; the personal letter's is the
|
|
207
|
+
// complimentary close, "Sincerely", which the handbook bars the authority line
|
|
208
|
+
// from. Hence the neutral parameter, and hence case belongs to the caller — the
|
|
209
|
+
// authority line is uppercased, "Sincerely" is not.
|
|
210
|
+
|
|
211
|
+
/// The memorandum's occupant of that slot, uppercased per AFH 33-337. Blank is
|
|
212
|
+
/// no line: the handbook forbids one where the commander signs.
|
|
213
|
+
///
|
|
214
|
+
/// - value (str | content | none): The authority line as authored
|
|
215
|
+
/// -> content | none
|
|
216
|
+
#let format-authority-line(value) = if falsey(value) { none } else { upper(value) }
|
|
217
|
+
|
|
218
|
+
#let render-signature-block(
|
|
219
|
+
signature-lines,
|
|
220
|
+
closing-line: none,
|
|
221
|
+
signature-blank-lines: 4,
|
|
222
|
+
signing-field: none,
|
|
223
|
+
// Lines of breaking height reserved below the block for the backmatter
|
|
224
|
+
// lead-in and its continuation note. Reclaimed immediately after, so it moves
|
|
225
|
+
// where the block may break and nothing else.
|
|
226
|
+
reserved-lines: 0,
|
|
227
|
+
) = {
|
|
228
|
+
signature-lines = ensure-array(signature-lines)
|
|
229
|
+
// Blank is no line, whichever occupant the caller passes.
|
|
230
|
+
if falsey(closing-line) { closing-line = none }
|
|
231
|
+
// AFH 33-337 allows two equivalent anchors: 4.5in from the left edge, or three
|
|
232
|
+
// spaces right of page center. On 8.5in stock these coincide (page center =
|
|
233
|
+
// 4.25in; three TNR-12pt spaces ≈ 0.25in), so we use 4.5in as the canonical
|
|
234
|
+
// anchor. pad() is relative to the text area, hence (4.5in - margin).
|
|
235
|
+
let default-pad = 4.5in - spacing.margin
|
|
236
|
+
context {
|
|
237
|
+
// Measure each line at its rendered settings to detect long-name overflow.
|
|
238
|
+
// The closing line shares the anchor, so it joins the measurement: the
|
|
239
|
+
// wider of the two decides the shift and they stay aligned.
|
|
240
|
+
let body-width = page.width - 2 * spacing.margin
|
|
241
|
+
let anchored-lines = signature-lines
|
|
242
|
+
if closing-line != none { anchored-lines.push(closing-line) }
|
|
243
|
+
let widest = 0pt
|
|
244
|
+
for line in anchored-lines {
|
|
245
|
+
let w = measure(text(hyphenate: false, line)).width
|
|
246
|
+
if w > widest { widest = w }
|
|
247
|
+
}
|
|
248
|
+
let stride = line-stride()
|
|
249
|
+
// If the widest line would overflow the right margin at the standard
|
|
250
|
+
// anchor, shift the block left just enough to fit. Clamp at 0 so the
|
|
251
|
+
// block never crosses the left margin.
|
|
252
|
+
let available = body-width - default-pad
|
|
253
|
+
let left-pad = if widest > available {
|
|
254
|
+
let shifted = body-width - widest
|
|
255
|
+
if shifted < 0pt { 0pt } else { shifted }
|
|
256
|
+
} else {
|
|
257
|
+
default-pad
|
|
258
|
+
}
|
|
259
|
+
block(breakable: false)[
|
|
260
|
+
#let gap = stride * signature-blank-lines
|
|
261
|
+
// AFH 33-337: "fifth line below the last line of text" = four blank lines
|
|
262
|
+
// between the text and the signature block. Carried INSIDE the
|
|
263
|
+
// unbreakable block rather than emitted ahead of it, which keeps the gap
|
|
264
|
+
// and the block one indivisible unit. The total space above the block is
|
|
265
|
+
// identical either way — `block.above` still contributes the same 0.5em —
|
|
266
|
+
// but a gap left outside can be consumed at the foot of one page while
|
|
267
|
+
// the block starts at the top margin of the next.
|
|
268
|
+
//
|
|
269
|
+
// The closing line takes one of those lines above it and the gap then
|
|
270
|
+
// measures from the line, not from the text. Inside the block, so no page
|
|
271
|
+
// break can put it on a different page from the signature.
|
|
272
|
+
#if closing-line != none {
|
|
273
|
+
v(stride)
|
|
274
|
+
pad(left: left-pad, text(hyphenate: false, closing-line))
|
|
275
|
+
}
|
|
276
|
+
#if signing-field != none {
|
|
277
|
+
// The signing field covers those blank lines — where a signature is
|
|
278
|
+
// actually written — so it is placed over the gap. It is placed BEFORE
|
|
279
|
+
// the gap is emitted: `place` anchors at the current flow position, so
|
|
280
|
+
// placing it after `v(gap)` anchors the box at the first name line and
|
|
281
|
+
// paints it down over the printed signature block. Anchored here it
|
|
282
|
+
// still travels with the block onto whatever page the block lands on,
|
|
283
|
+
// since the gap is inside the unbreakable block rather than ahead of
|
|
284
|
+
// it. The current position is below the closing line when there is
|
|
285
|
+
// one, which is where the signature is written.
|
|
286
|
+
//
|
|
287
|
+
// The widget keeps its own size (the helper's default is 50pt tall and
|
|
288
|
+
// it positions itself, so an `align` around it does nothing) and is
|
|
289
|
+
// offset to the BOTTOM of the gap: it always ends where the printed
|
|
290
|
+
// name begins, and whatever the gap has beyond the widget's height
|
|
291
|
+
// stays clear between the body text and the widget's frame.
|
|
292
|
+
let widget-height = {
|
|
293
|
+
let h = measure(signing-field).height
|
|
294
|
+
if h > 0pt { h } else { 50pt }
|
|
295
|
+
}
|
|
296
|
+
let drop = gap - widget-height - 3pt
|
|
297
|
+
place(
|
|
298
|
+
dx: left-pad,
|
|
299
|
+
dy: if drop > 0pt { drop } else { 0pt },
|
|
300
|
+
box(width: body-width - left-pad, height: widget-height, signing-field),
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
#v(gap)
|
|
304
|
+
#align(left)[
|
|
305
|
+
#pad(left: left-pad)[
|
|
306
|
+
#text(hyphenate: false)[
|
|
307
|
+
#for line in signature-lines {
|
|
308
|
+
// AFH 33-337: "indent the next line to begin under the third character
|
|
309
|
+
// of the line above" — 2-character indent ≈ 1em in Times New Roman 12pt
|
|
310
|
+
par(hanging-indent: .5em, line)
|
|
311
|
+
}
|
|
312
|
+
// The page the backmatter compares its own against. Must stay
|
|
313
|
+
// inside the unbreakable block: a marker outside one travels with
|
|
314
|
+
// the section that moves.
|
|
315
|
+
#metadata(none)<usaf-memo-flow-anchor>
|
|
316
|
+
#v(stride * reserved-lines)
|
|
317
|
+
]
|
|
318
|
+
]
|
|
319
|
+
]
|
|
320
|
+
]
|
|
321
|
+
// The reservation counted toward breaking height only; the backmatter
|
|
322
|
+
// emits its own lead-in below.
|
|
323
|
+
v(stride * -reserved-lines)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// The indorsement decision line is an "either / or" pair. Which pair prints
|
|
328
|
+
// follows from the indorsement's role in the coordination chain rather than
|
|
329
|
+
// from the endorser's choice, so the caller supplies the role and the same
|
|
330
|
+
// three `action` values carry over both pairs. The caller also suppresses the
|
|
331
|
+
// line entirely; reaching here means one is wanted.
|
|
332
|
+
|
|
333
|
+
// The option pair for each role, ordered (affirmative, negative).
|
|
334
|
+
#let APPROVAL_OPTIONS = ("Approve", "Disapprove")
|
|
335
|
+
#let COORDINATION_OPTIONS = ("Concur", "Nonconcur")
|
|
336
|
+
|
|
337
|
+
// Maps each `action` value to the index of the option it selects within
|
|
338
|
+
// whichever pair is rendered (`none` selects neither).
|
|
339
|
+
#let ACTION_SELECTIONS = (
|
|
340
|
+
"undecided": none,
|
|
341
|
+
"approve": 0,
|
|
342
|
+
"disapprove": 1,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
#let render-action-line(action, approval-authority: false, trailing-blank-line: true) = {
|
|
346
|
+
assert(
|
|
347
|
+
action in ACTION_SELECTIONS,
|
|
348
|
+
message: "action must be one of " + ACTION_SELECTIONS.keys().map(k => "\"" + k + "\"").join(", "),
|
|
349
|
+
)
|
|
350
|
+
let options = if approval-authority { APPROVAL_OPTIONS } else { COORDINATION_OPTIONS }
|
|
351
|
+
let selected = ACTION_SELECTIONS.at(action)
|
|
352
|
+
// Underline the selected option; strike the one it was chosen over. When
|
|
353
|
+
// neither is selected both render plain. An underline keeps the option on
|
|
354
|
+
// the line's own baseline — the box it replaces had to be nudged with
|
|
355
|
+
// `baseline` to sit straight — and reads as a mark made on the page, where a
|
|
356
|
+
// ruled rectangle in a PDF carrying real AcroForm widgets reads as one more
|
|
357
|
+
// fillable field.
|
|
358
|
+
let render-option(index) = {
|
|
359
|
+
let option = options.at(index)
|
|
360
|
+
if selected == none {
|
|
361
|
+
option
|
|
362
|
+
} else if selected == index {
|
|
363
|
+
underline(option)
|
|
364
|
+
} else {
|
|
365
|
+
strike(option)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// No leading blank-line: the caller (indorsement.typ) already emits the
|
|
369
|
+
// header→content gap once. The action line's `block(sticky: true)`
|
|
370
|
+
// additionally inherits `block.above: spacing.line` so the visual gap
|
|
371
|
+
// above matches the gap above a body's first paragraph.
|
|
372
|
+
//
|
|
373
|
+
// Keep the action line with the following content (body or signature block)
|
|
374
|
+
// using the same sticky-block pattern that body.typ applies to the last
|
|
375
|
+
// paragraph, per AFH 33-337 §11 orphan-prevention rules.
|
|
376
|
+
block(sticky: true)[#render-option(0) / #render-option(1)]
|
|
377
|
+
// Trailing blank-line places the body's first paragraph one line below
|
|
378
|
+
// the action, mirroring the gap above it. Suppressed when the body is
|
|
379
|
+
// empty so the signature block's own 4-line gap lands on AFH 33-337's
|
|
380
|
+
// "fifth line below the last line of text" anchor.
|
|
381
|
+
if trailing-blank-line {
|
|
382
|
+
blank-line()
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/// Renders a table in the memorandum's style.
|
|
387
|
+
///
|
|
388
|
+
/// AFH 33-337 does not specify table formatting, so this follows the general
|
|
389
|
+
/// aesthetic of the standard: plain black borders, a bold header row, no
|
|
390
|
+
/// decorative fills, and the body font and size inherited from the surrounding
|
|
391
|
+
/// text.
|
|
392
|
+
///
|
|
393
|
+
/// - it (content): The table element to style and render
|
|
394
|
+
/// -> content
|
|
395
|
+
#let render-memo-table(it) = {
|
|
396
|
+
show table.cell.where(y: 0): set text(weight: "bold")
|
|
397
|
+
set table(
|
|
398
|
+
stroke: 0.5pt + black,
|
|
399
|
+
inset: (x: 0.5em, y: 0.4em),
|
|
400
|
+
)
|
|
401
|
+
it
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
#let render-backmatter-section(
|
|
405
|
+
content,
|
|
406
|
+
section-label,
|
|
407
|
+
numbering-style: none,
|
|
408
|
+
continuation-label: none,
|
|
409
|
+
// Lines of breaking height reserved below this section for the lead-in and
|
|
410
|
+
// continuation note of the section that follows it. Reclaimed immediately
|
|
411
|
+
// after, so it moves where a break may fall and nothing else.
|
|
412
|
+
reserved-lines: 0,
|
|
413
|
+
) = {
|
|
414
|
+
let formatted-content = {
|
|
415
|
+
// `text()` keeps the label from being laid out as a paragraph of its own.
|
|
416
|
+
text()[#section-label]
|
|
417
|
+
linebreak()
|
|
418
|
+
if numbering-style != none {
|
|
419
|
+
let items = ensure-array(content)
|
|
420
|
+
enum(..items, numbering: numbering-style)
|
|
421
|
+
} else {
|
|
422
|
+
join-lines(content)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Attachments pass continuation-label ("… (listed on next page):" per AFH 33-337).
|
|
427
|
+
// cc: and DISTRIBUTION: use a neutral default — "listed" applies to attachment lists only.
|
|
428
|
+
let continuation-text = if continuation-label != none {
|
|
429
|
+
text()[#continuation-label]
|
|
430
|
+
} else {
|
|
431
|
+
text()[#(section-label + " (continued on next page)")]
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
context {
|
|
435
|
+
// `breakable: false` lets Typst's own breaker move the section to the next
|
|
436
|
+
// page as a unit; an explicit pagebreak here would feed this context's
|
|
437
|
+
// layout query back into its own input and the two would chase each other
|
|
438
|
+
// until layout gave up.
|
|
439
|
+
//
|
|
440
|
+
// The note is decided by observing where that breaker put the block. The
|
|
441
|
+
// nearest anchor above is the signature block or the preceding section — an
|
|
442
|
+
// element with real height that stays on the page this section departs.
|
|
443
|
+
//
|
|
444
|
+
// Do not reduce this to a `here()` reading. `here()` sits in the section and
|
|
445
|
+
// travels with it, so once the section has moved it reports the top of the
|
|
446
|
+
// page it moved to and every section measures as fitting. The room the note
|
|
447
|
+
// needs is reserved inside the anchor's own unbreakable block, so the
|
|
448
|
+
// decision can never move its own input.
|
|
449
|
+
let above = query(selector(<usaf-memo-flow-anchor>).before(here()))
|
|
450
|
+
let below = query(selector(<usaf-memo-flow-anchor>).after(here()))
|
|
451
|
+
if above.len() > 0 and below.len() > 0 {
|
|
452
|
+
if below.first().location().page() > above.last().location().page() {
|
|
453
|
+
continuation-text
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// A section taller than a page cannot move as a unit, so it splits where
|
|
457
|
+
// the page ends rather than overflowing it, and anchors its last page too:
|
|
458
|
+
// the section below measures from where this one ends.
|
|
459
|
+
let reserve = line-stride() * reserved-lines
|
|
460
|
+
layout(region => {
|
|
461
|
+
let oversized = measure(formatted-content, width: region.width).height + reserve > region.height
|
|
462
|
+
block(breakable: oversized)[
|
|
463
|
+
#metadata(none)<usaf-memo-flow-anchor>
|
|
464
|
+
#formatted-content
|
|
465
|
+
#if oversized [#metadata(none)<usaf-memo-flow-anchor>]
|
|
466
|
+
#v(reserve)
|
|
467
|
+
]
|
|
468
|
+
})
|
|
469
|
+
v(-reserve)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
#let calculate-backmatter-spacing(is-first-section) = {
|
|
474
|
+
context {
|
|
475
|
+
let line-count = if is-first-section { 2 } else { 1 }
|
|
476
|
+
blank-lines(line-count)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// AFH 33-337 "Attachment or Attachments": "Place 'Attachment:' (for a single attachment)
|
|
481
|
+
// or '# Attachments:' (for two or more attachments) at the left margin, on the third
|
|
482
|
+
// line below the signature element"
|
|
483
|
+
// AFH 33-337 "Courtesy Copy Element": "place 'cc:' flush with the left margin, on the
|
|
484
|
+
// second line below the attachment element"
|
|
485
|
+
|
|
486
|
+
#let render-backmatter-sections(
|
|
487
|
+
attachments: none,
|
|
488
|
+
cc: none,
|
|
489
|
+
distribution: none,
|
|
490
|
+
leading-pagebreak: false,
|
|
491
|
+
) = {
|
|
492
|
+
let has-backmatter = (
|
|
493
|
+
(attachments != none and attachments.len() > 0)
|
|
494
|
+
or (cc != none and cc.len() > 0)
|
|
495
|
+
or (distribution != none and distribution.len() > 0)
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
if leading-pagebreak and has-backmatter {
|
|
499
|
+
pagebreak(weak: true)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Collected first because a section's reservation depends on whether another
|
|
503
|
+
// follows it: with nothing below, no note can be owed.
|
|
504
|
+
let sections = ()
|
|
505
|
+
|
|
506
|
+
if attachments != none and attachments.len() > 0 {
|
|
507
|
+
let attachment-count = attachments.len()
|
|
508
|
+
let section-label = if attachment-count == 1 { "Attachment:" } else { str(attachment-count) + " Attachments:" }
|
|
509
|
+
let continuation-label = (
|
|
510
|
+
(if attachment-count == 1 { "Attachment" } else { str(attachment-count) + " Attachments" })
|
|
511
|
+
+ " (listed on next page):"
|
|
512
|
+
)
|
|
513
|
+
sections.push((
|
|
514
|
+
content: attachments,
|
|
515
|
+
label: section-label,
|
|
516
|
+
// AFH 33-337: a single attachment is not numbered; numbering applies to two or more.
|
|
517
|
+
numbering-style: if attachment-count == 1 { none } else { "1." },
|
|
518
|
+
continuation-label: continuation-label,
|
|
519
|
+
))
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if cc != none and cc.len() > 0 {
|
|
523
|
+
sections.push((content: cc, label: "cc:", numbering-style: none, continuation-label: none))
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if distribution != none and distribution.len() > 0 {
|
|
527
|
+
sections.push((content: distribution, label: "DISTRIBUTION:", numbering-style: none, continuation-label: none))
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
for (index, section) in sections.enumerate() {
|
|
531
|
+
calculate-backmatter-spacing(index == 0)
|
|
532
|
+
// The next section's one blank line of lead-in, plus one for its note. The
|
|
533
|
+
// signature block reserves 3: the first section's lead-in is two lines.
|
|
534
|
+
let reserved-lines = if index + 1 < sections.len() { 2 } else { 0 }
|
|
535
|
+
render-backmatter-section(
|
|
536
|
+
section.content,
|
|
537
|
+
section.label,
|
|
538
|
+
numbering-style: section.numbering-style,
|
|
539
|
+
continuation-label: section.continuation-label,
|
|
540
|
+
reserved-lines: reserved-lines,
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|