@tonguetoquill/collection 0.16.1 → 0.17.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 +1 -1
- package/quills/usaf_memo/0.2.0/Quill.yaml +30 -10
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +69 -66
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/config.typ +10 -3
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +15 -2
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +12 -19
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/lib.typ +4 -2
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +3 -3
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +34 -9
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +28 -63
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +1 -1
- package/quills/usaf_memo/0.2.0/plate.typ +5 -1
- package/templates/loc.md +0 -3
- package/templates/usaf_template.md +1 -1
- package/templates/ussf_template.md +2 -2
package/package.json
CHANGED
|
@@ -70,6 +70,15 @@ main:
|
|
|
70
70
|
group: Letterhead
|
|
71
71
|
description: The full organization name of your unit.
|
|
72
72
|
|
|
73
|
+
letterhead_seal_subtitle:
|
|
74
|
+
title: Subtitle under letterhead seal
|
|
75
|
+
type: string
|
|
76
|
+
default: ""
|
|
77
|
+
ui:
|
|
78
|
+
group: Letterhead
|
|
79
|
+
compact: true
|
|
80
|
+
description: Optional line below the DoW seal (bold caps). Leave blank to omit.
|
|
81
|
+
|
|
73
82
|
tag_line:
|
|
74
83
|
title: Tag line at bottom of memo
|
|
75
84
|
type: string
|
|
@@ -118,18 +127,19 @@ main:
|
|
|
118
127
|
ui:
|
|
119
128
|
group: Additional
|
|
120
129
|
description: List attachments in the order they are mentioned in the memo. Briefly describe each; do not use 'as stated' or abbreviations.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
title:
|
|
130
|
+
|
|
131
|
+
memo_style:
|
|
132
|
+
title: Memorandum style
|
|
124
133
|
type: string
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
-
|
|
134
|
+
enum:
|
|
135
|
+
- usaf
|
|
136
|
+
- daf
|
|
137
|
+
default: usaf
|
|
128
138
|
ui:
|
|
129
139
|
group: Additional
|
|
130
140
|
compact: true
|
|
131
|
-
description:
|
|
132
|
-
|
|
141
|
+
description: "USAF memorandum (default) or DAF headquarters memorandum. DAF changes date formatting and body paragraph layout per AFH 33-337."
|
|
142
|
+
|
|
133
143
|
font_size:
|
|
134
144
|
title: Font size for the memo text (int pt)
|
|
135
145
|
type: number
|
|
@@ -147,9 +157,19 @@ main:
|
|
|
147
157
|
default: ""
|
|
148
158
|
ui:
|
|
149
159
|
group: Additional
|
|
160
|
+
compact: true
|
|
150
161
|
description: YYYY-MM-DD. Leave blank to use today's date.
|
|
151
|
-
|
|
152
|
-
|
|
162
|
+
classification:
|
|
163
|
+
title: Classification level of the memo that displays in the banner
|
|
164
|
+
type: string
|
|
165
|
+
default: ""
|
|
166
|
+
examples:
|
|
167
|
+
- CONFIDENTIAL
|
|
168
|
+
ui:
|
|
169
|
+
group: Additional
|
|
170
|
+
compact: true
|
|
171
|
+
description: Follow AFI 31-401 and applicable DoD guidance for classification markings. Leave blank for unclassified.
|
|
172
|
+
|
|
153
173
|
cards:
|
|
154
174
|
indorsement:
|
|
155
175
|
title: Routing indorsement
|
|
@@ -13,29 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
/// Gets the numbering format for a specific paragraph level.
|
|
15
15
|
///
|
|
16
|
-
/// AFH 33-337 "The Text of the Official Memorandum" §2: "Number and letter each
|
|
17
|
-
/// paragraph and subparagraph" with hierarchical numbering implied by examples.
|
|
18
|
-
/// Standard military format follows the pattern: 1., a., (1), (a), etc.
|
|
19
|
-
///
|
|
20
|
-
/// Returns the appropriate numbering format for AFH 33-337 compliant
|
|
21
|
-
/// hierarchical paragraph numbering:
|
|
22
|
-
/// - Level 0: "1." (1., 2., 3., etc.)
|
|
23
|
-
/// - Level 1: "a." (a., b., c., etc.)
|
|
24
|
-
/// - Level 2: "(1)" ((1), (2), (3), etc.)
|
|
25
|
-
/// - Level 3: "(a)" ((a), (b), (c), etc.)
|
|
26
|
-
/// - Level 4+: Underlined format for deeper nesting
|
|
27
|
-
///
|
|
28
16
|
/// - level (int): Paragraph nesting level (0-based)
|
|
29
17
|
/// -> str | function
|
|
30
18
|
#let get-paragraph-numbering-format(level) = {
|
|
31
19
|
paragraph-config.numbering-formats.at(level, default: "i.")
|
|
32
20
|
}
|
|
33
21
|
|
|
34
|
-
/// Calculates indentation
|
|
35
|
-
///
|
|
36
|
-
/// Computes the exact indentation needed for hierarchical paragraph alignment
|
|
37
|
-
/// by measuring the cumulative width of all ancestor paragraph numbers and their
|
|
38
|
-
/// spacing. Uses the provided counter values directly (no Typst counter reads).
|
|
22
|
+
/// Calculates indentation for USAF-style paragraphs from explicit counter values.
|
|
39
23
|
///
|
|
40
24
|
/// - level (int): Paragraph nesting level (0-based)
|
|
41
25
|
/// - level-counts (dictionary): Maps level index strings to their current counter values
|
|
@@ -49,49 +33,52 @@
|
|
|
49
33
|
let ancestor-value = level-counts.at(str(ancestor-level), default: 1)
|
|
50
34
|
let ancestor-format = get-paragraph-numbering-format(ancestor-level)
|
|
51
35
|
let ancestor-number = numbering(ancestor-format, ancestor-value)
|
|
52
|
-
|
|
53
|
-
total-indent += width
|
|
36
|
+
total-indent += measure([#ancestor-number#" "]).width
|
|
54
37
|
}
|
|
55
38
|
total-indent
|
|
56
39
|
}
|
|
57
40
|
|
|
58
|
-
///
|
|
41
|
+
/// Calculates fixed indentation for DAF paragraph levels.
|
|
59
42
|
///
|
|
60
|
-
///
|
|
61
|
-
///
|
|
62
|
-
/// state propagation issues.
|
|
43
|
+
/// First nested level starts at `nested-first-level-indent` (1in); deeper levels
|
|
44
|
+
/// add `nested-step` (0.5in) per additional depth.
|
|
63
45
|
///
|
|
64
|
-
/// - body (content): Paragraph content to format
|
|
65
46
|
/// - level (int): Paragraph nesting level (0-based)
|
|
66
|
-
///
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
let indent-width = calculate-indent-from-counts(level, level-counts)
|
|
73
|
-
[#h(indent-width)#number-text#" "#body]
|
|
47
|
+
/// -> length
|
|
48
|
+
#let calculate-daf-indent(level) = {
|
|
49
|
+
if level <= 0 {
|
|
50
|
+
return 0pt
|
|
51
|
+
}
|
|
52
|
+
daf-paragraph.nested-first-level-indent + (level - 1) * daf-paragraph.nested-step
|
|
74
53
|
}
|
|
75
54
|
|
|
76
|
-
///
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
55
|
+
/// Resets counter entries from `start` upward to 1 in the level-counts dictionary.
|
|
56
|
+
#let reset-levels-from(level-counts, start, max-levels) = {
|
|
57
|
+
for child in range(start, max-levels) {
|
|
58
|
+
level-counts.insert(str(child), 1)
|
|
59
|
+
}
|
|
60
|
+
level-counts
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Formats a paragraph (or continuation) with a given indent strategy.
|
|
82
64
|
///
|
|
83
|
-
/// - body (content):
|
|
84
|
-
/// - level (int):
|
|
65
|
+
/// - body (content): Paragraph content
|
|
66
|
+
/// - level (int): Nesting level (0-based)
|
|
85
67
|
/// - level-counts (dictionary): Current counter values per level
|
|
68
|
+
/// - indent-fn (function): `(level, level-counts) -> length`
|
|
69
|
+
/// - continuation (bool): If true, adds number-label width to alignment
|
|
86
70
|
/// -> content
|
|
87
|
-
#let format-
|
|
88
|
-
let indent-width =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
71
|
+
#let format-par(body, level, level-counts, indent-fn, continuation: false) = {
|
|
72
|
+
let indent-width = indent-fn(level, level-counts)
|
|
73
|
+
if continuation {
|
|
74
|
+
let current-value = level-counts.at(str(level), default: 1)
|
|
75
|
+
let number-text = numbering(get-paragraph-numbering-format(level), current-value)
|
|
76
|
+
[#h(indent-width + measure([#number-text#" "]).width)#body]
|
|
77
|
+
} else {
|
|
78
|
+
let current-value = level-counts.at(str(level), default: 1)
|
|
79
|
+
let number-text = numbering(get-paragraph-numbering-format(level), current-value)
|
|
80
|
+
[#h(indent-width)#number-text#" "#body]
|
|
81
|
+
}
|
|
95
82
|
}
|
|
96
83
|
|
|
97
84
|
// =============================================================================
|
|
@@ -103,7 +90,7 @@
|
|
|
103
90
|
// - "A single paragraph is not numbered" (§2)
|
|
104
91
|
// - First paragraph flush left, never indented
|
|
105
92
|
// - Indent sub-paragraphs to align with first character of parent paragraph text
|
|
106
|
-
#let render-body(content, auto-numbering: true) = {
|
|
93
|
+
#let render-body(content, auto-numbering: true, memo-style: "usaf") = {
|
|
107
94
|
let PAR_BUFFER = state("PAR_BUFFER")
|
|
108
95
|
PAR_BUFFER.update(())
|
|
109
96
|
let NEST_DOWN = counter("NEST_DOWN")
|
|
@@ -254,6 +241,11 @@
|
|
|
254
241
|
|
|
255
242
|
// Format based on element kind
|
|
256
243
|
let nest_level = item.nest_level
|
|
244
|
+
let indent-fn = if memo-style == "daf" {
|
|
245
|
+
(level, _counts) => calculate-daf-indent(level)
|
|
246
|
+
} else {
|
|
247
|
+
(level, counts) => calculate-indent-from-counts(level, counts)
|
|
248
|
+
}
|
|
257
249
|
let final_par = {
|
|
258
250
|
if kind == "table" {
|
|
259
251
|
render-memo-table(item_content)
|
|
@@ -261,22 +253,37 @@
|
|
|
261
253
|
// Continuation block within a multi-block list item:
|
|
262
254
|
// indent to align with preceding numbered paragraph's text, no new number.
|
|
263
255
|
// level-counts still holds the value of the preceding numbered paragraph.
|
|
264
|
-
if
|
|
265
|
-
|
|
256
|
+
if memo-style == "daf" {
|
|
257
|
+
if nest_level > 0 {
|
|
258
|
+
format-par(item_content, nest_level, level-counts, indent-fn, continuation: true)
|
|
259
|
+
} else {
|
|
260
|
+
item_content
|
|
261
|
+
}
|
|
262
|
+
} else if auto-numbering {
|
|
263
|
+
format-par(item_content, nest_level, level-counts, indent-fn, continuation: true)
|
|
266
264
|
} else if nest_level > 0 {
|
|
267
|
-
format-
|
|
265
|
+
format-par(item_content, nest_level - 1, level-counts, indent-fn, continuation: true)
|
|
268
266
|
} else {
|
|
269
267
|
item_content
|
|
270
268
|
}
|
|
269
|
+
} else if memo-style == "daf" {
|
|
270
|
+
if nest_level > 0 {
|
|
271
|
+
let par = format-par(item_content, nest_level, level-counts, indent-fn)
|
|
272
|
+
level-counts.insert(str(nest_level), level-counts.at(str(nest_level), default: 1) + 1)
|
|
273
|
+
level-counts = reset-levels-from(level-counts, nest_level + 1, max-levels)
|
|
274
|
+
par
|
|
275
|
+
} else {
|
|
276
|
+
// DAF top-level paragraphs are unnumbered and first-line indented.
|
|
277
|
+
// Reset nested counters so each new top-level paragraph restarts children.
|
|
278
|
+
level-counts = reset-levels-from(level-counts, 0, max-levels)
|
|
279
|
+
[#h(daf-paragraph.top-first-line-indent)#item_content]
|
|
280
|
+
}
|
|
271
281
|
} else if auto-numbering {
|
|
272
282
|
if par_count > 1 {
|
|
273
283
|
// Apply paragraph numbering per AFH 33-337 §2
|
|
274
|
-
let par = format-
|
|
275
|
-
// Advance counter for this level and reset child levels
|
|
284
|
+
let par = format-par(item_content, nest_level, level-counts, indent-fn)
|
|
276
285
|
level-counts.insert(str(nest_level), level-counts.at(str(nest_level)) + 1)
|
|
277
|
-
|
|
278
|
-
level-counts.insert(str(child), 1)
|
|
279
|
-
}
|
|
286
|
+
level-counts = reset-levels-from(level-counts, nest_level + 1, max-levels)
|
|
280
287
|
par
|
|
281
288
|
} else {
|
|
282
289
|
// AFH 33-337 §2: "A single paragraph is not numbered"
|
|
@@ -286,18 +293,14 @@
|
|
|
286
293
|
// Unnumbered mode: only explicitly nested items (enum/list) get numbered
|
|
287
294
|
if nest_level > 0 {
|
|
288
295
|
let effective_level = nest_level - 1
|
|
289
|
-
let par = format-
|
|
296
|
+
let par = format-par(item_content, effective_level, level-counts, indent-fn)
|
|
290
297
|
level-counts.insert(str(effective_level), level-counts.at(str(effective_level)) + 1)
|
|
291
|
-
|
|
292
|
-
level-counts.insert(str(child), 1)
|
|
293
|
-
}
|
|
298
|
+
level-counts = reset-levels-from(level-counts, effective_level + 1, max-levels)
|
|
294
299
|
par
|
|
295
300
|
} else {
|
|
296
|
-
// Base-level paragraphs are flush left with no numbering
|
|
297
|
-
// Reset all child level counters so subsequent list items restart at 1
|
|
298
|
-
|
|
299
|
-
level-counts.insert(str(child), 1)
|
|
300
|
-
}
|
|
301
|
+
// Base-level paragraphs are flush left with no numbering.
|
|
302
|
+
// Reset all child level counters so subsequent list items restart at 1.
|
|
303
|
+
level-counts = reset-levels-from(level-counts, 0, max-levels)
|
|
301
304
|
item_content
|
|
302
305
|
}
|
|
303
306
|
}
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
// =============================================================================
|
|
22
22
|
// AFH 33-337 §5: "Use 12 point Times New Roman font for text"
|
|
23
23
|
|
|
24
|
-
#let DEFAULT_LETTERHEAD_FONTS = ("
|
|
25
|
-
#let DEFAULT_BODY_FONTS = ("times new roman"
|
|
24
|
+
#let DEFAULT_LETTERHEAD_FONTS = ("NimbusRomNo9L", "times new roman")
|
|
25
|
+
#let DEFAULT_BODY_FONTS = ("NimbusRomNo9L", "times new roman") // AFH 33-337 §5: Times New Roman required
|
|
26
26
|
#let LETTERHEAD_COLOR = rgb("#204093") // Faded USAF blue for letterhead
|
|
27
27
|
|
|
28
28
|
// =============================================================================
|
|
@@ -39,6 +39,14 @@
|
|
|
39
39
|
numbering-formats: ("1.", "a.", "(1)", "(a)", n => underline(str(n)), n => underline(str(n))),
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
+
// DAF (Headquarters) memo body: first-line indent for unnumbered paragraphs; nested
|
|
43
|
+
// items start at 1in, then +0.5in per additional nesting depth.
|
|
44
|
+
#let daf-paragraph = (
|
|
45
|
+
top-first-line-indent: 0.5in,
|
|
46
|
+
nested-first-level-indent: 1in,
|
|
47
|
+
nested-step: 0.5in,
|
|
48
|
+
)
|
|
49
|
+
|
|
42
50
|
// =============================================================================
|
|
43
51
|
// COUNTERS
|
|
44
52
|
// =============================================================================
|
|
@@ -58,7 +66,6 @@
|
|
|
58
66
|
|
|
59
67
|
#let CLASSIFICATION_COLORS = (
|
|
60
68
|
"UNCLASSIFIED": rgb(0, 122, 51), // Forest green (#007A33)
|
|
61
|
-
"CONFIDENTIAL": rgb(0, 51, 160), // Deep blue (#0033A0)
|
|
62
69
|
"SECRET": rgb(200, 16, 46), // Crimson red (#C8102E)
|
|
63
70
|
"TOP SECRET": rgb(255, 103, 31), // Burnt orange (#FF671F)
|
|
64
71
|
)
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
letterhead_title: "DEPARTMENT OF THE AIR FORCE",
|
|
19
19
|
letterhead_caption: "[YOUR SQUADRON/UNIT NAME]",
|
|
20
20
|
letterhead_seal: none,
|
|
21
|
+
letterhead_seal_subtitle: none, // optional line under seal (9pt bold caps); ignored if no seal
|
|
21
22
|
letterhead_font: DEFAULT_LETTERHEAD_FONTS,
|
|
22
23
|
body_font: DEFAULT_BODY_FONTS,
|
|
23
24
|
font_size: 12pt,
|
|
@@ -25,11 +26,16 @@
|
|
|
25
26
|
classification_level: none,
|
|
26
27
|
footer_tag_line: none,
|
|
27
28
|
auto_numbering: true,
|
|
29
|
+
memo_style: "usaf",
|
|
28
30
|
it,
|
|
29
31
|
) = {
|
|
30
32
|
assert(subject != none, message: "subject is required")
|
|
31
33
|
assert(memo_for != none, message: "memo_for is required")
|
|
32
34
|
assert(memo_from != none, message: "memo_from is required")
|
|
35
|
+
assert(
|
|
36
|
+
memo_style in ("usaf", "daf"),
|
|
37
|
+
message: "memo_style must be \"usaf\" or \"daf\"",
|
|
38
|
+
)
|
|
33
39
|
|
|
34
40
|
let actual_date = if date == none { datetime.today() } else { date }
|
|
35
41
|
let classification_color = get-classification-level-color(classification_level)
|
|
@@ -89,13 +95,19 @@
|
|
|
89
95
|
},
|
|
90
96
|
)
|
|
91
97
|
|
|
92
|
-
render-letterhead(
|
|
98
|
+
render-letterhead(
|
|
99
|
+
letterhead_title,
|
|
100
|
+
letterhead_caption,
|
|
101
|
+
letterhead_font,
|
|
102
|
+
letterhead-seal: letterhead_seal,
|
|
103
|
+
letterhead-seal-subtitle: letterhead_seal_subtitle,
|
|
104
|
+
)
|
|
93
105
|
|
|
94
106
|
// AFH 33-337 "Date": "Place the date 1 inch from the right edge, 1.75 inches from the top"
|
|
95
107
|
// Since we have a 1-inch top margin, we need (1.75in - margin) vertical space
|
|
96
108
|
v(1.75in - spacing.margin)
|
|
97
109
|
|
|
98
|
-
render-date-section(actual_date)
|
|
110
|
+
render-date-section(actual_date, memo-style: memo_style)
|
|
99
111
|
render-for-section(memo_for, memo_for_cols)
|
|
100
112
|
render-from-section(memo_from)
|
|
101
113
|
render-subject-section(subject)
|
|
@@ -108,6 +120,7 @@
|
|
|
108
120
|
body_font: body_font,
|
|
109
121
|
font_size: font_size,
|
|
110
122
|
auto_numbering: auto_numbering,
|
|
123
|
+
memo_style: memo_style,
|
|
111
124
|
))
|
|
112
125
|
|
|
113
126
|
it
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
|
|
52
52
|
context {
|
|
53
53
|
let config = query(metadata).last().value
|
|
54
|
+
let memo-style = config.at("memo_style", default: "usaf")
|
|
54
55
|
let original_subject = config.subject
|
|
55
56
|
let original_date = config.original_date
|
|
56
57
|
let original_from = config.original_from
|
|
@@ -61,12 +62,12 @@
|
|
|
61
62
|
|
|
62
63
|
if format == "separate_page" {
|
|
63
64
|
pagebreak()
|
|
64
|
-
[#indorsement_label to #original_from, #display-date(original_date), #original_subject]
|
|
65
|
+
[#indorsement_label to #original_from, #display-date(original_date, memo-style: memo-style), #original_subject]
|
|
65
66
|
|
|
66
67
|
blank-line()
|
|
67
68
|
grid(
|
|
68
69
|
columns: (auto, 1fr),
|
|
69
|
-
ind_from, align(right)[#display-date(actual_date)],
|
|
70
|
+
ind_from, align(right)[#display-date(actual_date, memo-style: memo-style)],
|
|
70
71
|
)
|
|
71
72
|
|
|
72
73
|
blank-line()
|
|
@@ -78,7 +79,7 @@
|
|
|
78
79
|
blank-line()
|
|
79
80
|
grid(
|
|
80
81
|
columns: (auto, 1fr),
|
|
81
|
-
[#indorsement_label, #ind_from], align(right)[#display-date(actual_date)],
|
|
82
|
+
[#indorsement_label, #ind_from], align(right)[#display-date(actual_date, memo-style: memo-style)],
|
|
82
83
|
)
|
|
83
84
|
|
|
84
85
|
blank-line()
|
|
@@ -96,23 +97,15 @@
|
|
|
96
97
|
render-action-line(action)
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
|
|
100
|
+
context {
|
|
101
|
+
let memo-style = {
|
|
102
|
+
let items = query(metadata)
|
|
103
|
+
if items.len() > 0 { items.last().value.at("memo_style", default: "usaf") } else { "usaf" }
|
|
104
|
+
}
|
|
105
|
+
render-body(content, memo-style: memo-style)
|
|
106
|
+
}
|
|
100
107
|
|
|
101
108
|
render-signature-block(signature_block, signature-blank-lines: signature_blank_lines)
|
|
102
109
|
|
|
103
|
-
|
|
104
|
-
calculate-backmatter-spacing(true)
|
|
105
|
-
let attachment-count = attachments.len()
|
|
106
|
-
let section-label = if attachment-count == 1 { "Attachment:" } else { str(attachment-count) + " Attachments:" }
|
|
107
|
-
let continuation-label = (
|
|
108
|
-
(if attachment-count == 1 { "Attachment" } else { str(attachment-count) + " Attachments" })
|
|
109
|
-
+ " (listed on next page):"
|
|
110
|
-
)
|
|
111
|
-
render-backmatter-section(attachments, section-label, numbering-style: "1.", continuation-label: continuation-label)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if not falsey(cc) {
|
|
115
|
-
calculate-backmatter-spacing(falsey(attachments))
|
|
116
|
-
render-backmatter-section(cc, "cc:")
|
|
117
|
-
}
|
|
110
|
+
render-backmatter-sections(attachments: attachments, cc: cc)
|
|
118
111
|
}
|
|
@@ -22,18 +22,20 @@
|
|
|
22
22
|
//
|
|
23
23
|
// Basic usage:
|
|
24
24
|
//
|
|
25
|
-
// #import "@preview/tonguetoquill-usaf-memo:
|
|
25
|
+
// #import "@preview/tonguetoquill-usaf-memo:3.0.0": frontmatter, mainmatter, backmatter, indorsement
|
|
26
26
|
//
|
|
27
27
|
// #show: frontmatter.with(
|
|
28
28
|
// subject: "Your Subject Here",
|
|
29
29
|
// memo_for: ("OFFICE/SYMBOL",),
|
|
30
30
|
// memo_from: ("YOUR/SYMBOL",),
|
|
31
|
+
// memo_style: "usaf", // "usaf" (default) or "daf"
|
|
31
32
|
// )
|
|
32
33
|
//
|
|
33
34
|
// #show: mainmatter
|
|
34
35
|
//
|
|
35
36
|
// Your memo body content here.
|
|
36
|
-
// (
|
|
37
|
+
// (USAF style auto-numbers per AFH 33-337; DAF style uses unnumbered
|
|
38
|
+
// top-level paragraphs with fixed 0.5in first-line indentation)
|
|
37
39
|
//
|
|
38
40
|
// #backmatter(
|
|
39
41
|
// signature_block: ("NAME, Rank, USAF", "Title"),
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// This module implements the mainmatter (body text) of a USAF memorandum per
|
|
4
4
|
// AFH 33-337 Chapter 14 "The Text of the Official Memorandum" (§1-12).
|
|
5
5
|
|
|
6
|
-
#import "primitives.typ": *
|
|
7
6
|
#import "body.typ": *
|
|
8
7
|
|
|
9
8
|
/// Mainmatter show rule for USAF memorandum body content.
|
|
@@ -19,7 +18,7 @@
|
|
|
19
18
|
/// of the memorandum. Automatically detects single vs. multiple paragraphs
|
|
20
19
|
/// to comply with AFH 33-337 numbering requirements.
|
|
21
20
|
///
|
|
22
|
-
/// When auto_numbering is false (set in frontmatter), base-level paragraphs
|
|
21
|
+
/// When `auto_numbering` is false (set in frontmatter), base-level paragraphs
|
|
23
22
|
/// render flush left without numbering. Only explicitly numbered or bulleted
|
|
24
23
|
/// items enter the numbering hierarchy.
|
|
25
24
|
///
|
|
@@ -28,5 +27,6 @@
|
|
|
28
27
|
#let mainmatter(it) = context {
|
|
29
28
|
let config = query(metadata).last().value
|
|
30
29
|
let auto-numbering = config.at("auto_numbering", default: true)
|
|
31
|
-
|
|
30
|
+
let memo-style = config.at("memo_style", default: "usaf")
|
|
31
|
+
render-body(it, auto-numbering: auto-numbering, memo-style: memo-style)
|
|
32
32
|
}
|
|
@@ -14,9 +14,18 @@
|
|
|
14
14
|
// Letterhead placement is not explicitly specified in AFH 33-337, but follows
|
|
15
15
|
// standard USAF memo formatting conventions
|
|
16
16
|
|
|
17
|
-
#let render-letterhead(
|
|
17
|
+
#let render-letterhead(
|
|
18
|
+
title,
|
|
19
|
+
caption,
|
|
20
|
+
font,
|
|
21
|
+
letterhead-seal: none,
|
|
22
|
+
letterhead-seal-subtitle: none,
|
|
23
|
+
) = {
|
|
18
24
|
font = ensure-array(font)
|
|
25
|
+
title = ensure-string(title)
|
|
19
26
|
caption = ensure-string(caption)
|
|
27
|
+
title = upper(title)
|
|
28
|
+
caption = upper(caption)
|
|
20
29
|
|
|
21
30
|
place(
|
|
22
31
|
dy: 0.625in - spacing.margin,
|
|
@@ -28,7 +37,7 @@
|
|
|
28
37
|
#place(
|
|
29
38
|
center + top,
|
|
30
39
|
align(center)[
|
|
31
|
-
#set text(12pt, font: font, fill: LETTERHEAD_COLOR)
|
|
40
|
+
#set text(12pt, font: font, fill: LETTERHEAD_COLOR, weight: "bold")
|
|
32
41
|
#title\
|
|
33
42
|
#text(10.5pt)[#caption]
|
|
34
43
|
],
|
|
@@ -38,13 +47,27 @@
|
|
|
38
47
|
)
|
|
39
48
|
|
|
40
49
|
if letterhead-seal != none {
|
|
50
|
+
let seal-body = if falsey(letterhead-seal-subtitle) {
|
|
51
|
+
block[
|
|
52
|
+
#fit-box(width: 2in, height: 1in)[#letterhead-seal]
|
|
53
|
+
]
|
|
54
|
+
} else {
|
|
55
|
+
block(width: 2in)[
|
|
56
|
+
#align(left)[
|
|
57
|
+
#stack(spacing: 0.15em)[
|
|
58
|
+
#fit-box(width: 2in, height: 1in)[#letterhead-seal]
|
|
59
|
+
#text(9pt, font: font, fill: LETTERHEAD_COLOR, weight: "bold")[
|
|
60
|
+
#upper(ensure-string(letterhead-seal-subtitle))
|
|
61
|
+
]
|
|
62
|
+
]
|
|
63
|
+
]
|
|
64
|
+
]
|
|
65
|
+
}
|
|
41
66
|
place(
|
|
42
67
|
left + top,
|
|
43
68
|
dx: -0.5in,
|
|
44
69
|
dy: -.5in,
|
|
45
|
-
|
|
46
|
-
#fit-box(width: 2in, height: 1in)[#letterhead-seal]
|
|
47
|
-
],
|
|
70
|
+
seal-body,
|
|
48
71
|
)
|
|
49
72
|
}
|
|
50
73
|
}
|
|
@@ -59,8 +82,8 @@
|
|
|
59
82
|
// - SUBJECT: Second line below FROM
|
|
60
83
|
|
|
61
84
|
// AFH 33-337 "Date": "Place the date 1 inch from the right edge, 1.75 inches from the top"
|
|
62
|
-
#let render-date-section(date) = {
|
|
63
|
-
align(right)[#display-date(date)]
|
|
85
|
+
#let render-date-section(date, memo-style: "usaf") = {
|
|
86
|
+
align(right)[#display-date(date, memo-style: memo-style)]
|
|
64
87
|
}
|
|
65
88
|
|
|
66
89
|
// AFH 33-337 "MEMORANDUM FOR": "Place 'MEMORANDUM FOR' on the second line below the date"
|
|
@@ -107,7 +130,7 @@
|
|
|
107
130
|
blank-line()
|
|
108
131
|
grid(
|
|
109
132
|
columns: (auto, auto, 1fr),
|
|
110
|
-
"References:", " ", enum(..references, numbering: "(a)"),
|
|
133
|
+
"References:", " ", enum(..references, numbering: "(a) ", body-indent: 0pt),
|
|
111
134
|
)
|
|
112
135
|
}
|
|
113
136
|
}
|
|
@@ -234,10 +257,12 @@
|
|
|
234
257
|
context {
|
|
235
258
|
let available-space = page.height - here().position().y - 1in
|
|
236
259
|
if measure(formatted-content).height > available-space {
|
|
260
|
+
// Attachments pass continuation-label ("… (listed on next page):" per AFH 33-337).
|
|
261
|
+
// cc: and DISTRIBUTION: use a neutral default — "listed" applies to attachment lists only.
|
|
237
262
|
let continuation-text = if continuation-label != none {
|
|
238
263
|
text()[#continuation-label]
|
|
239
264
|
} else {
|
|
240
|
-
text()[#section-label + " (
|
|
265
|
+
text()[#(section-label + " (continued on next page)")]
|
|
241
266
|
}
|
|
242
267
|
continuation-text
|
|
243
268
|
pagebreak()
|
|
@@ -79,86 +79,51 @@
|
|
|
79
79
|
]
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
///
|
|
82
|
+
/// Formats a date for the memo heading.
|
|
83
83
|
///
|
|
84
|
-
///
|
|
85
|
-
/// -
|
|
86
|
-
/// - Characters at positions 4 and 7 are dashes
|
|
87
|
-
///
|
|
88
|
-
/// - date-str (str): String to check for ISO date pattern
|
|
89
|
-
/// -> bool
|
|
90
|
-
#let is-iso-date-string(date-str) = {
|
|
91
|
-
if date-str.len() == 10 {
|
|
92
|
-
let char4 = date-str.at(4)
|
|
93
|
-
let char7 = date-str.at(7)
|
|
94
|
-
return char4 == "-" and char7 == "-"
|
|
95
|
-
} else if date-str.len() > 10 {
|
|
96
|
-
let char4 = date-str.at(4)
|
|
97
|
-
let char7 = date-str.at(7)
|
|
98
|
-
let char10 = date-str.at(10)
|
|
99
|
-
return char4 == "-" and char7 == "-" and char10 == "T"
|
|
100
|
-
}
|
|
101
|
-
return false
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/// Extracts the date portion (YYYY-MM-DD) from an ISO date string.
|
|
105
|
-
///
|
|
106
|
-
/// Returns the first 10 characters which contain the date portion of an
|
|
107
|
-
/// ISO 8601 date string, removing any time component if present.
|
|
108
|
-
///
|
|
109
|
-
/// - date-str (str): ISO date string to extract from
|
|
110
|
-
/// -> str
|
|
111
|
-
#let extract-iso-date(date-str) = {
|
|
112
|
-
date-str.slice(0, 10)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/// Formats a date in standard military format or ISO format depending on input type.
|
|
116
|
-
///
|
|
117
|
-
/// AFH 33-337 "Date": "Use the 'Day Month Year' or 'DD Mmm YY' format for documents
|
|
118
|
-
/// addressed to a military organization. For civilian addressees, use the 'Month Day, Year' format."
|
|
119
|
-
/// Examples: "15 October 2014" or "15 Oct 14" for military, "October 15, 2014" for civilian
|
|
120
|
-
///
|
|
121
|
-
/// Intelligently handles different date input formats:
|
|
122
|
-
/// - ISO string (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS): Parsed via TOML and displayed in military format
|
|
123
|
-
/// - Non-ISO string: Displayed as-is
|
|
124
|
-
/// - datetime object: Displayed in military format ("1 January 2024")
|
|
84
|
+
/// - String: shown as-is (use for fixed text like placeholders).
|
|
85
|
+
/// - datetime: USAF style `DD Month YYYY`; DAF style `Month DD, YYYY`.
|
|
125
86
|
///
|
|
126
87
|
/// - date (str|datetime): Date to format for display
|
|
127
|
-
///
|
|
128
|
-
|
|
88
|
+
/// - memo-style (str): `"usaf"` or `"daf"`
|
|
89
|
+
/// -> content
|
|
90
|
+
#let display-date(date, memo-style: "usaf") = {
|
|
91
|
+
assert(
|
|
92
|
+
memo-style in ("usaf", "daf"),
|
|
93
|
+
message: "memo-style for display-date must be \"usaf\" or \"daf\"",
|
|
94
|
+
)
|
|
129
95
|
if type(date) == str {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
let parsed = toml(bytes(toml-str))
|
|
135
|
-
parsed.date.display("[day padding:none] [month repr:long] [year]")
|
|
96
|
+
date
|
|
97
|
+
} else {
|
|
98
|
+
let pattern = if memo-style == "daf" {
|
|
99
|
+
"[month repr:long] [day padding:none], [year]"
|
|
136
100
|
} else {
|
|
137
|
-
|
|
101
|
+
"[day padding:none] [month repr:long] [year]"
|
|
138
102
|
}
|
|
139
|
-
|
|
140
|
-
date.display("[day padding:none] [month repr:long] [year]")
|
|
103
|
+
date.display(pattern)
|
|
141
104
|
}
|
|
142
105
|
}
|
|
143
106
|
|
|
144
|
-
/// Gets the color
|
|
107
|
+
/// Gets the banner color for a classification marking.
|
|
108
|
+
///
|
|
109
|
+
/// Matches when `level` (trimmed) starts with a known prefix: TOP SECRET, SECRET, or UNCLASSIFIED.
|
|
110
|
+
/// Otherwise returns black.
|
|
145
111
|
///
|
|
146
|
-
/// - level (str):
|
|
112
|
+
/// - level (str): Marking string shown in header/footer
|
|
147
113
|
/// -> color
|
|
148
114
|
#let get-classification-level-color(level) = {
|
|
149
|
-
if level == none {
|
|
150
|
-
return rgb(0, 0, 0)
|
|
115
|
+
if level == none or type(level) != str {
|
|
116
|
+
return rgb(0, 0, 0)
|
|
151
117
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
118
|
+
let s = level.trim()
|
|
119
|
+
// "TOP SECRET" before "SECRET" so the full phrase matches first.
|
|
120
|
+
let level-order = ("TOP SECRET", "SECRET", "UNCLASSIFIED")
|
|
155
121
|
for base-level in level-order {
|
|
156
|
-
if base-level
|
|
122
|
+
if s.starts-with(base-level) {
|
|
157
123
|
return CLASSIFICATION_COLORS.at(base-level)
|
|
158
124
|
}
|
|
159
125
|
}
|
|
160
|
-
|
|
161
|
-
rgb(0, 0, 0) // Default
|
|
126
|
+
rgb(0, 0, 0)
|
|
162
127
|
}
|
|
163
128
|
|
|
164
129
|
// =============================================================================
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#import "@local/quillmark-helper:0.1.0": data
|
|
2
|
-
#import "@local/tonguetoquill-usaf-memo:
|
|
2
|
+
#import "@local/tonguetoquill-usaf-memo:3.0.0": backmatter, frontmatter, indorsement, mainmatter
|
|
3
3
|
|
|
4
4
|
// Frontmatter configuration
|
|
5
5
|
#show: frontmatter.with(
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
letterhead_title: data.letterhead_title,
|
|
8
8
|
letterhead_caption: data.letterhead_caption,
|
|
9
9
|
letterhead_seal: image("assets/dow_seal.png"),
|
|
10
|
+
letterhead_seal_subtitle: data.at("letterhead_seal_subtitle", default: none),
|
|
10
11
|
|
|
11
12
|
// Date
|
|
12
13
|
date: data.at("date", default: none),
|
|
@@ -29,6 +30,9 @@
|
|
|
29
30
|
// Optional classification level
|
|
30
31
|
..if "classification" in data { (classification_level: data.classification) },
|
|
31
32
|
|
|
33
|
+
// USAF vs DAF memorandum style (date format, body indentation)
|
|
34
|
+
memo_style: data.at("memo_style", default: "usaf"),
|
|
35
|
+
|
|
32
36
|
// Font size
|
|
33
37
|
..if "font_size" in data { (font_size: float(data.font_size) * 1pt) },
|
|
34
38
|
|
package/templates/loc.md
CHANGED
|
@@ -48,7 +48,6 @@ CARD: indorsement
|
|
|
48
48
|
from: SUBJECT RANK FIRST M. LAST
|
|
49
49
|
for: ISSUER ORG/SYMBOL
|
|
50
50
|
signature_block: SUBJECT FIRST M. LAST, RANK, USAF
|
|
51
|
-
date: "Date: _____________"
|
|
52
51
|
---
|
|
53
52
|
|
|
54
53
|
<!-- EDIT: Second Indorsement: Subject's response - choose one option -->
|
|
@@ -59,7 +58,6 @@ CARD: indorsement
|
|
|
59
58
|
from: ISSUER RANK FIRST M. LAST
|
|
60
59
|
for: SUBJECT RANK FIRST M. LAST
|
|
61
60
|
signature_block: ISSUER FIRST M. LAST, RANK, USAF
|
|
62
|
-
date: "Date: _____________"
|
|
63
61
|
---
|
|
64
62
|
|
|
65
63
|
<!-- EDIT: Third Indorsement: Issuer's final decision - choose appropriate options -->
|
|
@@ -70,7 +68,6 @@ CARD: indorsement
|
|
|
70
68
|
from: SUBJECT RANK FIRST M. LAST
|
|
71
69
|
for: ISSUER ORG/SYMBOL
|
|
72
70
|
signature_block: SUBJECT FIRST M. LAST, RANK, USAF
|
|
73
|
-
date: "Date: _____________"
|
|
74
71
|
---
|
|
75
72
|
|
|
76
73
|
<!-- EDIT: Fourth Indorsement: Subject acknowledges final decision -->
|
|
@@ -18,12 +18,12 @@ signature_block:
|
|
|
18
18
|
tag_line: Semper Supra
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
Write your paragraphs here.
|
|
21
|
+
Write your paragraphs here.
|
|
22
22
|
|
|
23
23
|
- Use bullets to nest paragraphs.
|
|
24
24
|
- Indent to go deeper.
|
|
25
25
|
|
|
26
|
-
You can also **bold**, _italicize_, `code`,
|
|
26
|
+
You can also **bold**, _italicize_, `code`, ~~strikethrough~~,
|
|
27
27
|
and [link](https://example.com/) your text.
|
|
28
28
|
|
|
29
29
|
Less formatting. More lethality.
|