@airmark/quiver 0.1.0

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