@airmark/quiver 0.35.7 → 0.36.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.
@@ -1,23 +1,21 @@
1
- #import "@local/quillmark-helper:0.1.0": (
2
- data, display, form-field, signature-field,
3
- )
1
+ #import "@local/quillmark-helper:0.2.0" as qm
4
2
  #import "@local/tonguetoquill-usaf-memo:5.0.0": (
5
3
  backmatter, date-pattern, frontmatter, indorsement, mainmatter,
6
4
  )
7
5
 
8
6
  // A memo has no "no style" state, so the blank takes the package's default.
9
7
  // Resolved once here: `frontmatter` and the date's pattern must agree.
10
- #let memo_style = if data.memo_style != "" { data.memo_style } else { "usaf" }
8
+ #let memo_style = if qm.data.memo_style != "" { qm.data.memo_style } else { "usaf" }
11
9
 
12
10
  // The letterhead arrives as one ordered array of lines. Element 0 is the title,
13
11
  // set larger; the rest are caption lines beneath it. A single element renders
14
12
  // the title alone — the package skips a falsey caption. An empty array renders
15
13
  // no letterhead text at all, silently: the block is `place`d, so an empty title
16
14
  // leaves no glyphs and takes no space from the flow, and the seal stands alone.
17
- #let letterhead_lines = data.letterhead_title
15
+ #let letterhead_lines = qm.data.letterhead_title
18
16
 
19
17
  // Body text size, in points. Also the height of a blank date's fill-in widget.
20
- #let body_font_size = data.font_size * 1pt
18
+ #let body_font_size = qm.data.font_size * 1pt
21
19
 
22
20
  // A blank date's fill-in slot: an empty AcroForm text box the signer types the
23
21
  // date into, set in the body face, the value flush right in the date line's
@@ -34,7 +32,7 @@
34
32
  #let date_slot(name, field) = context box(
35
33
  width: date_slot_width,
36
34
  height: measure[0].height,
