@airmark/quiver 0.32.2 → 0.34.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 (90) hide show
  1. package/README.md +63 -21
  2. package/package.json +9 -7
  3. package/quills/af4141/0.1.0/Quill.yaml +4 -13
  4. package/quills/af4141/0.1.0/example.md +53 -0
  5. package/quills/afmc_moa/0.0.1/Quill.yaml +39 -62
  6. package/quills/afmc_moa/0.0.1/example.md +114 -0
  7. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/config.typ +1 -1
  8. package/quills/classic_resume/0.0.1/Quill.yaml +281 -0
  9. package/quills/classic_resume/0.0.1/example.md +111 -0
  10. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/LICENSE +21 -0
  11. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Bold.ttf +0 -0
  12. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-BoldItalic.ttf +0 -0
  13. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Italic.ttf +0 -0
  14. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Regular.ttf +0 -0
  15. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/OFL.txt +94 -0
  16. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/components.typ +214 -0
  17. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/config.typ +61 -0
  18. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/layout.typ +86 -0
  19. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/lib.typ +7 -0
  20. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/util.typ +37 -0
  21. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/typst.toml +26 -0
  22. package/quills/classic_resume/0.0.1/plate.typ +143 -0
  23. package/quills/cyber_ribbon_chart/0.0.1/Quill.yaml +327 -0
  24. package/quills/cyber_ribbon_chart/0.0.1/example.md +55 -0
  25. package/quills/cyber_ribbon_chart/0.0.1/plate.typ +609 -0
  26. package/quills/daf1206/0.1.0/Quill.yaml +15 -18
  27. package/quills/daf1206/0.1.0/example.md +55 -0
  28. package/quills/daf4392/0.1.0/Quill.yaml +16 -7
  29. package/quills/daf4392/0.1.0/example.md +51 -0
  30. package/quills/daf4392/0.1.0/plate.typ +2 -2
  31. package/quills/usaf_letter/0.1.0/Quill.yaml +179 -0
  32. package/quills/usaf_letter/0.1.0/assets/dod_seal.png +0 -0
  33. package/quills/usaf_letter/0.1.0/assets/dow_seal.png +0 -0
  34. package/quills/usaf_letter/0.1.0/example.md +35 -0
  35. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/.gitignore +7 -0
  36. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/LICENSE +201 -0
  37. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/CopperplateCC-Bold.otf +0 -0
  38. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/LICENSE.md +79 -0
  39. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LICENSE +102 -0
  40. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LiberationMono-Regular.ttf +0 -0
  41. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
  42. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
  43. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
  44. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
  45. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
  46. package/quills/{usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE → usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/OFL.txt} +2 -2
  47. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/SpectralSC-Italic.ttf +0 -0
  48. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/SpectralSC/SpectralSC-Regular.ttf +0 -0
  49. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/backmatter.typ +32 -0
  50. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/config.typ +45 -0
  51. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/frontmatter.typ +226 -0
  52. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/lib.typ +35 -0
  53. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/mainmatter.typ +140 -0
  54. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/primitives.typ +544 -0
  55. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/utils.typ +334 -0
  56. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/typst.toml +22 -0
  57. package/quills/usaf_letter/0.1.0/plate.typ +121 -0
  58. package/quills/usaf_memo/0.2.0/Quill.yaml +8 -41
  59. package/quills/usaf_memo/0.2.0/example.md +45 -0
  60. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
  61. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +1 -1
  62. package/quills/usaf_memo/0.3.0/Quill.yaml +51 -69
  63. package/quills/usaf_memo/0.3.0/example.md +48 -0
  64. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
  65. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/OFL.txt +93 -0
  66. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/SpectralSC-Italic.ttf +0 -0
  67. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/SpectralSC/SpectralSC-Regular.ttf +0 -0
  68. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +29 -17
  69. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +97 -126
  70. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/config.typ +12 -45
  71. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +81 -87
  72. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +58 -66
  73. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/lib.typ +14 -36
  74. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +47 -19
  75. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +158 -111
  76. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +41 -141
  77. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/typst.toml +3 -8
  78. package/quills/usaf_memo/0.3.0/plate.typ +78 -85
  79. package/templates/af4141.md +23 -0
  80. package/templates/appointment_letter.md +58 -0
  81. package/templates/daf1206.md +23 -0
  82. package/templates/daf4392.md +34 -0
  83. package/templates/deros_extension.md +40 -0
  84. package/templates/pass_request.md +48 -0
  85. package/templates/rebuttal.md +63 -0
  86. package/templates/templates.json +65 -0
  87. package/templates/usaf_template.md +30 -0
  88. package/templates/ussf_template.md +30 -0
  89. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel[wght].ttf +0 -0
  90. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
