@tonguetoquill/collection 0.1.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/LICENSE +201 -0
- package/README.md +39 -0
- package/index.d.ts +2 -0
- package/index.js +8 -0
- package/package.json +36 -0
- package/quills/classic_resume/0.1.0/Quill.yaml +118 -0
- package/quills/classic_resume/0.1.0/example.md +232 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/LICENSE +21 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/README.md +38 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/fonts/EBGaramond-Bold.ttf +0 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/fonts/EBGaramond-BoldItalic.ttf +0 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/fonts/EBGaramond-Italic.ttf +0 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/fonts/EBGaramond-Regular.ttf +0 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/components.typ +184 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/layout.typ +42 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/lib.typ +5 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/typst.toml +26 -0
- package/quills/classic_resume/0.1.0/plate.typ +44 -0
- package/quills/cmu_letter/0.1.0/.quillignore +31 -0
- package/quills/cmu_letter/0.1.0/Quill.yaml +64 -0
- package/quills/cmu_letter/0.1.0/assets/cmu-wordmark.svg +174 -0
- package/quills/cmu_letter/0.1.0/example.md +31 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/LICENSE +21 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/OFL.txt +93 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/OpenSans-Bold.ttf +0 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/OpenSans-BoldItalic.ttf +0 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/OpenSans-Italic.ttf +0 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/OpenSans-Regular.ttf +0 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/README.txt +100 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/backmatter.typ +13 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/config.typ +40 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/frontmatter.typ +72 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/lib.typ +47 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/mainmatter.typ +42 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/primitives.typ +70 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/utils.typ +85 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/typst.toml +17 -0
- package/quills/cmu_letter/0.1.0/plate.typ +19 -0
- package/quills/taro/0.1.0/Quill.yaml +29 -0
- package/quills/taro/0.1.0/assets/Figtree-Bold.ttf +0 -0
- package/quills/taro/0.1.0/assets/Figtree-Italic.ttf +0 -0
- package/quills/taro/0.1.0/assets/Figtree-Regular.ttf +0 -0
- package/quills/taro/0.1.0/example.md +27 -0
- package/quills/taro/0.1.0/plate.typ +31 -0
- package/quills/usaf_memo/0.1.0/.quillignore +31 -0
- package/quills/usaf_memo/0.1.0/Quill.yaml +209 -0
- package/quills/usaf_memo/0.1.0/assets/dow_seal.jpg +0 -0
- package/quills/usaf_memo/0.1.0/example.md +55 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/body.typ +325 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/config.typ +63 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +272 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/utils.typ +377 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/typst.toml +17 -0
- package/quills/usaf_memo/0.1.0/plate.typ +74 -0
- package/quills/usaf_memo/0.2.0/.quillignore +31 -0
- package/quills/usaf_memo/0.2.0/Quill.yaml +225 -0
- package/quills/usaf_memo/0.2.0/assets/dow_seal.jpg +0 -0
- package/quills/usaf_memo/0.2.0/example.md +57 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +325 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/config.typ +63 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +272 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +377 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +17 -0
- package/quills/usaf_memo/0.2.0/plate.typ +76 -0
- package/templates/cmu_letter_template.md +38 -0
- package/templates/loc.md +79 -0
- package/templates/pass_request.md +44 -0
- package/templates/rebuttal.md +56 -0
- package/templates/taro.md +27 -0
- package/templates/templates.json +44 -0
- package/templates/usaf_template.md +23 -0
- package/templates/ussf_template.md +29 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// mainmatter.typ: Mainmatter show rule for USAF memorandum
|
|
2
|
+
//
|
|
3
|
+
// This module implements the mainmatter (body text) of a USAF memorandum per
|
|
4
|
+
// AFH 33-337 Chapter 14 "The Text of the Official Memorandum" (§1-12).
|
|
5
|
+
|
|
6
|
+
#import "primitives.typ": *
|
|
7
|
+
#import "body.typ": *
|
|
8
|
+
|
|
9
|
+
/// Mainmatter show rule for USAF memorandum body content.
|
|
10
|
+
///
|
|
11
|
+
/// AFH 33-337 "The Text of the Official Memorandum" §1-12 requirements:
|
|
12
|
+
/// - Begin text on second line below subject/references
|
|
13
|
+
/// - Single-space text, double-space between paragraphs
|
|
14
|
+
/// - Number and letter each paragraph/subparagraph
|
|
15
|
+
/// - "A single paragraph is not numbered" (§2)
|
|
16
|
+
/// - First paragraph flush left, never indented
|
|
17
|
+
///
|
|
18
|
+
/// Applies AFH 33-337 paragraph numbering and formatting to the main body
|
|
19
|
+
/// of the memorandum. Automatically detects single vs. multiple paragraphs
|
|
20
|
+
/// to comply with AFH 33-337 numbering requirements.
|
|
21
|
+
///
|
|
22
|
+
/// When auto_numbering is false (set in frontmatter), base-level paragraphs
|
|
23
|
+
/// render flush left without numbering. Only explicitly numbered or bulleted
|
|
24
|
+
/// items enter the numbering hierarchy.
|
|
25
|
+
///
|
|
26
|
+
/// - content (content): The body content to render
|
|
27
|
+
/// -> content
|
|
28
|
+
#let mainmatter(it) = context {
|
|
29
|
+
let config = query(metadata).last().value
|
|
30
|
+
let auto-numbering = config.at("auto_numbering", default: true)
|
|
31
|
+
render-body(it, auto-numbering: auto-numbering)
|
|
32
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
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 APPROVED / DISAPPROVED action line for indorsement memos.
|
|
148
|
+
// action: none = both plain (no decision yet), "approved" = bold APPROVED /
|
|
149
|
+
// strike DISAPPROVED, "disapproved" = strike APPROVED / bold DISAPPROVED.
|
|
150
|
+
// Visibility is controlled by the caller (show_action / action != none).
|
|
151
|
+
|
|
152
|
+
#let render-action-line(action) = {
|
|
153
|
+
assert(
|
|
154
|
+
action in (none, "approved", "disapproved"),
|
|
155
|
+
message: "action must be none, \"approved\", or \"disapproved\"",
|
|
156
|
+
)
|
|
157
|
+
blank-line()
|
|
158
|
+
let approved-text = if action == "approved" { strong[APPROVED] } else if action == "disapproved" { strike[APPROVED] } else { [APPROVED] }
|
|
159
|
+
let disapproved-text = if action == "disapproved" { strong[DISAPPROVED] } else if action == "approved" { strike[DISAPPROVED] } else { [DISAPPROVED] }
|
|
160
|
+
[#approved-text / #disapproved-text]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// =============================================================================
|
|
164
|
+
// TABLE RENDERING
|
|
165
|
+
// =============================================================================
|
|
166
|
+
// AFH 33-337 does not specify table formatting, so we follow the general
|
|
167
|
+
// aesthetic principles of the standard: plain black borders, no decorative
|
|
168
|
+
// fills, and the body font inherited throughout.
|
|
169
|
+
|
|
170
|
+
/// Renders a table with USAF memorandum–consistent formatting.
|
|
171
|
+
///
|
|
172
|
+
/// Applies simple 0.5pt black cell borders and standard padding to any
|
|
173
|
+
/// Typst `table` element, keeping the visual style clean and formal.
|
|
174
|
+
/// Font and size are inherited from the surrounding body text.
|
|
175
|
+
///
|
|
176
|
+
/// - it (content): The table element to style and render
|
|
177
|
+
/// -> content
|
|
178
|
+
#let render-memo-table(it) = {
|
|
179
|
+
set table(
|
|
180
|
+
stroke: 0.5pt + black,
|
|
181
|
+
inset: (x: 0.5em, y: 0.4em),
|
|
182
|
+
)
|
|
183
|
+
it
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// =============================================================================
|
|
187
|
+
// BACKMATTER SECTIONS
|
|
188
|
+
// =============================================================================
|
|
189
|
+
// AFH 33-337 "Attachment or Attachments": "Place 'Attachment:' (for a single attachment)
|
|
190
|
+
// or '# Attachments:' (for two or more attachments) at the left margin, on the third
|
|
191
|
+
// line below the signature element"
|
|
192
|
+
// AFH 33-337 "Courtesy Copy Element": "place 'cc:' flush with the left margin, on the
|
|
193
|
+
// second line below the attachment element"
|
|
194
|
+
|
|
195
|
+
#let render-backmatter-section(
|
|
196
|
+
content,
|
|
197
|
+
section-label,
|
|
198
|
+
numbering-style: none,
|
|
199
|
+
continuation-label: none,
|
|
200
|
+
) = {
|
|
201
|
+
let formatted-content = {
|
|
202
|
+
// Use text() wrapper to prevent section label from being treated as a paragraph
|
|
203
|
+
text()[#section-label]
|
|
204
|
+
linebreak()
|
|
205
|
+
if numbering-style != none {
|
|
206
|
+
let items = ensure-array(content)
|
|
207
|
+
enum(..items, numbering: numbering-style)
|
|
208
|
+
} else {
|
|
209
|
+
ensure-string(content)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
context {
|
|
214
|
+
let available-space = page.height - here().position().y - 1in
|
|
215
|
+
if measure(formatted-content).height > available-space {
|
|
216
|
+
let continuation-text = if continuation-label != none {
|
|
217
|
+
text()[#continuation-label]
|
|
218
|
+
} else {
|
|
219
|
+
text()[#section-label + " (listed on next page):"]
|
|
220
|
+
}
|
|
221
|
+
continuation-text
|
|
222
|
+
pagebreak()
|
|
223
|
+
}
|
|
224
|
+
formatted-content
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#let calculate-backmatter-spacing(is-first-section) = {
|
|
229
|
+
context {
|
|
230
|
+
let line_count = if is-first-section { 2 } else { 1 }
|
|
231
|
+
blank-lines(line_count)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#let render-backmatter-sections(
|
|
236
|
+
attachments: none,
|
|
237
|
+
cc: none,
|
|
238
|
+
distribution: none,
|
|
239
|
+
leading-pagebreak: false,
|
|
240
|
+
) = {
|
|
241
|
+
let has-backmatter = (
|
|
242
|
+
(attachments != none and attachments.len() > 0)
|
|
243
|
+
or (cc != none and cc.len() > 0)
|
|
244
|
+
or (distribution != none and distribution.len() > 0)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if leading-pagebreak and has-backmatter {
|
|
248
|
+
pagebreak(weak: true)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if attachments != none and attachments.len() > 0 {
|
|
252
|
+
calculate-backmatter-spacing(true)
|
|
253
|
+
let attachment-count = attachments.len()
|
|
254
|
+
let section-label = if attachment-count == 1 { "Attachment:" } else { str(attachment-count) + " Attachments:" }
|
|
255
|
+
let continuation-label = (
|
|
256
|
+
(if attachment-count == 1 { "Attachment" } else { str(attachment-count) + " Attachments" })
|
|
257
|
+
+ " (listed on next page):"
|
|
258
|
+
)
|
|
259
|
+
render-backmatter-section(attachments, section-label, numbering-style: "1.", continuation-label: continuation-label)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if cc != none and cc.len() > 0 {
|
|
263
|
+
calculate-backmatter-spacing(attachments == none or attachments.len() == 0)
|
|
264
|
+
render-backmatter-section(cc, "cc:")
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if distribution != none and distribution.len() > 0 {
|
|
268
|
+
calculate-backmatter-spacing((attachments == none or attachments.len() == 0) and (cc == none or cc.len() == 0))
|
|
269
|
+
render-backmatter-section(distribution, "DISTRIBUTION:")
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// utils.typ: Utility functions and backend code for Typst usaf-memo package.
|
|
2
|
+
//
|
|
3
|
+
// This module provides core utility functions, configuration constants, and helper
|
|
4
|
+
// functions used by the main memorandum template. It handles spacing calculations,
|
|
5
|
+
// paragraph numbering, grid layouts, and various formatting utilities required
|
|
6
|
+
// for AFH 33-337 compliance.
|
|
7
|
+
//
|
|
8
|
+
// Key components:
|
|
9
|
+
// - Spacing constants and configuration management
|
|
10
|
+
// - Paragraph numbering and indentation utilities
|
|
11
|
+
// - Grid layout and backmatter formatting functions
|
|
12
|
+
// - Date formatting and content scaling utilities
|
|
13
|
+
// - Indorsement processing and ordinal number generation
|
|
14
|
+
|
|
15
|
+
#import "config.typ": CLASSIFICATION_COLORS, counters, paragraph-config, spacing
|
|
16
|
+
|
|
17
|
+
/// Creates vertical spacing equivalent to multiple blank lines.
|
|
18
|
+
///
|
|
19
|
+
/// Calculates proper vertical space based on line height and leading
|
|
20
|
+
/// to maintain consistent spacing throughout the document.
|
|
21
|
+
///
|
|
22
|
+
/// - count (int): Number of blank lines to create
|
|
23
|
+
/// - weak (bool): Whether spacing can be compressed at page breaks
|
|
24
|
+
/// -> content
|
|
25
|
+
#let blank-lines(count, weak: true) = {
|
|
26
|
+
if count == 0 {
|
|
27
|
+
v(0em, weak: weak)
|
|
28
|
+
} else {
|
|
29
|
+
let lead = spacing.line
|
|
30
|
+
let height = spacing.line-height
|
|
31
|
+
v(lead + (height + lead) * count, weak: weak)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Creates vertical spacing equivalent to one blank line.
|
|
36
|
+
/// Convenience function for single line spacing.
|
|
37
|
+
///
|
|
38
|
+
/// - weak (bool): Whether spacing can be compressed at page breaks
|
|
39
|
+
/// -> content
|
|
40
|
+
#let blank-line(weak: true) = blank-lines(1, weak: weak)
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// GENERAL UTILITY FUNCTIONS
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
/// Checks if a value is "falsey" (none, false, empty array, or empty string).
|
|
47
|
+
///
|
|
48
|
+
/// Provides a consistent way to test for empty or missing values across
|
|
49
|
+
/// the template system. Used for conditional rendering of optional sections.
|
|
50
|
+
///
|
|
51
|
+
/// - value (any): The value to check for "falsey" status
|
|
52
|
+
/// -> bool
|
|
53
|
+
#let falsey(value) = {
|
|
54
|
+
value == none or value == false or (type(value) == array and value.len() == 0) or (type(value) == str and value == "")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Scales content to fit within a specified box while maintaining aspect ratio.
|
|
58
|
+
///
|
|
59
|
+
/// Automatically measures content and calculates uniform scaling to fit within
|
|
60
|
+
/// the given dimensions. Commonly used for letterhead seals and other images
|
|
61
|
+
/// that need to fit specific size constraints while preserving proportions.
|
|
62
|
+
///
|
|
63
|
+
/// - width (length): Maximum width for the content (default: 2in)
|
|
64
|
+
/// - height (length): Maximum height for the content (default: 1in)
|
|
65
|
+
/// - alignment (alignment): Content alignment within the box (default: left+horizon)
|
|
66
|
+
/// - body (content): Content to scale and fit
|
|
67
|
+
/// -> content
|
|
68
|
+
#let fit-box(width: 2in, height: 1in, alignment: left + horizon, body) = context {
|
|
69
|
+
// 1) measure the unscaled content
|
|
70
|
+
let s = measure(body)
|
|
71
|
+
|
|
72
|
+
// 2) compute the uniform scale that fits inside the box
|
|
73
|
+
let f = calc.min(width / s.width, height / s.height) * 100% // ratio
|
|
74
|
+
|
|
75
|
+
// 3) fixed-size box, center the scaled content, and reflow so layout respects it
|
|
76
|
+
box(width: width, height: height, clip: true)[
|
|
77
|
+
#align(alignment)[
|
|
78
|
+
#scale(f, reflow: true)[#body]
|
|
79
|
+
]
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Checks if a string is in ISO date format (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS).
|
|
84
|
+
///
|
|
85
|
+
/// Performs a simple pattern check for ISO 8601 date strings by verifying:
|
|
86
|
+
/// - String is at least 10 characters long
|
|
87
|
+
/// - Characters at positions 4 and 7 are dashes
|
|
88
|
+
///
|
|
89
|
+
/// - date-str (str): String to check for ISO date pattern
|
|
90
|
+
/// -> bool
|
|
91
|
+
#let is-iso-date-string(date-str) = {
|
|
92
|
+
if date-str.len() == 10 {
|
|
93
|
+
let char4 = date-str.at(4)
|
|
94
|
+
let char7 = date-str.at(7)
|
|
95
|
+
return char4 == "-" and char7 == "-"
|
|
96
|
+
} else if date-str.len() > 10 {
|
|
97
|
+
let char4 = date-str.at(4)
|
|
98
|
+
let char7 = date-str.at(7)
|
|
99
|
+
let char10 = date-str.at(10)
|
|
100
|
+
return char4 == "-" and char7 == "-" and char10 == "T"
|
|
101
|
+
}
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Extracts the date portion (YYYY-MM-DD) from an ISO date string.
|
|
106
|
+
///
|
|
107
|
+
/// Returns the first 10 characters which contain the date portion of an
|
|
108
|
+
/// ISO 8601 date string, removing any time component if present.
|
|
109
|
+
///
|
|
110
|
+
/// - date-str (str): ISO date string to extract from
|
|
111
|
+
/// -> str
|
|
112
|
+
#let extract-iso-date(date-str) = {
|
|
113
|
+
date-str.slice(0, 10)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Formats a date in standard military format or ISO format depending on input type.
|
|
117
|
+
///
|
|
118
|
+
/// AFH 33-337 "Date": "Use the 'Day Month Year' or 'DD Mmm YY' format for documents
|
|
119
|
+
/// addressed to a military organization. For civilian addressees, use the 'Month Day, Year' format."
|
|
120
|
+
/// Examples: "15 October 2014" or "15 Oct 14" for military, "October 15, 2014" for civilian
|
|
121
|
+
///
|
|
122
|
+
/// Intelligently handles different date input formats:
|
|
123
|
+
/// - ISO string (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS): Parsed via TOML and displayed in military format
|
|
124
|
+
/// - Non-ISO string: Displayed as-is
|
|
125
|
+
/// - datetime object: Displayed in military format ("1 January 2024")
|
|
126
|
+
///
|
|
127
|
+
/// - date (str|datetime): Date to format for display
|
|
128
|
+
/// -> str
|
|
129
|
+
// NOTE: Consider simplification if Typst adds native ISO date parsing (see CASCADES.md §A)
|
|
130
|
+
#let display-date(date) = {
|
|
131
|
+
if type(date) == str {
|
|
132
|
+
if is-iso-date-string(date) {
|
|
133
|
+
// Parse ISO date string using TOML to get datetime object
|
|
134
|
+
let iso-date = extract-iso-date(date)
|
|
135
|
+
let toml-str = "date = " + iso-date
|
|
136
|
+
let parsed = toml(bytes(toml-str))
|
|
137
|
+
parsed.date.display("[day padding:none] [month repr:long] [year]")
|
|
138
|
+
} else {
|
|
139
|
+
date
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
date.display("[day padding:none] [month repr:long] [year]")
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Gets the color associated with a classification level.
|
|
147
|
+
///
|
|
148
|
+
/// - level (str): Classification level string
|
|
149
|
+
/// -> color
|
|
150
|
+
#let get-classification-level-color(level) = {
|
|
151
|
+
if level == none {
|
|
152
|
+
return rgb(0, 0, 0) // Default to black if no classification
|
|
153
|
+
}
|
|
154
|
+
// Order matters - check most specific first
|
|
155
|
+
let level-order = ("TOP SECRET", "SECRET", "CONFIDENTIAL", "UNCLASSIFIED")
|
|
156
|
+
|
|
157
|
+
for base-level in level-order {
|
|
158
|
+
if base-level in level {
|
|
159
|
+
return CLASSIFICATION_COLORS.at(base-level)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
rgb(0, 0, 0) // Default
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// =============================================================================
|
|
167
|
+
// GRID LAYOUT UTILITIES
|
|
168
|
+
// =============================================================================
|
|
169
|
+
|
|
170
|
+
/// Creates an automatic grid layout from string or array content.
|
|
171
|
+
///
|
|
172
|
+
/// Converts 1D content into a multi-column grid layout with proper spacing.
|
|
173
|
+
/// Used primarily for formatting recipient lists in the "MEMORANDUM FOR" section
|
|
174
|
+
/// where multiple organizations need to be displayed in columns.
|
|
175
|
+
///
|
|
176
|
+
/// Features:
|
|
177
|
+
/// - Automatic column distribution and row filling
|
|
178
|
+
/// - Configurable column spacing and count
|
|
179
|
+
/// - Handles both single strings and arrays of strings
|
|
180
|
+
/// - Adds padding cells to maintain consistent column alignment
|
|
181
|
+
///
|
|
182
|
+
/// - content (str | array): Content to arrange in grid (strings only)
|
|
183
|
+
/// - column-gutter (length): Space between columns (default: 0.5em)
|
|
184
|
+
/// - cols (int): Number of columns for the grid (default: 3)
|
|
185
|
+
/// -> grid
|
|
186
|
+
#let create-auto-grid(content, column-gutter: .5em, cols: 3) = {
|
|
187
|
+
let content_type = type(content)
|
|
188
|
+
|
|
189
|
+
assert(content_type == str or content_type == array, message: "Content must be a string or an array of strings.")
|
|
190
|
+
if content_type == array {
|
|
191
|
+
for item in content {
|
|
192
|
+
assert(type(item) == str, message: "All items in content array must be strings.")
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Normalize to 1d array
|
|
197
|
+
if content_type == str {
|
|
198
|
+
content = (content,)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
// Build cell array in row-major order
|
|
203
|
+
let cells = ()
|
|
204
|
+
let i = 0
|
|
205
|
+
for item in content {
|
|
206
|
+
i += 1
|
|
207
|
+
cells.push(item)
|
|
208
|
+
if calc.rem(i, cols) == 0 {
|
|
209
|
+
// Add empty cell to pad the page
|
|
210
|
+
cells.push([])
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Add padding cells to complete the last row if needed
|
|
215
|
+
let remainder = calc.rem(cells.len(), cols + 1)
|
|
216
|
+
if remainder != 0 {
|
|
217
|
+
let padding_needed = (cols + 1) - remainder
|
|
218
|
+
for _ in range(padding_needed) {
|
|
219
|
+
cells.push([])
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
grid(
|
|
224
|
+
columns: calc.max(1, cols) + 1,
|
|
225
|
+
column-gutter: .1fr,
|
|
226
|
+
row-gutter: spacing.line,
|
|
227
|
+
..cells
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// =============================================================================
|
|
232
|
+
// TYPE NORMALIZATION UTILITIES
|
|
233
|
+
// =============================================================================
|
|
234
|
+
|
|
235
|
+
/// Ensures the input is an array. If already an array, returns as-is.
|
|
236
|
+
/// If not an array, wraps the value in a tuple.
|
|
237
|
+
///
|
|
238
|
+
/// This utility eliminates repetitive `if type(x) == array` checks throughout
|
|
239
|
+
/// the codebase by providing a canonical "normalize to array" function.
|
|
240
|
+
///
|
|
241
|
+
/// - value: Any value to normalize to array form
|
|
242
|
+
/// - Returns: Array containing the value(s)
|
|
243
|
+
///
|
|
244
|
+
/// Examples:
|
|
245
|
+
/// - ensure-array("foo") → ("foo",)
|
|
246
|
+
/// - ensure-array(("a", "b")) → ("a", "b")
|
|
247
|
+
/// - ensure-array(none) → ()
|
|
248
|
+
#let ensure-array(value) = {
|
|
249
|
+
if value == none {
|
|
250
|
+
()
|
|
251
|
+
} else if type(value) == array {
|
|
252
|
+
value
|
|
253
|
+
} else {
|
|
254
|
+
(value,)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// Ensures the input is a string. If already a string, returns as-is.
|
|
259
|
+
/// If an array, joins elements with the specified separator.
|
|
260
|
+
///
|
|
261
|
+
/// This utility eliminates repetitive `if type(x) == array { x.join(...) }`
|
|
262
|
+
/// checks throughout the codebase by providing a canonical "normalize to string"
|
|
263
|
+
/// function.
|
|
264
|
+
///
|
|
265
|
+
/// - value: Any value to normalize to string form
|
|
266
|
+
/// - separator: String to use when joining array elements (default: "\n")
|
|
267
|
+
/// - Returns: String representation of the value
|
|
268
|
+
///
|
|
269
|
+
/// Examples:
|
|
270
|
+
/// - ensure-string("foo") → "foo"
|
|
271
|
+
/// - ensure-string(("a", "b")) → "a\nb"
|
|
272
|
+
/// - ensure-string(("a", "b"), ", ") → "a, b"
|
|
273
|
+
/// - ensure-string(none) → ""
|
|
274
|
+
#let ensure-string(value, separator: "\n") = {
|
|
275
|
+
if value == none {
|
|
276
|
+
""
|
|
277
|
+
} else if type(value) == array {
|
|
278
|
+
value.join(separator)
|
|
279
|
+
} else {
|
|
280
|
+
str(value)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// Extracts the first element from an array, or returns the value if not an array.
|
|
285
|
+
///
|
|
286
|
+
/// This utility eliminates repetitive ternary operators like
|
|
287
|
+
/// `if type(x) == array { x.at(0) } else { x }` by providing a canonical
|
|
288
|
+
/// "first element or self" function.
|
|
289
|
+
///
|
|
290
|
+
/// - value: Any value to extract from
|
|
291
|
+
/// - Returns: First array element if array, otherwise the value itself
|
|
292
|
+
///
|
|
293
|
+
/// Examples:
|
|
294
|
+
/// - first-or-value("foo") → "foo"
|
|
295
|
+
/// - first-or-value(("a", "b")) → "a"
|
|
296
|
+
/// - first-or-value(()) → none
|
|
297
|
+
/// - first-or-value(none) → none
|
|
298
|
+
#let first-or-value(value) = {
|
|
299
|
+
if value == none {
|
|
300
|
+
none
|
|
301
|
+
} else if type(value) == array {
|
|
302
|
+
if value.len() > 0 {
|
|
303
|
+
value.at(0)
|
|
304
|
+
} else {
|
|
305
|
+
none
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
value
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
// =============================================================================
|
|
314
|
+
// INDORSEMENT UTILITIES
|
|
315
|
+
// =============================================================================
|
|
316
|
+
|
|
317
|
+
/// Converts number to ordinal suffix for indorsements following AFH 33-337 conventions.
|
|
318
|
+
///
|
|
319
|
+
/// AFH 33-337 Chapter 14 indorsement examples show "1st Ind", "2d Ind", "3d Ind" format.
|
|
320
|
+
/// Note: Military style uses "2d" and "3d" instead of "2nd" and "3rd" per DoD correspondence standards.
|
|
321
|
+
///
|
|
322
|
+
/// Generates proper ordinal suffixes for indorsement numbering:
|
|
323
|
+
/// - 1st, 2d, 3d, 4th, 5th, etc. (note: military uses "2d" and "3d", not "2nd" and "3rd")
|
|
324
|
+
/// - Special handling for 11th, 12th, 13th (all use "th")
|
|
325
|
+
/// - Follows official military correspondence standards
|
|
326
|
+
///
|
|
327
|
+
/// - number (int): The indorsement number (1, 2, 3, etc.)
|
|
328
|
+
/// -> str
|
|
329
|
+
// NOTE: Could be table-driven vs algorithmic (see CASCADES.md §B) — current approach is correct
|
|
330
|
+
#let get-ordinal-suffix(number) = {
|
|
331
|
+
let last-digit = calc.rem(number, 10)
|
|
332
|
+
let last-two-digits = calc.rem(number, 100)
|
|
333
|
+
|
|
334
|
+
if last-two-digits >= 11 and last-two-digits <= 13 {
|
|
335
|
+
"th"
|
|
336
|
+
} else if last-digit == 1 {
|
|
337
|
+
"st"
|
|
338
|
+
} else if last-digit == 2 {
|
|
339
|
+
"d"
|
|
340
|
+
} else if last-digit == 3 {
|
|
341
|
+
"d"
|
|
342
|
+
} else {
|
|
343
|
+
"th"
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/// Formats indorsement number according to AFH 33-337 standards.
|
|
348
|
+
///
|
|
349
|
+
/// Creates properly formatted indorsement labels with ordinal suffixes:
|
|
350
|
+
/// - "1st Ind", "2d Ind", "3d Ind", "4th Ind", etc.
|
|
351
|
+
/// - Uses military-specific ordinal format (2d/3d instead of 2nd/3rd)
|
|
352
|
+
/// - Combines with "Ind" suffix for standard indorsement header format
|
|
353
|
+
///
|
|
354
|
+
/// - number (int): Indorsement sequence number (1, 2, 3, etc.)
|
|
355
|
+
/// -> str
|
|
356
|
+
#let format-indorsement-number(number) = {
|
|
357
|
+
let suffix = get-ordinal-suffix(number)
|
|
358
|
+
str(number) + suffix + " Ind"
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// Processes and renders an array of indorsements.
|
|
362
|
+
///
|
|
363
|
+
/// Iterates through an array of indorsement objects and renders each one
|
|
364
|
+
/// with proper formatting and font settings. Used by the main memorandum
|
|
365
|
+
/// template to process the indorsements parameter.
|
|
366
|
+
///
|
|
367
|
+
/// - indorsements (array): Array of indorsement objects created with indorsement()
|
|
368
|
+
/// - body-font (str | array): Font(s) to use for indorsement text
|
|
369
|
+
/// - font-size (length): Font size for indorsement text (default: 12pt)
|
|
370
|
+
/// -> content
|
|
371
|
+
#let process-indorsements(indorsements, body-font: none, font-size: 12pt) = {
|
|
372
|
+
if not falsey(indorsements) {
|
|
373
|
+
for indorsement in indorsements {
|
|
374
|
+
(indorsement.render)(body-font: body-font, font-size: font-size)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|