@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.
- package/LICENSE +201 -201
- package/README.md +39 -39
- package/index.d.ts +2 -2
- package/index.js +8 -8
- package/package.json +41 -41
- package/quills/af4141/0.1.0/Quill.yaml +88 -88
- package/quills/af4141/0.1.0/design/TASK.md +19 -19
- package/quills/af4141/0.1.0/example.md +35 -35
- package/quills/af4141/0.1.0/packages/typst-af4141/FIELDS.json +3169 -3169
- package/quills/af4141/0.1.0/packages/typst-af4141/form.typ +538 -538
- package/quills/af4141/0.1.0/packages/typst-af4141/lib.typ +227 -227
- package/quills/af4141/0.1.0/packages/typst-af4141/typst.toml +7 -7
- package/quills/af4141/0.1.0/plate.typ +48 -48
- package/quills/classic_resume/0.1.0/Quill.yaml +118 -118
- package/quills/classic_resume/0.1.0/example.md +232 -232
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/LICENSE +21 -21
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/README.md +38 -38
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/components.typ +184 -184
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/layout.typ +42 -42
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/lib.typ +5 -5
- package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/typst.toml +26 -26
- package/quills/classic_resume/0.1.0/plate.typ +44 -44
- package/quills/cmu_letter/0.1.0/.quillignore +30 -30
- package/quills/cmu_letter/0.1.0/Quill.yaml +64 -64
- package/quills/cmu_letter/0.1.0/assets/cmu-wordmark.svg +174 -174
- package/quills/cmu_letter/0.1.0/example.md +30 -30
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/LICENSE +21 -21
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/README.txt +100 -100
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/backmatter.typ +13 -13
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/config.typ +39 -39
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/frontmatter.typ +72 -72
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/lib.typ +47 -47
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/mainmatter.typ +42 -42
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/primitives.typ +70 -70
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/utils.typ +85 -85
- package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/typst.toml +17 -17
- package/quills/cmu_letter/0.1.0/plate.typ +19 -19
- package/quills/daf4392/0.1.0/Quill.yaml +110 -0
- package/quills/daf4392/0.1.0/assets/arimo-v35-latin-700.ttf +0 -0
- package/quills/daf4392/0.1.0/assets/arimo-v35-latin-700italic.ttf +0 -0
- package/quills/daf4392/0.1.0/assets/arimo-v35-latin-italic.ttf +0 -0
- package/quills/daf4392/0.1.0/assets/arimo-v35-latin-regular.ttf +0 -0
- package/quills/daf4392/0.1.0/assets/page1.png +0 -0
- package/quills/daf4392/0.1.0/example.md +33 -0
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/FIELDS.json +9 -0
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/form.typ +14 -0
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/lib.typ +227 -0
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/out/debug.typ +4 -0
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/out/example.typ +4 -0
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/page1.png +0 -0
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/typst.toml +7 -0
- package/quills/daf4392/0.1.0/plate.typ +60 -0
- package/quills/taro/0.1.0/Quill.yaml +29 -29
- package/quills/taro/0.1.0/example.md +26 -26
- package/quills/taro/0.1.0/plate.typ +31 -31
- package/quills/usaf_memo/0.1.0/.quillignore +30 -30
- package/quills/usaf_memo/0.1.0/Quill.yaml +209 -209
- package/quills/usaf_memo/0.1.0/example.md +54 -54
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -21
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -93
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -79
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +339 -339
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -28
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/body.typ +332 -332
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/config.typ +63 -63
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -114
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -118
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -55
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -32
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +272 -272
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/utils.typ +377 -377
- package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/typst.toml +16 -16
- package/quills/usaf_memo/0.1.0/plate.typ +74 -74
- package/quills/usaf_memo/0.2.0/.quillignore +30 -30
- package/quills/usaf_memo/0.2.0/Quill.yaml +219 -219
- package/quills/usaf_memo/0.2.0/example.md +55 -55
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/.gitignore +6 -6
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -21
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -93
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -79
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +339 -339
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -28
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +333 -333
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/config.typ +64 -64
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -114
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -118
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -55
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -32
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +293 -293
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +374 -374
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +27 -27
- package/quills/usaf_memo/0.2.0/plate.typ +75 -75
- package/templates/af4141.md +88 -88
- package/templates/cmu_letter_template.md +37 -37
- package/templates/daf4392.md +33 -0
- package/templates/loc.md +78 -78
- package/templates/pass_request.md +43 -43
- package/templates/rebuttal.md +55 -55
- package/templates/taro.md +26 -26
- package/templates/templates.json +55 -49
- package/templates/usaf_template.md +23 -23
- 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
|
+
|