@@ -1,19 +1,11 @@
1
- // primitives.typ: Reusable rendering primitives for USAF memorandum sections
2
- //
3
- // This module implements the visual rendering functions that produce AFH 33-337
4
- // compliant formatting for all sections of a USAF memorandum. Each function
5
- // corresponds to specific placement and formatting requirements from Chapter 14.
1
+ // One rendering function per section of the memorandum, each placing its
2
+ // section where AFH 33-337 Chapter 14 puts it.
6
3
 
7
4
  #import "config.typ": *
8
5
  #import "utils.typ": *
9
6
 
10
- // =============================================================================
11
- // LETTERHEAD RENDERING
12
- // =============================================================================
13
- // AFH 33-337 §1: "Use printed letterhead, computer-generated letterhead, or plain bond paper"
14
- // Letterhead placement is not explicitly specified in AFH 33-337, but follows
15
- // standard USAF memo formatting conventions
16
-
7
+ // AFH 33-337 does not specify letterhead placement; the geometry below follows
8
+ // standard USAF memo convention.
17
9
  #let render-letterhead(
18
10
  title,
19
11
  caption,
@@ -24,8 +16,8 @@
24
16
  letterhead-emblem-height: 1in, // emblem fit-box height; reduce for shorter emblems
25
17
  ) = {
26
18
  font = ensure-array(font)
27
- // `upper` lowers content as readily as `str`, so the letterhead's uppercasing
28
- // survives the title/caption arriving as `plaintext` markup blocks.
19
+ // `upper` takes content as readily as `str`, so the letterhead's uppercasing
20
+ // survives a title or caption handed over as markup.
29
21
  title = upper(join-lines(title))
30
22
  caption = upper(join-lines(caption))
31
23
 
@@ -78,8 +70,8 @@
78
70
  #fit-box(width: corner-width, height: band-height)[#letterhead-seal]
79
71
  ]
80
72
  } else {
81
- // Isolate seal column from document `font_size`: stack `em` spacing and subtitle
82
- // must not scale with body text (see frontmatter `set text(size: font_size)`).
73
+ // Isolate seal column from document `font-size`: stack `em` spacing and subtitle
74
+ // must not scale with body text (see frontmatter `set text(size: font-size)`).
83
75
  // Subtitle is wrapped in `box` so it stays on one line and may extend past
84
76
  // the seal's 2in column rather than wrapping.
85
77
  block[
@@ -115,15 +107,6 @@
115
107
  }
116
108
  }
117
109
 
118
- // =============================================================================
119
- // HEADER SECTIONS
120
- // =============================================================================
121
- // AFH 33-337 "The Heading Section" specifies exact placement and format for:
122
- // - Date: 1 inch from right edge, 1.75 inches from top
123
- // - MEMORANDUM FOR: Second line below date
124
- // - FROM: Second line below MEMORANDUM FOR
125
- // - SUBJECT: Second line below FROM
126
-
127
110
  // AFH 33-337 "Date": "Place the date 1 inch from the right edge, 1.75 inches from the top"
128
111
  #let render-date-section(date, memo-style: "usaf") = {
