@tonguetoquill/collection 0.2.3 → 0.2.5-beta.1
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 +88 -88
- 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 +48 -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/daf4392/0.1.0/Quill.yaml +110 -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 +33 -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/out/debug.typ +4 -0
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/out/example.typ +4 -0
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/page1.png +0 -0
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/typst.toml +7 -0
- package/quills/daf4392/0.1.0/plate.typ +60 -0
- 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/daf4392.md +33 -0
- 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 +55 -49
- package/templates/usaf_template.md +23 -23
- package/templates/ussf_template.md +29 -29
|
@@ -1,377 +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
|
-
}
|
|
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
|
+
}
|