@tonguetoquill/collection 0.2.5-beta.1 → 0.2.5

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 (107) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +39 -39
  3. package/index.d.ts +2 -2
  4. package/index.js +8 -8
  5. package/package.json +41 -41
  6. package/quills/af4141/0.1.0/Quill.yaml +92 -88
  7. package/quills/af4141/0.1.0/assets/GNU General Public License.txt +340 -0
  8. package/quills/af4141/0.1.0/assets/NimbusRomNo9L-Med.otf +0 -0
  9. package/quills/af4141/0.1.0/assets/NimbusRomNo9L-MedIta.otf +0 -0
  10. package/quills/af4141/0.1.0/assets/NimbusRomNo9L-Reg.otf +0 -0
  11. package/quills/af4141/0.1.0/assets/NimbusRomNo9L-RegIta.otf +0 -0
  12. package/quills/af4141/0.1.0/design/TASK.md +19 -19
  13. package/quills/af4141/0.1.0/example.md +35 -35
  14. package/quills/af4141/0.1.0/packages/typst-af4141/FIELDS.json +3169 -3169
  15. package/quills/af4141/0.1.0/packages/typst-af4141/form.typ +538 -538
  16. package/quills/af4141/0.1.0/packages/typst-af4141/lib.typ +227 -227
  17. package/quills/af4141/0.1.0/packages/typst-af4141/typst.toml +7 -7
  18. package/quills/af4141/0.1.0/plate.typ +50 -48
  19. package/quills/classic_resume/0.1.0/Quill.yaml +118 -118
  20. package/quills/classic_resume/0.1.0/example.md +232 -232
  21. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/LICENSE +21 -21
  22. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/README.md +38 -38
  23. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/components.typ +184 -184
  24. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/layout.typ +42 -42
  25. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/lib.typ +5 -5
  26. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/typst.toml +26 -26
  27. package/quills/classic_resume/0.1.0/plate.typ +44 -44
  28. package/quills/cmu_letter/0.1.0/.quillignore +30 -30
  29. package/quills/cmu_letter/0.1.0/Quill.yaml +64 -64
  30. package/quills/cmu_letter/0.1.0/assets/cmu-wordmark.svg +174 -174
  31. package/quills/cmu_letter/0.1.0/example.md +30 -30
  32. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/LICENSE +21 -21
  33. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/README.txt +100 -100
  34. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/backmatter.typ +13 -13
  35. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/config.typ +39 -39
  36. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/frontmatter.typ +72 -72
  37. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/lib.typ +47 -47
  38. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/mainmatter.typ +42 -42
  39. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/primitives.typ +70 -70
  40. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/utils.typ +85 -85
  41. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/typst.toml +17 -17
  42. package/quills/cmu_letter/0.1.0/plate.typ +19 -19
  43. package/quills/taro/0.1.0/Quill.yaml +29 -29
  44. package/quills/taro/0.1.0/example.md +26 -26
  45. package/quills/taro/0.1.0/plate.typ +31 -31
  46. package/quills/usaf_memo/0.1.0/.quillignore +30 -30
  47. package/quills/usaf_memo/0.1.0/Quill.yaml +209 -209
  48. package/quills/usaf_memo/0.1.0/example.md +54 -54
  49. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -21
  50. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -93
  51. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -79
  52. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +339 -339
  53. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -28
  54. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/body.typ +332 -332
  55. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/config.typ +63 -63
  56. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -114
  57. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -118
  58. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -55
  59. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -32
  60. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +272 -272
  61. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/utils.typ +377 -377
  62. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/typst.toml +16 -16
  63. package/quills/usaf_memo/0.1.0/plate.typ +74 -74
  64. package/quills/usaf_memo/0.2.0/.quillignore +30 -30
  65. package/quills/usaf_memo/0.2.0/Quill.yaml +219 -219
  66. package/quills/usaf_memo/0.2.0/example.md +55 -55
  67. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/.gitignore +6 -6
  68. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -21
  69. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -93
  70. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -79
  71. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +339 -339
  72. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -28
  73. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +333 -333
  74. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/config.typ +64 -64
  75. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -114
  76. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -118
  77. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -55
  78. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -32
  79. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +293 -293
  80. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +374 -374
  81. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +27 -27
  82. package/quills/usaf_memo/0.2.0/plate.typ +75 -75
  83. package/templates/af4141.md +88 -88
  84. package/templates/cmu_letter_template.md +37 -37
  85. package/templates/loc.md +78 -78
  86. package/templates/pass_request.md +43 -43
  87. package/templates/rebuttal.md +55 -55
  88. package/templates/taro.md +26 -26
  89. package/templates/templates.json +49 -55
  90. package/templates/usaf_template.md +23 -23
  91. package/templates/ussf_template.md +29 -29
  92. package/quills/daf4392/0.1.0/Quill.yaml +0 -110
  93. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-700.ttf +0 -0
  94. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-700italic.ttf +0 -0
  95. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-italic.ttf +0 -0
  96. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-regular.ttf +0 -0
  97. package/quills/daf4392/0.1.0/assets/page1.png +0 -0
  98. package/quills/daf4392/0.1.0/example.md +0 -33
  99. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/FIELDS.json +0 -9
  100. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/form.typ +0 -14
  101. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/lib.typ +0 -227
  102. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/out/debug.typ +0 -4
  103. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/out/example.typ +0 -4
  104. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/page1.png +0 -0
  105. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/typst.toml +0 -7
  106. package/quills/daf4392/0.1.0/plate.typ +0 -60
  107. package/templates/daf4392.md +0 -33
