@airmark/quiver 0.28.0 → 0.30.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 (37) hide show
  1. package/README.md +3 -2
  2. package/package.json +3 -3
  3. package/quills/daf4392/0.1.0/Quill.yaml +2 -2
  4. package/quills/usaf_memo/0.2.0/Quill.yaml +14 -11
  5. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +37 -15
  6. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +5 -6
  7. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +5 -3
  8. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +87 -35
  9. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +34 -1
  10. package/quills/usaf_memo/0.3.0/Quill.yaml +316 -0
  11. package/quills/usaf_memo/0.3.0/assets/dod_seal.png +0 -0
  12. package/quills/usaf_memo/0.3.0/assets/dow_seal.png +0 -0
  13. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/.gitignore +7 -0
  14. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -0
  15. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel[wght].ttf +0 -0
  16. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -0
  17. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Bold.otf +0 -0
  18. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
  19. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -0
  20. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/LiberationMono/LICENSE +102 -0
  21. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/LiberationMono/LiberationMono-Regular.ttf +0 -0
  22. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
  23. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
  24. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
  25. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
  26. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
  27. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +29 -0
  28. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +364 -0
  29. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/config.typ +78 -0
  30. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +242 -0
  31. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +170 -0
  32. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/lib.typ +57 -0
  33. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +27 -0
  34. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +462 -0
  35. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +419 -0
  36. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/typst.toml +27 -0
  37. package/quills/usaf_memo/0.3.0/plate.typ +127 -0
