@airmark/quiver 0.27.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.
- package/README.md +82 -33
- package/package.json +7 -5
- package/quills/af4141/0.1.0/Quill.yaml +21 -11
- package/quills/af4141/0.1.0/packages/typst-af4141/lib.typ +17 -3
- package/quills/af4141/0.1.0/plate.typ +11 -4
- package/quills/daf1206/0.1.0/Quill.yaml +3 -1
- package/quills/daf4392/0.1.0/Quill.yaml +14 -11
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/lib.typ +21 -4
- package/quills/usaf_memo/0.2.0/Quill.yaml +60 -44
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +37 -15
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +5 -6
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +5 -3
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +87 -35
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +47 -5
- package/quills/usaf_memo/0.2.0/plate.typ +15 -3
- package/quills/usaf_memo/0.3.0/Quill.yaml +321 -0
- package/quills/usaf_memo/0.3.0/assets/dod_seal.png +0 -0
- package/quills/usaf_memo/0.3.0/assets/dow_seal.png +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/.gitignore +7 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel[wght].ttf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Bold.otf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/LiberationMono/LICENSE +102 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/LiberationMono/LiberationMono-Regular.ttf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +29 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +364 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/config.typ +78 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +242 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +170 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/lib.typ +57 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +27 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +462 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +419 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/typst.toml +27 -0
- package/quills/usaf_memo/0.3.0/plate.typ +120 -0
- package/quills/af4141/0.1.0/design/TASK.md +0 -20
- package/quills/af4141/0.1.0/design/af4141.pdf +0 -0
- package/quills/daf1206/0.1.0/DAFForm_1206_prepared.pdf +0 -0
- package/quills/usaf_memo/0.2.0/.quillignore +0 -31
|
@@ -2,10 +2,14 @@ quill:
|
|
|
2
2
|
name: usaf_memo
|
|
3
3
|
version: 0.2.0
|
|
4
4
|
backend: typst
|
|
5
|
-
plate_file: plate.typ
|
|
6
5
|
description: U.S. Air Force and Space Force Official Memorandum
|
|
7
6
|
|
|
7
|
+
typst:
|
|
8
|
+
plate_file: plate.typ
|
|
9
|
+
|
|
8
10
|
main:
|
|
11
|
+
ui:
|
|
12
|
+
groups: [addressing, letterhead, classification, additional]
|
|
9
13
|
body:
|
|
10
14
|
example: |
|
|
11
15
|
The first paragraph. Top-level paragraphs are auto-numbered; do not add manual numbering.
|
|
@@ -20,7 +24,8 @@ main:
|
|
|
20
24
|
- ORG1/SYMBOL
|
|
21
25
|
- ORG2/SYMBOL
|
|
22
26
|
ui:
|
|
23
|
-
group:
|
|
27
|
+
group: addressing
|
|
28
|
+
compact: true
|
|
24
29
|
description: "Organization/office symbol in UPPERCASE. To address a specific person, add their rank and name in parentheses (e.g., 'ORG/SYMBOL (LT COL JANE DOE)'). For many recipients, enter 'SEE DISTRIBUTION' and list them in the Distribution field."
|
|
25
30
|
|
|
26
31
|
memo_from:
|
|
@@ -34,14 +39,15 @@ main:
|
|
|
34
39
|
- 123 Street Ave
|
|
35
40
|
- City ST 12345-6789
|
|
36
41
|
ui:
|
|
37
|
-
group:
|
|
42
|
+
group: addressing
|
|
43
|
+
compact: true
|
|
38
44
|
description: "If recipients are on the same installation, use only the office symbol. For recipients on other installations, include the full mailing address to enable return correspondence. Omit entirely to produce a Memorandum for Record (no MEMO FROM line)."
|
|
39
45
|
|
|
40
46
|
subject:
|
|
41
47
|
type: string
|
|
42
48
|
example: Subject of the Memorandum
|
|
43
49
|
ui:
|
|
44
|
-
group:
|
|
50
|
+
group: addressing
|
|
45
51
|
description: Be brief and clear. Capitalize the first letter of each word except articles, prepositions, and conjunctions. Include suspense dates in parentheses if applicable.
|
|
46
52
|
|
|
47
53
|
signature_block:
|
|
@@ -52,14 +58,15 @@ main:
|
|
|
52
58
|
- "FIRST M. LAST, Rank, USAF"
|
|
53
59
|
- Duty Title
|
|
54
60
|
ui:
|
|
55
|
-
group:
|
|
61
|
+
group: addressing
|
|
62
|
+
compact: true
|
|
56
63
|
description: "Line 1: Name in UPPERCASE as signed, grade, and service. Line 2: Duty title."
|
|
57
64
|
|
|
58
65
|
letterhead_title:
|
|
59
66
|
type: string
|
|
60
67
|
default: DEPARTMENT OF THE AIR FORCE
|
|
61
68
|
ui:
|
|
62
|
-
group:
|
|
69
|
+
group: letterhead
|
|
63
70
|
description: Change only for Joint Commands or DoW agencies.
|
|
64
71
|
|
|
65
72
|
letterhead_caption:
|
|
@@ -69,17 +76,17 @@ main:
|
|
|
69
76
|
example:
|
|
70
77
|
- HEADQUARTERS [UNIT NAME]
|
|
71
78
|
ui:
|
|
72
|
-
group:
|
|
79
|
+
group: letterhead
|
|
73
80
|
description: The full organization name of your unit.
|
|
74
81
|
|
|
75
82
|
letterhead_seal:
|
|
76
|
-
type:
|
|
77
|
-
|
|
83
|
+
type: enum
|
|
84
|
+
values:
|
|
78
85
|
- dow
|
|
79
86
|
- dod
|
|
80
87
|
default: dow
|
|
81
88
|
ui:
|
|
82
|
-
group:
|
|
89
|
+
group: letterhead
|
|
83
90
|
compact: true
|
|
84
91
|
description: Department of War (DoW) or Department of Defense (DoD) seal shown in the letterhead.
|
|
85
92
|
|
|
@@ -87,21 +94,22 @@ main:
|
|
|
87
94
|
type: string
|
|
88
95
|
default: ""
|
|
89
96
|
ui:
|
|
90
|
-
group:
|
|
97
|
+
group: letterhead
|
|
91
98
|
compact: true
|
|
92
99
|
description: Bold-caps line below the seal; blank to omit.
|
|
93
100
|
|
|
94
101
|
tag_line:
|
|
95
|
-
type:
|
|
102
|
+
type: richtext
|
|
103
|
+
inline: true
|
|
96
104
|
default: ""
|
|
97
105
|
ui:
|
|
98
|
-
group:
|
|
106
|
+
group: letterhead
|
|
99
107
|
compact: true
|
|
100
108
|
description: "Organizational motto at the bottom of the page. Supports Markdown emphasis (e.g. *italics*, **bold**)."
|
|
101
109
|
|
|
102
110
|
classification:
|
|
103
|
-
type:
|
|
104
|
-
|
|
111
|
+
type: enum
|
|
112
|
+
values:
|
|
105
113
|
- ""
|
|
106
114
|
- UNCLASSIFIED
|
|
107
115
|
- CUI
|
|
@@ -110,7 +118,7 @@ main:
|
|
|
110
118
|
- TOP SECRET
|
|
111
119
|
default: ""
|
|
112
120
|
ui:
|
|
113
|
-
group:
|
|
121
|
+
group: classification
|
|
114
122
|
compact: true
|
|
115
123
|
description: "Select the classification marking shown in the header and footer banner. Unclassified documents typically omit this banner entirely — leave blank unless the document is CUI or classified. Mark per DAFI 16-1404 (DoDM 5200.48 for CUI) and applicable DoD guidance."
|
|
116
124
|
|
|
@@ -118,7 +126,7 @@ main:
|
|
|
118
126
|
type: string
|
|
119
127
|
default: ""
|
|
120
128
|
ui:
|
|
121
|
-
group:
|
|
129
|
+
group: classification
|
|
122
130
|
compact: true
|
|
123
131
|
description: "Banner dissemination suffix appended after double slash (e.g. 'NF' renders as CUI//NF in the header/footer). Leave blank for no suffix."
|
|
124
132
|
|
|
@@ -126,7 +134,7 @@ main:
|
|
|
126
134
|
type: string
|
|
127
135
|
default: ""
|
|
128
136
|
ui:
|
|
129
|
-
group:
|
|
137
|
+
group: classification
|
|
130
138
|
compact: true
|
|
131
139
|
description: "Office or organization that designated this information as CUI (e.g., 'SAF/AA'). Required by DoDM 5200.48 when classification is CUI."
|
|
132
140
|
|
|
@@ -134,7 +142,7 @@ main:
|
|
|
134
142
|
type: string
|
|
135
143
|
default: ""
|
|
136
144
|
ui:
|
|
137
|
-
group:
|
|
145
|
+
group: classification
|
|
138
146
|
compact: true
|
|
139
147
|
description: "CUI category from the DoD CUI Registry (e.g., 'Privacy/MED', 'CTI', 'PRVCY'). Leave blank to omit."
|
|
140
148
|
|
|
@@ -142,7 +150,7 @@ main:
|
|
|
142
150
|
type: string
|
|
143
151
|
default: ""
|
|
144
152
|
ui:
|
|
145
|
-
group:
|
|
153
|
+
group: classification
|
|
146
154
|
compact: true
|
|
147
155
|
description: "Limited dissemination control shown in the indicator block (e.g., 'FEDONLY', 'NOFORN'). Independent from the banner suffix above."
|
|
148
156
|
|
|
@@ -150,20 +158,21 @@ main:
|
|
|
150
158
|
type: string
|
|
151
159
|
default: ""
|
|
152
160
|
ui:
|
|
153
|
-
group:
|
|
161
|
+
group: classification
|
|
154
162
|
compact: true
|
|
155
163
|
description: "Point of contact for CUI questions (e.g., 'Capt J. Smith, DSN 555-1234, john.smith@us.af.mil'). Required by DoDM 5200.48 when classification is CUI."
|
|
156
164
|
|
|
157
165
|
references:
|
|
158
166
|
type: array
|
|
159
167
|
items:
|
|
160
|
-
type:
|
|
168
|
+
type: richtext
|
|
169
|
+
inline: true
|
|
161
170
|
default: []
|
|
162
171
|
example:
|
|
163
172
|
- "AFMAN 33-326, 25 November 2011, *Preparing Official Communications*"
|
|
164
173
|
- "AFI 33-360, *Publications and Forms Management*"
|
|
165
174
|
ui:
|
|
166
|
-
group:
|
|
175
|
+
group: additional
|
|
167
176
|
description: "References, auto-lettered (a), (b), (c) per AFH 33-337. Cite each by organization, type, date, and title; italicize publication titles with *asterisks* (standard Markdown emphasis)."
|
|
168
177
|
|
|
169
178
|
cc:
|
|
@@ -174,7 +183,7 @@ main:
|
|
|
174
183
|
example:
|
|
175
184
|
- Rank and Name, ORG/SYMBOL
|
|
176
185
|
ui:
|
|
177
|
-
group:
|
|
186
|
+
group: additional
|
|
178
187
|
description: List office symbols of recipients to receive copies.
|
|
179
188
|
|
|
180
189
|
distribution:
|
|
@@ -186,7 +195,7 @@ main:
|
|
|
186
195
|
- ORG1/SYMBOL
|
|
187
196
|
- ORG2/SYMBOL
|
|
188
197
|
ui:
|
|
189
|
-
group:
|
|
198
|
+
group: additional
|
|
190
199
|
description: Complete list of recipients if 'SEE DISTRIBUTION' is used in the 'Memo For' field.
|
|
191
200
|
|
|
192
201
|
attachments:
|
|
@@ -197,17 +206,17 @@ main:
|
|
|
197
206
|
example:
|
|
198
207
|
- Attachment description, YYYY MMM DD
|
|
199
208
|
ui:
|
|
200
|
-
group:
|
|
209
|
+
group: additional
|
|
201
210
|
description: List attachments in the order they are mentioned in the memo. Briefly describe each; do not use 'as stated' or abbreviations.
|
|
202
211
|
|
|
203
212
|
memo_style:
|
|
204
|
-
type:
|
|
205
|
-
|
|
213
|
+
type: enum
|
|
214
|
+
values:
|
|
206
215
|
- usaf
|
|
207
216
|
- daf
|
|
208
217
|
default: usaf
|
|
209
218
|
ui:
|
|
210
|
-
group:
|
|
219
|
+
group: additional
|
|
211
220
|
compact: true
|
|
212
221
|
description: "USAF memorandum or DAF headquarters memorandum; DAF changes date format and body paragraph layout per AFH 33-337."
|
|
213
222
|
|
|
@@ -215,15 +224,15 @@ main:
|
|
|
215
224
|
type: number
|
|
216
225
|
default: 12
|
|
217
226
|
ui:
|
|
218
|
-
group:
|
|
227
|
+
group: additional
|
|
219
228
|
compact: true
|
|
220
229
|
description: Body text size in points; decimals allowed (e.g. 11.5).
|
|
221
230
|
|
|
222
231
|
date:
|
|
223
|
-
type:
|
|
232
|
+
type: date
|
|
224
233
|
default: ""
|
|
225
234
|
ui:
|
|
226
|
-
group:
|
|
235
|
+
group: additional
|
|
227
236
|
compact: true
|
|
228
237
|
description: YYYY-MM-DD. Leave blank to use today's date.
|
|
229
238
|
|
|
@@ -232,18 +241,21 @@ card_kinds:
|
|
|
232
241
|
description: Chain of routing endorsements. Each endorsement block adds an official response or forwarding action to the original memo.
|
|
233
242
|
ui:
|
|
234
243
|
title: Routing indorsement
|
|
244
|
+
groups: [addressing, additional]
|
|
235
245
|
fields:
|
|
236
246
|
from:
|
|
237
247
|
type: string
|
|
238
248
|
example: ORG/SYMBOL
|
|
239
249
|
ui:
|
|
240
|
-
group:
|
|
250
|
+
group: addressing
|
|
251
|
+
compact: true
|
|
241
252
|
description: Office symbol or Rank Name of the endorsing official.
|
|
242
253
|
for:
|
|
243
254
|
type: string
|
|
244
255
|
example: ORG/SYMBOL
|
|
245
256
|
ui:
|
|
246
|
-
group:
|
|
257
|
+
group: addressing
|
|
258
|
+
compact: true
|
|
247
259
|
description: Office symbol or organization receiving the endorsed memo.
|
|
248
260
|
signature_block:
|
|
249
261
|
type: array
|
|
@@ -253,35 +265,39 @@ card_kinds:
|
|
|
253
265
|
- "FIRST M. LAST, Rank, USAF"
|
|
254
266
|
- Duty Title
|
|
255
267
|
ui:
|
|
256
|
-
group:
|
|
268
|
+
group: addressing
|
|
269
|
+
compact: true
|
|
257
270
|
description: "Line 1: Name in UPPERCASE as signed, grade, and service. Line 2: Duty title."
|
|
258
271
|
format:
|
|
259
|
-
type:
|
|
260
|
-
|
|
272
|
+
type: enum
|
|
273
|
+
values:
|
|
261
274
|
- standard
|
|
262
275
|
- informal
|
|
263
276
|
- separate_page
|
|
264
277
|
default: standard
|
|
265
278
|
ui:
|
|
266
|
-
group:
|
|
279
|
+
group: additional
|
|
267
280
|
compact: true
|
|
268
281
|
description: "'standard' (formal, same page), 'informal' (less formal routing), or 'separate_page' (begins on a new page)."
|
|
269
282
|
action:
|
|
270
|
-
type:
|
|
271
|
-
|
|
283
|
+
type: enum
|
|
284
|
+
values:
|
|
272
285
|
- ""
|
|
273
286
|
- undecided
|
|
274
287
|
- approve
|
|
275
288
|
- disapprove
|
|
289
|
+
- undecided_concur
|
|
290
|
+
- concur
|
|
291
|
+
- nonconcur
|
|
276
292
|
default: ""
|
|
277
293
|
ui:
|
|
278
|
-
group:
|
|
294
|
+
group: additional
|
|
279
295
|
compact: true
|
|
280
|
-
description: "
|
|
296
|
+
description: "Decision taken by the endorser, shown as an either/or line with the choice circled. Leave blank to hide the line entirely. Approval authority: 'approve' or 'disapprove' (use 'undecided' to print Approve/Disapprove for the endorser to circle by hand). Coordinating official: 'concur' or 'nonconcur' (use 'undecided_concur' to print Concur/Nonconcur unmarked)."
|
|
281
297
|
date:
|
|
282
|
-
type:
|
|
298
|
+
type: date
|
|
283
299
|
default: ""
|
|
284
300
|
ui:
|
|
285
|
-
group:
|
|
301
|
+
group: additional
|
|
286
302
|
compact: true
|
|
287
303
|
description: "YYYY-MM-DD. Date the endorser signs/forwards the memo (per AFH 33-337 Ch. 14, this is distinct from the originating memo's date). Leave blank to render a fill-in line for the endorser to date by hand when signing."
|
|
@@ -199,6 +199,7 @@
|
|
|
199
199
|
// item.kind — "par", "heading", "table", or "continuation"
|
|
200
200
|
context {
|
|
201
201
|
let heading_buffer = none
|
|
202
|
+
let heading_level = 0
|
|
202
203
|
// Filter out zero-width paragraphs so that an empty body (e.g. empty
|
|
203
204
|
// string from a KIND with no text) emits nothing and collapses to zero
|
|
204
205
|
// vertical space. Tables are always kept regardless of measured width.
|
|
@@ -226,24 +227,36 @@
|
|
|
226
227
|
let kind = item.kind
|
|
227
228
|
let item_content = item.content
|
|
228
229
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
//
|
|
230
|
+
// A buffered heading runs into this element only when the two belong to
|
|
231
|
+
// the same item: a later block of this list item ("continuation"), or,
|
|
232
|
+
// at top level, the next paragraph. The first block of the *next* item,
|
|
233
|
+
// a table (nest_level −1, so the level test alone excludes it), and
|
|
234
|
+
// another heading all fail the test, and each would otherwise carry the
|
|
235
|
+
// heading's text somewhere it was not authored. Those emit the heading
|
|
236
|
+
// on its own line, the treatment a heading before a table takes.
|
|
236
237
|
if heading_buffer != none {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
} else {
|
|
238
|
+
let runs_in = (
|
|
239
|
+
kind != "heading"
|
|
240
|
+
and item.nest_level == heading_level
|
|
241
|
+
and (heading_level == 0 or kind == "continuation")
|
|
242
|
+
)
|
|
243
|
+
if runs_in {
|
|
244
244
|
item_content = [#strong[#heading_buffer.] #item_content]
|
|
245
|
-
|
|
245
|
+
} else {
|
|
246
|
+
if any_emitted { blank-line() }
|
|
247
|
+
strong[#heading_buffer.]
|
|
248
|
+
any_emitted = true
|
|
246
249
|
}
|
|
250
|
+
heading_buffer = none
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Buffer this heading for the next element to run into. The level it was
|
|
254
|
+
// captured at rides along: the run-in test needs it, and the buffer is
|
|
255
|
+
// read one iteration later, by which time `item` is the *next* element.
|
|
256
|
+
if kind == "heading" {
|
|
257
|
+
heading_buffer = item_content
|
|
258
|
+
heading_level = item.nest_level
|
|
259
|
+
continue
|
|
247
260
|
}
|
|
248
261
|
|
|
249
262
|
// Format based on element kind
|
|
@@ -336,6 +349,15 @@
|
|
|
336
349
|
block[#final_par]
|
|
337
350
|
}
|
|
338
351
|
}
|
|
352
|
+
|
|
353
|
+
// A heading with nothing after it never ran into anything, and the buffer
|
|
354
|
+
// dies with the loop unless it is drained here. Sticky for the same reason
|
|
355
|
+
// the last item is: a standalone heading is one line, so it keeps with the
|
|
356
|
+
// signature block rather than opening a page alone.
|
|
357
|
+
if heading_buffer != none {
|
|
358
|
+
if any_emitted { blank-line() }
|
|
359
|
+
block(sticky: true)[#strong[#heading_buffer.]]
|
|
360
|
+
}
|
|
339
361
|
}
|
|
340
362
|
}
|
|
341
363
|
|
|
@@ -221,13 +221,12 @@
|
|
|
221
221
|
render-date-section(actual_date, memo-style: memo_style)
|
|
222
222
|
render-for-section(memo_for, memo_for_cols)
|
|
223
223
|
if not falsey(memo_from) { render-from-section(memo_from) }
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
224
|
+
// Blank entries are dropped first so a stub `- ` left under `references:`
|
|
225
|
+
// cannot pass as the lone reference and render an empty `()` after the subject.
|
|
226
|
+
let refs = compact-references(references)
|
|
227
|
+
let single-ref = if refs.len() == 1 { refs.at(0) } else { none }
|
|
229
228
|
render-subject-section(subject, inline-reference: single-ref)
|
|
230
|
-
render-references-section(
|
|
229
|
+
render-references-section(refs)
|
|
231
230
|
|
|
232
231
|
// AFH 33-337: "Begin text on second line below subject/references".
|
|
233
232
|
// Emitted here (not inside body.typ) so the v() lands at the same lexical
|
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
date: none,
|
|
23
23
|
// Format of indorsement: "standard" (same page), "informal" (no header), or "separate_page" (starts on new page)
|
|
24
24
|
format: "standard",
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
25
|
+
// Decision action. `none` (default) displays no action line at all.
|
|
26
|
+
// Approval authority pair: "approve" or "disapprove" circles the selected
|
|
27
|
+
// option; "undecided" displays the pair with neither circled.
|
|
28
|
+
// Coordination pair: "concur" or "nonconcur" circles the selected option;
|
|
29
|
+
// "undecided_concur" displays the pair with neither circled.
|
|
28
30
|
action: none,
|
|
29
31
|
content,
|
|
30
32
|
) = {
|
|
@@ -39,23 +39,36 @@
|
|
|
39
39
|
let band-top = -band-height / 2 // puts the band center at dy 0
|
|
40
40
|
let band-center = band-top + band-height / 2
|
|
41
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
|
|
42
51
|
place(
|
|
43
52
|
dy: 0.625in - spacing.margin,
|
|
44
53
|
box(
|
|
45
54
|
width: 100%,
|
|
46
55
|
fill: none,
|
|
47
56
|
stroke: none,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
)),
|
|
59
72
|
),
|
|
60
73
|
)
|
|
61
74
|
|
|
@@ -145,11 +158,32 @@
|
|
|
145
158
|
)
|
|
146
159
|
}
|
|
147
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
|
+
|
|
148
182
|
// AFH 33-337 "SUBJECT:": "In all uppercase letters place 'SUBJECT:', flush with the
|
|
149
183
|
// left margin, on the second line below the last line of the FROM element"
|
|
150
184
|
#let render-subject-section(subject-text, inline-reference: none) = {
|
|
151
185
|
blank-line()
|
|
152
|
-
let content = if inline-reference
|
|
186
|
+
let content = if not blank-reference(inline-reference) {
|
|
153
187
|
[#subject-text (#box(inline-reference))]
|
|
154
188
|
} else {
|
|
155
189
|
[#subject-text]
|
|
@@ -163,7 +197,8 @@
|
|
|
163
197
|
// AFH 33-337: only render References block for two or more references.
|
|
164
198
|
// A single reference is rendered inline after the SUBJECT text instead.
|
|
165
199
|
#let render-references-section(references) = {
|
|
166
|
-
|
|
200
|
+
let references = compact-references(references)
|
|
201
|
+
if references.len() >= 2 {
|
|
167
202
|
blank-line()
|
|
168
203
|
grid(
|
|
169
204
|
columns: (auto, auto, 1fr),
|
|
@@ -245,40 +280,57 @@
|
|
|
245
280
|
// =============================================================================
|
|
246
281
|
// ACTION LINE RENDERING
|
|
247
282
|
// =============================================================================
|
|
248
|
-
// Renders the
|
|
249
|
-
//
|
|
250
|
-
//
|
|
283
|
+
// Renders the decision line for indorsement memos — an "either / or" pair
|
|
284
|
+
// where the endorser's choice is circled and the rejected option struck out.
|
|
285
|
+
//
|
|
286
|
+
// Two option pairs are supported, reflecting the two roles an indorsement
|
|
287
|
+
// plays in a coordination chain: coordinating officials Concur / Nonconcur,
|
|
288
|
+
// while the final approval authority Approves / Disapproves.
|
|
289
|
+
//
|
|
290
|
+
// Each pair also has an "undecided" form that renders both options plain, for
|
|
291
|
+
// printing a memo the endorser circles by hand when signing.
|
|
292
|
+
//
|
|
251
293
|
// Empty/none suppression is handled by the caller before this is invoked.
|
|
252
294
|
|
|
295
|
+
// Maps each `action` value to the option pair it renders and the index of the
|
|
296
|
+
// selected option within that pair (`none` selects neither).
|
|
297
|
+
#let ACTION_FORMS = (
|
|
298
|
+
"undecided": (("Approve", "Disapprove"), none),
|
|
299
|
+
"approve": (("Approve", "Disapprove"), 0),
|
|
300
|
+
"disapprove": (("Approve", "Disapprove"), 1),
|
|
301
|
+
"undecided_concur": (("Concur", "Nonconcur"), none),
|
|
302
|
+
"concur": (("Concur", "Nonconcur"), 0),
|
|
303
|
+
"nonconcur": (("Concur", "Nonconcur"), 1),
|
|
304
|
+
)
|
|
305
|
+
|
|
253
306
|
#let render-action-line(action, trailing-blank-line: true) = {
|
|
254
307
|
assert(
|
|
255
|
-
action in
|
|
256
|
-
message: "action must be
|
|
308
|
+
action in ACTION_FORMS,
|
|
309
|
+
message: "action must be one of " + ACTION_FORMS.keys().map(k => "\"" + k + "\"").join(", "),
|
|
257
310
|
)
|
|
311
|
+
let (options, selected) = ACTION_FORMS.at(action)
|
|
312
|
+
// Circle the selected option using a box with rounded corners; strike the
|
|
313
|
+
// one it was chosen over. When neither is selected both render plain.
|
|
314
|
+
// Use baseline parameter to maintain vertical text alignment.
|
|
315
|
+
let render-option(index) = {
|
|
316
|
+
let option = options.at(index)
|
|
317
|
+
if selected == none {
|
|
318
|
+
option
|
|
319
|
+
} else if selected == index {
|
|
320
|
+
box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[#option]
|
|
321
|
+
} else {
|
|
322
|
+
strike(option)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
258
325
|
// No leading blank-line: the caller (indorsement.typ) already emits the
|
|
259
326
|
// header→content gap once. The action line's `block(sticky: true)`
|
|
260
327
|
// additionally inherits `block.above: spacing.line` so the visual gap
|
|
261
328
|
// above matches the gap above a body's first paragraph.
|
|
262
|
-
//
|
|
263
|
-
// Use baseline parameter to maintain vertical text alignment
|
|
264
|
-
let approve-text = if action == "approve" {
|
|
265
|
-
box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[Approve]
|
|
266
|
-
} else if action == "disapprove" {
|
|
267
|
-
strike[Approve]
|
|
268
|
-
} else {
|
|
269
|
-
[Approve]
|
|
270
|
-
}
|
|
271
|
-
let disapprove-text = if action == "disapprove" {
|
|
272
|
-
box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[Disapprove]
|
|
273
|
-
} else if action == "approve" {
|
|
274
|
-
strike[Disapprove]
|
|
275
|
-
} else {
|
|
276
|
-
[Disapprove]
|
|
277
|
-
}
|
|
329
|
+
//
|
|
278
330
|
// Keep the action line with the following content (body or signature block)
|
|
279
331
|
// using the same sticky-block pattern that body.typ applies to the last
|
|
280
332
|
// paragraph, per AFH 33-337 §11 orphan-prevention rules.
|
|
281
|
-
block(sticky: true)[#
|
|
333
|
+
block(sticky: true)[#render-option(0) / #render-option(1)]
|
|
282
334
|
// Trailing blank-line places the body's first paragraph one line below
|
|
283
335
|
// the action, mirroring the gap above it. Suppressed when the body is
|
|
284
336
|
// empty so the signature block's own 4-line gap lands on AFH 33-337's
|
|
@@ -87,12 +87,47 @@
|
|
|
87
87
|
]
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/// Shrinks content to fit within a maximum width, never enlarging it.
|
|
91
|
+
///
|
|
92
|
+
/// Measures `body` at its current font size and scales it down uniformly only
|
|
93
|
+
/// when it is wider than `width`; content that already fits keeps its set size
|
|
94
|
+
/// (the scale is capped at 100%). Unlike `fit-box`, height flows naturally and
|
|
95
|
+
/// small content is never scaled up. Used for the letterhead caption (unit
|
|
96
|
+
/// designation): a long name is shrunk to the width that clears the seal rather
|
|
97
|
+
/// than running underneath it. Pass a non-wrapping `box` as `body` so the
|
|
98
|
+
/// measured width is the single-line width and the result stays on one line.
|
|
99
|
+
///
|
|
100
|
+
/// Empty content measures 0pt wide and has no scale to compute, so it is returned
|
|
101
|
+
/// as-is rather than divided by; a non-positive `width` likewise admits no scale
|
|
102
|
+
/// that fits, so the body is left at its set size instead of being flipped or
|
|
103
|
+
/// collapsed.
|
|
104
|
+
///
|
|
105
|
+
/// - width (length): Maximum width the content may occupy
|
|
106
|
+
/// - alignment (alignment): Horizontal placement within the reserved width
|
|
107
|
+
/// - body (content): Content to fit (typically a non-wrapping `box`)
|
|
108
|
+
/// -> content
|
|
109
|
+
#let fit-to-width(width, alignment: left, body) = context {
|
|
110
|
+
let s = measure(body)
|
|
111
|
+
if s.width <= 0pt or width <= 0pt { return body }
|
|
112
|
+
let f = calc.min(1, width / s.width) * 100% // ratio, capped so it only shrinks
|
|
113
|
+
box(width: width)[
|
|
114
|
+
#align(alignment)[
|
|
115
|
+
#scale(f, reflow: true)[#body]
|
|
116
|
+
]
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
|
|
90
120
|
/// Formats a date for the memo heading.
|
|
91
121
|
///
|
|
92
|
-
///
|
|
93
|
-
/// -
|
|
122
|
+
/// Dispatches on the shape a date field can arrive in:
|
|
123
|
+
/// - str: shown as-is (fixed text like a placeholder).
|
|
124
|
+
/// - datetime: a native Typst datetime (e.g. `datetime.today()`) — USAF style
|
|
125
|
+
/// `DD Month YYYY`, DAF style `Month DD, YYYY`.
|
|
126
|
+
/// - dict: a Quillmark `date`/`datetime` field lowers to a click-to-edit value
|
|
127
|
+
/// object `{ value: datetime, display: closure }`; `display` is a stored closure
|
|
128
|
+
/// over `.display`, so call it through parentheses — a dict has no `.display` method.
|
|
94
129
|
///
|
|
95
|
-
/// - date (str|datetime): Date to format for display
|
|
130
|
+
/// - date (str | datetime | dict): Date to format for display
|
|
96
131
|
/// - memo-style (str): `"usaf"` or `"daf"`
|
|
97
132
|
/// -> content
|
|
98
133
|
#let display-date(date, memo-style: "usaf") = {
|
|
@@ -108,7 +143,11 @@
|
|
|
108
143
|
} else {
|
|
109
144
|
"[day padding:none] [month repr:long] [year]"
|
|
110
145
|
}
|
|
111
|
-
date
|
|
146
|
+
if type(date) == datetime {
|
|
147
|
+
date.display(pattern)
|
|
148
|
+
} else {
|
|
149
|
+
(date.display)(pattern)
|
|
150
|
+
}
|
|
112
151
|
}
|
|
113
152
|
}
|
|
114
153
|
|
|
@@ -266,7 +305,10 @@
|
|
|
266
305
|
if value == none {
|
|
267
306
|
""
|
|
268
307
|
} else if type(value) == array {
|
|
269
|
-
|
|
308
|
+
// `().join(sep)` is `none`, not `""` — coerce so an empty array keeps the
|
|
309
|
+
// documented contract and stays `falsey` for callers that test the result.
|
|
310
|
+
let joined = value.join(separator)
|
|
311
|
+
if joined == none { "" } else { joined }
|
|
270
312
|
} else {
|
|
271
313
|
str(value)
|
|
272
314
|
}
|