@@ -1,293 +1,293 @@
1
- // primitives.typ: Reusable rendering primitives for USAF memorandum sections
2
- //
3
- // This module implements the visual rendering functions that produce AFH 33-337
4
- // compliant formatting for all sections of a USAF memorandum. Each function
5
- // corresponds to specific placement and formatting requirements from Chapter 14.
6
-
7
- #import "config.typ": *
8
- #import "utils.typ": *
9
-
10
- // =============================================================================
11
- // LETTERHEAD RENDERING
12
- // =============================================================================
13
- // AFH 33-337 §1: "Use printed letterhead, computer-generated letterhead, or plain bond paper"
14
- // Letterhead placement is not explicitly specified in AFH 33-337, but follows
15
- // standard USAF memo formatting conventions
16
-
17
- #let render-letterhead(title, caption, letterhead-seal, font) = {
18
- font = ensure-array(font)
19
- caption = ensure-string(caption)
20
-
21
- place(
22
- dy: 0.625in - spacing.margin,
23
- box(
24
- width: 100%,
25
- fill: none,
26
- stroke: none,
27
- [
28
- #place(
29
- center + top,
30
- align(center)[
31
- #set text(12pt, font: font, fill: LETTERHEAD_COLOR)
32
- #title\
33
- #text(10.5pt)[#caption]
34
- ],
35
- )
36
- ],
37
- ),
38
- )
39
-
40
- if letterhead-seal != none {
41
- place(
42
- left + top,
43
- dx: -0.5in,
44
- dy: -.5in,
45
- block[
46
- #fit-box(width: 2in, height: 1in)[#letterhead-seal]
47
- ],
48
- )
49
- }
50
- }
51
-
52
- // =============================================================================
53
- // HEADER SECTIONS
54
- // =============================================================================
55
- // AFH 33-337 "The Heading Section" specifies exact placement and format for:
56
- // - Date: 1 inch from right edge, 1.75 inches from top
57
- // - MEMORANDUM FOR: Second line below date
58
- // - FROM: Second line below MEMORANDUM FOR
59
- // - SUBJECT: Second line below FROM
60
-
61
- // AFH 33-337 "Date": "Place the date 1 inch from the right edge, 1.75 inches from the top"
62
- #let render-date-section(date) = {
63
- align(right)[#display-date(date)]
64
- }
65
-
66
- // AFH 33-337 "MEMORANDUM FOR": "Place 'MEMORANDUM FOR' on the second line below the date"
67
- #let render-for-section(recipients, cols) = {
68
- blank-line()
69
- grid(
70
- columns: (auto, auto, 1fr),
71
- "MEMORANDUM FOR",
72
- " ",
73
- align(left)[
74
- #if type(recipients) == array {
75
- create-auto-grid(recipients, column-gutter: spacing.tab, cols: cols)
76
- } else {
77
- recipients
78
- }
79
- ],
80
- )
81
- }
82
-
83
- // AFH 33-337 "FROM:": "Place 'FROM:' in uppercase, flush with the left margin,
84
- // on the second line below the last line of the MEMORANDUM FOR element"
85
- #let render-from-section(from-info) = {
86
- blank-line()
87
- from-info = ensure-string(from-info)
88
-
89
- grid(
90
- columns: (auto, auto, 1fr),
91
- "FROM:", " ", align(left)[#from-info],
92
- )
93
- }
94
-
95
- // AFH 33-337 "SUBJECT:": "In all uppercase letters place 'SUBJECT:', flush with the
96
- // left margin, on the second line below the last line of the FROM element"
97
- #let render-subject-section(subject-text) = {
98
- blank-line()
99
- grid(
100
- columns: (auto, auto, 1fr),
101
- "SUBJECT:", " ", [#subject-text],
102
- )
103
- }
104
-
105
- #let render-references-section(references) = {
106
- if not falsey(references) {
107
- blank-line()
108
- grid(
109
- columns: (auto, auto, 1fr),
110
- "References:", " ", enum(..references, numbering: "(a)"),
111
- )
112
- }
113
- }
114
-
115
- // =============================================================================
116
- // SIGNATURE BLOCK
117
- // =============================================================================
118
- // AFH 33-337 "Signature Block": "Start the signature block on the fifth line below
119
- // the last line of text and 4.5 inches from the left edge of the page"
120
- // AFH 33-337 "Do not place the signature element on a continuation page by itself"
121
-
122
- #let render-signature-block(signature-lines, signature-blank-lines: 4) = {
123
- signature-lines = ensure-array(signature-lines)
124
- // AFH 33-337: "The signature block is never on a page by itself"
125
- // Note: Perfect enforcement isn't feasible without over-engineering
126
- // We use weak: false spacing and breakable: false to discourage orphaning
127
- // AFH 33-337: "fifth line below" = 4 blank lines between text and signature block
128
- blank-lines(signature-blank-lines, weak: false)
129
- block(breakable: false)[
130
- #align(left)[
131
- // AFH 33-337: "4.5 inches from the left edge of the page"
132
- // We use (4.5in - margin) because Typst's pad() is relative to the text area, not page edge
133
- #pad(left: 4.5in - spacing.margin)[
134
- #text(hyphenate: false)[
135
- #for line in signature-lines {
136
- par(hanging-indent: 4 * 0.5em, line)
137
- }
138
- ]
139
- ]
140
- ]
141
- ]
142
- }
143
-
144
- // =============================================================================
145
- // ACTION LINE RENDERING
146
- // =============================================================================
147
- // Renders the Approve / Disapprove action line for indorsement memos.
148
- // action: "none" = no action line displayed (hidden), "undecided" = both options
149
- // rendered plain (no circle), "approve" = Approve circled,
150
- // "disapprove" = Disapprove circled. The action line is rendered when
151
- // action is "undecided", "approve", or "disapprove".
152
-
153
- #let render-action-line(action) = {
154
- assert(
155
- action in ("none", "undecided", "approve", "disapprove"),
156
- message: "action must be \"none\", \"undecided\", \"approve\", or \"disapprove\"",
157
- )
158
- blank-line()
159
- // Circle the selected option using a box with rounded corners
160
- // Use baseline parameter to maintain vertical text alignment
161
- let approve-text = if action == "approve" {
162
- box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[Approve]
163
- } else if action == "disapprove" {
164
- strike[Approve]
165
- } else {
166
- [Approve]
167
- }
168
- let disapprove-text = if action == "disapprove" {
169
- box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[Disapprove]
170
- } else if action == "approve" {
171
- strike[Disapprove]
172
- } else {
173
- [Disapprove]
174
- }
175
- // Keep the action line with the following content (body or signature block)
176
- // using the same sticky-block pattern that body.typ applies to the last
177
- // paragraph, per AFH 33-337 §11 orphan-prevention rules.
178
- block(sticky: true)[#approve-text / #disapprove-text]
179
- }
180
-
181
- // =============================================================================
182
- // TABLE RENDERING
183
- // =============================================================================
184
- // AFH 33-337 does not specify table formatting, so we follow the general
185
- // aesthetic principles of the standard: plain black borders, no decorative
186
- // fills, and the body font inherited throughout.
187
-
188
- /// Renders a table with USAF memorandum–consistent formatting.
189
- ///
190
- /// Applies simple 0.5pt black cell borders and standard padding to any
191
- /// Typst `table` element, keeping the visual style clean and formal.
192
- /// Font and size are inherited from the surrounding body text.
193
- ///
194
- /// - it (content): The table element to style and render
195
- /// -> content
196
- #let render-memo-table(it) = {
197
- // AFH 33-337 does not specify table formatting, so we follow the general
198
- // aesthetic principles of the standard: bold headers for clarity.
199
- show table.cell.where(y: 0): set text(weight: "bold")
200
- set table(
201
- stroke: 0.5pt + black,
202
- inset: (x: 0.5em, y: 0.4em),
203
- )
204
- it
205
- }
206
-
207
- // =============================================================================
208
- // BACKMATTER SECTIONS
209
- // =============================================================================
210
- // AFH 33-337 "Attachment or Attachments": "Place 'Attachment:' (for a single attachment)
211
- // or '# Attachments:' (for two or more attachments) at the left margin, on the third
212
- // line below the signature element"
213
- // AFH 33-337 "Courtesy Copy Element": "place 'cc:' flush with the left margin, on the
214
- // second line below the attachment element"
215
-
216
- #let render-backmatter-section(
217
- content,
218
- section-label,
219
- numbering-style: none,
220
- continuation-label: none,
221
- ) = {
222
- let formatted-content = {
223
- // Use text() wrapper to prevent section label from being treated as a paragraph
224
- text()[#section-label]
225
- linebreak()
226
- if numbering-style != none {
227
- let items = ensure-array(content)
228
- enum(..items, numbering: numbering-style)
229
- } else {
230
- ensure-string(content)
231
- }
232
- }
233
-
234
- context {
235
- let available-space = page.height - here().position().y - 1in
236
- if measure(formatted-content).height > available-space {
237
- let continuation-text = if continuation-label != none {
238
- text()[#continuation-label]
239
- } else {
240
- text()[#section-label + " (listed on next page):"]
241
- }
242
- continuation-text
243
- pagebreak()
244
- }
245
- formatted-content
246
- }
247
- }
248
-
249
- #let calculate-backmatter-spacing(is-first-section) = {
250
- context {
251
- let line_count = if is-first-section { 2 } else { 1 }
252
- blank-lines(line_count)
253
- }
254
- }
255
-
256
- #let render-backmatter-sections(
257
- attachments: none,
258
- cc: none,
259
- distribution: none,
260
- leading-pagebreak: false,
261
- ) = {
262
- let has-backmatter = (
263
- (attachments != none and attachments.len() > 0)
264
- or (cc != none and cc.len() > 0)
265
- or (distribution != none and distribution.len() > 0)
266
- )
267
-
268
- if leading-pagebreak and has-backmatter {
269
- pagebreak(weak: true)
270
- }
271
-
272
- if attachments != none and attachments.len() > 0 {
273
- calculate-backmatter-spacing(true)
274
- let attachment-count = attachments.len()
275
- let section-label = if attachment-count == 1 { "Attachment:" } else { str(attachment-count) + " Attachments:" }
276
- let continuation-label = (
277
- (if attachment-count == 1 { "Attachment" } else { str(attachment-count) + " Attachments" })
278
- + " (listed on next page):"
279
- )
280
- render-backmatter-section(attachments, section-label, numbering-style: "1.", continuation-label: continuation-label)
281
- }
282
-
283
- if cc != none and cc.len() > 0 {
284
- calculate-backmatter-spacing(attachments == none or attachments.len() == 0)
285
- render-backmatter-section(cc, "cc:")
286
- }
287
-
288
- if distribution != none and distribution.len() > 0 {
289
- calculate-backmatter-spacing((attachments == none or attachments.len() == 0) and (cc == none or cc.len() == 0))
290
- render-backmatter-section(distribution, "DISTRIBUTION:")
291
- }
292
- }
293
-
1
+ // primitives.typ: Reusable rendering primitives for USAF memorandum sections
2
+ //
3
+ // This module implements the visual rendering functions that produce AFH 33-337
4
+ // compliant formatting for all sections of a USAF memorandum. Each function
5
+ // corresponds to specific placement and formatting requirements from Chapter 14.
6
+
7
+ #import "config.typ": *
8
+ #import "utils.typ": *
9
+
10
+ // =============================================================================
11
+ // LETTERHEAD RENDERING
12
+ // =============================================================================
13
+ // AFH 33-337 §1: "Use printed letterhead, computer-generated letterhead, or plain bond paper"
14
+ // Letterhead placement is not explicitly specified in AFH 33-337, but follows
15
+ // standard USAF memo formatting conventions
16
+
17
+ #let render-letterhead(title, caption, letterhead-seal, font) = {
18
+ font = ensure-array(font)
19
+ caption = ensure-string(caption)
20
+
21
+ place(
22
+ dy: 0.625in - spacing.margin,
23
+ box(
24
+ width: 100%,
25
+ fill: none,
26
+ stroke: none,
27
+ [
28
+ #place(
29
+ center + top,
30
+ align(center)[
31
+ #set text(12pt, font: font, fill: LETTERHEAD_COLOR)
32
+ #title\
33
+ #text(10.5pt)[#caption]
34
+ ],
35
+ )
36
+ ],
37
+ ),
38
+ )
39
+
40
+ if letterhead-seal != none {
41
+ place(
42
+ left + top,
43
+ dx: -0.5in,
44
+ dy: -.5in,
45
+ block[
46
+ #fit-box(width: 2in, height: 1in)[#letterhead-seal]
47
+ ],
48
+ )
49
+ }
50
+ }
51
+
52
+ // =============================================================================
53
+ // HEADER SECTIONS
54
+ // =============================================================================
55
+ // AFH 33-337 "The Heading Section" specifies exact placement and format for:
56
+ // - Date: 1 inch from right edge, 1.75 inches from top
57
+ // - MEMORANDUM FOR: Second line below date
58
+ // - FROM: Second line below MEMORANDUM FOR
59
+ // - SUBJECT: Second line below FROM
60
+
61
+ // AFH 33-337 "Date": "Place the date 1 inch from the right edge, 1.75 inches from the top"
62
+ #let render-date-section(date) = {
63
+ align(right)[#display-date(date)]
64
+ }
65
+
66
+ // AFH 33-337 "MEMORANDUM FOR": "Place 'MEMORANDUM FOR' on the second line below the date"
67
+ #let render-for-section(recipients, cols) = {
68
+ blank-line()
69
+ grid(
70
+ columns: (auto, auto, 1fr),
71
+ "MEMORANDUM FOR",
72
+ " ",
73
+ align(left)[
74
+ #if type(recipients) == array {
75
+ create-auto-grid(recipients, column-gutter: spacing.tab, cols: cols)
76
+ } else {
77
+ recipients
78
+ }
79
+ ],
80
+ )
81
+ }
82
+
83
+ // AFH 33-337 "FROM:": "Place 'FROM:' in uppercase, flush with the left margin,
84
+ // on the second line below the last line of the MEMORANDUM FOR element"
85
+ #let render-from-section(from-info) = {
86
+ blank-line()
87
+ from-info = ensure-string(from-info)
88
+
89
+ grid(
90
+ columns: (auto, auto, 1fr),
91
+ "FROM:", " ", align(left)[#from-info],
92
+ )
93
+ }
94
+
95
+ // AFH 33-337 "SUBJECT:": "In all uppercase letters place 'SUBJECT:', flush with the
96
+ // left margin, on the second line below the last line of the FROM element"
97
+ #let render-subject-section(subject-text) = {
98
+ blank-line()
99
+ grid(
100
+ columns: (auto, auto, 1fr),
101
+ "SUBJECT:", " ", [#subject-text],
102
+ )
103
+ }
104
+
105
+ #let render-references-section(references) = {
106
+ if not falsey(references) {
107
+ blank-line()
108
+ grid(
109
+ columns: (auto, auto, 1fr),
110
+ "References:", " ", enum(..references, numbering: "(a)"),
111
+ )
112
+ }
113
+ }
114
+
115
+ // =============================================================================
116
+ // SIGNATURE BLOCK
117
+ // =============================================================================
118
+ // AFH 33-337 "Signature Block": "Start the signature block on the fifth line below
119
+ // the last line of text and 4.5 inches from the left edge of the page"
120
+ // AFH 33-337 "Do not place the signature element on a continuation page by itself"
121
+
122
+ #let render-signature-block(signature-lines, signature-blank-lines: 4) = {
123
+ signature-lines = ensure-array(signature-lines)
124
+ // AFH 33-337: "The signature block is never on a page by itself"
125
+ // Note: Perfect enforcement isn't feasible without over-engineering
126
+ // We use weak: false spacing and breakable: false to discourage orphaning
127
+ // AFH 33-337: "fifth line below" = 4 blank lines between text and signature block
128
+ blank-lines(signature-blank-lines, weak: false)
129
+ block(breakable: false)[
130
+ #align(left)[
131
+ // AFH 33-337: "4.5 inches from the left edge of the page"
132
+ // We use (4.5in - margin) because Typst's pad() is relative to the text area, not page edge
133
+ #pad(left: 4.5in - spacing.margin)[
134
+ #text(hyphenate: false)[
135
+ #for line in signature-lines {
136
+ par(hanging-indent: 4 * 0.5em, line)
137
+ }
138
+ ]
139
+ ]
140
+ ]
141
+ ]
142
+ }
143
+
144
+ // =============================================================================
145
+ // ACTION LINE RENDERING
146
+ // =============================================================================
147
+ // Renders the Approve / Disapprove action line for indorsement memos.
148
+ // action: "none" = no action line displayed (hidden), "undecided" = both options
149
+ // rendered plain (no circle), "approve" = Approve circled,
150
+ // "disapprove" = Disapprove circled. The action line is rendered when
151
+ // action is "undecided", "approve", or "disapprove".
152
+
153
+ #let render-action-line(action) = {
154
+ assert(
155
+ action in ("none", "undecided", "approve", "disapprove"),
156
+ message: "action must be \"none\", \"undecided\", \"approve\", or \"disapprove\"",
157
+ )
158
+ blank-line()
159
+ // Circle the selected option using a box with rounded corners
160
+ // Use baseline parameter to maintain vertical text alignment
161
+ let approve-text = if action == "approve" {
162
+ box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[Approve]
163
+ } else if action == "disapprove" {
164
+ strike[Approve]
165
+ } else {
166
+ [Approve]
167
+ }
168
+ let disapprove-text = if action == "disapprove" {
169
+ box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[Disapprove]
170
+ } else if action == "approve" {
171
+ strike[Disapprove]
172
+ } else {
173
+ [Disapprove]
174
+ }
175
+ // Keep the action line with the following content (body or signature block)
176
+ // using the same sticky-block pattern that body.typ applies to the last
177
+ // paragraph, per AFH 33-337 §11 orphan-prevention rules.
178
+ block(sticky: true)[#approve-text / #disapprove-text]
179
+ }
180
+
181
+ // =============================================================================
182
+ // TABLE RENDERING
183
+ // =============================================================================
184
+ // AFH 33-337 does not specify table formatting, so we follow the general
185
+ // aesthetic principles of the standard: plain black borders, no decorative
186
+ // fills, and the body font inherited throughout.
187
+
188
+ /// Renders a table with USAF memorandum–consistent formatting.
189
+ ///
190
+ /// Applies simple 0.5pt black cell borders and standard padding to any
191
+ /// Typst `table` element, keeping the visual style clean and formal.
192
+ /// Font and size are inherited from the surrounding body text.
193
+ ///
194
+ /// - it (content): The table element to style and render
195
+ /// -> content
196
+ #let render-memo-table(it) = {
197
+ // AFH 33-337 does not specify table formatting, so we follow the general
198
+ // aesthetic principles of the standard: bold headers for clarity.
199
+ show table.cell.where(y: 0): set text(weight: "bold")
200
+ set table(
201
+ stroke: 0.5pt + black,
202
+ inset: (x: 0.5em, y: 0.4em),
203
+ )
204
+ it
205
+ }
206
+
207
+ // =============================================================================
208
+ // BACKMATTER SECTIONS
209
+ // =============================================================================
210
+ // AFH 33-337 "Attachment or Attachments": "Place 'Attachment:' (for a single attachment)
211
+ // or '# Attachments:' (for two or more attachments) at the left margin, on the third
212
+ // line below the signature element"
213
+ // AFH 33-337 "Courtesy Copy Element": "place 'cc:' flush with the left margin, on the
214
+ // second line below the attachment element"
215
+
216
+ #let render-backmatter-section(
217
+ content,
218
+ section-label,
219
+ numbering-style: none,
220
+ continuation-label: none,
221
+ ) = {
222
+ let formatted-content = {
223
+ // Use text() wrapper to prevent section label from being treated as a paragraph
224
+ text()[#section-label]
225
+ linebreak()
226
+ if numbering-style != none {
227
+ let items = ensure-array(content)
228
+ enum(..items, numbering: numbering-style)
229
+ } else {
230
+ ensure-string(content)
231
+ }
232
+ }
233
+
234
+ context {
235
+ let available-space = page.height - here().position().y - 1in
236
+ if measure(formatted-content).height > available-space {
237
+ let continuation-text = if continuation-label != none {
238
+ text()[#continuation-label]
239
+ } else {
240
+ text()[#section-label + " (listed on next page):"]
241
+ }
242
+ continuation-text
243
+ pagebreak()
244
+ }
245
+ formatted-content
246
+ }
247
+ }
248
+
249
+ #let calculate-backmatter-spacing(is-first-section) = {
250
+ context {
251
+ let line_count = if is-first-section { 2 } else { 1 }
252
+ blank-lines(line_count)
253
+ }
254
+ }
255
+
256
+ #let render-backmatter-sections(
257
+ attachments: none,
258
+ cc: none,
259
+ distribution: none,
260
+ leading-pagebreak: false,
261
+ ) = {
262
+ let has-backmatter = (
263
+ (attachments != none and attachments.len() > 0)
264
+ or (cc != none and cc.len() > 0)
265
+ or (distribution != none and distribution.len() > 0)
266
+ )
267
+
268
+ if leading-pagebreak and has-backmatter {
269
+ pagebreak(weak: true)
270
+ }
271
+
272
+ if attachments != none and attachments.len() > 0 {
273
+ calculate-backmatter-spacing(true)
274
+ let attachment-count = attachments.len()
275
+ let section-label = if attachment-count == 1 { "Attachment:" } else { str(attachment-count) + " Attachments:" }
276
+ let continuation-label = (
277
+ (if attachment-count == 1 { "Attachment" } else { str(attachment-count) + " Attachments" })
278
+ + " (listed on next page):"
279
+ )
280
+ render-backmatter-section(attachments, section-label, numbering-style: "1.", continuation-label: continuation-label)
281
+ }
282
+
283
+ if cc != none and cc.len() > 0 {
284
+ calculate-backmatter-spacing(attachments == none or attachments.len() == 0)
285
+ render-backmatter-section(cc, "cc:")
286
+ }
287
+
288
+ if distribution != none and distribution.len() > 0 {
289
+ calculate-backmatter-spacing((attachments == none or attachments.len() == 0) and (cc == none or cc.len() == 0))
290
+ render-backmatter-section(distribution, "DISTRIBUTION:")
291
+ }
292
+ }
293
+