@airmark/quiver 0.32.2 → 0.34.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.
Files changed (90) hide show
  1. package/README.md +63 -21
  2. package/package.json +9 -7
  3. package/quills/af4141/0.1.0/Quill.yaml +4 -13
  4. package/quills/af4141/0.1.0/example.md +53 -0
  5. package/quills/afmc_moa/0.0.1/Quill.yaml +39 -62
  6. package/quills/afmc_moa/0.0.1/example.md +114 -0
  7. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/config.typ +1 -1
  8. package/quills/classic_resume/0.0.1/Quill.yaml +281 -0
  9. package/quills/classic_resume/0.0.1/example.md +111 -0
  10. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/LICENSE +21 -0
  11. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Bold.ttf +0 -0
  12. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-BoldItalic.ttf +0 -0
  13. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Italic.ttf +0 -0
  14. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Regular.ttf +0 -0
  15. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/OFL.txt +94 -0
  16. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/components.typ +214 -0
  17. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/config.typ +61 -0
  18. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/layout.typ +86 -0
  19. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/lib.typ +7 -0
  20. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/util.typ +37 -0
  21. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/typst.toml +26 -0
  22. package/quills/classic_resume/0.0.1/plate.typ +143 -0
  23. package/quills/cyber_ribbon_chart/0.0.1/Quill.yaml +327 -0
  24. package/quills/cyber_ribbon_chart/0.0.1/example.md +55 -0
  25. package/quills/cyber_ribbon_chart/0.0.1/plate.typ +609 -0
  26. package/quills/daf1206/0.1.0/Quill.yaml +15 -18
  27. package/quills/daf1206/0.1.0/example.md +55 -0
  28. package/quills/daf4392/0.1.0/Quill.yaml +16 -7
  29. package/quills/daf4392/0.1.0/example.md +51 -0
  30. package/quills/daf4392/0.1.0/plate.typ +2 -2
  31. package/quills/usaf_letter/0.1.0/Quill.yaml +179 -0
  32. package/quills/usaf_letter/0.1.0/assets/dod_seal.png +0 -0
  33. package/quills/usaf_letter/0.1.0/assets/dow_seal.png +0 -0
  34. package/quills/usaf_letter/0.1.0/example.md +35 -0
  35. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/.gitignore +7 -0
  36. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/LICENSE +201 -0
  37. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/CopperplateCC-Bold.otf +0 -0
  38. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/LICENSE.md +79 -0
  39. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LICENSE +102 -0
  40. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LiberationMono-Regular.ttf +0 -0
  41. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
  42. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
  43. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
  44. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
  45. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
  46. package/quills/{usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE → usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/OFL.txt} +2 -2
  47. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/SpectralSC-Italic.ttf +0 -0
  48. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/SpectralSC-Regular.ttf +0 -0
  49. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/backmatter.typ +32 -0
  50. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/config.typ +45 -0
  51. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/frontmatter.typ +226 -0
  52. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/lib.typ +35 -0
  53. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/mainmatter.typ +140 -0
  54. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/primitives.typ +544 -0
  55. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/utils.typ +334 -0
  56. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/typst.toml +22 -0
  57. package/quills/usaf_letter/0.1.0/plate.typ +121 -0
  58. package/quills/usaf_memo/0.2.0/Quill.yaml +8 -41
  59. package/quills/usaf_memo/0.2.0/example.md +45 -0
  60. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
  61. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +1 -1
  62. package/quills/usaf_memo/0.3.0/Quill.yaml +51 -69
  63. package/quills/usaf_memo/0.3.0/example.md +48 -0
  64. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
  65. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/OFL.txt +93 -0
  66. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/SpectralSC-Italic.ttf +0 -0
  67. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/SpectralSC-Regular.ttf +0 -0
  68. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +29 -17
  69. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +97 -126
  70. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/config.typ +12 -45
  71. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +81 -87
  72. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +58 -66
  73. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/lib.typ +14 -36
  74. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +47 -19
  75. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +158 -111
  76. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +41 -141
  77. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/typst.toml +3 -8
  78. package/quills/usaf_memo/0.3.0/plate.typ +78 -85
  79. package/templates/af4141.md +23 -0
  80. package/templates/appointment_letter.md +58 -0
  81. package/templates/daf1206.md +23 -0
  82. package/templates/daf4392.md +34 -0
  83. package/templates/deros_extension.md +40 -0
  84. package/templates/pass_request.md +48 -0
  85. package/templates/rebuttal.md +63 -0
  86. package/templates/templates.json +65 -0
  87. package/templates/usaf_template.md +30 -0
  88. package/templates/ussf_template.md +30 -0
  89. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel[wght].ttf +0 -0
  90. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
