@tonguetoquill/collection 0.2.5-beta.1 → 0.2.5
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/LICENSE +201 -201
- package/README.md +39 -39
- package/index.d.ts +2 -2
- package/index.js +8 -8
- package/package.json +41 -41
- package/quills/af4141/0.1.0/Quill.yaml +92 -88
- package/quills/af4141/0.1.0/assets/GNU General Public License.txt +340 -0
- package/quills/af4141/0.1.0/assets/NimbusRomNo9L-Med.otf +0 -0
- package/quills/af4141/0.1.0/assets/NimbusRomNo9L-MedIta.otf +0 -0
- package/quills/af4141/0.1.0/assets/NimbusRomNo9L-Reg.otf +0 -0
- package/quills/af4141/0.1.0/assets/NimbusRomNo9L-RegIta.otf +0 -0
- package/quills/af4141/0.1.0/design/TASK.md +19 -19
- package/quills/af4141/0.1.0/example.md +35 -35
- package/quills/af4141/0.1.0/packages/typst-af4141/FIELDS.json +3169 -3169
- package/quills/af4141/0.1.0/packages/typst-af4141/form.typ +538 -538
- package/quills/af4141/0.1.0/packages/typst-af4141/lib.typ +227 -227
- package/quills/af4141/0.1.0/packages/typst-af4141/typst.toml +7 -7
- package/quills/af4141/0.1.0/plate.typ +50 -48
- package/quills/classic_resume/0.1.0/Quill.yaml +118 -118
- package/quills/classic_resume/0.1.0/example.md +232 -232
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/LICENSE +21 -21
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/README.md +38 -38
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/components.typ +184 -184
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/layout.typ +42 -42
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/lib.typ +5 -5
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/typst.toml +26 -26
- package/quills/classic_resume/0.1.0/plate.typ +44 -44
- package/quills/cmu_letter/0.1.0/.quillignore +30 -30
- package/quills/cmu_letter/0.1.0/Quill.yaml +64 -64
- package/quills/cmu_letter/0.1.0/assets/cmu-wordmark.svg +174 -174
- package/quills/cmu_letter/0.1.0/example.md +30 -30
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/LICENSE +21 -21
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/README.txt +100 -100
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/backmatter.typ +13 -13
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/config.typ +39 -39
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/frontmatter.typ +72 -72
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/lib.typ +47 -47
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/mainmatter.typ +42 -42
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/primitives.typ +70 -70
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/utils.typ +85 -85
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/typst.toml +17 -17
- package/quills/cmu_letter/0.1.0/plate.typ +19 -19
- package/quills/taro/0.1.0/Quill.yaml +29 -29
- package/quills/taro/0.1.0/example.md +26 -26
- package/quills/taro/0.1.0/plate.typ +31 -31
- package/quills/usaf_memo/0.1.0/.quillignore +30 -30
- package/quills/usaf_memo/0.1.0/Quill.yaml +209 -209
- package/quills/usaf_memo/0.1.0/example.md +54 -54
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -21
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -93
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -79
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +339 -339
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -28
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/body.typ +332 -332
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/config.typ +63 -63
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -114
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -118
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -55
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -32
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +272 -272
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/utils.typ +377 -377
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/typst.toml +16 -16
- package/quills/usaf_memo/0.1.0/plate.typ +74 -74
- package/quills/usaf_memo/0.2.0/.quillignore +30 -30
- package/quills/usaf_memo/0.2.0/Quill.yaml +219 -219
- package/quills/usaf_memo/0.2.0/example.md +55 -55
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/.gitignore +6 -6
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -21
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -93
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -79
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +339 -339
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -28
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +333 -333
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/config.typ +64 -64
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -114
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -118
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -55
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -32
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +293 -293
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +374 -374
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +27 -27
- package/quills/usaf_memo/0.2.0/plate.typ +75 -75
- package/templates/af4141.md +88 -88
- package/templates/cmu_letter_template.md +37 -37
- package/templates/loc.md +78 -78
- package/templates/pass_request.md +43 -43
- package/templates/rebuttal.md +55 -55
- package/templates/taro.md +26 -26
- package/templates/templates.json +49 -55
- package/templates/usaf_template.md +23 -23
- package/templates/ussf_template.md +29 -29
- package/quills/daf4392/0.1.0/Quill.yaml +0 -110
- package/quills/daf4392/0.1.0/assets/arimo-v35-latin-700.ttf +0 -0
- package/quills/daf4392/0.1.0/assets/arimo-v35-latin-700italic.ttf +0 -0
- package/quills/daf4392/0.1.0/assets/arimo-v35-latin-italic.ttf +0 -0
- package/quills/daf4392/0.1.0/assets/arimo-v35-latin-regular.ttf +0 -0
- package/quills/daf4392/0.1.0/assets/page1.png +0 -0
- package/quills/daf4392/0.1.0/example.md +0 -33
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/FIELDS.json +0 -9
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/form.typ +0 -14
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/lib.typ +0 -227
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/out/debug.typ +0 -4
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/out/example.typ +0 -4
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/page1.png +0 -0
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/typst.toml +0 -7
- package/quills/daf4392/0.1.0/plate.typ +0 -60
- package/templates/daf4392.md +0 -33
|
@@ -1,293 +1,293 @@
|
|
|
1
|
-
// primitives.typ: Reusable rendering primitives for USAF memorandum sections
|
|
2
|
-
//
|
|
3
|
-
// This module implements the visual rendering functions that produce AFH 33-337
|
|
4
|
-
// compliant formatting for all sections of a USAF memorandum. Each function
|
|
5
|
-
// corresponds to specific placement and formatting requirements from Chapter 14.
|
|
6
|
-
|
|
7
|
-
#import "config.typ": *
|
|
8
|
-
#import "utils.typ": *
|
|
9
|
-
|
|
10
|
-
// =============================================================================
|
|
11
|
-
// LETTERHEAD RENDERING
|
|
12
|
-
// =============================================================================
|
|
13
|
-
// AFH 33-337 §1: "Use printed letterhead, computer-generated letterhead, or plain bond paper"
|
|
14
|
-
// Letterhead placement is not explicitly specified in AFH 33-337, but follows
|
|
15
|
-
// standard USAF memo formatting conventions
|
|
16
|
-
|
|
17
|
-
#let render-letterhead(title, caption, letterhead-seal, font) = {
|
|
18
|
-
font = ensure-array(font)
|
|
19
|
-
caption = ensure-string(caption)
|
|
20
|
-
|
|
21
|
-
place(
|
|
22
|
-
dy: 0.625in - spacing.margin,
|
|
23
|
-
box(
|
|
24
|
-
width: 100%,
|
|
25
|
-
fill: none,
|
|
26
|
-
stroke: none,
|
|
27
|
-
[
|
|
28
|
-
#place(
|
|
29
|
-
center + top,
|
|
30
|
-
align(center)[
|
|
31
|
-
#set text(12pt, font: font, fill: LETTERHEAD_COLOR)
|
|
32
|
-
#title\
|
|
33
|
-
#text(10.5pt)[#caption]
|
|
34
|
-
],
|
|
35
|
-
)
|
|
36
|
-
],
|
|
37
|
-
),
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
if letterhead-seal != none {
|
|
41
|
-
place(
|
|
42
|
-
left + top,
|
|
43
|
-
dx: -0.5in,
|
|
44
|
-
dy: -.5in,
|
|
45
|
-
block[
|
|
46
|
-
#fit-box(width: 2in, height: 1in)[#letterhead-seal]
|
|
47
|
-
],
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// =============================================================================
|
|
53
|
-
// HEADER SECTIONS
|
|
54
|
-
// =============================================================================
|
|
55
|
-
// AFH 33-337 "The Heading Section" specifies exact placement and format for:
|
|
56
|
-
// - Date: 1 inch from right edge, 1.75 inches from top
|
|
57
|
-
// - MEMORANDUM FOR: Second line below date
|
|
58
|
-
// - FROM: Second line below MEMORANDUM FOR
|
|
59
|
-
// - SUBJECT: Second line below FROM
|
|
60
|
-
|
|
61
|
-
// AFH 33-337 "Date": "Place the date 1 inch from the right edge, 1.75 inches from the top"
|
|
62
|
-
#let render-date-section(date) = {
|
|
63
|
-
align(right)[#display-date(date)]
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// AFH 33-337 "MEMORANDUM FOR": "Place 'MEMORANDUM FOR' on the second line below the date"
|
|
67
|
-
#let render-for-section(recipients, cols) = {
|
|
68
|
-
blank-line()
|
|
69
|
-
grid(
|
|
70
|
-
columns: (auto, auto, 1fr),
|
|
71
|
-
"MEMORANDUM FOR",
|
|
72
|
-
" ",
|
|
73
|
-
align(left)[
|
|
74
|
-
#if type(recipients) == array {
|
|
75
|
-
create-auto-grid(recipients, column-gutter: spacing.tab, cols: cols)
|
|
76
|
-
} else {
|
|
77
|
-
recipients
|
|
78
|
-
}
|
|
79
|
-
],
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// AFH 33-337 "FROM:": "Place 'FROM:' in uppercase, flush with the left margin,
|
|
84
|
-
// on the second line below the last line of the MEMORANDUM FOR element"
|
|
85
|
-
#let render-from-section(from-info) = {
|
|
86
|
-
blank-line()
|
|
87
|
-
from-info = ensure-string(from-info)
|
|
88
|
-
|
|
89
|
-
grid(
|
|
90
|
-
columns: (auto, auto, 1fr),
|
|
91
|
-
"FROM:", " ", align(left)[#from-info],
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// AFH 33-337 "SUBJECT:": "In all uppercase letters place 'SUBJECT:', flush with the
|
|
96
|
-
// left margin, on the second line below the last line of the FROM element"
|
|
97
|
-
#let render-subject-section(subject-text) = {
|
|
98
|
-
blank-line()
|
|
99
|
-
grid(
|
|
100
|
-
columns: (auto, auto, 1fr),
|
|
101
|
-
"SUBJECT:", " ", [#subject-text],
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
#let render-references-section(references) = {
|
|
106
|
-
if not falsey(references) {
|
|
107
|
-
blank-line()
|
|
108
|
-
grid(
|
|
109
|
-
columns: (auto, auto, 1fr),
|
|
110
|
-
"References:", " ", enum(..references, numbering: "(a)"),
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// =============================================================================
|
|
116
|
-
// SIGNATURE BLOCK
|
|
117
|
-
// =============================================================================
|
|
118
|
-
// AFH 33-337 "Signature Block": "Start the signature block on the fifth line below
|
|
119
|
-
// the last line of text and 4.5 inches from the left edge of the page"
|
|
120
|
-
// AFH 33-337 "Do not place the signature element on a continuation page by itself"
|
|
121
|
-
|
|
122
|
-
#let render-signature-block(signature-lines, signature-blank-lines: 4) = {
|
|
123
|
-
signature-lines = ensure-array(signature-lines)
|
|
124
|
-
// AFH 33-337: "The signature block is never on a page by itself"
|
|
125
|
-
// Note: Perfect enforcement isn't feasible without over-engineering
|
|
126
|
-
// We use weak: false spacing and breakable: false to discourage orphaning
|
|
127
|
-
// AFH 33-337: "fifth line below" = 4 blank lines between text and signature block
|
|
128
|
-
blank-lines(signature-blank-lines, weak: false)
|
|
129
|
-
block(breakable: false)[
|
|
130
|
-
#align(left)[
|
|
131
|
-
// AFH 33-337: "4.5 inches from the left edge of the page"
|
|
132
|
-
// We use (4.5in - margin) because Typst's pad() is relative to the text area, not page edge
|
|
133
|
-
#pad(left: 4.5in - spacing.margin)[
|
|
134
|
-
#text(hyphenate: false)[
|
|
135
|
-
#for line in signature-lines {
|
|
136
|
-
par(hanging-indent: 4 * 0.5em, line)
|
|
137
|
-
}
|
|
138
|
-
]
|
|
139
|
-
]
|
|
140
|
-
]
|
|
141
|
-
]
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// =============================================================================
|
|
145
|
-
// ACTION LINE RENDERING
|
|
146
|
-
// =============================================================================
|
|
147
|
-
// Renders the Approve / Disapprove action line for indorsement memos.
|
|
148
|
-
// action: "none" = no action line displayed (hidden), "undecided" = both options
|
|
149
|
-
// rendered plain (no circle), "approve" = Approve circled,
|
|
150
|
-
// "disapprove" = Disapprove circled. The action line is rendered when
|
|
151
|
-
// action is "undecided", "approve", or "disapprove".
|
|
152
|
-
|
|
153
|
-
#let render-action-line(action) = {
|
|
154
|
-
assert(
|
|
155
|
-
action in ("none", "undecided", "approve", "disapprove"),
|
|
156
|
-
message: "action must be \"none\", \"undecided\", \"approve\", or \"disapprove\"",
|
|
157
|
-
)
|
|
158
|
-
blank-line()
|
|
159
|
-
// Circle the selected option using a box with rounded corners
|
|
160
|
-
// Use baseline parameter to maintain vertical text alignment
|
|
161
|
-
let approve-text = if action == "approve" {
|
|
162
|
-
box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[Approve]
|
|
163
|
-
} else if action == "disapprove" {
|
|
164
|
-
strike[Approve]
|
|
165
|
-
} else {
|
|
166
|
-
[Approve]
|
|
167
|
-
}
|
|
168
|
-
let disapprove-text = if action == "disapprove" {
|
|
169
|
-
box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[Disapprove]
|
|
170
|
-
} else if action == "approve" {
|
|
171
|
-
strike[Disapprove]
|
|
172
|
-
} else {
|
|
173
|
-
[Disapprove]
|
|
174
|
-
}
|
|
175
|
-
// Keep the action line with the following content (body or signature block)
|
|
176
|
-
// using the same sticky-block pattern that body.typ applies to the last
|
|
177
|
-
// paragraph, per AFH 33-337 §11 orphan-prevention rules.
|
|
178
|
-
block(sticky: true)[#approve-text / #disapprove-text]
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// =============================================================================
|
|
182
|
-
// TABLE RENDERING
|
|
183
|
-
// =============================================================================
|
|
184
|
-
// AFH 33-337 does not specify table formatting, so we follow the general
|
|
185
|
-
// aesthetic principles of the standard: plain black borders, no decorative
|
|
186
|
-
// fills, and the body font inherited throughout.
|
|
187
|
-
|
|
188
|
-
/// Renders a table with USAF memorandum–consistent formatting.
|
|
189
|
-
///
|
|
190
|
-
/// Applies simple 0.5pt black cell borders and standard padding to any
|
|
191
|
-
/// Typst `table` element, keeping the visual style clean and formal.
|
|
192
|
-
/// Font and size are inherited from the surrounding body text.
|
|
193
|
-
///
|
|
194
|
-
/// - it (content): The table element to style and render
|
|
195
|
-
/// -> content
|
|
196
|
-
#let render-memo-table(it) = {
|
|
197
|
-
// AFH 33-337 does not specify table formatting, so we follow the general
|
|
198
|
-
// aesthetic principles of the standard: bold headers for clarity.
|
|
199
|
-
show table.cell.where(y: 0): set text(weight: "bold")
|
|
200
|
-
set table(
|
|
201
|
-
stroke: 0.5pt + black,
|
|
202
|
-
inset: (x: 0.5em, y: 0.4em),
|
|
203
|
-
)
|
|
204
|
-
it
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// =============================================================================
|
|
208
|
-
// BACKMATTER SECTIONS
|
|
209
|
-
// =============================================================================
|
|
210
|
-
// AFH 33-337 "Attachment or Attachments": "Place 'Attachment:' (for a single attachment)
|
|
211
|
-
// or '# Attachments:' (for two or more attachments) at the left margin, on the third
|
|
212
|
-
// line below the signature element"
|
|
213
|
-
// AFH 33-337 "Courtesy Copy Element": "place 'cc:' flush with the left margin, on the
|
|
214
|
-
// second line below the attachment element"
|
|
215
|
-
|
|
216
|
-
#let render-backmatter-section(
|
|
217
|
-
content,
|
|
218
|
-
section-label,
|
|
219
|
-
numbering-style: none,
|
|
220
|
-
continuation-label: none,
|
|
221
|
-
) = {
|
|
222
|
-
let formatted-content = {
|
|
223
|
-
// Use text() wrapper to prevent section label from being treated as a paragraph
|
|
224
|
-
text()[#section-label]
|
|
225
|
-
linebreak()
|
|
226
|
-
if numbering-style != none {
|
|
227
|
-
let items = ensure-array(content)
|
|
228
|
-
enum(..items, numbering: numbering-style)
|
|
229
|
-
} else {
|
|
230
|
-
ensure-string(content)
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
context {
|
|
235
|
-
let available-space = page.height - here().position().y - 1in
|
|
236
|
-
if measure(formatted-content).height > available-space {
|
|
237
|
-
let continuation-text = if continuation-label != none {
|
|
238
|
-
text()[#continuation-label]
|
|
239
|
-
} else {
|
|
240
|
-
text()[#section-label + " (listed on next page):"]
|
|
241
|
-
}
|
|
242
|
-
continuation-text
|
|
243
|
-
pagebreak()
|
|
244
|
-
}
|
|
245
|
-
formatted-content
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
#let calculate-backmatter-spacing(is-first-section) = {
|
|
250
|
-
context {
|
|
251
|
-
let line_count = if is-first-section { 2 } else { 1 }
|
|
252
|
-
blank-lines(line_count)
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
#let render-backmatter-sections(
|
|
257
|
-
attachments: none,
|
|
258
|
-
cc: none,
|
|
259
|
-
distribution: none,
|
|
260
|
-
leading-pagebreak: false,
|
|
261
|
-
) = {
|
|
262
|
-
let has-backmatter = (
|
|
263
|
-
(attachments != none and attachments.len() > 0)
|
|
264
|
-
or (cc != none and cc.len() > 0)
|
|
265
|
-
or (distribution != none and distribution.len() > 0)
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
if leading-pagebreak and has-backmatter {
|
|
269
|
-
pagebreak(weak: true)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if attachments != none and attachments.len() > 0 {
|
|
273
|
-
calculate-backmatter-spacing(true)
|
|
274
|
-
let attachment-count = attachments.len()
|
|
275
|
-
let section-label = if attachment-count == 1 { "Attachment:" } else { str(attachment-count) + " Attachments:" }
|
|
276
|
-
let continuation-label = (
|
|
277
|
-
(if attachment-count == 1 { "Attachment" } else { str(attachment-count) + " Attachments" })
|
|
278
|
-
+ " (listed on next page):"
|
|
279
|
-
)
|
|
280
|
-
render-backmatter-section(attachments, section-label, numbering-style: "1.", continuation-label: continuation-label)
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if cc != none and cc.len() > 0 {
|
|
284
|
-
calculate-backmatter-spacing(attachments == none or attachments.len() == 0)
|
|
285
|
-
render-backmatter-section(cc, "cc:")
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if distribution != none and distribution.len() > 0 {
|
|
289
|
-
calculate-backmatter-spacing((attachments == none or attachments.len() == 0) and (cc == none or cc.len() == 0))
|
|
290
|
-
render-backmatter-section(distribution, "DISTRIBUTION:")
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
1
|
+
// primitives.typ: Reusable rendering primitives for USAF memorandum sections
|
|
2
|
+
//
|
|
3
|
+
// This module implements the visual rendering functions that produce AFH 33-337
|
|
4
|
+
// compliant formatting for all sections of a USAF memorandum. Each function
|
|
5
|
+
// corresponds to specific placement and formatting requirements from Chapter 14.
|
|
6
|
+
|
|
7
|
+
#import "config.typ": *
|
|
8
|
+
#import "utils.typ": *
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// LETTERHEAD RENDERING
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// AFH 33-337 §1: "Use printed letterhead, computer-generated letterhead, or plain bond paper"
|
|
14
|
+
// Letterhead placement is not explicitly specified in AFH 33-337, but follows
|
|
15
|
+
// standard USAF memo formatting conventions
|
|
16
|
+
|
|
17
|
+
#let render-letterhead(title, caption, letterhead-seal, font) = {
|
|
18
|
+
font = ensure-array(font)
|
|
19
|
+
caption = ensure-string(caption)
|
|
20
|
+
|
|
21
|
+
place(
|
|
22
|
+
dy: 0.625in - spacing.margin,
|
|
23
|
+
box(
|
|
24
|
+
width: 100%,
|
|
25
|
+
fill: none,
|
|
26
|
+
stroke: none,
|
|
27
|
+
[
|
|
28
|
+
#place(
|
|
29
|
+
center + top,
|
|
30
|
+
align(center)[
|
|
31
|
+
#set text(12pt, font: font, fill: LETTERHEAD_COLOR)
|
|
32
|
+
#title\
|
|
33
|
+
#text(10.5pt)[#caption]
|
|
34
|
+
],
|
|
35
|
+
)
|
|
36
|
+
],
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if letterhead-seal != none {
|
|
41
|
+
place(
|
|
42
|
+
left + top,
|
|
43
|
+
dx: -0.5in,
|
|
44
|
+
dy: -.5in,
|
|
45
|
+
block[
|
|
46
|
+
#fit-box(width: 2in, height: 1in)[#letterhead-seal]
|
|
47
|
+
],
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// HEADER SECTIONS
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// AFH 33-337 "The Heading Section" specifies exact placement and format for:
|
|
56
|
+
// - Date: 1 inch from right edge, 1.75 inches from top
|
|
57
|
+
// - MEMORANDUM FOR: Second line below date
|
|
58
|
+
// - FROM: Second line below MEMORANDUM FOR
|
|
59
|
+
// - SUBJECT: Second line below FROM
|
|
60
|
+
|
|
61
|
+
// AFH 33-337 "Date": "Place the date 1 inch from the right edge, 1.75 inches from the top"
|
|
62
|
+
#let render-date-section(date) = {
|
|
63
|
+
align(right)[#display-date(date)]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// AFH 33-337 "MEMORANDUM FOR": "Place 'MEMORANDUM FOR' on the second line below the date"
|
|
67
|
+
#let render-for-section(recipients, cols) = {
|
|
68
|
+
blank-line()
|
|
69
|
+
grid(
|
|
70
|
+
columns: (auto, auto, 1fr),
|
|
71
|
+
"MEMORANDUM FOR",
|
|
72
|
+
" ",
|
|
73
|
+
align(left)[
|
|
74
|
+
#if type(recipients) == array {
|
|
75
|
+
create-auto-grid(recipients, column-gutter: spacing.tab, cols: cols)
|
|
76
|
+
} else {
|
|
77
|
+
recipients
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// AFH 33-337 "FROM:": "Place 'FROM:' in uppercase, flush with the left margin,
|
|
84
|
+
// on the second line below the last line of the MEMORANDUM FOR element"
|
|
85
|
+
#let render-from-section(from-info) = {
|
|
86
|
+
blank-line()
|
|
87
|
+
from-info = ensure-string(from-info)
|
|
88
|
+
|
|
89
|
+
grid(
|
|
90
|
+
columns: (auto, auto, 1fr),
|
|
91
|
+
"FROM:", " ", align(left)[#from-info],
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// AFH 33-337 "SUBJECT:": "In all uppercase letters place 'SUBJECT:', flush with the
|
|
96
|
+
// left margin, on the second line below the last line of the FROM element"
|
|
97
|
+
#let render-subject-section(subject-text) = {
|
|
98
|
+
blank-line()
|
|
99
|
+
grid(
|
|
100
|
+
columns: (auto, auto, 1fr),
|
|
101
|
+
"SUBJECT:", " ", [#subject-text],
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#let render-references-section(references) = {
|
|
106
|
+
if not falsey(references) {
|
|
107
|
+
blank-line()
|
|
108
|
+
grid(
|
|
109
|
+
columns: (auto, auto, 1fr),
|
|
110
|
+
"References:", " ", enum(..references, numbering: "(a)"),
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// =============================================================================
|
|
116
|
+
// SIGNATURE BLOCK
|
|
117
|
+
// =============================================================================
|
|
118
|
+
// AFH 33-337 "Signature Block": "Start the signature block on the fifth line below
|
|
119
|
+
// the last line of text and 4.5 inches from the left edge of the page"
|
|
120
|
+
// AFH 33-337 "Do not place the signature element on a continuation page by itself"
|
|
121
|
+
|
|
122
|
+
#let render-signature-block(signature-lines, signature-blank-lines: 4) = {
|
|
123
|
+
signature-lines = ensure-array(signature-lines)
|
|
124
|
+
// AFH 33-337: "The signature block is never on a page by itself"
|
|
125
|
+
// Note: Perfect enforcement isn't feasible without over-engineering
|
|
126
|
+
// We use weak: false spacing and breakable: false to discourage orphaning
|
|
127
|
+
// AFH 33-337: "fifth line below" = 4 blank lines between text and signature block
|
|
128
|
+
blank-lines(signature-blank-lines, weak: false)
|
|
129
|
+
block(breakable: false)[
|
|
130
|
+
#align(left)[
|
|
131
|
+
// AFH 33-337: "4.5 inches from the left edge of the page"
|
|
132
|
+
// We use (4.5in - margin) because Typst's pad() is relative to the text area, not page edge
|
|
133
|
+
#pad(left: 4.5in - spacing.margin)[
|
|
134
|
+
#text(hyphenate: false)[
|
|
135
|
+
#for line in signature-lines {
|
|
136
|
+
par(hanging-indent: 4 * 0.5em, line)
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
]
|
|
140
|
+
]
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// ACTION LINE RENDERING
|
|
146
|
+
// =============================================================================
|
|
147
|
+
// Renders the Approve / Disapprove action line for indorsement memos.
|
|
148
|
+
// action: "none" = no action line displayed (hidden), "undecided" = both options
|
|
149
|
+
// rendered plain (no circle), "approve" = Approve circled,
|
|
150
|
+
// "disapprove" = Disapprove circled. The action line is rendered when
|
|
151
|
+
// action is "undecided", "approve", or "disapprove".
|
|
152
|
+
|
|
153
|
+
#let render-action-line(action) = {
|
|
154
|
+
assert(
|
|
155
|
+
action in ("none", "undecided", "approve", "disapprove"),
|
|
156
|
+
message: "action must be \"none\", \"undecided\", \"approve\", or \"disapprove\"",
|
|
157
|
+
)
|
|
158
|
+
blank-line()
|
|
159
|
+
// Circle the selected option using a box with rounded corners
|
|
160
|
+
// Use baseline parameter to maintain vertical text alignment
|
|
161
|
+
let approve-text = if action == "approve" {
|
|
162
|
+
box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[Approve]
|
|
163
|
+
} else if action == "disapprove" {
|
|
164
|
+
strike[Approve]
|
|
165
|
+
} else {
|
|
166
|
+
[Approve]
|
|
167
|
+
}
|
|
168
|
+
let disapprove-text = if action == "disapprove" {
|
|
169
|
+
box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[Disapprove]
|
|
170
|
+
} else if action == "approve" {
|
|
171
|
+
strike[Disapprove]
|
|
172
|
+
} else {
|
|
173
|
+
[Disapprove]
|
|
174
|
+
}
|
|
175
|
+
// Keep the action line with the following content (body or signature block)
|
|
176
|
+
// using the same sticky-block pattern that body.typ applies to the last
|
|
177
|
+
// paragraph, per AFH 33-337 §11 orphan-prevention rules.
|
|
178
|
+
block(sticky: true)[#approve-text / #disapprove-text]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// =============================================================================
|
|
182
|
+
// TABLE RENDERING
|
|
183
|
+
// =============================================================================
|
|
184
|
+
// AFH 33-337 does not specify table formatting, so we follow the general
|
|
185
|
+
// aesthetic principles of the standard: plain black borders, no decorative
|
|
186
|
+
// fills, and the body font inherited throughout.
|
|
187
|
+
|
|
188
|
+
/// Renders a table with USAF memorandum–consistent formatting.
|
|
189
|
+
///
|
|
190
|
+
/// Applies simple 0.5pt black cell borders and standard padding to any
|
|
191
|
+
/// Typst `table` element, keeping the visual style clean and formal.
|
|
192
|
+
/// Font and size are inherited from the surrounding body text.
|
|
193
|
+
///
|
|
194
|
+
/// - it (content): The table element to style and render
|
|
195
|
+
/// -> content
|
|
196
|
+
#let render-memo-table(it) = {
|
|
197
|
+
// AFH 33-337 does not specify table formatting, so we follow the general
|
|
198
|
+
// aesthetic principles of the standard: bold headers for clarity.
|
|
199
|
+
show table.cell.where(y: 0): set text(weight: "bold")
|
|
200
|
+
set table(
|
|
201
|
+
stroke: 0.5pt + black,
|
|
202
|
+
inset: (x: 0.5em, y: 0.4em),
|
|
203
|
+
)
|
|
204
|
+
it
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// =============================================================================
|
|
208
|
+
// BACKMATTER SECTIONS
|
|
209
|
+
// =============================================================================
|
|
210
|
+
// AFH 33-337 "Attachment or Attachments": "Place 'Attachment:' (for a single attachment)
|
|
211
|
+
// or '# Attachments:' (for two or more attachments) at the left margin, on the third
|
|
212
|
+
// line below the signature element"
|
|
213
|
+
// AFH 33-337 "Courtesy Copy Element": "place 'cc:' flush with the left margin, on the
|
|
214
|
+
// second line below the attachment element"
|
|
215
|
+
|
|
216
|
+
#let render-backmatter-section(
|
|
217
|
+
content,
|
|
218
|
+
section-label,
|
|
219
|
+
numbering-style: none,
|
|
220
|
+
continuation-label: none,
|
|
221
|
+
) = {
|
|
222
|
+
let formatted-content = {
|
|
223
|
+
// Use text() wrapper to prevent section label from being treated as a paragraph
|
|
224
|
+
text()[#section-label]
|
|
225
|
+
linebreak()
|
|
226
|
+
if numbering-style != none {
|
|
227
|
+
let items = ensure-array(content)
|
|
228
|
+
enum(..items, numbering: numbering-style)
|
|
229
|
+
} else {
|
|
230
|
+
ensure-string(content)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
context {
|
|
235
|
+
let available-space = page.height - here().position().y - 1in
|
|
236
|
+
if measure(formatted-content).height > available-space {
|
|
237
|
+
let continuation-text = if continuation-label != none {
|
|
238
|
+
text()[#continuation-label]
|
|
239
|
+
} else {
|
|
240
|
+
text()[#section-label + " (listed on next page):"]
|
|
241
|
+
}
|
|
242
|
+
continuation-text
|
|
243
|
+
pagebreak()
|
|
244
|
+
}
|
|
245
|
+
formatted-content
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#let calculate-backmatter-spacing(is-first-section) = {
|
|
250
|
+
context {
|
|
251
|
+
let line_count = if is-first-section { 2 } else { 1 }
|
|
252
|
+
blank-lines(line_count)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#let render-backmatter-sections(
|
|
257
|
+
attachments: none,
|
|
258
|
+
cc: none,
|
|
259
|
+
distribution: none,
|
|
260
|
+
leading-pagebreak: false,
|
|
261
|
+
) = {
|
|
262
|
+
let has-backmatter = (
|
|
263
|
+
(attachments != none and attachments.len() > 0)
|
|
264
|
+
or (cc != none and cc.len() > 0)
|
|
265
|
+
or (distribution != none and distribution.len() > 0)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if leading-pagebreak and has-backmatter {
|
|
269
|
+
pagebreak(weak: true)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if attachments != none and attachments.len() > 0 {
|
|
273
|
+
calculate-backmatter-spacing(true)
|
|
274
|
+
let attachment-count = attachments.len()
|
|
275
|
+
let section-label = if attachment-count == 1 { "Attachment:" } else { str(attachment-count) + " Attachments:" }
|
|
276
|
+
let continuation-label = (
|
|
277
|
+
(if attachment-count == 1 { "Attachment" } else { str(attachment-count) + " Attachments" })
|
|
278
|
+
+ " (listed on next page):"
|
|
279
|
+
)
|
|
280
|
+
render-backmatter-section(attachments, section-label, numbering-style: "1.", continuation-label: continuation-label)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if cc != none and cc.len() > 0 {
|
|
284
|
+
calculate-backmatter-spacing(attachments == none or attachments.len() == 0)
|
|
285
|
+
render-backmatter-section(cc, "cc:")
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if distribution != none and distribution.len() > 0 {
|
|
289
|
+
calculate-backmatter-spacing((attachments == none or attachments.len() == 0) and (cc == none or cc.len() == 0))
|
|
290
|
+
render-backmatter-section(distribution, "DISTRIBUTION:")
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|