@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.
Files changed (30) hide show
  1. package/README.md +1 -0
  2. package/package.json +4 -4
  3. package/quills/af4141/0.1.0/plate.typ +11 -19
  4. package/quills/afmc_moa/0.0.1/Quill.yaml +289 -0
  5. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
  6. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
  7. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
  8. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
  9. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
  10. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/attachment.typ +51 -0
  11. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/backmatter.typ +66 -0
  12. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/body.typ +126 -0
  13. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/config.typ +31 -0
  14. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/frontmatter.typ +74 -0
  15. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/lib.typ +41 -0
  16. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/mainmatter.typ +5 -0
  17. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/src/utils.typ +126 -0
  18. package/quills/afmc_moa/0.0.1/packages/typst-afmc-moa/typst.toml +19 -0
  19. package/quills/afmc_moa/0.0.1/plate.typ +116 -0
  20. package/quills/daf1206/0.1.0/plate.typ +10 -10
  21. package/quills/daf4392/0.1.0/plate.typ +13 -13
  22. package/quills/usaf_memo/0.2.0/Quill.yaml +0 -2
  23. package/quills/usaf_memo/0.2.0/plate.typ +33 -28
  24. package/quills/usaf_memo/0.3.0/Quill.yaml +25 -44
  25. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +30 -25
  26. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +18 -6
  27. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/lib.typ +1 -0
  28. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +56 -31
  29. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +41 -33
  30. 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,5 @@
1
+ // mainmatter.typ — freeform body show rule for a DoDI 4000.19 MOA
2
+
3
+ #import "body.typ": render-moa-body
4
+
5
+ #let mainmatter(it) = render-moa-body(it)
@@ -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 "award" in data { vals.insert("AWARD", data.award) }
10
- #if "category_if_applicable" in data { vals.insert("CATEGORY_If_Applicable", data.category_if_applicable) }
11
- #if "award_period" in data { vals.insert("AWARD_PERIOD", data.award_period) }
12
- #if "rankname_of_nominee" in data {
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 "majcom_fldcom_foa_or_dru" in data { vals.insert("MAJCOM_FLDCOM_FOA_OR_DRU", data.majcom_fldcom_foa_or_dru) }
17
- #if "dafsc_duty_title" in data { vals.insert("DAFSCDUTY_TITLE", data.dafsc_duty_title) }
18
- #if "nominees_telephone_dsn_commercial" in data {
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 "unit_office_symbol_street_address_base_state_zip" in data {
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 "unit_commander_rank_name_and_telephone" in data {
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.at("transportation_mode", default: "")
10
+ #let mode = data.transportation_mode
11
11
  #if mode != "" { vals.insert("mode_" + mode, true) }
12
12
 
13
13
  // Header row
14
- #if "departure_date" in data { vals.insert("departure_date", data.departure_date) }
15
- #if "final_destination" in data { vals.insert("final_destination", data.final_destination) }
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.at("dept_flight_num", default: ""))
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.at("arrival_flight_num", default: ""))
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 "notes" in data { vals.insert("notes", data.notes) }
43
+ #if data.notes != "" { vals.insert("notes", data.notes) }
44
44
 
45
45
  // Acknowledgements
46
- #if "organization" in data { vals.insert("organization", data.organization) }
47
- #if "briefed_date" in data { vals.insert("briefed_date", data.briefed_date) }
48
- #if "briefee_name" in data { vals.insert("briefee_name", data.briefee_name) }
49
- #if "briefee_grade" in data { vals.insert("briefee_grade", data.briefee_grade) }
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.at("emergency_contact_name", default: ""))
56
- let ep = str(data.at("emergency_contact_phone", default: ""))
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.at("letterhead_seal_subtitle", default: none),
10
- letterhead_seal: image(
11
- if data.at("letterhead_seal", default: "dow") == "dod" {
12
- "assets/dod_seal.png"
13
- } else {
14
- "assets/dow_seal.png"
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.at("date", default: none),
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.at("memo_from", default: ()).len() > 0 { (memo_from: data.memo_from) },
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 "references" in data { (references: data.references) },
35
+ ..if data.references.len() > 0 { (references: data.references) },
32
36
 
33
37
  // Optional footer tag line
34
- ..if "tag_line" in data { (footer_tag_line: data.tag_line) },
38
+ footer_tag_line: data.tag_line,
35
39
 
36
40
  // Optional classification level
37
- ..if "classification" in data { (classification_level: data.classification) },
41
+ ..if data.classification != "" { (classification_level: data.classification) },
38
42
 
39
- ..if "dissemination" in data { (dissemination: data.dissemination) },
43
+ ..if data.dissemination != "" { (dissemination: data.dissemination) },
40
44
 
41
45
  // CUI designation indicator block fields (DoDM 5200.48)
42
- ..if "cui_controlled_by" in data { (cui_controlled_by: data.cui_controlled_by) },
43
- ..if "cui_category" in data { (cui_category: data.cui_category) },
44
- ..if "cui_limited_dissemination" in data { (cui_limited_dissemination: data.cui_limited_dissemination) },
45
- ..if "cui_poc" in data { (cui_poc: data.cui_poc) },
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
- memo_style: data.at("memo_style", default: "usaf"),
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.at("font_size", default: 12) * 1pt,
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 "cc" in data { (cc: data.cc) },
77
+ ..if data.cc.len() > 0 { (cc: data.cc) },
73
78
 
74
79
  // Optional distribution
75
- ..if "distribution" in data { (distribution: data.distribution) },
80
+ ..if data.distribution.len() > 0 { (distribution: data.distribution) },
76
81
 
77
82
  // Optional attachments
78
- ..if "attachments" in data { (attachments: data.attachments) },
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.at("date", default: none)
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
- format: card.at("format", default: "standard"),
119
+ ..if card.format != "" { (format: card.format) },
115
120
  date: resolved_date,
116
- ..if "action" in card { (action: card.action) },
121
+ ..if card.action != "" { (action: card.action) },
117
122
  body_content,
118
123
  )
119
124
  }