@airmark/quiver 0.7.0 → 0.10.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 +20 -12
- package/package.json +6 -15
- package/quills/usaf_memo/0.2.0/Quill.yaml +17 -3
- package/quills/usaf_memo/0.2.0/example.md +2 -2
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +17 -3
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/config.typ +11 -5
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +30 -7
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +8 -1
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +4 -4
- package/quills/usaf_memo/0.2.0/plate.typ +2 -0
- package/index.d.ts +0 -16
- package/index.js +0 -28
package/README.md
CHANGED
|
@@ -17,20 +17,31 @@ and aligned to the current Quillmark spec.
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npm install @airmark/quiver @quillmark/quiver
|
|
20
|
+
npm install @airmark/quiver @quillmark/quiver@^0.6.0 @quillmark/wasm
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
This package ships only the Source Quiver assets (`Quiver.yaml` + `quills/`)
|
|
24
|
+
and exposes no JavaScript API. All loading is performed by `@quillmark/quiver`,
|
|
25
|
+
either from the on-disk package directory or from a packed archive fetched
|
|
26
|
+
over HTTP.
|
|
27
|
+
|
|
23
28
|
## Usage
|
|
24
29
|
|
|
25
|
-
### Load
|
|
30
|
+
### Load from the exported directory
|
|
31
|
+
|
|
32
|
+
Resolve the package's `Quiver.yaml` via `import.meta.resolve` and hand it to
|
|
33
|
+
`@quillmark/quiver`:
|
|
26
34
|
|
|
27
35
|
```ts
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
37
|
+
import path from 'node:path';
|
|
28
38
|
import { Quillmark, Document } from '@quillmark/wasm';
|
|
29
|
-
import {
|
|
39
|
+
import { Quiver } from '@quillmark/quiver/node';
|
|
30
40
|
|
|
31
|
-
const
|
|
32
|
-
const quiver = await
|
|
41
|
+
const quiverYaml = fileURLToPath(import.meta.resolve('@airmark/quiver/Quiver.yaml'));
|
|
42
|
+
const quiver = await Quiver.fromDir(path.dirname(quiverYaml));
|
|
33
43
|
|
|
44
|
+
const engine = new Quillmark();
|
|
34
45
|
const doc = Document.fromMarkdown(`---
|
|
35
46
|
QUILL: usaf_memo@0.2
|
|
36
47
|
memo_for: ["ORG/SYMBOL"]
|
|
@@ -45,14 +56,11 @@ const quill = await quiver.getQuill(doc.quillRef, { engine });
|
|
|
45
56
|
const { artifacts } = quill.render(doc, { format: 'pdf' });
|
|
46
57
|
```
|
|
47
58
|
|
|
48
|
-
### Load
|
|
59
|
+
### Load a packed Quiver via HTTP
|
|
49
60
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const quiver = await Quiver.fromDir(QUIVER_DIR);
|
|
55
|
-
```
|
|
61
|
+
Publish a packed Quiver archive (e.g. as a CDN or GitHub release asset) and
|
|
62
|
+
load it in the browser or any fetch-capable runtime through `@quillmark/quiver`.
|
|
63
|
+
See the upstream `@quillmark/quiver` docs for the packed-format and HTTP API.
|
|
56
64
|
|
|
57
65
|
## Layout
|
|
58
66
|
|
package/package.json
CHANGED
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@airmark/quiver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Airmark Quiver — Air Force and DAF document Quills for Quillmark.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./index.js",
|
|
7
|
-
"types": "./index.d.ts",
|
|
8
6
|
"exports": {
|
|
9
|
-
".":
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
7
|
+
"./Quiver.yaml": "./Quiver.yaml",
|
|
8
|
+
"./quills/*": "./quills/*",
|
|
9
|
+
"./package.json": "./package.json"
|
|
13
10
|
},
|
|
14
11
|
"files": [
|
|
15
12
|
"quills",
|
|
16
|
-
"Quiver.yaml"
|
|
17
|
-
"index.js",
|
|
18
|
-
"index.d.ts"
|
|
13
|
+
"Quiver.yaml"
|
|
19
14
|
],
|
|
20
15
|
"engines": {
|
|
21
16
|
"node": ">=18"
|
|
22
17
|
},
|
|
23
|
-
"peerDependencies": {
|
|
24
|
-
"@quillmark/quiver": ">=0.5.0 <1.0.0",
|
|
25
|
-
"@quillmark/wasm": ">=0.66.2 <1.0.0"
|
|
26
|
-
},
|
|
27
18
|
"devDependencies": {
|
|
28
|
-
"@quillmark/quiver": "^0.
|
|
19
|
+
"@quillmark/quiver": "^0.6.0",
|
|
29
20
|
"@quillmark/wasm": "0.66.2"
|
|
30
21
|
},
|
|
31
22
|
"scripts": {
|
|
@@ -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
|
-
|
|
179
|
-
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
"
|
|
67
|
-
"SECRET": rgb(200, 16, 46),
|
|
68
|
-
"
|
|
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
|
|
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(
|
|
95
|
+
text(12pt, font: DEFAULT_BODY_FONTS, fill: classification_color)[#strong(classification_marking)],
|
|
75
96
|
)
|
|
76
97
|
}
|
|
77
98
|
},
|
|
78
99
|
footer: {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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,
|
|
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"
|
|
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
|
|
package/index.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { Quiver } from '@quillmark/quiver/node';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Absolute path to the root of this Source Quiver (contains `Quiver.yaml`).
|
|
5
|
-
*/
|
|
6
|
-
export declare const QUIVER_DIR: string;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Absolute path to the `quills/` directory inside this Quiver.
|
|
10
|
-
*/
|
|
11
|
-
export declare const QUILLS_DIR: string;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Convenience loader that resolves this Source Quiver through `@quillmark/quiver/node`.
|
|
15
|
-
*/
|
|
16
|
-
export declare function loadAirmarkQuiver(): Promise<Quiver>;
|
package/index.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
|
|
4
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
-
const __dirname = path.dirname(__filename);
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Absolute path to the root of this Source Quiver (contains `Quiver.yaml`).
|
|
9
|
-
*/
|
|
10
|
-
export const QUIVER_DIR = __dirname;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Absolute path to the `quills/` directory inside this Quiver.
|
|
14
|
-
*/
|
|
15
|
-
export const QUILLS_DIR = path.join(__dirname, 'quills');
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Convenience loader that resolves this Source Quiver through `@quillmark/quiver/node`.
|
|
19
|
-
*
|
|
20
|
-
* ```ts
|
|
21
|
-
* import { loadAirmarkQuiver } from '@airmark/quiver';
|
|
22
|
-
* const quiver = await loadAirmarkQuiver();
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
export async function loadAirmarkQuiver() {
|
|
26
|
-
const { Quiver } = await import('@quillmark/quiver/node');
|
|
27
|
-
return Quiver.fromDir(QUIVER_DIR);
|
|
28
|
-
}
|