129
112
  align(right)[#display-date(date, memo-style: memo-style)]
@@ -160,12 +143,10 @@
160
143
 
161
144
  /// Whether a `references` entry carries no citation text.
162
145
  ///
163
- /// Blank entries reach the template routinely: a template author leaves a
164
- /// stub `- ` under `references:` for the user to fill in, and the quillmark
165
- /// helper hands back `""` for an unset or whitespace-only markdown field.
166
- /// Such an entry must not count as a reference — otherwise a lone blank one
167
- /// satisfies the "exactly one reference" test below and renders as an empty
168
- /// `()` after the subject.
146
+ /// Blank entries reach the template routinely — a stub entry left under
147
+ /// `references:` for the user to fill in. Such an entry must not count as a
148
+ /// reference: a lone blank one would otherwise satisfy the "exactly one
149
+ /// reference" test below and render as an empty `()` after the subject.
169
150
  ///
170
151
  /// - entry (any): A single `references` element
171
152
  /// -> bool
@@ -205,25 +186,48 @@
205
186
  blank-line()
206
187
  grid(
207
188
  columns: (auto, auto, 1fr),
208
- // Each entry is markdown-converted content; spread them as enum items
209
- // lettered "(a) (b) (c)" per AFH 33-337.
189
+ // Spread the entries as enum items lettered "(a) (b) (c)" per AFH 33-337.
210
190
  "References:", " ", enum(..references, numbering: "(a) ", body-indent: 0pt),
211
191
  )
212
192
  }
213
193
  }
214
194
 
215
- // =============================================================================
216
- // SIGNATURE BLOCK
217
- // =============================================================================
218
195
  // AFH 33-337 "Signature Block": "Start the signature block on the fifth line below
219
196
  // the last line of text and 4.5 inches from the left edge of the page or three
220
197
  // spaces to the right of page center"
221
198
  // AFH 33-337 "Do not place the signature element on a continuation page by itself"
222
199
  // AFH 33-337 long-name example: "Signature block adjusted to the left" when a
223
200
  // long name would otherwise exceed the right margin.
224
-
225
- #let render-signature-block(signature-lines, signature-blank-lines: 4, signing-field: none) = {
201
+ //
202
+ // A closing line may open the section above the block, and AFH 33-337 gives it
203
+ // one geometry across the two documents that have one: on the second line below
204
+ // the text at the block's own anchor, with the signature then five lines below
205
+ // the line rather than below the body. The memorandum's occupant is the
206
+ // authority line, "FOR THE COMMANDER"; the personal letter's is the
207
+ // complimentary close, "Sincerely", which the handbook bars the authority line
208
+ // from. Hence the neutral parameter, and hence case belongs to the caller — the
209
+ // authority line is uppercased, "Sincerely" is not.
210
+
211
+ /// The memorandum's occupant of that slot, uppercased per AFH 33-337. Blank is
212
+ /// no line: the handbook forbids one where the commander signs.
213
+ ///
214
+ /// - value (str | content | none): The authority line as authored
215
+ /// -> content | none
216
+ #let format-authority-line(value) = if falsey(value) { none } else { upper(value) }
217
+
218
+ #let render-signature-block(
219
+ signature-lines,
220
+ closing-line: none,
221
+ signature-blank-lines: 4,
222
+ signing-field: none,
223
+ // Lines of breaking height reserved below the block for the backmatter
224
+ // lead-in and its continuation note. Reclaimed immediately after, so it moves
225
+ // where the block may break and nothing else.
226
+ reserved-lines: 0,
227
+ ) = {
226
228
  signature-lines = ensure-array(signature-lines)
229
+ // Blank is no line, whichever occupant the caller passes.
230
+ if falsey(closing-line) { closing-line = none }
227
231
  // AFH 33-337 allows two equivalent anchors: 4.5in from the left edge, or three
228
232
  // spaces right of page center. On 8.5in stock these coincide (page center =
229
233
  // 4.25in; three TNR-12pt spaces ≈ 0.25in), so we use 4.5in as the canonical
@@ -231,19 +235,17 @@
231
235
  let default-pad = 4.5in - spacing.margin
232
236
  context {
233
237
  // Measure each line at its rendered settings to detect long-name overflow.
238
+ // The closing line shares the anchor, so it joins the measurement: the
239
+ // wider of the two decides the shift and they stay aligned.
234
240
  let body-width = page.width - 2 * spacing.margin
241
+ let anchored-lines = signature-lines
242
+ if closing-line != none { anchored-lines.push(closing-line) }
235
243
  let widest = 0pt
236
- for line in signature-lines {
244
+ for line in anchored-lines {
237
245
  let w = measure(text(hyphenate: false, line)).width
238
246
  if w > widest { widest = w }
239
247
  }
240
- let stride = {
241
- let s = LINE_STRIDE.get()
242
- if s == none {
243
- let one-line = measure(par(spacing: 0pt)[x]).height
244
- measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
245
- } else { s }
246
- }
248
+ let stride = line-stride()
247
249
  // If the widest line would overflow the right margin at the standard
248
250
  // anchor, shift the block left just enough to fit. Clamp at 0 so the
249
251
  // block never crosses the left margin.
@@ -262,8 +264,15 @@
262
264
  // and the block one indivisible unit. The total space above the block is
263
265
  // identical either way — `block.above` still contributes the same 0.5em —
264
266
  // but a gap left outside can be consumed at the foot of one page while
265
- // the block starts at the top margin of the next, which is also what put
266
- // the signing field off the page (see below).
267
+ // the block starts at the top margin of the next.
268
+ //
269
+ // The closing line takes one of those lines above it and the gap then
270
+ // measures from the line, not from the text. Inside the block, so no page
271
+ // break can put it on a different page from the signature.
272
+ #if closing-line != none {
273
+ v(stride)
274
+ pad(left: left-pad, text(hyphenate: false, closing-line))
275
+ }
267
276
  #if signing-field != none {
268
277
  // The signing field covers those blank lines — where a signature is
269
278
  // actually written — so it is placed over the gap. It is placed BEFORE
@@ -272,7 +281,8 @@
272
281
  // paints it down over the printed signature block. Anchored here it
273
282
  // still travels with the block onto whatever page the block lands on,
274
283
  // since the gap is inside the unbreakable block rather than ahead of
275
- // it.
284
+ // it. The current position is below the closing line when there is
285
+ // one, which is where the signature is written.
276
286
  //
277
287
  // The widget keeps its own size (the helper's default is 50pt tall and
278
288
  // it positions itself, so an `align` around it does nothing) and is
@@ -299,31 +309,26 @@
299
309
  // of the line above" — 2-character indent ≈ 1em in Times New Roman 12pt
300
310
  par(hanging-indent: .5em, line)
301
311
  }
312
+ // The page the backmatter compares its own against. Must stay
313
+ // inside the unbreakable block: a marker outside one travels with
314
+ // the section that moves.
315
+ #metadata(none)<usaf-memo-flow-anchor>
316
+ #v(stride * reserved-lines)
302
317
  ]