@@ -1,21 +1,8 @@
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
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
- // Shared measured line-stride cache used by body line-count heuristics.
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
- /// Provides a consistent way to test for empty or missing values across
64
- /// the template system. Used for conditional rendering of optional sections.
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 within a specified box while maintaining aspect ratio.
61
+ /// Scales content uniformly to fit a fixed-size box, preserving aspect ratio.
84
62
  ///
85
- /// Automatically measures content and calculates uniform scaling to fit within
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 — the Quillmark helper's
181
- /// `form-field(.., type: "text")`, which lowers to a fillable AcroForm text box
182
- /// in PDF output. It is anchored at the slot's top-left corner, the corner the
183
- /// widget's own rectangle grows right and down from, so a widget declared at
184
- /// this slot's dimensions covers it exactly. `none` (a caller with no widget to
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
- // GRID LAYOUT UTILITIES
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
- /// Features:
234
- /// - Automatic column distribution and row filling
235
- /// - Configurable column spacing and count
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 content_type = type(content)
208
+ let content-type = type(content)
249
209
 
250
- // Normalize to 1d array. A lone cell — `str` or content — is a one-cell grid.
251
- if content_type != array {
210
+ if content-type != array {
252
211
  content = (content,)
253
212
  }
254
213
 
255
214
 
256
- // Build cell array in row-major order
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 padding_needed = (cols + 1) - remainder
272
- for _ in range(padding_needed) {
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
- /// - Returns: Array containing the value(s)
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
- /// This is the content-safe successor to the former `ensure-string`: every
316
- /// prose field the quill declares is now a `plaintext` / `richtext` field, so
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 content field arrives as `""` and lands as empty content, which
322
- /// stays `falsey` for callers that test the result.
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
- /// Extracts the first element from an array, or returns the value if not an array.
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
- /// - Returns: First array element if array, otherwise the value itself
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
- /// AFH 33-337 Chapter 14 indorsement examples show "1st Ind", "2d Ind", "3d Ind" format.
378
- /// Note: Military style uses "2d" and "3d" instead of "2nd" and "3rd" per DoD correspondence standards.
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
- /// Formats indorsement number according to AFH 33-337 standards.
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.0.0"
4
- compiler = "0.14.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 = "MIT"
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, field-region, form-field, signature-field,
2
+ data, display, form-field, signature-field,
3
3
  )
4
- #import "@local/tonguetoquill-usaf-memo:4.0.0": (
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 one line of the indorsement
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
- // Frontmatter configuration
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
- // Letterhead configuration
27
- letterhead_title: letterhead_lines.at(0, default: ""),
28
- letterhead_caption: if letterhead_lines.len() > 1 { letterhead_lines.slice(1) } else { () },
29
- letterhead_seal_subtitle: data.letterhead_seal_subtitle,
30
- // Enum blank is `""`, not a seal. Omit so the package renders none rather
31
- // than treating the blank as DoW.
32
- ..if data.letterhead_seal != "" {
33
- (letterhead_seal: image(
34
- if data.letterhead_seal == "dod" {
35
- "assets/dod_seal.png"
36
- } else {
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,26 @@
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 means today's, and the plate stamps it rather than falling
49
- // through to `frontmatter`'s own `datetime.today()`: package-born ink carries
50
- // no address, so the one date a memo never types would be the one date a
51
- // preview cannot click. `field-region` claims that ink for the field instead.
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
- // Receiver information
66
- memo_for: data.memo_for,
74
+ memo-for: data.memo_for,
67
75
 
68
- // Sender information (omitted for Memorandum for Record)
69
- ..if data.memo_from.len() > 0 { (memo_from: data.memo_from) },
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
- // Optional footer tag line
78
- footer_tag_line: data.tag_line,
83
+ footer-tag-line: data.tag_line,
79
84
 
80
85
  // The blank reads as no banner, which is what the package's own
81
- // `classification_level: none` default means.
82
- classification_level: data.classification.value,
86
+ // `classification-level: none` default means.
87
+ classification-level: data.classification.value,
83
88
 
84
89
  dissemination: data.dissemination,
85
90
 
@@ -90,51 +95,45 @@
90
95
  // own `cui_*: none` defaults cover the worlds that omit them.
91
96
  ..if data.classification.value == "CUI" {
92
97
  (
93
- cui_controlled_by: data.classification.controlled_by,
94
- cui_category: data.classification.category,
95
- cui_limited_dissemination: data.classification.limited_dissemination,
96
- cui_poc: data.classification.poc,
98
+ cui-controlled-by: data.classification.controlled_by,
99
+ cui-category: data.classification.category,
100
+ cui-limited-dissemination: data.classification.limited_dissemination,
101
+ cui-poc: data.classification.poc,
97
102
  )
98
103
  },
99
104
 
100
- // USAF vs DAF memorandum style (date format, body indentation).
101
- memo_style: memo_style,
105
+ memo-style: memo_style,
102
106
 
103
- // Font size
104
- font_size: body_font_size,
107
+ font-size: body_font_size,
105
108
 
106
- // List recipients in vertical list
107
- memo_for_cols: 1,
109
+ // One recipient per line; the package's own default is three columns.
110
+ memo-for-cols: 1,
108
111
  )
109
112
 
110
- // Mainmatter. The body's region needs no recovery step here: the package's
111
- // render-body rebuilds paragraphs through a state buffer (AFH 33-337
112
- // auto-numbering), but the rebuilt glyphs keep their spans, which is what
113
- // the backend reads regions from.
113
+ // The body's region needs no recovery step here: the package's render-body
114
+ // rebuilds paragraphs through a state buffer (AFH 33-337 auto-numbering), but
115
+ // the rebuilt glyphs keep their spans, which is what the backend reads regions
116
+ // from.
114
117
  #mainmatter[
115
118
  #data.at("$body")
116
119
  ]
117
120
 
118
- // Backmatter
119
121
  #backmatter(
120
- // Signature block
121
- signature_block: data.signature_block,
122
+ authority-line: data.authority_line,
123
+ signature-block: data.signature_block,
122
124
  // The widget sits at the bottom of AFH 33-337's four blank lines and is
123
125
  // sized to two and a half of them, so the line and a half above it stays
124
126
  // clear of the body text without moving the block off the fifth line.
125
- signing_field: signature-field(
127
+ signing-field: signature-field(
126
128
  "Signature",
127
129
  field: "signature_block",
128
130
  height: body_font_size * 2.5,
129
131
  ),
130
132
 
131
- // Optional cc
132
133
  ..if data.cc.len() > 0 { (cc: data.cc) },
133
134
 
134
- // Optional distribution
135
135
  ..if data.distribution.len() > 0 { (distribution: data.distribution) },
136
136
 
137
- // Optional attachments
138
137
  ..if data.attachments.len() > 0 { (attachments: data.attachments) },
139
138
  )
140
139
 
@@ -158,7 +157,6 @@
158
157
  .at(-1, default: -1)
159
158
  )
160
159
 
161
- // Indorsements - iterate through CARDS array and filter by CARD tag
162
160
  #for (i, card) in data.at("$cards").enumerate() {
163
161
  if card.at("$kind", default: none) == "indorsement" {
164
162
  // The quillmark helper leaves an unset/whitespace-only markdown body as
@@ -187,29 +185,24 @@
187
185
  indorsement(
188
186
  from: card.at("from", default: ""),
189
187
  to: card.at("for", default: ""),
190
- signature_block: card.signature_block,
191
- signing_field: signature-field(
188
+ authority-line: card.authority_line,
189
+ signature-block: card.signature_block,
190
+ signing-field: signature-field(
192
191
  "Ind_" + str(i) + "_Signature",
193
192
  field: card.at("$path") + "signature_block",
194
193
  height: body_font_size * 2.5,
195
194
  ),
196
195
  ..if card.format != "" { (format: card.format) },
197
- date: resolved_date,
198
- // An omitted date becomes an empty AcroForm text box the endorser types
199
- // the signing date into, sized to the slot the package reserves for it.
200
- // Built only when there is no date to print: a widget over a printed date
201
- // would offer an edit that the rendered document does not carry back.
202
- ..if resolved_date == none {
203
- (date_field: form-field(
204
- "Ind_" + str(i) + "_Date",
205
- type: "text",
206
- width: 1in,
207
- height: body_font_size,
208
- field: card.at("$path") + "date",
209
- ))
196
+ // An omitted date becomes a fill-in slot for the signing date. Built only
197
+ // when there is no date to print: a widget over a printed date would offer
198
+ // an edit that the rendered document does not carry back.
199
+ date: if resolved_date != none {
200
+ resolved_date
201
+ } else {
202
+ date_slot("Ind_" + str(i) + "_Date", card.at("$path") + "date")
210
203
  },
211
204
  ..if card.action != "" { (action: card.action) },
212
- approval_authority: i == last_indorsement_index,
205
+ approval-authority: i == last_indorsement_index,
213
206
  body_content,
214
207
  )
215
208
  }
@@ -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
+ ~~~