@airmark/quiver 0.32.1 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -18
- package/package.json +8 -6
- package/quills/af4141/0.1.0/Quill.yaml +1 -1
- package/quills/afmc_moa/0.0.1/Quill.yaml +10 -17
- package/quills/classic_resume/0.0.1/Quill.yaml +361 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/LICENSE +21 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Bold.ttf +0 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-BoldItalic.ttf +0 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Italic.ttf +0 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Regular.ttf +0 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/OFL.txt +94 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/components.typ +181 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/config.typ +53 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/layout.typ +57 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/lib.typ +6 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/util.typ +32 -0
- package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/typst.toml +26 -0
- package/quills/classic_resume/0.0.1/plate.typ +143 -0
- package/quills/cyber_ribbon_chart/0.0.1/Quill.yaml +320 -0
- package/quills/cyber_ribbon_chart/0.0.1/plate.typ +428 -0
- package/quills/daf1206/0.1.0/Quill.yaml +1 -2
- package/quills/daf4392/0.1.0/Quill.yaml +15 -1
- package/quills/daf4392/0.1.0/plate.typ +2 -2
- package/quills/usaf_letter/0.1.0/Quill.yaml +214 -0
- package/quills/usaf_letter/0.1.0/assets/dod_seal.png +0 -0
- package/quills/usaf_letter/0.1.0/assets/dow_seal.png +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/.gitignore +7 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/LICENSE +201 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/Cinzel/LICENSE +93 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/CopperplateCC-Bold.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/LICENSE.md +79 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LICENSE +102 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LiberationMono-Regular.ttf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/backmatter.typ +32 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/config.typ +45 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/frontmatter.typ +226 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/lib.typ +35 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/mainmatter.typ +140 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/primitives.typ +544 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/utils.typ +334 -0
- package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/typst.toml +22 -0
- package/quills/usaf_letter/0.1.0/plate.typ +124 -0
- package/quills/usaf_memo/0.2.0/Quill.yaml +1 -1
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +1 -1
- package/quills/usaf_memo/0.3.0/Quill.yaml +75 -56
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +29 -17
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +97 -126
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/config.typ +12 -45
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +81 -87
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +58 -66
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/lib.typ +14 -36
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +47 -19
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +178 -121
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +41 -141
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/typst.toml +3 -8
- package/quills/usaf_memo/0.3.0/plate.typ +89 -85
- package/templates/af4141.md +23 -0
- package/templates/appointment_letter.md +58 -0
- package/templates/daf1206.md +23 -0
- package/templates/daf4392.md +34 -0
- package/templates/pass_request.md +48 -0
- package/templates/rebuttal.md +63 -0
- package/templates/templates.json +58 -0
- package/templates/usaf_template.md +30 -0
- package/templates/ussf_template.md +30 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel[wght].ttf +0 -0
|
@@ -1,21 +1,8 @@
|
|
|
1
|
-
//
|
|
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
|
|
1
|
+
// Spacing, normalization, and formatting helpers shared across the template.
|
|
14
2
|
|
|
15
3
|
#import "config.typ": CLASSIFICATION_COLORS, counters, paragraph-config, spacing
|
|
16
4
|
|
|
17
|
-
//
|
|
18
|
-
// Value is a `length` set once in `frontmatter`.
|
|
5
|
+
// Measured height of one wrapped body line, set once in `frontmatter`.
|
|
19
6
|
#let LINE_STRIDE = state("LINE_STRIDE")
|
|
20
7
|
|
|
21
8
|
/// The height one wrapped line of body text occupies.
|
|
@@ -48,25 +35,16 @@
|
|
|
48
35
|
}
|
|
49
36
|
|
|
50
37
|
/// Creates vertical spacing equivalent to one blank line.
|
|
51
|
-
/// Convenience function for single line spacing.
|
|
52
38
|
///
|
|
53
39
|
/// -> content
|
|
54
40
|
#let blank-line() = blank-lines(1)
|
|
55
41
|
|
|
56
|
-
// =============================================================================
|
|
57
|
-
// GENERAL UTILITY FUNCTIONS
|
|
58
|
-
// =============================================================================
|
|
59
|
-
|
|
60
42
|
/// Checks if a value is "falsey" (none, false, empty array, empty string, or
|
|
61
43
|
/// empty content).
|
|
62
44
|
///
|
|
63
|
-
///
|
|
64
|
-
///
|
|
65
|
-
///
|
|
66
|
-
/// Content fields (`plaintext` / `richtext`) reach the template in one of two
|
|
67
|
-
/// shapes: a blank one as the empty string `""`, a filled one as a markup
|
|
68
|
-
/// block. Empty content (`[]`) is covered too, since that is what an emptied
|
|
69
|
-
/// block and `join()`-over-nothing produce.
|
|
45
|
+
/// A caller may hand a field over as a `str` or as content, so both empty
|
|
46
|
+
/// shapes count: `""` and `[]`. Empty content is what an emptied markup block
|
|
47
|
+
/// and `join()`-over-nothing both produce.
|
|
70
48
|
///
|
|
71
49
|
/// - value (any): The value to check for "falsey" status
|
|
72
50
|
/// -> bool
|
|
@@ -80,11 +58,9 @@
|
|
|
80
58
|
)
|
|
81
59
|
}
|
|
82
60
|
|
|
83
|
-
/// Scales content to fit
|
|
61
|
+
/// Scales content uniformly to fit a fixed-size box, preserving aspect ratio.
|
|
84
62
|
///
|
|
85
|
-
///
|
|
86
|
-
/// the given dimensions. Commonly used for letterhead seals and other images
|
|
87
|
-
/// that need to fit specific size constraints while preserving proportions.
|
|
63
|
+
/// Used for letterhead seals and emblems, whose source images vary in size.
|
|
88
64
|
///
|
|
89
65
|
/// - width (length): Maximum width for the content (default: 2in)
|
|
90
66
|
/// - height (length): Maximum height for the content (default: 1in)
|
|
@@ -92,13 +68,9 @@
|
|
|
92
68
|
/// - body (content): Content to scale and fit
|
|
93
69
|
/// -> content
|
|
94
70
|
#let fit-box(width: 2in, height: 1in, alignment: left + horizon, body) = context {
|
|
95
|
-
// 1) measure the unscaled content
|
|
96
71
|
let s = measure(body)
|
|
97
|
-
|
|
98
|
-
// 2) compute the uniform scale that fits inside the box
|
|
99
72
|
let f = calc.min(width / s.width, height / s.height) * 100% // ratio
|
|
100
|
-
|
|
101
|
-
// 3) fixed-size box, center the scaled content, and reflow so layout respects it
|
|
73
|
+
// `reflow: true` so layout respects the scaled size rather than the original.
|
|
102
74
|
box(width: width, height: height, clip: true)[
|
|
103
75
|
#align(alignment)[
|
|
104
76
|
#scale(f, reflow: true)[#body]
|
|
@@ -177,24 +149,25 @@
|
|
|
177
149
|
/// line tall and sits on the baseline, so the slot lines up with where a
|
|
178
150
|
/// printed date would sit and takes exactly the space one would take.
|
|
179
151
|
///
|
|
180
|
-
/// `field` is the caller's fill-in widget
|
|
181
|
-
///
|
|
182
|
-
///
|
|
183
|
-
///
|
|
184
|
-
///
|
|
185
|
-
/// give, e.g. the package used outside Quillmark) leaves the slot blank.
|
|
152
|
+
/// `field` is the caller's fill-in widget, an interactive element such as a
|
|
153
|
+
/// PDF form field. It is anchored at the slot's top-left corner, the corner
|
|
154
|
+
/// such a widget's own rectangle grows right and down from, so one declared at
|
|
155
|
+
/// this slot's dimensions covers it exactly. With no widget to anchor, the slot
|
|
156
|
+
/// rules its baseline instead, for the endorser to date by hand.
|
|
186
157
|
///
|
|
187
158
|
/// - field (content | none): Fill-in widget to anchor in the slot
|
|
188
159
|
/// - width (length): Width of the slot; defaults to fit a long date such as
|
|
189
160
|
/// "15 September 2026".
|
|
190
161
|
/// -> content
|
|
191
|
-
#let date-placeholder-slot(field, width: 1in) = box(
|
|
162
|
+
#let date-placeholder-slot(field: none, width: 1in) = box(
|
|
192
163
|
width: width,
|
|
193
164
|
height: 1em,
|
|
194
165
|
// Keep the slot's bottom edge on the line's baseline, where the printed date
|
|
195
|
-
// would sit; the 1em height then reserves the line above it
|
|
166
|
+
// would sit; the 1em height then reserves the line above it — the space a
|
|
167
|
+
// handwritten date is written in.
|
|
196
168
|
// (A positive baseline shift would drop the box a full line too low.)
|
|
197
169
|
baseline: 0pt,
|
|
170
|
+
stroke: if field == none { (bottom: 0.5pt + black) },
|
|
198
171
|
if field != none { place(top + left, field) },
|
|
199
172
|
)
|
|
200
173
|
|
|
@@ -220,56 +193,41 @@
|
|
|
220
193
|
rgb(0, 0, 0)
|
|
221
194
|
}
|
|
222
195
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
// =============================================================================
|
|
226
|
-
|
|
227
|
-
/// Creates an automatic grid layout from a cell value or array of them.
|
|
228
|
-
///
|
|
229
|
-
/// Converts 1D content into a multi-column grid layout with proper spacing.
|
|
230
|
-
/// Used primarily for formatting recipient lists in the "MEMORANDUM FOR" section
|
|
231
|
-
/// where multiple organizations need to be displayed in columns.
|
|
196
|
+
/// Arranges one cell or an array of them into a `cols`-wide grid, filled
|
|
197
|
+
/// row-major. Used for the MEMORANDUM FOR recipient list.
|
|
232
198
|
///
|
|
233
|
-
///
|
|
234
|
-
///
|
|
235
|
-
///
|
|
236
|
-
/// - Handles a single cell or an array of them, as `str` or as content
|
|
237
|
-
/// - Adds padding cells to maintain consistent column alignment
|
|
238
|
-
///
|
|
239
|
-
/// Cells may be content, not just `str`: a `plaintext` recipient list lowers
|
|
240
|
-
/// each element to a markup block, which is what makes the rendered glyphs
|
|
241
|
-
/// carry their own spans and so become click-navigable.
|
|
199
|
+
/// Cells may be content as readily as `str`, so a caller that builds each
|
|
200
|
+
/// recipient as markup — to keep the rendered glyphs traceable to their
|
|
201
|
+
/// source — passes it through unflattened.
|
|
242
202
|
///
|
|
243
203
|
/// - content (str | content | array): Cell(s) to arrange in grid
|
|
244
204
|
/// - column-gutter (length): Space between columns (default: 0.5em)
|
|
245
205
|
/// - cols (int): Number of columns for the grid (default: 3)
|
|
246
206
|
/// -> grid
|
|
247
207
|
#let create-auto-grid(content, column-gutter: .5em, cols: 3) = {
|
|
248
|
-
let
|
|
208
|
+
let content-type = type(content)
|
|
249
209
|
|
|
250
|
-
|
|
251
|
-
if content_type != array {
|
|
210
|
+
if content-type != array {
|
|
252
211
|
content = (content,)
|
|
253
212
|
}
|
|
254
213
|
|
|
255
214
|
|
|
256
|
-
//
|
|
215
|
+
// Each row carries a trailing empty cell, so a row is `cols + 1` wide — the
|
|
216
|
+
// width both this loop and the tail padding below count against.
|
|
257
217
|
let cells = ()
|
|
258
218
|
let i = 0
|
|
259
219
|
for item in content {
|
|
260
220
|
i += 1
|
|
261
221
|
cells.push(item)
|
|
262
222
|
if calc.rem(i, cols) == 0 {
|
|
263
|
-
// Add empty cell to pad the page
|
|
264
223
|
cells.push([])
|
|
265
224
|
}
|
|
266
225
|
}
|
|
267
226
|
|
|
268
|
-
// Add padding cells to complete the last row if needed
|
|
269
227
|
let remainder = calc.rem(cells.len(), cols + 1)
|
|
270
228
|
if remainder != 0 {
|
|
271
|
-
let
|
|
272
|
-
for _ in range(
|
|
229
|
+
let padding-needed = (cols + 1) - remainder
|
|
230
|
+
for _ in range(padding-needed) {
|
|
273
231
|
cells.push([])
|
|
274
232
|
}
|
|
275
233
|
}
|
|
@@ -282,23 +240,10 @@
|
|
|
282
240
|
)
|
|
283
241
|
}
|
|
284
242
|
|
|
285
|
-
|
|
286
|
-
// TYPE NORMALIZATION UTILITIES
|
|
287
|
-
// =============================================================================
|
|
288
|
-
|
|
289
|
-
/// Ensures the input is an array. If already an array, returns as-is.
|
|
290
|
-
/// If not an array, wraps the value in a tuple.
|
|
291
|
-
///
|
|
292
|
-
/// This utility eliminates repetitive `if type(x) == array` checks throughout
|
|
293
|
-
/// the codebase by providing a canonical "normalize to array" function.
|
|
243
|
+
/// Normalizes a value to an array, wrapping a scalar and mapping `none` to `()`.
|
|
294
244
|
///
|
|
295
245
|
/// - value: Any value to normalize to array form
|
|
296
|
-
///
|
|
297
|
-
///
|
|
298
|
-
/// Examples:
|
|
299
|
-
/// - ensure-array("foo") → ("foo",)
|
|
300
|
-
/// - ensure-array(("a", "b")) → ("a", "b")
|
|
301
|
-
/// - ensure-array(none) → ()
|
|
246
|
+
/// -> array
|
|
302
247
|
#let ensure-array(value) = {
|
|
303
248
|
if value == none {
|
|
304
249
|
()
|
|
@@ -312,22 +257,18 @@
|
|
|
312
257
|
/// Normalizes a scalar-or-array field to a single stacked-lines **content**
|
|
313
258
|
/// value, one array element per line.
|
|
314
259
|
///
|
|
315
|
-
///
|
|
316
|
-
///
|
|
317
|
-
/// it arrives as Typst *content* (a markup block), and `str(..)` on content is
|
|
318
|
-
/// an error. Joining with `linebreak()` rather than `"\n"` renders the same
|
|
260
|
+
/// A field may arrive as a `str` or as content, and `str(..)` on content is an
|
|
261
|
+
/// error. Joining with `linebreak()` rather than `"\n"` renders the same
|
|
319
262
|
/// stacked lines while accepting either shape.
|
|
320
263
|
///
|
|
321
|
-
/// A blank
|
|
322
|
-
///
|
|
264
|
+
/// A blank field lands as empty content, which stays `falsey` for callers that
|
|
265
|
+
/// test the result.
|
|
323
266
|
///
|
|
324
267
|
/// - value (any): Scalar or array of scalars/content to stack
|
|
325
268
|
/// -> content
|
|
326
269
|
///
|
|
327
270
|
/// Examples:
|
|
328
|
-
/// - join-lines("foo") → [foo]
|
|
329
271
|
/// - join-lines(([a], [b])) → [a] + linebreak() + [b]
|
|
330
|
-
/// - join-lines(()) → []
|
|
331
272
|
/// - join-lines(none) → []
|
|
332
273
|
#let join-lines(value) = {
|
|
333
274
|
if value == none { return [] }
|
|
@@ -339,20 +280,11 @@
|
|
|
339
280
|
if joined == none { [] } else { joined }
|
|
340
281
|
}
|
|
341
282
|
|
|
342
|
-
///
|
|
343
|
-
///
|
|
344
|
-
/// This utility eliminates repetitive ternary operators like
|
|
345
|
-
/// `if type(x) == array { x.at(0) } else { x }` by providing a canonical
|
|
346
|
-
/// "first element or self" function.
|
|
283
|
+
/// The first element of an array, or the value itself when it is not one.
|
|
284
|
+
/// An empty array and `none` both yield `none`.
|
|
347
285
|
///
|
|
348
286
|
/// - value: Any value to extract from
|
|
349
|
-
///
|
|
350
|
-
///
|
|
351
|
-
/// Examples:
|
|
352
|
-
/// - first-or-value("foo") → "foo"
|
|
353
|
-
/// - first-or-value(("a", "b")) → "a"
|
|
354
|
-
/// - first-or-value(()) → none
|
|
355
|
-
/// - first-or-value(none) → none
|
|
287
|
+
/// -> any
|
|
356
288
|
#let first-or-value(value) = {
|
|
357
289
|
if value == none {
|
|
358
290
|
none
|
|
@@ -368,19 +300,10 @@
|
|
|
368
300
|
}
|
|
369
301
|
|
|
370
302
|
|
|
371
|
-
|
|
372
|
-
// INDORSEMENT UTILITIES
|
|
373
|
-
// =============================================================================
|
|
374
|
-
|
|
375
|
-
/// Converts number to ordinal suffix for indorsements following AFH 33-337 conventions.
|
|
303
|
+
/// The ordinal suffix for an indorsement number: 1st, 2d, 3d, 4th, …
|
|
376
304
|
///
|
|
377
|
-
///
|
|
378
|
-
///
|
|
379
|
-
///
|
|
380
|
-
/// Generates proper ordinal suffixes for indorsement numbering:
|
|
381
|
-
/// - 1st, 2d, 3d, 4th, 5th, etc. (note: military uses "2d" and "3d", not "2nd" and "3rd")
|
|
382
|
-
/// - Special handling for 11th, 12th, 13th (all use "th")
|
|
383
|
-
/// - Follows official military correspondence standards
|
|
305
|
+
/// Military correspondence writes "2d" and "3d" where ordinary English writes
|
|
306
|
+
/// "2nd" and "3rd"; 11th through 13th take "th" as usual.
|
|
384
307
|
///
|
|
385
308
|
/// - number (int): The indorsement number (1, 2, 3, etc.)
|
|
386
309
|
/// -> str
|
|
@@ -401,12 +324,7 @@
|
|
|
401
324
|
}
|
|
402
325
|
}
|
|
403
326
|
|
|
404
|
-
///
|
|
405
|
-
///
|
|
406
|
-
/// Creates properly formatted indorsement labels with ordinal suffixes:
|
|
407
|
-
/// - "1st Ind", "2d Ind", "3d Ind", "4th Ind", etc.
|
|
408
|
-
/// - Uses military-specific ordinal format (2d/3d instead of 2nd/3rd)
|
|
409
|
-
/// - Combines with "Ind" suffix for standard indorsement header format
|
|
327
|
+
/// An indorsement's header label: "1st Ind", "2d Ind", "3d Ind", …
|
|
410
328
|
///
|
|
411
329
|
/// - number (int): Indorsement sequence number (1, 2, 3, etc.)
|
|
412
330
|
/// -> str
|
|
@@ -414,21 +332,3 @@
|
|
|
414
332
|
let suffix = get-ordinal-suffix(number)
|
|
415
333
|
str(number) + suffix + " Ind"
|
|
416
334
|
}
|
|
417
|
-
|
|
418
|
-
/// Processes and renders an array of indorsements.
|
|
419
|
-
///
|
|
420
|
-
/// Iterates through an array of indorsement objects and renders each one
|
|
421
|
-
/// with proper formatting and font settings. Used by the main memorandum
|
|
422
|
-
/// template to process the indorsements parameter.
|
|
423
|
-
///
|
|
424
|
-
/// - indorsements (array): Array of indorsement objects created with indorsement()
|
|
425
|
-
/// - body-font (str | array): Font(s) to use for indorsement text
|
|
426
|
-
/// - font-size (length): Font size for indorsement text (default: 12pt)
|
|
427
|
-
/// -> content
|
|
428
|
-
#let process-indorsements(indorsements, body-font: none, font-size: 12pt) = {
|
|
429
|
-
if not falsey(indorsements) {
|
|
430
|
-
for indorsement in indorsements {
|
|
431
|
-
(indorsement.render)(body-font: body-font, font-size: font-size)
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "tonguetoquill-usaf-memo"
|
|
3
|
-
version = "
|
|
4
|
-
compiler = "0.
|
|
3
|
+
version = "5.0.0"
|
|
4
|
+
compiler = "0.15.1"
|
|
5
5
|
entrypoint = "src/lib.typ"
|
|
6
6
|
repository = "https://github.com/nibsbin/tonguetoquill-usaf-memo"
|
|
7
7
|
authors = ["Nibs"]
|
|
8
|
-
license = "
|
|
8
|
+
license = "Apache-2.0"
|
|
9
9
|
description = "Typeset memos that are fully compliant with AFH 33-337 'The Tongue and Quill'."
|
|
10
10
|
keywords = [
|
|
11
11
|
"USAF",
|
|
@@ -20,8 +20,3 @@ keywords = [
|
|
|
20
20
|
]
|
|
21
21
|
categories = ["office", "report"]
|
|
22
22
|
disciplines = ["politics", "law", "business", "engineering", "education"]
|
|
23
|
-
|
|
24
|
-
[template]
|
|
25
|
-
path = "template"
|
|
26
|
-
entrypoint = "usaf-template.typ"
|
|
27
|
-
thumbnail = "template/assets/thumbnail.png"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#import "@local/quillmark-helper:0.1.0": (
|
|
2
|
-
data, display,
|
|
2
|
+
data, display, form-field, signature-field,
|
|
3
3
|
)
|
|
4
|
-
#import "@local/tonguetoquill-usaf-memo:
|
|
4
|
+
#import "@local/tonguetoquill-usaf-memo:5.0.0": (
|
|
5
5
|
backmatter, date-pattern, frontmatter, indorsement, mainmatter,
|
|
6
6
|
)
|
|
7
7
|
|
|
@@ -16,28 +16,48 @@
|
|
|
16
16
|
// leaves no glyphs and takes no space from the flow, and the seal stands alone.
|
|
17
17
|
#let letterhead_lines = data.letterhead_title
|
|
18
18
|
|
|
19
|
-
// Body text size, in points. Also the height of
|
|
20
|
-
// header, which is what an omitted indorsement date reserves for its fill-in
|
|
21
|
-
// widget (`date-placeholder-slot`, whose slot is `1em` tall and 1in wide).
|
|
19
|
+
// Body text size, in points. Also the height of a blank date's fill-in widget.
|
|
22
20
|
#let body_font_size = data.font_size * 1pt
|
|
23
21
|
|
|
24
|
-
//
|
|
22
|
+
// A blank date's fill-in slot: an empty AcroForm text box the signer types the
|
|
23
|
+
// date into, set in the body face, the value flush right in the date line's
|
|
24
|
+
// slot as the printed date would sit. Wide enough for the longest date either
|
|
25
|
+
// style prints, "September 30, 2026", which runs 8em in the body face.
|
|
26
|
+
//
|
|
27
|
+
// The slot is built here rather than through the package's
|
|
28
|
+
// `date-placeholder-slot`: that slot stands `1em` above the baseline, and a
|
|
29
|
+
// text line only a cap-height, so a blank date grew its line and pushed
|
|
30
|
+
// everything below it down. This one claims exactly a cap-height, the extent of
|
|
31
|
+
// a printed date, and the widget overhangs it about the line's middle, taking
|
|
32
|
+
// no room from the flow.
|
|
33
|
+
#let date_slot_width = body_font_size * 8.5
|
|
34
|
+
#let date_slot(name, field) = context box(
|
|
35
|
+
width: date_slot_width,
|
|
36
|
+
height: measure[0].height,
|
|
37
|
+
place(horizon + left, form-field(
|
|
38
|
+
name,
|
|
39
|
+
type: "text",
|
|
40
|
+
width: date_slot_width,
|
|
41
|
+
height: body_font_size,
|
|
42
|
+
field: field,
|
|
43
|
+
font: "times",
|
|
44
|
+
size: body_font_size,
|
|
45
|
+
align: "right",
|
|
46
|
+
)),
|
|
47
|
+
)
|
|
48
|
+
|
|
25
49
|
#show: frontmatter.with(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"assets/dow_seal.png"
|
|
38
|
-
}
|
|
39
|
-
))
|
|
40
|
-
},
|
|
50
|
+
letterhead-title: letterhead_lines.at(0, default: ""),
|
|
51
|
+
letterhead-caption: if letterhead_lines.len() > 1 { letterhead_lines.slice(1) } else { () },
|
|
52
|
+
letterhead-seal-subtitle: data.letterhead_seal_subtitle,
|
|
53
|
+
// A memo has no seal-less state, so the blank takes the default, DoW.
|
|
54
|
+
letterhead-seal: image(
|
|
55
|
+
if data.letterhead_seal == "dod" {
|
|
56
|
+
"assets/dod_seal.png"
|
|
57
|
+
} else {
|
|
58
|
+
"assets/dow_seal.png"
|
|
59
|
+
}
|
|
60
|
+
),
|
|
41
61
|
|
|
42
62
|
// Date. `data.date` is the native `datetime` and would render identically,
|
|
43
63
|
// but its ink would be born inside the package and carry no schema address.
|
|
@@ -45,41 +65,29 @@
|
|
|
45
65
|
// born in the generated helper, so the memo date stays click-to-edit however
|
|
46
66
|
// deep the package formats it.
|
|
47
67
|
//
|
|
48
|
-
// A blank date
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// The stamp is markup, not the bare `str` `.display()` returns: a `str` off a
|
|
54
|
-
// function call carries no source position, and ink with none is unclaimable.
|
|
55
|
-
date: {
|
|
56
|
-
let pattern = date-pattern(memo-style: memo_style)
|
|
57
|
-
let authored = display("date", pattern)
|
|
58
|
-
if authored != none {
|
|
59
|
-
authored
|
|
60
|
-
} else {
|
|
61
|
-
field-region("date", [#datetime.today().display(pattern)])
|
|
62
|
-
}
|
|
63
|
-
},
|
|
68
|
+
// A blank date is left for the signer: a memo is dated when it is signed,
|
|
69
|
+
// which is generally not when it is rendered, so the slot is fillable rather
|
|
70
|
+
// than stamped with the compile date.
|
|
71
|
+
date: display("date", date-pattern(memo-style: memo_style)),
|
|
72
|
+
date-field: date_slot("Date", "date"),
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
memo_for: data.memo_for,
|
|
74
|
+
memo-for: data.memo_for,
|
|
67
75
|
|
|
68
|
-
//
|
|
69
|
-
..if data.memo_from.len() > 0 { (
|
|
76
|
+
// A memo with no FROM line is a Memorandum for Record.
|
|
77
|
+
..if data.memo_from.len() > 0 { (memo-from: data.memo_from) },
|
|
70
78
|
|
|
71
|
-
// Subject line
|
|
72
79
|
subject: data.subject,
|
|
73
80
|
|
|
74
|
-
// Optional references
|
|
75
81
|
..if data.references.len() > 0 { (references: data.references) },
|
|
76
82
|
|
|
77
|
-
//
|
|
78
|
-
|
|
83
|
+
// The package sets the tag line in Cinzel, which has no italic or bold face.
|
|
84
|
+
// The body font set here, nearer the text, wins and carries real italics and
|
|
85
|
+
// bold.
|
|
86
|
+
footer-tag-line: text(font: "NimbusRomNo9L", data.tag_line),
|
|
79
87
|
|
|
80
88
|
// The blank reads as no banner, which is what the package's own
|
|
81
|
-
// `
|
|
82
|
-
|
|
89
|
+
// `classification-level: none` default means.
|
|
90
|
+
classification-level: data.classification.value,
|
|
83
91
|
|
|
84
92
|
dissemination: data.dissemination,
|
|
85
93
|
|
|
@@ -90,44 +98,45 @@
|
|
|
90
98
|
// own `cui_*: none` defaults cover the worlds that omit them.
|
|
91
99
|
..if data.classification.value == "CUI" {
|
|
92
100
|
(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
cui-controlled-by: data.classification.controlled_by,
|
|
102
|
+
cui-category: data.classification.category,
|
|
103
|
+
cui-limited-dissemination: data.classification.limited_dissemination,
|
|
104
|
+
cui-poc: data.classification.poc,
|
|
97
105
|
)
|
|
98
106
|
},
|
|
99
107
|
|
|
100
|
-
|
|
101
|
-
memo_style: memo_style,
|
|
108
|
+
memo-style: memo_style,
|
|
102
109
|
|
|
103
|
-
|
|
104
|
-
font_size: body_font_size,
|
|
110
|
+
font-size: body_font_size,
|
|
105
111
|
|
|
106
|
-
//
|
|
107
|
-
|
|
112
|
+
// One recipient per line; the package's own default is three columns.
|
|
113
|
+
memo-for-cols: 1,
|
|
108
114
|
)
|
|
109
115
|
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
116
|
+
// The body's region needs no recovery step here: the package's render-body
|
|
117
|
+
// rebuilds paragraphs through a state buffer (AFH 33-337 auto-numbering), but
|
|
118
|
+
// the rebuilt glyphs keep their spans, which is what the backend reads regions
|
|
119
|
+
// from.
|
|
114
120
|
#mainmatter[
|
|
115
121
|
#data.at("$body")
|
|
116
122
|
]
|
|
117
123
|
|
|
118
|
-
// Backmatter
|
|
119
124
|
#backmatter(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
authority-line: data.authority_line,
|
|
126
|
+
signature-block: data.signature_block,
|
|
127
|
+
// The widget sits at the bottom of AFH 33-337's four blank lines and is
|
|
128
|
+
// sized to two and a half of them, so the line and a half above it stays
|
|
129
|
+
// clear of the body text without moving the block off the fifth line.
|
|
130
|
+
signing-field: signature-field(
|
|
131
|
+
"Signature",
|
|
132
|
+
field: "signature_block",
|
|
133
|
+
height: body_font_size * 2.5,
|
|
134
|
+
),
|
|
123
135
|
|
|
124
|
-
// Optional cc
|
|
125
136
|
..if data.cc.len() > 0 { (cc: data.cc) },
|
|
126
137
|
|
|
127
|
-
// Optional distribution
|
|
128
138
|
..if data.distribution.len() > 0 { (distribution: data.distribution) },
|
|
129
139
|
|
|
130
|
-
// Optional attachments
|
|
131
140
|
..if data.attachments.len() > 0 { (attachments: data.attachments) },
|
|
132
141
|
)
|
|
133
142
|
|
|
@@ -151,7 +160,6 @@
|
|
|
151
160
|
.at(-1, default: -1)
|
|
152
161
|
)
|
|
153
162
|
|
|
154
|
-
// Indorsements - iterate through CARDS array and filter by CARD tag
|
|
155
163
|
#for (i, card) in data.at("$cards").enumerate() {
|
|
156
164
|
if card.at("$kind", default: none) == "indorsement" {
|
|
157
165
|
// The quillmark helper leaves an unset/whitespace-only markdown body as
|
|
@@ -180,28 +188,24 @@
|
|
|
180
188
|
indorsement(
|
|
181
189
|
from: card.at("from", default: ""),
|
|
182
190
|
to: card.at("for", default: ""),
|
|
183
|
-
|
|
184
|
-
|
|
191
|
+
authority-line: card.authority_line,
|
|
192
|
+
signature-block: card.signature_block,
|
|
193
|
+
signing-field: signature-field(
|
|
185
194
|
"Ind_" + str(i) + "_Signature",
|
|
186
195
|
field: card.at("$path") + "signature_block",
|
|
196
|
+
height: body_font_size * 2.5,
|
|
187
197
|
),
|
|
188
198
|
..if card.format != "" { (format: card.format) },
|
|
189
|
-
date
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
(
|
|
196
|
-
"Ind_" + str(i) + "_Date",
|
|
197
|
-
type: "text",
|
|
198
|
-
width: 1in,
|
|
199
|
-
height: body_font_size,
|
|
200
|
-
field: card.at("$path") + "date",
|
|
201
|
-
))
|
|
199
|
+
// An omitted date becomes a fill-in slot for the signing date. Built only
|
|
200
|
+
// when there is no date to print: a widget over a printed date would offer
|
|
201
|
+
// an edit that the rendered document does not carry back.
|
|
202
|
+
date: if resolved_date != none {
|
|
203
|
+
resolved_date
|
|
204
|
+
} else {
|
|
205
|
+
date_slot("Ind_" + str(i) + "_Date", card.at("$path") + "date")
|
|
202
206
|
},
|
|
203
207
|
..if card.action != "" { (action: card.action) },
|
|
204
|
-
|
|
208
|
+
approval-authority: i == last_indorsement_index,
|
|
205
209
|
body_content,
|
|
206
210
|
)
|
|
207
211
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
~~~card-yaml
|
|
2
|
+
$quill: af4141@0.1
|
|
3
|
+
$kind: main
|
|
4
|
+
# EDIT: Full name in Last, First, Middle Initial format
|
|
5
|
+
name: LAST, FIRST M.
|
|
6
|
+
# EDIT: Unit of assignment (e.g., "1 ACCS/DOT", "726 ACS/MOC")
|
|
7
|
+
unit: UNIT/SYMBOL
|
|
8
|
+
# EDIT: Current grade or CCC level (e.g., "SSgt", "GS-12", "3")
|
|
9
|
+
grade: GRADE
|
|
10
|
+
# EDIT: Commander's authentication entry
|
|
11
|
+
commanders_auth: ""
|
|
12
|
+
~~~
|
|
13
|
+
|
|
14
|
+
~~~card-yaml
|
|
15
|
+
$kind: experience
|
|
16
|
+
date: 2025-01-01
|
|
17
|
+
action: Describe the action or event
|
|
18
|
+
written_grade: ""
|
|
19
|
+
written_grade_date: 2025-01-01
|
|
20
|
+
positional_grade: ""
|
|
21
|
+
positional_grade_date: 2025-01-01
|
|
22
|
+
auth_or_remarks: ""
|
|
23
|
+
~~~
|