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