@airmark/quiver 0.15.0 → 0.16.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.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Airmark Quiver — Air Force and DAF document Quills for Quillmark.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "devDependencies": {
19
19
  "@quillmark/quiver": "^0.7.0",
20
- "@quillmark/wasm": "0.75.0"
20
+ "@quillmark/wasm": "0.77.0"
21
21
  },
22
22
  "scripts": {
23
23
  "test": "node --test quiver.test.js",
@@ -4,7 +4,7 @@ quill:
4
4
  backend: typst
5
5
  plate_file: plate.typ
6
6
  example_file: example.md
7
- description: Typesetted USAF Official Memorandum
7
+ description: U.S. Air Force and Space Force Official Memorandum
8
8
 
9
9
  main:
10
10
  body:
@@ -225,13 +225,14 @@ card_types:
225
225
  action:
226
226
  type: string
227
227
  enum:
228
- - none
228
+ - ""
229
229
  - undecided
230
230
  - approve
231
231
  - disapprove
232
+ default: ""
232
233
  ui:
233
234
  group: Addressing
234
- description: "Action taken by the endorser. Use 'none' to hide the Approve/Disapprove line entirely, 'undecided' to display the line with neither option circled, 'approve' or 'disapprove' to circle the selected option."
235
+ description: "Action taken by the endorser. Leave blank to hide the Approve/Disapprove line entirely, 'undecided' to display the line with neither option circled, 'approve' or 'disapprove' to circle the selected option."
235
236
  attachments:
236
237
  type: array
237
238
  ui:
@@ -12,13 +12,14 @@
12
12
  #let backmatter(
13
13
  signature_block: none,
14
14
  signature_blank_lines: 4,
15
+ signing_field: none,
15
16
  attachments: none,
16
17
  cc: none,
17
18
  distribution: none,
18
19
  leading_pagebreak: false,
19
20
  ) = {
20
21
  // Render backmatter sections without paragraph numbering
21
- render-signature-block(signature_block, signature-blank-lines: signature_blank_lines)
22
+ render-signature-block(signature_block, signature-blank-lines: signature_blank_lines, signing-field: signing_field)
22
23
  render-backmatter-sections(
23
24
  attachments: attachments,
24
25
  cc: cc,
@@ -136,14 +136,14 @@
136
136
  LINE_STRIDE.update(line-stride)
137
137
  }
138
138
 
139
- metadata((
139
+ [#metadata((
140
140
  subject: subject,
141
141
  original_date: actual_date,
142
142
  original_from: first-or-value(memo_from),
143
143
  body_font: body_font,
144
144
  font_size: font_size,
145
145
  memo_style: memo_style,
146
- ))
146
+ )) <usaf-memo-config>]
147
147
 
148
148
  render-date-section(actual_date, memo-style: memo_style)
149
149
  render-for-section(memo_for, memo_for_cols)
@@ -18,6 +18,7 @@
18
18
  to: none,
19
19
  signature_block: none,
20
20
  signature_blank_lines: 4,
21
+ signing_field: none,
21
22
  attachments: none,
22
23
  cc: none,
23
24
  date: none,
@@ -57,12 +58,18 @@
57
58
  // or whitespace-only; comparing against `[]` reliably detects that.
58
59
  let body_empty = content == []
59
60
 
61
+ let effective_action = if action == none or type(action) != str or action.trim() == "" {
62
+ none
63
+ } else {
64
+ action
65
+ }
66
+
60
67
  if format != "informal" {
61
68
  // Step the counter BEFORE the context block to avoid read-then-update loop
62
69
  counters.indorsement.step()
63
70
 
64
71
  context {
65
- let config = query(metadata).last().value
72
+ let config = query(<usaf-memo-config>).first().value
66
73
  let memo-style = config.at("memo_style", default: "usaf")
67
74
  let original_subject = config.subject
68
75
  let original_date = config.original_date
@@ -103,27 +110,24 @@
103
110
  }
104
111
  // Header→content gap. Skipped when there is neither an action line nor
105
112
  // body to follow — render-signature-block supplies its own 4-line gap.
106
- if (action != none and action != "none") or not body_empty {
113
+ if effective_action != none or not body_empty {
107
114
  blank-line()
108
115
  }
109
116
  }
110
117
 
111
118
  // Show action line only when an action decision is set (not `none`)
112
- if action != none and action != "none" {
113
- render-action-line(action, trailing-blank-line: not body_empty)
119
+ if effective_action != none {
120
+ render-action-line(effective_action, trailing-blank-line: not body_empty)
114
121
  }
115
122
 
116
123
  if not body_empty {
117
124
  context {
118
- let memo-style = {
119
- let items = query(metadata)
120
- if items.len() > 0 { items.last().value.at("memo_style", default: "usaf") } else { "usaf" }
121
- }
125
+ let memo-style = query(<usaf-memo-config>).first().value.at("memo_style", default: "usaf")
122
126
  render-body(content, memo-style: memo-style)
123
127
  }
124
128
  }
125
129
 
126
- render-signature-block(signature_block, signature-blank-lines: signature_blank_lines)
130
+ render-signature-block(signature_block, signature-blank-lines: signature_blank_lines, signing-field: signing_field)
127
131
 
128
132
  render-backmatter-sections(attachments: attachments, cc: cc)
129
133
  }
@@ -21,7 +21,7 @@
21
21
  /// - content (content): The body content to render
22
22
  /// -> content
23
23
  #let mainmatter(it) = context {
24
- let config = query(metadata).last().value
24
+ let config = query(<usaf-memo-config>).first().value
25
25
  let memo-style = config.at("memo_style", default: "usaf")
26
26
  render-body(it, memo-style: memo-style)
27
27
  }
