@tonguetoquill/collection 0.2.3 → 0.2.5-beta.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.
Files changed (102) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +39 -39
  3. package/index.d.ts +2 -2
  4. package/index.js +8 -8
  5. package/package.json +41 -41
  6. package/quills/af4141/0.1.0/Quill.yaml +88 -88
  7. package/quills/af4141/0.1.0/design/TASK.md +19 -19
  8. package/quills/af4141/0.1.0/example.md +35 -35
  9. package/quills/af4141/0.1.0/packages/typst-af4141/FIELDS.json +3169 -3169
  10. package/quills/af4141/0.1.0/packages/typst-af4141/form.typ +538 -538
  11. package/quills/af4141/0.1.0/packages/typst-af4141/lib.typ +227 -227
  12. package/quills/af4141/0.1.0/packages/typst-af4141/typst.toml +7 -7
  13. package/quills/af4141/0.1.0/plate.typ +48 -48
  14. package/quills/classic_resume/0.1.0/Quill.yaml +118 -118
  15. package/quills/classic_resume/0.1.0/example.md +232 -232
  16. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/LICENSE +21 -21
  17. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/README.md +38 -38
  18. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/components.typ +184 -184
  19. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/layout.typ +42 -42
  20. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/lib.typ +5 -5
  21. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/typst.toml +26 -26
  22. package/quills/classic_resume/0.1.0/plate.typ +44 -44
  23. package/quills/cmu_letter/0.1.0/.quillignore +30 -30
  24. package/quills/cmu_letter/0.1.0/Quill.yaml +64 -64
  25. package/quills/cmu_letter/0.1.0/assets/cmu-wordmark.svg +174 -174
  26. package/quills/cmu_letter/0.1.0/example.md +30 -30
  27. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/LICENSE +21 -21
  28. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/README.txt +100 -100
  29. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/backmatter.typ +13 -13
  30. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/config.typ +39 -39
  31. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/frontmatter.typ +72 -72
  32. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/lib.typ +47 -47
  33. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/mainmatter.typ +42 -42
  34. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/primitives.typ +70 -70
  35. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/utils.typ +85 -85
  36. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/typst.toml +17 -17
  37. package/quills/cmu_letter/0.1.0/plate.typ +19 -19
  38. package/quills/daf4392/0.1.0/Quill.yaml +110 -0
  39. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-700.ttf +0 -0
  40. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-700italic.ttf +0 -0
  41. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-italic.ttf +0 -0
  42. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-regular.ttf +0 -0
  43. package/quills/daf4392/0.1.0/assets/page1.png +0 -0
  44. package/quills/daf4392/0.1.0/example.md +33 -0
  45. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/FIELDS.json +9 -0
  46. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/form.typ +14 -0
  47. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/lib.typ +227 -0
  48. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/out/debug.typ +4 -0
  49. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/out/example.typ +4 -0
  50. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/page1.png +0 -0
  51. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/typst.toml +7 -0
  52. package/quills/daf4392/0.1.0/plate.typ +60 -0
  53. package/quills/taro/0.1.0/Quill.yaml +29 -29
  54. package/quills/taro/0.1.0/example.md +26 -26
  55. package/quills/taro/0.1.0/plate.typ +31 -31
  56. package/quills/usaf_memo/0.1.0/.quillignore +30 -30
  57. package/quills/usaf_memo/0.1.0/Quill.yaml +209 -209
  58. package/quills/usaf_memo/0.1.0/example.md +54 -54
  59. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -21
  60. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -93
  61. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -79
  62. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +339 -339
  63. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -28
  64. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/body.typ +332 -332
  65. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/config.typ +63 -63
  66. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -114
  67. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -118
  68. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -55
  69. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -32
  70. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +272 -272
  71. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/utils.typ +377 -377
  72. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/typst.toml +16 -16
  73. package/quills/usaf_memo/0.1.0/plate.typ +74 -74
  74. package/quills/usaf_memo/0.2.0/.quillignore +30 -30
  75. package/quills/usaf_memo/0.2.0/Quill.yaml +219 -219
  76. package/quills/usaf_memo/0.2.0/example.md +55 -55
  77. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/.gitignore +6 -6
  78. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -21
  79. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -93
  80. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -79
  81. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +339 -339
  82. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -28
  83. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +333 -333
  84. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/config.typ +64 -64
  85. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -114
  86. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -118
  87. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -55
  88. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -32
  89. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +293 -293
  90. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +374 -374
  91. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +27 -27
  92. package/quills/usaf_memo/0.2.0/plate.typ +75 -75
  93. package/templates/af4141.md +88 -88
  94. package/templates/cmu_letter_template.md +37 -37
  95. package/templates/daf4392.md +33 -0
  96. package/templates/loc.md +78 -78
  97. package/templates/pass_request.md +43 -43
  98. package/templates/rebuttal.md +55 -55
  99. package/templates/taro.md +26 -26
  100. package/templates/templates.json +55 -49
  101. package/templates/usaf_template.md +23 -23
  102. package/templates/ussf_template.md +29 -29
