@airmark/quiver 0.32.0 → 0.32.1
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@airmark/quiver",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.1",
|
|
4
4
|
"description": "Airmark Quiver — Air Force and DAF document Quills for Quillmark.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"node": ">=24"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@quillmark/quiver": "0.
|
|
20
|
-
"@quillmark/wasm": "^0.
|
|
21
|
-
"quillkit": "0.5.
|
|
19
|
+
"@quillmark/quiver": "0.26.0",
|
|
20
|
+
"@quillmark/wasm": "^0.109.1",
|
|
21
|
+
"quillkit": "0.5.3"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"test": "quillkit test",
|
|
@@ -15,6 +15,11 @@ main:
|
|
|
15
15
|
The first paragraph. Top-level paragraphs are auto-numbered; do not add manual numbering.
|
|
16
16
|
|
|
17
17
|
- Nested bullets are automatically lettered.
|
|
18
|
+
|
|
19
|
+
Lines that take no number, letter, or bullet — a roster of names, an address — go in a block quote, typeset as written. End a line with a backslash to break it.
|
|
20
|
+
|
|
21
|
+
> FIRST M. LAST, Maj, USAF\
|
|
22
|
+
> SECOND N. LAST, Capt, USAF
|
|
18
23
|
fields:
|
|
19
24
|
memo_for:
|
|
20
25
|
type: array
|
|
@@ -46,12 +51,12 @@ main:
|
|
|
46
51
|
description: "If recipients are on the same installation, use only the office symbol. For recipients on other installations, include the full mailing address to enable return correspondence. Omit entirely to produce a Memorandum for Record (no MEMO FROM line)."
|
|
47
52
|
|
|
48
53
|
subject:
|
|
49
|
-
type:
|
|
54
|
+
type: richtext
|
|
50
55
|
inline: true
|
|
51
56
|
example: Subject of the Memorandum
|
|
52
57
|
ui:
|
|
53
58
|
group: addressing
|
|
54
|
-
description: Be brief and clear. Capitalize the first letter of each word except articles, prepositions, and conjunctions. Include suspense dates in parentheses if applicable.
|
|
59
|
+
description: "Be brief and clear. Capitalize the first letter of each word except articles, prepositions, and conjunctions. Include suspense dates in parentheses if applicable. Italicize publication titles with *asterisks* (standard Markdown emphasis), as in the References field."
|
|
55
60
|
|
|
56
61
|
signature_block:
|
|
57
62
|
type: array
|
|
@@ -66,6 +71,14 @@ main:
|
|
|
66
71
|
compact: true
|
|
67
72
|
description: "Line 1: Name in UPPERCASE as signed, grade, and service. Line 2: Duty title."
|
|
68
73
|
|
|
74
|
+
date:
|
|
75
|
+
type: date
|
|
76
|
+
default: ""
|
|
77
|
+
ui:
|
|
78
|
+
group: addressing
|
|
79
|
+
compact: true
|
|
80
|
+
description: YYYY-MM-DD. Leave blank to use today's date.
|
|
81
|
+
|
|
69
82
|
letterhead_title:
|
|
70
83
|
type: array
|
|
71
84
|
items:
|
|
@@ -195,14 +208,14 @@ main:
|
|
|
195
208
|
attachments:
|
|
196
209
|
type: array
|
|
197
210
|
items:
|
|
198
|
-
type:
|
|
211
|
+
type: richtext
|
|
199
212
|
inline: true
|
|
200
213
|
default: []
|
|
201
214
|
example:
|
|
202
215
|
- Attachment description, YYYY MMM DD
|
|
203
216
|
ui:
|
|
204
217
|
group: additional
|
|
205
|
-
description: List attachments in the order they are mentioned in the memo. Briefly describe each; do not use 'as stated' or abbreviations.
|
|
218
|
+
description: "List attachments in the order they are mentioned in the memo. Briefly describe each; do not use 'as stated' or abbreviations. Italicize publication titles with *asterisks* (standard Markdown emphasis), as in the References field."
|
|
206
219
|
|
|
207
220
|
memo_style:
|
|
208
221
|
type: enum
|
|
@@ -223,14 +236,6 @@ main:
|
|
|
223
236
|
compact: true
|
|
224
237
|
description: Body text size in points; decimals allowed (e.g. 11.5).
|
|
225
238
|
|
|
226
|
-
date:
|
|
227
|
-
type: date
|
|
228
|
-
default: ""
|
|
229
|
-
ui:
|
|
230
|
-
group: additional
|
|
231
|
-
compact: true
|
|
232
|
-
description: YYYY-MM-DD. Leave blank to use today's date.
|
|
233
|
-
|
|
234
239
|
card_kinds:
|
|
235
240
|
indorsement:
|
|
236
241
|
description: Chain of routing endorsements. Each endorsement block adds an official response or forwarding action to the original memo.
|
|
@@ -266,6 +271,13 @@ card_kinds:
|
|
|
266
271
|
group: addressing
|
|
267
272
|
compact: true
|
|
268
273
|
description: "Line 1: Name in UPPERCASE as signed, grade, and service. Line 2: Duty title."
|
|
274
|
+
date:
|
|
275
|
+
type: date
|
|
276
|
+
default: ""
|
|
277
|
+
ui:
|
|
278
|
+
group: addressing
|
|
279
|
+
compact: true
|
|
280
|
+
description: "YYYY-MM-DD. Date the endorser signs/forwards the memo (per AFH 33-337 Ch. 14, this is distinct from the originating memo's date). Leave blank to render a fillable PDF text field for the endorser to type the date into when signing."
|
|
269
281
|
format:
|
|
270
282
|
type: enum
|
|
271
283
|
values:
|
|
@@ -288,10 +300,3 @@ card_kinds:
|
|
|
288
300
|
group: additional
|
|
289
301
|
compact: true
|
|
290
302
|
description: "Decision taken by the endorser, shown as an either/or line with the choice underlined and the rejected option struck out. Leave blank to hide the line entirely. The wording is automatic: the last indorsement is the approval authority and reads Approve/Disapprove, while every earlier one is a coordinating official and reads Concur/Nonconcur. Use 'undecided' to print the pair unmarked for the endorser to mark by hand."
|
|
291
|
-
date:
|
|
292
|
-
type: date
|
|
293
|
-
default: ""
|
|
294
|
-
ui:
|
|
295
|
-
group: additional
|
|
296
|
-
compact: true
|
|
297
|
-
description: "YYYY-MM-DD. Date the endorser signs/forwards the memo (per AFH 33-337 Ch. 14, this is distinct from the originating memo's date). Leave blank to render a fillable PDF text field for the endorser to type the date into when signing."
|
|
@@ -60,6 +60,25 @@
|
|
|
60
60
|
level-counts
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/// The horizontal offset at which a paragraph's *text* sits: its indent plus
|
|
64
|
+
/// the width of the number label that precedes it.
|
|
65
|
+
///
|
|
66
|
+
/// This is where a later block of the same paragraph — a continuation, a block
|
|
67
|
+
/// quote — lines up, so the two callers share one measurement rather than each
|
|
68
|
+
/// spelling it. A top-level paragraph carries no indent and its label is the
|
|
69
|
+
/// left margin itself, so the offset is zero.
|
|
70
|
+
///
|
|
71
|
+
/// - level (int): Nesting level (0-based)
|
|
72
|
+
/// - level-counts (dictionary): Current counter values per level
|
|
73
|
+
/// - indent-fn (function): `(level, level-counts) -> length`
|
|
74
|
+
/// -> length
|
|
75
|
+
#let paragraph-text-offset(level, level-counts, indent-fn) = {
|
|
76
|
+
if level <= 0 { return 0pt }
|
|
77
|
+
let current-value = level-counts.at(str(level), default: 1)
|
|
78
|
+
let number-text = numbering(get-paragraph-numbering-format(level), current-value)
|
|
79
|
+
indent-fn(level, level-counts) + measure([#number-text#" "]).width
|
|
80
|
+
}
|
|
81
|
+
|
|
63
82
|
/// Formats a paragraph (or continuation) with a given indent strategy.
|
|
64
83
|
///
|
|
65
84
|
/// - body (content): Paragraph content
|
|
@@ -69,12 +88,13 @@
|
|
|
69
88
|
/// - continuation (bool): If true, adds number-label width to alignment
|
|
70
89
|
/// -> content
|
|
71
90
|
#let format-par(body, level, level-counts, indent-fn, continuation: false) = {
|
|
72
|
-
let indent-width = indent-fn(level, level-counts)
|
|
73
91
|
if continuation {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
// Offset the first line only, which is how the numbered paragraph this
|
|
93
|
+
// block continues is set: its own wrapped lines return to the left margin
|
|
94
|
+
// too, so the two read as one paragraph.
|
|
95
|
+
[#h(paragraph-text-offset(level, level-counts, indent-fn))#body]
|
|
77
96
|
} else {
|
|
97
|
+
let indent-width = indent-fn(level, level-counts)
|
|
78
98
|
let current-value = level-counts.at(str(level), default: 1)
|
|
79
99
|
let number-text = numbering(get-paragraph-numbering-format(level), current-value)
|
|
80
100
|
[#h(indent-width)#number-text#" "#body]
|
|
@@ -145,6 +165,38 @@
|
|
|
145
165
|
})
|
|
146
166
|
t
|
|
147
167
|
}
|
|
168
|
+
// Collect block quotes — captured as-is, with paragraph processing kept
|
|
169
|
+
// out of them. AFH 33-337 numbers paragraphs and letters subparagraphs, and
|
|
170
|
+
// a body sometimes has to hold lines that are neither: a roster of names, a
|
|
171
|
+
// quoted passage, an address. The block quote is where an author says so —
|
|
172
|
+
// its content is emitted verbatim, so nothing inside it takes a number, a
|
|
173
|
+
// letter, or a bullet.
|
|
174
|
+
//
|
|
175
|
+
// Returning `none` rather than the quote is what makes that true: the
|
|
176
|
+
// quote's own paragraphs are never laid out here, so the `show par` above
|
|
177
|
+
// never sees them and cannot number them. The buffered body is emitted in
|
|
178
|
+
// the second pass, where these show rules are out of scope. Typst's own
|
|
179
|
+
// block-quote framing (padding, attribution) is dropped with the element;
|
|
180
|
+
// what reaches the page is the author's lines and nothing else.
|
|
181
|
+
//
|
|
182
|
+
// The nesting level rides along so a quote inside a list item lines up with
|
|
183
|
+
// that item's text (see the emission below). @quillmark/wasm 0.109.0 is
|
|
184
|
+
// what makes that level trustworthy: before it, a container inside a list
|
|
185
|
+
// item opened at column 0 and terminated the list, so a quote's siblings
|
|
186
|
+
// came back as top-level paragraphs (#123). It now nests, and this quill
|
|
187
|
+
// stopped declining the construct with it.
|
|
188
|
+
show quote.where(block: true): q => context {
|
|
189
|
+
let nest_level = NEST_DOWN.get().at(0) - NEST_UP.get().at(0)
|
|
190
|
+
PAR_BUFFER.update(pars => {
|
|
191
|
+
pars.push((
|
|
192
|
+
content: q.body,
|
|
193
|
+
nest_level: nest_level,
|
|
194
|
+
kind: "quote",
|
|
195
|
+
))
|
|
196
|
+
pars
|
|
197
|
+
})
|
|
198
|
+
none
|
|
199
|
+
}
|
|
148
200
|
{
|
|
149
201
|
show heading: h => {
|
|
150
202
|
IS_HEADING.update(true)
|
|
@@ -194,9 +246,9 @@
|
|
|
194
246
|
// Second pass: consume par buffer
|
|
195
247
|
//
|
|
196
248
|
// PAR_BUFFER item dictionary layout:
|
|
197
|
-
// item.content — the paragraph body or
|
|
249
|
+
// item.content — the paragraph body, table element, or block-quote body
|
|
198
250
|
// item.nest_level — nesting depth (−1 for tables)
|
|
199
|
-
// item.kind — "par", "heading", "table", or "
|
|
251
|
+
// item.kind — "par", "heading", "table", "continuation", or "quote"
|
|
200
252
|
context {
|
|
201
253
|
let heading_buffer = none
|
|
202
254
|
let heading_level = 0
|
|
@@ -230,13 +282,15 @@
|
|
|
230
282
|
// A buffered heading runs into this element only when the two belong to
|
|
231
283
|
// the same item: a later block of this list item ("continuation"), or,
|
|
232
284
|
// at top level, the next paragraph. The first block of the *next* item,
|
|
233
|
-
// a table (nest_level −1, so the level test alone excludes it),
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
285
|
+
// a table (nest_level −1, so the level test alone excludes it), a block
|
|
286
|
+
// quote (verbatim: a heading prepended to it would be ink the author did
|
|
287
|
+
// not put inside the quote), and another heading all fail the test, and
|
|
288
|
+
// each would otherwise carry the heading's text somewhere it was not
|
|
289
|
+
// authored. Those emit the heading on its own line, the treatment a
|
|
290
|
+
// heading before a table takes.
|
|
237
291
|
if heading_buffer != none {
|
|
238
292
|
let runs_in = (
|
|
239
|
-
kind
|
|
293
|
+
kind not in ("heading", "quote")
|
|
240
294
|
and item.nest_level == heading_level
|
|
241
295
|
and (heading_level == 0 or kind == "continuation")
|
|
242
296
|
)
|
|
@@ -269,6 +323,35 @@
|
|
|
269
323
|
let final_par = {
|
|
270
324
|
if kind == "table" {
|
|
271
325
|
render-memo-table(item_content)
|
|
326
|
+
} else if kind == "quote" {
|
|
327
|
+
// A block quote is the body's unlabeled block: no number, no letter,
|
|
328
|
+
// no bullet — the author's lines as written. It is placed, not
|
|
329
|
+
// processed, which is what lets a memorandum carry a roster of names,
|
|
330
|
+
// an address, or a quoted passage without AFH 33-337 numbering
|
|
331
|
+
// claiming the first line of it.
|
|
332
|
+
//
|
|
333
|
+
// Two things are imposed, and both serve that reading.
|
|
334
|
+
//
|
|
335
|
+
// Where the block starts: at the offset where the text of the
|
|
336
|
+
// paragraph it sits in starts, so a quote inside a subparagraph hangs
|
|
337
|
+
// under that subparagraph's text instead of falling back to the left
|
|
338
|
+
// margin. `pad` shifts the whole block, where a continuation takes a
|
|
339
|
+
// first-line `h(..)`: a quote is kept for its line structure, and
|
|
340
|
+
// lines that wrapped back to the margin would read as a block other
|
|
341
|
+
// than the one authored. Top level offsets by 0pt — flush at the
|
|
342
|
+
// margin, where an unnumbered line belongs.
|
|
343
|
+
//
|
|
344
|
+
// How its own paragraphs are spaced: `par.leading` and `par.spacing`
|
|
345
|
+
// are both half an em in a memorandum, so a paragraph break inside
|
|
346
|
+
// the quote would land on the page as an ordinary line break and read
|
|
347
|
+
// as one block where the author wrote two. A blank line — what the
|
|
348
|
+
// body puts between its own paragraphs — keeps that break visible.
|
|
349
|
+
let quoted = {
|
|
350
|
+
set par(spacing: spacing.line + line-stride())
|
|
351
|
+
item_content
|
|
352
|
+
}
|
|
353
|
+
let offset = paragraph-text-offset(nest_level, level-counts, indent-fn)
|
|
354
|
+
if offset == 0pt { quoted } else { pad(left: offset, quoted) }
|
|
272
355
|
} else if kind == "continuation" {
|
|
273
356
|
// Continuation block within a multi-block list item:
|
|
274
357
|
// indent to align with preceding numbered paragraph's text, no new number.
|
|
@@ -18,26 +18,33 @@
|
|
|
18
18
|
// Value is a `length` set once in `frontmatter`.
|
|
19
19
|
#let LINE_STRIDE = state("LINE_STRIDE")
|
|
20
20
|
|
|
21
|
+
/// The height one wrapped line of body text occupies.
|
|
22
|
+
///
|
|
23
|
+
/// Read from `LINE_STRIDE` (cached in `frontmatter`), falling back to an inline
|
|
24
|
+
/// measurement when the cache is unset. Reads state, so callers must already be
|
|
25
|
+
/// in a `context`.
|
|
26
|
+
///
|
|
27
|
+
/// -> length
|
|
28
|
+
#let line-stride() = {
|
|
29
|
+
let stride = LINE_STRIDE.get()
|
|
30
|
+
if stride == none {
|
|
31
|
+
let one-line = measure(par(spacing: 0pt)[x]).height
|
|
32
|
+
stride = measure(par(spacing: 0pt)[x#linebreak()x]).height - one-line
|
|
33
|
+
}
|
|
34
|
+
stride
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
/// Creates vertical spacing equivalent to multiple blank lines.
|
|
22
38
|
///
|
|
23
39
|
/// Adds `count` wrapped-line strides on top of the natural inter-paragraph
|
|
24
40
|
/// gap, so a blank line occupies exactly the same vertical space as a line
|
|
25
|
-
/// produced by natural paragraph wrapping.
|
|
26
|
-
/// `LINE_STRIDE` (cached in `frontmatter`) and falls back to an inline
|
|
27
|
-
/// measurement when the cache is unset.
|
|
41
|
+
/// produced by natural paragraph wrapping.
|
|
28
42
|
///
|
|
29
43
|
/// - count (int): Number of blank lines to create
|
|
30
44
|
/// -> content
|
|
31
45
|
#let blank-lines(count) = {
|
|
32
46
|
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
|
|
38
|
-
}
|
|
39
|
-
v(stride * count)
|
|
40
|
-
}
|
|
47
|
+
context { v(line-stride() * count) }
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
/// Creates vertical spacing equivalent to one blank line.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
#import "@local/quillmark-helper:0.1.0":
|
|
1
|
+
#import "@local/quillmark-helper:0.1.0": (
|
|
2
|
+
data, display, field-region, form-field, signature-field,
|
|
3
|
+
)
|
|
2
4
|
#import "@local/tonguetoquill-usaf-memo:4.0.0": (
|
|
3
5
|
backmatter, date-pattern, frontmatter, indorsement, mainmatter,
|
|
4
6
|
)
|
|
@@ -41,9 +43,24 @@
|
|
|
41
43
|
// but its ink would be born inside the package and carry no schema address.
|
|
42
44
|
// `display` places the field's *content* projection instead: the glyphs are
|
|
43
45
|
// born in the generated helper, so the memo date stays click-to-edit however
|
|
44
|
-
// deep the package formats it.
|
|
45
|
-
//
|
|
46
|
-
date
|
|
46
|
+
// deep the package formats it.
|
|
47
|
+
//
|
|
48
|
+
// A blank date means today's, and the plate stamps it rather than falling
|
|
49
|
+
// through to `frontmatter`'s own `datetime.today()`: package-born ink carries
|
|
50
|
+
// no address, so the one date a memo never types would be the one date a
|
|
51
|
+
// preview cannot click. `field-region` claims that ink for the field instead.
|
|
52
|
+
//
|
|
53
|
+
// The stamp is markup, not the bare `str` `.display()` returns: a `str` off a
|
|
54
|
+
// function call carries no source position, and ink with none is unclaimable.
|
|
55
|
+
date: {
|
|
56
|
+
let pattern = date-pattern(memo-style: memo_style)
|
|
57
|
+
let authored = display("date", pattern)
|
|
58
|
+
if authored != none {
|
|
59
|
+
authored
|
|
60
|
+
} else {
|
|
61
|
+
field-region("date", [#datetime.today().display(pattern)])
|
|
62
|
+
}
|
|
63
|
+
},
|
|
47
64
|
|
|
48
65
|
// Receiver information
|
|
49
66
|
memo_for: data.memo_for,
|