@@ -0,0 +1,364 @@
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, 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
+ let heading_level = 0
203
+ // Filter out zero-width paragraphs so that an empty body (e.g. empty
204
+ // string from a KIND with no text) emits nothing and collapses to zero
205
+ // vertical space. Tables are always kept regardless of measured width.
206
+ let items = PAR_BUFFER.get().filter(item =>
207
+ item.kind == "table" or measure(item.content).width > 0pt
208
+ )
209
+ if items.len() == 0 { return }
210
+ // Only top-level paragraphs count for AFH 33-337 §2 numbering purposes
211
+ let par_count = items.filter(item => item.kind == "par").len()
212
+ let total_count = items.len()
213
+
214
+ // Track paragraph numbers per level manually to avoid nested-context
215
+ // counter propagation issues. Dictionary maps level index (as string)
216
+ // to the current counter value at that level.
217
+ let max-levels = paragraph-config.numbering-formats.len()
218
+ let level-counts = (:)
219
+ for lvl in range(max-levels) {
220
+ level-counts.insert(str(lvl), 1)
221
+ }
222
+
223
+ let i = 0
224
+ let any_emitted = false
225
+ for item in items {
226
+ i += 1
227
+ let kind = item.kind
228
+ let item_content = item.content
229
+
230
+ // A buffered heading runs into this element only when the two belong to
231
+ // the same item: a later block of this list item ("continuation"), or,
232
+ // at top level, the next paragraph. The first block of the *next* item,
233
+ // a table (nest_level −1, so the level test alone excludes it), and
234
+ // another heading all fail the test, and each would otherwise carry the
235
+ // heading's text somewhere it was not authored. Those emit the heading
236
+ // on its own line, the treatment a heading before a table takes.
237
+ if heading_buffer != none {
238
+ let runs_in = (
239
+ kind != "heading"
240
+ and item.nest_level == heading_level
241
+ and (heading_level == 0 or kind == "continuation")
242
+ )
243
+ if runs_in {
244
+ item_content = [#strong[#heading_buffer.] #item_content]
245
+ } else {
246
+ if any_emitted { blank-line() }
247
+ strong[#heading_buffer.]
248
+ any_emitted = true
249
+ }
250
+ heading_buffer = none
251
+ }
252
+
253
+ // Buffer this heading for the next element to run into. The level it was
254
+ // captured at rides along: the run-in test needs it, and the buffer is
255
+ // read one iteration later, by which time `item` is the *next* element.
256
+ if kind == "heading" {
257
+ heading_buffer = item_content
258
+ heading_level = item.nest_level
259
+ continue
260
+ }
261
+
262
+ // Format based on element kind
263
+ let nest_level = item.nest_level
264
+ let indent-fn = if memo-style == "daf" {
265
+ (level, _counts) => calculate-daf-indent(level)
266
+ } else {
267
+ (level, counts) => calculate-indent-from-counts(level, counts)
268
+ }
269
+ let final_par = {
270
+ if kind == "table" {
271
+ render-memo-table(item_content)
272
+ } else if kind == "continuation" {
273
+ // Continuation block within a multi-block list item:
274
+ // indent to align with preceding numbered paragraph's text, no new number.
275
+ // level-counts still holds the value of the preceding numbered paragraph.
276
+ if memo-style == "daf" and nest_level == 0 {
277
+ item_content
278
+ } else {
279
+ format-par(item_content, nest_level, level-counts, indent-fn, continuation: true)
280
+ }
281
+ } else if memo-style == "daf" {
282
+ if nest_level > 0 {
283
+ let par = format-par(item_content, nest_level, level-counts, indent-fn)
284
+ level-counts.insert(str(nest_level), level-counts.at(str(nest_level), default: 1) + 1)
285
+ level-counts = reset-levels-from(level-counts, nest_level + 1, max-levels)
286
+ par
287
+ } else {
288
+ // DAF top-level paragraphs are unnumbered and first-line indented.
289
+ // Reset nested counters so each new top-level paragraph restarts children.
290
+ level-counts = reset-levels-from(level-counts, 0, max-levels)
291
+ [#h(daf-paragraph.top-first-line-indent)#item_content]
292
+ }
293
+ } else if par_count > 1 {
294
+ // Apply paragraph numbering per AFH 33-337 §2
295
+ let par = format-par(item_content, nest_level, level-counts, indent-fn)
296
+ level-counts.insert(str(nest_level), level-counts.at(str(nest_level)) + 1)
297
+ level-counts = reset-levels-from(level-counts, nest_level + 1, max-levels)
298
+ par
299
+ } else {
300
+ // AFH 33-337 §2: "A single paragraph is not numbered"
301
+ item_content
302
+ }
303
+ }
304
+
305
+ // Blank line between paragraphs. The header→body gap (i.e. before
306
+ // the first emitted paragraph) is the caller's responsibility —
307
+ // emitting it here would put the v() inside a nested
308
+ // `context { for … }` block, where it does not combine with the
309
+ // preceding header section's block-spacing the same way as a
310
+ // top-level blank-line() call.
311
+ // AFH 33-337 §11 rule applied below: "Avoid dividing a paragraph of
312
+ // less than four lines between two pages"
313
+ if any_emitted { blank-line() }
314
+ any_emitted = true
315
+ if i == total_count {
316
+ let available_width = page.width - spacing.margin * 2
317
+
318
+ // Use the shared measured line stride used by blank-line spacing.
319
+ let line_height = {
320
+ let cached = LINE_STRIDE.get()
321
+ if cached != none {
322
+ cached
323
+ } else {
324
+ let one-line = measure(par(spacing: 0pt)[x]).height
325
+ measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
326
+ }
327
+ }
328
+ // Calculate last item's height
329
+ let par_height = measure(final_par, width: available_width).height
330
+
331
+ let estimated_lines = calc.ceil(par_height / line_height)
332
+
333
+ if estimated_lines < 4 {
334
+ // Short content (< 4 lines): make sticky to keep with signature
335
+ block(sticky: true)[#final_par]
336
+ } else {
337
+ // Longer content (≥ 4 lines): use default breaking behavior
338
+ block(breakable: true)[#final_par]
339
+ }
340
+ } else {
341
+ // Wrap every non-last emission in a plain block so the document-wide
342
+ // `set block(above: spacing.line)` rule contributes the same 0.5em
343
+ // gap above every paragraph, including the first (matching the
344
+ // single-paragraph case which always passes through the
345
+ // `i == total_count` branch) and middle paragraphs (so the gap
346
+ // between the 1st and 2nd doesn't shrink relative to the gap
347
+ // between the 2nd-to-last and last). Bare emission would skip
348
+ // `block.above` entirely and visibly compress the spacing.
349
+ block[#final_par]
350
+ }
351
+ }
352
+
353
+ // A heading with nothing after it never ran into anything, and the buffer
354
+ // dies with the loop unless it is drained here. Sticky for the same reason
355
+ // the last item is: a standalone heading is one line, so it keeps with the
356
+ // signature block rather than opening a page alone.
357
+ if heading_buffer != none {
358
+ if any_emitted { blank-line() }
359
+ block(sticky: true)[#strong[#heading_buffer.]]
360
+ }
361
+ }
362
+ }
363
+
364
+
@@ -0,0 +1,78 @@
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 (`par.leading`; gap between line boxes)
13
+ tab: 0.5in, // Tab stop for multi-column recipient alignment
14
+ margin: 1in, // AFH 33-337 §4: "Use 1-inch margins on the left, right and bottom"
15
+ )
16
+
17
+ // =============================================================================
18
+ // TYPOGRAPHY DEFAULTS
19
+ // =============================================================================
20
+ // AFH 33-337 §5: "Use 12 point Times New Roman font for text"
21
+
22
+ #let DEFAULT_LETTERHEAD_FONTS = ("Copperplate CC", "NimbusRomNo9L")
23
+ #let DEFAULT_BODY_FONTS = ("NimbusRomNo9L",) // AFH 33-337 §5: Times New Roman required; NimbusRomNo9L is a metric-compatible clone
24
+ // Static monospace face for raw/code text. Liberation Mono is metric-compatible with
25
+ // Courier New; only the regular weight is bundled (no bold or italic variants).
26
+ #let DEFAULT_MONO_FONTS = ("Liberation Mono",)
27
+ #let LETTERHEAD_COLOR = rgb("#355e93") // Faded USAF blue for letterhead
28
+
29
+ // =============================================================================
30
+ // PARAGRAPH CONFIGURATION
31
+ // =============================================================================
32
+ // AFH 33-337 "The Text of the Official Memorandum" §2:
33
+ // "Number and letter each paragraph and subparagraph"
34
+ // Hierarchical numbering: 1., a., (1), (a), etc.
35
+
36
+ #let paragraph-config = (
37
+ counter-prefix: "par-counter-",
38
+ // AFH 33-337 §2: Hierarchical paragraph numbering format
39
+ // Level 0: 1., 2., 3. | Level 1: a., b., c. | Level 2: (1), (2), (3) | Level 3: (a), (b), (c)
40
+ numbering-formats: ("1.", "a.", "(1)", "(a)", n => underline(str(n)), n => underline(str(n))),
41
+ )
42
+
43
+ // DAF (Headquarters) memo body: first-line indent for unnumbered paragraphs; nested
44
+ // items start at 1in, then +0.5in per additional nesting depth.
45
+ #let daf-paragraph = (
46
+ top-first-line-indent: 0.5in,
47
+ nested-first-level-indent: 1in,
48
+ nested-step: 0.5in,
49
+ )
50
+
51
+ // =============================================================================
52
+ // COUNTERS
53
+ // =============================================================================
54
+
55
+ #let counters = (
56
+ indorsement: counter("indorsement"),
57
+ )
58
+
59
+ // =============================================================================
60
+ // CLASSIFICATION COLORS
61
+ // =============================================================================
62
+ // AFH 33-337 §3: "Follow AFI 31-401, Information Security Program Management,
63
+ // applicable executive orders and DoD guidance for the necessary markings on
64
+ // classified correspondence."
65
+ // Color values follow DoD/CAPCO standard classification marking colors:
66
+ // - TOP SECRET: orange (#FF671F)
67
+ // - SECRET: red (#C8102E)
68
+ // - CONFIDENTIAL: black (per project style guide)
69
+ // - CUI: black (DoDM 5200.48; per project style guide)
70
+ // - UNCLASSIFIED: green (#007A33)
71
+
72
+ #let CLASSIFICATION_COLORS = (
73
+ "TOP SECRET": rgb(255, 103, 31),
74
+ "SECRET": rgb(200, 16, 46),
75
+ "CONFIDENTIAL": rgb(0, 0, 0),
76
+ "CUI": rgb(0, 0, 0),
77
+ "UNCLASSIFIED": rgb(0, 122, 51),
78
+ )
@@ -0,0 +1,242 @@
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_emblem: none, // optional image placed opposite the seal (right side)
23
+ letterhead_emblem_height: 1in, // emblem fit-box height; reduce for shorter emblems
24
+ letterhead_font: DEFAULT_LETTERHEAD_FONTS,
25
+ body_font: DEFAULT_BODY_FONTS,
26
+ font_size: 12pt,
27
+ memo_for_cols: 3,
28
+ classification_level: none,
29
+ dissemination: none,
30
+ cui_controlled_by: none,
31
+ cui_category: none,
32
+ cui_limited_dissemination: none,
33
+ cui_poc: none,
34
+ footer_tag_line: none,
35
+ memo_style: "usaf",
36
+ it,
37
+ ) = {
38
+ assert(subject != none, message: "subject is required")
39
+ assert(memo_for != none, message: "memo_for is required")
40
+ assert(
41
+ memo_style in ("usaf", "daf"),
42
+ message: "memo_style must be \"usaf\" or \"daf\"",
43
+ )
44
+
45
+ let actual_date = if date == none { datetime.today() } else { date }
46
+
47
+ // The banner is `LEVEL` or `LEVEL//SUFFIX`. `classification_level` is an enum
48
+ // (a `str`), but `dissemination` is a `plaintext` field and so arrives as
49
+ // content, which `str + str` cannot absorb — the marking is assembled as
50
+ // content instead, keeping the suffix's glyphs click-navigable on every page
51
+ // the banner repeats on.
52
+ let classification_marking = if classification_level == none or type(classification_level) != str {
53
+ none
54
+ } else {
55
+ let base = classification_level.trim()
56
+ if base == "" {
57
+ none
58
+ } else if falsey(dissemination) {
59
+ [#base]
60
+ } else {
61
+ // `//` is a line comment in Typst markup, so the separator is
62
+ // interpolated as a string rather than written literally. The suffix is
63
+ // boxed so the markup block's edge newlines do not read as a space and
64
+ // split the banner into `CUI// NF`.
65
+ let separator = "//"
66
+ [#base#separator#box(upper(dissemination))]
67
+ }
68
+ }
69
+ let classification_color = get-classification-level-color(classification_level)
70
+
71
+ // Build the CUI designation indicator block (DoDM 5200.48, Table 1), shown
72
+ // only for CUI when at least one indicator field is set. Rendered as a
73
+ // bottom-right page-1 float (see placement below).
74
+ let cui_indicator = if (
75
+ classification_level != none
76
+ and type(classification_level) == str
77
+ and classification_level.trim().starts-with("CUI")
78
+ ) {
79
+ // Each indicator is a `plaintext` field, so it arrives as content when set
80
+ // and as `""` when blank — `falsey` is the shape-agnostic presence test
81
+ // that the former `type(..) == str and ..trim() != ""` guard replaces.
82
+ let lines = ()
83
+ if not falsey(cui_controlled_by) {
84
+ lines.push([Controlled By: #cui_controlled_by])
85
+ }
86
+ if not falsey(cui_category) {
87
+ lines.push([CUI Category: #cui_category])
88
+ }
89
+ if not falsey(cui_limited_dissemination) {
90
+ lines.push([LDC: #upper(cui_limited_dissemination)])
91
+ }
92
+ if not falsey(cui_poc) {
93
+ lines.push([POC: #cui_poc])
94
+ }
95
+ if lines.len() > 0 { lines.join(linebreak()) } else { none }
96
+ } else {
97
+ none
98
+ }
99
+
100
+ // Document-wide typography settings (inlined from configure())
101
+ set par(leading: spacing.line, spacing: spacing.line, justify: false)
102
+ set block(above: spacing.line, below: 0em, spacing: 0em)
103
+ set text(font: body_font, size: font_size, fallback: true)
104
+ show raw: set text(font: DEFAULT_MONO_FONTS) // Static monospace face for inline code and code blocks
105
+
106
+ set page(
107
+ paper: "us-letter",
108
+ // AFH 33-337 §4: "Use 1-inch margins on the left, right and bottom"
109
+ margin: (
110
+ left: spacing.margin,
111
+ right: spacing.margin,
112
+ top: spacing.margin,
113
+ bottom: spacing.margin,
114
+ ),
115
+ header: {
116
+ // AFH 33-337 "Page numbering" §12: "The first page of a memorandum is never numbered.
117
+ // Number the succeeding pages starting with page 2. Place page numbers 0.5-inch from
118
+ // the top of the page, flush with the right margin."
119
+ context if counter(page).get().first() > 1 {
120
+ place(
121
+ dy: +.5in,
122
+ block(
123
+ width: 100%,
124
+ align(right, text(12pt)[#counter(page).display()]),
125
+ ),
126
+ )
127
+ }
128
+
129
+ if classification_marking != none {
130
+ place(
131
+ top + center,
132
+ dy: 0.375in,
133
+ text(12pt, font: DEFAULT_BODY_FONTS, fill: classification_color)[#strong(classification_marking)],
134
+ )
135
+ }
136
+ },
137
+ footer: {
138
+ if classification_marking != none {
139
+ place(
140
+ bottom + center,
141
+ dy: -.375in,
142
+ text(12pt, font: DEFAULT_BODY_FONTS, fill: classification_color)[#strong(classification_marking)],
143
+ )
144
+ }
145
+
146
+ if not falsey(footer_tag_line) {
147
+ place(
148
+ bottom + center,
149
+ dy: -0.625in,
150
+ align(center)[
151
+ #text(fill: LETTERHEAD_COLOR, font: "cinzel", size: 15pt)[#footer_tag_line]
152
+ ],
153
+ )
154
+ }
155
+ },
156
+ )
157
+
158
+ // DoDM 5200.48 §3: CUI designation indicator block — page 1 only, bottom-right
159
+ // corner, dropped into the 0.5in page-edge band. Emitted as a bottom float so
160
+ // it (1) reserves flow space, raising page 1's effective bottom margin so body
161
+ // text never overlaps it, and (2) stays pinned to page 1 — as the first flow
162
+ // content it can never be bumped to page 2.
163
+ if cui_indicator != none {
164
+ context {
165
+ // The box shrink-wraps to its widest line; `set align(left)` keeps the
166
+ // text flush-left within it, overriding the `align(right)` the placement
167
+ // below imposes.
168
+ let indicator_box = box({
169
+ set text(font: DEFAULT_BODY_FONTS, size: 10pt)
170
+ set par(leading: 0.4em, spacing: 0pt)
171
+ set align(left)
172
+ cui_indicator
173
+ })
174
+ // Reserve only the part of the block inside the text area (`reserved`):
175
+ // float a box of that height, then `place` the full block inside it pushed
176
+ // down by `overhang` so the surplus overflows into the edge band. (A bare
177
+ // `box(height: reserved, indicator_box)` overflows *upward* into the body
178
+ // instead.) The inner `place` adds no size, so the box stays `reserved`
179
+ // tall and the block's bottom lands 0.5in from the page edge.
180
+ let overhang = spacing.margin - 0.5in
181
+ let reserved = measure(indicator_box).height - overhang
182
+ place(
183
+ bottom + right,
184
+ float: true,
185
+ // Slide the right edge into the page-edge band, 0.5in from the border;
186
+ // the inner place right-aligns the block to that edge.
187
+ dx: spacing.margin - 0.5in,
188
+ // Minimum gap to the body's last line; the actual gap is larger when the
189
+ // next paragraph can't fit above the block and breaks to the next page.
190
+ clearance: spacing.line,
191
+ box(height: reserved, place(bottom + right, dy: overhang, indicator_box)),
192
+ )
193
+ }
194
+ }
195
+
196
+ render-letterhead(
197
+ letterhead_title,
198
+ letterhead_caption,
199
+ letterhead_font,
200
+ letterhead-seal: letterhead_seal,
201
+ letterhead-seal-subtitle: letterhead_seal_subtitle,
202
+ letterhead-emblem: letterhead_emblem,
203
+ letterhead-emblem-height: letterhead_emblem_height,
204
+ )
205
+
206
+ // AFH 33-337 "Date": "Place the date 1 inch from the right edge, 1.75 inches from the top"
207
+ // Since we have a 1-inch top margin, we need (1.75in - margin) vertical space
208
+ v(1.75in - spacing.margin)
209
+
210
+ // Measure and cache body line stride once for body line-count heuristics.
211
+ context {
212
+ let one-line = measure(par(spacing: 0pt)[x]).height
213
+ let line-stride = measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
214
+ LINE_STRIDE.update(line-stride)
215
+ }
216
+
217
+ [#metadata((
218
+ subject: subject,
219
+ original_date: actual_date,
220
+ original_from: first-or-value(memo_from),
221
+ body_font: body_font,
222
+ font_size: font_size,
223
+ memo_style: memo_style,
224
+ )) <usaf-memo-config>]
225
+
226
+ render-date-section(actual_date, memo-style: memo_style)
227
+ render-for-section(memo_for, memo_for_cols)
228
+ if not falsey(memo_from) { render-from-section(memo_from) }
229
+ // Blank entries are dropped first so a stub `- ` left under `references:`
230
+ // cannot pass as the lone reference and render an empty `()` after the subject.
231
+ let refs = compact-references(references)
232
+ let single-ref = if refs.len() == 1 { refs.at(0) } else { none }
233
+ render-subject-section(subject, inline-reference: single-ref)
234
+ render-references-section(refs)
235
+
236
+ // AFH 33-337: "Begin text on second line below subject/references".
237
+ // Emitted here (not inside body.typ) so the v() lands at the same lexical
238
+ // level as the preceding header sections and combines correctly with their
239
+ // block spacing.
240
+ blank-line()
241
+ it
242
+ }