@airmark/quiver 0.30.0 → 0.31.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 +5 -5
- 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 +127 -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 +111 -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 +2 -4
- 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 +5 -1
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +28 -12
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +22 -14
- package/quills/usaf_memo/0.3.0/plate.typ +54 -31
|
@@ -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,6 +20,10 @@
|
|
|
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.
|
|
@@ -79,7 +83,7 @@
|
|
|
79
83
|
let indorsement_number = counters.indorsement.get().at(0, default: 1)
|
|
80
84
|
let indorsement_label = format-indorsement-number(indorsement_number)
|
|
81
85
|
|
|
82
|
-
let ind_date = align(right)[#if actual_date != none { display-date(actual_date, memo-style: memo-style) } else { date-placeholder-
|
|
86
|
+
let ind_date = align(right)[#if actual_date != none { display-date(actual_date, memo-style: memo-style) } else { date-placeholder-slot(date_field) }]
|
|
83
87
|
|
|
84
88
|
// Separate-page header body: restates the original memo's identity (FROM,
|
|
85
89
|
// date, subject) on its own line, since the indorsement no longer shares a
|
|
@@ -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)[
|
|
@@ -163,24 +163,32 @@
|
|
|
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.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#import "@local/quillmark-helper:0.1.0": data, signature-field
|
|
1
|
+
#import "@local/quillmark-helper:0.1.0": data, form-field, signature-field
|
|
2
2
|
#import "@local/tonguetoquill-usaf-memo:4.0.0": backmatter, frontmatter, indorsement, mainmatter
|
|
3
3
|
|
|
4
4
|
// The letterhead arrives as one ordered array of lines. Element 0 is the title,
|
|
@@ -8,54 +8,64 @@
|
|
|
8
8
|
// leaves no glyphs and takes no space from the flow, and the seal stands alone.
|
|
9
9
|
#let letterhead_lines = data.letterhead_title
|
|
10
10
|
|
|
11
|
+
// Body text size, in points. Also the height of one line of the indorsement
|
|
12
|
+
// header, which is what an omitted indorsement date reserves for its fill-in
|
|
13
|
+
// widget (`date-placeholder-slot`, whose slot is `1em` tall and 1in wide).
|
|
14
|
+
#let body_font_size = data.font_size * 1pt
|
|
15
|
+
|
|
11
16
|
// Frontmatter configuration
|
|
12
17
|
#show: frontmatter.with(
|
|
13
18
|
// Letterhead configuration
|
|
14
19
|
letterhead_title: letterhead_lines.at(0, default: ""),
|
|
15
20
|
letterhead_caption: if letterhead_lines.len() > 1 { letterhead_lines.slice(1) } else { () },
|
|
16
|
-
letterhead_seal_subtitle: data.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
letterhead_seal_subtitle: data.letterhead_seal_subtitle,
|
|
22
|
+
// Enum blank is `""`, not a seal. Omit so the package renders none rather
|
|
23
|
+
// than treating the blank as DoW.
|
|
24
|
+
..if data.letterhead_seal != "" {
|
|
25
|
+
(letterhead_seal: image(
|
|
26
|
+
if data.letterhead_seal == "dod" {
|
|
27
|
+
"assets/dod_seal.png"
|
|
28
|
+
} else {
|
|
29
|
+
"assets/dow_seal.png"
|
|
30
|
+
}
|
|
31
|
+
))
|
|
32
|
+
},
|
|
24
33
|
|
|
25
34
|
// Date
|
|
26
|
-
date: data.
|
|
35
|
+
date: data.date,
|
|
27
36
|
|
|
28
37
|
// Receiver information
|
|
29
38
|
memo_for: data.memo_for,
|
|
30
39
|
|
|
31
40
|
// Sender information (omitted for Memorandum for Record)
|
|
32
|
-
..if data.
|
|
41
|
+
..if data.memo_from.len() > 0 { (memo_from: data.memo_from) },
|
|
33
42
|
|
|
34
43
|
// Subject line
|
|
35
44
|
subject: data.subject,
|
|
36
45
|
|
|
37
46
|
// Optional references
|
|
38
|
-
..if
|
|
47
|
+
..if data.references.len() > 0 { (references: data.references) },
|
|
39
48
|
|
|
40
49
|
// Optional footer tag line
|
|
41
|
-
|
|
50
|
+
footer_tag_line: data.tag_line,
|
|
42
51
|
|
|
43
52
|
// Optional classification level
|
|
44
|
-
..if
|
|
53
|
+
..if data.classification != "" { (classification_level: data.classification) },
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
dissemination: data.dissemination,
|
|
47
56
|
|
|
48
57
|
// CUI designation indicator block fields (DoDM 5200.48)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
cui_controlled_by: data.cui_controlled_by,
|
|
59
|
+
cui_category: data.cui_category,
|
|
60
|
+
cui_limited_dissemination: data.cui_limited_dissemination,
|
|
61
|
+
cui_poc: data.cui_poc,
|
|
53
62
|
|
|
54
|
-
// USAF vs DAF memorandum style (date format, body indentation)
|
|
55
|
-
|
|
63
|
+
// USAF vs DAF memorandum style (date format, body indentation). Blank
|
|
64
|
+
// falls through to the package's `"usaf"` default.
|
|
65
|
+
..if data.memo_style != "" { (memo_style: data.memo_style) },
|
|
56
66
|
|
|
57
67
|
// Font size
|
|
58
|
-
font_size:
|
|
68
|
+
font_size: body_font_size,
|
|
59
69
|
|
|
60
70
|
// List recipients in vertical list
|
|
61
71
|
memo_for_cols: 1,
|
|
@@ -76,18 +86,18 @@
|
|
|
76
86
|
signing_field: signature-field("Signature", field: "signature_block"),
|
|
77
87
|
|
|
78
88
|
// Optional cc
|
|
79
|
-
..if
|
|
89
|
+
..if data.cc.len() > 0 { (cc: data.cc) },
|
|
80
90
|
|
|
81
91
|
// Optional distribution
|
|
82
|
-
..if
|
|
92
|
+
..if data.distribution.len() > 0 { (distribution: data.distribution) },
|
|
83
93
|
|
|
84
94
|
// Optional attachments
|
|
85
|
-
..if
|
|
95
|
+
..if data.attachments.len() > 0 { (attachments: data.attachments) },
|
|
86
96
|
)
|
|
87
97
|
|
|
88
98
|
// Indorsements - iterate through CARDS array and filter by CARD tag
|
|
89
99
|
#for (i, card) in data.at("$cards").enumerate() {
|
|
90
|
-
if card.at("$kind") == "indorsement" {
|
|
100
|
+
if card.at("$kind", default: none) == "indorsement" {
|
|
91
101
|
// The quillmark helper leaves an unset/whitespace-only markdown body as
|
|
92
102
|
// the empty string `""`; only non-empty bodies are eval'd into content.
|
|
93
103
|
// Pass truly empty content (`[]`) in the empty case so indorsement can
|
|
@@ -96,9 +106,9 @@
|
|
|
96
106
|
let body_content = if type(body) == str { [] } else { body }
|
|
97
107
|
// Per AFH 33-337 Ch. 14, an indorsement is dated when the endorser signs
|
|
98
108
|
// it (distinct from the originating memo's date). The signing date is
|
|
99
|
-
// generally unknown at compile time
|
|
100
|
-
//
|
|
101
|
-
let card_date = card.
|
|
109
|
+
// generally unknown at compile time, so a blank or omitted date leaves the
|
|
110
|
+
// date slot fillable rather than stamping the compile date into it.
|
|
111
|
+
let card_date = card.date
|
|
102
112
|
let resolved_date = if card_date == none or card_date == "" {
|
|
103
113
|
none
|
|
104
114
|
} else {
|
|
@@ -118,9 +128,22 @@
|
|
|
118
128
|
"Ind_" + str(i) + "_Signature",
|
|
119
129
|
field: card.at("$path") + "signature_block",
|
|
120
130
|
),
|
|
121
|
-
|
|
131
|
+
..if card.format != "" { (format: card.format) },
|
|
122
132
|
date: resolved_date,
|
|
123
|
-
|
|
133
|
+
// An omitted date becomes an empty AcroForm text box the endorser types
|
|
134
|
+
// the signing date into, sized to the slot the package reserves for it.
|
|
135
|
+
// Built only when there is no date to print: a widget over a printed date
|
|
136
|
+
// would offer an edit that the rendered document does not carry back.
|
|
137
|
+
..if resolved_date == none {
|
|
138
|
+
(date_field: form-field(
|
|
139
|
+
"Ind_" + str(i) + "_Date",
|
|
140
|
+
type: "text",
|
|
141
|
+
width: 1in,
|
|
142
|
+
height: body_font_size,
|
|
143
|
+
field: card.at("$path") + "date",
|
|
144
|
+
))
|
|
145
|
+
},
|
|
146
|
+
..if card.action != "" { (action: card.action) },
|
|
124
147
|
body_content,
|
|
125
148
|
)
|
|
126
149
|
}
|