@airmark/quiver 0.32.2 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -21
- package/package.json +9 -7
- package/quills/af4141/0.1.0/Quill.yaml +4 -13
- package/quills/af4141/0.1.0/example.md +53 -0
- package/quills/afmc_moa/0.0.1/Quill.yaml +39 -62
- package/quills/afmc_moa/0.0.1/example.md +114 -0
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/config.typ +1 -1
- package/quills/classic_resume/0.0.1/Quill.yaml +281 -0
- package/quills/classic_resume/0.0.1/example.md +111 -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 +214 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/config.typ +61 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/layout.typ +86 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/lib.typ +7 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/util.typ +37 -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 +327 -0
- package/quills/cyber_ribbon_chart/0.0.1/example.md +55 -0
- package/quills/cyber_ribbon_chart/0.0.1/plate.typ +609 -0
- package/quills/daf1206/0.1.0/Quill.yaml +15 -18
- package/quills/daf1206/0.1.0/example.md +55 -0
- package/quills/daf4392/0.1.0/Quill.yaml +16 -7
- package/quills/daf4392/0.1.0/example.md +51 -0
- package/quills/daf4392/0.1.0/plate.typ +2 -2
- package/quills/usaf_letter/0.1.0/Quill.yaml +179 -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/example.md +35 -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/CopperplateCC/CopperplateCC-Bold.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_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE → usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/OFL.txt} +2 -2
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/SpectralSC-Italic.ttf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/SpectralSC-Regular.ttf +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 +121 -0
- package/quills/usaf_memo/0.2.0/Quill.yaml +8 -41
- package/quills/usaf_memo/0.2.0/example.md +45 -0
- 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 +51 -69
- package/quills/usaf_memo/0.3.0/example.md +48 -0
- 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/SpectralSC/OFL.txt +93 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/SpectralSC-Italic.ttf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/SpectralSC-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 +158 -111
- 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 +78 -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/deros_extension.md +40 -0
- package/templates/pass_request.md +48 -0
- package/templates/rebuttal.md +63 -0
- package/templates/templates.json +65 -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
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// The personal letter's heading elements, and the page setup the rest of the
|
|
2
|
+
// document inherits: AFH 33-337 "The Personal Letter".
|
|
3
|
+
|
|
4
|
+
#import "primitives.typ": *
|
|
5
|
+
|
|
6
|
+
#let frontmatter(
|
|
7
|
+
letter-for: none,
|
|
8
|
+
letter-from: none,
|
|
9
|
+
salutation: none,
|
|
10
|
+
date: none,
|
|
11
|
+
letterhead-title: "DEPARTMENT OF THE AIR FORCE",
|
|
12
|
+
letterhead-caption: "[YOUR SQUADRON/UNIT NAME]",
|
|
13
|
+
letterhead-seal: none,
|
|
14
|
+
letterhead-seal-subtitle: none, // optional line under seal (9pt bold caps); ignored if no seal
|
|
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
|
+
letterhead-font: DEFAULT_LETTERHEAD_FONTS,
|
|
18
|
+
body-font: DEFAULT_BODY_FONTS,
|
|
19
|
+
font-size: 12pt,
|
|
20
|
+
classification-level: none,
|
|
21
|
+
dissemination: none,
|
|
22
|
+
cui-controlled-by: none,
|
|
23
|
+
cui-category: none,
|
|
24
|
+
cui-limited-dissemination: none,
|
|
25
|
+
cui-poc: none,
|
|
26
|
+
footer-tag-line: none,
|
|
27
|
+
it,
|
|
28
|
+
) = {
|
|
29
|
+
assert(letter-for != none, message: "letter-for is required")
|
|
30
|
+
|
|
31
|
+
let actual-date = if date == none { datetime.today() } else { date }
|
|
32
|
+
|
|
33
|
+
// The banner is `LEVEL` or `LEVEL//SUFFIX`. `classification-level` is an enum
|
|
34
|
+
// (a `str`), but `dissemination` may arrive as content, which `str + str`
|
|
35
|
+
// cannot absorb — so the marking is assembled as content instead.
|
|
36
|
+
let classification-marking = if classification-level == none or type(classification-level) != str {
|
|
37
|
+
none
|
|
38
|
+
} else {
|
|
39
|
+
let base = classification-level.trim()
|
|
40
|
+
if base == "" {
|
|
41
|
+
none
|
|
42
|
+
} else if falsey(dissemination) {
|
|
43
|
+
[#base]
|
|
44
|
+
} else {
|
|
45
|
+
// `//` is a line comment in Typst markup, so the separator is
|
|
46
|
+
// interpolated as a string rather than written literally. The suffix is
|
|
47
|
+
// boxed so the markup block's edge newlines do not read as a space and
|
|
48
|
+
// split the banner into `CUI// NF`.
|
|
49
|
+
let separator = "//"
|
|
50
|
+
[#base#separator#box(upper(dissemination))]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
let classification-color = get-classification-level-color(classification-level)
|
|
54
|
+
|
|
55
|
+
// Build the CUI designation indicator block (DoDM 5200.48, Table 1), shown
|
|
56
|
+
// only for CUI when at least one indicator field is set. Rendered as a
|
|
57
|
+
// bottom-right page-1 float (see placement below).
|
|
58
|
+
let cui-indicator = if (
|
|
59
|
+
classification-level != none
|
|
60
|
+
and type(classification-level) == str
|
|
61
|
+
and classification-level.trim().starts-with("CUI")
|
|
62
|
+
) {
|
|
63
|
+
// An indicator may arrive as content or as a `str`; `falsey` is the
|
|
64
|
+
// presence test that reads both shapes.
|
|
65
|
+
let lines = ()
|
|
66
|
+
if not falsey(cui-controlled-by) {
|
|
67
|
+
lines.push([Controlled By: #cui-controlled-by])
|
|
68
|
+
}
|
|
69
|
+
if not falsey(cui-category) {
|
|
70
|
+
lines.push([CUI Category: #cui-category])
|
|
71
|
+
}
|
|
72
|
+
if not falsey(cui-limited-dissemination) {
|
|
73
|
+
lines.push([LDC: #upper(cui-limited-dissemination)])
|
|
74
|
+
}
|
|
75
|
+
if not falsey(cui-poc) {
|
|
76
|
+
lines.push([POC: #cui-poc])
|
|
77
|
+
}
|
|
78
|
+
if lines.len() > 0 { lines.join(linebreak()) } else { none }
|
|
79
|
+
} else {
|
|
80
|
+
none
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
set par(leading: spacing.line, spacing: spacing.line, justify: false)
|
|
84
|
+
set block(above: spacing.line, below: 0em, spacing: 0em)
|
|
85
|
+
set text(font: body-font, size: font-size, fallback: true)
|
|
86
|
+
show raw: set text(font: DEFAULT_MONO_FONTS)
|
|
87
|
+
|
|
88
|
+
set page(
|
|
89
|
+
paper: "us-letter",
|
|
90
|
+
// AFH 33-337: 1-inch margins on the left, right and bottom, on every page,
|
|
91
|
+
// and on the top of every page but the first, whose text starts lower (the
|
|
92
|
+
// `v()` below the letterhead).
|
|
93
|
+
margin: (
|
|
94
|
+
left: spacing.margin,
|
|
95
|
+
right: spacing.margin,
|
|
96
|
+
top: spacing.margin,
|
|
97
|
+
bottom: spacing.margin,
|
|
98
|
+
),
|
|
99
|
+
header: {
|
|
100
|
+
// AFH 33-337: the first page of a personal letter is never numbered.
|
|
101
|
+
// Succeeding pages are numbered from 2, flush with the right margin.
|
|
102
|
+
context if counter(page).get().first() > 1 {
|
|
103
|
+
place(
|
|
104
|
+
dy: +.5in,
|
|
105
|
+
block(
|
|
106
|
+
width: 100%,
|
|
107
|
+
align(right, text(12pt)[#counter(page).display()]),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if classification-marking != none {
|
|
113
|
+
place(
|
|
114
|
+
top + center,
|
|
115
|
+
dy: 0.375in,
|
|
116
|
+
text(12pt, font: DEFAULT_BODY_FONTS, fill: classification-color)[#strong(classification-marking)],
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
footer: {
|
|
121
|
+
if classification-marking != none {
|
|
122
|
+
place(
|
|
123
|
+
bottom + center,
|
|
124
|
+
dy: -.375in,
|
|
125
|
+
text(12pt, font: DEFAULT_BODY_FONTS, fill: classification-color)[#strong(classification-marking)],
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if not falsey(footer-tag-line) {
|
|
130
|
+
place(
|
|
131
|
+
bottom + center,
|
|
132
|
+
dy: -0.625in,
|
|
133
|
+
align(center)[
|
|
134
|
+
#text(fill: LETTERHEAD_COLOR, font: "Spectral SC", size: 15pt)[#footer-tag-line]
|
|
135
|
+
],
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
// DoDM 5200.48 §3: CUI designation indicator block — page 1 only, bottom-right
|
|
142
|
+
// corner, dropped into the 0.5in page-edge band. Emitted as a bottom float so
|
|
143
|
+
// it (1) reserves flow space, raising page 1's effective bottom margin so body
|
|
144
|
+
// 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.
|
|
146
|
+
if cui-indicator != none {
|
|
147
|
+
context {
|
|
148
|
+
// The box shrink-wraps to its widest line; `set align(left)` keeps the
|
|
149
|
+
// text flush-left within it, overriding the `align(right)` the placement
|
|
150
|
+
// below imposes.
|
|
151
|
+
let indicator-box = box({
|
|
152
|
+
set text(font: DEFAULT_BODY_FONTS, size: 10pt)
|
|
153
|
+
set par(leading: 0.4em, spacing: 0pt)
|
|
154
|
+
set align(left)
|
|
155
|
+
cui-indicator
|
|
156
|
+
})
|
|
157
|
+
// Reserve only the part of the block inside the text area (`reserved`):
|
|
158
|
+
// float a box of that height, then `place` the full block inside it pushed
|
|
159
|
+
// down by `overhang` so the surplus overflows into the edge band. (A bare
|
|
160
|
+
// `box(height: reserved, indicator-box)` overflows *upward* into the body
|
|
161
|
+
// instead.) The inner `place` adds no size, so the box stays `reserved`
|
|
162
|
+
// tall and the block's bottom lands 0.5in from the page edge.
|
|
163
|
+
let overhang = spacing.margin - 0.5in
|
|
164
|
+
let reserved = measure(indicator-box).height - overhang
|
|
165
|
+
place(
|
|
166
|
+
bottom + right,
|
|
167
|
+
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
|
+
// Minimum gap to the body's last line; the actual gap is larger when the
|
|
172
|
+
// next paragraph can't fit above the block and breaks to the next page.
|
|
173
|
+
clearance: spacing.line,
|
|
174
|
+
box(height: reserved, place(bottom + right, dy: overhang, indicator-box)),
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
render-letterhead(
|
|
180
|
+
letterhead-title,
|
|
181
|
+
letterhead-caption,
|
|
182
|
+
letterhead-font,
|
|
183
|
+
letterhead-seal: letterhead-seal,
|
|
184
|
+
letterhead-seal-subtitle: letterhead-seal-subtitle,
|
|
185
|
+
letterhead-emblem: letterhead-emblem,
|
|
186
|
+
letterhead-emblem-height: letterhead-emblem-height,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
// AFH 33-337: the date sits 1.75 inches from the top of the first page. The
|
|
190
|
+
// top margin is 1 inch, so the remainder is made up here.
|
|
191
|
+
v(1.75in - spacing.margin)
|
|
192
|
+
|
|
193
|
+
// Measure one line's stride once, under the typography just set, for the
|
|
194
|
+
// blank-line spacing every element below is laid out on.
|
|
195
|
+
context {
|
|
196
|
+
let one-line = measure(par(spacing: 0pt)[x]).height
|
|
197
|
+
LINE_STRIDE.update(measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
render-date-section(actual-date)
|
|
201
|
+
|
|
202
|
+
// AFH 33-337: the sender's block on the second line below the date, the
|
|
203
|
+
// receiver's on the third line below the sender's last line. With no sender's
|
|
204
|
+
// block the receiver's takes the line the sender's would have started on.
|
|
205
|
+
let has-sender = not falsey(letter-from)
|
|
206
|
+
if has-sender {
|
|
207
|
+
blank-line()
|
|
208
|
+
join-lines(letter-from)
|
|
209
|
+
}
|
|
210
|
+
blank-lines(if has-sender { 2 } else { 1 })
|
|
211
|
+
join-lines(letter-for)
|
|
212
|
+
|
|
213
|
+
// AFH 33-337: the salutation sits on the second line below the receiver's
|
|
214
|
+
// address, and takes no punctuation after the receiver's last name. A blank
|
|
215
|
+
// one prints nothing and takes no line, so the body opens on the second line
|
|
216
|
+
// below the receiver's address.
|
|
217
|
+
if not falsey(salutation) {
|
|
218
|
+
blank-line()
|
|
219
|
+
[#salutation]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// The body opens on the second line below the salutation, which `mainmatter`
|
|
223
|
+
// supplies: the gap above its first paragraph is the same blank line it puts
|
|
224
|
+
// between any two.
|
|
225
|
+
it
|
|
226
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Public API of the USAF personal letter template, which typesets per AFH
|
|
2
|
+
// 33-337 "The Tongue and Quill", "The Personal Letter".
|
|
3
|
+
//
|
|
4
|
+
// The three sections compose in document order: `frontmatter` and `mainmatter`
|
|
5
|
+
// as show rules, `backmatter` as a function.
|
|
6
|
+
//
|
|
7
|
+
// `config.typ`, `utils.typ` and `primitives.typ` are `tonguetoquill-usaf-memo`
|
|
8
|
+
// 5.0.0's, byte for byte, so the letterhead, signature block and closing
|
|
9
|
+
// elements the two documents share are set by one implementation and a sync is
|
|
10
|
+
// a copy. The other files are the letter's own.
|
|
11
|
+
//
|
|
12
|
+
// #import "@preview/tonguetoquill-usaf-letter:0.1.0": (
|
|
13
|
+
// backmatter, frontmatter, mainmatter,
|
|
14
|
+
// )
|
|
15
|
+
//
|
|
16
|
+
// #show: frontmatter.with(
|
|
17
|
+
// letter-from: ("Colonel Jane A. Doe", "Commander", "123d Example Squadron",
|
|
18
|
+
// "1 Example Way", "Placeholder AFB ST 12345-6789"),
|
|
19
|
+
// letter-for: ("Major John B. Smith", "Chief, Plans and Programs",
|
|
20
|
+
// "456th Example Group", "2 Sample Blvd", "Sample AFB ST 98765-4321"),
|
|
21
|
+
// salutation: "Dear Major Smith",
|
|
22
|
+
// )
|
|
23
|
+
// #show: mainmatter
|
|
24
|
+
//
|
|
25
|
+
// Body paragraphs.
|
|
26
|
+
//
|
|
27
|
+
// #backmatter(
|
|
28
|
+
// complimentary-close: "Sincerely",
|
|
29
|
+
// signature-block: ("FIRST M. LAST, Rank, USAF",),
|
|
30
|
+
// )
|
|
31
|
+
|
|
32
|
+
#import "frontmatter.typ": frontmatter
|
|
33
|
+
#import "mainmatter.typ": mainmatter
|
|
34
|
+
#import "backmatter.typ": backmatter
|
|
35
|
+
#import "utils.typ": date-pattern
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// The personal letter's body: AFH 33-337 "For the body of the personal letter,
|
|
2
|
+
// the paragraphs are indented 0.5 inches from the left margin but they are not
|
|
3
|
+
// numbered."
|
|
4
|
+
|
|
5
|
+
#import "config.typ": *
|
|
6
|
+
#import "utils.typ": *
|
|
7
|
+
#import "primitives.typ": render-memo-table
|
|
8
|
+
|
|
9
|
+
// AFH 33-337: a personal letter's paragraphs are indented half an inch from the
|
|
10
|
+
// left margin and carry no number or letter.
|
|
11
|
+
#let paragraph-indent = 0.5in
|
|
12
|
+
|
|
13
|
+
/// Splits content at the closing section: the part this styles as body
|
|
14
|
+
/// paragraphs, and the part that reaches the page untouched.
|
|
15
|
+
///
|
|
16
|
+
/// Applied as a show rule, `mainmatter` is handed the closing section along
|
|
17
|
+
/// with the body, and the body's first-line indent would then displace the
|
|
18
|
+
/// complimentary close and the signature lines. `backmatter` labels its output
|
|
19
|
+
/// to mark the boundary.
|
|
20
|
+
///
|
|
21
|
+
/// Everything from the marker on stays together, prose a caller wrote after the
|
|
22
|
+
/// closing included.
|
|
23
|
+
///
|
|
24
|
+
/// A marker is found on `it` itself, on a direct child, or inside the `styled`
|
|
25
|
+
/// element that a `set` or `show` rule after `#show: mainmatter` wraps the
|
|
26
|
+
/// remainder in; both halves come back under those styles. A marker a caller
|
|
27
|
+
/// nested in a container of their own — a `block`, a `grid` cell — is not
|
|
28
|
+
/// found. A closing section built in a code block or a loop is not nested:
|
|
29
|
+
/// joining content extends the sequence on the left in place, so its marker
|
|
30
|
+
/// stays a direct child.
|
|
31
|
+
///
|
|
32
|
+
/// - it (content): Content handed to `mainmatter`
|
|
33
|
+
/// -> array: the body content and the closing content
|
|
34
|
+
#let split-closing(it) = {
|
|
35
|
+
if it.at("label", default: none) == <usaf-letter-closing> { return ([], it) }
|
|
36
|
+
// `styled` is not a name in scope; its two fields identify it.
|
|
37
|
+
if it.has("child") and it.has("styles") {
|
|
38
|
+
let (body, closing) = split-closing(it.child)
|
|
39
|
+
let restyle = part => (it.func())(part, it.styles)
|
|
40
|
+
return (restyle(body), restyle(closing))
|
|
41
|
+
}
|
|
42
|
+
if not it.has("children") { return (it, []) }
|
|
43
|
+
let children = it.children
|
|
44
|
+
let boundary = children.position(child => child.at("label", default: none) == <usaf-letter-closing>)
|
|
45
|
+
if boundary == none { return (it, []) }
|
|
46
|
+
// `sum` over `join`: an empty half is `none` from a join and `[]` from this.
|
|
47
|
+
(children.slice(0, boundary).sum(default: []), children.slice(boundary).sum(default: []))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The body's blocks in order — its own paragraphs, and each list, table and
|
|
51
|
+
// block quote whole — so the last can be found; and how deep in one of those
|
|
52
|
+
// containers the current paragraph sits.
|
|
53
|
+
#let BLOCKS = counter("usaf-letter-blocks")
|
|
54
|
+
#let NESTED = state("usaf-letter-nested", 0)
|
|
55
|
+
|
|
56
|
+
/// Whether a body's last block holds the signature to its page: a sticky block
|
|
57
|
+
/// relocates whole rather than splitting, so one taller than a third of the
|
|
58
|
+
/// text height is left to the break, which leaves its own tail above the
|
|
59
|
+
/// signature.
|
|
60
|
+
///
|
|
61
|
+
/// - it (content): The last block
|
|
62
|
+
/// -> bool
|
|
63
|
+
#let keeps-signature(it) = {
|
|
64
|
+
let budget = (page.height - spacing.margin * 2) / 3
|
|
65
|
+
measure(it, width: page.width - spacing.margin * 2).height <= budget
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Counts a container as one block of the body and the paragraphs inside it
|
|
69
|
+
/// as none, and sets it a blank line off the paragraphs around it.
|
|
70
|
+
///
|
|
71
|
+
/// - it (content): The container
|
|
72
|
+
/// -> content
|
|
73
|
+
#let container(it) = context {
|
|
74
|
+
if NESTED.get() > 0 { return it }
|
|
75
|
+
BLOCKS.step()
|
|
76
|
+
NESTED.update(n => n + 1)
|
|
77
|
+
context {
|
|
78
|
+
let last = BLOCKS.get() == BLOCKS.final()
|
|
79
|
+
block(above: par.spacing, sticky: last and keeps-signature(it), it)
|
|
80
|
+
}
|
|
81
|
+
NESTED.update(n => n - 1)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Show rule for the personal letter's body.
|
|
85
|
+
///
|
|
86
|
+
/// - it (content): Body content
|
|
87
|
+
/// -> content
|
|
88
|
+
#let mainmatter(it) = {
|
|
89
|
+
let (body, closing) = split-closing(it)
|
|
90
|
+
context {
|
|
91
|
+
// `par.leading` and `par.spacing` are both half an em, so a paragraph break
|
|
92
|
+
// would otherwise land on the page as an ordinary line break. A blank line
|
|
93
|
+
// is what separates one letter paragraph from the next.
|
|
94
|
+
set par(
|
|
95
|
+
first-line-indent: (amount: paragraph-indent, all: true),
|
|
96
|
+
spacing: spacing.line + line-stride(),
|
|
97
|
+
)
|
|
98
|
+
// The indent belongs to the letter's own paragraphs. Inside a list, a
|
|
99
|
+
// quote, or a table cell, a line already sits where its container put it.
|
|
100
|
+
show list: it => {
|
|
101
|
+
set par(first-line-indent: 0pt)
|
|
102
|
+
container(it)
|
|
103
|
+
}
|
|
104
|
+
show enum: it => {
|
|
105
|
+
set par(first-line-indent: 0pt)
|
|
106
|
+
container(it)
|
|
107
|
+
}
|
|
108
|
+
show table: it => {
|
|
109
|
+
set par(first-line-indent: 0pt)
|
|
110
|
+
container(render-memo-table(it))
|
|
111
|
+
}
|
|
112
|
+
// A block quote is the body's unindented block: the author's lines as
|
|
113
|
+
// written, flush with the margin, which is what lets a letter carry a
|
|
114
|
+
// roster of names or an address without the paragraph indent claiming its
|
|
115
|
+
// first line.
|
|
116
|
+
show quote.where(block: true): it => {
|
|
117
|
+
set par(first-line-indent: 0pt)
|
|
118
|
+
container(it.body)
|
|
119
|
+
}
|
|
120
|
+
show raw.where(block: true): container
|
|
121
|
+
// AFH 33-337: "Do not place the signature element on a continuation page
|
|
122
|
+
// by itself." The signature block has no keep-with-previous of its own —
|
|
123
|
+
// Typst has no such property — so the anchor comes from this side: the
|
|
124
|
+
// body's last block, a paragraph or a container, is set sticky, carried
|
|
125
|
+
// onto the next page along with the signature instead of breaking away
|
|
126
|
+
// from it.
|
|
127
|
+
show par: p => context {
|
|
128
|
+
if NESTED.get() > 0 { return p }
|
|
129
|
+
BLOCKS.step()
|
|
130
|
+
context {
|
|
131
|
+
if BLOCKS.get() != BLOCKS.final() { return p }
|
|
132
|
+
// `above` stands in for the paragraph spacing a bare paragraph would
|
|
133
|
+
// have taken from the one before it.
|
|
134
|
+
block(sticky: keeps-signature(p), above: par.spacing, p)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
body
|
|
138
|
+
}
|
|
139
|
+
closing
|
|
140
|
+
}
|