@tonguetoquill/collection 0.2.5-beta.1 → 0.2.6

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 (107) 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 +92 -88
  7. package/quills/af4141/0.1.0/assets/GNU General Public License.txt +340 -0
  8. package/quills/af4141/0.1.0/assets/NimbusRomNo9L-Med.otf +0 -0
  9. package/quills/af4141/0.1.0/assets/NimbusRomNo9L-MedIta.otf +0 -0
  10. package/quills/af4141/0.1.0/assets/NimbusRomNo9L-Reg.otf +0 -0
  11. package/quills/af4141/0.1.0/assets/NimbusRomNo9L-RegIta.otf +0 -0
  12. package/quills/af4141/0.1.0/design/TASK.md +19 -19
  13. package/quills/af4141/0.1.0/example.md +35 -35
  14. package/quills/af4141/0.1.0/packages/typst-af4141/FIELDS.json +3169 -3169
  15. package/quills/af4141/0.1.0/packages/typst-af4141/form.typ +538 -538
  16. package/quills/af4141/0.1.0/packages/typst-af4141/lib.typ +239 -227
  17. package/quills/af4141/0.1.0/packages/typst-af4141/typst.toml +7 -7
  18. package/quills/af4141/0.1.0/plate.typ +50 -48
  19. package/quills/classic_resume/0.1.0/Quill.yaml +118 -118
  20. package/quills/classic_resume/0.1.0/example.md +232 -232
  21. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/LICENSE +21 -21
  22. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/README.md +38 -38
  23. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/components.typ +184 -184
  24. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/layout.typ +42 -42
  25. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/src/lib.typ +5 -5
  26. package/quills/classic_resume/0.1.0/packages/ttq-classic-resume/typst.toml +26 -26
  27. package/quills/classic_resume/0.1.0/plate.typ +44 -44
  28. package/quills/cmu_letter/0.1.0/.quillignore +30 -30
  29. package/quills/cmu_letter/0.1.0/Quill.yaml +64 -64
  30. package/quills/cmu_letter/0.1.0/assets/cmu-wordmark.svg +174 -174
  31. package/quills/cmu_letter/0.1.0/example.md +30 -30
  32. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/LICENSE +21 -21
  33. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/fonts/README.txt +100 -100
  34. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/backmatter.typ +13 -13
  35. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/config.typ +39 -39
  36. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/frontmatter.typ +72 -72
  37. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/lib.typ +47 -47
  38. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/mainmatter.typ +42 -42
  39. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/primitives.typ +70 -70
  40. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/src/utils.typ +85 -85
  41. package/quills/cmu_letter/0.1.0/packages/tonguetoquill-cmu-letter/typst.toml +17 -17
  42. package/quills/cmu_letter/0.1.0/plate.typ +19 -19
  43. package/quills/taro/0.1.0/Quill.yaml +29 -29
  44. package/quills/taro/0.1.0/example.md +26 -26
  45. package/quills/taro/0.1.0/plate.typ +31 -31
  46. package/quills/usaf_memo/0.1.0/.quillignore +30 -30
  47. package/quills/usaf_memo/0.1.0/Quill.yaml +209 -209
  48. package/quills/usaf_memo/0.1.0/example.md +54 -54
  49. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -21
  50. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -93
  51. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -79
  52. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +339 -339
  53. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -28
  54. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/body.typ +332 -332
  55. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/config.typ +63 -63
  56. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -114
  57. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -118
  58. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -55
  59. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -32
  60. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +272 -272
  61. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/src/utils.typ +377 -377
  62. package/quills/usaf_memo/0.1.0/packages/tonguetoquill-usaf-memo/typst.toml +16 -16
  63. package/quills/usaf_memo/0.1.0/plate.typ +74 -74
  64. package/quills/usaf_memo/0.2.0/.quillignore +30 -30
  65. package/quills/usaf_memo/0.2.0/Quill.yaml +219 -219
  66. package/quills/usaf_memo/0.2.0/example.md +55 -55
  67. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/.gitignore +6 -6
  68. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -21
  69. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -93
  70. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -79
  71. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +339 -339
  72. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -28
  73. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +333 -333
  74. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/config.typ +64 -64
  75. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +114 -114
  76. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +118 -118
  77. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/lib.typ +55 -55
  78. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -32
  79. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +293 -293
  80. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +374 -374
  81. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +27 -27
  82. package/quills/usaf_memo/0.2.0/plate.typ +75 -75
  83. package/templates/af4141.md +88 -88
  84. package/templates/cmu_letter_template.md +37 -37
  85. package/templates/loc.md +78 -78
  86. package/templates/pass_request.md +43 -43
  87. package/templates/rebuttal.md +55 -55
  88. package/templates/taro.md +26 -26
  89. package/templates/templates.json +49 -55
  90. package/templates/usaf_template.md +23 -23
  91. package/templates/ussf_template.md +29 -29
  92. package/quills/daf4392/0.1.0/Quill.yaml +0 -110
  93. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-700.ttf +0 -0
  94. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-700italic.ttf +0 -0
  95. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-italic.ttf +0 -0
  96. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-regular.ttf +0 -0
  97. package/quills/daf4392/0.1.0/assets/page1.png +0 -0
  98. package/quills/daf4392/0.1.0/example.md +0 -33
  99. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/FIELDS.json +0 -9
  100. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/form.typ +0 -14
  101. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/lib.typ +0 -227
  102. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/out/debug.typ +0 -4
  103. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/out/example.typ +0 -4
  104. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/page1.png +0 -0
  105. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/typst.toml +0 -7
  106. package/quills/daf4392/0.1.0/plate.typ +0 -60
  107. package/templates/daf4392.md +0 -33
