@tonguetoquill/collection 0.2.3 → 0.2.5-beta.1

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 (102) 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 +88 -88
  7. package/quills/af4141/0.1.0/design/TASK.md +19 -19
  8. package/quills/af4141/0.1.0/example.md +35 -35
  9. package/quills/af4141/0.1.0/packages/typst-af4141/FIELDS.json +3169 -3169
  10. package/quills/af4141/0.1.0/packages/typst-af4141/form.typ +538 -538
  11. package/quills/af4141/0.1.0/packages/typst-af4141/lib.typ +227 -227
  12. package/quills/af4141/0.1.0/packages/typst-af4141/typst.toml +7 -7
  13. package/quills/af4141/0.1.0/plate.typ +48 -48
  14. package/quills/classic_resume/0.1.0/Quill.yaml +118 -118
  15. package/quills/classic_resume/0.1.0/example.md +232 -232
  16. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/LICENSE +21 -21
  17. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/README.md +38 -38
  18. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/components.typ +184 -184
  19. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/layout.typ +42 -42
  20. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/lib.typ +5 -5
  21. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/typst.toml +26 -26
  22. package/quills/classic_resume/0.1.0/plate.typ +44 -44
  23. package/quills/cmu_letter/0.1.0/.quillignore +30 -30
  24. package/quills/cmu_letter/0.1.0/Quill.yaml +64 -64
  25. package/quills/cmu_letter/0.1.0/assets/cmu-wordmark.svg +174 -174
  26. package/quills/cmu_letter/0.1.0/example.md +30 -30
  27. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/LICENSE +21 -21
  28. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/README.txt +100 -100
  29. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/backmatter.typ +13 -13
  30. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/config.typ +39 -39
  31. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/frontmatter.typ +72 -72
  32. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/lib.typ +47 -47
  33. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/mainmatter.typ +42 -42
  34. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/primitives.typ +70 -70
  35. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/utils.typ +85 -85
  36. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/typst.toml +17 -17
  37. package/quills/cmu_letter/0.1.0/plate.typ +19 -19
  38. package/quills/daf4392/0.1.0/Quill.yaml +110 -0
  39. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-700.ttf +0 -0
  40. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-700italic.ttf +0 -0
  41. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-italic.ttf +0 -0
  42. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-regular.ttf +0 -0
  43. package/quills/daf4392/0.1.0/assets/page1.png +0 -0
  44. package/quills/daf4392/0.1.0/example.md +33 -0
  45. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/FIELDS.json +9 -0
  46. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/form.typ +14 -0
  47. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/lib.typ +227 -0
  48. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/out/debug.typ +4 -0
  49. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/out/example.typ +4 -0
  50. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/page1.png +0 -0
  51. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/typst.toml +7 -0
  52. package/quills/daf4392/0.1.0/plate.typ +60 -0
  53. package/quills/taro/0.1.0/Quill.yaml +29 -29
  54. package/quills/taro/0.1.0/example.md +26 -26
  55. package/quills/taro/0.1.0/plate.typ +31 -31
  56. package/quills/usaf_memo/0.1.0/.quillignore +30 -30
  57. package/quills/usaf_memo/0.1.0/Quill.yaml +209 -209
  58. package/quills/usaf_memo/0.1.0/example.md +54 -54
  59. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -21
  60. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -93
  61. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -79
  62. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +339 -339
  63. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -28
  64. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/body.typ +332 -332
  65. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/config.typ +63 -63
  66. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -114
  67. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -118
  68. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -55
  69. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -32
  70. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +272 -272
  71. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/utils.typ +377 -377
  72. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/typst.toml +16 -16
  73. package/quills/usaf_memo/0.1.0/plate.typ +74 -74
  74. package/quills/usaf_memo/0.2.0/.quillignore +30 -30
  75. package/quills/usaf_memo/0.2.0/Quill.yaml +219 -219
  76. package/quills/usaf_memo/0.2.0/example.md +55 -55
  77. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/.gitignore +6 -6
  78. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -21
  79. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -93
  80. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -79
  81. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +339 -339
  82. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -28
  83. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +333 -333
  84. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/config.typ +64 -64
  85. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -114
  86. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -118
  87. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -55
  88. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -32
  89. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +293 -293
  90. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +374 -374
  91. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +27 -27
  92. package/quills/usaf_memo/0.2.0/plate.typ +75 -75
  93. package/templates/af4141.md +88 -88
  94. package/templates/cmu_letter_template.md +37 -37
  95. package/templates/daf4392.md +33 -0
  96. package/templates/loc.md +78 -78
  97. package/templates/pass_request.md +43 -43
  98. package/templates/rebuttal.md +55 -55
  99. package/templates/taro.md +26 -26
  100. package/templates/templates.json +55 -49
  101. package/templates/usaf_template.md +23 -23
  102. package/templates/ussf_template.md +29 -29
