@airmark/quiver 0.32.1 → 0.33.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 (76) hide show
  1. package/README.md +54 -18
  2. package/package.json +8 -6
  3. package/quills/af4141/0.1.0/Quill.yaml +1 -1
  4. package/quills/afmc_moa/0.0.1/Quill.yaml +10 -17
  5. package/quills/classic_resume/0.0.1/Quill.yaml +361 -0
  6. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/LICENSE +21 -0
  7. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Bold.ttf +0 -0
  8. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-BoldItalic.ttf +0 -0
  9. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Italic.ttf +0 -0
  10. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/EBGaramond-Regular.ttf +0 -0
  11. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/fonts/EBGaramond/OFL.txt +94 -0
  12. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/components.typ +181 -0
  13. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/config.typ +53 -0
  14. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/layout.typ +57 -0
  15. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/lib.typ +6 -0
  16. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/src/util.typ +32 -0
  17. package/quills/classic_resume/0.0.1/packages/ttq-classic-resume/typst.toml +26 -0
  18. package/quills/classic_resume/0.0.1/plate.typ +143 -0
  19. package/quills/cyber_ribbon_chart/0.0.1/Quill.yaml +320 -0
  20. package/quills/cyber_ribbon_chart/0.0.1/plate.typ +428 -0
  21. package/quills/daf1206/0.1.0/Quill.yaml +1 -2
  22. package/quills/daf4392/0.1.0/Quill.yaml +15 -1
  23. package/quills/daf4392/0.1.0/plate.typ +2 -2
  24. package/quills/usaf_letter/0.1.0/Quill.yaml +214 -0
  25. package/quills/usaf_letter/0.1.0/assets/dod_seal.png +0 -0
  26. package/quills/usaf_letter/0.1.0/assets/dow_seal.png +0 -0
  27. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/.gitignore +7 -0
  28. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/LICENSE +201 -0
  29. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
  30. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/Cinzel/LICENSE +93 -0
  31. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/CopperplateCC-Bold.otf +0 -0
  32. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
  33. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/CopperplateCC/LICENSE.md +79 -0
  34. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LICENSE +102 -0
  35. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/LiberationMono/LiberationMono-Regular.ttf +0 -0
  36. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
  37. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
  38. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
  39. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
  40. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
  41. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/backmatter.typ +32 -0
  42. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/config.typ +45 -0
  43. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/frontmatter.typ +226 -0
  44. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/lib.typ +35 -0
  45. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/mainmatter.typ +140 -0
  46. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/primitives.typ +544 -0
  47. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/src/utils.typ +334 -0
  48. package/quills/usaf_letter/0.1.0/packages/tonguetoquill-usaf-letter/typst.toml +22 -0
  49. package/quills/usaf_letter/0.1.0/plate.typ +124 -0
  50. package/quills/usaf_memo/0.2.0/Quill.yaml +1 -1
  51. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
  52. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +1 -1
  53. package/quills/usaf_memo/0.3.0/Quill.yaml +75 -56
  54. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/LICENSE +201 -21
  55. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
  56. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +29 -17
  57. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +97 -126
  58. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/config.typ +12 -45
  59. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +81 -87
  60. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +58 -66
  61. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/lib.typ +14 -36
  62. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +47 -19
  63. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +178 -121
  64. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +41 -141
  65. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/typst.toml +3 -8
  66. package/quills/usaf_memo/0.3.0/plate.typ +89 -85
  67. package/templates/af4141.md +23 -0
  68. package/templates/appointment_letter.md +58 -0
  69. package/templates/daf1206.md +23 -0
  70. package/templates/daf4392.md +34 -0
  71. package/templates/pass_request.md +48 -0
  72. package/templates/rebuttal.md +63 -0
  73. package/templates/templates.json +58 -0
  74. package/templates/usaf_template.md +30 -0
  75. package/templates/ussf_template.md +30 -0
  76. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel[wght].ttf +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,25 +264,43 @@
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
- #v(gap)
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
+ }
268
276
  #if signing-field != none {
