@airmark/quiver 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/Quiver.yaml +2 -0
- package/README.md +92 -0
- package/index.d.ts +16 -0
- package/index.js +28 -0
- package/package.json +44 -0
- package/quills/af4141/0.1.0/Quill.yaml +95 -0
- package/quills/af4141/0.1.0/assets/Figtree-Bold.ttf +0 -0
- package/quills/af4141/0.1.0/assets/Figtree-Italic.ttf +0 -0
- package/quills/af4141/0.1.0/assets/Figtree-Regular.ttf +0 -0
- 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 +20 -0
- package/quills/af4141/0.1.0/design/af4141.pdf +0 -0
- package/quills/af4141/0.1.0/example.md +86 -0
- package/quills/af4141/0.1.0/packages/typst-af4141/FIELDS.json +3170 -0
- package/quills/af4141/0.1.0/packages/typst-af4141/form.typ +538 -0
- package/quills/af4141/0.1.0/packages/typst-af4141/lib.typ +270 -0
- package/quills/af4141/0.1.0/packages/typst-af4141/page1.png +0 -0
- package/quills/af4141/0.1.0/packages/typst-af4141/page2.png +0 -0
- package/quills/af4141/0.1.0/packages/typst-af4141/typst.toml +7 -0
- package/quills/af4141/0.1.0/plate.typ +67 -0
- package/quills/daf1206/0.1.0/DAFForm_1206_prepared.pdf +0 -0
- package/quills/daf1206/0.1.0/Quill.yaml +55 -0
- package/quills/daf1206/0.1.0/assets/arimo-v35-latin-700.ttf +0 -0
- package/quills/daf1206/0.1.0/assets/arimo-v35-latin-700italic.ttf +0 -0
- package/quills/daf1206/0.1.0/assets/arimo-v35-latin-italic.ttf +0 -0
- package/quills/daf1206/0.1.0/assets/arimo-v35-latin-regular.ttf +0 -0
- package/quills/daf1206/0.1.0/example.md +23 -0
- package/quills/daf1206/0.1.0/packages/typst-daf1206/FIELDS.json +158 -0
- package/quills/daf1206/0.1.0/packages/typst-daf1206/form.typ +36 -0
- package/quills/daf1206/0.1.0/packages/typst-daf1206/lib.typ +297 -0
- package/quills/daf1206/0.1.0/packages/typst-daf1206/page1.svg +826 -0
- package/quills/daf1206/0.1.0/packages/typst-daf1206/page2.svg +497 -0
- package/quills/daf1206/0.1.0/packages/typst-daf1206/typst.toml +7 -0
- package/quills/daf1206/0.1.0/plate.typ +47 -0
- package/quills/daf4392/0.1.0/Quill.yaml +139 -0
- 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 +34 -0
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/FIELDS.json +9 -0
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/form.typ +14 -0
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/lib.typ +227 -0
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/typst.toml +7 -0
- package/quills/daf4392/0.1.0/plate.typ +86 -0
- package/quills/usaf_memo/0.2.0/.quillignore +31 -0
- package/quills/usaf_memo/0.2.0/Quill.yaml +255 -0
- package/quills/usaf_memo/0.2.0/assets/dod_seal.png +0 -0
- package/quills/usaf_memo/0.2.0/assets/dow_seal.png +0 -0
- package/quills/usaf_memo/0.2.0/example.md +56 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/.gitignore +7 -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 +344 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/config.typ +73 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +137 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +111 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/lib.typ +57 -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 +321 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +356 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +27 -0
- package/quills/usaf_memo/0.2.0/plate.typ +85 -0
|
@@ -0,0 +1,356 @@
|
|
|
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
|
+
// Shared measured line-stride cache used by blank-line spacing and body
|
|
18
|
+
// line-count heuristics. Value is a `length` set once in `frontmatter`.
|
|
19
|
+
#let LINE_STRIDE = state("LINE_STRIDE")
|
|
20
|
+
// Shared blank-line step cache used by structural spacing.
|
|
21
|
+
#let BLANK_LINE_STEP = state("BLANK_LINE_STEP")
|
|
22
|
+
|
|
23
|
+
/// Creates vertical spacing equivalent to multiple blank lines.
|
|
24
|
+
///
|
|
25
|
+
/// Each step matches one row of the body paragraph line grid.
|
|
26
|
+
///
|
|
27
|
+
/// - count (int): Number of blank lines to create
|
|
28
|
+
/// - weak (bool): Whether spacing can be compressed at page breaks
|
|
29
|
+
/// -> content
|
|
30
|
+
#let blank-lines(count, weak: true) = {
|
|
31
|
+
if count == 0 {
|
|
32
|
+
v(0em, weak: weak)
|
|
33
|
+
} else {
|
|
34
|
+
context {
|
|
35
|
+
let measured-stride = {
|
|
36
|
+
let one-line = measure(par(spacing: 0pt)[x]).height
|
|
37
|
+
measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
|
|
38
|
+
}
|
|
39
|
+
// Scale legacy visual step with current font size:
|
|
40
|
+
// 19.05pt was tuned for 12pt body text.
|
|
41
|
+
let em-size = measure(box(width: 1em)[]).width
|
|
42
|
+
let legacy-scaled = spacing.vertical * (em-size / 12pt)
|
|
43
|
+
let fallback-step = calc.max(measured-stride, legacy-scaled)
|
|
44
|
+
let cached-step = BLANK_LINE_STEP.get()
|
|
45
|
+
let step = if cached-step != none { cached-step } else { fallback-step }
|
|
46
|
+
v(step * count, weak: weak)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Creates vertical spacing equivalent to one blank line.
|
|
52
|
+
/// Convenience function for single line spacing.
|
|
53
|
+
///
|
|
54
|
+
/// - weak (bool): Whether spacing can be compressed at page breaks
|
|
55
|
+
/// -> content
|
|
56
|
+
#let blank-line(weak: true) = blank-lines(1, weak: weak)
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// GENERAL UTILITY FUNCTIONS
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
/// Checks if a value is "falsey" (none, false, empty array, or empty string).
|
|
63
|
+
///
|
|
64
|
+
/// Provides a consistent way to test for empty or missing values across
|
|
65
|
+
/// the template system. Used for conditional rendering of optional sections.
|
|
66
|
+
///
|
|
67
|
+
/// - value (any): The value to check for "falsey" status
|
|
68
|
+
/// -> bool
|
|
69
|
+
#let falsey(value) = {
|
|
70
|
+
value == none or value == false or (type(value) == array and value.len() == 0) or (type(value) == str and value == "")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Scales content to fit within a specified box while maintaining aspect ratio.
|
|
74
|
+
///
|
|
75
|
+
/// Automatically measures content and calculates uniform scaling to fit within
|
|
76
|
+
/// the given dimensions. Commonly used for letterhead seals and other images
|
|
77
|
+
/// that need to fit specific size constraints while preserving proportions.
|
|
78
|
+
///
|
|
79
|
+
/// - width (length): Maximum width for the content (default: 2in)
|
|
80
|
+
/// - height (length): Maximum height for the content (default: 1in)
|
|
81
|
+
/// - alignment (alignment): Content alignment within the box (default: left+horizon)
|
|
82
|
+
/// - body (content): Content to scale and fit
|
|
83
|
+
/// -> content
|
|
84
|
+
#let fit-box(width: 2in, height: 1in, alignment: left + horizon, body) = context {
|
|
85
|
+
// 1) measure the unscaled content
|
|
86
|
+
let s = measure(body)
|
|
87
|
+
|
|
88
|
+
// 2) compute the uniform scale that fits inside the box
|
|
89
|
+
let f = calc.min(width / s.width, height / s.height) * 100% // ratio
|
|
90
|
+
|
|
91
|
+
// 3) fixed-size box, center the scaled content, and reflow so layout respects it
|
|
92
|
+
box(width: width, height: height, clip: true)[
|
|
93
|
+
#align(alignment)[
|
|
94
|
+
#scale(f, reflow: true)[#body]
|
|
95
|
+
]
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Formats a date for the memo heading.
|
|
100
|
+
///
|
|
101
|
+
/// - String: shown as-is (use for fixed text like placeholders).
|
|
102
|
+
/// - datetime: USAF style `DD Month YYYY`; DAF style `Month DD, YYYY`.
|
|
103
|
+
///
|
|
104
|
+
/// - date (str|datetime): Date to format for display
|
|
105
|
+
/// - memo-style (str): `"usaf"` or `"daf"`
|
|
106
|
+
/// -> content
|
|
107
|
+
#let display-date(date, memo-style: "usaf") = {
|
|
108
|
+
assert(
|
|
109
|
+
memo-style in ("usaf", "daf"),
|
|
110
|
+
message: "memo-style for display-date must be \"usaf\" or \"daf\"",
|
|
111
|
+
)
|
|
112
|
+
if type(date) == str {
|
|
113
|
+
date
|
|
114
|
+
} else {
|
|
115
|
+
let pattern = if memo-style == "daf" {
|
|
116
|
+
"[month repr:long] [day padding:none], [year]"
|
|
117
|
+
} else {
|
|
118
|
+
"[day padding:none] [month repr:long] [year]"
|
|
119
|
+
}
|
|
120
|
+
date.display(pattern)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Gets the banner color for a classification marking.
|
|
125
|
+
///
|
|
126
|
+
/// Matches when `level` (trimmed) starts with a known prefix: TOP SECRET, SECRET, or UNCLASSIFIED.
|
|
127
|
+
/// Otherwise returns black.
|
|
128
|
+
///
|
|
129
|
+
/// - level (str): Marking string shown in header/footer
|
|
130
|
+
/// -> color
|
|
131
|
+
#let get-classification-level-color(level) = {
|
|
132
|
+
if level == none or type(level) != str {
|
|
133
|
+
return rgb(0, 0, 0)
|
|
134
|
+
}
|
|
135
|
+
let s = level.trim()
|
|
136
|
+
// "TOP SECRET" before "SECRET" so the full phrase matches first.
|
|
137
|
+
let level-order = ("TOP SECRET", "SECRET", "UNCLASSIFIED")
|
|
138
|
+
for base-level in level-order {
|
|
139
|
+
if s.starts-with(base-level) {
|
|
140
|
+
return CLASSIFICATION_COLORS.at(base-level)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
rgb(0, 0, 0)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// =============================================================================
|
|
147
|
+
// GRID LAYOUT UTILITIES
|
|
148
|
+
// =============================================================================
|
|
149
|
+
|
|
150
|
+
/// Creates an automatic grid layout from string or array content.
|
|
151
|
+
///
|
|
152
|
+
/// Converts 1D content into a multi-column grid layout with proper spacing.
|
|
153
|
+
/// Used primarily for formatting recipient lists in the "MEMORANDUM FOR" section
|
|
154
|
+
/// where multiple organizations need to be displayed in columns.
|
|
155
|
+
///
|
|
156
|
+
/// Features:
|
|
157
|
+
/// - Automatic column distribution and row filling
|
|
158
|
+
/// - Configurable column spacing and count
|
|
159
|
+
/// - Handles both single strings and arrays of strings
|
|
160
|
+
/// - Adds padding cells to maintain consistent column alignment
|
|
161
|
+
///
|
|
162
|
+
/// - content (str | array): Content to arrange in grid (strings only)
|
|
163
|
+
/// - column-gutter (length): Space between columns (default: 0.5em)
|
|
164
|
+
/// - cols (int): Number of columns for the grid (default: 3)
|
|
165
|
+
/// -> grid
|
|
166
|
+
#let create-auto-grid(content, column-gutter: .5em, cols: 3) = {
|
|
167
|
+
let content_type = type(content)
|
|
168
|
+
|
|
169
|
+
assert(content_type == str or content_type == array, message: "Content must be a string or an array of strings.")
|
|
170
|
+
if content_type == array {
|
|
171
|
+
for item in content {
|
|
172
|
+
assert(type(item) == str, message: "All items in content array must be strings.")
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Normalize to 1d array
|
|
177
|
+
if content_type == str {
|
|
178
|
+
content = (content,)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
// Build cell array in row-major order
|
|
183
|
+
let cells = ()
|
|
184
|
+
let i = 0
|
|
185
|
+
for item in content {
|
|
186
|
+
i += 1
|
|
187
|
+
cells.push(item)
|
|
188
|
+
if calc.rem(i, cols) == 0 {
|
|
189
|
+
// Add empty cell to pad the page
|
|
190
|
+
cells.push([])
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Add padding cells to complete the last row if needed
|
|
195
|
+
let remainder = calc.rem(cells.len(), cols + 1)
|
|
196
|
+
if remainder != 0 {
|
|
197
|
+
let padding_needed = (cols + 1) - remainder
|
|
198
|
+
for _ in range(padding_needed) {
|
|
199
|
+
cells.push([])
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
grid(
|
|
204
|
+
columns: calc.max(1, cols) + 1,
|
|
205
|
+
column-gutter: .1fr,
|
|
206
|
+
row-gutter: spacing.line,
|
|
207
|
+
..cells
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// =============================================================================
|
|
212
|
+
// TYPE NORMALIZATION UTILITIES
|
|
213
|
+
// =============================================================================
|
|
214
|
+
|
|
215
|
+
/// Ensures the input is an array. If already an array, returns as-is.
|
|
216
|
+
/// If not an array, wraps the value in a tuple.
|
|
217
|
+
///
|
|
218
|
+
/// This utility eliminates repetitive `if type(x) == array` checks throughout
|
|
219
|
+
/// the codebase by providing a canonical "normalize to array" function.
|
|
220
|
+
///
|
|
221
|
+
/// - value: Any value to normalize to array form
|
|
222
|
+
/// - Returns: Array containing the value(s)
|
|
223
|
+
///
|
|
224
|
+
/// Examples:
|
|
225
|
+
/// - ensure-array("foo") → ("foo",)
|
|
226
|
+
/// - ensure-array(("a", "b")) → ("a", "b")
|
|
227
|
+
/// - ensure-array(none) → ()
|
|
228
|
+
#let ensure-array(value) = {
|
|
229
|
+
if value == none {
|
|
230
|
+
()
|
|
231
|
+
} else if type(value) == array {
|
|
232
|
+
value
|
|
233
|
+
} else {
|
|
234
|
+
(value,)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Ensures the input is a string. If already a string, returns as-is.
|
|
239
|
+
/// If an array, joins elements with the specified separator.
|
|
240
|
+
///
|
|
241
|
+
/// This utility eliminates repetitive `if type(x) == array { x.join(...) }`
|
|
242
|
+
/// checks throughout the codebase by providing a canonical "normalize to string"
|
|
243
|
+
/// function.
|
|
244
|
+
///
|
|
245
|
+
/// - value: Any value to normalize to string form
|
|
246
|
+
/// - separator: String to use when joining array elements (default: "\n")
|
|
247
|
+
/// - Returns: String representation of the value
|
|
248
|
+
///
|
|
249
|
+
/// Examples:
|
|
250
|
+
/// - ensure-string("foo") → "foo"
|
|
251
|
+
/// - ensure-string(("a", "b")) → "a\nb"
|
|
252
|
+
/// - ensure-string(("a", "b"), ", ") → "a, b"
|
|
253
|
+
/// - ensure-string(none) → ""
|
|
254
|
+
#let ensure-string(value, separator: "\n") = {
|
|
255
|
+
if value == none {
|
|
256
|
+
""
|
|
257
|
+
} else if type(value) == array {
|
|
258
|
+
value.join(separator)
|
|
259
|
+
} else {
|
|
260
|
+
str(value)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/// Extracts the first element from an array, or returns the value if not an array.
|
|
265
|
+
///
|
|
266
|
+
/// This utility eliminates repetitive ternary operators like
|
|
267
|
+
/// `if type(x) == array { x.at(0) } else { x }` by providing a canonical
|
|
268
|
+
/// "first element or self" function.
|
|
269
|
+
///
|
|
270
|
+
/// - value: Any value to extract from
|
|
271
|
+
/// - Returns: First array element if array, otherwise the value itself
|
|
272
|
+
///
|
|
273
|
+
/// Examples:
|
|
274
|
+
/// - first-or-value("foo") → "foo"
|
|
275
|
+
/// - first-or-value(("a", "b")) → "a"
|
|
276
|
+
/// - first-or-value(()) → none
|
|
277
|
+
/// - first-or-value(none) → none
|
|
278
|
+
#let first-or-value(value) = {
|
|
279
|
+
if value == none {
|
|
280
|
+
none
|
|
281
|
+
} else if type(value) == array {
|
|
282
|
+
if value.len() > 0 {
|
|
283
|
+
value.at(0)
|
|
284
|
+
} else {
|
|
285
|
+
none
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
value
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
// =============================================================================
|
|
294
|
+
// INDORSEMENT UTILITIES
|
|
295
|
+
// =============================================================================
|
|
296
|
+
|
|
297
|
+
/// Converts number to ordinal suffix for indorsements following AFH 33-337 conventions.
|
|
298
|
+
///
|
|
299
|
+
/// AFH 33-337 Chapter 14 indorsement examples show "1st Ind", "2d Ind", "3d Ind" format.
|
|
300
|
+
/// Note: Military style uses "2d" and "3d" instead of "2nd" and "3rd" per DoD correspondence standards.
|
|
301
|
+
///
|
|
302
|
+
/// Generates proper ordinal suffixes for indorsement numbering:
|
|
303
|
+
/// - 1st, 2d, 3d, 4th, 5th, etc. (note: military uses "2d" and "3d", not "2nd" and "3rd")
|
|
304
|
+
/// - Special handling for 11th, 12th, 13th (all use "th")
|
|
305
|
+
/// - Follows official military correspondence standards
|
|
306
|
+
///
|
|
307
|
+
/// - number (int): The indorsement number (1, 2, 3, etc.)
|
|
308
|
+
/// -> str
|
|
309
|
+
#let get-ordinal-suffix(number) = {
|
|
310
|
+
let last-digit = calc.rem(number, 10)
|
|
311
|
+
let last-two-digits = calc.rem(number, 100)
|
|
312
|
+
|
|
313
|
+
if last-two-digits >= 11 and last-two-digits <= 13 {
|
|
314
|
+
"th"
|
|
315
|
+
} else if last-digit == 1 {
|
|
316
|
+
"st"
|
|
317
|
+
} else if last-digit == 2 {
|
|
318
|
+
"d"
|
|
319
|
+
} else if last-digit == 3 {
|
|
320
|
+
"d"
|
|
321
|
+
} else {
|
|
322
|
+
"th"
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/// Formats indorsement number according to AFH 33-337 standards.
|
|
327
|
+
///
|
|
328
|
+
/// Creates properly formatted indorsement labels with ordinal suffixes:
|
|
329
|
+
/// - "1st Ind", "2d Ind", "3d Ind", "4th Ind", etc.
|
|
330
|
+
/// - Uses military-specific ordinal format (2d/3d instead of 2nd/3rd)
|
|
331
|
+
/// - Combines with "Ind" suffix for standard indorsement header format
|
|
332
|
+
///
|
|
333
|
+
/// - number (int): Indorsement sequence number (1, 2, 3, etc.)
|
|
334
|
+
/// -> str
|
|
335
|
+
#let format-indorsement-number(number) = {
|
|
336
|
+
let suffix = get-ordinal-suffix(number)
|
|
337
|
+
str(number) + suffix + " Ind"
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/// Processes and renders an array of indorsements.
|
|
341
|
+
///
|
|
342
|
+
/// Iterates through an array of indorsement objects and renders each one
|
|
343
|
+
/// with proper formatting and font settings. Used by the main memorandum
|
|
344
|
+
/// template to process the indorsements parameter.
|
|
345
|
+
///
|
|
346
|
+
/// - indorsements (array): Array of indorsement objects created with indorsement()
|
|
347
|
+
/// - body-font (str | array): Font(s) to use for indorsement text
|
|
348
|
+
/// - font-size (length): Font size for indorsement text (default: 12pt)
|
|
349
|
+
/// -> content
|
|
350
|
+
#let process-indorsements(indorsements, body-font: none, font-size: 12pt) = {
|
|
351
|
+
if not falsey(indorsements) {
|
|
352
|
+
for indorsement in indorsements {
|
|
353
|
+
(indorsement.render)(body-font: body-font, font-size: font-size)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "tonguetoquill-usaf-memo"
|
|
3
|
+
version = "3.0.0"
|
|
4
|
+
compiler = "0.14.0"
|
|
5
|
+
entrypoint = "src/lib.typ"
|
|
6
|
+
repository = "https://github.com/nibsbin/tonguetoquill-usaf-memo"
|
|
7
|
+
authors = ["Nibs"]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
description = "Typeset memos that are fully compliant with AFH 33-337 'The Tongue and Quill'."
|
|
10
|
+
keywords = [
|
|
11
|
+
"USAF",
|
|
12
|
+
"Air Force",
|
|
13
|
+
"memorandum",
|
|
14
|
+
"memo",
|
|
15
|
+
"military",
|
|
16
|
+
"AFH 33-337",
|
|
17
|
+
"official",
|
|
18
|
+
"correspondence",
|
|
19
|
+
"formatting",
|
|
20
|
+
]
|
|
21
|
+
categories = ["office", "report"]
|
|
22
|
+
disciplines = ["politics", "law", "business", "engineering", "education"]
|
|
23
|
+
|
|
24
|
+
[template]
|
|
25
|
+
path = "template"
|
|
26
|
+
entrypoint = "usaf-template.typ"
|
|
27
|
+
thumbnail = "template/assets/thumbnail.png"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#import "@local/quillmark-helper:0.1.0": data
|
|
2
|
+
#import "@local/tonguetoquill-usaf-memo:3.0.0": backmatter, frontmatter, indorsement, mainmatter
|
|
3
|
+
|
|
4
|
+
// Frontmatter configuration
|
|
5
|
+
#show: frontmatter.with(
|
|
6
|
+
// Letterhead configuration
|
|
7
|
+
letterhead_title: data.letterhead_title,
|
|
8
|
+
letterhead_caption: data.letterhead_caption,
|
|
9
|
+
letterhead_seal_subtitle: data.at("letterhead_seal_subtitle", default: none),
|
|
10
|
+
letterhead_seal: image(
|
|
11
|
+
if data.at("letterhead_seal", default: "dow") == "dod" {
|
|
12
|
+
"assets/dod_seal.png"
|
|
13
|
+
} else {
|
|
14
|
+
"assets/dow_seal.png"
|
|
15
|
+
}
|
|
16
|
+
),
|
|
17
|
+
|
|
18
|
+
// Date
|
|
19
|
+
date: data.at("date", default: none),
|
|
20
|
+
|
|
21
|
+
// Receiver information
|
|
22
|
+
memo_for: data.memo_for,
|
|
23
|
+
|
|
24
|
+
// Sender information
|
|
25
|
+
memo_from: data.memo_from,
|
|
26
|
+
|
|
27
|
+
// Subject line
|
|
28
|
+
subject: data.subject,
|
|
29
|
+
|
|
30
|
+
// Optional references
|
|
31
|
+
..if "references" in data { (references: data.references) },
|
|
32
|
+
|
|
33
|
+
// Optional footer tag line
|
|
34
|
+
..if "tag_line" in data { (footer_tag_line: data.tag_line) },
|
|
35
|
+
|
|
36
|
+
// Optional classification level
|
|
37
|
+
..if "classification" in data { (classification_level: data.classification) },
|
|
38
|
+
|
|
39
|
+
// USAF vs DAF memorandum style (date format, body indentation)
|
|
40
|
+
memo_style: data.at("memo_style", default: "usaf"),
|
|
41
|
+
|
|
42
|
+
// Font size
|
|
43
|
+
font_size: data.at("font_size", default: 12) * 1pt,
|
|
44
|
+
|
|
45
|
+
// List recipients in vertical list
|
|
46
|
+
memo_for_cols: 1,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// Mainmatter configuration
|
|
50
|
+
#mainmatter[
|
|
51
|
+
#data.BODY
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
// Backmatter
|
|
55
|
+
#backmatter(
|
|
56
|
+
// Signature block
|
|
57
|
+
signature_block: data.signature_block,
|
|
58
|
+
|
|
59
|
+
// Optional cc
|
|
60
|
+
..if "cc" in data { (cc: data.cc) },
|
|
61
|
+
|
|
62
|
+
// Optional distribution
|
|
63
|
+
..if "distribution" in data { (distribution: data.distribution) },
|
|
64
|
+
|
|
65
|
+
// Optional attachments
|
|
66
|
+
..if "attachments" in data { (attachments: data.attachments) },
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// Indorsements - iterate through CARDS array and filter by CARD type
|
|
70
|
+
#for card in data.CARDS {
|
|
71
|
+
if card.CARD == "indorsement" {
|
|
72
|
+
indorsement(
|
|
73
|
+
from: card.at("from", default: ""),
|
|
74
|
+
to: card.at("for", default: ""),
|
|
75
|
+
signature_block: card.signature_block,
|
|
76
|
+
..if "attachments" in card { (attachments: card.attachments) },
|
|
77
|
+
..if "cc" in card { (cc: card.cc) },
|
|
78
|
+
format: card.at("format", default: "standard"),
|
|
79
|
+
..if "date" in card { (date: card.date) },
|
|
80
|
+
..if "action" in card { (action: card.action) },
|
|
81
|
+
)[
|
|
82
|
+
#card.BODY
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
}
|