303
318
  ]
304
319
  ]
305
320
  ]
321
+ // The reservation counted toward breaking height only; the backmatter
322
+ // emits its own lead-in below.
323
+ v(stride * -reserved-lines)
306
324
  }
307
325
  }
308
326
 
309
- // =============================================================================
310
- // ACTION LINE RENDERING
311
- // =============================================================================
312
- // Renders the decision line for indorsement memos — an "either / or" pair
313
- // where the endorser's choice is circled and the rejected option struck out.
314
- //
315
- // Two option pairs are supported, reflecting the two roles an indorsement
316
- // plays in a coordination chain: coordinating officials Concur / Nonconcur,
317
- // while the final approval authority Approves / Disapproves.
318
- //
319
- // Which pair prints is not the endorser's to choose — it follows from the
320
- // role, so the caller supplies the role via `approval-authority` and the same
321
- // three `action` values (affirm, reject, or leave open) carry over both pairs.
322
- //
323
- // The "undecided" form renders both options plain, for printing a memo the
324
- // endorser marks by hand when signing.
325
- //
326
- // Empty/none suppression is handled by the caller before this is invoked.
327
+ // The indorsement decision line is an "either / or" pair. Which pair prints
328
+ // follows from the indorsement's role in the coordination chain rather than
329
+ // from the endorser's choice, so the caller supplies the role and the same
330
+ // three `action` values carry over both pairs. The caller also suppresses the
331
+ // line entirely; reaching here means one is wanted.
327
332
 
328
333
  // The option pair for each role, ordered (affirmative, negative).
329
334
  #let APPROVAL_OPTIONS = ("Approve", "Disapprove")
@@ -378,24 +383,16 @@
378
383
  }
379
384
  }
380
385
 
381
- // =============================================================================
382
- // TABLE RENDERING
383
- // =============================================================================
384
- // AFH 33-337 does not specify table formatting, so we follow the general
385
- // aesthetic principles of the standard: plain black borders, no decorative
386
- // fills, and the body font inherited throughout.
387
-
388
- /// Renders a table with USAF memorandum–consistent formatting.
386
+ /// Renders a table in the memorandum's style.
389
387
  ///
