@tonguetoquill/collection 0.1.3 → 0.1.5
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 +5 -2
- package/quills/usaf_memo/0.2.0/Quill.yaml +1 -1
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +111 -105
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +5 -6
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +19 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tonguetoquill/collection",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/nibsbin/tonguetoquill-collection.git"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"type": "module",
|
|
23
23
|
"scripts": {
|
|
24
24
|
"update-subtrees": "node scripts/update-subtrees.mjs",
|
|
25
|
-
"test": "
|
|
25
|
+
"test": "node scripts/test.mjs",
|
|
26
26
|
"build": "echo \"No build specified\"",
|
|
27
27
|
"preversion": "npm test",
|
|
28
28
|
"postversion": "git push --follow-tags",
|
|
@@ -35,5 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@quillmark/registry": "^0.5.3"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@quillmark/wasm": "^0.39.0"
|
|
38
41
|
}
|
|
39
42
|
}
|
|
@@ -31,115 +31,67 @@
|
|
|
31
31
|
paragraph-config.numbering-formats.at(level, default: "i.")
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
///
|
|
35
|
-
///
|
|
36
|
-
/// Creates properly formatted paragraph numbers for the hierarchical numbering
|
|
37
|
-
/// system using Typst's native counter display capabilities.
|
|
38
|
-
///
|
|
39
|
-
/// - level (int): Paragraph nesting level (0-based)
|
|
40
|
-
/// - counter-value (none | int): Optional explicit counter value to use (for measuring widths)
|
|
41
|
-
/// - increment (bool): Whether to increment the counter after display
|
|
42
|
-
/// -> content
|
|
43
|
-
#let generate-paragraph-number(level, counter-value: none) = {
|
|
44
|
-
let paragraph-counter = counter(paragraph-config.counter-prefix + str(level))
|
|
45
|
-
let numbering-format = get-paragraph-numbering-format(level)
|
|
46
|
-
|
|
47
|
-
if counter-value != none {
|
|
48
|
-
// For measuring widths: create temporary counter at specific value
|
|
49
|
-
assert(counter-value >= 0, message: "Counter value of `" + str(counter-value) + "` cannot be less than 0")
|
|
50
|
-
let temp-counter = counter("temp-counter")
|
|
51
|
-
temp-counter.update(counter-value)
|
|
52
|
-
temp-counter.display(numbering-format)
|
|
53
|
-
} else {
|
|
54
|
-
// Standard case: display and increment
|
|
55
|
-
let result = paragraph-counter.display(numbering-format)
|
|
56
|
-
paragraph-counter.step()
|
|
57
|
-
result
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/// Calculates proper indentation width for a paragraph level.
|
|
62
|
-
///
|
|
63
|
-
/// AFH 33-337 "The Text of the Official Memorandum" §4-5:
|
|
64
|
-
/// - "The first paragraph is never indented; it is numbered and flush left"
|
|
65
|
-
/// - "Indent the first line of sub-paragraphs to align the number or letter with
|
|
66
|
-
/// the first character of its parent level paragraph"
|
|
34
|
+
/// Calculates indentation width using explicit counter values.
|
|
67
35
|
///
|
|
68
36
|
/// Computes the exact indentation needed for hierarchical paragraph alignment
|
|
69
37
|
/// by measuring the cumulative width of all ancestor paragraph numbers and their
|
|
70
|
-
/// spacing. Uses
|
|
71
|
-
///
|
|
72
|
-
/// Per AFH 33-337: Sub-paragraph text aligns with first character of parent text,
|
|
73
|
-
/// which means indentation = sum of all ancestor number widths + spacing.
|
|
38
|
+
/// spacing. Uses the provided counter values directly (no Typst counter reads).
|
|
74
39
|
///
|
|
75
40
|
/// - level (int): Paragraph nesting level (0-based)
|
|
41
|
+
/// - level-counts (dictionary): Maps level index strings to their current counter values
|
|
76
42
|
/// -> length
|
|
77
|
-
#let calculate-
|
|
78
|
-
assert(level >= 0)
|
|
43
|
+
#let calculate-indent-from-counts(level, level-counts) = {
|
|
79
44
|
if level == 0 {
|
|
80
45
|
return 0pt
|
|
81
46
|
}
|
|
82
|
-
|
|
83
|
-
// Accumulate widths of all ancestor numbers iteratively
|
|
84
47
|
let total-indent = 0pt
|
|
85
48
|
for ancestor-level in range(level) {
|
|
86
|
-
let ancestor-
|
|
87
|
-
let ancestor-
|
|
88
|
-
|
|
49
|
+
let ancestor-value = level-counts.at(str(ancestor-level), default: 1)
|
|
50
|
+
let ancestor-format = get-paragraph-numbering-format(ancestor-level)
|
|
51
|
+
let ancestor-number = numbering(ancestor-format, ancestor-value)
|
|
89
52
|
let width = measure([#ancestor-number#" "]).width
|
|
90
53
|
total-indent += width
|
|
91
54
|
}
|
|
92
|
-
|
|
93
55
|
total-indent
|
|
94
56
|
}
|
|
95
57
|
|
|
96
|
-
///
|
|
97
|
-
/// Used internally by the paragraph numbering system to maintain proper nesting.
|
|
98
|
-
/// -> state
|
|
99
|
-
#let PAR_LEVEL_STATE = state("PAR_LEVEL", 0)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
/// Sets the current paragraph level state.
|
|
58
|
+
/// Formats a numbered paragraph with proper indentation.
|
|
103
59
|
///
|
|
104
|
-
///
|
|
105
|
-
///
|
|
60
|
+
/// Generates a properly formatted paragraph with AFH 33-337 compliant numbering
|
|
61
|
+
/// and indentation. Uses explicit level and counter values to avoid nested-context
|
|
62
|
+
/// state propagation issues.
|
|
106
63
|
///
|
|
107
|
-
/// -
|
|
64
|
+
/// - body (content): Paragraph content to format
|
|
65
|
+
/// - level (int): Paragraph nesting level (0-based)
|
|
66
|
+
/// - level-counts (dictionary): Current counter values per level
|
|
108
67
|
/// -> content
|
|
109
|
-
#let
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
68
|
+
#let format-numbered-par(body, level, level-counts) = {
|
|
69
|
+
let current-value = level-counts.at(str(level), default: 1)
|
|
70
|
+
let format = get-paragraph-numbering-format(level)
|
|
71
|
+
let number-text = numbering(format, current-value)
|
|
72
|
+
let indent-width = calculate-indent-from-counts(level, level-counts)
|
|
73
|
+
[#h(indent-width)#number-text#" "#body]
|
|
116
74
|
}
|
|
117
75
|
|
|
118
|
-
///
|
|
119
|
-
///
|
|
120
|
-
/// Generates a properly formatted paragraph with AFH 33-337 compliant numbering,
|
|
121
|
-
/// indentation, and spacing. Automatically manages counter incrementation and
|
|
122
|
-
/// nested paragraph state. Used internally by the body rendering system.
|
|
76
|
+
/// Formats a continuation paragraph within a multi-block list item.
|
|
123
77
|
///
|
|
124
|
-
///
|
|
125
|
-
///
|
|
126
|
-
///
|
|
127
|
-
///
|
|
128
|
-
/// - Widow/orphan prevention settings
|
|
78
|
+
/// Renders a paragraph that belongs to the same list item as the preceding
|
|
79
|
+
/// numbered paragraph. The text is indented to align with the first character
|
|
80
|
+
/// of the preceding numbered paragraph's text (past the number and spacing),
|
|
81
|
+
/// but no new number is generated.
|
|
129
82
|
///
|
|
130
|
-
/// -
|
|
83
|
+
/// - body (content): Continuation paragraph content to format
|
|
84
|
+
/// - level (int): Paragraph nesting level (0-based)
|
|
85
|
+
/// - level-counts (dictionary): Current counter values per level
|
|
131
86
|
/// -> content
|
|
132
|
-
#let
|
|
133
|
-
let
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
let
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// Number + two spaces + content, with left padding for nesting
|
|
141
|
-
//pad(left: indent-width, paragraph-number + " " + content)
|
|
142
|
-
[#h(indent-width)#paragraph-number#" "#content]
|
|
87
|
+
#let format-continuation-par(body, level, level-counts) = {
|
|
88
|
+
let indent-width = calculate-indent-from-counts(level, level-counts)
|
|
89
|
+
// Add the width of the current level's number + spacing to align with text
|
|
90
|
+
let current-value = level-counts.at(str(level), default: 1)
|
|
91
|
+
let format = get-paragraph-numbering-format(level)
|
|
92
|
+
let number-text = numbering(format, current-value)
|
|
93
|
+
let number-width = measure([#number-text#" "]).width
|
|
94
|
+
[#h(indent-width + number-width)#body]
|
|
143
95
|
}
|
|
144
96
|
|
|
145
97
|
// =============================================================================
|
|
@@ -160,10 +112,11 @@
|
|
|
160
112
|
NEST_UP.update(0)
|
|
161
113
|
let IS_HEADING = state("IS_HEADING")
|
|
162
114
|
IS_HEADING.update(false)
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
115
|
+
// Tracks whether the next paragraph is the first block in a list item.
|
|
116
|
+
// When true, the next `show par` captures a numbered item; subsequent
|
|
117
|
+
// paragraphs within the same item are continuations (no new number).
|
|
118
|
+
let ITEM_FIRST_PAR = state("ITEM_FIRST_PAR")
|
|
119
|
+
ITEM_FIRST_PAR.update(false)
|
|
167
120
|
|
|
168
121
|
// The first pass parses paragraphs, list items, etc. into standardized arrays
|
|
169
122
|
let first_pass = {
|
|
@@ -171,18 +124,29 @@
|
|
|
171
124
|
show par: p => context {
|
|
172
125
|
let nest_level = NEST_DOWN.get().at(0) - NEST_UP.get().at(0)
|
|
173
126
|
let is_heading = IS_HEADING.get()
|
|
127
|
+
let is_first_par = ITEM_FIRST_PAR.get()
|
|
128
|
+
|
|
129
|
+
// Determine if this is a continuation block within a multi-block list item.
|
|
130
|
+
// A continuation is a non-first paragraph inside a list item (nest_level > 0).
|
|
131
|
+
let is_continuation = nest_level > 0 and not is_first_par
|
|
174
132
|
|
|
175
133
|
PAR_BUFFER.update(pars => {
|
|
176
|
-
// Item tuple: (content, nest_level, is_heading, is_table)
|
|
177
|
-
pars.push((text([#p.body]), nest_level, is_heading, false))
|
|
134
|
+
// Item tuple: (content, nest_level, is_heading, is_table, is_continuation)
|
|
135
|
+
pars.push((text([#p.body]), nest_level, is_heading, false, is_continuation))
|
|
178
136
|
pars
|
|
179
137
|
})
|
|
138
|
+
|
|
139
|
+
// After the first paragraph of a list item, mark subsequent ones as continuations
|
|
140
|
+
if nest_level > 0 and is_first_par {
|
|
141
|
+
ITEM_FIRST_PAR.update(false)
|
|
142
|
+
}
|
|
143
|
+
|
|
180
144
|
p
|
|
181
145
|
}
|
|
182
146
|
// Collect tables — captured as-is without paragraph numbering
|
|
183
147
|
show table: t => context {
|
|
184
148
|
PAR_BUFFER.update(pars => {
|
|
185
|
-
pars.push((t, -1, false, true))
|
|
149
|
+
pars.push((t, -1, false, true, false))
|
|
186
150
|
pars
|
|
187
151
|
})
|
|
188
152
|
t
|
|
@@ -199,11 +163,13 @@
|
|
|
199
163
|
// layout convergence issues with many list items
|
|
200
164
|
show enum.item: it => {
|
|
201
165
|
NEST_DOWN.step()
|
|
166
|
+
ITEM_FIRST_PAR.update(true)
|
|
202
167
|
[#parbreak()#it.body#parbreak()]
|
|
203
168
|
NEST_UP.step()
|
|
204
169
|
}
|
|
205
170
|
show list.item: it => {
|
|
206
171
|
NEST_DOWN.step()
|
|
172
|
+
ITEM_FIRST_PAR.update(true)
|
|
207
173
|
[#parbreak()#it.body#parbreak()]
|
|
208
174
|
NEST_UP.step()
|
|
209
175
|
}
|
|
@@ -234,17 +200,34 @@
|
|
|
234
200
|
//Second pass: consume par buffer
|
|
235
201
|
//
|
|
236
202
|
// PAR_BUFFER item tuple layout:
|
|
237
|
-
// item.at(0) — content
|
|
238
|
-
// item.at(1) — nest_level
|
|
239
|
-
// item.at(2) — is_heading
|
|
240
|
-
// item.at(3) — is_table
|
|
203
|
+
// item.at(0) — content : the paragraph body or table element
|
|
204
|
+
// item.at(1) — nest_level : nesting depth (−1 for tables)
|
|
205
|
+
// item.at(2) — is_heading : bool, true if item is a heading paragraph
|
|
206
|
+
// item.at(3) — is_table : bool, true if item is a table element
|
|
207
|
+
// item.at(4) — is_continuation : bool, true if this is a continuation block
|
|
208
|
+
// within a multi-block list item
|
|
241
209
|
let ITEM_IS_TABLE = 3
|
|
210
|
+
let ITEM_IS_CONTINUATION = 4
|
|
242
211
|
context {
|
|
243
212
|
let heading_buffer = none
|
|
244
|
-
// Tables do not count as paragraphs
|
|
245
|
-
|
|
213
|
+
// Tables and continuation blocks do not count as distinct paragraphs
|
|
214
|
+
// for AFH 33-337 §2 numbering purposes
|
|
215
|
+
let par_count = PAR_BUFFER.get().filter(item =>
|
|
216
|
+
not item.at(ITEM_IS_TABLE, default: false)
|
|
217
|
+
and not item.at(ITEM_IS_CONTINUATION, default: false)
|
|
218
|
+
).len()
|
|
246
219
|
let items = PAR_BUFFER.get()
|
|
247
220
|
let total_count = items.len()
|
|
221
|
+
|
|
222
|
+
// Track paragraph numbers per level manually to avoid nested-context
|
|
223
|
+
// counter propagation issues. Dictionary maps level index (as string)
|
|
224
|
+
// to the current counter value at that level.
|
|
225
|
+
let max-levels = paragraph-config.numbering-formats.len()
|
|
226
|
+
let level-counts = (:)
|
|
227
|
+
for lvl in range(max-levels) {
|
|
228
|
+
level-counts.insert(str(lvl), 1)
|
|
229
|
+
}
|
|
230
|
+
|
|
248
231
|
let i = 0
|
|
249
232
|
for item in items {
|
|
250
233
|
i += 1
|
|
@@ -260,6 +243,7 @@
|
|
|
260
243
|
let par_content = item.at(0)
|
|
261
244
|
let nest_level = item.at(1)
|
|
262
245
|
let is_heading = item.at(2)
|
|
246
|
+
let is_continuation = item.at(ITEM_IS_CONTINUATION, default: false)
|
|
263
247
|
|
|
264
248
|
// Prepend heading as bolded sentence
|
|
265
249
|
if heading_buffer != none {
|
|
@@ -271,25 +255,47 @@
|
|
|
271
255
|
}
|
|
272
256
|
|
|
273
257
|
let final_par = {
|
|
274
|
-
if
|
|
258
|
+
if is_continuation {
|
|
259
|
+
// Continuation block within a multi-block list item:
|
|
260
|
+
// indent to align with preceding numbered paragraph's text, no new number.
|
|
261
|
+
// level-counts still holds the value of the preceding numbered paragraph.
|
|
262
|
+
if auto-numbering {
|
|
263
|
+
format-continuation-par(par_content, nest_level, level-counts)
|
|
264
|
+
} else if nest_level > 0 {
|
|
265
|
+
format-continuation-par(par_content, nest_level - 1, level-counts)
|
|
266
|
+
} else {
|
|
267
|
+
par_content
|
|
268
|
+
}
|
|
269
|
+
} else if auto-numbering {
|
|
275
270
|
if par_count > 1 {
|
|
276
271
|
// Apply paragraph numbering per AFH 33-337 §2
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
272
|
+
let par = format-numbered-par(par_content, nest_level, level-counts)
|
|
273
|
+
// Advance counter for this level and reset child levels
|
|
274
|
+
level-counts.insert(str(nest_level), level-counts.at(str(nest_level)) + 1)
|
|
275
|
+
for child in range(nest_level + 1, max-levels) {
|
|
276
|
+
level-counts.insert(str(child), 1)
|
|
277
|
+
}
|
|
278
|
+
par
|
|
280
279
|
} else {
|
|
281
280
|
// AFH 33-337 §2: "A single paragraph is not numbered"
|
|
282
|
-
// Return body content wrapped in block (like numbered case, but without numbering)
|
|
283
281
|
par_content
|
|
284
282
|
}
|
|
285
283
|
} else {
|
|
286
284
|
// Unnumbered mode: only explicitly nested items (enum/list) get numbered
|
|
287
285
|
if nest_level > 0 {
|
|
288
|
-
|
|
289
|
-
let
|
|
290
|
-
|
|
286
|
+
let effective_level = nest_level - 1
|
|
287
|
+
let par = format-numbered-par(par_content, effective_level, level-counts)
|
|
288
|
+
level-counts.insert(str(effective_level), level-counts.at(str(effective_level)) + 1)
|
|
289
|
+
for child in range(effective_level + 1, max-levels) {
|
|
290
|
+
level-counts.insert(str(child), 1)
|
|
291
|
+
}
|
|
292
|
+
par
|
|
291
293
|
} else {
|
|
292
294
|
// Base-level paragraphs are flush left with no numbering
|
|
295
|
+
// Reset all child level counters so subsequent list items restart at 1
|
|
296
|
+
for child in range(max-levels) {
|
|
297
|
+
level-counts.insert(str(child), 1)
|
|
298
|
+
}
|
|
293
299
|
par_content
|
|
294
300
|
}
|
|
295
301
|
}
|
|
@@ -23,10 +23,9 @@
|
|
|
23
23
|
date: none,
|
|
24
24
|
// Format of indorsement: "standard" (same page), "informal" (no header), or "separate_page" (starts on new page)
|
|
25
25
|
format: "standard",
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
action: none,
|
|
26
|
+
// Approval action: "none" (default, no action line displayed), "approve", or "disapprove".
|
|
27
|
+
// When set to "approve" or "disapprove", the action line is displayed with the selected option circled.
|
|
28
|
+
action: "none",
|
|
30
29
|
content,
|
|
31
30
|
) = {
|
|
32
31
|
// Validate format parameter
|
|
@@ -91,8 +90,8 @@
|
|
|
91
90
|
blank-line()
|
|
92
91
|
}
|
|
93
92
|
|
|
94
|
-
// Show action line
|
|
95
|
-
if
|
|
93
|
+
// Show action line only when an action decision is set (not "none")
|
|
94
|
+
if action != "none" {
|
|
96
95
|
render-action-line(action)
|
|
97
96
|
}
|
|
98
97
|
|
|
@@ -144,20 +144,30 @@
|
|
|
144
144
|
// =============================================================================
|
|
145
145
|
// ACTION LINE RENDERING
|
|
146
146
|
// =============================================================================
|
|
147
|
-
// Renders the
|
|
148
|
-
// action: none =
|
|
149
|
-
//
|
|
150
|
-
//
|
|
147
|
+
// Renders the APPROVE / DISAPPROVE action line for indorsement memos.
|
|
148
|
+
// action: "none" = no action line displayed (hidden), "approve" = APPROVE circled,
|
|
149
|
+
// "disapprove" = DISAPPROVE circled. The action line is only rendered when
|
|
150
|
+
// action is "approve" or "disapprove".
|
|
151
151
|
|
|
152
152
|
#let render-action-line(action) = {
|
|
153
153
|
assert(
|
|
154
|
-
action in (none, "
|
|
155
|
-
message: "action must be none, \"
|
|
154
|
+
action in ("none", "approve", "disapprove"),
|
|
155
|
+
message: "action must be \"none\", \"approve\", or \"disapprove\"",
|
|
156
156
|
)
|
|
157
157
|
blank-line()
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
// Circle the selected option using a box with rounded corners
|
|
159
|
+
// Use baseline parameter to maintain vertical text alignment
|
|
160
|
+
let approve-text = if action == "approve" {
|
|
161
|
+
box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[APPROVE]
|
|
162
|
+
} else {
|
|
163
|
+
[APPROVE]
|
|
164
|
+
}
|
|
165
|
+
let disapprove-text = if action == "disapprove" {
|
|
166
|
+
box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[DISAPPROVE]
|
|
167
|
+
} else {
|
|
168
|
+
[DISAPPROVE]
|
|
169
|
+
}
|
|
170
|
+
[#approve-text / #disapprove-text]
|
|
161
171
|
}
|
|
162
172
|
|
|
163
173
|
// =============================================================================
|