@@ -1,227 +1,239 @@
1
- // Formalizer Engine – rendering engine
2
- // Renders pixel-perfect PDF form replicas from a PyMuPDF-extracted schema.
3
-
4
- /// Render a single field's content overlay.
5
- ///
6
- /// - field-type (str): normalised lowercase type
7
- /// - value: user-supplied value for this field (or none)
8
- /// - width (length): field width
9
- /// - height (length): field height
10
- /// - field (dictionary): raw field entry from the schema
11
- #let render-field(field-type, value, width, height, field) = {
12
- if field-type == "text" {
13
- if value != none and str(value) != "" {
14
- context {
15
- let default-size = height * 0.6
16
- let min-size = 6pt
17
- let x-inset = 1.5pt
18
- let m = measure(text(size: default-size, str(value)))
19
- let scale = calc.min(1.0, (width - 2 * x-inset) / m.width)
20
- let final-size = calc.max(min-size, default-size * scale)
21
- box(
22
- width: width,
23
- height: height,
24
- clip: true,
25
- inset: (x: x-inset),
26
- align(left + horizon, text(size: final-size, str(value))),
27
- )
28
- }
29
- }
30
- } else if field-type == "checkbox" {
31
- if value == true {
32
- box(
33
- width: width,
34
- height: height,
35
- align(center + horizon, text(size: height * 0.8, "✓")),
36
- )
37
- }
38
- } else if field-type == "radio" {
39
- // value is true when this specific button is the selected one
40
- // (resolved at group level before calling this helper)
41
- if value == true {
42
- let dot-r = calc.min(width, height) * 0.3
43
- box(
44
- width: width,
45
- height: height,
46
- align(center + horizon, circle(radius: dot-r, fill: black)),
47
- )
48
- }
49
- } else if field-type == "combobox" or field-type == "listbox" {
50
- let display = if value != none { str(value) } else { "" }
51
- // Resolve export value display label when options are present
52
- if field.at("options", default: none) != none and value != none {
53
- for opt in field.options {
54
- if str(opt.at(0)) == str(value) {
55
- display = str(opt.at(1))
56
- }
57
- }
58
- }
59
- if display != "" {
60
- context {
61
- let default-size = height * 0.6
62
- let min-size = 6pt
63
- let x-inset = 2pt
64
- let m = measure(text(size: default-size, display))
65
- let scale = calc.min(1.0, (width - 2 * x-inset) / m.width)
66
- let final-size = calc.max(min-size, default-size * scale)
67
- box(
68
- width: width,
69
- height: height,
70
- clip: true,
71
- inset: (x: x-inset),
72
- align(left + horizon, text(size: final-size, display)),
73
- )
74
- }
75
- }
76
- }
77
- }
78
-
79
- /// Determine whether a single radio button should appear selected.
80
- ///
81
- /// Strategy:
82
- /// 1. If the field carries an `export_value` key, match against it.
83
- /// 2. Otherwise fall back to matching the 0-based index within the group
84
- /// (stringified) against the supplied value.
85
- #let radio-button-selected(field, group-value, index-in-group) = {
86
- if group-value == none { return false }
87
- let ev = field.at("export_value", default: none)
88
- if ev != none {
89
- return str(ev) == str(group-value)
90
- }
91
- // Fallback: match against the stringified index
92
- str(index-in-group) == str(group-value)
93
- }
94
-
95
- /// Draw a debug overlay rectangle with a label for a field.
96
- ///
97
- /// - field-type (str): normalised lowercase type
98
- /// - name (str): field name
99
- /// - width (length): field width
100
- /// - height (length): field height
101
- #let debug-overlay(field-type, name, width, height) = {
102
- let color = if field-type == "text" {
103
- rgb(0, 0, 255, 40%)
104
- } else if field-type == "checkbox" {
105
- rgb(0, 128, 0, 40%)
106
- } else if field-type == "radio" {
107
- rgb(255, 165, 0, 40%)
108
- } else if field-type == "combobox" {
109
- rgb(128, 0, 128, 40%)
110
- } else if field-type == "listbox" {
111
- rgb(0, 128, 128, 40%)
112
- } else {
113
- rgb(128, 128, 128, 40%)
114
- }
115
-
116
- let stroke-color = if field-type == "text" {
117
- rgb(0, 0, 255)
118
- } else if field-type == "checkbox" {
119
- rgb(0, 128, 0)
120
- } else if field-type == "radio" {
121
- rgb(255, 165, 0)
122
- } else if field-type == "combobox" {
123
- rgb(128, 0, 128)
124
- } else if field-type == "listbox" {
125
- rgb(0, 128, 128)
126
- } else {
127
- rgb(128, 128, 128)
128
- }
129
-
130
- // Insert zero-width spaces after _ and - so the label can wrap
131
- let breakable-name = name.replace("_", "_\u{200B}").replace("-", "-\u{200B}")
132
- let label = breakable-name + " [" + field-type + "]"
133
-
134
- box(width: width, height: height, {
135
- // Semi-transparent colored background
136
- rect(width: 100%, height: 100%, fill: color, stroke: 0.5pt + stroke-color)
137
- // Label in top-left — uses block so text wraps within field width
138
- place(
139
- top + left,
140
- dx: 1pt,
141
- dy: 1pt,
142
- block(
143
- width: calc.max(width - 2pt, 10pt),
144
- fill: white,
145
- inset: (x: 2pt, y: 1pt),
146
- radius: 2pt,
147
- stroke: 0.3pt + stroke-color,
148
- breakable: false,
149
- text(size: 5pt, fill: stroke-color, weight: "bold", label),
150
- ),
151
- )
152
- })
153
- }
154
-
155
- /// Main entry point.
156
- ///
157
- /// - schema (dictionary): result of `json("FIELDS.json")`
158
- /// - backgrounds (array): list of image paths / bytes, one per page
159
- /// - values (dictionary): field name → value; omit to render blank
160
- /// - debug (bool): when true, draw colored overlays on each field
161
- #let render-form(schema: none, backgrounds: (), values: (:), debug: false) = {
162
- assert(schema != none, message: "render-form: `schema` is required")
163
- assert(backgrounds.len() > 0, message: "render-form: `backgrounds` is required")
164
- let pages = schema.pages
165
- let fields = schema.fields
166
-
167
- // Track how many radio buttons we have seen per group so far
168
- let radio-counters = (:)
169
-
170
- for (i, page-info) in pages.enumerate() {
171
- let page-num = i + 1
172
- let page-fields = fields.filter(f => f.page == page-num)
173
-
174
- let bg = backgrounds.at(i)
175
- let pw = page-info.width * 1pt
176
- let ph = page-info.height * 1pt
177
-
178
- page(
179
- width: pw,
180
- height: ph,
181
- margin: 0pt,
182
- )[
183
- // Background image (the original PDF page rasterised as PNG)
184
- #place(top + left, image(bg, width: pw, height: ph, fit: "stretch"))
185
-
186
- // Field overlays
187
- #for field in page-fields.filter(f => ("text", "checkbox", "radio", "combobox", "listbox").contains(lower(
188
- f.type,
189
- ))) {
190
- let x = field.bbox.at(0) * 1pt
191
- let y = field.bbox.at(1) * 1pt
192
- let w = (field.bbox.at(2) - field.bbox.at(0)) * 1pt
193
- let h = (field.bbox.at(3) - field.bbox.at(1)) * 1pt
194
-
195
- let field-type = lower(field.type)
196
- let val = values.at(field.name, default: none)
197
-
198
- // --- Radio: resolve group-level value to per-button boolean ---
199
- if field-type == "radio" {
200
- let group-name = field.name
201
- let idx = radio-counters.at(group-name, default: 0)
202
- radio-counters.insert(group-name, idx + 1)
203
- val = radio-button-selected(field, val, idx)
204
- }
205
-
206
- place(
207
- top + left,
208
- dx: x,
209
- dy: y,
210
- render-field(field-type, val, w, h, field),
211
- )
212
-
213
- if debug {
214
- place(
215
- top + left,
216
- dx: x,
217
- dy: y,
218
- debug-overlay(field-type, field.name, w, h),
219
- )
220
- }
221
-
222
- // Zero-width fence to break PDF viewer text-selection grouping
223
- place(top + left, dx: x, dy: y, text(size: 0.001pt, "\u{FEFF}"))
224
- }
225
- ]
226
- }
227
- }
1
+ // Formalizer Engine – rendering engine
2
+ // Renders pixel-perfect PDF form replicas from a PyMuPDF-extracted schema.
3
+
4
+ /// Global configuration for text rendering.
5
+ /// Adjust these to change the overall form text appearance.
6
+ #let FORM_MAX_TEXT_SIZE = 14pt
7
+ #let FORM_MIN_TEXT_SIZE = 6pt
8
+ #let FORM_MIN_CHARS_PER_LINE = 7
9
+
10
+ /// Render a text-like field with word-wrapping and shrink-to-fit.
11
+ #let render-text-field(display, width, height, x-inset, y-inset) = {
12
+ set par(leading: 0.25em)
13
+ context {
14
+ let final-size = FORM_MIN_TEXT_SIZE
15
+ let current = FORM_MAX_TEXT_SIZE
16
+ let step = 0.5pt
17
+
18
+ // Find the largest font size that fits both horizontally and vertically
19
+ // and optionally leaves enough room for a minimum number of characters
20
+ while current >= FORM_MIN_TEXT_SIZE {
21
+ let m = measure(block(width: width - 2 * x-inset, text(size: current, display)))
22
+ let char-m = measure(text(size: current, "0" * FORM_MIN_CHARS_PER_LINE))
23
+
24
+ if m.height <= height - 2 * y-inset and char-m.width <= width - 2 * x-inset {
25
+ final-size = current
26
+ break
27
+ }
28
+ current = current - step
29
+ }
30
+ // Alignment: center vertically if it's a short box (likely single line),
31
+ // otherwise top-align for multi-line paragraphs.
32
+ let vert-align = if height < 24pt { horizon } else { top }
33
+
34
+ box(
35
+ width: width,
36
+ height: height,
37
+ clip: true,
38
+ inset: (x: x-inset, y: y-inset),
39
+ align(left + vert-align, text(size: final-size, display)),
40
+ )
41
+ }
42
+ }
43
+
44
+ /// Render a single field's content overlay.
45
+ ///
46
+ /// - field-type (str): normalised lowercase type
47
+ /// - value: user-supplied value for this field (or none)
48
+ /// - width (length): field width
49
+ /// - height (length): field height
50
+ /// - field (dictionary): raw field entry from the schema
51
+ #let render-field(field-type, value, width, height, field) = {
52
+ if field-type == "text" {
53
+ if value != none and str(value) != "" {
54
+ render-text-field(str(value), width, height, 1.5pt, 1pt)
55
+ }
56
+ } else if field-type == "checkbox" {
57
+ if value == true {
58
+ box(
59
+ width: width,
60
+ height: height,
61
+ align(center + horizon, text(size: height * 0.8, "✓")),
62
+ )
63
+ }
64
+ } else if field-type == "radio" {
65
+ // value is true when this specific button is the selected one
66
+ // (resolved at group level before calling this helper)
67
+ if value == true {
68
+ let dot-r = calc.min(width, height) * 0.3
69
+ box(
70
+ width: width,
71
+ height: height,
72
+ align(center + horizon, circle(radius: dot-r, fill: black)),
73
+ )
74
+ }
75
+ } else if field-type == "combobox" or field-type == "listbox" {
76
+ let display = if value != none { str(value) } else { "" }
77
+ // Resolve export value → display label when options are present
78
+ if field.at("options", default: none) != none and value != none {
79
+ for opt in field.options {
80
+ if str(opt.at(0)) == str(value) {
81
+ display = str(opt.at(1))
82
+ }
83
+ }
84
+ }
85
+ if display != "" {
86
+ render-text-field(display, width, height, 2pt, 1pt)
87
+ }
88
+ }
89
+ }
90
+
91
+ /// Determine whether a single radio button should appear selected.
92
+ ///
93
+ /// Strategy:
94
+ /// 1. If the field carries an `export_value` key, match against it.
95
+ /// 2. Otherwise fall back to matching the 0-based index within the group
96
+ /// (stringified) against the supplied value.
97
+ #let radio-button-selected(field, group-value, index-in-group) = {
98
+ if group-value == none { return false }
99
+ let ev = field.at("export_value", default: none)
100
+ if ev != none {
101
+ return str(ev) == str(group-value)
102
+ }
103
+ // Fallback: match against the stringified index
104
+ str(index-in-group) == str(group-value)
105
+ }
106
+
107
+ /// Draw a debug overlay rectangle with a label for a field.
108
+ ///
109
+ /// - field-type (str): normalised lowercase type
110
+ /// - name (str): field name
111
+ /// - width (length): field width
112
+ /// - height (length): field height
113
+ #let debug-overlay(field-type, name, width, height) = {
114
+ let color = if field-type == "text" {
115
+ rgb(0, 0, 255, 40%)
116
+ } else if field-type == "checkbox" {
117
+ rgb(0, 128, 0, 40%)
118
+ } else if field-type == "radio" {
119
+ rgb(255, 165, 0, 40%)
120
+ } else if field-type == "combobox" {
121
+ rgb(128, 0, 128, 40%)
122
+ } else if field-type == "listbox" {
123
+ rgb(0, 128, 128, 40%)
124
+ } else {
125
+ rgb(128, 128, 128, 40%)
126
+ }
127
+
128
+ let stroke-color = if field-type == "text" {
129
+ rgb(0, 0, 255)
130
+ } else if field-type == "checkbox" {
131
+ rgb(0, 128, 0)
132
+ } else if field-type == "radio" {
133
+ rgb(255, 165, 0)
134
+ } else if field-type == "combobox" {
135
+ rgb(128, 0, 128)
136
+ } else if field-type == "listbox" {
137
+ rgb(0, 128, 128)
138
+ } else {
139
+ rgb(128, 128, 128)
140
+ }
141
+
142
+ // Insert zero-width spaces after _ and - so the label can wrap
143
+ let breakable-name = name.replace("_", "_\u{200B}").replace("-", "-\u{200B}")
144
+ let label = breakable-name + " [" + field-type + "]"
145
+
146
+ box(width: width, height: height, {
147
+ // Semi-transparent colored background
148
+ rect(width: 100%, height: 100%, fill: color, stroke: 0.5pt + stroke-color)
149
+ // Label in top-left uses block so text wraps within field width
150
+ place(
151
+ top + left,
152
+ dx: 1pt,
153
+ dy: 1pt,
154
+ block(
155
+ width: calc.max(width - 2pt, 10pt),
156
+ fill: white,
157
+ inset: (x: 2pt, y: 1pt),
158
+ radius: 2pt,
159
+ stroke: 0.3pt + stroke-color,
160
+ breakable: false,
161
+ text(size: 5pt, fill: stroke-color, weight: "bold", label),
162
+ ),
163
+ )
164
+ })
165
+ }
166
+
167
+ /// Main entry point.
168
+ ///
169
+ /// - schema (dictionary): result of `json("FIELDS.json")`
170
+ /// - backgrounds (array): list of image paths / bytes, one per page
171
+ /// - values (dictionary): field name → value; omit to render blank
172
+ /// - debug (bool): when true, draw colored overlays on each field
173
+ #let render-form(schema: none, backgrounds: (), values: (:), debug: false) = {
174
+ assert(schema != none, message: "render-form: `schema` is required")
175
+ assert(backgrounds.len() > 0, message: "render-form: `backgrounds` is required")
176
+ let pages = schema.pages
177
+ let fields = schema.fields
178
+
179
+ // Track how many radio buttons we have seen per group so far
180
+ let radio-counters = (:)
181
+
182
+ for (i, page-info) in pages.enumerate() {
183
+ let page-num = i + 1
184
+ let page-fields = fields.filter(f => f.page == page-num)
185
+
186
+ let bg = backgrounds.at(i)
187
+ let pw = page-info.width * 1pt
188
+ let ph = page-info.height * 1pt
189
+
190
+ page(
191
+ width: pw,
192
+ height: ph,
193
+ margin: 0pt,
194
+ )[
195
+ // Background image (the original PDF page rasterised as PNG)
196
+ #place(top + left, image(bg, width: pw, height: ph, fit: "stretch"))
197
+
198
+ // Field overlays
199
+ #for field in page-fields.filter(f => ("text", "checkbox", "radio", "combobox", "listbox").contains(lower(
200
+ f.type,
201
+ ))) {
202
+ let x = field.bbox.at(0) * 1pt
203
+ let y = field.bbox.at(1) * 1pt
204
+ let w = (field.bbox.at(2) - field.bbox.at(0)) * 1pt
205
+ let h = (field.bbox.at(3) - field.bbox.at(1)) * 1pt
206
+
207
+ let field-type = lower(field.type)
208
+ let val = values.at(field.name, default: none)
209
+
210
+ // --- Radio: resolve group-level value to per-button boolean ---
211
+ if field-type == "radio" {
212
+ let group-name = field.name
213
+ let idx = radio-counters.at(group-name, default: 0)
214
+ radio-counters.insert(group-name, idx + 1)
215
+ val = radio-button-selected(field, val, idx)
216
+ }
217
+
218
+ place(
219
+ top + left,
220
+ dx: x,
221
+ dy: y,
222
+ render-field(field-type, val, w, h, field),
223
+ )
224
+
225
+ if debug {
226
+ place(
227
+ top + left,
228
+ dx: x,
229
+ dy: y,
230
+ debug-overlay(field-type, field.name, w, h),
231
+ )
232
+ }
233
+
234
+ // Zero-width fence to break PDF viewer text-selection grouping
235
+ place(top + left, dx: x, dy: y, text(size: 0.001pt, "\u{FEFF}"))
236
+ }
237
+ ]
238
+ }
239
+ }
@@ -1,7 +1,7 @@
1
- [package]
2
- name = "typst-af4141"
3
- version = "0.1.0"
4
- entrypoint = "form.typ"
5
- authors = ["formalizer"]
6
- license = "Apache-2.0"
7
- description = "Fillable form package generated by formalizer"
1
+ [package]
2
+ name = "typst-af4141"
3
+ version = "0.1.0"
4
+ entrypoint = "form.typ"
5
+ authors = ["formalizer"]
6
+ license = "Apache-2.0"
7
+ description = "Fillable form package generated by formalizer"
@@ -1,48 +1,50 @@
1
- #import "@local/quillmark-helper:0.1.0": data
2
- #import "@local/typst-af4141:0.1.0": form
3
-
4
- // Column order within each 7-field row group
5
- #let col-keys = (
6
- "date",
7
- "action",
8
- "written_grade",
9
- "written_grade_date",
10
- "positional_grade",
11
- "positional_grade_date",
12
- "auth_or_remarks",
13
- )
14
-
15
- // Build the values dictionary for the form
16
- #let vals = (:)
17
-
18
- // --- Admin fields (page 1 header) ---
19
- #if "name" in data { vals.insert("commonforms_text_p1_1", data.name) }
20
- #if "unit" in data { vals.insert("commonforms_text_p1_2", data.unit) }
21
- #if "grade" in data { vals.insert("commonforms_text_p1_3", data.grade) }
22
- #if "commanders_auth" in data { vals.insert("commonforms_text_p1_116", data.commanders_auth) }
23
-
24
- // --- Experience table rows from cards ---
25
- #{
26
- let row = 0
27
- for card in data.CARDS {
28
- if card.CARD == "experience" {
29
- for (col, key) in col-keys.enumerate() {
30
- let value = card.at(key, default: "")
31
- if value != "" {
32
- let field-name = if row < 16 {
33
- // Page 1: fields start at index 4, stride 7
34
- "commonforms_text_p1_" + str(4 + row * 7 + col)
35
- } else {
36
- // Page 2: fields start at index 1, stride 7
37
- "commonforms_text_p2_" + str(1 + (row - 16) * 7 + col)
38
- }
39
- vals.insert(field-name, value)
40
- }
41
- }
42
- row = row + 1
43
- }
44
- }
45
- }
46
-
47
- // Render the form with assembled values
48
- #form(..vals)
1
+ #import "@local/quillmark-helper:0.1.0": data
2
+ #import "@local/typst-af4141:0.1.0": form
3
+
4
+ #set text(font: ("NimbusRomNo9L", "Times New Roman", "serif"))
5
+
6
+ // Column order within each 7-field row group
7
+ #let col-keys = (
8
+ "date",
9
+ "action",
10
+ "written_grade",
11
+ "written_grade_date",
12
+ "positional_grade",
13
+ "positional_grade_date",
14
+ "auth_or_remarks",
15
+ )
16
+
17
+ // Build the values dictionary for the form
18
+ #let vals = (:)
19
+
20
+ // --- Admin fields (page 1 header) ---
21
+ #if "name" in data { vals.insert("commonforms_text_p1_1", data.name) }
22
+ #if "unit" in data { vals.insert("commonforms_text_p1_2", data.unit) }
23
+ #if "grade" in data { vals.insert("commonforms_text_p1_3", data.grade) }
24
+ #if "commanders_auth" in data { vals.insert("commonforms_text_p1_116", data.commanders_auth) }
25
+
26
+ // --- Experience table rows from cards ---
27
+ #{
28
+ let row = 0
29
+ for card in data.CARDS {
30
+ if card.CARD == "experience" {
31
+ for (col, key) in col-keys.enumerate() {
32
+ let value = card.at(key, default: "")
33
+ if value != "" {
34
+ let field-name = if row < 16 {
35
+ // Page 1: fields start at index 4, stride 7
36
+ "commonforms_text_p1_" + str(4 + row * 7 + col)
37
+ } else {
38
+ // Page 2: fields start at index 1, stride 7
39
+ "commonforms_text_p2_" + str(1 + (row - 16) * 7 + col)
40
+ }
41
+ vals.insert(field-name, value)
42
+ }
43
+ }
44
+ row = row + 1
45
+ }
46
+ }
47
+ }
48
+
49
+ // Render the form with assembled values
50
+ #form(..vals)