@airmark/quiver 0.7.0 → 0.8.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@airmark/quiver",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Airmark Quiver — Air Force and DAF document Quills for Quillmark.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -175,13 +175,27 @@ main:
175
175
  classification:
176
176
  title: Classification level of the memo that displays in the banner
177
177
  type: string
178
- default: ""
179
- examples:
178
+ enum:
179
+ - ""
180
+ - UNCLASSIFIED
181
+ - CUI
180
182
  - CONFIDENTIAL
183
+ - SECRET
184
+ - TOP SECRET
185
+ default: ""
186
+ ui:
187
+ group: Additional
188
+ compact: true
189
+ description: "Select the classification marking displayed in the header/footer banner. Banner colors follow DoD/CAPCO marking guidance (UNCLASSIFIED green, CUI/CONFIDENTIAL black, SECRET red, TOP SECRET orange). Follow AFI 31-401 and applicable DoD guidance. Leave blank to omit the banner."
190
+
191
+ dissemination:
192
+ title: Dissemination control marking (portion after //)
193
+ type: string
194
+ default: ""
181
195
  ui:
182
196
  group: Additional
183
197
  compact: true
184
- description: Follow AFI 31-401 and applicable DoD guidance for classification markings. Leave blank for unclassified.
198
+ description: "Optional marking appended after double slash (e.g. NF with classification CUI renders as CUI//NF). Leave blank to show only the classification with no //."
185
199
 
186
200
  card_types:
187
201
  indorsement:
@@ -30,7 +30,7 @@ signature_block:
30
30
  - FIRST M. LAST, Rank, USAF
31
31
  - Duty Title
32
32
  tag_line: Aim High
33
- classification: SECRET//FICTIONAL
33
+ classification: UNCLASSIFIED
34
34
  ---
35
35
 
36
36
  The `usaf_memo` Quill package takes care of all 33-337 formatting details. Focus on the content.
@@ -53,4 +53,4 @@ signature_block:
53
53
  - Duty Title
54
54
  ---
55
55
 
56
- This body and the metdata above are an indorsement card. Multiple or no indorsements cards can be used.
56
+ This body and the metdata above are an indorsement card. Multiple or no indorsements cards can be used.
@@ -199,9 +199,15 @@
199
199
  // item.kind — "par", "heading", "table", or "continuation"
200
200
  context {
201
201
  let heading_buffer = none
202
+ // Filter out zero-width paragraphs so that an empty body (e.g. empty
203
+ // string from a CARD with no text) emits nothing and collapses to zero
204
+ // vertical space. Tables are always kept regardless of measured width.
205
+ let items = PAR_BUFFER.get().filter(item =>
206
+ item.kind == "table" or measure(item.content).width > 0pt
207
+ )
208
+ if items.len() == 0 { return }
202
209
  // Only top-level paragraphs count for AFH 33-337 §2 numbering purposes
203
- let par_count = PAR_BUFFER.get().filter(item => item.kind == "par").len()
204
- let items = PAR_BUFFER.get()
210
+ let par_count = items.filter(item => item.kind == "par").len()
205
211
  let total_count = items.len()
206
212
 
207
213
  // Track paragraph numbers per level manually to avoid nested-context
@@ -319,7 +325,15 @@
319
325
  block(breakable: true)[#final_par]
320
326
  }
321
327
  } else {
322
- final_par
328
+ // Wrap every non-last emission in a plain block so the document-wide
329
+ // `set block(above: spacing.line)` rule contributes the same 0.5em
330
+ // gap above every paragraph, including the first (matching the
331
+ // single-paragraph case which always passes through the
332
+ // `i == total_count` branch) and middle paragraphs (so the gap
333
+ // between the 1st and 2nd doesn't shrink relative to the gap
334
+ // between the 2nd-to-last and last). Bare emission would skip
335
+ // `block.above` entirely and visibly compress the spacing.
336
+ block[#final_par]
323
337
  }
324
338
  }
325
339
  }
@@ -59,11 +59,17 @@
59
59
  // AFH 33-337 §3: "Follow AFI 31-401, Information Security Program Management,
60
60
  // applicable executive orders and DoD guidance for the necessary markings on
61
61
  // classified correspondence."
62
- // Color values follow DoD standard classification marking colors
63
- // Source: https://security.stackexchange.com/questions/161829
62
+ // Color values follow DoD/CAPCO standard classification marking colors:
63
+ // - TOP SECRET: orange (#FF671F)
64
+ // - SECRET: red (#C8102E)
65
+ // - CONFIDENTIAL: black (per project style guide)
66
+ // - CUI: black (DoDM 5200.48; per project style guide)
67
+ // - UNCLASSIFIED: green (#007A33)
64
68
 
65
69
  #let CLASSIFICATION_COLORS = (
66
- "UNCLASSIFIED": rgb(0, 122, 51), // Forest green (#007A33)
67
- "SECRET": rgb(200, 16, 46), // Crimson red (#C8102E)
68
- "TOP SECRET": rgb(255, 103, 31), // Burnt orange (#FF671F)
70
+ "TOP SECRET": rgb(255, 103, 31),
71
+ "SECRET": rgb(200, 16, 46),
72
+ "CONFIDENTIAL": rgb(0, 0, 0),
73
+ "CUI": rgb(0, 0, 0),
74
+ "UNCLASSIFIED": rgb(0, 122, 51),
69
75
  )
