@airmark/quiver 0.31.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/package.json +4 -4
- package/quills/af4141/0.1.0/plate.typ +6 -14
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/lib.typ +1 -1
- package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/utils.typ +9 -10
- package/quills/afmc_moa/0.0.1/plate.typ +8 -3
- package/quills/usaf_memo/0.3.0/Quill.yaml +23 -40
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +13 -5
- 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 +28 -19
- package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +19 -19
- package/quills/usaf_memo/0.3.0/plate.typ +61 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@airmark/quiver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Airmark Quiver — Air Force and DAF document Quills for Quillmark.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"node": ">=24"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@quillmark/quiver": "0.
|
|
20
|
-
"@quillmark/wasm": "^0.
|
|
21
|
-
"quillkit": "0.
|
|
19
|
+
"@quillmark/quiver": "0.24.0",
|
|
20
|
+
"@quillmark/wasm": "^0.108.1",
|
|
21
|
+
"quillkit": "0.5.1"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"test": "quillkit test",
|
|
@@ -3,23 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
#set text(font: "NimbusRomNo9L")
|
|
5
5
|
|
|
6
|
-
// A `type: date` field
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// is still accepted. Missing / blank dates arrive as `none`; empty strings may
|
|
11
|
-
// also appear from input.
|
|
6
|
+
// A `type: date` field arrives as a native `datetime`, blank as `none`. The
|
|
7
|
+
// form's cells are strings, so a date is formatted here rather than placed as
|
|
8
|
+
// ink: `display` would give the glyphs a schema address, but the form draws
|
|
9
|
+
// each cell into a fixed box it builds itself, which takes a `str`.
|
|
12
10
|
#let form-cell(v) = {
|
|
13
11
|
let pattern = "[month padding:none]/[day padding:none]/[year]"
|
|
14
12
|
if v == none { "" }
|
|
15
|
-
else if type(v) ==
|
|
16
|
-
else
|
|
17
|
-
(v.display)(pattern)
|
|
18
|
-
} else if type(v) == datetime {
|
|
19
|
-
v.display(pattern)
|
|
20
|
-
} else {
|
|
21
|
-
str(v)
|
|
22
|
-
}
|
|
13
|
+
else if type(v) == datetime { v.display(pattern) }
|
|
14
|
+
else { str(v) }
|
|
23
15
|
}
|
|
24
16
|
|
|
25
17
|
// Column order within each 7-field row group
|
|
@@ -38,4 +38,4 @@
|
|
|
38
38
|
#import "mainmatter.typ": mainmatter
|
|
39
39
|
#import "attachment.typ": render-attachment-a as attachment, render-attachment-card as attachment-card
|
|
40
40
|
#import "backmatter.typ": backmatter
|
|
41
|
-
#import "utils.typ": trim-inline
|
|
41
|
+
#import "utils.typ": date-pattern, trim-inline
|
|
@@ -82,23 +82,22 @@
|
|
|
82
82
|
if joined == none { [] } else { joined }
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/// The date pattern this package prints ("22 July 2026"). Exported so a caller
|
|
86
|
+
/// that pre-formats a date matches it instead of restating it.
|
|
87
|
+
#let date-pattern = "[day padding:none] [month repr:long] [year]"
|
|
88
|
+
|
|
85
89
|
/// Format a date value for inline display ("22 July 2026").
|
|
86
90
|
///
|
|
87
|
-
///
|
|
88
|
-
///
|
|
89
|
-
/// - datetime:
|
|
90
|
-
/// - dict: a Quillmark `date`/`datetime` field lowers to a click-to-edit value
|
|
91
|
-
/// object `{ value: datetime, display: closure }`; `display` is a stored
|
|
92
|
-
/// closure, so it must be called through parentheses — a dict has no
|
|
93
|
-
/// `.display` method.
|
|
91
|
+
/// - str, content: shown as-is (a fixed placeholder, or ink the caller already
|
|
92
|
+
/// formatted through [`date-pattern`]).
|
|
93
|
+
/// - datetime: formatted to [`date-pattern`].
|
|
94
94
|
#let format-date(d) = {
|
|
95
95
|
if d == none {
|
|
96
96
|
""
|
|
97
|
-
} else if type(d)
|
|
97
|
+
} else if type(d) in (str, content) {
|
|
98
98
|
d
|
|
99
99
|
} else {
|
|
100
|
-
|
|
101
|
-
if type(d) == datetime { d.display(pattern) } else { (d.display)(pattern) }
|
|
100
|
+
d.display(date-pattern)
|
|
102
101
|
}
|
|
103
102
|
}
|
|
104
103
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#import "@local/quillmark-helper:0.1.0": data, signature-field
|
|
1
|
+
#import "@local/quillmark-helper:0.1.0": data, display, signature-field
|
|
2
2
|
#import "@local/typst-afmc-moa:0.1.0": (
|
|
3
|
-
attachment, attachment-card, backmatter, frontmatter, mainmatter, trim-inline,
|
|
3
|
+
attachment, attachment-card, backmatter, date-pattern, frontmatter, mainmatter, trim-inline,
|
|
4
4
|
)
|
|
5
5
|
|
|
6
6
|
// Every text field is `plaintext`/`richtext`, so it arrives as content carrying
|
|
@@ -34,7 +34,12 @@
|
|
|
34
34
|
|
|
35
35
|
first_party_signatory: text-field("first_party_signatory"),
|
|
36
36
|
second_party_signatory: text-field("second_party_signatory"),
|
|
37
|
-
|
|
37
|
+
// `data.mid_point_review_due_date` is the native `datetime` and would render
|
|
38
|
+
// identically, but its ink would be born inside the package and carry no
|
|
39
|
+
// schema address. `display` places the field's content projection instead, so
|
|
40
|
+
// the printed date stays click-to-edit; a blank one yields `none`, which
|
|
41
|
+
// `format-date` renders as nothing.
|
|
42
|
+
mid_point_review_due_date: display("mid_point_review_due_date", date-pattern),
|
|
38
43
|
)
|
|
39
44
|
|
|
40
45
|
// ── Frontmatter: page setup, title block, intro paragraph ──────────────────
|
|
@@ -122,6 +122,28 @@ main:
|
|
|
122
122
|
group: classification
|
|
123
123
|
compact: true
|
|
124
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."
|
|
125
147
|
|
|
126
148
|
dissemination:
|
|
127
149
|
type: plaintext
|
|
@@ -132,42 +154,6 @@ main:
|
|
|
132
154
|
compact: true
|
|
133
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."
|
|
134
156
|
|
|
135
|
-
cui_controlled_by:
|
|
136
|
-
type: plaintext
|
|
137
|
-
inline: true
|
|
138
|
-
default: ""
|
|
139
|
-
ui:
|
|
140
|
-
group: classification
|
|
141
|
-
compact: true
|
|
142
|
-
description: "Office or organization that designated this information as CUI (e.g., 'SAF/AA'). Required by DoDM 5200.48 when classification is CUI."
|
|
143
|
-
|
|
144
|
-
cui_category:
|
|
145
|
-
type: plaintext
|
|
146
|
-
inline: true
|
|
147
|
-
default: ""
|
|
148
|
-
ui:
|
|
149
|
-
group: classification
|
|
150
|
-
compact: true
|
|
151
|
-
description: "CUI category from the DoD CUI Registry (e.g., 'Privacy/MED', 'CTI', 'PRVCY'). Leave blank to omit."
|
|
152
|
-
|
|
153
|
-
cui_limited_dissemination:
|
|
154
|
-
type: plaintext
|
|
155
|
-
inline: true
|
|
156
|
-
default: ""
|
|
157
|
-
ui:
|
|
158
|
-
group: classification
|
|
159
|
-
compact: true
|
|
160
|
-
description: "Limited dissemination control shown in the indicator block (e.g., 'FEDONLY', 'NOFORN'). Independent from the banner suffix above."
|
|
161
|
-
|
|
162
|
-
cui_poc:
|
|
163
|
-
type: plaintext
|
|
164
|
-
inline: true
|
|
165
|
-
default: ""
|
|
166
|
-
ui:
|
|
167
|
-
group: classification
|
|
168
|
-
compact: true
|
|
169
|
-
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."
|
|
170
|
-
|
|
171
157
|
references:
|
|
172
158
|
type: array
|
|
173
159
|
items:
|
|
@@ -297,14 +283,11 @@ card_kinds:
|
|
|
297
283
|
- undecided
|
|
298
284
|
- approve
|
|
299
285
|
- disapprove
|
|
300
|
-
- undecided_concur
|
|
301
|
-
- concur
|
|
302
|
-
- nonconcur
|
|
303
286
|
default: ""
|
|
304
287
|
ui:
|
|
305
288
|
group: additional
|
|
306
289
|
compact: true
|
|
307
|
-
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."
|
|
308
291
|
date:
|
|
309
292
|
type: date
|
|
310
293
|
default: ""
|
|
@@ -27,11 +27,15 @@
|
|
|
27
27
|
// Format of indorsement: "standard" (same page), "informal" (no header), or "separate_page" (starts on new page)
|
|
28
28
|
format: "standard",
|
|
29
29
|
// Decision action. `none` (default) displays no action line at all.
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// Coordination pair: "concur" or "nonconcur" circles the selected option;
|
|
33
|
-
// "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.
|
|
34
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,
|
|
35
39
|
content,
|
|
36
40
|
) = {
|
|
37
41
|
// Validate format parameter
|
|
@@ -160,7 +164,11 @@
|
|
|
160
164
|
|
|
161
165
|
// Show action line only when an action decision is set (not `none`)
|
|
162
166
|
if effective_action != none {
|
|
163
|
-
render-action-line(
|
|
167
|
+
render-action-line(
|
|
168
|
+
effective_action,
|
|
169
|
+
approval-authority: approval_authority,
|
|
170
|
+
trailing-blank-line: not body_empty,
|
|
171
|
+
)
|
|
164
172
|
}
|
|
165
173
|
|
|
166
174
|
if not body_empty {
|
|
@@ -306,37 +306,46 @@
|
|
|
306
306
|
// plays in a coordination chain: coordinating officials Concur / Nonconcur,
|
|
307
307
|
// while the final approval authority Approves / Disapproves.
|
|
308
308
|
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
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.
|
|
311
315
|
//
|
|
312
316
|
// Empty/none suppression is handled by the caller before this is invoked.
|
|
313
317
|
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
#let
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
"
|
|
322
|
-
"
|
|
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,
|
|
323
328
|
)
|
|
324
329
|
|
|
325
|
-
#let render-action-line(action, trailing-blank-line: true) = {
|
|
330
|
+
#let render-action-line(action, approval-authority: false, trailing-blank-line: true) = {
|
|
326
331
|
assert(
|
|
327
|
-
action in
|
|
328
|
-
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(", "),
|
|
329
334
|
)
|
|
330
|
-
let
|
|
331
|
-
|
|
332
|
-
// one it was chosen over. When
|
|
333
|
-
//
|
|
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.
|
|
334
343
|
let render-option(index) = {
|
|
335
344
|
let option = options.at(index)
|
|
336
345
|
if selected == none {
|
|
337
346
|
option
|
|
338
347
|
} else if selected == index {
|
|
339
|
-
|
|
348
|
+
underline(option)
|
|
340
349
|
} else {
|
|
341
350
|
strike(option)
|
|
342
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,19 +154,12 @@
|
|
|
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
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
#import "@local/quillmark-helper:0.1.0": data, form-field, signature-field
|
|
2
|
-
#import "@local/tonguetoquill-usaf-memo:4.0.0":
|
|
1
|
+
#import "@local/quillmark-helper:0.1.0": data, display, form-field, signature-field
|
|
2
|
+
#import "@local/tonguetoquill-usaf-memo:4.0.0": (
|
|
3
|
+
backmatter, date-pattern, frontmatter, indorsement, mainmatter,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
// A memo has no "no style" state, so the blank takes the package's default.
|
|
7
|
+
// Resolved once here: `frontmatter` and the date's pattern must agree.
|
|
8
|
+
#let memo_style = if data.memo_style != "" { data.memo_style } else { "usaf" }
|
|
3
9
|
|
|
4
10
|
// The letterhead arrives as one ordered array of lines. Element 0 is the title,
|
|
5
11
|
// set larger; the rest are caption lines beneath it. A single element renders
|
|
@@ -31,8 +37,13 @@
|
|
|
31
37
|
))
|
|
32
38
|
},
|
|
33
39
|
|
|
34
|
-
// Date
|
|
35
|
-
|
|
40
|
+
// Date. `data.date` is the native `datetime` and would render identically,
|
|
41
|
+
// but its ink would be born inside the package and carry no schema address.
|
|
42
|
+
// `display` places the field's *content* projection instead: the glyphs are
|
|
43
|
+
// born in the generated helper, so the memo date stays click-to-edit however
|
|
44
|
+
// deep the package formats it. A blank date yields `none`, which is what
|
|
45
|
+
// `frontmatter`'s `datetime.today()` fallback keys on.
|
|
46
|
+
date: display("date", date-pattern(memo-style: memo_style)),
|
|
36
47
|
|
|
37
48
|
// Receiver information
|
|
38
49
|
memo_for: data.memo_for,
|
|
@@ -49,20 +60,28 @@
|
|
|
49
60
|
// Optional footer tag line
|
|
50
61
|
footer_tag_line: data.tag_line,
|
|
51
62
|
|
|
52
|
-
//
|
|
53
|
-
|
|
63
|
+
// The blank reads as no banner, which is what the package's own
|
|
64
|
+
// `classification_level: none` default means.
|
|
65
|
+
classification_level: data.classification.value,
|
|
54
66
|
|
|
55
67
|
dissemination: data.dissemination,
|
|
56
68
|
|
|
57
|
-
// CUI designation indicator block fields (DoDM 5200.48)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
// CUI designation indicator block fields (DoDM 5200.48). `classification`
|
|
70
|
+
// declares a `CUI` variant, so these four exist only where the discriminant
|
|
71
|
+
// reads CUI, and the branch is what makes reading them total: inside it every
|
|
72
|
+
// declared field of that world is present, outside it none is. The package's
|
|
73
|
+
// own `cui_*: none` defaults cover the worlds that omit them.
|
|
74
|
+
..if data.classification.value == "CUI" {
|
|
75
|
+
(
|
|
76
|
+
cui_controlled_by: data.classification.controlled_by,
|
|
77
|
+
cui_category: data.classification.category,
|
|
78
|
+
cui_limited_dissemination: data.classification.limited_dissemination,
|
|
79
|
+
cui_poc: data.classification.poc,
|
|
80
|
+
)
|
|
81
|
+
},
|
|
62
82
|
|
|
63
|
-
// USAF vs DAF memorandum style (date format, body indentation).
|
|
64
|
-
|
|
65
|
-
..if data.memo_style != "" { (memo_style: data.memo_style) },
|
|
83
|
+
// USAF vs DAF memorandum style (date format, body indentation).
|
|
84
|
+
memo_style: memo_style,
|
|
66
85
|
|
|
67
86
|
// Font size
|
|
68
87
|
font_size: body_font_size,
|
|
@@ -95,6 +114,26 @@
|
|
|
95
114
|
..if data.attachments.len() > 0 { (attachments: data.attachments) },
|
|
96
115
|
)
|
|
97
116
|
|
|
117
|
+
// The action line's wording follows the indorsement's place in the chain, not
|
|
118
|
+
// an endorser's choice: the last indorsement is the approval authority and
|
|
119
|
+
// reads Approve / Disapprove, every one before it is a coordinating official
|
|
120
|
+
// and reads Concur / Nonconcur. Only this loop can see whether an indorsement
|
|
121
|
+
// is the last one, so the position is resolved here and passed down. Cards of
|
|
122
|
+
// other kinds may interleave, so the last indorsement is the last card *of
|
|
123
|
+
// this kind*, not the last card. `-1` never matches a real index, which is the
|
|
124
|
+
// wanted result when there are no indorsements at all.
|
|
125
|
+
// Parenthesized so the method chain can span lines: in markup, a `#let` whose
|
|
126
|
+
// expression ends at a line break stops there, and the next line's leading
|
|
127
|
+
// `.at(…)` would be read as text.
|
|
128
|
+
#let last_indorsement_index = (
|
|
129
|
+
data
|
|
130
|
+
.at("$cards")
|
|
131
|
+
.enumerate()
|
|
132
|
+
.filter(entry => entry.at(1).at("$kind", default: none) == "indorsement")
|
|
133
|
+
.map(entry => entry.at(0))
|
|
134
|
+
.at(-1, default: -1)
|
|
135
|
+
)
|
|
136
|
+
|
|
98
137
|
// Indorsements - iterate through CARDS array and filter by CARD tag
|
|
99
138
|
#for (i, card) in data.at("$cards").enumerate() {
|
|
100
139
|
if card.at("$kind", default: none) == "indorsement" {
|
|
@@ -108,12 +147,13 @@
|
|
|
108
147
|
// it (distinct from the originating memo's date). The signing date is
|
|
109
148
|
// generally unknown at compile time, so a blank or omitted date leaves the
|
|
110
149
|
// date slot fillable rather than stamping the compile date into it.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
150
|
+
// `display` takes an address, so a per-card call yields a per-card region
|
|
151
|
+
// even though every iteration shares one `card` loop variable, and it
|
|
152
|
+
// returns `none` for a blank date — the fill-in case below.
|
|
153
|
+
let resolved_date = display(
|
|
154
|
+
card.at("$path") + "date",
|
|
155
|
+
date-pattern(memo-style: memo_style),
|
|
156
|
+
)
|
|
117
157
|
// The card's `$path` prefix composes its canonical schema addresses
|
|
118
158
|
// (`$cards.indorsement.<n>.…`, per-kind ordinal) — the absolute loop
|
|
119
159
|
// index `i` is NOT that ordinal once kinds interleave, so it stays a
|
|
@@ -144,6 +184,7 @@
|
|
|
144
184
|
))
|
|
145
185
|
},
|
|
146
186
|
..if card.action != "" { (action: card.action) },
|
|
187
|
+
approval_authority: i == last_indorsement_index,
|
|
147
188
|
body_content,
|
|
148
189
|
)
|
|
149
190
|
}
|