390
- /// Applies simple 0.5pt black cell borders and standard padding to any
391
- /// Typst `table` element, keeping the visual style clean and formal.
392
- /// Font and size are inherited from the surrounding body text.
388
+ /// AFH 33-337 does not specify table formatting, so this follows the general
389
+ /// aesthetic of the standard: plain black borders, a bold header row, no
390
+ /// decorative fills, and the body font and size inherited from the surrounding
391
+ /// text.
393
392
  ///
394
393
  /// - it (content): The table element to style and render
395
394
  /// -> content
396
395
  #let render-memo-table(it) = {
397
- // AFH 33-337 does not specify table formatting, so we follow the general
398
- // aesthetic principles of the standard: bold headers for clarity.
399
396
  show table.cell.where(y: 0): set text(weight: "bold")
400
397
  set table(
401
398
  stroke: 0.5pt + black,
@@ -404,23 +401,18 @@
404
401
  it
405
402
  }
406
403
 
407
- // =============================================================================
408
- // BACKMATTER SECTIONS
409
- // =============================================================================
410
- // AFH 33-337 "Attachment or Attachments": "Place 'Attachment:' (for a single attachment)
411
- // or '# Attachments:' (for two or more attachments) at the left margin, on the third
412
- // line below the signature element"
413
- // AFH 33-337 "Courtesy Copy Element": "place 'cc:' flush with the left margin, on the
414
- // second line below the attachment element"
415
-
416
404
  #let render-backmatter-section(
417
405
  content,
418
406
  section-label,
419
407
  numbering-style: none,
420
408
  continuation-label: none,
