@tonguetoquill/collection 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 (102) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +39 -0
  3. package/index.d.ts +2 -0
  4. package/index.js +8 -0
  5. package/package.json +36 -0
  6. package/quills/classic_resume/0.1.0/Quill.yaml +118 -0
  7. package/quills/classic_resume/0.1.0/example.md +232 -0
  8. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/LICENSE +21 -0
  9. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/README.md +38 -0
  10. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/fonts/EBGaramond-Bold.ttf +0 -0
  11. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/fonts/EBGaramond-BoldItalic.ttf +0 -0
  12. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/fonts/EBGaramond-Italic.ttf +0 -0
  13. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/fonts/EBGaramond-Regular.ttf +0 -0
  14. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/components.typ +184 -0
  15. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/layout.typ +42 -0
  16. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/lib.typ +5 -0
  17. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/typst.toml +26 -0
  18. package/quills/classic_resume/0.1.0/plate.typ +44 -0
  19. package/quills/cmu_letter/0.1.0/.quillignore +31 -0
  20. package/quills/cmu_letter/0.1.0/Quill.yaml +64 -0
  21. package/quills/cmu_letter/0.1.0/assets/cmu-wordmark.svg +174 -0
  22. package/quills/cmu_letter/0.1.0/example.md +31 -0
  23. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/LICENSE +21 -0
  24. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/OFL.txt +93 -0
  25. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/OpenSans-Bold.ttf +0 -0
  26. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/OpenSans-BoldItalic.ttf +0 -0
  27. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/OpenSans-Italic.ttf +0 -0
  28. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/OpenSans-Regular.ttf +0 -0
  29. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/README.txt +100 -0
  30. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/backmatter.typ +13 -0
  31. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/config.typ +40 -0
  32. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/frontmatter.typ +72 -0
  33. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/lib.typ +47 -0
  34. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/mainmatter.typ +42 -0
  35. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/primitives.typ +70 -0
  36. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/utils.typ +85 -0
  37. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/typst.toml +17 -0
  38. package/quills/cmu_letter/0.1.0/plate.typ +19 -0
  39. package/quills/taro/0.1.0/Quill.yaml +29 -0
  40. package/quills/taro/0.1.0/assets/Figtree-Bold.ttf +0 -0
  41. package/quills/taro/0.1.0/assets/Figtree-Italic.ttf +0 -0
  42. package/quills/taro/0.1.0/assets/Figtree-Regular.ttf +0 -0
  43. package/quills/taro/0.1.0/example.md +27 -0
  44. package/quills/taro/0.1.0/plate.typ +31 -0
  45. package/quills/usaf_memo/0.1.0/.quillignore +31 -0
  46. package/quills/usaf_memo/0.1.0/Quill.yaml +209 -0
  47. package/quills/usaf_memo/0.1.0/assets/dow_seal.jpg +0 -0
  48. package/quills/usaf_memo/0.1.0/example.md +55 -0
  49. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -0
  50. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
  51. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -0
  52. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
  53. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -0
  54. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
  55. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
  56. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
  57. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
  58. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
  59. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -0
  60. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/body.typ +325 -0
  61. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/config.typ +63 -0
  62. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -0
  63. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -0
  64. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -0
  65. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -0
  66. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +272 -0
  67. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/utils.typ +377 -0
  68. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/typst.toml +17 -0
  69. package/quills/usaf_memo/0.1.0/plate.typ +74 -0
  70. package/quills/usaf_memo/0.2.0/.quillignore +31 -0
  71. package/quills/usaf_memo/0.2.0/Quill.yaml +225 -0
  72. package/quills/usaf_memo/0.2.0/assets/dow_seal.jpg +0 -0
  73. package/quills/usaf_memo/0.2.0/example.md +57 -0
  74. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -0
  75. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
  76. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -0
  77. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
  78. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -0
  79. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
  80. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
  81. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
  82. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
  83. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
  84. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -0
  85. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +325 -0
  86. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/config.typ +63 -0
  87. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -0
  88. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -0
  89. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -0
  90. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -0
  91. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +272 -0
  92. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +377 -0
  93. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +17 -0
  94. package/quills/usaf_memo/0.2.0/plate.typ +76 -0
  95. package/templates/cmu_letter_template.md +38 -0
  96. package/templates/loc.md +79 -0
  97. package/templates/pass_request.md +44 -0
  98. package/templates/rebuttal.md +56 -0
  99. package/templates/taro.md +27 -0
  100. package/templates/templates.json +44 -0
  101. package/templates/usaf_template.md +23 -0
  102. package/templates/ussf_template.md +29 -0
