@tonguetoquill/collection 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonguetoquill/collection",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/nibsbin/tonguetoquill-collection.git"
@@ -0,0 +1,131 @@
1
+ Quill:
2
+ name: daf4392
3
+ version: 0.1.0
4
+ backend: typst
5
+ plate_file: plate.typ
6
+ ui:
7
+ hide_body: true
8
+ example_file: example.md
9
+ description: "DAF Form 4392 - Pre-Departure Safety Briefing (Page 2)"
10
+
11
+ fields:
12
+ transportation_mode:
13
+ title: Mode of Transportation
14
+ type: string
15
+ enum: [pmv, airplane, bus, train, motorcycle, other]
16
+ required: true
17
+ description: "Matches the check box."
18
+
19
+ departure_date:
20
+ title: Departure Date
21
+ type: string
22
+ required: true
23
+ ui:
24
+ compact: true
25
+ description: "Date of initial departure."
26
+
27
+ final_destination:
28
+ title: Final Destination
29
+ type: string
30
+ required: true
31
+ ui:
32
+ compact: true
33
+ description: "Final travel destination."
34
+
35
+ notes:
36
+ title: Notes
37
+ type: string
38
+ description: "Additional notes or remarks for the travel."
39
+
40
+ organization:
41
+ title: Organization
42
+ type: string
43
+ required: true
44
+ ui:
45
+ compact: true
46
+ description: "Unit / Organization conducting the brief."
47
+
48
+ briefed_date:
49
+ title: Date Briefed
50
+ type: string
51
+ required: true
52
+ ui:
53
+ compact: true
54
+ description: "Date the brief took place."
55
+
56
+ briefee_name:
57
+ title: Individual Receiving Brief (Name)
58
+ type: string
59
+ required: true
60
+ ui:
61
+ compact: true
62
+ description: "Name (Last, First) of the individual receiving the brief."
63
+
64
+ briefee_grade:
65
+ title: Individual Receiving Brief (Grade)
66
+ type: string
67
+ ui:
68
+ compact: true
69
+ description: "Grade of the individual receiving the brief."
70
+
71
+ briefer_name:
72
+ title: Briefer / Reviewer Name
73
+ type: string
74
+ ui:
75
+ compact: true
76
+ description: "Name (Last, First) of the briefer."
77
+
78
+ briefer_grade:
79
+ title: Briefer / Reviewer Grade
80
+ type: string
81
+ ui:
82
+ compact: true
83
+ description: "Grade of the briefer."
84
+
85
+ emergency_contact_name:
86
+ title: Emergency Contact Name
87
+ type: string
88
+ description: "Name of the emergency contact person at the bottom of the form."
89
+
90
+ emergency_contact_phone:
91
+ title: Emergency Contact Phone
92
+ type: string
93
+ description: "Phone number of the emergency contact person at the bottom of the form."
94
+
95
+ cards:
96
+ itinerary:
97
+ title: Proposed Travel Itinerary
98
+ description: "Each card represents one row in the travel itinerary table. Up to 5 rows are supported."
99
+ ui:
100
+ hide_body: true
101
+ fields:
102
+ date:
103
+ title: Date
104
+ type: date
105
+ ui:
106
+ group: Itinerary Entry
107
+ description: "Date for the itinerary row."
108
+ departure_point:
109
+ title: Departure Point
110
+ type: string
111
+ ui:
112
+ group: Itinerary Entry
113
+ description: "Point of departure."
114
+ arrival_point:
115
+ title: Arrival Point
116
+ type: string
117
+ ui:
118
+ group: Itinerary Entry
119
+ description: "Point of arrival."
120
+ rest_length:
121
+ title: Length of Rest Period
122
+ type: string
123
+ ui:
124
+ group: Itinerary Entry
125
+ description: "Duration of rest period."
126
+ mileage:
127
+ title: Approximate Mileage
128
+ type: string
129
+ ui:
130
+ group: Itinerary Entry
131
+ description: "Approximate mileage for this leg."
@@ -0,0 +1,33 @@
1
+ ---
2
+ QUILL: daf4392
3
+
4
+ transportation_mode: "other"
5
+ departure_date: "XX Mar 26"
6
+ final_destination: "123 Sesame St, Fake City, MS"
7
+ notes: "Visiting Donald Duck over 3 day weekend."
8
+ organization: "333 TRS"
9
+ briefed_date: "XX Mar 26"
10
+ briefee_name: "Doe, Jane"
11
+ briefee_grade: "O-1"
12
+ briefer_name: ""
13
+ briefer_grade: ""
14
+ emergency_contact: "John Doe: 999-999-9999"
15
+ ---
16
+
17
+ ---
18
+ CARD: itinerary
19
+ date: "XX Mar"
20
+ departure_point: "Biloxi, MS"
21
+ arrival_point: "Fake City, MS"
22
+ rest_length: "2 days"
23
+ mileage: "250"
24
+ ---
25
+
26
+ ---
27
+ CARD: itinerary
28
+ date: "XX Mar"
29
+ departure_point: "Fake City, MS"
30
+ arrival_point: "Biloxi, MS"
31
+ rest_length: "0"
32
+ mileage: "0"
33
+ ---
@@ -0,0 +1,9 @@
1
+ {
2
+ "pages": [
3
+ {
4
+ "width": 612.0,
5
+ "height": 792.0
6
+ }
7
+ ],
8
+ "fields": []
9
+ }
@@ -0,0 +1,14 @@
1
+ // form.typ (generated � do not edit)
2
+ #import "lib.typ": render-form
3
+
4
+ #let form(
5
+ debug: false,
6
+
7
+ ) = render-form(
8
+ schema: json("FIELDS.json"),
9
+ backgrounds: ("page1.png",),
10
+ values: (
11
+ ,
12
+ ),
13
+ debug: debug,
14
+ )
@@ -0,0 +1,227 @@
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
+ }
@@ -0,0 +1,4 @@
1
+ // debug.typ (generated � renders form with debug overlays)
2
+ #import "../form.typ": form
3
+
4
+ #form(debug: true)
@@ -0,0 +1,4 @@
1
+ // example.typ (edit this file to fill the form)
2
+ #import "../form.typ": form
3
+
4
+ #form()
@@ -0,0 +1,7 @@
1
+ [package]
2
+ name = "daf4392page2-pkg"
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"
@@ -0,0 +1,60 @@
1
+ #import "@local/quillmark-helper:0.1.0": data
2
+
3
+ #set page(width: 8.5in, height: 11in, margin: 0in)
4
+ #set text(font: "Arimo", size: 10pt)
5
+
6
+ // Background
7
+ #place(image("assets/page1.png", width: 100%, height: 100%))
8
+
9
+ #let tf(dx, dy, content) = place(dx: dx, dy: dy, content)
10
+ #let check(dx, dy, is_checked) = if is_checked { place(dx: dx, dy: dy, text(weight: "bold", size: 12pt)[✓]) }
11
+
12
+ // Mode of Transportation
13
+ #let mode = data.at("transportation_mode", default: "")
14
+ #check(40pt, 67pt, mode == "pmv")
15
+ #check(173pt, 67pt, mode == "airplane")
16
+ #check(243pt, 67pt, mode == "bus")
17
+ #check(286pt, 67pt, mode == "train")
18
+ #check(336pt, 67pt, mode == "motorcycle")
19
+ #check(411pt, 67pt, mode == "other")
20
+
21
+ // Fields
22
+ #tf(40pt, 100pt)[#data.at("departure_date", default: "")]
23
+ #tf(120pt, 100pt)[#data.at("final_destination", default: "")]
24
+
25
+ // Itinerary Rows (via CARDS)
26
+ #{
27
+ let row = 0
28
+ let dy-start = 160pt
29
+ let dy-step = 40pt
30
+ if "CARDS" in data {
31
+ for card in data.CARDS {
32
+ if card.CARD == "itinerary" and row < 5 {
33
+ let dy = dy-start + (row * dy-step)
34
+ tf(80pt, dy)[#card.at("date", default: "")]
35
+ tf(135pt, dy)[#card.at("departure_point", default: "")]
36
+ tf(300pt, dy)[#card.at("arrival_point", default: "")]
37
+ tf(450pt, dy)[#card.at("rest_length", default: "")]
38
+ tf(515pt, dy)[#card.at("mileage", default: "")]
39
+ row = row + 1
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ // Notes
46
+ #tf(40pt, 360pt)[#block(width: 530pt)[#data.at("notes", default: "")]]
47
+
48
+ // Acknowledgements
49
+ #tf(40pt, 420pt)[#data.at("organization", default: "")]
50
+ #tf(500pt, 420pt)[#data.at("briefed_date", default: "")]
51
+
52
+ #tf(40pt, 450pt)[#data.at("briefee_name", default: "")]
53
+ #tf(270pt, 450pt)[#data.at("briefee_grade", default: "")]
54
+
55
+ #tf(40pt, 480pt)[#data.at("briefer_name", default: "")]
56
+ #tf(270pt, 480pt)[#data.at("briefer_grade", default: "")]
57
+
58
+ // Emergency Contact
59
+ #tf(40pt, 500pt)[#text(size: 16pt, weight: "bold")[EMERGENCY CONTACT]]
60
+ #tf(40pt, 520pt)[#text(size: 12pt)[#data.at("emergency_contact_name", default: ""): #data.at("emergency_contact_phone", default: "")]]
@@ -0,0 +1,33 @@
1
+ ---
2
+ QUILL: daf4392
3
+
4
+ transportation_mode: "other"
5
+ departure_date: "XX Mar 26"
6
+ final_destination: "123 Sesame St, Fake City, MS"
7
+ notes: "Visiting Donald Duck over 3 day weekend."
8
+ organization: "333 TRS"
9
+ briefed_date: "XX Mar 26"
10
+ briefee_name: "Doe, Jane"
11
+ briefee_grade: "O-1"
12
+ briefer_name: ""
13
+ briefer_grade: ""
14
+ emergency_contact: "John Doe: 999-999-9999"
15
+ ---
16
+
17
+ ---
18
+ CARD: itinerary
19
+ date: "XX Mar"
20
+ departure_point: "Biloxi, MS"
21
+ arrival_point: "Fake City, MS"
22
+ rest_length: "2 days"
23
+ mileage: "250"
24
+ ---
25
+
26
+ ---
27
+ CARD: itinerary
28
+ date: "XX Mar"
29
+ departure_point: "Fake City, MS"
30
+ arrival_point: "Biloxi, MS"
31
+ rest_length: "0"
32
+ mileage: "0"
33
+ ---
@@ -46,5 +46,11 @@
46
46
  "description": "Individual's Record of Duties and Experience, Ground Environment Personnel",
47
47
  "file": "af4141.md",
48
48
  "production": true
49
+ },
50
+ {
51
+ "name": "DAF Form 4392",
52
+ "description": "Pre-Departure Safety Briefing (Page 2)",
53
+ "file": "daf4392.md",
54
+ "production": true
49
55
  }
50
56
  ]