@tonguetoquill/collection 0.1.13 → 0.1.15
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": "@tonguetoquill/collection",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
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": "node scripts/
|
|
25
|
+
"test": "node scripts/check-duplicates.mjs && node scripts/validate-quills.mjs",
|
|
26
26
|
"build": "echo \"No build specified\"",
|
|
27
27
|
"preversion": "npm test",
|
|
28
28
|
"postversion": "git push --follow-tags",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@quillmark/registry": "^0.
|
|
38
|
+
"@quillmark/registry": "^0.6.1",
|
|
39
39
|
"@quillmark/wasm": "^0.39.0"
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -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,36 @@
|
|
|
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
|
-
|
|
177
|
-
|
|
134
|
+
pars.push((
|
|
135
|
+
content: text([#p.body]),
|
|
136
|
+
nest_level: nest_level,
|
|
137
|
+
kind: if is_heading { "heading" } else if is_continuation { "continuation" } else { "par" },
|
|
138
|
+
))
|
|
178
139
|
pars
|
|
179
140
|
})
|
|
141
|
+
|
|
142
|
+
// After the first paragraph of a list item, mark subsequent ones as continuations
|
|
143
|
+
if nest_level > 0 and is_first_par {
|
|
144
|
+
ITEM_FIRST_PAR.update(false)
|
|
145
|
+
}
|
|
146
|
+
|
|
180
147
|
p
|
|
181
148
|
}
|
|
182
149
|
// Collect tables — captured as-is without paragraph numbering
|
|
183
150
|
show table: t => context {
|
|
184
151
|
PAR_BUFFER.update(pars => {
|
|
185
|
-
pars.push((
|
|
152
|
+
pars.push((
|
|
153
|
+
content: t,
|
|
154
|
+
nest_level: -1,
|
|
155
|
+
kind: "table",
|
|
156
|
+
))
|
|
186
157
|
pars
|
|
187
158
|
})
|
|
188
159
|
t
|
|
@@ -199,11 +170,13 @@
|
|
|
199
170
|
// layout convergence issues with many list items
|
|
200
171
|
show enum.item: it => {
|
|
201
172
|
NEST_DOWN.step()
|
|
173
|
+
ITEM_FIRST_PAR.update(true)
|
|
202
174
|
[#parbreak()#it.body#parbreak()]
|
|
203
175
|
NEST_UP.step()
|
|
204
176
|
}
|
|
205
177
|
show list.item: it => {
|
|
206
178
|
NEST_DOWN.step()
|
|
179
|
+
ITEM_FIRST_PAR.update(true)
|
|
207
180
|
[#parbreak()#it.body#parbreak()]
|
|
208
181
|
NEST_UP.step()
|
|
209
182
|
}
|
|
@@ -231,71 +204,106 @@
|
|
|
231
204
|
// Use place() to prevent hidden content from affecting layout flow
|
|
232
205
|
place(hide(first_pass))
|
|
233
206
|
|
|
234
|
-
//Second pass: consume par buffer
|
|
207
|
+
// Second pass: consume par buffer
|
|
235
208
|
//
|
|
236
|
-
// PAR_BUFFER item
|
|
237
|
-
// item.
|
|
238
|
-
// item.
|
|
239
|
-
// item.
|
|
240
|
-
// item.at(3) — is_table : bool, true if item is a table element
|
|
241
|
-
let ITEM_IS_TABLE = 3
|
|
209
|
+
// PAR_BUFFER item dictionary layout:
|
|
210
|
+
// item.content — the paragraph body or table element
|
|
211
|
+
// item.nest_level — nesting depth (−1 for tables)
|
|
212
|
+
// item.kind — "par", "heading", "table", or "continuation"
|
|
242
213
|
context {
|
|
243
214
|
let heading_buffer = none
|
|
244
|
-
//
|
|
245
|
-
let par_count = PAR_BUFFER.get().filter(item =>
|
|
215
|
+
// Only top-level paragraphs count for AFH 33-337 §2 numbering purposes
|
|
216
|
+
let par_count = PAR_BUFFER.get().filter(item => item.kind == "par").len()
|
|
246
217
|
let items = PAR_BUFFER.get()
|
|
247
218
|
let total_count = items.len()
|
|
219
|
+
|
|
220
|
+
// Track paragraph numbers per level manually to avoid nested-context
|
|
221
|
+
// counter propagation issues. Dictionary maps level index (as string)
|
|
222
|
+
// to the current counter value at that level.
|
|
223
|
+
let max-levels = paragraph-config.numbering-formats.len()
|
|
224
|
+
let level-counts = (:)
|
|
225
|
+
for lvl in range(max-levels) {
|
|
226
|
+
level-counts.insert(str(lvl), 1)
|
|
227
|
+
}
|
|
228
|
+
|
|
248
229
|
let i = 0
|
|
249
230
|
for item in items {
|
|
250
231
|
i += 1
|
|
251
|
-
let
|
|
232
|
+
let kind = item.kind
|
|
233
|
+
let item_content = item.content
|
|
252
234
|
|
|
253
|
-
//
|
|
254
|
-
if
|
|
255
|
-
|
|
256
|
-
render-memo-table(item.at(0))
|
|
235
|
+
// Buffer headings for prepend to the next rendered element
|
|
236
|
+
if kind == "heading" {
|
|
237
|
+
heading_buffer = item_content
|
|
257
238
|
continue
|
|
258
239
|
}
|
|
259
240
|
|
|
260
|
-
|
|
261
|
-
let nest_level = item.at(1)
|
|
262
|
-
let is_heading = item.at(2)
|
|
263
|
-
|
|
264
|
-
// Prepend heading as bolded sentence
|
|
241
|
+
// Prepend buffered heading to the next non-heading element
|
|
265
242
|
if heading_buffer != none {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
243
|
+
if kind == "table" {
|
|
244
|
+
// Tables cannot have inline text prepended; emit heading as
|
|
245
|
+
// a standalone bold line above the table
|
|
246
|
+
blank-line()
|
|
247
|
+
strong[#heading_buffer.]
|
|
248
|
+
heading_buffer = none
|
|
249
|
+
} else {
|
|
250
|
+
item_content = [#strong[#heading_buffer.] #item_content]
|
|
251
|
+
heading_buffer = none
|
|
252
|
+
}
|
|
271
253
|
}
|
|
272
254
|
|
|
255
|
+
// Format based on element kind
|
|
256
|
+
let nest_level = item.nest_level
|
|
273
257
|
let final_par = {
|
|
274
|
-
if
|
|
258
|
+
if kind == "table" {
|
|
259
|
+
render-memo-table(item_content)
|
|
260
|
+
} else if kind == "continuation" {
|
|
261
|
+
// Continuation block within a multi-block list item:
|
|
262
|
+
// indent to align with preceding numbered paragraph's text, no new number.
|
|
263
|
+
// level-counts still holds the value of the preceding numbered paragraph.
|
|
264
|
+
if auto-numbering {
|
|
265
|
+
format-continuation-par(item_content, nest_level, level-counts)
|
|
266
|
+
} else if nest_level > 0 {
|
|
267
|
+
format-continuation-par(item_content, nest_level - 1, level-counts)
|
|
268
|
+
} else {
|
|
269
|
+
item_content
|
|
270
|
+
}
|
|
271
|
+
} else if auto-numbering {
|
|
275
272
|
if par_count > 1 {
|
|
276
273
|
// Apply paragraph numbering per AFH 33-337 §2
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
274
|
+
let par = format-numbered-par(item_content, nest_level, level-counts)
|
|
275
|
+
// Advance counter for this level and reset child levels
|
|
276
|
+
level-counts.insert(str(nest_level), level-counts.at(str(nest_level)) + 1)
|
|
277
|
+
for child in range(nest_level + 1, max-levels) {
|
|
278
|
+
level-counts.insert(str(child), 1)
|
|
279
|
+
}
|
|
280
|
+
par
|
|
280
281
|
} else {
|
|
281
282
|
// AFH 33-337 §2: "A single paragraph is not numbered"
|
|
282
|
-
|
|
283
|
-
par_content
|
|
283
|
+
item_content
|
|
284
284
|
}
|
|
285
285
|
} else {
|
|
286
286
|
// Unnumbered mode: only explicitly nested items (enum/list) get numbered
|
|
287
287
|
if nest_level > 0 {
|
|
288
|
-
|
|
289
|
-
let
|
|
290
|
-
|
|
288
|
+
let effective_level = nest_level - 1
|
|
289
|
+
let par = format-numbered-par(item_content, effective_level, level-counts)
|
|
290
|
+
level-counts.insert(str(effective_level), level-counts.at(str(effective_level)) + 1)
|
|
291
|
+
for child in range(effective_level + 1, max-levels) {
|
|
292
|
+
level-counts.insert(str(child), 1)
|
|
293
|
+
}
|
|
294
|
+
par
|
|
291
295
|
} else {
|
|
292
296
|
// Base-level paragraphs are flush left with no numbering
|
|
293
|
-
|
|
297
|
+
// Reset all child level counters so subsequent list items restart at 1
|
|
298
|
+
for child in range(max-levels) {
|
|
299
|
+
level-counts.insert(str(child), 1)
|
|
300
|
+
}
|
|
301
|
+
item_content
|
|
294
302
|
}
|
|
295
303
|
}
|
|
296
304
|
}
|
|
297
305
|
|
|
298
|
-
//If this is the final
|
|
306
|
+
// If this is the final item, apply AFH 33-337 §11 rule:
|
|
299
307
|
// "Avoid dividing a paragraph of less than four lines between two pages"
|
|
300
308
|
blank-line()
|
|
301
309
|
if i == total_count {
|
|
@@ -303,16 +311,16 @@
|
|
|
303
311
|
|
|
304
312
|
// Use configured spacing for line height calculation
|
|
305
313
|
let line_height = measure(line(length: spacing.line + spacing.line-height)).width
|
|
306
|
-
// Calculate last
|
|
314
|
+
// Calculate last item's height
|
|
307
315
|
let par_height = measure(final_par, width: available_width).height
|
|
308
316
|
|
|
309
317
|
let estimated_lines = calc.ceil(par_height / line_height)
|
|
310
318
|
|
|
311
319
|
if estimated_lines < 4 {
|
|
312
|
-
// Short
|
|
320
|
+
// Short content (< 4 lines): make sticky to keep with signature
|
|
313
321
|
block(sticky: true)[#final_par]
|
|
314
322
|
} else {
|
|
315
|
-
// Longer
|
|
323
|
+
// Longer content (≥ 4 lines): use default breaking behavior
|
|
316
324
|
block(breakable: true)[#final_par]
|
|
317
325
|
}
|
|
318
326
|
} else {
|
|
@@ -322,4 +330,3 @@
|
|
|
322
330
|
}
|
|
323
331
|
}
|
|
324
332
|
|
|
325
|
-
|
|
@@ -172,7 +172,10 @@
|
|
|
172
172
|
} else {
|
|
173
173
|
[Disapprove]
|
|
174
174
|
}
|
|
175
|
-
|
|
175
|
+
// Keep the action line with the following content (body or signature block)
|
|
176
|
+
// using the same sticky-block pattern that body.typ applies to the last
|
|
177
|
+
// paragraph, per AFH 33-337 §11 orphan-prevention rules.
|
|
178
|
+
block(sticky: true)[#approve-text / #disapprove-text]
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
// =============================================================================
|