@@ -0,0 +1,325 @@
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
+ /// AFH 33-337 "The Text of the Official Memorandum" §2: "Number and letter each
17
+ /// paragraph and subparagraph" with hierarchical numbering implied by examples.
18
+ /// Standard military format follows the pattern: 1., a., (1), (a), etc.
19
+ ///
20
+ /// Returns the appropriate numbering format for AFH 33-337 compliant
21
+ /// hierarchical paragraph numbering:
22
+ /// - Level 0: "1." (1., 2., 3., etc.)
23
+ /// - Level 1: "a." (a., b., c., etc.)
24
+ /// - Level 2: "(1)" ((1), (2), (3), etc.)
25
+ /// - Level 3: "(a)" ((a), (b), (c), etc.)
26
+ /// - Level 4+: Underlined format for deeper nesting
27
+ ///
28
+ /// - level (int): Paragraph nesting level (0-based)
29
+ /// -> str | function
30
+ #let get-paragraph-numbering-format(level) = {
31
+ paragraph-config.numbering-formats.at(level, default: "i.")
32
+ }
33
+
34
+ /// Generates paragraph number for a given level with proper formatting.
35
+ ///
36
+ /// Creates properly formatted paragraph numbers for the hierarchical numbering
37
+ /// system using Typst's native counter display capabilities.
38
+ ///
39
+ /// - level (int): Paragraph nesting level (0-based)
40
+ /// - counter-value (none | int): Optional explicit counter value to use (for measuring widths)
41
+ /// - increment (bool): Whether to increment the counter after display
42
+ /// -> content
43
+ #let generate-paragraph-number(level, counter-value: none) = {
44
+ let paragraph-counter = counter(paragraph-config.counter-prefix + str(level))
45
+ let numbering-format = get-paragraph-numbering-format(level)
46
+
47
+ if counter-value != none {
48
+ // For measuring widths: create temporary counter at specific value
49
+ assert(counter-value >= 0, message: "Counter value of `" + str(counter-value) + "` cannot be less than 0")
50
+ let temp-counter = counter("temp-counter")
51
+ temp-counter.update(counter-value)
52
+ temp-counter.display(numbering-format)
53
+ } else {
54
+ // Standard case: display and increment
55
+ let result = paragraph-counter.display(numbering-format)
56
+ paragraph-counter.step()
57
+ result
58
+ }
59
+ }
60
+
61
+ /// Calculates proper indentation width for a paragraph level.
62
+ ///
63
+ /// AFH 33-337 "The Text of the Official Memorandum" §4-5:
64
+ /// - "The first paragraph is never indented; it is numbered and flush left"
65
+ /// - "Indent the first line of sub-paragraphs to align the number or letter with
66
+ /// the first character of its parent level paragraph"
67
+ ///
68
+ /// Computes the exact indentation needed for hierarchical paragraph alignment
69
+ /// by measuring the cumulative width of all ancestor paragraph numbers and their
70
+ /// spacing. Uses direct iteration instead of recursion for better performance.
71
+ ///
72
+ /// Per AFH 33-337: Sub-paragraph text aligns with first character of parent text,
73
+ /// which means indentation = sum of all ancestor number widths + spacing.
74
+ ///
75
+ /// - level (int): Paragraph nesting level (0-based)
76
+ /// -> length
77
+ #let calculate-paragraph-indent(level) = {
78
+ assert(level >= 0)
79
+ if level == 0 {
80
+ return 0pt
81
+ }
82
+
83
+ // Accumulate widths of all ancestor numbers iteratively
84
+ let total-indent = 0pt
85
+ for ancestor-level in range(level) {
86
+ let ancestor-counter-value = counter(paragraph-config.counter-prefix + str(ancestor-level)).get().at(0)
87
+ let ancestor-number = generate-paragraph-number(ancestor-level, counter-value: ancestor-counter-value)
88
+ // Measure number + spacing buffer
89
+ let width = measure([#ancestor-number#" "]).width
90
+ total-indent += width
91
+ }
92
+
93
+ total-indent
94
+ }
95
+
96
+ /// Global state for tracking current paragraph level.
97
+ /// Used internally by the paragraph numbering system to maintain proper nesting.
98
+ /// -> state
99
+ #let PAR_LEVEL_STATE = state("PAR_LEVEL", 0)
100
+
101
+
102
+ /// Sets the current paragraph level state.
103
+ ///
104
+ /// Internal function used by the paragraph numbering system to track
105
+ /// the current nesting level for proper indentation and numbering.
106
+ ///
107
+ /// - level (int): Paragraph nesting level to set
108
+ /// -> content
109
+ #let SET_PAR_LEVEL(level) = {
110
+ context {
111
+ PAR_LEVEL_STATE.update(level)
112
+ if level == 0 {
113
+ counter(paragraph-config.counter-prefix + str(level + 1)).update(1)
114
+ }
115
+ }
116
+ }
117
+
118
+ /// Creates a formatted paragraph with automatic numbering and indentation.
119
+ ///
120
+ /// Generates a properly formatted paragraph with AFH 33-337 compliant numbering,
121
+ /// indentation, and spacing. Automatically manages counter incrementation and
122
+ /// nested paragraph state. Used internally by the body rendering system.
123
+ ///
124
+ /// Features:
125
+ /// - Automatic paragraph number generation and formatting
126
+ /// - Proper indentation based on nesting level via direct width measurement
127
+ /// - Counter management for hierarchical numbering
128
+ /// - Widow/orphan prevention settings
129
+ ///
130
+ /// - content (content): Paragraph content to format
131
+ /// -> content
132
+ #let memo-par(content) = context {
133
+ let level = PAR_LEVEL_STATE.get()
134
+ let paragraph-number = generate-paragraph-number(level)
135
+ // Reset child level counter
136
+ counter(paragraph-config.counter-prefix + str(level + 1)).update(1)
137
+ let indent-width = calculate-paragraph-indent(level)
138
+
139
+
140
+ // Number + two spaces + content, with left padding for nesting
141
+ //pad(left: indent-width, paragraph-number + " " + content)
142
+ [#h(indent-width)#paragraph-number#" "#content]
143
+ }
144
+
145
+ // =============================================================================
146
+ // PARAGRAPH BODY RENDERING
147
+ // =============================================================================
148
+ // AFH 33-337 "The Text of the Official Memorandum" §1-12 specifies:
149
+ // - Single-space text, double-space between paragraphs
150
+ // - Number and letter each paragraph and subparagraph
151
+ // - "A single paragraph is not numbered" (§2)
152
+ // - First paragraph flush left, never indented
153
+ // - Indent sub-paragraphs to align with first character of parent paragraph text
154
+ #let render-body(content, auto-numbering: true) = {
155
+ let PAR_BUFFER = state("PAR_BUFFER")
156
+ PAR_BUFFER.update(())
157
+ let NEST_DOWN = counter("NEST_DOWN")
158
+ NEST_DOWN.update(0)
159
+ let NEST_UP = counter("NEST_UP")
160
+ NEST_UP.update(0)
161
+ let IS_HEADING = state("IS_HEADING")
162
+ IS_HEADING.update(false)
163
+ // Initialize level counters to 1 (Typst counters default to 0)
164
+ for i in range(0, 5) {
165
+ counter(paragraph-config.counter-prefix + "0").update(1)
166
+ }
167
+
168
+ // The first pass parses paragraphs, list items, etc. into standardized arrays
169
+ let first_pass = {
170
+ // Collect pars with nesting level
171
+ show par: p => context {
172
+ let nest_level = NEST_DOWN.get().at(0) - NEST_UP.get().at(0)
173
+ let is_heading = IS_HEADING.get()
174
+
175
+ PAR_BUFFER.update(pars => {
176
+ // Item tuple: (content, nest_level, is_heading, is_table)
177
+ pars.push((text([#p.body]), nest_level, is_heading, false))
178
+ pars
179
+ })
180
+ p
181
+ }
182
+ // Collect tables — captured as-is without paragraph numbering
183
+ show table: t => context {
184
+ PAR_BUFFER.update(pars => {
185
+ pars.push((t, -1, false, true))
186
+ pars
187
+ })
188
+ t
189
+ }
190
+ {
191
+ show heading: h => {
192
+ IS_HEADING.update(true)
193
+ [#parbreak()#h.body#parbreak()]
194
+ IS_HEADING.update(false)
195
+ }
196
+
197
+ // Convert list/enum items to pars
198
+ // Note: No context wrapper here - state updates don't need it and cause
199
+ // layout convergence issues with many list items
200
+ show enum.item: it => {
201
+ NEST_DOWN.step()
202
+ [#parbreak()#it.body#parbreak()]
203
+ NEST_UP.step()
204
+ }
205
+ show list.item: it => {
206
+ NEST_DOWN.step()
207
+ [#parbreak()#it.body#parbreak()]
208
+ NEST_UP.step()
209
+ }
210
+
211
+ {
212
+ // Typst bug bandaid:
213
+ // `show par` will not collect wrappers unless there is content outside
214
+ // Add zero width space to always have content outside of wrapper
215
+ show strong: it => {
216
+ [#it#sym.zws]
217
+ }
218
+ show emph: it => {
219
+ [#it#sym.zws]
220
+ }
221
+ show underline: it => {
222
+ [#it#sym.zws]
223
+ }
224
+ show raw: it => {
225
+ [#it#sym.zws]
226
+ }
227
+ [#content#parbreak()]
228
+ }
229
+ }
230
+ }
231
+ // Use place() to prevent hidden content from affecting layout flow
232
+ place(hide(first_pass))
233
+
234
+ //Second pass: consume par buffer
235
+ //
236
+ // PAR_BUFFER item tuple layout:
237
+ // item.at(0) — content : the paragraph body or table element
238
+ // item.at(1) — nest_level : nesting depth (−1 for tables)
239
+ // item.at(2) — is_heading : bool, true if item is a heading paragraph
240
+ // item.at(3) — is_table : bool, true if item is a table element
241
+ let ITEM_IS_TABLE = 3
242
+ context {
243
+ let heading_buffer = none
244
+ // Tables do not count as paragraphs for AFH 33-337 §2 numbering purposes
245
+ let par_count = PAR_BUFFER.get().filter(item => not item.at(ITEM_IS_TABLE, default: false)).len()
246
+ let items = PAR_BUFFER.get()
247
+ let total_count = items.len()
248
+ let i = 0
249
+ for item in items {
250
+ i += 1
251
+ let is_table = item.at(ITEM_IS_TABLE, default: false)
252
+
253
+ // Render tables inline without paragraph numbering
254
+ if is_table {
255
+ blank-line()
256
+ render-memo-table(item.at(0))
257
+ continue
258
+ }
259
+
260
+ let par_content = item.at(0)
261
+ let nest_level = item.at(1)
262
+ let is_heading = item.at(2)
263
+
264
+ // Prepend heading as bolded sentence
265
+ if heading_buffer != none {
266
+ par_content = [#strong[#heading_buffer.] #par_content]
267
+ }
268
+ if is_heading {
269
+ heading_buffer = par_content
270
+ continue
271
+ }
272
+
273
+ let final_par = {
274
+ if auto-numbering {
275
+ if par_count > 1 {
276
+ // Apply paragraph numbering per AFH 33-337 §2
277
+ SET_PAR_LEVEL(nest_level)
278
+ let paragraph = memo-par(par_content)
279
+ paragraph
280
+ } else {
281
+ // AFH 33-337 §2: "A single paragraph is not numbered"
282
+ // Return body content wrapped in block (like numbered case, but without numbering)
283
+ par_content
284
+ }
285
+ } else {
286
+ // Unnumbered mode: only explicitly nested items (enum/list) get numbered
287
+ if nest_level > 0 {
288
+ SET_PAR_LEVEL(nest_level - 1)
289
+ let paragraph = memo-par(par_content)
290
+ paragraph
291
+ } else {
292
+ // Base-level paragraphs are flush left with no numbering
293
+ par_content
294
+ }
295
+ }
296
+ }
297
+
298
+ //If this is the final paragraph, apply AFH 33-337 §11 rule:
299
+ // "Avoid dividing a paragraph of less than four lines between two pages"
300
+ blank-line()
301
+ if i == total_count {
302
+ let available_width = page.width - spacing.margin * 2
303
+
304
+ // Use configured spacing for line height calculation
305
+ let line_height = measure(line(length: spacing.line + spacing.line-height)).width
306
+ // Calculate last par's height
307
+ let par_height = measure(final_par, width: available_width).height
308
+
309
+ let estimated_lines = calc.ceil(par_height / line_height)
310
+
311
+ if estimated_lines < 4 {
312
+ // Short paragraph (< 4 lines): make sticky to keep with signature
313
+ block(sticky: true)[#final_par]
314
+ } else {
315
+ // Longer paragraph (≥ 4 lines): use default breaking behavior
316
+ block(breakable: true)[#final_par]
317
+ }
318
+ } else {
319
+ final_par
320
+ }
321
+ }
322
+ }
323
+ }
324
+
325
+
@@ -0,0 +1,63 @@
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
+ line: .5em, // Internal line spacing for readability
13
+ line-height: .7em, // Base line height for spacing calculations
14
+ tab: 0.5in, // Tab stop for multi-column recipient alignment
15
+ margin: 1in, // AFH 33-337 §4: "Use 1-inch margins on the left, right and bottom"
16
+ )
17
+
18
+ // =============================================================================
19
+ // TYPOGRAPHY DEFAULTS
20
+ // =============================================================================
21
+ // AFH 33-337 §5: "Use 12 point Times New Roman font for text"
22
+
23
+ #let DEFAULT_LETTERHEAD_FONTS = ("Copperplate CC",)
24
+ #let DEFAULT_BODY_FONTS = ("times new roman", "NimbusRomNo9L") // AFH 33-337 §5: Times New Roman required
25
+ #let LETTERHEAD_COLOR = rgb("#000099") // Standard USAF blue for letterhead
26
+
27
+ // =============================================================================
28
+ // PARAGRAPH CONFIGURATION
29
+ // =============================================================================
30
+ // AFH 33-337 "The Text of the Official Memorandum" §2:
31
+ // "Number and letter each paragraph and subparagraph"
32
+ // Hierarchical numbering: 1., a., (1), (a), etc.
33
+
34
+ #let paragraph-config = (
35
+ counter-prefix: "par-counter-",
36
+ // AFH 33-337 §2: Hierarchical paragraph numbering format
37
+ // Level 0: 1., 2., 3. | Level 1: a., b., c. | Level 2: (1), (2), (3) | Level 3: (a), (b), (c)
38
+ numbering-formats: ("1.", "a.", "(1)", "(a)", n => underline(str(n)), n => underline(str(n))),
39
+ )
40
+
41
+ // =============================================================================
42
+ // COUNTERS
43
+ // =============================================================================
44
+
45
+ #let counters = (
46
+ indorsement: counter("indorsement"),
47
+ )
48
+
49
+ // =============================================================================
50
+ // CLASSIFICATION COLORS
51
+ // =============================================================================
52
+ // AFH 33-337 §3: "Follow AFI 31-401, Information Security Program Management,
53
+ // applicable executive orders and DoD guidance for the necessary markings on
54
+ // classified correspondence."
55
+ // Color values follow DoD standard classification marking colors
56
+ // Source: https://security.stackexchange.com/questions/161829
57
+
58
+ #let CLASSIFICATION_COLORS = (
59
+ "UNCLASSIFIED": rgb(0, 122, 51), // Forest green (#007A33)
60
+ "CONFIDENTIAL": rgb(0, 51, 160), // Deep blue (#0033A0)
61
+ "SECRET": rgb(200, 16, 46), // Crimson red (#C8102E)
62
+ "TOP SECRET": rgb(255, 103, 31), // Burnt orange (#FF671F)
63
+ )
@@ -0,0 +1,114 @@
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_font: DEFAULT_LETTERHEAD_FONTS,
22
+ body_font: DEFAULT_BODY_FONTS,
23
+ font_size: 12pt,
24
+ memo_for_cols: 3,
25
+ classification_level: none,
26
+ footer_tag_line: none,
27
+ auto_numbering: true,
28
+ it,
29
+ ) = {
30
+ assert(subject != none, message: "subject is required")
31
+ assert(memo_for != none, message: "memo_for is required")
32
+ assert(memo_from != none, message: "memo_from is required")
33
+
34
+ let actual_date = if date == none { datetime.today() } else { date }
35
+ let classification_color = get-classification-level-color(classification_level)
36
+
37
+ // Document-wide typography settings (inlined from configure())
38
+ set par(leading: spacing.line, spacing: spacing.line, justify: false)
39
+ set block(above: spacing.line, below: 0em, spacing: 0em)
40
+ set text(font: body_font, size: font_size, fallback: true)
41
+
42
+ set page(
43
+ paper: "us-letter",
44
+ // AFH 33-337 §4: "Use 1-inch margins on the left, right and bottom"
45
+ margin: (
46
+ left: spacing.margin,
47
+ right: spacing.margin,
48
+ top: spacing.margin,
49
+ bottom: spacing.margin,
50
+ ),
51
+ header: {
52
+ // AFH 33-337 "Page numbering" §12: "The first page of a memorandum is never numbered.
53
+ // Number the succeeding pages starting with page 2. Place page numbers 0.5-inch from
54
+ // the top of the page, flush with the right margin."
55
+ context if counter(page).get().first() > 1 {
56
+ place(
57
+ dy: +.5in,
58
+ block(
59
+ width: 100%,
60
+ align(right, text(12pt)[#counter(page).display()]),
61
+ ),
62
+ )
63
+ }
64
+
65
+ if classification_level != none {
66
+ place(
67
+ top + center,
68
+ dy: 0.375in,
69
+ text(12pt, font: DEFAULT_BODY_FONTS, fill: classification_color)[#strong(classification_level)],
70
+ )
71
+ }
72
+ },
73
+ footer: {
74
+ place(
75
+ bottom + center,
76
+ dy: -.375in,
77
+ text(12pt, font: DEFAULT_BODY_FONTS, fill: classification_color)[#strong(classification_level)],
78
+ )
79
+
80
+ if not falsey(footer_tag_line) {
81
+ place(
82
+ bottom + center,
83
+ dy: -0.625in,
84
+ align(center)[
85
+ #text(fill: LETTERHEAD_COLOR, font: "cinzel", size: 15pt)[#footer_tag_line]
86
+ ],
87
+ )
88
+ }
89
+ },
90
+ )
91
+
92
+ render-letterhead(letterhead_title, letterhead_caption, letterhead_seal, letterhead_font)
93
+
94
+ // AFH 33-337 "Date": "Place the date 1 inch from the right edge, 1.75 inches from the top"
95
+ // Since we have a 1-inch top margin, we need (1.75in - margin) vertical space
96
+ v(1.75in - spacing.margin)
97
+
98
+ render-date-section(actual_date)
99
+ render-for-section(memo_for, memo_for_cols)
100
+ render-from-section(memo_from)
101
+ render-subject-section(subject)
102
+ render-references-section(references)
103
+
104
+ metadata((
105
+ subject: subject,
106
+ original_date: actual_date,
107
+ original_from: first-or-value(memo_from),
108
+ body_font: body_font,
109
+ font_size: font_size,
110
+ auto_numbering: auto_numbering,
111
+ ))
112
+
113
+ it
114
+ }
@@ -0,0 +1,118 @@
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
+ // Show the APPROVED / DISAPPROVED action line. Default: false.
27
+ show_action: false,
28
+ // Approval decision: none (no decision), "approved", or "disapproved".
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 original_subject = config.subject
55
+ let original_date = config.original_date
56
+ let original_from = config.original_from
57
+
58
+ // Read the counter value (already stepped above)
59
+ let indorsement_number = counters.indorsement.get().at(0, default: 1)
60
+ let indorsement_label = format-indorsement-number(indorsement_number)
61
+
62
+ if format == "separate_page" {
63
+ pagebreak()
64
+ [#indorsement_label to #original_from, #display-date(original_date), #original_subject]
65
+
66
+ blank-line()
67
+ grid(
68
+ columns: (auto, 1fr),
69
+ ind_from, align(right)[#display-date(actual_date)],
70
+ )
71
+
72
+ blank-line()
73
+ grid(
74
+ columns: (auto, auto, 1fr),
75
+ "MEMORANDUM FOR", " ", ind_for,
76
+ )
77
+ } else {
78
+ blank-line()
79
+ grid(
80
+ columns: (auto, 1fr),
81
+ [#indorsement_label, #ind_from], align(right)[#display-date(actual_date)],
82
+ )
83
+
84
+ blank-line()
85
+ grid(
86
+ columns: (auto, auto, 1fr),
87
+ "MEMORANDUM FOR", " ", ind_for,
88
+ )
89
+ }
90
+ }
91
+ blank-line()
92
+ }
93
+
94
+ // Show action line if explicitly requested or if an action decision is set
95
+ if show_action or action != none {
96
+ render-action-line(action)
97
+ }
98
+
99
+ render-body(content)
100
+
101
+ render-signature-block(signature_block, signature-blank-lines: signature_blank_lines)
102
+
103
+ if not falsey(attachments) {
104
+ calculate-backmatter-spacing(true)
105
+ let attachment-count = attachments.len()
106
+ let section-label = if attachment-count == 1 { "Attachment:" } else { str(attachment-count) + " Attachments:" }
107
+ let continuation-label = (
108
+ (if attachment-count == 1 { "Attachment" } else { str(attachment-count) + " Attachments" })
109
+ + " (listed on next page):"
110
+ )
111
+ render-backmatter-section(attachments, section-label, numbering-style: "1.", continuation-label: continuation-label)
112
+ }
113
+
114
+ if not falsey(cc) {
115
+ calculate-backmatter-spacing(falsey(attachments))
116
+ render-backmatter-section(cc, "cc:")
117
+ }
118
+ }
@@ -0,0 +1,55 @@
1
+ // lib.typ: Public API for USAF memorandum template
2
+ //
3
+ // This module provides a composable API for creating United States Air Force
4
+ // memorandums that comply with AFH 33-337 "The Tongue and Quill" Chapter 14
5
+ // "The Official Memorandum" formatting standards.
6
+ //
7
+ // AFH 33-337 Chapter 14 specifies exact requirements for:
8
+ // - Margins: 1 inch on all sides (§4)
9
+ // - Font: 12pt Times New Roman (§5)
10
+ // - Date placement: 1.75 inches from top, 1 inch from right (Date section)
11
+ // - Heading elements: MEMORANDUM FOR, FROM, SUBJECT with 2-line spacing
12
+ // - Paragraph numbering: Hierarchical 1., a., (1), (a) format (§2)
13
+ // - Signature block: 4.5 inches from left, never orphaned (Signature Block section)
14
+ // - Backmatter: Attachments, cc:, distribution with specific spacing
15
+ //
16
+ // Key features:
17
+ // - Composable show rules for frontmatter and mainmatter
18
+ // - Function-based backmatter and indorsements for correct ordering
19
+ // - No global state - configuration flows through metadata
20
+ // - Reusable primitives for common rendering tasks
21
+ // - AFH 33-337 compliant formatting throughout
22
+ //
23
+ // Basic usage:
24
+ //
25
+ // #import "@preview/tonguetoquill-usaf-memo:0.2.0": frontmatter, mainmatter, backmatter, indorsement
26
+ //
27
+ // #show: frontmatter.with(
28
+ // subject: "Your Subject Here",
29
+ // memo_for: ("OFFICE/SYMBOL",),
30
+ // memo_from: ("YOUR/SYMBOL",),
31
+ // )
32
+ //
33
+ // #show: mainmatter
34
+ //
35
+ // Your memo body content here.
36
+ // (Paragraphs are automatically numbered per AFH 33-337)
37
+ //
38
+ // #backmatter(
39
+ // signature_block: ("NAME, Rank, USAF", "Title"),
40
+ // attachments: (...),
41
+ // cc: (...),
42
+ // )
43
+ //
44
+ // #indorsement(
45
+ // from: "ORG/SYMBOL",
46
+ // to: "RECIPIENT/SYMBOL",
47
+ // signature_block: ("NAME, Rank, USAF", "Title"),
48
+ // )[
49
+ // Indorsement content here.
50
+ // ]
51
+
52
+ #import "frontmatter.typ": frontmatter
53
+ #import "mainmatter.typ": mainmatter
54
+ #import "backmatter.typ": backmatter
55
+ #import "indorsement.typ": indorsement