@airmark/quiver 0.30.0 → 0.32.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 +1 -0
- package/package.json +4 -4
- package/quills/af4141/0.1.0/plate.typ +11 -19
- package/quills/afmc_moa/0.0.1/Quill.yaml +289 -0
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/attachment.typ +51 -0
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/backmatter.typ +66 -0
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/body.typ +126 -0
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/config.typ +31 -0
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/frontmatter.typ +74 -0
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/lib.typ +41 -0
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/mainmatter.typ +5 -0
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/utils.typ +126 -0
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/typst.toml +19 -0
- package/quills/afmc_moa/0.0.1/plate.typ +116 -0
- package/quills/daf1206/0.1.0/plate.typ +10 -10
- package/quills/daf4392/0.1.0/plate.typ +13 -13
- package/quills/usaf_memo/0.2.0/Quill.yaml +0 -2
- package/quills/usaf_memo/0.2.0/plate.typ +33 -28
- package/quills/usaf_memo/0.3.0/Quill.yaml +25 -44
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +30 -25
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +18 -6
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/lib.typ +1 -0
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +56 -31
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +41 -33
- package/quills/usaf_memo/0.3.0/plate.typ +108 -44
|
@@ -112,7 +112,6 @@ main:
|
|
|
112
112
|
classification:
|
|
113
113
|
type: enum
|
|
114
114
|
values:
|
|
115
|
-
- ""
|
|
116
115
|
- UNCLASSIFIED
|
|
117
116
|
- CUI
|
|
118
117
|
- CONFIDENTIAL
|
|
@@ -123,6 +122,28 @@ main:
|
|
|
123
122
|
group: classification
|
|
124
123
|
compact: true
|
|
125
124
|
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."
|
|
125
|
+
variants:
|
|
126
|
+
CUI:
|
|
127
|
+
controlled_by:
|
|
128
|
+
type: plaintext
|
|
129
|
+
inline: true
|
|
130
|
+
example: SAF/AA
|
|
131
|
+
description: "Office or organization that designated this information as CUI. Required by DoDM 5200.48."
|
|
132
|
+
poc:
|
|
133
|
+
type: plaintext
|
|
134
|
+
inline: true
|
|
135
|
+
example: "Capt J. Smith, DSN 555-1234, john.smith@us.af.mil"
|
|
136
|
+
description: "Point of contact for CUI questions. Required by DoDM 5200.48."
|
|
137
|
+
category:
|
|
138
|
+
type: plaintext
|
|
139
|
+
inline: true
|
|
140
|
+
default: ""
|
|
141
|
+
description: "CUI category from the DoD CUI Registry (e.g., 'Privacy/MED', 'CTI', 'PRVCY'). Leave blank to omit."
|
|
142
|
+
limited_dissemination:
|
|
143
|
+
type: plaintext
|
|
144
|
+
inline: true
|
|
145
|
+
default: ""
|
|
146
|
+
description: "Limited dissemination control shown in the indicator block (e.g., 'FEDONLY', 'NOFORN'). Independent from the banner suffix."
|
|
126
147
|
|
|
127
148
|
dissemination:
|
|
128
149
|
type: plaintext
|
|
@@ -133,42 +154,6 @@ main:
|
|
|
133
154
|
compact: true
|
|
134
155
|
description: "Banner dissemination suffix appended after double slash (e.g. 'NF' renders as CUI//NF in the header/footer). Leave blank for no suffix."
|
|
135
156
|
|
|
136
|
-
cui_controlled_by:
|
|
137
|
-
type: plaintext
|
|
138
|
-
inline: true
|
|
139
|
-
default: ""
|
|
140
|
-
ui:
|
|
141
|
-
group: classification
|
|
142
|
-
compact: true
|
|
143
|
-
description: "Office or organization that designated this information as CUI (e.g., 'SAF/AA'). Required by DoDM 5200.48 when classification is CUI."
|
|
144
|
-
|
|
145
|
-
cui_category:
|
|
146
|
-
type: plaintext
|
|
147
|
-
inline: true
|
|
148
|
-
default: ""
|
|
149
|
-
ui:
|
|
150
|
-
group: classification
|
|
151
|
-
compact: true
|
|
152
|
-
description: "CUI category from the DoD CUI Registry (e.g., 'Privacy/MED', 'CTI', 'PRVCY'). Leave blank to omit."
|
|
153
|
-
|
|
154
|
-
cui_limited_dissemination:
|
|
155
|
-
type: plaintext
|
|
156
|
-
inline: true
|
|
157
|
-
default: ""
|
|
158
|
-
ui:
|
|
159
|
-
group: classification
|
|
160
|
-
compact: true
|
|
161
|
-
description: "Limited dissemination control shown in the indicator block (e.g., 'FEDONLY', 'NOFORN'). Independent from the banner suffix above."
|
|
162
|
-
|
|
163
|
-
cui_poc:
|
|
164
|
-
type: plaintext
|
|
165
|
-
inline: true
|
|
166
|
-
default: ""
|
|
167
|
-
ui:
|
|
168
|
-
group: classification
|
|
169
|
-
compact: true
|
|
170
|
-
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."
|
|
171
|
-
|
|
172
157
|
references:
|
|
173
158
|
type: array
|
|
174
159
|
items:
|
|
@@ -291,26 +276,22 @@ card_kinds:
|
|
|
291
276
|
ui:
|
|
292
277
|
group: additional
|
|
293
278
|
compact: true
|
|
294
|
-
description: "'standard' (formal, same page), 'informal' (less formal routing), or 'separate_page' (begins on a new page)."
|
|
279
|
+
description: "'standard' (formal, same page), 'informal' (less formal routing), or 'separate_page' (begins on a new page). 'informal' prints no header, so an informal indorsement with an empty body is nothing but a signature block — it has no text of its own to hold it to the page, and can open a page alone. Give it a body or an action, or use 'standard'."
|
|
295
280
|
action:
|
|
296
281
|
type: enum
|
|
297
282
|
values:
|
|
298
|
-
- ""
|
|
299
283
|
- undecided
|
|
300
284
|
- approve
|
|
301
285
|
- disapprove
|
|
302
|
-
- undecided_concur
|
|
303
|
-
- concur
|
|
304
|
-
- nonconcur
|
|
305
286
|
default: ""
|
|
306
287
|
ui:
|
|
307
288
|
group: additional
|
|
308
289
|
compact: true
|
|
309
|
-
description: "Decision taken by the endorser, shown as an either/or line with the choice
|
|
290
|
+
description: "Decision taken by the endorser, shown as an either/or line with the choice underlined and the rejected option struck out. Leave blank to hide the line entirely. The wording is automatic: the last indorsement is the approval authority and reads Approve/Disapprove, while every earlier one is a coordinating official and reads Concur/Nonconcur. Use 'undecided' to print the pair unmarked for the endorser to mark by hand."
|
|
310
291
|
date:
|
|
311
292
|
type: date
|
|
312
293
|
default: ""
|
|
313
294
|
ui:
|
|
314
295
|
group: additional
|
|
315
296
|
compact: true
|
|
316
|
-
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
|
|
297
|
+
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 fillable PDF text field for the endorser to type the date into when signing."
|
|
@@ -308,35 +308,40 @@
|
|
|
308
308
|
// `context { for … }` block, where it does not combine with the
|
|
309
309
|
// preceding header section's block-spacing the same way as a
|
|
310
310
|
// top-level blank-line() call.
|
|
311
|
-
// AFH 33-337 §11 rule applied below: "Avoid dividing a paragraph of
|
|
312
|
-
// less than four lines between two pages"
|
|
313
311
|
if any_emitted { blank-line() }
|
|
314
312
|
any_emitted = true
|
|
315
313
|
if i == total_count {
|
|
314
|
+
// AFH 33-337 "Signature Block": "Do not place the signature element on a
|
|
315
|
+
// continuation page by itself." The signature block that follows has no
|
|
316
|
+
// keep-with-previous of its own — Typst has no such property — so the
|
|
317
|
+
// anchor comes from this side: a sticky last element is carried onto the
|
|
318
|
+
// next page along with the signature instead of breaking away from it.
|
|
319
|
+
//
|
|
320
|
+
// Bounded, because a sticky block does not *split* at a page boundary,
|
|
321
|
+
// it relocates whole; applied to a half-page block that turns an
|
|
322
|
+
// ordinary mid-paragraph break into a wholesale jump. A third of the
|
|
323
|
+
// text block covers what a memorandum actually ends on — a closing
|
|
324
|
+
// paragraph, a short table — while leaving the long block to be divided
|
|
325
|
+
// by the break as before, which leaves its own tail on the continuation
|
|
326
|
+
// page for the signature to sit under.
|
|
327
|
+
//
|
|
328
|
+
// The budget is a fixed fraction rather than the space actually left
|
|
329
|
+
// below this element, which is the question one would rather ask. A
|
|
330
|
+
// position-dependent test cannot be answered honestly here: on the first
|
|
331
|
+
// layout pass `here()` resolves against an empty introspector and reports
|
|
332
|
+
// the top of the page, so every element measures as fitting and
|
|
333
|
+
// relocates — and from the top of its new page the same test agrees, so
|
|
334
|
+
// the wrong answer is a fixed point that later passes never revisit.
|
|
335
|
+
//
|
|
336
|
+
// (The superseded rule was sticky under four lines, reading §11 — "avoid
|
|
337
|
+
// dividing a paragraph of less than four lines between two pages" — as
|
|
338
|
+
// if it governed the signature. It does not; it governs how a paragraph
|
|
339
|
+
// may be divided. All the four-line gate did was leave every body ending
|
|
340
|
+
// in a longer paragraph, or in a table, free to strand its signature.)
|
|
316
341
|
let available_width = page.width - spacing.margin * 2
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
let cached = LINE_STRIDE.get()
|
|
321
|
-
if cached != none {
|
|
322
|
-
cached
|
|
323
|
-
} else {
|
|
324
|
-
let one-line = measure(par(spacing: 0pt)[x]).height
|
|
325
|
-
measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
// Calculate last item's height
|
|
329
|
-
let par_height = measure(final_par, width: available_width).height
|
|
330
|
-
|
|
331
|
-
let estimated_lines = calc.ceil(par_height / line_height)
|
|
332
|
-
|
|
333
|
-
if estimated_lines < 4 {
|
|
334
|
-
// Short content (< 4 lines): make sticky to keep with signature
|
|
335
|
-
block(sticky: true)[#final_par]
|
|
336
|
-
} else {
|
|
337
|
-
// Longer content (≥ 4 lines): use default breaking behavior
|
|
338
|
-
block(breakable: true)[#final_par]
|
|
339
|
-
}
|
|
342
|
+
let relocation_budget = (page.height - spacing.margin * 2) / 3
|
|
343
|
+
let element_height = measure(final_par, width: available_width).height
|
|
344
|
+
block(sticky: element_height <= relocation_budget)[#final_par]
|
|
340
345
|
} else {
|
|
341
346
|
// Wrap every non-last emission in a plain block so the document-wide
|
|
342
347
|
// `set block(above: spacing.line)` rule contributes the same 0.5em
|
|
@@ -20,14 +20,22 @@
|
|
|
20
20
|
signature_blank_lines: 4,
|
|
21
21
|
signing_field: none,
|
|
22
22
|
date: none,
|
|
23
|
+
// Fill-in widget for an omitted `date`, anchored in the date slot of the
|
|
24
|
+
// indorsement header (see `date-placeholder-slot`). `none` leaves the slot
|
|
25
|
+
// blank.
|
|
26
|
+
date_field: none,
|
|
23
27
|
// Format of indorsement: "standard" (same page), "informal" (no header), or "separate_page" (starts on new page)
|
|
24
28
|
format: "standard",
|
|
25
29
|
// Decision action. `none` (default) displays no action line at all.
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// Coordination pair: "concur" or "nonconcur" circles the selected option;
|
|
29
|
-
// "undecided_concur" displays the pair with neither circled.
|
|
30
|
+
// "approve" or "disapprove" underlines the selected option and strikes the
|
|
31
|
+
// other; "undecided" displays the pair with neither marked.
|
|
30
32
|
action: none,
|
|
33
|
+
// Role this indorsement plays in the coordination chain, which selects the
|
|
34
|
+
// wording of the action line: the approval authority (the last indorsement
|
|
35
|
+
// in the chain) Approves / Disapproves, every coordinating official before
|
|
36
|
+
// it Concurs / Nonconcurs. The caller owns this because only it can see
|
|
37
|
+
// whether further indorsements follow.
|
|
38
|
+
approval_authority: false,
|
|
31
39
|
content,
|
|
32
40
|
) = {
|
|
33
41
|
// Validate format parameter
|
|
@@ -79,7 +87,7 @@
|
|
|
79
87
|
let indorsement_number = counters.indorsement.get().at(0, default: 1)
|
|
80
88
|
let indorsement_label = format-indorsement-number(indorsement_number)
|
|
81
89
|
|
|
82
|
-
let ind_date = align(right)[#if actual_date != none { display-date(actual_date, memo-style: memo-style) } else { date-placeholder-
|
|
90
|
+
let ind_date = align(right)[#if actual_date != none { display-date(actual_date, memo-style: memo-style) } else { date-placeholder-slot(date_field) }]
|
|
83
91
|
|
|
84
92
|
// Separate-page header body: restates the original memo's identity (FROM,
|
|
85
93
|
// date, subject) on its own line, since the indorsement no longer shares a
|
|
@@ -156,7 +164,11 @@
|
|
|
156
164
|
|
|
157
165
|
// Show action line only when an action decision is set (not `none`)
|
|
158
166
|
if effective_action != none {
|
|
159
|
-
render-action-line(
|
|
167
|
+
render-action-line(
|
|
168
|
+
effective_action,
|
|
169
|
+
approval-authority: approval_authority,
|
|
170
|
+
trailing-blank-line: not body_empty,
|
|
171
|
+
)
|
|
160
172
|
}
|
|
161
173
|
|
|
162
174
|
if not body_empty {
|
|
@@ -224,9 +224,6 @@
|
|
|
224
224
|
|
|
225
225
|
#let render-signature-block(signature-lines, signature-blank-lines: 4, signing-field: none) = {
|
|
226
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
227
|
// AFH 33-337 allows two equivalent anchors: 4.5in from the left edge, or three
|
|
231
228
|
// spaces right of page center. On 8.5in stock these coincide (page center =
|
|
232
229
|
// 4.25in; three TNR-12pt spaces ≈ 0.25in), so we use 4.5in as the canonical
|
|
@@ -240,6 +237,13 @@
|
|
|
240
237
|
let w = measure(text(hyphenate: false, line)).width
|
|
241
238
|
if w > widest { widest = w }
|
|
242
239
|
}
|
|
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
|
+
}
|
|
243
247
|
// If the widest line would overflow the right margin at the standard
|
|
244
248
|
// anchor, shift the block left just enough to fit. Clamp at 0 so the
|
|
245
249
|
// block never crosses the left margin.
|
|
@@ -251,18 +255,30 @@
|
|
|
251
255
|
default-pad
|
|
252
256
|
}
|
|
253
257
|
block(breakable: false)[
|
|
258
|
+
#let gap = stride * signature-blank-lines
|
|
259
|
+
// AFH 33-337: "fifth line below the last line of text" = four blank lines
|
|
260
|
+
// between the text and the signature block. Carried INSIDE the
|
|
261
|
+
// unbreakable block rather than emitted ahead of it, which keeps the gap
|
|
262
|
+
// and the block one indivisible unit. The total space above the block is
|
|
263
|
+
// identical either way — `block.above` still contributes the same 0.5em —
|
|
264
|
+
// 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)
|
|
254
268
|
#if signing-field != none {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
269
|
+
// 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.
|
|
262
278
|
place(
|
|
263
279
|
dx: left-pad,
|
|
264
|
-
dy:
|
|
265
|
-
box(width: body-width - left-pad, height:
|
|
280
|
+
dy: 0pt,
|
|
281
|
+
box(width: body-width - left-pad, height: gap, signing-field),
|
|
266
282
|
)
|
|
267
283
|
}
|
|
268
284
|
#align(left)[
|
|
@@ -290,37 +306,46 @@
|
|
|
290
306
|
// plays in a coordination chain: coordinating officials Concur / Nonconcur,
|
|
291
307
|
// while the final approval authority Approves / Disapproves.
|
|
292
308
|
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
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.
|
|
295
315
|
//
|
|
296
316
|
// Empty/none suppression is handled by the caller before this is invoked.
|
|
297
317
|
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
#let
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
"
|
|
306
|
-
"
|
|
318
|
+
// The option pair for each role, ordered (affirmative, negative).
|
|
319
|
+
#let APPROVAL_OPTIONS = ("Approve", "Disapprove")
|
|
320
|
+
#let COORDINATION_OPTIONS = ("Concur", "Nonconcur")
|
|
321
|
+
|
|
322
|
+
// Maps each `action` value to the index of the option it selects within
|
|
323
|
+
// whichever pair is rendered (`none` selects neither).
|
|
324
|
+
#let ACTION_SELECTIONS = (
|
|
325
|
+
"undecided": none,
|
|
326
|
+
"approve": 0,
|
|
327
|
+
"disapprove": 1,
|
|
307
328
|
)
|
|
308
329
|
|
|
309
|
-
#let render-action-line(action, trailing-blank-line: true) = {
|
|
330
|
+
#let render-action-line(action, approval-authority: false, trailing-blank-line: true) = {
|
|
310
331
|
assert(
|
|
311
|
-
action in
|
|
312
|
-
message: "action must be one of " +
|
|
332
|
+
action in ACTION_SELECTIONS,
|
|
333
|
+
message: "action must be one of " + ACTION_SELECTIONS.keys().map(k => "\"" + k + "\"").join(", "),
|
|
313
334
|
)
|
|
314
|
-
let
|
|
315
|
-
|
|
316
|
-
// one it was chosen over. When
|
|
317
|
-
//
|
|
335
|
+
let options = if approval-authority { APPROVAL_OPTIONS } else { COORDINATION_OPTIONS }
|
|
336
|
+
let selected = ACTION_SELECTIONS.at(action)
|
|
337
|
+
// Underline the selected option; strike the one it was chosen over. When
|
|
338
|
+
// neither is selected both render plain. An underline keeps the option on
|
|
339
|
+
// the line's own baseline — the box it replaces had to be nudged with
|
|
340
|
+
// `baseline` to sit straight — and reads as a mark made on the page, where a
|
|
341
|
+
// ruled rectangle in a PDF carrying real AcroForm widgets reads as one more
|
|
342
|
+
// fillable field.
|
|
318
343
|
let render-option(index) = {
|
|
319
344
|
let option = options.at(index)
|
|
320
345
|
if selected == none {
|
|
321
346
|
option
|
|
322
347
|
} else if selected == index {
|
|
323
|
-
|
|
348
|
+
underline(option)
|
|
324
349
|
} else {
|
|
325
350
|
strike(option)
|
|
326
351
|
}
|
|
@@ -129,17 +129,24 @@
|
|
|
129
129
|
]
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/// The date pattern a memo style prints, per AFH 33-337. Exported so a caller
|
|
133
|
+
/// that pre-formats the date matches this package instead of restating it.
|
|
134
|
+
///
|
|
135
|
+
/// - memo-style (str): `"usaf"` or `"daf"`
|
|
136
|
+
/// -> str
|
|
137
|
+
#let date-pattern(memo-style: "usaf") = if memo-style == "daf" {
|
|
138
|
+
"[month repr:long] [day padding:none], [year]"
|
|
139
|
+
} else {
|
|
140
|
+
"[day padding:none] [month repr:long] [year]"
|
|
141
|
+
}
|
|
142
|
+
|
|
132
143
|
/// Formats a date for the memo heading.
|
|
133
144
|
///
|
|
134
|
-
///
|
|
135
|
-
///
|
|
136
|
-
/// - datetime:
|
|
137
|
-
/// `DD Month YYYY`, DAF style `Month DD, YYYY`.
|
|
138
|
-
/// - dict: a Quillmark `date`/`datetime` field lowers to a click-to-edit value
|
|
139
|
-
/// object `{ value: datetime, display: closure }`; `display` is a stored closure
|
|
140
|
-
/// over `.display`, so call it through parentheses — a dict has no `.display` method.
|
|
145
|
+
/// - str, content: shown as-is (a fixed placeholder, or ink the caller already
|
|
146
|
+
/// formatted through [`date-pattern`]).
|
|
147
|
+
/// - datetime: USAF style `DD Month YYYY`; DAF style `Month DD, YYYY`.
|
|
141
148
|
///
|
|
142
|
-
/// - date (str |
|
|
149
|
+
/// - date (str | content | datetime): Date to format for display
|
|
143
150
|
/// - memo-style (str): `"usaf"` or `"daf"`
|
|
144
151
|
/// -> content
|
|
145
152
|
#let display-date(date, memo-style: "usaf") = {
|
|
@@ -147,40 +154,41 @@
|
|
|
147
154
|
memo-style in ("usaf", "daf"),
|
|
148
155
|
message: "memo-style for display-date must be \"usaf\" or \"daf\"",
|
|
149
156
|
)
|
|
150
|
-
|
|
157
|
+
// A caller that pre-formatted — to keep a click-to-edit region the package
|
|
158
|
+
// cannot mint itself — passes the finished ink through untouched.
|
|
159
|
+
if type(date) in (str, content) {
|
|
151
160
|
date
|
|
152
161
|
} else {
|
|
153
|
-
|
|
154
|
-
"[month repr:long] [day padding:none], [year]"
|
|
155
|
-
} else {
|
|
156
|
-
"[day padding:none] [month repr:long] [year]"
|
|
157
|
-
}
|
|
158
|
-
if type(date) == datetime {
|
|
159
|
-
date.display(pattern)
|
|
160
|
-
} else {
|
|
161
|
-
(date.display)(pattern)
|
|
162
|
-
}
|
|
162
|
+
date.display(date-pattern(memo-style: memo-style))
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
///
|
|
167
|
-
///
|
|
168
|
-
/// Used for indorsements whose signing date is
|
|
169
|
-
///
|
|
170
|
-
///
|
|
171
|
-
///
|
|
172
|
-
///
|
|
173
|
-
///
|
|
174
|
-
///
|
|
166
|
+
/// Reserves the space a date occupies, for a date unknown at compile time.
|
|
167
|
+
///
|
|
168
|
+
/// Used for indorsements whose signing date is not known when the memo is
|
|
169
|
+
/// rendered: the endorser supplies it after the fact. The reserved box is one
|
|
170
|
+
/// line tall and sits on the baseline, so the slot lines up with where a
|
|
171
|
+
/// printed date would sit and takes exactly the space one would take.
|
|
172
|
+
///
|
|
173
|
+
/// `field` is the caller's fill-in widget — the Quillmark helper's
|
|
174
|
+
/// `form-field(.., type: "text")`, which lowers to a fillable AcroForm text box
|
|
175
|
+
/// in PDF output. It is anchored at the slot's top-left corner, the corner the
|
|
176
|
+
/// widget's own rectangle grows right and down from, so a widget declared at
|
|
177
|
+
/// this slot's dimensions covers it exactly. `none` (a caller with no widget to
|
|
178
|
+
/// give, e.g. the package used outside Quillmark) leaves the slot blank.
|
|
179
|
+
///
|
|
180
|
+
/// - field (content | none): Fill-in widget to anchor in the slot
|
|
181
|
+
/// - width (length): Width of the slot; defaults to fit a long date such as
|
|
182
|
+
/// "15 September 2026".
|
|
175
183
|
/// -> content
|
|
176
|
-
#let date-placeholder-
|
|
184
|
+
#let date-placeholder-slot(field, width: 1in) = box(
|
|
177
185
|
width: width,
|
|
178
186
|
height: 1em,
|
|
179
|
-
// Keep the
|
|
180
|
-
//
|
|
181
|
-
// (A positive baseline shift would drop the
|
|
187
|
+
// Keep the slot's bottom edge on the line's baseline, where the printed date
|
|
188
|
+
// would sit; the 1em height then reserves the line above it.
|
|
189
|
+
// (A positive baseline shift would drop the box a full line too low.)
|
|
182
190
|
baseline: 0pt,
|
|
183
|
-
|
|
191
|
+
if field != none { place(top + left, field) },
|
|
184
192
|
)
|
|
185
193
|
|
|
186
194
|
/// Gets the banner color for a classification marking.
|