@@ -24,6 +24,7 @@
24
24
  font_size: 12pt,
25
25
  memo_for_cols: 3,
26
26
  classification_level: none,
27
+ dissemination: none,
27
28
  footer_tag_line: none,
28
29
  memo_style: "usaf",
29
30
  it,
@@ -37,6 +38,26 @@
37
38
  )
38
39
 
39
40
  let actual_date = if date == none { datetime.today() } else { date }
41
+
42
+ let classification_marking = if classification_level == none or type(classification_level) != str {
43
+ none
44
+ } else {
45
+ let base = classification_level.trim()
46
+ if base == "" {
47
+ none
48
+ } else {
49
+ let disp = if dissemination == none or type(dissemination) != str {
50
+ ""
51
+ } else {
52
+ dissemination.trim()
53
+ }
54
+ if disp != "" {
55
+ base + "//" + upper(disp)
56
+ } else {
57
+ base
58
+ }
59
+ }
60
+ }
40
61
  let classification_color = get-classification-level-color(classification_level)
41
62
 
42
63
  // Document-wide typography settings (inlined from configure())
@@ -67,20 +88,22 @@
67
88
  )
68
89
  }
69
90
 
70
- if classification_level != none {
91
+ if classification_marking != none {
71
92
  place(
72
93
  top + center,
73
94
  dy: 0.375in,
74
- text(12pt, font: DEFAULT_BODY_FONTS, fill: classification_color)[#strong(classification_level)],
95
+ text(12pt, font: DEFAULT_BODY_FONTS, fill: classification_color)[#strong(classification_marking)],
75
96
  )
76
97
  }
77
98
  },
78
99
  footer: {
79
- place(
80
- bottom + center,
81
- dy: -.375in,
82
- text(12pt, font: DEFAULT_BODY_FONTS, fill: classification_color)[#strong(classification_level)],
83
- )
100
+ if classification_marking != none {
101
+ place(
102
+ bottom + center,
103
+ dy: -.375in,
104
+ text(12pt, font: DEFAULT_BODY_FONTS, fill: classification_color)[#strong(classification_marking)],
105
+ )
106
+ }
84
107
 
85
108
  if not falsey(footer_tag_line) {
86
109
  place(
@@ -206,7 +206,10 @@
206
206
  action in ("none", "undecided", "approve", "disapprove"),
207
207
  message: "action must be \"none\", \"undecided\", \"approve\", or \"disapprove\"",
208
208
  )
209
- blank-line()
209
+ // No leading blank-line: the caller (indorsement.typ) already emits the
210
+ // header→content gap once. The action line's `block(sticky: true)`
211
+ // additionally inherits `block.above: spacing.line` so the visual gap
212
+ // above matches the gap above a body's first paragraph.
210
213
  // Circle the selected option using a box with rounded corners
211
214
  // Use baseline parameter to maintain vertical text alignment
212
215
  let approve-text = if action == "approve" {
@@ -227,6 +230,10 @@
227
230
  // using the same sticky-block pattern that body.typ applies to the last
228
231
  // paragraph, per AFH 33-337 §11 orphan-prevention rules.
229
232
  block(sticky: true)[#approve-text / #disapprove-text]
233
+ // Trailing blank-line so the body's first paragraph (or signature block,
234
+ // when the body is empty) sits one line below the action line, mirroring
235
+ // the gap above it.
236
+ blank-line()
230
237
  }
231
238
 
232
239
  // =============================================================================
@@ -114,8 +114,8 @@
114
114
 
115
115
  /// Gets the banner color for a classification marking.
116
116
  ///
117
- /// Matches when `level` (trimmed) starts with a known prefix: TOP SECRET, SECRET, or UNCLASSIFIED.
118
- /// Otherwise returns black.
117
+ /// Matches when `level` (trimmed) starts with a known prefix: TOP SECRET, SECRET,
118
+ /// CONFIDENTIAL, CUI, or UNCLASSIFIED. Otherwise returns black.
119
119
  ///
120
120
  /// - level (str): Marking string shown in header/footer
121
121
  /// -> color
@@ -124,8 +124,8 @@
124
124
  return rgb(0, 0, 0)
125
125
  }
126
126
  let s = level.trim()
127
- // "TOP SECRET" before "SECRET" so the full phrase matches first.
128
- let level-order = ("TOP SECRET", "SECRET", "UNCLASSIFIED")
127
+ // Longest-prefix-first so e.g. "TOP SECRET" wins over "SECRET".
128
+ let level-order = ("TOP SECRET", "SECRET", "CONFIDENTIAL", "CUI", "UNCLASSIFIED")
129
129
  for base-level in level-order {
130
130
  if s.starts-with(base-level) {
131
131
  return CLASSIFICATION_COLORS.at(base-level)
@@ -36,6 +36,8 @@
36
36
  // Optional classification level
37
37
  ..if "classification" in data { (classification_level: data.classification) },
38
38
 
39
+ ..if "dissemination" in data { (dissemination: data.dissemination) },
40
+
39
41
  // USAF vs DAF memorandum style (date format, body indentation)
40
42
  memo_style: data.at("memo_style", default: "usaf"),
41
43