@airmark/quiver 0.1.0

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 (77) hide show
  1. package/Quiver.yaml +2 -0
  2. package/README.md +92 -0
  3. package/index.d.ts +16 -0
  4. package/index.js +28 -0
  5. package/package.json +44 -0
  6. package/quills/af4141/0.1.0/Quill.yaml +95 -0
  7. package/quills/af4141/0.1.0/assets/Figtree-Bold.ttf +0 -0
  8. package/quills/af4141/0.1.0/assets/Figtree-Italic.ttf +0 -0
  9. package/quills/af4141/0.1.0/assets/Figtree-Regular.ttf +0 -0
  10. package/quills/af4141/0.1.0/assets/GNU General Public License.txt +340 -0
  11. package/quills/af4141/0.1.0/assets/NimbusRomNo9L-Med.otf +0 -0
  12. package/quills/af4141/0.1.0/assets/NimbusRomNo9L-MedIta.otf +0 -0
  13. package/quills/af4141/0.1.0/assets/NimbusRomNo9L-Reg.otf +0 -0
  14. package/quills/af4141/0.1.0/assets/NimbusRomNo9L-RegIta.otf +0 -0
  15. package/quills/af4141/0.1.0/design/TASK.md +20 -0
  16. package/quills/af4141/0.1.0/design/af4141.pdf +0 -0
  17. package/quills/af4141/0.1.0/example.md +86 -0
  18. package/quills/af4141/0.1.0/packages/typst-af4141/FIELDS.json +3170 -0
  19. package/quills/af4141/0.1.0/packages/typst-af4141/form.typ +538 -0
  20. package/quills/af4141/0.1.0/packages/typst-af4141/lib.typ +270 -0
  21. package/quills/af4141/0.1.0/packages/typst-af4141/page1.png +0 -0
  22. package/quills/af4141/0.1.0/packages/typst-af4141/page2.png +0 -0
  23. package/quills/af4141/0.1.0/packages/typst-af4141/typst.toml +7 -0
  24. package/quills/af4141/0.1.0/plate.typ +67 -0
  25. package/quills/daf1206/0.1.0/DAFForm_1206_prepared.pdf +0 -0
  26. package/quills/daf1206/0.1.0/Quill.yaml +55 -0
  27. package/quills/daf1206/0.1.0/assets/arimo-v35-latin-700.ttf +0 -0
  28. package/quills/daf1206/0.1.0/assets/arimo-v35-latin-700italic.ttf +0 -0
  29. package/quills/daf1206/0.1.0/assets/arimo-v35-latin-italic.ttf +0 -0
  30. package/quills/daf1206/0.1.0/assets/arimo-v35-latin-regular.ttf +0 -0
  31. package/quills/daf1206/0.1.0/example.md +23 -0
  32. package/quills/daf1206/0.1.0/packages/typst-daf1206/FIELDS.json +158 -0
  33. package/quills/daf1206/0.1.0/packages/typst-daf1206/form.typ +36 -0
  34. package/quills/daf1206/0.1.0/packages/typst-daf1206/lib.typ +297 -0
  35. package/quills/daf1206/0.1.0/packages/typst-daf1206/page1.svg +826 -0
  36. package/quills/daf1206/0.1.0/packages/typst-daf1206/page2.svg +497 -0
  37. package/quills/daf1206/0.1.0/packages/typst-daf1206/typst.toml +7 -0
  38. package/quills/daf1206/0.1.0/plate.typ +47 -0
  39. package/quills/daf4392/0.1.0/Quill.yaml +139 -0
  40. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-700.ttf +0 -0
  41. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-700italic.ttf +0 -0
  42. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-italic.ttf +0 -0
  43. package/quills/daf4392/0.1.0/assets/arimo-v35-latin-regular.ttf +0 -0
  44. package/quills/daf4392/0.1.0/assets/page1.png +0 -0
  45. package/quills/daf4392/0.1.0/example.md +34 -0
  46. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/FIELDS.json +9 -0
  47. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/form.typ +14 -0
  48. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/lib.typ +227 -0
  49. package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/typst.toml +7 -0
  50. package/quills/daf4392/0.1.0/plate.typ +86 -0
  51. package/quills/usaf_memo/0.2.0/.quillignore +31 -0
  52. package/quills/usaf_memo/0.2.0/Quill.yaml +255 -0
  53. package/quills/usaf_memo/0.2.0/assets/dod_seal.png +0 -0
  54. package/quills/usaf_memo/0.2.0/assets/dow_seal.png +0 -0
  55. package/quills/usaf_memo/0.2.0/example.md +56 -0
  56. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/.gitignore +7 -0
  57. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -0
  58. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel-Regular.ttf +0 -0
  59. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -0
  60. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
  61. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -0
  62. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
  63. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
  64. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
  65. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
  66. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
  67. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +28 -0
  68. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +344 -0
  69. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/config.typ +73 -0
  70. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +137 -0
  71. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +111 -0
  72. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/lib.typ +57 -0
  73. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +32 -0
  74. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +321 -0
  75. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +356 -0
  76. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/typst.toml +27 -0
  77. package/quills/usaf_memo/0.2.0/plate.typ +85 -0
