@airmark/quiver 0.6.0 → 0.7.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 +1 -1
- package/package.json +3 -3
- package/quills/usaf_memo/0.2.0/Quill.yaml +4 -3
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +23 -39
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/config.typ +0 -4
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +6 -6
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +3 -3
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +1 -6
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +41 -16
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +16 -25
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@airmark/quiver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Airmark Quiver — Air Force and DAF document Quills for Quillmark.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"@quillmark/quiver": ">=0.5.0 <1.0.0",
|
|
25
|
-
"@quillmark/wasm": ">=0.
|
|
25
|
+
"@quillmark/wasm": ">=0.66.2 <1.0.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@quillmark/quiver": "^0.5.0",
|
|
29
|
-
"@quillmark/wasm": "0.
|
|
29
|
+
"@quillmark/wasm": "0.66.2"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"test": "node --test",
|
|
@@ -153,15 +153,16 @@ main:
|
|
|
153
153
|
description: "USAF memorandum (default) or DAF headquarters memorandum. DAF changes date formatting and body paragraph layout per AFH 33-337."
|
|
154
154
|
|
|
155
155
|
font_size:
|
|
156
|
-
title: Font size for the memo text (
|
|
157
|
-
type:
|
|
156
|
+
title: Font size for the memo text (pt)
|
|
157
|
+
type: number
|
|
158
158
|
default: 12
|
|
159
159
|
examples:
|
|
160
160
|
- 12
|
|
161
|
+
- 11.5
|
|
161
162
|
ui:
|
|
162
163
|
group: Additional
|
|
163
164
|
compact: true
|
|
164
|
-
description: Font size for the memo text (pt).
|
|
165
|
+
description: Font size for the memo text (pt). Decimal values (e.g. 11.5) are supported.
|
|
165
166
|
|
|
166
167
|
date:
|
|
167
168
|
title: Date of memo (YYYY-MM-DD); defaults to today
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
// - "A single paragraph is not numbered" (§2)
|
|
91
91
|
// - First paragraph flush left, never indented
|
|
92
92
|
// - Indent sub-paragraphs to align with first character of parent paragraph text
|
|
93
|
-
#let render-body(content,
|
|
93
|
+
#let render-body(content, memo-style: "usaf") = {
|
|
94
94
|
let PAR_BUFFER = state("PAR_BUFFER")
|
|
95
95
|
PAR_BUFFER.update(())
|
|
96
96
|
let NEST_DOWN = counter("NEST_DOWN")
|
|
@@ -214,6 +214,7 @@
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
let i = 0
|
|
217
|
+
let any_emitted = false
|
|
217
218
|
for item in items {
|
|
218
219
|
i += 1
|
|
219
220
|
let kind = item.kind
|
|
@@ -253,18 +254,10 @@
|
|
|
253
254
|
// Continuation block within a multi-block list item:
|
|
254
255
|
// indent to align with preceding numbered paragraph's text, no new number.
|
|
255
256
|
// level-counts still holds the value of the preceding numbered paragraph.
|
|
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)
|
|
264
|
-
} else if nest_level > 0 {
|
|
265
|
-
format-par(item_content, nest_level - 1, level-counts, indent-fn, continuation: true)
|
|
266
|
-
} else {
|
|
257
|
+
if memo-style == "daf" and nest_level == 0 {
|
|
267
258
|
item_content
|
|
259
|
+
} else {
|
|
260
|
+
format-par(item_content, nest_level, level-counts, indent-fn, continuation: true)
|
|
268
261
|
}
|
|
269
262
|
} else if memo-style == "daf" {
|
|
270
263
|
if nest_level > 0 {
|
|
@@ -278,37 +271,28 @@
|
|
|
278
271
|
level-counts = reset-levels-from(level-counts, 0, max-levels)
|
|
279
272
|
[#h(daf-paragraph.top-first-line-indent)#item_content]
|
|
280
273
|
}
|
|
281
|
-
} else if
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
par
|
|
288
|
-
} else {
|
|
289
|
-
// AFH 33-337 §2: "A single paragraph is not numbered"
|
|
290
|
-
item_content
|
|
291
|
-
}
|
|
274
|
+
} else if par_count > 1 {
|
|
275
|
+
// Apply paragraph numbering per AFH 33-337 §2
|
|
276
|
+
let par = format-par(item_content, nest_level, level-counts, indent-fn)
|
|
277
|
+
level-counts.insert(str(nest_level), level-counts.at(str(nest_level)) + 1)
|
|
278
|
+
level-counts = reset-levels-from(level-counts, nest_level + 1, max-levels)
|
|
279
|
+
par
|
|
292
280
|
} else {
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
let effective_level = nest_level - 1
|
|
296
|
-
let par = format-par(item_content, effective_level, level-counts, indent-fn)
|
|
297
|
-
level-counts.insert(str(effective_level), level-counts.at(str(effective_level)) + 1)
|
|
298
|
-
level-counts = reset-levels-from(level-counts, effective_level + 1, max-levels)
|
|
299
|
-
par
|
|
300
|
-
} else {
|
|
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)
|
|
304
|
-
item_content
|
|
305
|
-
}
|
|
281
|
+
// AFH 33-337 §2: "A single paragraph is not numbered"
|
|
282
|
+
item_content
|
|
306
283
|
}
|
|
307
284
|
}
|
|
308
285
|
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
286
|
+
// Blank line between paragraphs. The header→body gap (i.e. before
|
|
287
|
+
// the first emitted paragraph) is the caller's responsibility —
|
|
288
|
+
// emitting it here would put the v() inside a nested
|
|
289
|
+
// `context { for … }` block, where it does not combine with the
|
|
290
|
+
// preceding header section's block-spacing the same way as a
|
|
291
|
+
// top-level blank-line() call.
|
|
292
|
+
// AFH 33-337 §11 rule applied below: "Avoid dividing a paragraph of
|
|
293
|
+
// less than four lines between two pages"
|
|
294
|
+
if any_emitted { blank-line() }
|
|
295
|
+
any_emitted = true
|
|
312
296
|
if i == total_count {
|
|
313
297
|
let available_width = page.width - spacing.margin * 2
|
|
314
298
|
|
|
@@ -9,11 +9,7 @@
|
|
|
9
9
|
// AFH 33-337 specifies precise spacing requirements throughout Chapter 14
|
|
10
10
|
|
|
11
11
|
#let spacing = (
|
|
12
|
-
// Legacy exported field retained for compatibility with downstream imports.
|
|
13
|
-
// Internal blank-line spacing now uses measured line stride from utils.typ.
|
|
14
|
-
vertical: 19.05pt,
|
|
15
12
|
line: .5em, // Internal line spacing for readability (`par.leading`; gap between line boxes)
|
|
16
|
-
line-height: .7em, // Base height of text lines for pagination estimates
|
|
17
13
|
tab: 0.5in, // Tab stop for multi-column recipient alignment
|
|
18
14
|
margin: 1in, // AFH 33-337 §4: "Use 1-inch margins on the left, right and bottom"
|
|
19
15
|
)
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
memo_for_cols: 3,
|
|
26
26
|
classification_level: none,
|
|
27
27
|
footer_tag_line: none,
|
|
28
|
-
auto_numbering: true,
|
|
29
28
|
memo_style: "usaf",
|
|
30
29
|
it,
|
|
31
30
|
) = {
|
|
@@ -107,14 +106,11 @@
|
|
|
107
106
|
// Since we have a 1-inch top margin, we need (1.75in - margin) vertical space
|
|
108
107
|
v(1.75in - spacing.margin)
|
|
109
108
|
|
|
110
|
-
// Measure and cache body line stride once for
|
|
109
|
+
// Measure and cache body line stride once for body line-count heuristics.
|
|
111
110
|
context {
|
|
112
111
|
let one-line = measure(par(spacing: 0pt)[x]).height
|
|
113
112
|
let line-stride = measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
|
|
114
|
-
let em-size = measure(box(width: 1em)[]).width
|
|
115
|
-
let legacy-scaled = spacing.vertical * (em-size / 12pt)
|
|
116
113
|
LINE_STRIDE.update(line-stride)
|
|
117
|
-
BLANK_LINE_STEP.update(calc.max(line-stride, legacy-scaled))
|
|
118
114
|
}
|
|
119
115
|
|
|
120
116
|
metadata((
|
|
@@ -123,7 +119,6 @@
|
|
|
123
119
|
original_from: first-or-value(memo_from),
|
|
124
120
|
body_font: body_font,
|
|
125
121
|
font_size: font_size,
|
|
126
|
-
auto_numbering: auto_numbering,
|
|
127
122
|
memo_style: memo_style,
|
|
128
123
|
))
|
|
129
124
|
|
|
@@ -133,5 +128,10 @@
|
|
|
133
128
|
render-subject-section(subject)
|
|
134
129
|
render-references-section(references)
|
|
135
130
|
|
|
131
|
+
// AFH 33-337: "Begin text on second line below subject/references".
|
|
132
|
+
// Emitted here (not inside body.typ) so the v() lands at the same lexical
|
|
133
|
+
// level as the preceding header sections and combines correctly with their
|
|
134
|
+
// block spacing.
|
|
135
|
+
blank-line()
|
|
136
136
|
it
|
|
137
137
|
}
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
let actual_date =
|
|
44
|
+
let actual_date = date
|
|
45
45
|
let ind_from = first-or-value(from)
|
|
46
46
|
let ind_for = to
|
|
47
47
|
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
blank-line()
|
|
68
68
|
grid(
|
|
69
69
|
columns: (auto, 1fr),
|
|
70
|
-
ind_from, align(right)[#display-date(actual_date, memo-style: memo-style)],
|
|
70
|
+
ind_from, align(right)[#if actual_date != none { display-date(actual_date, memo-style: memo-style) }],
|
|
71
71
|
)
|
|
72
72
|
|
|
73
73
|
blank-line()
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
blank-line()
|
|
80
80
|
grid(
|
|
81
81
|
columns: (auto, 1fr),
|
|
82
|
-
[#indorsement_label, #ind_from], align(right)[#display-date(actual_date, memo-style: memo-style)],
|
|
82
|
+
[#indorsement_label, #ind_from], align(right)[#if actual_date != none { display-date(actual_date, memo-style: memo-style) }],
|
|
83
83
|
)
|
|
84
84
|
|
|
85
85
|
blank-line()
|
|
@@ -18,15 +18,10 @@
|
|
|
18
18
|
/// of the memorandum. Automatically detects single vs. multiple paragraphs
|
|
19
19
|
/// to comply with AFH 33-337 numbering requirements.
|
|
20
20
|
///
|
|
21
|
-
/// When `auto_numbering` is false (set in frontmatter), base-level paragraphs
|
|
22
|
-
/// render flush left without numbering. Only explicitly numbered or bulleted
|
|
23
|
-
/// items enter the numbering hierarchy.
|
|
24
|
-
///
|
|
25
21
|
/// - content (content): The body content to render
|
|
26
22
|
/// -> content
|
|
27
23
|
#let mainmatter(it) = context {
|
|
28
24
|
let config = query(metadata).last().value
|
|
29
|
-
let auto-numbering = config.at("auto_numbering", default: true)
|
|
30
25
|
let memo-style = config.at("memo_style", default: "usaf")
|
|
31
|
-
render-body(it,
|
|
26
|
+
render-body(it, memo-style: memo-style)
|
|
32
27
|
}
|
|
@@ -142,29 +142,54 @@
|
|
|
142
142
|
// SIGNATURE BLOCK
|
|
143
143
|
// =============================================================================
|
|
144
144
|
// AFH 33-337 "Signature Block": "Start the signature block on the fifth line below
|
|
145
|
-
// the last line of text and 4.5 inches from the left edge of the page
|
|
145
|
+
// the last line of text and 4.5 inches from the left edge of the page or three
|
|
146
|
+
// spaces to the right of page center"
|
|
146
147
|
// AFH 33-337 "Do not place the signature element on a continuation page by itself"
|
|
148
|
+
// AFH 33-337 long-name example: "Signature block adjusted to the left" when a
|
|
149
|
+
// long name would otherwise exceed the right margin.
|
|
147
150
|
|
|
148
151
|
#let render-signature-block(signature-lines, signature-blank-lines: 4) = {
|
|
149
152
|
signature-lines = ensure-array(signature-lines)
|
|
150
|
-
// AFH 33-337: "
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
// AFH 33-337
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
153
|
+
// AFH 33-337: "fifth line below" = 4 blank lines between text and signature block.
|
|
154
|
+
// breakable: false discourages orphaning the signature block onto a page by itself.
|
|
155
|
+
blank-lines(signature-blank-lines)
|
|
156
|
+
// AFH 33-337 allows two equivalent anchors: 4.5in from the left edge, or three
|
|
157
|
+
// spaces right of page center. On 8.5in stock these coincide (page center =
|
|
158
|
+
// 4.25in; three TNR-12pt spaces ≈ 0.25in), so we use 4.5in as the canonical
|
|
159
|
+
// anchor. pad() is relative to the text area, hence (4.5in - margin).
|
|
160
|
+
let default-pad = 4.5in - spacing.margin
|
|
161
|
+
context {
|
|
162
|
+
// Measure each line at its rendered settings to detect long-name overflow.
|
|
163
|
+
let body-width = page.width - 2 * spacing.margin
|
|
164
|
+
let widest = 0pt
|
|
165
|
+
for line in signature-lines {
|
|
166
|
+
let w = measure(text(hyphenate: false, line)).width
|
|
167
|
+
if w > widest { widest = w }
|
|
168
|
+
}
|
|
169
|
+
// If the widest line would overflow the right margin at the standard
|
|
170
|
+
// anchor, shift the block left just enough to fit. Clamp at 0 so the
|
|
171
|
+
// block never crosses the left margin.
|
|
172
|
+
let available = body-width - default-pad
|
|
173
|
+
let left-pad = if widest > available {
|
|
174
|
+
let shifted = body-width - widest
|
|
175
|
+
if shifted < 0pt { 0pt } else { shifted }
|
|
176
|
+
} else {
|
|
177
|
+
default-pad
|
|
178
|
+
}
|
|
179
|
+
block(breakable: false)[
|
|
180
|
+
#align(left)[
|
|
181
|
+
#pad(left: left-pad)[
|
|
182
|
+
#text(hyphenate: false)[
|
|
183
|
+
#for line in signature-lines {
|
|
184
|
+
// AFH 33-337: "indent the next line to begin under the third character
|
|
185
|
+
// of the line above" — 2-character indent ≈ 1em in Times New Roman 12pt
|
|
186
|
+
par(hanging-indent: .5em, line)
|
|
187
|
+
}
|
|
188
|
+
]
|
|
164
189
|
]
|
|
165
190
|
]
|
|
166
191
|
]
|
|
167
|
-
|
|
192
|
+
}
|
|
168
193
|
}
|
|
169
194
|
|
|
170
195
|
// =============================================================================
|
|
@@ -14,46 +14,37 @@
|
|
|
14
14
|
|
|
15
15
|
#import "config.typ": CLASSIFICATION_COLORS, counters, paragraph-config, spacing
|
|
16
16
|
|
|
17
|
-
// Shared measured line-stride cache used by
|
|
18
|
-
//
|
|
17
|
+
// Shared measured line-stride cache used by body line-count heuristics.
|
|
18
|
+
// Value is a `length` set once in `frontmatter`.
|
|
19
19
|
#let LINE_STRIDE = state("LINE_STRIDE")
|
|
20
|
-
// Shared blank-line step cache used by structural spacing.
|
|
21
|
-
#let BLANK_LINE_STEP = state("BLANK_LINE_STEP")
|
|
22
20
|
|
|
23
21
|
/// Creates vertical spacing equivalent to multiple blank lines.
|
|
24
22
|
///
|
|
25
|
-
///
|
|
23
|
+
/// Adds `count` wrapped-line strides on top of the natural inter-paragraph
|
|
24
|
+
/// gap, so a blank line occupies exactly the same vertical space as a line
|
|
25
|
+
/// produced by natural paragraph wrapping. The stride is measured from
|
|
26
|
+
/// `LINE_STRIDE` (cached in `frontmatter`) and falls back to an inline
|
|
27
|
+
/// measurement when the cache is unset.
|
|
26
28
|
///
|
|
27
29
|
/// - count (int): Number of blank lines to create
|
|
28
|
-
/// - weak (bool): Whether spacing can be compressed at page breaks
|
|
29
30
|
/// -> content
|
|
30
|
-
#let blank-lines(count
|
|
31
|
-
if count
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let
|
|
36
|
-
|
|
37
|
-
measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
|
|
38
|
-
}
|
|
39
|
-
// Scale legacy visual step with current font size:
|
|
40
|
-
// 19.05pt was tuned for 12pt body text.
|
|
41
|
-
let em-size = measure(box(width: 1em)[]).width
|
|
42
|
-
let legacy-scaled = spacing.vertical * (em-size / 12pt)
|
|
43
|
-
let fallback-step = calc.max(measured-stride, legacy-scaled)
|
|
44
|
-
let cached-step = BLANK_LINE_STEP.get()
|
|
45
|
-
let step = if cached-step != none { cached-step } else { fallback-step }
|
|
46
|
-
v(step * count, weak: weak)
|
|
31
|
+
#let blank-lines(count) = {
|
|
32
|
+
if count <= 0 { return }
|
|
33
|
+
context {
|
|
34
|
+
let stride = LINE_STRIDE.get()
|
|
35
|
+
if stride == none {
|
|
36
|
+
let one-line = measure(par(spacing: 0pt)[x]).height
|
|
37
|
+
stride = measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
|
|
47
38
|
}
|
|
39
|
+
v(stride * count)
|
|
48
40
|
}
|
|
49
41
|
}
|
|
50
42
|
|
|
51
43
|
/// Creates vertical spacing equivalent to one blank line.
|
|
52
44
|
/// Convenience function for single line spacing.
|
|
53
45
|
///
|
|
54
|
-
/// - weak (bool): Whether spacing can be compressed at page breaks
|
|
55
46
|
/// -> content
|
|
56
|
-
#let blank-line(
|
|
47
|
+
#let blank-line() = blank-lines(1)
|
|
57
48
|
|
|
58
49
|
// =============================================================================
|
|
59
50
|
// GENERAL UTILITY FUNCTIONS
|