269
277
  // The signing field covers those blank lines — where a signature is
270
- // actually written — so it is placed over the gap. With the gap inside
271
- // the block that is a downward offset from the block's own origin, and
272
- // the field therefore travels with the block onto whatever page it
273
- // lands on. The superseded form placed it at a NEGATIVE dy, reaching
274
- // above the block into space that belongs to the previous page: when
275
- // the block started at the top margin, the field was painted into the
276
- // header band over the page number and the classification banner, or
277
- // off the sheet entirely.
278
+ // actually written — so it is placed over the gap. It is placed BEFORE
279
+ // the gap is emitted: `place` anchors at the current flow position, so
280
+ // placing it after `v(gap)` anchors the box at the first name line and
281
+ // paints it down over the printed signature block. Anchored here it
282
+ // still travels with the block onto whatever page the block lands on,
283
+ // since the gap is inside the unbreakable block rather than ahead of
284
+ // it. The current position is below the closing line when there is
285
+ // one, which is where the signature is written.
286
+ //
287
+ // The widget keeps its own size (the helper's default is 50pt tall and
288
+ // it positions itself, so an `align` around it does nothing) and is
289
+ // offset to the BOTTOM of the gap: it always ends where the printed
290
+ // name begins, and whatever the gap has beyond the widget's height
291
+ // stays clear between the body text and the widget's frame.
292
+ let widget-height = {
293
+ let h = measure(signing-field).height
294
+ if h > 0pt { h } else { 50pt }
295
+ }
296
+ let drop = gap - widget-height - 3pt
278
297
  place(
279
298
  dx: left-pad,
280
- dy: 0pt,
281
- box(width: body-width - left-pad, height: gap, signing-field),
299
+ dy: if drop > 0pt { drop } else { 0pt },
300
+ box(width: body-width - left-pad, height: widget-height, signing-field),
282
301
  )
283
302
  }
303
+ #v(gap)
284
304
  #align(left)[
285
305
  #pad(left: left-pad)[
286
306
  #text(hyphenate: false)[
@@ -289,31 +309,26 @@
289
309
  // of the line above" — 2-character indent ≈ 1em in Times New Roman 12pt
290
310
  par(hanging-indent: .5em, line)
291
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)
292
317
  ]
293
318
  ]
294
319
  ]
295
320
  ]
321
+ // The reservation counted toward breaking height only; the backmatter
322
+ // emits its own lead-in below.
323
+ v(stride * -reserved-lines)
296
324
  }
297
325
  }
298
326
 
299
- // =============================================================================
300
- // ACTION LINE RENDERING
301
- // =============================================================================
302
- // Renders the decision line for indorsement memos — an "either / or" pair
303
- // where the endorser's choice is circled and the rejected option struck out.
304
- //
305
- // Two option pairs are supported, reflecting the two roles an indorsement
306
- // plays in a coordination chain: coordinating officials Concur / Nonconcur,
307
- // while the final approval authority Approves / Disapproves.
308
- //
309
- // Which pair prints is not the endorser's to choose — it follows from the
310
- // role, so the caller supplies the role via `approval-authority` and the same
311
- // three `action` values (affirm, reject, or leave open) carry over both pairs.
312
- //
313
- // The "undecided" form renders both options plain, for printing a memo the
314
- // endorser marks by hand when signing.
315
- //
316
- // 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.
317
332
 
318
333
  // The option pair for each role, ordered (affirmative, negative).
319
334
  #let APPROVAL_OPTIONS = ("Approve", "Disapprove")
@@ -368,24 +383,16 @@
368
383
  }
369
384
  }
370
385
 
371
- // =============================================================================
372
- // TABLE RENDERING
373
- // =============================================================================
374
- // AFH 33-337 does not specify table formatting, so we follow the general
375
- // aesthetic principles of the standard: plain black borders, no decorative
376
- // fills, and the body font inherited throughout.
377
-
378
- /// Renders a table with USAF memorandum–consistent formatting.
386
+ /// Renders a table in the memorandum's style.
379
387
  ///