@@ -0,0 +1,297 @@
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
+ /// Coerce any user-supplied scalar value to a plain string.
11
+ /// Accepts the same types as Typst's built-in str(), plus content –
12
+ /// content is extracted via repr() with its surrounding square brackets stripped.
13
+ /// This lets callers write `[Hello]` (content) instead of `"Hello"` (string).
14
+ #let to-str(value) = {
15
+ if type(value) == content {
16
+ let r = repr(value)
17
+ // repr([hello]) → "[hello]" — strip the outer square brackets
18
+ if r.starts-with("[") and r.ends-with("]") {
19
+ r.slice(1, r.len() - 1)
20
+ } else {
21
+ r
22
+ }
23
+ } else {
24
+ str(value)
25
+ }
26
+ }
27
+
28
+ /// Should this field shrink text to a single line rather than word-wrap?
29
+ /// True for short/narrow fields with brief content (grades, ranks, dates).
30
+ /// `display` may be a string or content; char-count is skipped for content.
31
+ #let should-shrink-to-fit(display, width, height) = {
32
+ let aspect = width / height
33
+ // content has no .len(); treat as "long" so only the dimension conditions apply
34
+ let char-count = if type(display) == str { display.len() } else { 99 }
35
+ char-count <= 10 or height < 20pt or aspect > 4.0
36
+ }
37
+
38
+ /// Render a text-like field with shrink-to-fit and word-wrap fallback.
39
+ #let render-text-field(display, width, height, x-inset, y-inset) = {
40
+ set par(leading: 0.25em)
41
+ context {
42
+ let avail-w = width - 2 * x-inset
43
+ let avail-h = height - 2 * y-inset
44
+ let shrink = should-shrink-to-fit(display, width, height)
45
+
46
+ let final-size = FORM_MIN_TEXT_SIZE
47
+ let current = FORM_MAX_TEXT_SIZE
48
+ let step = 0.5pt
49
+
50
+ while current >= FORM_MIN_TEXT_SIZE {
51
+ let m = if shrink {
52
+ // Measure as a single line (no width constraint → no wrapping)
53
+ measure(text(size: current, display))
54
+ } else {
55
+ // Measure with wrapping within the available width
56
+ measure(block(width: avail-w, text(size: current, display)))
57
+ }
58
+ let char-m = measure(text(size: current, "0" * FORM_MIN_CHARS_PER_LINE))
59
+
60
+ if m.width <= avail-w and m.height <= avail-h and char-m.width <= avail-w {
61
+ final-size = current
62
+ break
63
+ }
64
+ current = current - step
65
+ }
66
+
67
+ // Fallback: if shrink-to-fit hit min size and still overflows,
68
+ // re-try with word-wrap enabled as a last resort.
69
+ if shrink and current < FORM_MIN_TEXT_SIZE {
70
+ current = FORM_MAX_TEXT_SIZE
71
+ while current >= FORM_MIN_TEXT_SIZE {
72
+ let m = measure(block(width: avail-w, text(size: current, display)))
73
+ let char-m = measure(text(size: current, "0" * FORM_MIN_CHARS_PER_LINE))
74
+ if m.height <= avail-h and char-m.width <= avail-w {
75
+ final-size = current
76
+ break
77
+ }
78
+ current = current - step
79
+ }
80
+ }
81
+
82
+ // Default to vertically centered. Only top-align for tall "text areas".
83
+ let vert-align = if height >= 40pt { top } else { horizon }
84
+
85
+ box(
86
+ width: width,
87
+ height: height,
88
+ clip: true,
89
+ inset: (x: x-inset, y: y-inset),
90
+ align(left + vert-align, text(size: final-size, display)),
91
+ )
92
+ }
93
+ }
94
+
95
+ /// Render a single field's content overlay.
96
+ ///
97
+ /// - field-type (str): normalised lowercase type
98
+ /// - value: user-supplied value for this field (or none)
99
+ /// - width (length): field width
100
+ /// - height (length): field height
101
+ /// - field (dictionary): raw field entry from the schema
102
+ #let render-field(field-type, value, width, height, field) = {
103
+ if field-type == "text" {
104
+ if value != none {
105
+ // Pass content through directly so styling (bold, italic, …) is preserved;
106
+ // for plain strings, skip empty values.
107
+ if type(value) == content {
108
+ render-text-field(value, width, height, 1.5pt, 1pt)
109
+ } else if str(value) != "" {
110
+ render-text-field(str(value), width, height, 1.5pt, 1pt)
111
+ }
112
+ }
113
+ } else if field-type == "checkbox" {
114
+ if value == true {
115
+ box(
116
+ width: width,
117
+ height: height,
118
+ align(center + horizon, text(size: height * 0.8, "✓")),
119
+ )
120
+ }
121
+ } else if field-type == "radio" {
122
+ // value is true when this specific button is the selected one
123
+ // (resolved at group level before calling this helper)
124
+ if value == true {
125
+ let dot-r = calc.min(width, height) * 0.3
126
+ box(
127
+ width: width,
128
+ height: height,
129
+ align(center + horizon, circle(radius: dot-r, fill: black)),
130
+ )
131
+ }
132
+ } else if field-type == "combobox" or field-type == "listbox" {
133
+ // Default: render the raw value, preserving content styling if present.
134
+ // Override with the schema's display label when the export value matches.
135
+ let display = value
136
+ if field.at("options", default: none) != none and value != none {
137
+ for opt in field.options {
138
+ if str(opt.at(0)) == to-str(value) {
139
+ display = str(opt.at(1))
140
+ }
141
+ }
142
+ }
143
+ if display != none and to-str(display) != "" {
144
+ render-text-field(display, width, height, 2pt, 1pt)
145
+ }
146
+ }
147
+ }
148
+
149
+ /// Determine whether a single radio button should appear selected.
150
+ ///
151
+ /// Strategy:
152
+ /// 1. If the field carries an `export_value` key, match against it.
153
+ /// 2. Otherwise fall back to matching the 0-based index within the group
154
+ /// (stringified) against the supplied value.
155
+ #let radio-button-selected(field, group-value, index-in-group) = {
156
+ if group-value == none { return false }
157
+ let ev = field.at("export_value", default: none)
158
+ if ev != none {
159
+ return str(ev) == to-str(group-value)
160
+ }
161
+ // Fallback: match against the stringified index
162
+ str(index-in-group) == to-str(group-value)
163
+ }
164
+
165
+ /// Draw a debug overlay rectangle with a label for a field.
166
+ ///
167
+ /// - field-type (str): normalised lowercase type
168
+ /// - name (str): field name
169
+ /// - width (length): field width
170
+ /// - height (length): field height
171
+ #let debug-overlay(field-type, name, width, height) = {
172
+ let color = if field-type == "text" {
173
+ rgb(0, 0, 255, 40%)
174
+ } else if field-type == "checkbox" {
175
+ rgb(0, 128, 0, 40%)
176
+ } else if field-type == "radio" {
177
+ rgb(255, 165, 0, 40%)
178
+ } else if field-type == "combobox" {
179
+ rgb(128, 0, 128, 40%)
180
+ } else if field-type == "listbox" {
181
+ rgb(0, 128, 128, 40%)
182
+ } else {
183
+ rgb(128, 128, 128, 40%)
184
+ }
185
+
186
+ let stroke-color = if field-type == "text" {
187
+ rgb(0, 0, 255)
188
+ } else if field-type == "checkbox" {
189
+ rgb(0, 128, 0)
190
+ } else if field-type == "radio" {
191
+ rgb(255, 165, 0)
192
+ } else if field-type == "combobox" {
193
+ rgb(128, 0, 128)
194
+ } else if field-type == "listbox" {
195
+ rgb(0, 128, 128)
196
+ } else {
197
+ rgb(128, 128, 128)
198
+ }
199
+
200
+ // Insert zero-width spaces after _ and - so the label can wrap
201
+ let breakable-name = name.replace("_", "_\u{200B}").replace("-", "-\u{200B}")
202
+ let label = breakable-name + " [" + field-type + "]"
203
+
204
+ box(width: width, height: height, {
205
+ // Semi-transparent colored background
206
+ rect(width: 100%, height: 100%, fill: color, stroke: 0.5pt + stroke-color)
207
+ // Label in top-left — uses block so text wraps within field width
208
+ place(
209
+ top + left,
210
+ dx: 1pt,
211
+ dy: 1pt,
212
+ block(
213
+ width: calc.max(width - 2pt, 10pt),
214
+ fill: white,
215
+ inset: (x: 2pt, y: 1pt),
216
+ radius: 2pt,
217
+ stroke: 0.3pt + stroke-color,
218
+ breakable: false,
219
+ text(size: 5pt, fill: stroke-color, weight: "bold", label),
220
+ ),
221
+ )
222
+ })
223
+ }
224
+
225
+ /// Main entry point.
226
+ ///
227
+ /// - schema (dictionary): result of `json("FIELDS.json")`
228
+ /// - backgrounds (array): list of image paths / bytes, one per page
229
+ /// - values (dictionary): field name → value; omit to render blank
230
+ /// - debug (bool): when true, draw colored overlays on each field
231
+ #let render-form(schema: none, backgrounds: (), values: (:), debug: false) = {
232
+ assert(schema != none, message: "render-form: `schema` is required")
233
+ assert(backgrounds.len() > 0, message: "render-form: `backgrounds` is required")
234
+ let pages = schema.pages
235
+ let fields = schema.fields
236
+
237
+ // Track how many radio buttons we have seen per group so far
238
+ let radio-counters = (:)
239
+
240
+ for (i, page-info) in pages.enumerate() {
241
+ let page-num = i + 1
242
+ let page-fields = fields.filter(f => f.page == page-num)
243
+
244
+ let bg = backgrounds.at(i)
245
+ let pw = page-info.width * 1pt
246
+ let ph = page-info.height * 1pt
247
+
248
+ page(
249
+ width: pw,
250
+ height: ph,
251
+ margin: 0pt,
252
+ )[
253
+ // Background image (the original PDF page rasterised as PNG)
254
+ #place(top + left, image(bg, width: pw, height: ph, fit: "stretch"))
255
+
256
+ // Field overlays
257
+ #for field in page-fields.filter(f => ("text", "checkbox", "radio", "combobox", "listbox").contains(lower(
258
+ f.type,
259
+ ))) {
260
+ let x = field.bbox.at(0) * 1pt
261
+ let y = field.bbox.at(1) * 1pt
262
+ let w = (field.bbox.at(2) - field.bbox.at(0)) * 1pt
263
+ let h = (field.bbox.at(3) - field.bbox.at(1)) * 1pt
264
+
265
+ let field-type = lower(field.type)
266
+ let val = values.at(field.name, default: none)
267
+
268
+ // --- Radio: resolve group-level value to per-button boolean ---
269
+ if field-type == "radio" {
270
+ let group-name = field.name
271
+ let idx = radio-counters.at(group-name, default: 0)
272
+ radio-counters.insert(group-name, idx + 1)
273
+ val = radio-button-selected(field, val, idx)
274
+ }
275
+
276
+ place(
277
+ top + left,
278
+ dx: x,
279
+ dy: y,
280
+ render-field(field-type, val, w, h, field),
281
+ )
282
+
283
+ if debug {
284
+ place(
285
+ top + left,
286
+ dx: x,
287
+ dy: y,
288
+ debug-overlay(field-type, field.name, w, h),
289
+ )
290
+ }
291
+
292
+ // Zero-width fence to break PDF viewer text-selection grouping
293
+ place(top + left, dx: x, dy: y, text(size: 0.001pt, "\u{FEFF}"))
294
+ }
295
+ ]
296
+ }
297
+ }