@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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// frontmatter.typ — page setup, title block, and intro paragraph for a
|
|
2
|
+
// DoDI 4000.19 Memorandum of Agreement (Figure 1)
|
|
3
|
+
|
|
4
|
+
#import "config.typ": DEFAULT_BODY_FONTS, TITLE_TOP_GAP, spacing
|
|
5
|
+
#import "utils.typ": LINE_STRIDE, blank-line, blank-lines, falsey, join-inline
|
|
6
|
+
|
|
7
|
+
#let frontmatter(
|
|
8
|
+
first_party_name: none,
|
|
9
|
+
second_party_name: none,
|
|
10
|
+
second_party_is_non_government: false,
|
|
11
|
+
second_party_mailing_address: none,
|
|
12
|
+
subject: none,
|
|
13
|
+
agreement_number: none,
|
|
14
|
+
body_font: DEFAULT_BODY_FONTS,
|
|
15
|
+
font_size: 12pt,
|
|
16
|
+
it,
|
|
17
|
+
) = {
|
|
18
|
+
assert(first_party_name != none, message: "first_party_name is required")
|
|
19
|
+
assert(second_party_name != none, message: "second_party_name is required")
|
|
20
|
+
assert(subject != none, message: "subject is required")
|
|
21
|
+
assert(agreement_number != none, message: "agreement_number is required")
|
|
22
|
+
|
|
23
|
+
// ── Document-wide typography ─────────────────────────────────────────────
|
|
24
|
+
set par(leading: spacing.line, spacing: spacing.line, justify: false)
|
|
25
|
+
set block(above: spacing.line, below: 0em, spacing: 0em)
|
|
26
|
+
set text(font: body_font, size: font_size, fallback: true)
|
|
27
|
+
|
|
28
|
+
// ── Page geometry ────────────────────────────────────────────────────────
|
|
29
|
+
set page(
|
|
30
|
+
paper: "us-letter",
|
|
31
|
+
margin: (top: spacing.margin, bottom: spacing.margin, left: spacing.margin, right: spacing.margin),
|
|
32
|
+
header: context if counter(page).get().first() > 1 {
|
|
33
|
+
place(dy: +.5in, block(width: 100%, align(right, text(12pt)[#counter(page).display()])))
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
// ── Cache line stride ────────────────────────────────────────────────────
|
|
38
|
+
context {
|
|
39
|
+
let h1 = measure(par(spacing: 0pt)[x]).height
|
|
40
|
+
LINE_STRIDE.update(measure(par(spacing: 0pt)[x#linebreak()x]).height - h1)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Title block ──────────────────────────────────────────────────────────
|
|
44
|
+
v(TITLE_TOP_GAP)
|
|
45
|
+
align(center)[
|
|
46
|
+
#upper[MEMORANDUM OF AGREEMENT] \
|
|
47
|
+
BETWEEN \
|
|
48
|
+
THE #upper(first_party_name) \
|
|
49
|
+
AND \
|
|
50
|
+
THE #upper(second_party_name) \
|
|
51
|
+
FOR \
|
|
52
|
+
#upper(subject) \
|
|
53
|
+
#agreement_number
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
blank-lines(2)
|
|
57
|
+
|
|
58
|
+
// ── Intro paragraph ──────────────────────────────────────────────────────
|
|
59
|
+
// Figure 1: "This is a memorandum of agreement (MOA) between the [First
|
|
60
|
+
// Party] and the [Second Party] [if the Second Party is a non-governmental
|
|
61
|
+
// entity, include its address]. When referred to collectively, the [First
|
|
62
|
+
// Party] and the [Second Party] are referred to as the 'Parties.'"
|
|
63
|
+
let second-party-clause = if second_party_is_non_government {
|
|
64
|
+
[#second_party_name (#join-inline(second_party_mailing_address))]
|
|
65
|
+
} else {
|
|
66
|
+
second_party_name
|
|
67
|
+
}
|
|
68
|
+
block[
|
|
69
|
+
#h(0.5in)This is a memorandum of agreement (MOA) between the #first_party_name and the #second-party-clause. When referred to collectively, the #first_party_name and the #second_party_name are referred to as the "Parties."
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
blank-line()
|
|
73
|
+
it
|
|
74
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// lib.typ — public API for the typst-afmc-moa package
|
|
2
|
+
//
|
|
3
|
+
// Implements a DoDI 4000.19 Memorandum of Agreement (Figure 1).
|
|
4
|
+
//
|
|
5
|
+
// Basic usage:
|
|
6
|
+
//
|
|
7
|
+
// #import "@local/typst-afmc-moa:0.1.0": frontmatter, mainmatter, attachment, attachment-card, backmatter
|
|
8
|
+
//
|
|
9
|
+
// #show: frontmatter.with(
|
|
10
|
+
// first_party_name: "Air Force Materiel Command (AFMC)",
|
|
11
|
+
// second_party_name: "National Aeronautics and Space Administration (NASA)",
|
|
12
|
+
// subject: "Reciprocal Use of Test Facilities",
|
|
13
|
+
// agreement_number: "AFMC-2026-0142",
|
|
14
|
+
// )
|
|
15
|
+
//
|
|
16
|
+
// // Freeform body text (paragraphs 1-N), auto-numbered per DoDI 4000.19
|
|
17
|
+
// // Figure 1's chained decimal scheme (1., 4.1., 6.1.1.1.).
|
|
18
|
+
// #mainmatter[
|
|
19
|
+
// PURPOSE AND SCOPE: ...
|
|
20
|
+
// ]
|
|
21
|
+
//
|
|
22
|
+
// #backmatter(
|
|
23
|
+
// first_party_name: data.first_party_name,
|
|
24
|
+
// second_party_name: data.second_party_name,
|
|
25
|
+
// first_party_signatory: data.first_party_signatory,
|
|
26
|
+
// second_party_signatory: data.second_party_signatory,
|
|
27
|
+
// mid_point_review_due_date: data.mid_point_review_due_date,
|
|
28
|
+
// )
|
|
29
|
+
//
|
|
30
|
+
// #if data.reimbursable { attachment(data) }
|
|
31
|
+
//
|
|
32
|
+
// // Additional attachments (B, C, ...) from `attachment` cards.
|
|
33
|
+
// #attachment-card("B", data, "Facility Safety Requirements")[
|
|
34
|
+
// All personnel entering ... will complete ...
|
|
35
|
+
// ]
|
|
36
|
+
|
|
37
|
+
#import "frontmatter.typ": frontmatter
|
|
38
|
+
#import "mainmatter.typ": mainmatter
|
|
39
|
+
#import "attachment.typ": render-attachment-a as attachment, render-attachment-card as attachment-card
|
|
40
|
+
#import "backmatter.typ": backmatter
|
|
41
|
+
#import "utils.typ": date-pattern, trim-inline
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// utils.typ — shared helpers for the MOA package
|
|
2
|
+
|
|
3
|
+
// Line-stride cache (set once in frontmatter; read by blank-lines).
|
|
4
|
+
#let LINE_STRIDE = state("MOA_LINE_STRIDE")
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Spacing helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/// Emit `count` blank line strides of vertical space.
|
|
11
|
+
#let blank-lines(count) = {
|
|
12
|
+
if count <= 0 { return }
|
|
13
|
+
context {
|
|
14
|
+
let s = LINE_STRIDE.get()
|
|
15
|
+
if s == none {
|
|
16
|
+
let h = measure(par(spacing: 0pt)[x]).height
|
|
17
|
+
s = measure(par(spacing: 0pt)[x#linebreak()x]).height - h
|
|
18
|
+
}
|
|
19
|
+
v(s * count)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Emit one blank line of vertical space.
|
|
24
|
+
#let blank-line() = blank-lines(1)
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// General helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/// True when value is none / false / "" / empty content / empty array.
|
|
31
|
+
///
|
|
32
|
+
/// A `plaintext`/`richtext` field lowers to content when it holds text, but an
|
|
33
|
+
/// unset one arrives as the empty `str` — so both shapes have to be checked.
|
|
34
|
+
#let falsey(v) = (
|
|
35
|
+
v == none or v == false
|
|
36
|
+
or (type(v) == array and v.len() == 0)
|
|
37
|
+
or (type(v) == str and v == "")
|
|
38
|
+
or (type(v) == content and v == [])
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
/// Wrap a non-array value in a tuple; pass arrays through.
|
|
42
|
+
#let ensure-array(v) = {
|
|
43
|
+
if v == none { () } else if type(v) == array { v } else { (v,) }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Strips the padding a `plaintext`/`richtext` field carries, so the value can
|
|
47
|
+
/// be interpolated straight into prose.
|
|
48
|
+
///
|
|
49
|
+
/// Such a field lowers to a sequence padded with space elements —
|
|
50
|
+
/// `[ ], [Air Force Materiel Command], [ ], [(AFMC)], [ ]` — and, as an array
|
|
51
|
+
/// item, a trailing `parbreak()`: `[ ], [NASA/ABC], parbreak()`. Left alone
|
|
52
|
+
/// those print as "the Foo and", "(NASA) .", or push each array element onto
|
|
53
|
+
/// its own line. Boxing the value hides them but makes it unbreakable, which
|
|
54
|
+
/// forces a long party name onto a line of its own; trimming keeps it
|
|
55
|
+
/// breakable and tight.
|
|
56
|
+
///
|
|
57
|
+
/// Non-content values (an unset field arrives as the empty `str`) pass through.
|
|
58
|
+
#let trim-inline(v) = {
|
|
59
|
+
if type(v) != content { return v }
|
|
60
|
+
let kids = v.at("children", default: none)
|
|
61
|
+
if kids == none { return v }
|
|
62
|
+
let padding(c) = c == [ ] or c.func() == parbreak
|
|
63
|
+
while kids.len() > 0 and padding(kids.first()) { kids = kids.slice(1) }
|
|
64
|
+
while kids.len() > 0 and padding(kids.last()) { kids = kids.slice(0, -1) }
|
|
65
|
+
kids.sum(default: [])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Join a value (or array of values) into one inline run, separated by `sep`.
|
|
69
|
+
///
|
|
70
|
+
/// `plaintext`/`richtext` fields lower to content, and `str + content` is not a
|
|
71
|
+
/// legal Typst addition, so every element is wrapped as content before joining.
|
|
72
|
+
///
|
|
73
|
+
/// - join-inline("foo") → [foo]
|
|
74
|
+
/// - join-inline(([a], [b]), sep: ", ") → [a, b]
|
|
75
|
+
/// - join-inline(()) → []
|
|
76
|
+
/// - join-inline(none) → []
|
|
77
|
+
#let join-inline(value, sep: ", ") = {
|
|
78
|
+
if value == none { return [] }
|
|
79
|
+
let items = if type(value) == array { value } else { (value,) }
|
|
80
|
+
// `().join(..)` is `none`, not `[]` — coerce so an empty array stays falsey.
|
|
81
|
+
let joined = items.map(item => [#trim-inline(item)]).join(sep)
|
|
82
|
+
if joined == none { [] } else { joined }
|
|
83
|
+
}
|
|
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
|
+
|
|
89
|
+
/// Format a date value for inline display ("22 July 2026").
|
|
90
|
+
///
|
|
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
|
+
#let format-date(d) = {
|
|
95
|
+
if d == none {
|
|
96
|
+
""
|
|
97
|
+
} else if type(d) in (str, content) {
|
|
98
|
+
d
|
|
99
|
+
} else {
|
|
100
|
+
d.display(date-pattern)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Decimal outline numbering
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/// Builds the next chained decimal number string from a parent prefix.
|
|
109
|
+
/// decimal-numbering("", 1) -> "1." decimal-numbering("4.", 1) -> "4.1."
|
|
110
|
+
#let decimal-numbering(prefix, n) = {
|
|
111
|
+
if prefix == "" { str(n) + "." } else { prefix + str(n) + "." }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Renders a sequence of entries as consecutively-numbered siblings under
|
|
115
|
+
/// `prefix`, skipping entries whose `active` is false and renumbering the
|
|
116
|
+
/// remaining entries with no gaps.
|
|
117
|
+
///
|
|
118
|
+
/// - prefix (str): parent number string ("" at the top level, "4." under §4, …)
|
|
119
|
+
/// - items (array): each item is (active: bool, render: numstr => content)
|
|
120
|
+
#let numbered-list(prefix, items) = {
|
|
121
|
+
let seq = items.filter(it => it.active)
|
|
122
|
+
for (i, it) in seq.enumerate() {
|
|
123
|
+
if i > 0 { blank-line() }
|
|
124
|
+
(it.render)(decimal-numbering(prefix, i + 1))
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "typst-afmc-moa"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
compiler = "0.14.0"
|
|
5
|
+
entrypoint = "src/lib.typ"
|
|
6
|
+
authors = ["Airmark"]
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
description = "Typeset DoD Memoranda of Agreement compliant with DoDI 4000.19."
|
|
9
|
+
keywords = [
|
|
10
|
+
"MOA",
|
|
11
|
+
"Memorandum of Agreement",
|
|
12
|
+
"DoDI 4000.19",
|
|
13
|
+
"DoD",
|
|
14
|
+
"support agreement",
|
|
15
|
+
"official",
|
|
16
|
+
"correspondence",
|
|
17
|
+
]
|
|
18
|
+
categories = ["office", "report"]
|
|
19
|
+
disciplines = ["law", "business", "politics"]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#import "@local/quillmark-helper:0.1.0": data, display, signature-field
|
|
2
|
+
#import "@local/typst-afmc-moa:0.1.0": (
|
|
3
|
+
attachment, attachment-card, backmatter, date-pattern, frontmatter, mainmatter, trim-inline,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
// Every text field is `plaintext`/`richtext`, so it arrives as content carrying
|
|
7
|
+
// a space element on each side. `trim-inline` strips those once, here, so the
|
|
8
|
+
// package can interpolate a field straight into prose without it printing as
|
|
9
|
+
// "the Foo and" or "(NASA) ." — and without boxing, which would stop a long
|
|
10
|
+
// party name from wrapping.
|
|
11
|
+
#let text-field(key) = trim-inline(data.at(key))
|
|
12
|
+
|
|
13
|
+
// Resolve optional fields with defaults; required fields are read directly
|
|
14
|
+
// since Quill.yaml enforces their presence.
|
|
15
|
+
#let resolved = (
|
|
16
|
+
first_party_name: text-field("first_party_name"),
|
|
17
|
+
second_party_name: text-field("second_party_name"),
|
|
18
|
+
second_party_is_non_government: data.second_party_is_non_government,
|
|
19
|
+
subject: text-field("subject"),
|
|
20
|
+
agreement_number: text-field("agreement_number"),
|
|
21
|
+
|
|
22
|
+
// Joined inline by the package, which trims each line itself.
|
|
23
|
+
second_party_mailing_address: data.second_party_mailing_address,
|
|
24
|
+
|
|
25
|
+
reimbursable: data.reimbursable,
|
|
26
|
+
reimbursable_support: text-field("reimbursable_support"),
|
|
27
|
+
estimated_amount: text-field("estimated_amount"),
|
|
28
|
+
appropriation_fy: text-field("appropriation_fy"),
|
|
29
|
+
cost_center_provider: text-field("cost_center_provider"),
|
|
30
|
+
cost_center_receiver: text-field("cost_center_receiver"),
|
|
31
|
+
financial_poc_provider: text-field("financial_poc_provider"),
|
|
32
|
+
financial_poc_receiver: text-field("financial_poc_receiver"),
|
|
33
|
+
financial_additional_info: text-field("financial_additional_info"),
|
|
34
|
+
|
|
35
|
+
first_party_signatory: text-field("first_party_signatory"),
|
|
36
|
+
second_party_signatory: text-field("second_party_signatory"),
|
|
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),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// ── Frontmatter: page setup, title block, intro paragraph ──────────────────
|
|
46
|
+
#show: frontmatter.with(
|
|
47
|
+
first_party_name: resolved.first_party_name,
|
|
48
|
+
second_party_name: resolved.second_party_name,
|
|
49
|
+
second_party_is_non_government: resolved.second_party_is_non_government,
|
|
50
|
+
second_party_mailing_address: resolved.second_party_mailing_address,
|
|
51
|
+
subject: resolved.subject,
|
|
52
|
+
agreement_number: resolved.agreement_number,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// The MOA's standard sections (Background, Authorities, ...) are authored
|
|
56
|
+
// as their own cards but rendered inline as the body's numbered paragraphs,
|
|
57
|
+
// chained into the same DoDI 4000.19 decimal numbering as the rest of the
|
|
58
|
+
// body (no page break, no numbering restart). Only the first card found of
|
|
59
|
+
// each kind is used.
|
|
60
|
+
#let card-body(kind) = {
|
|
61
|
+
let bodies = ()
|
|
62
|
+
for card in data.at("$cards") {
|
|
63
|
+
if card.at("$kind", default: none) == kind and "$body" in card {
|
|
64
|
+
bodies.push(card.at("$body"))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if bodies.len() > 0 { bodies.at(0) } else { none }
|
|
68
|
+
}
|
|
69
|
+
#let background = card-body("background")
|
|
70
|
+
#let authorities = card-body("authorities")
|
|
71
|
+
#let purpose_and_scope = card-body("purpose_and_scope")
|
|
72
|
+
#let responsibilities_of_the_parties = card-body("responsibilities_of_the_parties")
|
|
73
|
+
#let personnel = card-body("personnel")
|
|
74
|
+
#let general_provisions = card-body("general_provisions")
|
|
75
|
+
#let financial_details = card-body("financial_details")
|
|
76
|
+
#let list_of_attachments = card-body("list_of_attachments")
|
|
77
|
+
|
|
78
|
+
// ── Mainmatter: every standard section is its own card, spliced together
|
|
79
|
+
// here in DoDI 4000.19 document order ─────────────────────────────────────
|
|
80
|
+
#mainmatter[
|
|
81
|
+
#if background != none [#background]
|
|
82
|
+
#if authorities != none [#authorities]
|
|
83
|
+
#if purpose_and_scope != none [#purpose_and_scope]
|
|
84
|
+
#if responsibilities_of_the_parties != none [#responsibilities_of_the_parties]
|
|
85
|
+
#if personnel != none [#personnel]
|
|
86
|
+
#if general_provisions != none [#general_provisions]
|
|
87
|
+
#if financial_details != none [#financial_details]
|
|
88
|
+
#if list_of_attachments != none [#list_of_attachments]
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
// ── Backmatter: AGREED signature blocks + mid-point review ─────────────────
|
|
92
|
+
#backmatter(
|
|
93
|
+
first_party_name: resolved.first_party_name,
|
|
94
|
+
second_party_name: resolved.second_party_name,
|
|
95
|
+
first_party_signatory: resolved.first_party_signatory,
|
|
96
|
+
second_party_signatory: resolved.second_party_signatory,
|
|
97
|
+
mid_point_review_due_date: resolved.mid_point_review_due_date,
|
|
98
|
+
first_signing_field: signature-field("first_party_signature"),
|
|
99
|
+
second_signing_field: signature-field("second_party_signature"),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
// ── Attachment A: only for reimbursable MOAs ────────────────────────────────
|
|
103
|
+
#if resolved.reimbursable { attachment(resolved) }
|
|
104
|
+
|
|
105
|
+
// ── Attachments B, C, ...: filter to attachment cards first, then letter them
|
|
106
|
+
// by their position among *attachments*. The standard sections above are also
|
|
107
|
+
// cards in the same `$cards` array, so enumerating the array as a whole would
|
|
108
|
+
// letter the first attachment by its global index (J, not B).
|
|
109
|
+
#let attachment_cards = data.at("$cards").filter(card => card.at("$kind", default: none) == "attachment")
|
|
110
|
+
#for (i, card) in attachment_cards.enumerate() {
|
|
111
|
+
// The quillmark helper leaves an unset/whitespace-only markdown body as
|
|
112
|
+
// the empty string `""`; only non-empty bodies are eval'd into content.
|
|
113
|
+
let body = card.at("$body", default: "")
|
|
114
|
+
let body_content = if type(body) == str { [] } else { body }
|
|
115
|
+
attachment-card(numbering("A", i + 2), resolved, trim-inline(card.title), body_content)
|
|
116
|
+
}
|
|
@@ -6,25 +6,25 @@
|
|
|
6
6
|
// Map snake_case Quill `data` keys to generated `form.typ` parameter names.
|
|
7
7
|
#let vals = (:)
|
|
8
8
|
|
|
9
|
-
#if
|
|
10
|
-
#if
|
|
11
|
-
#if
|
|
12
|
-
#if
|
|
9
|
+
#if data.award != "" { vals.insert("AWARD", data.award) }
|
|
10
|
+
#if data.category_if_applicable != "" { vals.insert("CATEGORY_If_Applicable", data.category_if_applicable) }
|
|
11
|
+
#if data.award_period != "" { vals.insert("AWARD_PERIOD", data.award_period) }
|
|
12
|
+
#if data.rankname_of_nominee != "" {
|
|
13
13
|
vals.insert("RANKNAME_OF_NOMINEE_First_Middle_Initial_Last", data.rankname_of_nominee)
|
|
14
14
|
vals.insert("RANKNAME_OF_NOMINEE_First_Middle_Initial_Last_2", data.rankname_of_nominee)
|
|
15
15
|
}
|
|
16
|
-
#if
|
|
17
|
-
#if
|
|
18
|
-
#if
|
|
16
|
+
#if data.majcom_fldcom_foa_or_dru != "" { vals.insert("MAJCOM_FLDCOM_FOA_OR_DRU", data.majcom_fldcom_foa_or_dru) }
|
|
17
|
+
#if data.dafsc_duty_title != "" { vals.insert("DAFSCDUTY_TITLE", data.dafsc_duty_title) }
|
|
18
|
+
#if data.nominees_telephone_dsn_commercial != "" {
|
|
19
19
|
vals.insert("NOMINEES_TELEPHONE__DSN__Commercial", data.nominees_telephone_dsn_commercial)
|
|
20
20
|
}
|
|
21
|
-
#if
|
|
21
|
+
#if data.unit_office_symbol_street_address_base_state_zip != "" {
|
|
22
22
|
vals.insert(
|
|
23
23
|
"UNITOFFICE_SYMBOLSTREET_ADDRESSBASESTATEZIP_CODE",
|
|
24
24
|
data.unit_office_symbol_street_address_base_state_zip,
|
|
25
25
|
)
|
|
26
26
|
}
|
|
27
|
-
#if
|
|
27
|
+
#if data.unit_commander_rank_name_and_telephone != "" {
|
|
28
28
|
vals.insert(
|
|
29
29
|
"RANKNAME_OF_UNIT_COMMANDER_First_Middle_Initial_LastCOMMANDERS_TELEPHONE_DSN__Commercial",
|
|
30
30
|
data.unit_commander_rank_name_and_telephone,
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
#let continued-key = "SPECIFIC_ACCOMPLISHMENTS_Use_Performance_Statements_IAW_DAFMAN_362806_Continued"
|
|
41
41
|
#for card in data.at("$cards") {
|
|
42
|
-
if card.at("$kind") == "accomplishments_continued" and continued-key not in vals and "$body" in card {
|
|
42
|
+
if card.at("$kind", default: none) == "accomplishments_continued" and continued-key not in vals and "$body" in card {
|
|
43
43
|
vals.insert(continued-key, card.at("$body"))
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -7,19 +7,19 @@
|
|
|
7
7
|
#let vals = (:)
|
|
8
8
|
|
|
9
9
|
// Mode of Transportation — enum string drives one of six checkboxes.
|
|
10
|
-
#let mode = data.
|
|
10
|
+
#let mode = data.transportation_mode
|
|
11
11
|
#if mode != "" { vals.insert("mode_" + mode, true) }
|
|
12
12
|
|
|
13
13
|
// Header row
|
|
14
|
-
#if
|
|
15
|
-
#if
|
|
14
|
+
#if data.departure_date != none { vals.insert("departure_date", data.departure_date) }
|
|
15
|
+
#if data.final_destination != "" { vals.insert("final_destination", data.final_destination) }
|
|
16
16
|
|
|
17
17
|
// Proposed Travel Itinerary — one card per row, up to 10 rows.
|
|
18
18
|
#let itin-cols = ("date", "departure_point", "arrival_point", "rest_length", "mileage")
|
|
19
19
|
#{
|
|
20
20
|
let row = 0
|
|
21
21
|
for card in data.at("$cards", default: ()) {
|
|
22
|
-
if card.at("$kind") == "itinerary" and row < 10 {
|
|
22
|
+
if card.at("$kind", default: none) == "itinerary" and row < 10 {
|
|
23
23
|
let n = str(row + 1)
|
|
24
24
|
for col in itin-cols {
|
|
25
25
|
let v = card.at(col, default: none)
|
|
@@ -33,27 +33,27 @@
|
|
|
33
33
|
|
|
34
34
|
// Flight info — bold label prefixes a tentative/confirmed flight number.
|
|
35
35
|
#{
|
|
36
|
-
let d = str(data.
|
|
36
|
+
let d = str(data.dept_flight_num)
|
|
37
37
|
if d != "" { vals.insert("dept_flight", [#text(weight: "bold")[Dept Flight:] #d]) }
|
|
38
|
-
let a = str(data.
|
|
38
|
+
let a = str(data.arrival_flight_num)
|
|
39
39
|
if a != "" { vals.insert("arrival_flight", [#text(weight: "bold")[Arr Flight:] #a]) }
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// Notes
|
|
43
|
-
#if
|
|
43
|
+
#if data.notes != "" { vals.insert("notes", data.notes) }
|
|
44
44
|
|
|
45
45
|
// Acknowledgements
|
|
46
|
-
#if
|
|
47
|
-
#if
|
|
48
|
-
#if
|
|
49
|
-
#if
|
|
46
|
+
#if data.organization != "" { vals.insert("organization", data.organization) }
|
|
47
|
+
#if data.briefed_date != none { vals.insert("briefed_date", data.briefed_date) }
|
|
48
|
+
#if data.briefee_name != "" { vals.insert("briefee_name", data.briefee_name) }
|
|
49
|
+
#if data.briefee_grade != "" { vals.insert("briefee_grade", data.briefee_grade) }
|
|
50
50
|
#if "briefer_name" in data { vals.insert("briefer_name", data.briefer_name) }
|
|
51
51
|
#if "briefer_grade" in data { vals.insert("briefer_grade", data.briefer_grade) }
|
|
52
52
|
|
|
53
53
|
// Emergency contact — bold label prefixes a "name: phone" pair.
|
|
54
54
|
#{
|
|
55
|
-
let en = str(data.
|
|
56
|
-
let ep = str(data.
|
|
55
|
+
let en = str(data.emergency_contact_name)
|
|
56
|
+
let ep = str(data.emergency_contact_phone)
|
|
57
57
|
let contact = if en != "" and ep != "" { en + ": " + ep } else if en != "" { en } else { ep }
|
|
58
58
|
if contact != "" {
|
|
59
59
|
vals.insert("emergency_contact", [#text(weight: "bold")[EMERGENCY CONTACT:] #contact])
|
|
@@ -110,7 +110,6 @@ main:
|
|
|
110
110
|
classification:
|
|
111
111
|
type: enum
|
|
112
112
|
values:
|
|
113
|
-
- ""
|
|
114
113
|
- UNCLASSIFIED
|
|
115
114
|
- CUI
|
|
116
115
|
- CONFIDENTIAL
|
|
@@ -282,7 +281,6 @@ card_kinds:
|
|
|
282
281
|
action:
|
|
283
282
|
type: enum
|
|
284
283
|
values:
|
|
285
|
-
- ""
|
|
286
284
|
- undecided
|
|
287
285
|
- approve
|
|
288
286
|
- disapprove
|
|
@@ -6,49 +6,54 @@
|
|
|
6
6
|
// Letterhead configuration
|
|
7
7
|
letterhead_title: data.letterhead_title,
|
|
8
8
|
letterhead_caption: data.letterhead_caption,
|
|
9
|
-
letterhead_seal_subtitle: data.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
letterhead_seal_subtitle: data.letterhead_seal_subtitle,
|
|
10
|
+
// Enum blank is `""`, not a seal. Omit so the package renders none rather
|
|
11
|
+
// than treating the blank as DoW.
|
|
12
|
+
..if data.letterhead_seal != "" {
|
|
13
|
+
(letterhead_seal: image(
|
|
14
|
+
if data.letterhead_seal == "dod" {
|
|
15
|
+
"assets/dod_seal.png"
|
|
16
|
+
} else {
|
|
17
|
+
"assets/dow_seal.png"
|
|
18
|
+
}
|
|
19
|
+
))
|
|
20
|
+
},
|
|
17
21
|
|
|
18
22
|
// Date
|
|
19
|
-
date: data.
|
|
23
|
+
date: data.date,
|
|
20
24
|
|
|
21
25
|
// Receiver information
|
|
22
26
|
memo_for: data.memo_for,
|
|
23
27
|
|
|
24
28
|
// Sender information (omitted for Memorandum for Record)
|
|
25
|
-
..if data.
|
|
29
|
+
..if data.memo_from.len() > 0 { (memo_from: data.memo_from) },
|
|
26
30
|
|
|
27
31
|
// Subject line
|
|
28
32
|
subject: data.subject,
|
|
29
33
|
|
|
30
34
|
// Optional references
|
|
31
|
-
..if
|
|
35
|
+
..if data.references.len() > 0 { (references: data.references) },
|
|
32
36
|
|
|
33
37
|
// Optional footer tag line
|
|
34
|
-
|
|
38
|
+
footer_tag_line: data.tag_line,
|
|
35
39
|
|
|
36
40
|
// Optional classification level
|
|
37
|
-
..if
|
|
41
|
+
..if data.classification != "" { (classification_level: data.classification) },
|
|
38
42
|
|
|
39
|
-
..if
|
|
43
|
+
..if data.dissemination != "" { (dissemination: data.dissemination) },
|
|
40
44
|
|
|
41
45
|
// CUI designation indicator block fields (DoDM 5200.48)
|
|
42
|
-
..if
|
|
43
|
-
..if
|
|
44
|
-
..if
|
|
45
|
-
..if
|
|
46
|
+
..if data.cui_controlled_by != "" { (cui_controlled_by: data.cui_controlled_by) },
|
|
47
|
+
..if data.cui_category != "" { (cui_category: data.cui_category) },
|
|
48
|
+
..if data.cui_limited_dissemination != "" { (cui_limited_dissemination: data.cui_limited_dissemination) },
|
|
49
|
+
..if data.cui_poc != "" { (cui_poc: data.cui_poc) },
|
|
46
50
|
|
|
47
|
-
// USAF vs DAF memorandum style (date format, body indentation)
|
|
48
|
-
|
|
51
|
+
// USAF vs DAF memorandum style (date format, body indentation). Blank
|
|
52
|
+
// falls through to the package's `"usaf"` default.
|
|
53
|
+
..if data.memo_style != "" { (memo_style: data.memo_style) },
|
|
49
54
|
|
|
50
55
|
// Font size
|
|
51
|
-
font_size: data.
|
|
56
|
+
font_size: data.font_size * 1pt,
|
|
52
57
|
|
|
53
58
|
// List recipients in vertical list
|
|
54
59
|
memo_for_cols: 1,
|
|
@@ -69,18 +74,18 @@
|
|
|
69
74
|
signing_field: signature-field("Signature", field: "signature_block"),
|
|
70
75
|
|
|
71
76
|
// Optional cc
|
|
72
|
-
..if
|
|
77
|
+
..if data.cc.len() > 0 { (cc: data.cc) },
|
|
73
78
|
|
|
74
79
|
// Optional distribution
|
|
75
|
-
..if
|
|
80
|
+
..if data.distribution.len() > 0 { (distribution: data.distribution) },
|
|
76
81
|
|
|
77
82
|
// Optional attachments
|
|
78
|
-
..if
|
|
83
|
+
..if data.attachments.len() > 0 { (attachments: data.attachments) },
|
|
79
84
|
)
|
|
80
85
|
|
|
81
86
|
// Indorsements - iterate through CARDS array and filter by CARD tag
|
|
82
87
|
#for (i, card) in data.at("$cards").enumerate() {
|
|
83
|
-
if card.at("$kind") == "indorsement" {
|
|
88
|
+
if card.at("$kind", default: none) == "indorsement" {
|
|
84
89
|
// The quillmark helper leaves an unset/whitespace-only markdown body as
|
|
85
90
|
// the empty string `""`; only non-empty bodies are eval'd into content.
|
|
86
91
|
// Pass truly empty content (`[]`) in the empty case so indorsement can
|
|
@@ -91,7 +96,7 @@
|
|
|
91
96
|
// it (distinct from the originating memo's date). The signing date is
|
|
92
97
|
// generally unknown at compile time and filled in by hand, so a blank or
|
|
93
98
|
// omitted date renders a fill-in line rather than stamping the compile date.
|
|
94
|
-
let card_date = card.
|
|
99
|
+
let card_date = card.date
|
|
95
100
|
let resolved_date = if card_date == none or card_date == "" {
|
|
96
101
|
none
|
|
97
102
|
} else {
|
|
@@ -111,9 +116,9 @@
|
|
|
111
116
|
"Ind_" + str(i) + "_Signature",
|
|
112
117
|
field: card.at("$path") + "signature_block",
|
|
113
118
|
),
|
|
114
|
-
|
|
119
|
+
..if card.format != "" { (format: card.format) },
|
|
115
120
|
date: resolved_date,
|
|
116
|
-
..if
|
|
121
|
+
..if card.action != "" { (action: card.action) },
|
|
117
122
|
body_content,
|
|
118
123
|
)
|
|
119
124
|
}
|