@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
@@ -0,0 +1,226 @@
1
+ // The personal letter's heading elements, and the page setup the rest of the
2
+ // document inherits: AFH 33-337 "The Personal Letter".
3
+
4
+ #import "primitives.typ": *
5
+
6
+ #let frontmatter(
7
+ letter-for: none,
8
+ letter-from: none,
9
+ salutation: none,
10
+ date: none,
11
+ letterhead-title: "DEPARTMENT OF THE AIR FORCE",
12
+ letterhead-caption: "[YOUR SQUADRON/UNIT NAME]",
13
+ letterhead-seal: none,
14
+ letterhead-seal-subtitle: none, // optional line under seal (9pt bold caps); ignored if no seal
15
+ letterhead-emblem: none, // optional image placed opposite the seal (right side)
16
+ letterhead-emblem-height: 1in, // emblem fit-box height; reduce for shorter emblems
17
+ letterhead-font: DEFAULT_LETTERHEAD_FONTS,
18
+ body-font: DEFAULT_BODY_FONTS,
19
+ font-size: 12pt,
20
+ classification-level: none,
21
+ dissemination: none,
22
+ cui-controlled-by: none,
23
+ cui-category: none,
24
+ cui-limited-dissemination: none,
25
+ cui-poc: none,
26
+ footer-tag-line: none,
27
+ it,
28
+ ) = {
29
+ assert(letter-for != none, message: "letter-for is required")
30
+
31
+ let actual-date = if date == none { datetime.today() } else { date }
32
+
33
+ // The banner is `LEVEL` or `LEVEL//SUFFIX`. `classification-level` is an enum
34
+ // (a `str`), but `dissemination` may arrive as content, which `str + str`
35
+ // cannot absorb — so the marking is assembled as content instead.
36
+ let classification-marking = if classification-level == none or type(classification-level) != str {
37
+ none
38
+ } else {
39
+ let base = classification-level.trim()
40
+ if base == "" {
41
+ none
42
+ } else if falsey(dissemination) {
43
+ [#base]
44
+ } else {
45
+ // `//` is a line comment in Typst markup, so the separator is
46
+ // interpolated as a string rather than written literally. The suffix is
47
+ // boxed so the markup block's edge newlines do not read as a space and
48
+ // split the banner into `CUI// NF`.
49
+ let separator = "//"
50
+ [#base#separator#box(upper(dissemination))]
51
+ }
52
+ }
53
+ let classification-color = get-classification-level-color(classification-level)
54
+
55
+ // Build the CUI designation indicator block (DoDM 5200.48, Table 1), shown
56
+ // only for CUI when at least one indicator field is set. Rendered as a
57
+ // bottom-right page-1 float (see placement below).
58
+ let cui-indicator = if (
59
+ classification-level != none
60
+ and type(classification-level) == str
61
+ and classification-level.trim().starts-with("CUI")
62
+ ) {
63
+ // An indicator may arrive as content or as a `str`; `falsey` is the
64
+ // presence test that reads both shapes.
65
+ let lines = ()
66
+ if not falsey(cui-controlled-by) {
67
+ lines.push([Controlled By: #cui-controlled-by])
68
+ }
69
+ if not falsey(cui-category) {
70
+ lines.push([CUI Category: #cui-category])
71
+ }
72
+ if not falsey(cui-limited-dissemination) {
73
+ lines.push([LDC: #upper(cui-limited-dissemination)])
74
+ }
75
+ if not falsey(cui-poc) {
76
+ lines.push([POC: #cui-poc])
77
+ }
78
+ if lines.len() > 0 { lines.join(linebreak()) } else { none }
79
+ } else {
80
+ none
81
+ }
82
+
83
+ set par(leading: spacing.line, spacing: spacing.line, justify: false)
84
+ set block(above: spacing.line, below: 0em, spacing: 0em)
85
+ set text(font: body-font, size: font-size, fallback: true)
86
+ show raw: set text(font: DEFAULT_MONO_FONTS)
87
+
88
+ set page(
89
+ paper: "us-letter",
90
+ // AFH 33-337: 1-inch margins on the left, right and bottom, on every page,
91
+ // and on the top of every page but the first, whose text starts lower (the
92
+ // `v()` below the letterhead).
93
+ margin: (
94
+ left: spacing.margin,
95
+ right: spacing.margin,
96
+ top: spacing.margin,
97
+ bottom: spacing.margin,
98
+ ),
99
+ header: {
100
+ // AFH 33-337: the first page of a personal letter is never numbered.
101
+ // Succeeding pages are numbered from 2, flush with the right margin.
102
+ context if counter(page).get().first() > 1 {
103
+ place(
104
+ dy: +.5in,
105
+ block(
106
+ width: 100%,
107
+ align(right, text(12pt)[#counter(page).display()]),
108
+ ),
109
+ )
110
+ }
111
+
112
+ if classification-marking != none {
113
+ place(
114
+ top + center,
115
+ dy: 0.375in,
116
+ text(12pt, font: DEFAULT_BODY_FONTS, fill: classification-color)[#strong(classification-marking)],
117
+ )
118
+ }
119
+ },
120
+ footer: {
121
+ if classification-marking != none {
122
+ place(
123
+ bottom + center,
124
+ dy: -.375in,
125
+ text(12pt, font: DEFAULT_BODY_FONTS, fill: classification-color)[#strong(classification-marking)],
126
+ )
127
+ }
128
+
129
+ if not falsey(footer-tag-line) {
130
+ place(
131
+ bottom + center,
132
+ dy: -0.625in,
133
+ align(center)[
134
+ #text(fill: LETTERHEAD_COLOR, font: "Spectral SC", size: 15pt)[#footer-tag-line]
135
+ ],
136
+ )
137
+ }
138
+ },
139
+ )
140
+
141
+ // DoDM 5200.48 §3: CUI designation indicator block — page 1 only, bottom-right
142
+ // corner, dropped into the 0.5in page-edge band. Emitted as a bottom float so
143
+ // it (1) reserves flow space, raising page 1's effective bottom margin so body
144
+ // text never overlaps it, and (2) stays pinned to page 1 — as the first flow
145
+ // content it can never be bumped to page 2.
146
+ if cui-indicator != none {
147
+ context {
148
+ // The box shrink-wraps to its widest line; `set align(left)` keeps the
149
+ // text flush-left within it, overriding the `align(right)` the placement
150
+ // below imposes.
151
+ let indicator-box = box({
152
+ set text(font: DEFAULT_BODY_FONTS, size: 10pt)
153
+ set par(leading: 0.4em, spacing: 0pt)
154
+ set align(left)
155
+ cui-indicator
156
+ })
157
+ // Reserve only the part of the block inside the text area (`reserved`):
158
+ // float a box of that height, then `place` the full block inside it pushed
159
+ // down by `overhang` so the surplus overflows into the edge band. (A bare
160
+ // `box(height: reserved, indicator-box)` overflows *upward* into the body
161
+ // instead.) The inner `place` adds no size, so the box stays `reserved`
162
+ // tall and the block's bottom lands 0.5in from the page edge.
163
+ let overhang = spacing.margin - 0.5in
164
+ let reserved = measure(indicator-box).height - overhang
165
+ place(
166
+ bottom + right,
167
+ float: true,
168
+ // Slide the right edge into the page-edge band, 0.5in from the border;
169
+ // the inner place right-aligns the block to that edge.
170
+ dx: spacing.margin - 0.5in,
171
+ // Minimum gap to the body's last line; the actual gap is larger when the
172
+ // next paragraph can't fit above the block and breaks to the next page.
173
+ clearance: spacing.line,
174
+ box(height: reserved, place(bottom + right, dy: overhang, indicator-box)),
175
+ )
176
+ }
177
+ }
178
+
179
+ render-letterhead(
180
+ letterhead-title,
181
+ letterhead-caption,
182
+ letterhead-font,
183
+ letterhead-seal: letterhead-seal,
184
+ letterhead-seal-subtitle: letterhead-seal-subtitle,
185
+ letterhead-emblem: letterhead-emblem,
186
+ letterhead-emblem-height: letterhead-emblem-height,
187
+ )
188
+
189
+ // AFH 33-337: the date sits 1.75 inches from the top of the first page. The
190
+ // top margin is 1 inch, so the remainder is made up here.
191
+ v(1.75in - spacing.margin)
192
+
193
+ // Measure one line's stride once, under the typography just set, for the
194
+ // blank-line spacing every element below is laid out on.
195
+ context {
196
+ let one-line = measure(par(spacing: 0pt)[x]).height
197
+ LINE_STRIDE.update(measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line)
198
+ }
199
+
200
+ render-date-section(actual-date)
201
+
202
+ // AFH 33-337: the sender's block on the second line below the date, the
203
+ // receiver's on the third line below the sender's last line. With no sender's
204
+ // block the receiver's takes the line the sender's would have started on.
205
+ let has-sender = not falsey(letter-from)
206
+ if has-sender {
207
+ blank-line()
208
+ join-lines(letter-from)
209
+ }
210
+ blank-lines(if has-sender { 2 } else { 1 })
211
+ join-lines(letter-for)
212
+
213
+ // AFH 33-337: the salutation sits on the second line below the receiver's
214
+ // address, and takes no punctuation after the receiver's last name. A blank
215
+ // one prints nothing and takes no line, so the body opens on the second line
216
+ // below the receiver's address.
217
+ if not falsey(salutation) {
218
+ blank-line()
219
+ [#salutation]
220
+ }
221
+
222
+ // The body opens on the second line below the salutation, which `mainmatter`
223
+ // supplies: the gap above its first paragraph is the same blank line it puts
224
+ // between any two.
225
+ it
226
+ }
@@ -0,0 +1,35 @@
1
+ // Public API of the USAF personal letter template, which typesets per AFH
2
+ // 33-337 "The Tongue and Quill", "The Personal Letter".
3
+ //
4
+ // The three sections compose in document order: `frontmatter` and `mainmatter`
5
+ // as show rules, `backmatter` as a function.
6
+ //
7
+ // `config.typ`, `utils.typ` and `primitives.typ` are `tonguetoquill-usaf-memo`
8
+ // 5.0.0's, byte for byte, so the letterhead, signature block and closing
9
+ // elements the two documents share are set by one implementation and a sync is
10
+ // a copy. The other files are the letter's own.
11
+ //
12
+ // #import "@preview/tonguetoquill-usaf-letter:0.1.0": (
13
+ // backmatter, frontmatter, mainmatter,
14
+ // )
15
+ //
16
+ // #show: frontmatter.with(
17
+ // letter-from: ("Colonel Jane A. Doe", "Commander", "123d Example Squadron",
18
+ // "1 Example Way", "Placeholder AFB ST 12345-6789"),
19
+ // letter-for: ("Major John B. Smith", "Chief, Plans and Programs",
20
+ // "456th Example Group", "2 Sample Blvd", "Sample AFB ST 98765-4321"),
21
+ // salutation: "Dear Major Smith",
22
+ // )
23
+ // #show: mainmatter
24
+ //
25
+ // Body paragraphs.
26
+ //
27
+ // #backmatter(
28
+ // complimentary-close: "Sincerely",
29
+ // signature-block: ("FIRST M. LAST, Rank, USAF",),
30
+ // )
31
+
32
+ #import "frontmatter.typ": frontmatter
33
+ #import "mainmatter.typ": mainmatter
34
+ #import "backmatter.typ": backmatter
35
+ #import "utils.typ": date-pattern
@@ -0,0 +1,140 @@
1
+ // The personal letter's body: AFH 33-337 "For the body of the personal letter,
2
+ // the paragraphs are indented 0.5 inches from the left margin but they are not
3
+ // numbered."
4
+
5
+ #import "config.typ": *
6
+ #import "utils.typ": *
7
+ #import "primitives.typ": render-memo-table
8
+
9
+ // AFH 33-337: a personal letter's paragraphs are indented half an inch from the
10
+ // left margin and carry no number or letter.
11
+ #let paragraph-indent = 0.5in
12
+
13
+ /// Splits content at the closing section: the part this styles as body
14
+ /// paragraphs, and the part that reaches the page untouched.
15
+ ///
16
+ /// Applied as a show rule, `mainmatter` is handed the closing section along
17
+ /// with the body, and the body's first-line indent would then displace the
18
+ /// complimentary close and the signature lines. `backmatter` labels its output
19
+ /// to mark the boundary.
20
+ ///
21
+ /// Everything from the marker on stays together, prose a caller wrote after the
22
+ /// closing included.
23
+ ///
24
+ /// A marker is found on `it` itself, on a direct child, or inside the `styled`
25
+ /// element that a `set` or `show` rule after `#show: mainmatter` wraps the
26
+ /// remainder in; both halves come back under those styles. A marker a caller
27
+ /// nested in a container of their own — a `block`, a `grid` cell — is not
28
+ /// found. A closing section built in a code block or a loop is not nested:
29
+ /// joining content extends the sequence on the left in place, so its marker
30
+ /// stays a direct child.
31
+ ///
32
+ /// - it (content): Content handed to `mainmatter`
33
+ /// -> array: the body content and the closing content
34
+ #let split-closing(it) = {
35
+ if it.at("label", default: none) == <usaf-letter-closing> { return ([], it) }
36
+ // `styled` is not a name in scope; its two fields identify it.
37
+ if it.has("child") and it.has("styles") {
38
+ let (body, closing) = split-closing(it.child)
39
+ let restyle = part => (it.func())(part, it.styles)
40
+ return (restyle(body), restyle(closing))
41
+ }
42
+ if not it.has("children") { return (it, []) }
43
+ let children = it.children
44
+ let boundary = children.position(child => child.at("label", default: none) == <usaf-letter-closing>)
45
+ if boundary == none { return (it, []) }
46
+ // `sum` over `join`: an empty half is `none` from a join and `[]` from this.
47
+ (children.slice(0, boundary).sum(default: []), children.slice(boundary).sum(default: []))
48
+ }
49
+
50
+ // The body's blocks in order — its own paragraphs, and each list, table and
51
+ // block quote whole — so the last can be found; and how deep in one of those
52
+ // containers the current paragraph sits.
53
+ #let BLOCKS = counter("usaf-letter-blocks")
54
+ #let NESTED = state("usaf-letter-nested", 0)
55
+
56
+ /// Whether a body's last block holds the signature to its page: a sticky block
57
+ /// relocates whole rather than splitting, so one taller than a third of the
58
+ /// text height is left to the break, which leaves its own tail above the
59
+ /// signature.
60
+ ///
61
+ /// - it (content): The last block
62
+ /// -> bool
63
+ #let keeps-signature(it) = {
64
+ let budget = (page.height - spacing.margin * 2) / 3
65
+ measure(it, width: page.width - spacing.margin * 2).height <= budget
66
+ }
67
+
68
+ /// Counts a container as one block of the body and the paragraphs inside it
69
+ /// as none, and sets it a blank line off the paragraphs around it.
70
+ ///
71
+ /// - it (content): The container
72
+ /// -> content
73
+ #let container(it) = context {
74
+ if NESTED.get() > 0 { return it }
75
+ BLOCKS.step()
76
+ NESTED.update(n => n + 1)
77
+ context {
78
+ let last = BLOCKS.get() == BLOCKS.final()
79
+ block(above: par.spacing, sticky: last and keeps-signature(it), it)
80
+ }
81
+ NESTED.update(n => n - 1)
82
+ }
83
+
84
+ /// Show rule for the personal letter's body.
85
+ ///
86
+ /// - it (content): Body content
87
+ /// -> content
88
+ #let mainmatter(it) = {
89
+ let (body, closing) = split-closing(it)
90
+ context {
91
+ // `par.leading` and `par.spacing` are both half an em, so a paragraph break
92
+ // would otherwise land on the page as an ordinary line break. A blank line
93
+ // is what separates one letter paragraph from the next.
94
+ set par(
95
+ first-line-indent: (amount: paragraph-indent, all: true),
96
+ spacing: spacing.line + line-stride(),
97
+ )
98
+ // The indent belongs to the letter's own paragraphs. Inside a list, a
99
+ // quote, or a table cell, a line already sits where its container put it.
100
+ show list: it => {
101
+ set par(first-line-indent: 0pt)
102
+ container(it)
103
+ }
104
+ show enum: it => {
105
+ set par(first-line-indent: 0pt)
106
+ container(it)
107
+ }
108
+ show table: it => {
109
+ set par(first-line-indent: 0pt)
110
+ container(render-memo-table(it))
111
+ }
112
+ // A block quote is the body's unindented block: the author's lines as
113
+ // written, flush with the margin, which is what lets a letter carry a
114
+ // roster of names or an address without the paragraph indent claiming its
115
+ // first line.
116
+ show quote.where(block: true): it => {
117
+ set par(first-line-indent: 0pt)
118
+ container(it.body)
119
+ }
120
+ show raw.where(block: true): container
121
+ // AFH 33-337: "Do not place the signature element on a continuation page
122
+ // by itself." The signature block has no keep-with-previous of its own —
123
+ // Typst has no such property — so the anchor comes from this side: the
124
+ // body's last block, a paragraph or a container, is set sticky, carried
125
+ // onto the next page along with the signature instead of breaking away
126
+ // from it.
127
+ show par: p => context {
128
+ if NESTED.get() > 0 { return p }
129
+ BLOCKS.step()
130
+ context {
131
+ if BLOCKS.get() != BLOCKS.final() { return p }
132
+ // `above` stands in for the paragraph spacing a bare paragraph would
133
+ // have taken from the one before it.
134
+ block(sticky: keeps-signature(p), above: par.spacing, p)
135
+ }
136
+ }
137
+ body
138
+ }
139
+ closing
140
+ }