@airmark/quiver 0.28.0 → 0.29.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 +321 -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 +120 -0
@@ -0,0 +1,462 @@
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.
6
+
7
+ #import "config.typ": *
8
+ #import "utils.typ": *
9
+
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
+
17
+ #let render-letterhead(
18
+ title,
19
+ caption,
20
+ font,
21
+ letterhead-seal: none,
22
+ letterhead-seal-subtitle: none,
23
+ letterhead-emblem: none, // optional image placed opposite the seal (right side)
24
+ letterhead-emblem-height: 1in, // emblem fit-box height; reduce for shorter emblems
25
+ ) = {
26
+ 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.
29
+ title = upper(join-lines(title))
30
+ caption = upper(join-lines(caption))
31
+
32
+ // Letterhead corner geometry. The seal (left) and emblem (right) share one
33
+ // reference band so the corners stay in parity: both bleed `corner-overhang`
34
+ // past the page margin and center on the same axis (`band-center`). The
35
+ // emblem may be shorter than the band but stays centered on that axis.
36
+ let corner-overhang = 0.5in
37
+ let corner-width = 2in
38
+ let band-height = 1in // seal height; also the emblem's reference band
39
+ let band-top = -band-height / 2 // puts the band center at dy 0
40
+ let band-center = band-top + band-height / 2
41
+
42
+ // The centered title/caption is placed content (not width-constrained), so a
43
+ // long caption would otherwise render on one line and run underneath the seal.
44
+ // Bound the caption to the width that stays clear of the seal on both sides
45
+ // and auto-shrink it to fit: the seal reaches `band-height - corner-overhang`
46
+ // into the text area, so reserving that plus a gutter on each side keeps the
47
+ // centered caption from touching the seal. `layout` resolves the text-area
48
+ // width so the bound tracks the page/margins.
49
+ let caption-gutter = 0.25in
50
+ let caption-clear = (band-height - corner-overhang) + caption-gutter
51
+ place(
52
+ dy: 0.625in - spacing.margin,
53
+ box(
54
+ width: 100%,
55
+ fill: none,
56
+ stroke: none,
57
+ layout(size => place(
58
+ center + top,
59
+ align(center)[
60
+ #set text(12pt, font: font, fill: LETTERHEAD_COLOR, weight: "bold")
61
+ #title\
62
+ #v(1pt)
63
+ #if not falsey(caption) {
64
+ fit-to-width(
65
+ size.width - 2 * caption-clear,
66
+ alignment: center,
67
+ box(text(10.5pt)[#caption]),
68
+ )
69
+ }
70
+ ],
71
+ )),
72
+ ),
73
+ )
74
+
75
+ if letterhead-seal != none {
76
+ let seal-body = if falsey(letterhead-seal-subtitle) {
77
+ block[
78
+ #fit-box(width: corner-width, height: band-height)[#letterhead-seal]
79
+ ]
80
+ } 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)`).
83
+ // Subtitle is wrapped in `box` so it stays on one line and may extend past
84
+ // the seal's 2in column rather than wrapping.
85
+ block[
86
+ #set text(9pt, font: font, fill: LETTERHEAD_COLOR, weight: "bold")
87
+ // Spacing applies between positional stack children only, not one `[…]` body.
88
+ #stack(
89
+ spacing: 0.5em,
90
+ fit-box(width: corner-width, height: band-height)[#letterhead-seal],
91
+ box(upper(join-lines(letterhead-seal-subtitle))),
92
+ )
93
+ ]
94
+ }
95
+ place(
96
+ left + top,
97
+ dx: -corner-overhang,
98
+ dy: band-top,
99
+ seal-body,
100
+ )
101
+ }
102
+
103
+ if letterhead-emblem != none {
104
+ // Mirror the seal: same overhang and width, centered on the seal's band
105
+ // axis. Placing the emblem's top at `band-center - height/2` keeps its
106
+ // center on `band-center` for any (possibly shorter) emblem height.
107
+ place(
108
+ right + top,
109
+ dx: corner-overhang,
110
+ dy: band-center - letterhead-emblem-height / 2,
111
+ block[
112
+ #fit-box(width: corner-width, height: letterhead-emblem-height, alignment: right + horizon)[#letterhead-emblem]
113
+ ],
114
+ )
115
+ }
116
+ }
117
+
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
+ // AFH 33-337 "Date": "Place the date 1 inch from the right edge, 1.75 inches from the top"
128
+ #let render-date-section(date, memo-style: "usaf") = {
129
+ align(right)[#display-date(date, memo-style: memo-style)]
130
+ }
131
+
132
+ // AFH 33-337 "MEMORANDUM FOR": "Place 'MEMORANDUM FOR' on the second line below the date"
133
+ #let render-for-section(recipients, cols) = {
134
+ blank-line()
135
+ grid(
136
+ columns: (auto, auto, 1fr),
137
+ "MEMORANDUM FOR",
138
+ " ",
139
+ align(left)[
140
+ #if type(recipients) == array {
141
+ create-auto-grid(recipients.map(upper), column-gutter: spacing.tab, cols: cols)
142
+ } else {
143
+ upper(recipients)
144
+ }
145
+ ],
146
+ )
147
+ }
148
+
149
+ // AFH 33-337 "FROM:": "Place 'FROM:' in uppercase, flush with the left margin,
150
+ // on the second line below the last line of the MEMORANDUM FOR element"
151
+ #let render-from-section(from-info) = {
152
+ blank-line()
153
+ from-info = join-lines(from-info)
154
+
155
+ grid(
156
+ columns: (auto, auto, 1fr),
157
+ "FROM:", " ", align(left)[#from-info],
158
+ )
159
+ }
160
+
161
+ /// Whether a `references` entry carries no citation text.
162
+ ///
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.
169
+ ///
170
+ /// - entry (any): A single `references` element
171
+ /// -> bool
172
+ #let blank-reference(entry) = falsey(entry) or entry == []
173
+
174
+ /// Drops `references` entries that carry no citation text and normalizes the
175
+ /// result to an array, so blank placeholder entries neither render on their
176
+ /// own nor affect the inline-vs-block decision.
177
+ ///
178
+ /// - references (array | none): Raw reference entries
179
+ /// -> array
180
+ #let compact-references(references) = ensure-array(references).filter(entry => not blank-reference(entry))
181
+
182
+ // AFH 33-337 "SUBJECT:": "In all uppercase letters place 'SUBJECT:', flush with the
183
+ // left margin, on the second line below the last line of the FROM element"
184
+ #let render-subject-section(subject-text, inline-reference: none) = {
185
+ blank-line()
186
+ let content = if not blank-reference(inline-reference) {
187
+ // `subject-text` is a content field, so it is boxed for the same reason the
188
+ // inline reference is: a markup block's edge newlines would otherwise read
189
+ // as a space, doubling the one before the parenthesis.
190
+ [#box(subject-text) (#box(inline-reference))]
191
+ } else {
192
+ [#subject-text]
193
+ }
194
+ grid(
195
+ columns: (auto, auto, 1fr),
196
+ "SUBJECT:", " ", content,
197
+ )
198
+ }
199
+
200
+ // AFH 33-337: only render References block for two or more references.
201
+ // A single reference is rendered inline after the SUBJECT text instead.
202
+ #let render-references-section(references) = {
203
+ let references = compact-references(references)
204
+ if references.len() >= 2 {
205
+ blank-line()
206
+ grid(
207
+ 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.
210
+ "References:", " ", enum(..references, numbering: "(a) ", body-indent: 0pt),
211
+ )
212
+ }
213
+ }
214
+
215
+ // =============================================================================
216
+ // SIGNATURE BLOCK
217
+ // =============================================================================
218
+ // AFH 33-337 "Signature Block": "Start the signature block on the fifth line below
219
+ // the last line of text and 4.5 inches from the left edge of the page or three
220
+ // spaces to the right of page center"
221
+ // AFH 33-337 "Do not place the signature element on a continuation page by itself"
222
+ // AFH 33-337 long-name example: "Signature block adjusted to the left" when a
223
+ // long name would otherwise exceed the right margin.
224
+
225
+ #let render-signature-block(signature-lines, signature-blank-lines: 4, signing-field: none) = {
226
+ signature-lines = ensure-array(signature-lines)
227
+ // AFH 33-337: "fifth line below" = 4 blank lines between text and signature block.
228
+ // breakable: false discourages orphaning the signature block onto a page by itself.
229
+ blank-lines(signature-blank-lines)
230
+ // AFH 33-337 allows two equivalent anchors: 4.5in from the left edge, or three
231
+ // spaces right of page center. On 8.5in stock these coincide (page center =
232
+ // 4.25in; three TNR-12pt spaces ≈ 0.25in), so we use 4.5in as the canonical
233
+ // anchor. pad() is relative to the text area, hence (4.5in - margin).
234
+ let default-pad = 4.5in - spacing.margin
235
+ context {
236
+ // Measure each line at its rendered settings to detect long-name overflow.
237
+ let body-width = page.width - 2 * spacing.margin
238
+ let widest = 0pt
239
+ for line in signature-lines {
240
+ let w = measure(text(hyphenate: false, line)).width
241
+ if w > widest { widest = w }
242
+ }
243
+ // If the widest line would overflow the right margin at the standard
244
+ // anchor, shift the block left just enough to fit. Clamp at 0 so the
245
+ // block never crosses the left margin.
246
+ let available = body-width - default-pad
247
+ let left-pad = if widest > available {
248
+ let shifted = body-width - widest
249
+ if shifted < 0pt { 0pt } else { shifted }
250
+ } else {
251
+ default-pad
252
+ }
253
+ block(breakable: false)[
254
+ #if signing-field != none {
255
+ let stride = {
256
+ let s = LINE_STRIDE.get()
257
+ if s == none {
258
+ let one-line = measure(par(spacing: 0pt)[x]).height
259
+ measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
260
+ } else { s }
261
+ }
262
+ place(
263
+ dx: left-pad,
264
+ dy: -(stride * signature-blank-lines),
265
+ box(width: body-width - left-pad, height: stride * signature-blank-lines, signing-field),
266
+ )
267
+ }
268
+ #align(left)[
269
+ #pad(left: left-pad)[
270
+ #text(hyphenate: false)[
271
+ #for line in signature-lines {
272
+ // AFH 33-337: "indent the next line to begin under the third character
273
+ // of the line above" — 2-character indent ≈ 1em in Times New Roman 12pt
274
+ par(hanging-indent: .5em, line)
275
+ }
276
+ ]
277
+ ]
278
+ ]
279
+ ]
280
+ }
281
+ }
282
+
283
+ // =============================================================================
284
+ // ACTION LINE RENDERING
285
+ // =============================================================================
286
+ // Renders the decision line for indorsement memos — an "either / or" pair
287
+ // where the endorser's choice is circled and the rejected option struck out.
288
+ //
289
+ // Two option pairs are supported, reflecting the two roles an indorsement
290
+ // plays in a coordination chain: coordinating officials Concur / Nonconcur,
291
+ // while the final approval authority Approves / Disapproves.
292
+ //
293
+ // Each pair also has an "undecided" form that renders both options plain, for
294
+ // printing a memo the endorser circles by hand when signing.
295
+ //
296
+ // Empty/none suppression is handled by the caller before this is invoked.
297
+
298
+ // Maps each `action` value to the option pair it renders and the index of the
299
+ // selected option within that pair (`none` selects neither).
300
+ #let ACTION_FORMS = (
301
+ "undecided": (("Approve", "Disapprove"), none),
302
+ "approve": (("Approve", "Disapprove"), 0),
303
+ "disapprove": (("Approve", "Disapprove"), 1),
304
+ "undecided_concur": (("Concur", "Nonconcur"), none),
305
+ "concur": (("Concur", "Nonconcur"), 0),
306
+ "nonconcur": (("Concur", "Nonconcur"), 1),
307
+ )
308
+
309
+ #let render-action-line(action, trailing-blank-line: true) = {
310
+ assert(
311
+ action in ACTION_FORMS,
312
+ message: "action must be one of " + ACTION_FORMS.keys().map(k => "\"" + k + "\"").join(", "),
313
+ )
314
+ let (options, selected) = ACTION_FORMS.at(action)
315
+ // Circle the selected option using a box with rounded corners; strike the
316
+ // one it was chosen over. When neither is selected both render plain.
317
+ // Use baseline parameter to maintain vertical text alignment.
318
+ let render-option(index) = {
319
+ let option = options.at(index)
320
+ if selected == none {
321
+ option
322
+ } else if selected == index {
323
+ box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[#option]
324
+ } else {
325
+ strike(option)
326
+ }
327
+ }
328
+ // No leading blank-line: the caller (indorsement.typ) already emits the
329
+ // header→content gap once. The action line's `block(sticky: true)`
330
+ // additionally inherits `block.above: spacing.line` so the visual gap
331
+ // above matches the gap above a body's first paragraph.
332
+ //
333
+ // Keep the action line with the following content (body or signature block)
334
+ // using the same sticky-block pattern that body.typ applies to the last
335
+ // paragraph, per AFH 33-337 §11 orphan-prevention rules.
336
+ block(sticky: true)[#render-option(0) / #render-option(1)]
337
+ // Trailing blank-line places the body's first paragraph one line below
338
+ // the action, mirroring the gap above it. Suppressed when the body is
339
+ // empty so the signature block's own 4-line gap lands on AFH 33-337's
340
+ // "fifth line below the last line of text" anchor.
341
+ if trailing-blank-line {
342
+ blank-line()
343
+ }
344
+ }
345
+
346
+ // =============================================================================
347
+ // TABLE RENDERING
348
+ // =============================================================================
349
+ // AFH 33-337 does not specify table formatting, so we follow the general
350
+ // aesthetic principles of the standard: plain black borders, no decorative
351
+ // fills, and the body font inherited throughout.
352
+
353
+ /// Renders a table with USAF memorandum–consistent formatting.
354
+ ///
355
+ /// Applies simple 0.5pt black cell borders and standard padding to any
356
+ /// Typst `table` element, keeping the visual style clean and formal.
357
+ /// Font and size are inherited from the surrounding body text.
358
+ ///
359
+ /// - it (content): The table element to style and render
360
+ /// -> content
361
+ #let render-memo-table(it) = {
362
+ // AFH 33-337 does not specify table formatting, so we follow the general
363
+ // aesthetic principles of the standard: bold headers for clarity.
364
+ show table.cell.where(y: 0): set text(weight: "bold")
365
+ set table(
366
+ stroke: 0.5pt + black,
367
+ inset: (x: 0.5em, y: 0.4em),
368
+ )
369
+ it
370
+ }
371
+
372
+ // =============================================================================
373
+ // BACKMATTER SECTIONS
374
+ // =============================================================================
375
+ // AFH 33-337 "Attachment or Attachments": "Place 'Attachment:' (for a single attachment)
376
+ // or '# Attachments:' (for two or more attachments) at the left margin, on the third
377
+ // line below the signature element"
378
+ // AFH 33-337 "Courtesy Copy Element": "place 'cc:' flush with the left margin, on the
379
+ // second line below the attachment element"
380
+
381
+ #let render-backmatter-section(
382
+ content,
383
+ section-label,
384
+ numbering-style: none,
385
+ continuation-label: none,
386
+ ) = {
387
+ let formatted-content = {
388
+ // Use text() wrapper to prevent section label from being treated as a paragraph
389
+ text()[#section-label]
390
+ linebreak()
391
+ if numbering-style != none {
392
+ let items = ensure-array(content)
393
+ enum(..items, numbering: numbering-style)
394
+ } else {
395
+ join-lines(content)
396
+ }
397
+ }
398
+
399
+ context {
400
+ let available-space = page.height - here().position().y - 1in
401
+ if measure(formatted-content).height > available-space {
402
+ // Attachments pass continuation-label ("… (listed on next page):" per AFH 33-337).
403
+ // cc: and DISTRIBUTION: use a neutral default — "listed" applies to attachment lists only.
404
+ let continuation-text = if continuation-label != none {
405
+ text()[#continuation-label]
406
+ } else {
407
+ text()[#(section-label + " (continued on next page)")]
408
+ }
409
+ continuation-text
410
+ pagebreak()
411
+ }
412
+ formatted-content
413
+ }
414
+ }
415
+
416
+ #let calculate-backmatter-spacing(is-first-section) = {
417
+ context {
418
+ let line_count = if is-first-section { 2 } else { 1 }
419
+ blank-lines(line_count)
420
+ }
421
+ }
422
+
423
+ #let render-backmatter-sections(
424
+ attachments: none,
425
+ cc: none,
426
+ distribution: none,
427
+ leading-pagebreak: false,
428
+ ) = {
429
+ let has-backmatter = (
430
+ (attachments != none and attachments.len() > 0)
431
+ or (cc != none and cc.len() > 0)
432
+ or (distribution != none and distribution.len() > 0)
433
+ )
434
+
435
+ if leading-pagebreak and has-backmatter {
436
+ pagebreak(weak: true)
437
+ }
438
+
439
+ if attachments != none and attachments.len() > 0 {
440
+ calculate-backmatter-spacing(true)
441
+ let attachment-count = attachments.len()
442
+ let section-label = if attachment-count == 1 { "Attachment:" } else { str(attachment-count) + " Attachments:" }
443
+ let continuation-label = (
444
+ (if attachment-count == 1 { "Attachment" } else { str(attachment-count) + " Attachments" })
445
+ + " (listed on next page):"
446
+ )
447
+ // AFH 33-337: a single attachment is not numbered; numbering applies to two or more.
448
+ let numbering-style = if attachment-count == 1 { none } else { "1." }
449
+ render-backmatter-section(attachments, section-label, numbering-style: numbering-style, continuation-label: continuation-label)
450
+ }
451
+
452
+ if cc != none and cc.len() > 0 {
453
+ calculate-backmatter-spacing(attachments == none or attachments.len() == 0)
454
+ render-backmatter-section(cc, "cc:")
455
+ }
456
+
457
+ if distribution != none and distribution.len() > 0 {
458
+ calculate-backmatter-spacing((attachments == none or attachments.len() == 0) and (cc == none or cc.len() == 0))
459
+ render-backmatter-section(distribution, "DISTRIBUTION:")
460
+ }
461
+ }
462
+