37
- place(horizon + left, form-field(
35
+ place(horizon + left, qm.form-field(
38
36
  name,
39
37
  type: "text",
40
38
  width: date_slot_width,
@@ -49,10 +47,10 @@
49
47
  #show: frontmatter.with(
50
48
  letterhead-title: letterhead_lines.at(0, default: ""),
51
49
  letterhead-caption: if letterhead_lines.len() > 1 { letterhead_lines.slice(1) } else { () },
52
- letterhead-seal-subtitle: data.letterhead_seal_subtitle,
50
+ letterhead-seal-subtitle: qm.data.letterhead_seal_subtitle,
53
51
  // A memo has no seal-less state, so the blank takes the default, DoW.
54
52
  letterhead-seal: image(
55
- if data.letterhead_seal == "dod" {
53
+ if qm.data.letterhead_seal == "dod" {
56
54
  "assets/dod_seal.png"
57
55
  } else {
58
56
  "assets/dow_seal.png"
@@ -61,44 +59,44 @@
61
59
 
62
60
  // Date. `data.date` is the native `datetime` and would render identically,
63
61
  // but its ink would be born inside the package and carry no schema address.
64
- // `display` places the field's *content* projection instead: the glyphs are
62
+ // `qm.ink` places the field's *content* projection instead: the glyphs are
65
63
  // born in the generated helper, so the memo date stays click-to-edit however
66
64
  // deep the package formats it.
67
65
  //
68
66
  // A blank date is left for the signer: a memo is dated when it is signed,
69
67
  // which is generally not when it is rendered, so the slot is fillable rather
70
68
  // than stamped with the compile date.
71
- date: display("date", date-pattern(memo-style: memo_style)),
69
+ date: qm.ink(qm.data, "date", date-pattern(memo-style: memo_style)),
72
70
  date-field: date_slot("Date", "date"),
73
71
 
74
- memo-for: data.memo_for,
72
+ memo-for: qm.data.memo_for,
75
73
 
76
74
  // A memo with no FROM line is a Memorandum for Record.
77
- ..if data.memo_from.len() > 0 { (memo-from: data.memo_from) },
75
+ ..if qm.data.memo_from.len() > 0 { (memo-from: qm.data.memo_from) },
78
76
 
79
- subject: data.subject,
77
+ subject: qm.data.subject,
80
78
 
81
- ..if data.references.len() > 0 { (references: data.references) },
79
+ ..if qm.data.references.len() > 0 { (references: qm.data.references) },
82
80
 
83
- footer-tag-line: data.tag_line,
81
+ footer-tag-line: qm.data.tag_line,
84
82
 
85
83
  // The blank reads as no banner, which is what the package's own
86
84
  // `classification-level: none` default means.
87
- classification-level: data.classification.value,
85
+ classification-level: qm.data.classification.value,
88
86
 
89
- dissemination: data.dissemination,
87
+ dissemination: qm.data.dissemination,
90
88
 
91
89
  // CUI designation indicator block fields (DoDM 5200.48). `classification`
92
90
  // declares a `CUI` variant, so these four exist only where the discriminant
93
91
  // reads CUI, and the branch is what makes reading them total: inside it every
94
92
  // declared field of that world is present, outside it none is. The package's
95
93
  // own `cui_*: none` defaults cover the worlds that omit them.
96
- ..if data.classification.value == "CUI" {
94
+ ..if qm.data.classification.value == "CUI" {
97
95
  (
98
- cui-controlled-by: data.classification.controlled_by,
99
- cui-category: data.classification.category,
100
- cui-limited-dissemination: data.classification.limited_dissemination,
101
- cui-poc: data.classification.poc,
96
+ cui-controlled-by: qm.data.classification.controlled_by,
97
+ cui-category: qm.data.classification.category,
98
+ cui-limited-dissemination: qm.data.classification.limited_dissemination,
99
+ cui-poc: qm.data.classification.poc,
102
100
  )
103
101
  },
104
102
 
@@ -115,26 +113,26 @@
115
113
  // the rebuilt glyphs keep their spans, which is what the backend reads regions
116
114
  // from.
117
115
  #mainmatter[
118
- #data.at("$body")
116
+ #qm.data.at("$body")
119
117
  ]
120
118
 
121
119
  #backmatter(
122
- authority-line: data.authority_line,
123
- signature-block: data.signature_block,
120
+ authority-line: qm.data.authority_line,
121
+ signature-block: qm.data.signature_block,
124
122
  // The widget sits at the bottom of AFH 33-337's four blank lines and is
125
123
  // sized to two and a half of them, so the line and a half above it stays
126
124
  // clear of the body text without moving the block off the fifth line.
127
- signing-field: signature-field(
125
+ signing-field: qm.signature-field(
128
126
  "Signature",
129
127
  field: "signature_block",
130
128
  height: body_font_size * 2.5,
131
129
  ),
132
130
 
133
- ..if data.cc.len() > 0 { (cc: data.cc) },
131
+ ..if qm.data.cc.len() > 0 { (cc: qm.data.cc) },
134
132
 
135
- ..if data.distribution.len() > 0 { (distribution: data.distribution) },
133
+ ..if qm.data.distribution.len() > 0 { (distribution: qm.data.distribution) },
136
134
 
137
- ..if data.attachments.len() > 0 { (attachments: data.attachments) },
135
+ ..if qm.data.attachments.len() > 0 { (attachments: qm.data.attachments) },
138
136
  )
139
137
 
140
138
  // The action line's wording follows the indorsement's place in the chain, not
@@ -149,7 +147,7 @@
149
147
  // expression ends at a line break stops there, and the next line's leading
150
148
  // `.at(…)` would be read as text.
151
149
  #let last_indorsement_index = (
152
- data
150
+ qm.data
153
151
  .at("$cards")
154
152
  .enumerate()
155
153
  .filter(entry => entry.at(1).at("$kind", default: none) == "indorsement")
@@ -157,7 +155,7 @@
157
155
  .at(-1, default: -1)
158
156
  )
159
157
 
160
- #for (i, card) in data.at("$cards").enumerate() {
158
+ #for (i, card) in qm.data.at("$cards").enumerate() {
161
159
  if card.at("$kind", default: none) == "indorsement" {
162
160
  // The quillmark helper leaves an unset/whitespace-only markdown body as
163
161
  // the empty string `""`; only non-empty bodies are eval'd into content.
@@ -169,11 +167,12 @@
169
167
  // it (distinct from the originating memo's date). The signing date is
170
168
  // generally unknown at compile time, so a blank or omitted date leaves the
171
169
  // date slot fillable rather than stamping the compile date into it.
172
- // `display` takes an address, so a per-card call yields a per-card region
170
+ // `qm.ink` reads the card's own address, so a per-card call yields a per-card region
173
171
  // even though every iteration shares one `card` loop variable, and it
174
172
  // returns `none` for a blank date — the fill-in case below.
175
- let resolved_date = display(
176
- card.at("$path") + "date",
173
+ let resolved_date = qm.ink(
174
+ card,
175
+ "date",
177
176
  date-pattern(memo-style: memo_style),
178
177
  )
179
178
  // The card's `$path` prefix composes its canonical schema addresses
@@ -187,7 +186,7 @@
187
186
  to: card.at("for", default: ""),
188
187
  authority-line: card.authority_line,
189
188
  signature-block: card.signature_block,
190
- signing-field: signature-field(
189
+ signing-field: qm.signature-field(
191
190
  "Ind_" + str(i) + "_Signature",
192
191
  field: card.at("$path") + "signature_block",
193
192
  height: body_font_size * 2.5,
@@ -0,0 +1,56 @@
1
+ ~~~
2
+ $quill: cyber_career_plan@0.1
3
+ $kind: main
4
+ name: Capt John A. Snuffy
5
+ afsc: 17S3
6
+ duty_title: Instructor
7
+ unit: 333 TRS, Keesler AFB
8
+ date_arrived_station: 2025-06-15
9
+ move_year: 2028
10
+ move_cycle: Summer
11
+ commissioning_yg: 2022
12
+ date_of_rank: 2026-05-01
13
+ advanced_degree: MS Cyber Operations, AFIT
14
+ constraints:
15
+ - { note: "Join-spouse, no OCONUS", through: 2029 }
16
+ vectors:
17
+ - track: Vector 1
18
+ tours:
19
+ - { title: 333 TRS/DO, years: 2 }
20
+ - { title: IDE in residence, years: 1, school: true }
21
+ - { title: "Sq/CC, Scott AFB", years: 2 }
22
+ - track: Vector 2
23
+ tours:
24
+ - { title: 17X CFM Staff, years: 2.5 }
25
+ - { title: MAJCOM A3 Division Chief, years: 2 }
26
+ - track: Vector 3
27
+ tours:
28
+ - { title: Joint Staff J6, years: 3 }
29
+ - { title: USCYBERCOM JFHQ-C, years: 2.5 }
30
+ stratifications:
31
+ - { year: 2026, rater: "#1/14 Capts", hlr: "#1/45 Capts" }
32
+ - { year: 2025, rater: "#2/12 Lts", hlr: "#3/40 CGOs" }
33
+ - { year: 2024, rater: "#1/8 Lts, #2/40 Instructors" }
34
+ awards:
35
+ - Wing CGOQ x2
36
+ - HAF A2/6 CGOY 2025
37
+ certifications:
38
+ - SANS FOR577 (GLIR)
39
+ - CISSP
40
+ - Security+
41
+ deployments:
42
+ - 1x short tour, returned Jan 2024
43
+ qualifications:
44
+ operations:
45
+ dodin_ops: true
46
+ dco: { detail: 23 IOS }
47
+ crew_cc: true
48
+ staff:
49
+ instructor: { detail: 333 TRS }
50
+ education:
51
+ sos: { detail: DG }
52
+ cyber_200: { detail: "2025" }
53
+ timeline_start_year: 2026
54
+ ~~~
55
+
56
+ What I want out of this discussion: the DO seat at the 333 TRS when my instructor tour ends, then IDE in residence. I'd rather not go to staff before I've had a DO job.
@@ -61,5 +61,12 @@
61
61
  "description": "Classic single-page resume",
62
62
  "file": "classic_resume.md",
63
63
  "tags": ["resume", "career"]
64
+ },
65
+ {
66
+ "id": "cyber-career-plan",
67
+ "name": "Cyber Career Plan",
68
+ "description": "17X Cyber Operations Officer career plan: current assignment, vectors and record",
69
+ "file": "cyber_career_plan.md",
70
+ "tags": ["career", "cyber", "development"]
64
71
  }
65
72
  ]