@@ -1,272 +1,272 @@
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 APPROVED / DISAPPROVED action line for indorsement memos.
148
- // action: none = both plain (no decision yet), "approved" = bold APPROVED /
149
- // strike DISAPPROVED, "disapproved" = strike APPROVED / bold DISAPPROVED.
150
- // Visibility is controlled by the caller (show_action / action != none).
151
-
152
- #let render-action-line(action) = {
153
- assert(
154
- action in (none, "approved", "disapproved"),
155
- message: "action must be none, \"approved\", or \"disapproved\"",
156
- )
157
- blank-line()
158
- let approved-text = if action == "approved" { strong[APPROVED] } else if action == "disapproved" { strike[APPROVED] } else { [APPROVED] }
159
- let disapproved-text = if action == "disapproved" { strong[DISAPPROVED] } else if action == "approved" { strike[DISAPPROVED] } else { [DISAPPROVED] }
160
- [#approved-text / #disapproved-text]
161
- }
162
-
163
- // =============================================================================
164
- // TABLE RENDERING
165
- // =============================================================================
166
- // AFH 33-337 does not specify table formatting, so we follow the general
167
- // aesthetic principles of the standard: plain black borders, no decorative
168
- // fills, and the body font inherited throughout.
169
-
170
- /// Renders a table with USAF memorandum–consistent formatting.
171
- ///
172
- /// Applies simple 0.5pt black cell borders and standard padding to any
173
- /// Typst `table` element, keeping the visual style clean and formal.
174
- /// Font and size are inherited from the surrounding body text.
175
- ///
176
- /// - it (content): The table element to style and render
177
- /// -> content
178
- #let render-memo-table(it) = {
179
- set table(
180
- stroke: 0.5pt + black,
181
- inset: (x: 0.5em, y: 0.4em),
182
- )
183
- it
184
- }
185
-
186
- // =============================================================================
187
- // BACKMATTER SECTIONS
188
- // =============================================================================
189
- // AFH 33-337 "Attachment or Attachments": "Place 'Attachment:' (for a single attachment)
190
- // or '# Attachments:' (for two or more attachments) at the left margin, on the third
191
- // line below the signature element"
192
- // AFH 33-337 "Courtesy Copy Element": "place 'cc:' flush with the left margin, on the
193
- // second line below the attachment element"
194
-
195
- #let render-backmatter-section(
196
- content,
197
- section-label,
198
- numbering-style: none,
199
- continuation-label: none,
200
- ) = {
201
- let formatted-content = {
202
- // Use text() wrapper to prevent section label from being treated as a paragraph
203
- text()[#section-label]
204
- linebreak()
205
- if numbering-style != none {
206
- let items = ensure-array(content)
207
- enum(..items, numbering: numbering-style)
208
- } else {
209
- ensure-string(content)
210
- }
211
- }
212
-
213
- context {
214
- let available-space = page.height - here().position().y - 1in
215
- if measure(formatted-content).height > available-space {
216
- let continuation-text = if continuation-label != none {
217
- text()[#continuation-label]
218
- } else {
219
- text()[#section-label + " (listed on next page):"]
220
- }
221
- continuation-text
222
- pagebreak()
223
- }
224
- formatted-content
225
- }
226
- }
227
-
228
- #let calculate-backmatter-spacing(is-first-section) = {
229
- context {
230
- let line_count = if is-first-section { 2 } else { 1 }
231
- blank-lines(line_count)
232
- }
233
- }
234
-
235
- #let render-backmatter-sections(
236
- attachments: none,
237
- cc: none,
238
- distribution: none,
239
- leading-pagebreak: false,
240
- ) = {
241
- let has-backmatter = (
242
- (attachments != none and attachments.len() > 0)
243
- or (cc != none and cc.len() > 0)
244
- or (distribution != none and distribution.len() > 0)
245
- )
246
-
247
- if leading-pagebreak and has-backmatter {
248
- pagebreak(weak: true)
249
- }
250
-
251
- if attachments != none and attachments.len() > 0 {
252
- calculate-backmatter-spacing(true)
253
- let attachment-count = attachments.len()
254
- let section-label = if attachment-count == 1 { "Attachment:" } else { str(attachment-count) + " Attachments:" }
255
- let continuation-label = (
256
- (if attachment-count == 1 { "Attachment" } else { str(attachment-count) + " Attachments" })
257
- + " (listed on next page):"
258
- )
259
- render-backmatter-section(attachments, section-label, numbering-style: "1.", continuation-label: continuation-label)
260
- }
261
-
262
- if cc != none and cc.len() > 0 {
263
- calculate-backmatter-spacing(attachments == none or attachments.len() == 0)
264
- render-backmatter-section(cc, "cc:")
265
- }
266
-
267
- if distribution != none and distribution.len() > 0 {
268
- calculate-backmatter-spacing((attachments == none or attachments.len() == 0) and (cc == none or cc.len() == 0))
269
- render-backmatter-section(distribution, "DISTRIBUTION:")
270
- }
271
- }
272
-
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 APPROVED / DISAPPROVED action line for indorsement memos.
148
+ // action: none = both plain (no decision yet), "approved" = bold APPROVED /
149
+ // strike DISAPPROVED, "disapproved" = strike APPROVED / bold DISAPPROVED.
150
+ // Visibility is controlled by the caller (show_action / action != none).
151
+
152
+ #let render-action-line(action) = {
153
+ assert(
154
+ action in (none, "approved", "disapproved"),
155
+ message: "action must be none, \"approved\", or \"disapproved\"",
156
+ )
157
+ blank-line()
158
+ let approved-text = if action == "approved" { strong[APPROVED] } else if action == "disapproved" { strike[APPROVED] } else { [APPROVED] }
159
+ let disapproved-text = if action == "disapproved" { strong[DISAPPROVED] } else if action == "approved" { strike[DISAPPROVED] } else { [DISAPPROVED] }
160
+ [#approved-text / #disapproved-text]
161
+ }
162
+
163
+ // =============================================================================
164
+ // TABLE RENDERING
165
+ // =============================================================================
166
+ // AFH 33-337 does not specify table formatting, so we follow the general
167
+ // aesthetic principles of the standard: plain black borders, no decorative
168
+ // fills, and the body font inherited throughout.
169
+
170
+ /// Renders a table with USAF memorandum–consistent formatting.
171
+ ///
172
+ /// Applies simple 0.5pt black cell borders and standard padding to any
173
+ /// Typst `table` element, keeping the visual style clean and formal.
174
+ /// Font and size are inherited from the surrounding body text.
175
+ ///
176
+ /// - it (content): The table element to style and render
177
+ /// -> content
178
+ #let render-memo-table(it) = {
179
+ set table(
180
+ stroke: 0.5pt + black,
181
+ inset: (x: 0.5em, y: 0.4em),
182
+ )
183
+ it
184
+ }
185
+
186
+ // =============================================================================
187
+ // BACKMATTER SECTIONS
188
+ // =============================================================================
189
+ // AFH 33-337 "Attachment or Attachments": "Place 'Attachment:' (for a single attachment)
190
+ // or '# Attachments:' (for two or more attachments) at the left margin, on the third
191
+ // line below the signature element"
192
+ // AFH 33-337 "Courtesy Copy Element": "place 'cc:' flush with the left margin, on the
193
+ // second line below the attachment element"
194
+
195
+ #let render-backmatter-section(
196
+ content,
197
+ section-label,
198
+ numbering-style: none,
199
+ continuation-label: none,
200
+ ) = {
201
+ let formatted-content = {
202
+ // Use text() wrapper to prevent section label from being treated as a paragraph
203
+ text()[#section-label]
204
+ linebreak()
205
+ if numbering-style != none {
206
+ let items = ensure-array(content)
207
+ enum(..items, numbering: numbering-style)
208
+ } else {
209
+ ensure-string(content)
210
+ }
211
+ }
212
+
213
+ context {
214
+ let available-space = page.height - here().position().y - 1in
215
+ if measure(formatted-content).height > available-space {
216
+ let continuation-text = if continuation-label != none {
217
+ text()[#continuation-label]
218
+ } else {
219
+ text()[#section-label + " (listed on next page):"]
220
+ }
221
+ continuation-text
222
+ pagebreak()
223
+ }
224
+ formatted-content
225
+ }
226
+ }
227
+
228
+ #let calculate-backmatter-spacing(is-first-section) = {
229
+ context {
230
+ let line_count = if is-first-section { 2 } else { 1 }
231
+ blank-lines(line_count)
232
+ }
233
+ }
234
+
235
+ #let render-backmatter-sections(
236
+ attachments: none,
237
+ cc: none,
238
+ distribution: none,
239
+ leading-pagebreak: false,
240
+ ) = {
241
+ let has-backmatter = (
242
+ (attachments != none and attachments.len() > 0)
243
+ or (cc != none and cc.len() > 0)
244
+ or (distribution != none and distribution.len() > 0)
245
+ )
246
+
247
+ if leading-pagebreak and has-backmatter {
248
+ pagebreak(weak: true)
249
+ }
250
+
251
+ if attachments != none and attachments.len() > 0 {
252
+ calculate-backmatter-spacing(true)
253
+ let attachment-count = attachments.len()
254
+ let section-label = if attachment-count == 1 { "Attachment:" } else { str(attachment-count) + " Attachments:" }
255
+ let continuation-label = (
256
+ (if attachment-count == 1 { "Attachment" } else { str(attachment-count) + " Attachments" })
257
+ + " (listed on next page):"
258
+ )
259
+ render-backmatter-section(attachments, section-label, numbering-style: "1.", continuation-label: continuation-label)
260
+ }
261
+
262
+ if cc != none and cc.len() > 0 {
263
+ calculate-backmatter-spacing(attachments == none or attachments.len() == 0)
264
+ render-backmatter-section(cc, "cc:")
265
+ }
266
+
267
+ if distribution != none and distribution.len() > 0 {
268
+ calculate-backmatter-spacing((attachments == none or attachments.len() == 0) and (cc == none or cc.len() == 0))
269
+ render-backmatter-section(distribution, "DISTRIBUTION:")
270
+ }
271
+ }
272
+