@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,344 @@
1
+ // body.typ: Paragraph body rendering for USAF memorandum sections
2
+ //
3
+ // This module implements the visual rendering of AFH 33-337 compliant
4
+ // paragraph bodies with proper numbering, nesting, and formatting.
5
+
6
+ #import "config.typ": *
7
+ #import "utils.typ": *
8
+ #import "primitives.typ": render-memo-table
9
+
10
+ // =============================================================================
11
+ // PARAGRAPH NUMBERING UTILITIES
12
+ // =============================================================================
13
+
14
+ /// Gets the numbering format for a specific paragraph level.
15
+ ///
16
+ /// - level (int): Paragraph nesting level (0-based)
17
+ /// -> str | function
18
+ #let get-paragraph-numbering-format(level) = {
19
+ paragraph-config.numbering-formats.at(level, default: "i.")
20
+ }
21
+
22
+ /// Calculates indentation for USAF-style paragraphs from explicit counter values.
23
+ ///
24
+ /// - level (int): Paragraph nesting level (0-based)
25
+ /// - level-counts (dictionary): Maps level index strings to their current counter values
26
+ /// -> length
27
+ #let calculate-indent-from-counts(level, level-counts) = {
28
+ if level == 0 {
29
+ return 0pt
30
+ }
31
+ let total-indent = 0pt
32
+ for ancestor-level in range(level) {
33
+ let ancestor-value = level-counts.at(str(ancestor-level), default: 1)
34
+ let ancestor-format = get-paragraph-numbering-format(ancestor-level)
35
+ let ancestor-number = numbering(ancestor-format, ancestor-value)
36
+ total-indent += measure([#ancestor-number#" "]).width
37
+ }
38
+ total-indent
39
+ }
40
+
41
+ /// Calculates fixed indentation for DAF paragraph levels.
42
+ ///
43
+ /// First nested level starts at `nested-first-level-indent` (1in); deeper levels
44
+ /// add `nested-step` (0.5in) per additional depth.
45
+ ///
46
+ /// - level (int): Paragraph nesting level (0-based)
47
+ /// -> length
48
+ #let calculate-daf-indent(level) = {
49
+ if level <= 0 {
50
+ return 0pt
51
+ }
52
+ daf-paragraph.nested-first-level-indent + (level - 1) * daf-paragraph.nested-step
53
+ }
54
+
55
+ /// Resets counter entries from `start` upward to 1 in the level-counts dictionary.
56
+ #let reset-levels-from(level-counts, start, max-levels) = {
57
+ for child in range(start, max-levels) {
58
+ level-counts.insert(str(child), 1)
59
+ }
60
+ level-counts
61
+ }
62
+
63
+ /// Formats a paragraph (or continuation) with a given indent strategy.
64
+ ///
65
+ /// - body (content): Paragraph content
66
+ /// - level (int): Nesting level (0-based)
67
+ /// - level-counts (dictionary): Current counter values per level
68
+ /// - indent-fn (function): `(level, level-counts) -> length`
69
+ /// - continuation (bool): If true, adds number-label width to alignment
70
+ /// -> content
71
+ #let format-par(body, level, level-counts, indent-fn, continuation: false) = {
72
+ let indent-width = indent-fn(level, level-counts)
73
+ if continuation {
74
+ let current-value = level-counts.at(str(level), default: 1)
75
+ let number-text = numbering(get-paragraph-numbering-format(level), current-value)
76
+ [#h(indent-width + measure([#number-text#" "]).width)#body]
77
+ } else {
78
+ let current-value = level-counts.at(str(level), default: 1)
79
+ let number-text = numbering(get-paragraph-numbering-format(level), current-value)
80
+ [#h(indent-width)#number-text#" "#body]
81
+ }
82
+ }
83
+
84
+ // =============================================================================
85
+ // PARAGRAPH BODY RENDERING
86
+ // =============================================================================
87
+ // AFH 33-337 "The Text of the Official Memorandum" §1-12 specifies:
88
+ // - Single-space text, double-space between paragraphs
89
+ // - Number and letter each paragraph and subparagraph
90
+ // - "A single paragraph is not numbered" (§2)
91
+ // - First paragraph flush left, never indented
92
+ // - Indent sub-paragraphs to align with first character of parent paragraph text
93
+ #let render-body(content, auto-numbering: true, memo-style: "usaf") = {
94
+ let PAR_BUFFER = state("PAR_BUFFER")
95
+ PAR_BUFFER.update(())
96
+ let NEST_DOWN = counter("NEST_DOWN")
97
+ NEST_DOWN.update(0)
98
+ let NEST_UP = counter("NEST_UP")
99
+ NEST_UP.update(0)
100
+ let IS_HEADING = state("IS_HEADING")
101
+ IS_HEADING.update(false)
102
+ // Tracks whether the next paragraph is the first block in a list item.
103
+ // When true, the next `show par` captures a numbered item; subsequent
104
+ // paragraphs within the same item are continuations (no new number).
105
+ let ITEM_FIRST_PAR = state("ITEM_FIRST_PAR")
106
+ ITEM_FIRST_PAR.update(false)
107
+
108
+ // The first pass parses paragraphs, list items, etc. into standardized arrays
109
+ let first_pass = {
110
+ // Collect pars with nesting level
111
+ show par: p => context {
112
+ let nest_level = NEST_DOWN.get().at(0) - NEST_UP.get().at(0)
113
+ let is_heading = IS_HEADING.get()
114
+ let is_first_par = ITEM_FIRST_PAR.get()
115
+
116
+ // Determine if this is a continuation block within a multi-block list item.
117
+ // A continuation is a non-first paragraph inside a list item (nest_level > 0).
118
+ let is_continuation = nest_level > 0 and not is_first_par
119
+
120
+ PAR_BUFFER.update(pars => {
121
+ pars.push((
122
+ content: text([#p.body]),
123
+ nest_level: nest_level,
124
+ kind: if is_heading { "heading" } else if is_continuation { "continuation" } else { "par" },
125
+ ))
126
+ pars
127
+ })
128
+
129
+ // After the first paragraph of a list item, mark subsequent ones as continuations
130
+ if nest_level > 0 and is_first_par {
131
+ ITEM_FIRST_PAR.update(false)
132
+ }
133
+
134
+ p
135
+ }
136
+ // Collect tables — captured as-is without paragraph numbering
137
+ show table: t => context {
138
+ PAR_BUFFER.update(pars => {
139
+ pars.push((
140
+ content: t,
141
+ nest_level: -1,
142
+ kind: "table",
143
+ ))
144
+ pars
145
+ })
146
+ t
147
+ }
148
+ {
149
+ show heading: h => {
150
+ IS_HEADING.update(true)
151
+ [#parbreak()#h.body#parbreak()]
152
+ IS_HEADING.update(false)
153
+ }
154
+
155
+ // Convert list/enum items to pars
156
+ // Note: No context wrapper here - state updates don't need it and cause
157
+ // layout convergence issues with many list items
158
+ show enum.item: it => {
159
+ NEST_DOWN.step()
160
+ ITEM_FIRST_PAR.update(true)
161
+ [#parbreak()#it.body#parbreak()]
162
+ NEST_UP.step()
163
+ }
164
+ show list.item: it => {
165
+ NEST_DOWN.step()
166
+ ITEM_FIRST_PAR.update(true)
167
+ [#parbreak()#it.body#parbreak()]
168
+ NEST_UP.step()
169
+ }
170
+
171
+ {
172
+ // Typst bug bandaid:
173
+ // `show par` will not collect wrappers unless there is content outside
174
+ // Add zero width space to always have content outside of wrapper
175
+ show strong: it => {
176
+ [#it#sym.zws]
177
+ }
178
+ show emph: it => {
179
+ [#it#sym.zws]
180
+ }
181
+ show underline: it => {
182
+ [#it#sym.zws]
183
+ }
184
+ show raw: it => {
185
+ [#it#sym.zws]
186
+ }
187
+ [#content#parbreak()]
188
+ }
189
+ }
190
+ }
191
+ // Use place() to prevent hidden content from affecting layout flow
192
+ place(hide(first_pass))
193
+
194
+ // Second pass: consume par buffer
195
+ //
196
+ // PAR_BUFFER item dictionary layout:
197
+ // item.content — the paragraph body or table element
198
+ // item.nest_level — nesting depth (−1 for tables)
199
+ // item.kind — "par", "heading", "table", or "continuation"
200
+ context {
201
+ let heading_buffer = none
202
+ // Only top-level paragraphs count for AFH 33-337 §2 numbering purposes
203
+ let par_count = PAR_BUFFER.get().filter(item => item.kind == "par").len()
204
+ let items = PAR_BUFFER.get()
205
+ let total_count = items.len()
206
+
207
+ // Track paragraph numbers per level manually to avoid nested-context
208
+ // counter propagation issues. Dictionary maps level index (as string)
209
+ // to the current counter value at that level.
210
+ let max-levels = paragraph-config.numbering-formats.len()
211
+ let level-counts = (:)
212
+ for lvl in range(max-levels) {
213
+ level-counts.insert(str(lvl), 1)
214
+ }
215
+
216
+ let i = 0
217
+ for item in items {
218
+ i += 1
219
+ let kind = item.kind
220
+ let item_content = item.content
221
+
222
+ // Buffer headings for prepend to the next rendered element
223
+ if kind == "heading" {
224
+ heading_buffer = item_content
225
+ continue
226
+ }
227
+
228
+ // Prepend buffered heading to the next non-heading element
229
+ if heading_buffer != none {
230
+ if kind == "table" {
231
+ // Tables cannot have inline text prepended; emit heading as
232
+ // a standalone bold line above the table
233
+ blank-line()
234
+ strong[#heading_buffer.]
235
+ heading_buffer = none
236
+ } else {
237
+ item_content = [#strong[#heading_buffer.] #item_content]
238
+ heading_buffer = none
239
+ }
240
+ }
241
+
242
+ // Format based on element kind
243
+ let nest_level = item.nest_level
244
+ let indent-fn = if memo-style == "daf" {
245
+ (level, _counts) => calculate-daf-indent(level)
246
+ } else {
247
+ (level, counts) => calculate-indent-from-counts(level, counts)
248
+ }
249
+ let final_par = {
250
+ if kind == "table" {
251
+ render-memo-table(item_content)
252
+ } else if kind == "continuation" {
253
+ // Continuation block within a multi-block list item:
254
+ // indent to align with preceding numbered paragraph's text, no new number.
255
+ // level-counts still holds the value of the preceding numbered paragraph.
256
+ if memo-style == "daf" {
257
+ if nest_level > 0 {
258
+ format-par(item_content, nest_level, level-counts, indent-fn, continuation: true)
259
+ } else {
260
+ item_content
261
+ }
262
+ } else if auto-numbering {
263
+ format-par(item_content, nest_level, level-counts, indent-fn, continuation: true)
264
+ } else if nest_level > 0 {
265
+ format-par(item_content, nest_level - 1, level-counts, indent-fn, continuation: true)
266
+ } else {
267
+ item_content
268
+ }
269
+ } else if memo-style == "daf" {
270
+ if nest_level > 0 {
271
+ let par = format-par(item_content, nest_level, level-counts, indent-fn)
272
+ level-counts.insert(str(nest_level), level-counts.at(str(nest_level), default: 1) + 1)
273
+ level-counts = reset-levels-from(level-counts, nest_level + 1, max-levels)
274
+ par
275
+ } else {
276
+ // DAF top-level paragraphs are unnumbered and first-line indented.
277
+ // Reset nested counters so each new top-level paragraph restarts children.
278
+ level-counts = reset-levels-from(level-counts, 0, max-levels)
279
+ [#h(daf-paragraph.top-first-line-indent)#item_content]
280
+ }
281
+ } else if auto-numbering {
282
+ if par_count > 1 {
283
+ // Apply paragraph numbering per AFH 33-337 §2
284
+ let par = format-par(item_content, nest_level, level-counts, indent-fn)
285
+ level-counts.insert(str(nest_level), level-counts.at(str(nest_level)) + 1)
286
+ level-counts = reset-levels-from(level-counts, nest_level + 1, max-levels)
287
+ par
288
+ } else {
289
+ // AFH 33-337 §2: "A single paragraph is not numbered"
290
+ item_content
291
+ }
292
+ } else {
293
+ // Unnumbered mode: only explicitly nested items (enum/list) get numbered
294
+ if nest_level > 0 {
295
+ let effective_level = nest_level - 1
296
+ let par = format-par(item_content, effective_level, level-counts, indent-fn)
297
+ level-counts.insert(str(effective_level), level-counts.at(str(effective_level)) + 1)
298
+ level-counts = reset-levels-from(level-counts, effective_level + 1, max-levels)
299
+ par
300
+ } else {
301
+ // Base-level paragraphs are flush left with no numbering.
302
+ // Reset all child level counters so subsequent list items restart at 1.
303
+ level-counts = reset-levels-from(level-counts, 0, max-levels)
304
+ item_content
305
+ }
306
+ }
307
+ }
308
+
309
+ // If this is the final item, apply AFH 33-337 §11 rule:
310
+ // "Avoid dividing a paragraph of less than four lines between two pages"
311
+ blank-line()
312
+ if i == total_count {
313
+ let available_width = page.width - spacing.margin * 2
314
+
315
+ // Use the shared measured line stride used by blank-line spacing.
316
+ let line_height = {
317
+ let cached = LINE_STRIDE.get()
318
+ if cached != none {
319
+ cached
320
+ } else {
321
+ let one-line = measure(par(spacing: 0pt)[x]).height
322
+ measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
323
+ }
324
+ }
325
+ // Calculate last item's height
326
+ let par_height = measure(final_par, width: available_width).height
327
+
328
+ let estimated_lines = calc.ceil(par_height / line_height)
329
+
330
+ if estimated_lines < 4 {
331
+ // Short content (< 4 lines): make sticky to keep with signature
332
+ block(sticky: true)[#final_par]
333
+ } else {
334
+ // Longer content (≥ 4 lines): use default breaking behavior
335
+ block(breakable: true)[#final_par]
336
+ }
337
+ } else {
338
+ final_par
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+
@@ -0,0 +1,73 @@
1
+ // config.typ: Configuration constants and defaults for USAF memorandum template
2
+ //
3
+ // This module defines core configuration values that implement AFH 33-337 Chapter 14
4
+ // formatting requirements for official USAF memorandums.
5
+
6
+ // =============================================================================
7
+ // SPACING CONSTANTS
8
+ // =============================================================================
9
+ // AFH 33-337 specifies precise spacing requirements throughout Chapter 14
10
+
11
+ #let spacing = (
12
+ // Legacy exported field retained for compatibility with downstream imports.
13
+ // Internal blank-line spacing now uses measured line stride from utils.typ.
14
+ vertical: 19.05pt,
15
+ line: .5em, // Internal line spacing for readability (`par.leading`; gap between line boxes)
16
+ line-height: .7em, // Base height of text lines for pagination estimates
17
+ tab: 0.5in, // Tab stop for multi-column recipient alignment
18
+ margin: 1in, // AFH 33-337 §4: "Use 1-inch margins on the left, right and bottom"
19
+ )
20
+
21
+ // =============================================================================
22
+ // TYPOGRAPHY DEFAULTS
23
+ // =============================================================================
24
+ // AFH 33-337 §5: "Use 12 point Times New Roman font for text"
25
+
26
+ #let DEFAULT_LETTERHEAD_FONTS = ("NimbusRomNo9L", "times new roman")
27
+ #let DEFAULT_BODY_FONTS = ("NimbusRomNo9L", "times new roman") // AFH 33-337 §5: Times New Roman required
28
+ #let LETTERHEAD_COLOR = rgb("#204093") // Faded USAF blue for letterhead
29
+
30
+ // =============================================================================
31
+ // PARAGRAPH CONFIGURATION
32
+ // =============================================================================
33
+ // AFH 33-337 "The Text of the Official Memorandum" §2:
34
+ // "Number and letter each paragraph and subparagraph"
35
+ // Hierarchical numbering: 1., a., (1), (a), etc.
36
+
37
+ #let paragraph-config = (
38
+ counter-prefix: "par-counter-",
39
+ // AFH 33-337 §2: Hierarchical paragraph numbering format
40
+ // Level 0: 1., 2., 3. | Level 1: a., b., c. | Level 2: (1), (2), (3) | Level 3: (a), (b), (c)
41
+ numbering-formats: ("1.", "a.", "(1)", "(a)", n => underline(str(n)), n => underline(str(n))),
42
+ )
43
+
44
+ // DAF (Headquarters) memo body: first-line indent for unnumbered paragraphs; nested
45
+ // items start at 1in, then +0.5in per additional nesting depth.
46
+ #let daf-paragraph = (
47
+ top-first-line-indent: 0.5in,
48
+ nested-first-level-indent: 1in,
49
+ nested-step: 0.5in,
50
+ )
51
+
52
+ // =============================================================================
53
+ // COUNTERS
54
+ // =============================================================================
55
+
56
+ #let counters = (
57
+ indorsement: counter("indorsement"),
58
+ )
59
+
60
+ // =============================================================================
61
+ // CLASSIFICATION COLORS
62
+ // =============================================================================
63
+ // AFH 33-337 §3: "Follow AFI 31-401, Information Security Program Management,
64
+ // applicable executive orders and DoD guidance for the necessary markings on
65
+ // classified correspondence."
66
+ // Color values follow DoD standard classification marking colors
67
+ // Source: https://security.stackexchange.com/questions/161829
68
+
69
+ #let CLASSIFICATION_COLORS = (
70
+ "UNCLASSIFIED": rgb(0, 122, 51), // Forest green (#007A33)
71
+ "SECRET": rgb(200, 16, 46), // Crimson red (#C8102E)
72
+ "TOP SECRET": rgb(255, 103, 31), // Burnt orange (#FF671F)
73
+ )
@@ -0,0 +1,137 @@
1
+ // frontmatter.typ: Frontmatter show rule for USAF memorandum
2
+ //
3
+ // This module implements the frontmatter (heading section) of a USAF memorandum
4
+ // per AFH 33-337 Chapter 14 "The Heading Section". It handles:
5
+ // - Page setup with proper margins
6
+ // - Letterhead rendering
7
+ // - Date, MEMORANDUM FOR, FROM, SUBJECT, and References placement
8
+ // - Classification markings in headers/footers
9
+
10
+ #import "primitives.typ": *
11
+
12
+ #let frontmatter(
13
+ subject: none,
14
+ memo_for: none,
15
+ memo_from: none,
16
+ date: none,
17
+ references: none,
18
+ letterhead_title: "DEPARTMENT OF THE AIR FORCE",
19
+ letterhead_caption: "[YOUR SQUADRON/UNIT NAME]",
20
+ letterhead_seal: none,
21
+ letterhead_seal_subtitle: none, // optional line under seal (9pt bold caps); ignored if no seal
22
+ letterhead_font: DEFAULT_LETTERHEAD_FONTS,
23
+ body_font: DEFAULT_BODY_FONTS,
24
+ font_size: 12pt,
25
+ memo_for_cols: 3,
26
+ classification_level: none,
27
+ footer_tag_line: none,
28
+ auto_numbering: true,
29
+ memo_style: "usaf",
30
+ it,
31
+ ) = {
32
+ assert(subject != none, message: "subject is required")
33
+ assert(memo_for != none, message: "memo_for is required")
34
+ assert(memo_from != none, message: "memo_from is required")
35
+ assert(
36
+ memo_style in ("usaf", "daf"),
37
+ message: "memo_style must be \"usaf\" or \"daf\"",
38
+ )
39
+
40
+ let actual_date = if date == none { datetime.today() } else { date }
41
+ let classification_color = get-classification-level-color(classification_level)
42
+
43
+ // Document-wide typography settings (inlined from configure())
44
+ set par(leading: spacing.line, spacing: spacing.line, justify: false)
45
+ set block(above: spacing.line, below: 0em, spacing: 0em)
46
+ set text(font: body_font, size: font_size, fallback: true)
47
+
48
+ set page(
49
+ paper: "us-letter",
50
+ // AFH 33-337 §4: "Use 1-inch margins on the left, right and bottom"
51
+ margin: (
52
+ left: spacing.margin,
53
+ right: spacing.margin,
54
+ top: spacing.margin,
55
+ bottom: spacing.margin,
56
+ ),
57
+ header: {
58
+ // AFH 33-337 "Page numbering" §12: "The first page of a memorandum is never numbered.
59
+ // Number the succeeding pages starting with page 2. Place page numbers 0.5-inch from
60
+ // the top of the page, flush with the right margin."
61
+ context if counter(page).get().first() > 1 {
62
+ place(
63
+ dy: +.5in,
64
+ block(
65
+ width: 100%,
66
+ align(right, text(12pt)[#counter(page).display()]),
67
+ ),
68
+ )
69
+ }
70
+
71
+ if classification_level != none {
72
+ place(
73
+ top + center,
74
+ dy: 0.375in,
75
+ text(12pt, font: DEFAULT_BODY_FONTS, fill: classification_color)[#strong(classification_level)],
76
+ )
77
+ }
78
+ },
79
+ footer: {
80
+ place(
81
+ bottom + center,
82
+ dy: -.375in,
83
+ text(12pt, font: DEFAULT_BODY_FONTS, fill: classification_color)[#strong(classification_level)],
84
+ )
85
+
86
+ if not falsey(footer_tag_line) {
87
+ place(
88
+ bottom + center,
89
+ dy: -0.625in,
90
+ align(center)[
91
+ #text(fill: LETTERHEAD_COLOR, font: "cinzel", size: 15pt)[#footer_tag_line]
92
+ ],
93
+ )
94
+ }
95
+ },
96
+ )
97
+
98
+ render-letterhead(
99
+ letterhead_title,
100
+ letterhead_caption,
101
+ letterhead_font,
102
+ letterhead-seal: letterhead_seal,
103
+ letterhead-seal-subtitle: letterhead_seal_subtitle,
104
+ )
105
+
106
+ // AFH 33-337 "Date": "Place the date 1 inch from the right edge, 1.75 inches from the top"
107
+ // Since we have a 1-inch top margin, we need (1.75in - margin) vertical space
108
+ v(1.75in - spacing.margin)
109
+
110
+ // Measure and cache body line stride once for downstream spacing logic.
111
+ context {
112
+ let one-line = measure(par(spacing: 0pt)[x]).height
113
+ let line-stride = measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
114
+ let em-size = measure(box(width: 1em)[]).width
115
+ let legacy-scaled = spacing.vertical * (em-size / 12pt)
116
+ LINE_STRIDE.update(line-stride)
117
+ BLANK_LINE_STEP.update(calc.max(line-stride, legacy-scaled))
118
+ }
119
+
120
+ metadata((
121
+ subject: subject,
122
+ original_date: actual_date,
123
+ original_from: first-or-value(memo_from),
124
+ body_font: body_font,
125
+ font_size: font_size,
126
+ auto_numbering: auto_numbering,
127
+ memo_style: memo_style,
128
+ ))
129
+
130
+ render-date-section(actual_date, memo-style: memo_style)
131
+ render-for-section(memo_for, memo_for_cols)
132
+ render-from-section(memo_from)
133
+ render-subject-section(subject)
134
+ render-references-section(references)
135
+
136
+ it
137
+ }
@@ -0,0 +1,111 @@
1
+ // indorsement.typ: Indorsement rendering for USAF memorandum
2
+ //
3
+ // This module implements indorsements (endorsements) per AFH 33-337 Chapter 14.
4
+ // Indorsements are used to forward memorandums with additional commentary.
5
+ // They follow the format: "1st Ind", "2d Ind", "3d Ind", etc.
6
+ // Each indorsement includes its own body text and signature block.
7
+ //
8
+ // Note: When using #show: indorsement.with(...), the indorsement wraps the
9
+ // entire remainder of the document. This works for a single indorsement at
10
+ // the end of a file. For multiple indorsements, use the function call syntax:
11
+ // #indorsement(...)[Body text...]
12
+
13
+ #import "primitives.typ": *
14
+ #import "body.typ": *
15
+
16
+ #let indorsement(
17
+ from: none,
18
+ to: none,
19
+ signature_block: none,
20
+ signature_blank_lines: 4,
21
+ attachments: none,
22
+ cc: none,
23
+ date: none,
24
+ // Format of indorsement: "standard" (same page), "informal" (no header), or "separate_page" (starts on new page)
25
+ format: "standard",
26
+ // Approval action: none (default, no action line displayed), "undecided", "approve", or "disapprove".
27
+ // When set to "undecided", the action line is displayed with neither option circled.
28
+ // When set to "approve" or "disapprove", the action line is displayed with the selected option circled.
29
+ action: none,
30
+ content,
31
+ ) = {
32
+ // Validate format parameter
33
+ assert(
34
+ format in ("standard", "informal", "separate_page"),
35
+ message: "format must be \"standard\", \"informal\", or \"separate_page\"",
36
+ )
37
+
38
+ if format != "informal" {
39
+ assert(from != none, message: "from is required")
40
+ assert(to != none, message: "to is required")
41
+ }
42
+
43
+
44
+ let actual_date = if date == none { datetime.today() } else { date }
45
+ let ind_from = first-or-value(from)
46
+ let ind_for = to
47
+
48
+ if format != "informal" {
49
+ // Step the counter BEFORE the context block to avoid read-then-update loop
50
+ counters.indorsement.step()
51
+
52
+ context {
53
+ let config = query(metadata).last().value
54
+ let memo-style = config.at("memo_style", default: "usaf")
55
+ let original_subject = config.subject
56
+ let original_date = config.original_date
57
+ let original_from = config.original_from
58
+
59
+ // Read the counter value (already stepped above)
60
+ let indorsement_number = counters.indorsement.get().at(0, default: 1)
61
+ let indorsement_label = format-indorsement-number(indorsement_number)
62
+
63
+ if format == "separate_page" {
64
+ pagebreak()
65
+ [#indorsement_label to #original_from, #display-date(original_date, memo-style: memo-style), #original_subject]
66
+
67
+ blank-line()
68
+ grid(
69
+ columns: (auto, 1fr),
70
+ ind_from, align(right)[#display-date(actual_date, memo-style: memo-style)],
71
+ )
72
+
73
+ blank-line()
74
+ grid(
75
+ columns: (auto, auto, 1fr),
76
+ "MEMORANDUM FOR", " ", ind_for,
77
+ )
78
+ } else {
79
+ blank-line()
80
+ grid(
81
+ columns: (auto, 1fr),
82
+ [#indorsement_label, #ind_from], align(right)[#display-date(actual_date, memo-style: memo-style)],
83
+ )
84
+
85
+ blank-line()
86
+ grid(
87
+ columns: (auto, auto, 1fr),
88
+ "MEMORANDUM FOR", " ", ind_for,
89
+ )
90
+ }
91
+ }
92
+ blank-line()
93
+ }
94
+
95
+ // Show action line only when an action decision is set (not `none`)
96
+ if action != none {
97
+ render-action-line(action)
98
+ }
99
+
100
+ context {
101
+ let memo-style = {
102
+ let items = query(metadata)
103
+ if items.len() > 0 { items.last().value.at("memo_style", default: "usaf") } else { "usaf" }
104
+ }
105
+ render-body(content, memo-style: memo-style)
106
+ }
107
+
108
+ render-signature-block(signature_block, signature-blank-lines: signature_blank_lines)
109
+
110
+ render-backmatter-sections(attachments: attachments, cc: cc)
111
+ }