409
+ // Lines of breaking height reserved below this section for the lead-in and
410
+ // continuation note of the section that follows it. Reclaimed immediately
411
+ // after, so it moves where a break may fall and nothing else.
412
+ reserved-lines: 0,
421
413
  ) = {
422
414
  let formatted-content = {
423
- // Use text() wrapper to prevent section label from being treated as a paragraph
415
+ // `text()` keeps the label from being laid out as a paragraph of its own.
424
416
  text()[#section-label]
425
417
  linebreak()
426
418
  if numbering-style != none {
@@ -431,30 +423,66 @@
431
423
  }
432
424
  }
433
425
 
426
+ // Attachments pass continuation-label ("… (listed on next page):" per AFH 33-337).
427
+ // cc: and DISTRIBUTION: use a neutral default — "listed" applies to attachment lists only.
428
+ let continuation-text = if continuation-label != none {
429
+ text()[#continuation-label]
430
+ } else {
431
+ text()[#(section-label + " (continued on next page)")]
432
+ }
433
+
434
434
  context {
435
- let available-space = page.height - here().position().y - 1in
436
- if measure(formatted-content).height > available-space {
437
- // Attachments pass continuation-label ("… (listed on next page):" per AFH 33-337).
438
- // cc: and DISTRIBUTION: use a neutral default — "listed" applies to attachment lists only.
439
- let continuation-text = if continuation-label != none {
440
- text()[#continuation-label]
441
- } else {
442
- text()[#(section-label + " (continued on next page)")]
435
+ // `breakable: false` lets Typst's own breaker move the section to the next
436
+ // page as a unit; an explicit pagebreak here would feed this context's
437
+ // layout query back into its own input and the two would chase each other
438
+ // until layout gave up.
439
+ //
440
+ // The note is decided by observing where that breaker put the block. The
441
+ // nearest anchor above is the signature block or the preceding section — an
442
+ // element with real height that stays on the page this section departs.
443
+ //
444
+ // Do not reduce this to a `here()` reading. `here()` sits in the section and
445
+ // travels with it, so once the section has moved it reports the top of the
446
+ // page it moved to and every section measures as fitting. The room the note
447
+ // needs is reserved inside the anchor's own unbreakable block, so the
448
+ // decision can never move its own input.
449
+ let above = query(selector(<usaf-memo-flow-anchor>).before(here()))
450
+ let below = query(selector(<usaf-memo-flow-anchor>).after(here()))
451
+ if above.len() > 0 and below.len() > 0 {
452
+ if below.first().location().page() > above.last().location().page() {
453
+ continuation-text
443
454
  }
444
- continuation-text
445
- pagebreak()
446
455
  }
447
- formatted-content
456
+ // A section taller than a page cannot move as a unit, so it splits where
457
+ // the page ends rather than overflowing it, and anchors its last page too:
458
+ // the section below measures from where this one ends.
459
+ let reserve = line-stride() * reserved-lines
460
+ layout(region => {
461
+ let oversized = measure(formatted-content, width: region.width).height + reserve > region.height
462
+ block(breakable: oversized)[
463
+ #metadata(none)<usaf-memo-flow-anchor>
464
+ #formatted-content
465
+ #if oversized [#metadata(none)<usaf-memo-flow-anchor>]
466
+ #v(reserve)
467
+ ]
468
+ })
469
+ v(-reserve)
448
470
  }
449
471
  }
450
472
 
451
473
  #let calculate-backmatter-spacing(is-first-section) = {
452
474
  context {
453
- let line_count = if is-first-section { 2 } else { 1 }
454
- blank-lines(line_count)
475
+ let line-count = if is-first-section { 2 } else { 1 }
476
+ blank-lines(line-count)
455
477
  }
456
478
  }
457
479
 
480
+ // AFH 33-337 "Attachment or Attachments": "Place 'Attachment:' (for a single attachment)
481
+ // or '# Attachments:' (for two or more attachments) at the left margin, on the third
482
+ // line below the signature element"
483
+ // AFH 33-337 "Courtesy Copy Element": "place 'cc:' flush with the left margin, on the
484
+ // second line below the attachment element"
485
+
458
486
  #let render-backmatter-sections(
459
487
  attachments: none,
460
488
  cc: none,
@@ -471,27 +499,46 @@
471
499
  pagebreak(weak: true)
472
500
  }
473
501
 
502
+ // Collected first because a section's reservation depends on whether another
503
+ // follows it: with nothing below, no note can be owed.
504
+ let sections = ()
505
+
474
506
  if attachments != none and attachments.len() > 0 {
475
- calculate-backmatter-spacing(true)
476
507
  let attachment-count = attachments.len()
477
508
  let section-label = if attachment-count == 1 { "Attachment:" } else { str(attachment-count) + " Attachments:" }
478
509
  let continuation-label = (
479
510
  (if attachment-count == 1 { "Attachment" } else { str(attachment-count) + " Attachments" })
480
511
  + " (listed on next page):"
481
512
  )
482
- // AFH 33-337: a single attachment is not numbered; numbering applies to two or more.
483
- let numbering-style = if attachment-count == 1 { none } else { "1." }
484
- render-backmatter-section(attachments, section-label, numbering-style: numbering-style, continuation-label: continuation-label)
513
+ sections.push((
514
+ content: attachments,
515
+ label: section-label,
516
+ // AFH 33-337: a single attachment is not numbered; numbering applies to two or more.
517
+ numbering-style: if attachment-count == 1 { none } else { "1." },
518
+ continuation-label: continuation-label,
519
+ ))
485
520
  }
486
521
 
487
522
  if cc != none and cc.len() > 0 {
488
- calculate-backmatter-spacing(attachments == none or attachments.len() == 0)
489
- render-backmatter-section(cc, "cc:")
523
+ sections.push((content: cc, label: "cc:", numbering-style: none, continuation-label: none))
490
524
  }
491
525
 
492
526
  if distribution != none and distribution.len() > 0 {
493
- calculate-backmatter-spacing((attachments == none or attachments.len() == 0) and (cc == none or cc.len() == 0))
494
- render-backmatter-section(distribution, "DISTRIBUTION:")
527
+ sections.push((content: distribution, label: "DISTRIBUTION:", numbering-style: none, continuation-label: none))
528
+ }
529
+
530
+ for (index, section) in sections.enumerate() {
531
+ calculate-backmatter-spacing(index == 0)
532
+ // The next section's one blank line of lead-in, plus one for its note. The
533
+ // signature block reserves 3: the first section's lead-in is two lines.
534
+ let reserved-lines = if index + 1 < sections.len() { 2 } else { 0 }
535
+ render-backmatter-section(
536
+ section.content,
537
+ section.label,
538
+ numbering-style: section.numbering-style,
539
+ continuation-label: section.continuation-label,
540
+ reserved-lines: reserved-lines,
541
+ )
495
542
  }
496
543
  }
497
544