380
- /// Applies simple 0.5pt black cell borders and standard padding to any
381
- /// Typst `table` element, keeping the visual style clean and formal.
382
- /// 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.
383
392
  ///
384
393
  /// - it (content): The table element to style and render
385
394
  /// -> content
386
395
  #let render-memo-table(it) = {
387
- // AFH 33-337 does not specify table formatting, so we follow the general
388
- // aesthetic principles of the standard: bold headers for clarity.
389
396
  show table.cell.where(y: 0): set text(weight: "bold")
390
397
  set table(
391
398
  stroke: 0.5pt + black,
@@ -394,23 +401,18 @@
394
401
  it
395
402
  }
396
403
 
397
- // =============================================================================
398
- // BACKMATTER SECTIONS
399
- // =============================================================================
400
- // AFH 33-337 "Attachment or Attachments": "Place 'Attachment:' (for a single attachment)
401
- // or '# Attachments:' (for two or more attachments) at the left margin, on the third
402
- // line below the signature element"
403
- // AFH 33-337 "Courtesy Copy Element": "place 'cc:' flush with the left margin, on the
404
- // second line below the attachment element"
405
-
406
404
  #let render-backmatter-section(
407
405
  content,
408
406
  section-label,
409
407
  numbering-style: none,
410
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,
411
413
  ) = {
412
414
  let formatted-content = {
413
- // 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.
414
416
  text()[#section-label]
415
417
  linebreak()
416
418
  if numbering-style != none {
@@ -421,30 +423,66 @@
421
423
  }
422
424
  }
423
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
+
424
434
  context {
425
- let available-space = page.height - here().position().y - 1in
426
- if measure(formatted-content).height > available-space {
427
- // Attachments pass continuation-label ("… (listed on next page):" per AFH 33-337).
428
- // cc: and DISTRIBUTION: use a neutral default — "listed" applies to attachment lists only.
429
- let continuation-text = if continuation-label != none {
430
- text()[#continuation-label]
431
- } else {
432
- 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
433
454
  }
434
- continuation-text
435
- pagebreak()
436
455
  }
437
- 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)
438
470
  }
439
471
  }
440
472
 
441
473
  #let calculate-backmatter-spacing(is-first-section) = {
442
474
  context {
443
- let line_count = if is-first-section { 2 } else { 1 }
444
- blank-lines(line_count)
475
+ let line-count = if is-first-section { 2 } else { 1 }
476
+ blank-lines(line-count)
445
477
  }
446
478
  }
447
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
+
448
486
  #let render-backmatter-sections(
449
487
  attachments: none,
450
488
  cc: none,
@@ -461,27 +499,46 @@
461
499
  pagebreak(weak: true)
462
500
  }
463
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
+
464
506
  if attachments != none and attachments.len() > 0 {
465
- calculate-backmatter-spacing(true)
466
507
  let attachment-count = attachments.len()
467
508
  let section-label = if attachment-count == 1 { "Attachment:" } else { str(attachment-count) + " Attachments:" }
468
509
  let continuation-label = (
469
510
  (if attachment-count == 1 { "Attachment" } else { str(attachment-count) + " Attachments" })
470
511
  + " (listed on next page):"
471
512
  )
472
- // AFH 33-337: a single attachment is not numbered; numbering applies to two or more.
473
- let numbering-style = if attachment-count == 1 { none } else { "1." }
474
- 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
+ ))
475
520
  }
476
521
 
477
522
  if cc != none and cc.len() > 0 {
478
- calculate-backmatter-spacing(attachments == none or attachments.len() == 0)
479
- render-backmatter-section(cc, "cc:")
523
+ sections.push((content: cc, label: "cc:", numbering-style: none, continuation-label: none))
480
524
  }
481
525
 
482
526
  if distribution != none and distribution.len() > 0 {
483
- calculate-backmatter-spacing((attachments == none or attachments.len() == 0) and (cc == none or cc.len() == 0))
484
- 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
+ )
485
542
  }
486
543
  }
487
544