@@ -54,16 +54,16 @@
54
54
  } else {
55
55
  // Isolate seal column from document `font_size`: stack `em` spacing and subtitle
56
56
  // must not scale with body text (see frontmatter `set text(size: font_size)`).
57
- block(width: 2in)[
57
+ // Subtitle is wrapped in `box` so it stays on one line and may extend past
58
+ // the seal's 2in column rather than wrapping.
59
+ block[
58
60
  #set text(9pt, font: font, fill: LETTERHEAD_COLOR, weight: "bold")
59
- #align(left)[
60
- // Spacing applies between positional stack children only, not one `[…]` body.
61
- #stack(
62
- spacing: 0.5em,
63
- fit-box(width: 2in, height: 1in)[#letterhead-seal],
64
- upper(ensure-string(letterhead-seal-subtitle)),
65
- )
66
- ]
61
+ // Spacing applies between positional stack children only, not one `[…]` body.
62
+ #stack(
63
+ spacing: 0.5em,
64
+ fit-box(width: 2in, height: 1in)[#letterhead-seal],
65
+ box(upper(ensure-string(letterhead-seal-subtitle))),
66
+ )
67
67
  ]
68
68
  }
69
69
  place(
@@ -148,7 +148,7 @@
148
148
  // AFH 33-337 long-name example: "Signature block adjusted to the left" when a
149
149
  // long name would otherwise exceed the right margin.
150
150
 
151
- #let render-signature-block(signature-lines, signature-blank-lines: 4) = {
151
+ #let render-signature-block(signature-lines, signature-blank-lines: 4, signing-field: none) = {
152
152
  signature-lines = ensure-array(signature-lines)
153
153
  // AFH 33-337: "fifth line below" = 4 blank lines between text and signature block.
154
154
  // breakable: false discourages orphaning the signature block onto a page by itself.
@@ -177,6 +177,20 @@
177
177
  default-pad
178
178
  }
179
179
  block(breakable: false)[
180
+ #if signing-field != none {
181
+ let stride = {
182
+ let s = LINE_STRIDE.get()
183
+ if s == none {
184
+ let one-line = measure(par(spacing: 0pt)[x]).height
185
+ measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
186
+ } else { s }
187
+ }
188
+ place(
189
+ dx: left-pad,
190
+ dy: -(stride * signature-blank-lines),
191
+ box(width: body-width - left-pad, height: stride * signature-blank-lines, signing-field),
192
+ )
193
+ }
180
194
  #align(left)[
181
195
  #pad(left: left-pad)[
182
196
  #text(hyphenate: false)[
@@ -196,16 +210,14 @@
196
210
  // ACTION LINE RENDERING
197
211
  // =============================================================================
198
212
  // Renders the Approve / Disapprove action line for indorsement memos.
199
- // action: "none" = no action line displayed (hidden), "undecided" = both options
200
- // rendered plain (no circle), "approve" = Approve circled,
201
- // "disapprove" = Disapprove circled. The action line is rendered when
202
- // action is "undecided", "approve", or "disapprove". "none" suppresses
203
- // the line entirely (handled by the caller).
213
+ // action: "undecided" = both options rendered plain (no circle),
214
+ // "approve" = Approve circled, "disapprove" = Disapprove circled.
215
+ // Empty/none suppression is handled by the caller before this is invoked.
204
216
 
205
217
  #let render-action-line(action, trailing-blank-line: true) = {
206
218
  assert(
207
- action in ("none", "undecided", "approve", "disapprove"),
208
- message: "action must be \"none\", \"undecided\", \"approve\", or \"disapprove\"",
219
+ action in ("undecided", "approve", "disapprove"),
220
+ message: "action must be \"undecided\", \"approve\", or \"disapprove\"",
209
221
  )
210
222
  // No leading blank-line: the caller (indorsement.typ) already emits the
211
223
  // header→content gap once. The action line's `block(sticky: true)`
@@ -1,4 +1,4 @@
1
- #import "@local/quillmark-helper:0.1.0": data
1
+ #import "@local/quillmark-helper:0.1.0": data, signature-field
2
2
  #import "@local/tonguetoquill-usaf-memo:3.0.0": backmatter, frontmatter, indorsement, mainmatter
3
3
 
4
4
  // Frontmatter configuration
@@ -57,6 +57,7 @@
57
57
  #backmatter(
58
58
  // Signature block
59
59
  signature_block: data.signature_block,
60
+ signing_field: signature-field("Signature"),
60
61
 
61
62
  // Optional cc
62
63
  ..if "cc" in data { (cc: data.cc) },
@@ -69,7 +70,7 @@
69
70
  )
70
71
 
71
72
  // Indorsements - iterate through CARDS array and filter by CARD type
72
- #for card in data.CARDS {
73
+ #for (i, card) in data.CARDS.enumerate() {
73
74
  if card.CARD == "indorsement" {
74
75
  // The quillmark helper leaves an unset/whitespace-only markdown body as
75
76
  // the empty string `""`; only non-empty bodies are eval'd into content.
@@ -90,6 +91,7 @@
90
91
  from: card.at("from", default: ""),
91
92
  to: card.at("for", default: ""),
92
93
  signature_block: card.signature_block,
94
+ signing_field: signature-field("Ind_" + str(i) + "_Signature"),
93
95
  ..if "attachments" in card { (attachments: card.attachments) },
94
96
  ..if "cc" in card { (cc: card.cc) },
95
97
  format: card.at("format", default: "standard"),