@@ -1,333 +1,333 @@
1
- // body.typ: Paragraph body rendering for USAF memorandum sections
2
- //
3
- // This module implements the visual rendering of AFH 33-337 compliant
4
- // paragraph bodies with proper numbering, nesting, and formatting.
5
-
6
- #import "config.typ": *
7
- #import "utils.typ": *
8
- #import "primitives.typ": render-memo-table
9
-
10
- // =============================================================================
11
- // PARAGRAPH NUMBERING UTILITIES
12
- // =============================================================================
13
-
14
- /// Gets the numbering format for a specific paragraph level.
15
- ///
16
- /// AFH 33-337 "The Text of the Official Memorandum" §2: "Number and letter each
17
- /// paragraph and subparagraph" with hierarchical numbering implied by examples.
18
- /// Standard military format follows the pattern: 1., a., (1), (a), etc.
19
- ///
20
- /// Returns the appropriate numbering format for AFH 33-337 compliant
21
- /// hierarchical paragraph numbering:
22
- /// - Level 0: "1." (1., 2., 3., etc.)
23
- /// - Level 1: "a." (a., b., c., etc.)
24
- /// - Level 2: "(1)" ((1), (2), (3), etc.)
25
- /// - Level 3: "(a)" ((a), (b), (c), etc.)
26
- /// - Level 4+: Underlined format for deeper nesting
27
- ///
28
- /// - level (int): Paragraph nesting level (0-based)
29
- /// -> str | function
30
- #let get-paragraph-numbering-format(level) = {
31
- paragraph-config.numbering-formats.at(level, default: "i.")
32
- }
33
-
34
- /// Calculates indentation width using explicit counter values.
35
- ///
36
- /// Computes the exact indentation needed for hierarchical paragraph alignment
37
- /// by measuring the cumulative width of all ancestor paragraph numbers and their
38
- /// spacing. Uses the provided counter values directly (no Typst counter reads).
39
- ///
40
- /// - level (int): Paragraph nesting level (0-based)
41
- /// - level-counts (dictionary): Maps level index strings to their current counter values
42
- /// -> length
43
- #let calculate-indent-from-counts(level, level-counts) = {
44
- if level == 0 {
45
- return 0pt
46
- }
47
- let total-indent = 0pt
48
- for ancestor-level in range(level) {
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)
52
- let width = measure([#ancestor-number#" "]).width
53
- total-indent += width
54
- }
55
- total-indent
56
- }
57
-
58
- /// Formats a numbered paragraph with proper indentation.
59
- ///
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.
63
- ///
64
- /// - body (content): Paragraph content to format
65
- /// - level (int): Paragraph nesting level (0-based)
66
- /// - level-counts (dictionary): Current counter values per level
67
- /// -> content
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]
74
- }
75
-
76
- /// Formats a continuation paragraph within a multi-block list item.
77
- ///
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.
82
- ///
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
86
- /// -> 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]
95
- }
96
-
97
- // =============================================================================
98
- // PARAGRAPH BODY RENDERING
99
- // =============================================================================
100
- // AFH 33-337 "The Text of the Official Memorandum" §1-12 specifies:
101
- // - Single-space text, double-space between paragraphs
102
- // - Number and letter each paragraph and subparagraph
103
- // - "A single paragraph is not numbered" (§2)
104
- // - First paragraph flush left, never indented
105
- // - Indent sub-paragraphs to align with first character of parent paragraph text
106
- #let render-body(content, auto-numbering: true) = {
107
- let PAR_BUFFER = state("PAR_BUFFER")
108
- PAR_BUFFER.update(())
109
- let NEST_DOWN = counter("NEST_DOWN")
110
- NEST_DOWN.update(0)
111
- let NEST_UP = counter("NEST_UP")
112
- NEST_UP.update(0)
113
- let IS_HEADING = state("IS_HEADING")
114
- IS_HEADING.update(false)
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)
120
-
121
- // The first pass parses paragraphs, list items, etc. into standardized arrays
122
- let first_pass = {
123
- // Collect pars with nesting level
124
- show par: p => context {
125
- let nest_level = NEST_DOWN.get().at(0) - NEST_UP.get().at(0)
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
132
-
133
- PAR_BUFFER.update(pars => {
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
- ))
139
- pars
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
-
147
- p
148
- }
149
- // Collect tables — captured as-is without paragraph numbering
150
- show table: t => context {
151
- PAR_BUFFER.update(pars => {
152
- pars.push((
153
- content: t,
154
- nest_level: -1,
155
- kind: "table",
156
- ))
157
- pars
158
- })
159
- t
160
- }
161
- {
162
- show heading: h => {
163
- IS_HEADING.update(true)
164
- [#parbreak()#h.body#parbreak()]
165
- IS_HEADING.update(false)
166
- }
167
-
168
- // Convert list/enum items to pars
169
- // Note: No context wrapper here - state updates don't need it and cause
170
- // layout convergence issues with many list items
171
- show enum.item: it => {
172
- NEST_DOWN.step()
173
- ITEM_FIRST_PAR.update(true)
174
- [#parbreak()#it.body#parbreak()]
175
- NEST_UP.step()
176
- }
177
- show list.item: it => {
178
- NEST_DOWN.step()
179
- ITEM_FIRST_PAR.update(true)
180
- [#parbreak()#it.body#parbreak()]
181
- NEST_UP.step()
182
- }
183
-
184
- {
185
- // Typst bug bandaid:
186
- // `show par` will not collect wrappers unless there is content outside
187
- // Add zero width space to always have content outside of wrapper
188
- show strong: it => {
189
- [#it#sym.zws]
190
- }
191
- show emph: it => {
192
- [#it#sym.zws]
193
- }
194
- show underline: it => {
195
- [#it#sym.zws]
196
- }
197
- show raw: it => {
198
- [#it#sym.zws]
199
- }
200
- [#content#parbreak()]
201
- }
202
- }
203
- }
204
- // Use place() to prevent hidden content from affecting layout flow
205
- place(hide(first_pass))
206
-
207
- // Second pass: consume par buffer
208
- //
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"
213
- context {
214
- let heading_buffer = none
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()
217
- let items = PAR_BUFFER.get()
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
-
229
- let i = 0
230
- for item in items {
231
- i += 1
232
- let kind = item.kind
233
- let item_content = item.content
234
-
235
- // Buffer headings for prepend to the next rendered element
236
- if kind == "heading" {
237
- heading_buffer = item_content
238
- continue
239
- }
240
-
241
- // Prepend buffered heading to the next non-heading element
242
- if heading_buffer != none {
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
- }
253
- }
254
-
255
- // Format based on element kind
256
- let nest_level = item.nest_level
257
- let final_par = {
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 {
272
- if par_count > 1 {
273
- // Apply paragraph numbering per AFH 33-337 §2
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
281
- } else {
282
- // AFH 33-337 §2: "A single paragraph is not numbered"
283
- item_content
284
- }
285
- } else {
286
- // Unnumbered mode: only explicitly nested items (enum/list) get numbered
287
- if nest_level > 0 {
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
295
- } else {
296
- // Base-level paragraphs are flush left with no numbering
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
302
- }
303
- }
304
- }
305
-
306
- // If this is the final item, apply AFH 33-337 §11 rule:
307
- // "Avoid dividing a paragraph of less than four lines between two pages"
308
- blank-line()
309
- if i == total_count {
310
- let available_width = page.width - spacing.margin * 2
311
-
312
- // Use configured paragraph metrics for line height estimation
313
- let line_height = measure(line(length: spacing.line + spacing.line-height)).width
314
- // Calculate last item's height
315
- let par_height = measure(final_par, width: available_width).height
316
-
317
- let estimated_lines = calc.ceil(par_height / line_height)
318
-
319
- if estimated_lines < 4 {
320
- // Short content (< 4 lines): make sticky to keep with signature
321
- block(sticky: true)[#final_par]
322
- } else {
323
- // Longer content (≥ 4 lines): use default breaking behavior
324
- block(breakable: true)[#final_par]
325
- }
326
- } else {
327
- final_par
328
- }
329
- }
330
- }
331
- }
332
-
333
-
1
+ // body.typ: Paragraph body rendering for USAF memorandum sections
2
+ //
3
+ // This module implements the visual rendering of AFH 33-337 compliant
4
+ // paragraph bodies with proper numbering, nesting, and formatting.
5
+
6
+ #import "config.typ": *
7
+ #import "utils.typ": *
8
+ #import "primitives.typ": render-memo-table
9
+
10
+ // =============================================================================
11
+ // PARAGRAPH NUMBERING UTILITIES
12
+ // =============================================================================
13
+
14
+ /// Gets the numbering format for a specific paragraph level.
15
+ ///
16
+ /// AFH 33-337 "The Text of the Official Memorandum" §2: "Number and letter each
17
+ /// paragraph and subparagraph" with hierarchical numbering implied by examples.
18
+ /// Standard military format follows the pattern: 1., a., (1), (a), etc.
19
+ ///
20
+ /// Returns the appropriate numbering format for AFH 33-337 compliant
21
+ /// hierarchical paragraph numbering:
22
+ /// - Level 0: "1." (1., 2., 3., etc.)
23
+ /// - Level 1: "a." (a., b., c., etc.)
24
+ /// - Level 2: "(1)" ((1), (2), (3), etc.)
25
+ /// - Level 3: "(a)" ((a), (b), (c), etc.)
26
+ /// - Level 4+: Underlined format for deeper nesting
27
+ ///
28
+ /// - level (int): Paragraph nesting level (0-based)
29
+ /// -> str | function
30
+ #let get-paragraph-numbering-format(level) = {
31
+ paragraph-config.numbering-formats.at(level, default: "i.")
32
+ }
33
+
34
+ /// Calculates indentation width using explicit counter values.
35
+ ///
36
+ /// Computes the exact indentation needed for hierarchical paragraph alignment
37
+ /// by measuring the cumulative width of all ancestor paragraph numbers and their
38
+ /// spacing. Uses the provided counter values directly (no Typst counter reads).
39
+ ///
40
+ /// - level (int): Paragraph nesting level (0-based)
41
+ /// - level-counts (dictionary): Maps level index strings to their current counter values
42
+ /// -> length
43
+ #let calculate-indent-from-counts(level, level-counts) = {
44
+ if level == 0 {
45
+ return 0pt
46
+ }
47
+ let total-indent = 0pt
48
+ for ancestor-level in range(level) {
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)
52
+ let width = measure([#ancestor-number#" "]).width
53
+ total-indent += width
54
+ }
55
+ total-indent
56
+ }
57
+
58
+ /// Formats a numbered paragraph with proper indentation.
59
+ ///
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.
63
+ ///
64
+ /// - body (content): Paragraph content to format
65
+ /// - level (int): Paragraph nesting level (0-based)
66
+ /// - level-counts (dictionary): Current counter values per level
67
+ /// -> content
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]
74
+ }
75
+
76
+ /// Formats a continuation paragraph within a multi-block list item.
77
+ ///
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.
82
+ ///
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
86
+ /// -> 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]
95
+ }
96
+
97
+ // =============================================================================
98
+ // PARAGRAPH BODY RENDERING
99
+ // =============================================================================
100
+ // AFH 33-337 "The Text of the Official Memorandum" §1-12 specifies:
101
+ // - Single-space text, double-space between paragraphs
102
+ // - Number and letter each paragraph and subparagraph
103
+ // - "A single paragraph is not numbered" (§2)
104
+ // - First paragraph flush left, never indented
105
+ // - Indent sub-paragraphs to align with first character of parent paragraph text
106
+ #let render-body(content, auto-numbering: true) = {
107
+ let PAR_BUFFER = state("PAR_BUFFER")
108
+ PAR_BUFFER.update(())
109
+ let NEST_DOWN = counter("NEST_DOWN")
110
+ NEST_DOWN.update(0)
111
+ let NEST_UP = counter("NEST_UP")
112
+ NEST_UP.update(0)
113
+ let IS_HEADING = state("IS_HEADING")
114
+ IS_HEADING.update(false)
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)
120
+
121
+ // The first pass parses paragraphs, list items, etc. into standardized arrays
122
+ let first_pass = {
123
+ // Collect pars with nesting level
124
+ show par: p => context {
125
+ let nest_level = NEST_DOWN.get().at(0) - NEST_UP.get().at(0)
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
132
+
133
+ PAR_BUFFER.update(pars => {
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
+ ))
139
+ pars
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
+
147
+ p
148
+ }
149
+ // Collect tables — captured as-is without paragraph numbering
150
+ show table: t => context {
151
+ PAR_BUFFER.update(pars => {
152
+ pars.push((
153
+ content: t,
154
+ nest_level: -1,
155
+ kind: "table",
156
+ ))
157
+ pars
158
+ })
159
+ t
160
+ }
161
+ {
162
+ show heading: h => {
163
+ IS_HEADING.update(true)
164
+ [#parbreak()#h.body#parbreak()]
165
+ IS_HEADING.update(false)
166
+ }
167
+
168
+ // Convert list/enum items to pars
169
+ // Note: No context wrapper here - state updates don't need it and cause
170
+ // layout convergence issues with many list items
171
+ show enum.item: it => {
172
+ NEST_DOWN.step()
173
+ ITEM_FIRST_PAR.update(true)
174
+ [#parbreak()#it.body#parbreak()]
175
+ NEST_UP.step()
176
+ }
177
+ show list.item: it => {
178
+ NEST_DOWN.step()
179
+ ITEM_FIRST_PAR.update(true)
180
+ [#parbreak()#it.body#parbreak()]
181
+ NEST_UP.step()
182
+ }
183
+
184
+ {
185
+ // Typst bug bandaid:
186
+ // `show par` will not collect wrappers unless there is content outside
187
+ // Add zero width space to always have content outside of wrapper
188
+ show strong: it => {
189
+ [#it#sym.zws]
190
+ }
191
+ show emph: it => {
192
+ [#it#sym.zws]
193
+ }
194
+ show underline: it => {
195
+ [#it#sym.zws]
196
+ }
197
+ show raw: it => {
198
+ [#it#sym.zws]
199
+ }
200
+ [#content#parbreak()]
201
+ }
202
+ }
203
+ }
204
+ // Use place() to prevent hidden content from affecting layout flow
205
+ place(hide(first_pass))
206
+
207
+ // Second pass: consume par buffer
208
+ //
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"
213
+ context {
214
+ let heading_buffer = none
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()
217
+ let items = PAR_BUFFER.get()
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
+
229
+ let i = 0
230
+ for item in items {
231
+ i += 1
232
+ let kind = item.kind
233
+ let item_content = item.content
234
+
235
+ // Buffer headings for prepend to the next rendered element
236
+ if kind == "heading" {
237
+ heading_buffer = item_content
238
+ continue
239
+ }
240
+
241
+ // Prepend buffered heading to the next non-heading element
242
+ if heading_buffer != none {
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
+ }
253
+ }
254
+
255
+ // Format based on element kind
256
+ let nest_level = item.nest_level
257
+ let final_par = {
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 {
272
+ if par_count > 1 {
273
+ // Apply paragraph numbering per AFH 33-337 §2
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
281
+ } else {
282
+ // AFH 33-337 §2: "A single paragraph is not numbered"
283
+ item_content
284
+ }
285
+ } else {
286
+ // Unnumbered mode: only explicitly nested items (enum/list) get numbered
287
+ if nest_level > 0 {
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
295
+ } else {
296
+ // Base-level paragraphs are flush left with no numbering
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
302
+ }
303
+ }
304
+ }
305
+
306
+ // If this is the final item, apply AFH 33-337 §11 rule:
307
+ // "Avoid dividing a paragraph of less than four lines between two pages"
308
+ blank-line()
309
+ if i == total_count {
310
+ let available_width = page.width - spacing.margin * 2
311
+
312
+ // Use configured paragraph metrics for line height estimation
313
+ let line_height = measure(line(length: spacing.line + spacing.line-height)).width
314
+ // Calculate last item's height
315
+ let par_height = measure(final_par, width: available_width).height
316
+
317
+ let estimated_lines = calc.ceil(par_height / line_height)
318
+
319
+ if estimated_lines < 4 {
320
+ // Short content (< 4 lines): make sticky to keep with signature
321
+ block(sticky: true)[#final_par]
322
+ } else {
323
+ // Longer content (≥ 4 lines): use default breaking behavior
324
+ block(breakable: true)[#final_par]
325
+ }
326
+ } else {
327
+ final_par
328
+ }
329
+ }
330
+ }
331
+ }
332
+
333
+