@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.
- package/LICENSE +201 -0
- package/README.md +39 -0
- package/index.d.ts +2 -0
- package/index.js +8 -0
- package/package.json +36 -0
- package/quills/classic_resume/0.1.0/Quill.yaml +118 -0
- package/quills/classic_resume/0.1.0/example.md +232 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/LICENSE +21 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/README.md +38 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/fonts/EBGaramond-Bold.ttf +0 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/fonts/EBGaramond-BoldItalic.ttf +0 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/fonts/EBGaramond-Italic.ttf +0 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/fonts/EBGaramond-Regular.ttf +0 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/components.typ +184 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/layout.typ +42 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/lib.typ +5 -0
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/typst.toml +26 -0
- package/quills/classic_resume/0.1.0/plate.typ +44 -0
- package/quills/cmu_letter/0.1.0/.quillignore +31 -0
- package/quills/cmu_letter/0.1.0/Quill.yaml +64 -0
- package/quills/cmu_letter/0.1.0/assets/cmu-wordmark.svg +174 -0
- package/quills/cmu_letter/0.1.0/example.md +31 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/LICENSE +21 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/OFL.txt +93 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/OpenSans-Bold.ttf +0 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/OpenSans-BoldItalic.ttf +0 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/OpenSans-Italic.ttf +0 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/OpenSans-Regular.ttf +0 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/README.txt +100 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/backmatter.typ +13 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/config.typ +40 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/frontmatter.typ +72 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/lib.typ +47 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/mainmatter.typ +42 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/primitives.typ +70 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/utils.typ +85 -0
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/typst.toml +17 -0
- package/quills/cmu_letter/0.1.0/plate.typ +19 -0
- package/quills/taro/0.1.0/Quill.yaml +29 -0
- package/quills/taro/0.1.0/assets/Figtree-Bold.ttf +0 -0
- package/quills/taro/0.1.0/assets/Figtree-Italic.ttf +0 -0
- package/quills/taro/0.1.0/assets/Figtree-Regular.ttf +0 -0
- package/quills/taro/0.1.0/example.md +27 -0
- package/quills/taro/0.1.0/plate.typ +31 -0
- package/quills/usaf_memo/0.1.0/.quillignore +31 -0
- package/quills/usaf_memo/0.1.0/Quill.yaml +209 -0
- package/quills/usaf_memo/0.1.0/assets/dow_seal.jpg +0 -0
- package/quills/usaf_memo/0.1.0/example.md +55 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/body.typ +325 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/config.typ +63 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +272 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/utils.typ +377 -0
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/typst.toml +17 -0
- package/quills/usaf_memo/0.1.0/plate.typ +74 -0
- package/quills/usaf_memo/0.2.0/.quillignore +31 -0
- package/quills/usaf_memo/0.2.0/Quill.yaml +225 -0
- package/quills/usaf_memo/0.2.0/assets/dow_seal.jpg +0 -0
- package/quills/usaf_memo/0.2.0/example.md +57 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +325 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/config.typ +63 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +272 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +377 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +17 -0
- package/quills/usaf_memo/0.2.0/plate.typ +76 -0
- package/templates/cmu_letter_template.md +38 -0
- package/templates/loc.md +79 -0
- package/templates/pass_request.md +44 -0
- package/templates/rebuttal.md +56 -0
- package/templates/taro.md +27 -0
- package/templates/templates.json +44 -0
- package/templates/usaf_template.md +23 -0
- 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
|