@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 CHANGED
@@ -17,7 +17,7 @@ and aligned to the current Quillmark spec.
17
17
  ## Install
18
18
 
19
19
  ```bash
20
- npm install @airmark/quiver @quillmark/quiver@0.5.0 @quillmark/wasm@0.60.0
20
+ npm install @airmark/quiver @quillmark/quiver@0.5.0 @quillmark/wasm@0.66.2
21
21
  ```
22
22
 
23
23
  ## Usage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@airmark/quiver",
3
- "version": "0.6.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.60.0 <1.0.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.60.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 (int pt)
157
- type: integer
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, auto-numbering: true, memo-style: "usaf") = {
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 auto-numbering {
282
- if par_count > 1 {
283
- // Apply paragraph numbering per AFH 33-337 §2
284
- let par = format-par(item_content, nest_level, level-counts, indent-fn)
285
- level-counts.insert(str(nest_level), level-counts.at(str(nest_level)) + 1)
286
- level-counts = reset-levels-from(level-counts, nest_level + 1, max-levels)
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
- // Unnumbered mode: only explicitly nested items (enum/list) get numbered
294
- if nest_level > 0 {
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
- // If this is the final item, apply AFH 33-337 §11 rule:
310
- // "Avoid dividing a paragraph of less than four lines between two pages"
311
- blank-line()
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 downstream spacing logic.
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 = if date == none { datetime.today() } else { 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, auto-numbering: auto-numbering, memo-style: memo-style)
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: "The signature block is never on a page by itself"
151
- // Note: Perfect enforcement isn't feasible without over-engineering
152
- // We use weak: false spacing and breakable: false to discourage orphaning
153
- // AFH 33-337: "fifth line below" = 4 blank lines between text and signature block
154
- blank-lines(signature-blank-lines, weak: false)
155
- block(breakable: false)[
156
- #align(left)[
157
- // AFH 33-337: "4.5 inches from the left edge of the page"
158
- // We use (4.5in - margin) because Typst's pad() is relative to the text area, not page edge
159
- #pad(left: 4.5in - spacing.margin)[
160
- #text(hyphenate: false)[
161
- #for line in signature-lines {
162
- par(hanging-indent: 4 * 0.5em, line)
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 blank-line spacing and body
18
- // line-count heuristics. Value is a `length` set once in `frontmatter`.
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
- /// Each step matches one row of the body paragraph line grid.
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, weak: true) = {
31
- if count == 0 {
32
- v(0em, weak: weak)
33
- } else {
34
- context {
35
- let measured-stride = {
36
- let one-line = measure(par(spacing: 0pt)[x]).height
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(weak: true) = blank-lines(1, weak: weak)
47
+ #let blank-line() = blank-lines(1)
57
48
 
58
49
  // =============================================================================
59
50
  // GENERAL UTILITY FUNCTIONS