@airmark/quiver 0.19.0 → 0.20.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/package.json +1 -1
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/FIELDS.json +819 -2
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/form.typ +137 -3
- package/quills/daf4392/0.1.0/packages/daf4392page2_pkg/lib.typ +146 -37
- package/quills/daf4392/0.1.0/plate.typ +40 -63
- package/quills/usaf_memo/0.2.0/Quill.yaml +11 -2
- package/quills/usaf_memo/0.2.0/assets/freedom250.png +0 -0
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +3 -2
- package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +14 -2
- package/quills/usaf_memo/0.2.0/plate.typ +7 -2
- /package/quills/daf4392/0.1.0/{assets → packages/daf4392page2_pkg}/page1.png +0 -0
|
@@ -1,14 +1,148 @@
|
|
|
1
|
-
// form.typ (generated
|
|
1
|
+
// form.typ (generated — do not edit)
|
|
2
2
|
#import "lib.typ": render-form
|
|
3
3
|
|
|
4
4
|
#let form(
|
|
5
5
|
debug: false,
|
|
6
|
-
|
|
6
|
+
mode_pmv: false, // checkbox
|
|
7
|
+
mode_airplane: false, // checkbox
|
|
8
|
+
mode_bus: false, // checkbox
|
|
9
|
+
mode_train: false, // checkbox
|
|
10
|
+
mode_motorcycle: false, // checkbox
|
|
11
|
+
mode_other: false, // checkbox
|
|
12
|
+
departure_date: "", // text
|
|
13
|
+
final_destination: "", // text
|
|
14
|
+
itin_date_1: "", // text
|
|
15
|
+
itin_departure_point_1: "", // text
|
|
16
|
+
itin_arrival_point_1: "", // text
|
|
17
|
+
itin_rest_length_1: "", // text
|
|
18
|
+
itin_mileage_1: "", // text
|
|
19
|
+
itin_date_2: "", // text
|
|
20
|
+
itin_departure_point_2: "", // text
|
|
21
|
+
itin_arrival_point_2: "", // text
|
|
22
|
+
itin_rest_length_2: "", // text
|
|
23
|
+
itin_mileage_2: "", // text
|
|
24
|
+
itin_date_3: "", // text
|
|
25
|
+
itin_departure_point_3: "", // text
|
|
26
|
+
itin_arrival_point_3: "", // text
|
|
27
|
+
itin_rest_length_3: "", // text
|
|
28
|
+
itin_mileage_3: "", // text
|
|
29
|
+
itin_date_4: "", // text
|
|
30
|
+
itin_departure_point_4: "", // text
|
|
31
|
+
itin_arrival_point_4: "", // text
|
|
32
|
+
itin_rest_length_4: "", // text
|
|
33
|
+
itin_mileage_4: "", // text
|
|
34
|
+
itin_date_5: "", // text
|
|
35
|
+
itin_departure_point_5: "", // text
|
|
36
|
+
itin_arrival_point_5: "", // text
|
|
37
|
+
itin_rest_length_5: "", // text
|
|
38
|
+
itin_mileage_5: "", // text
|
|
39
|
+
itin_date_6: "", // text
|
|
40
|
+
itin_departure_point_6: "", // text
|
|
41
|
+
itin_arrival_point_6: "", // text
|
|
42
|
+
itin_rest_length_6: "", // text
|
|
43
|
+
itin_mileage_6: "", // text
|
|
44
|
+
itin_date_7: "", // text
|
|
45
|
+
itin_departure_point_7: "", // text
|
|
46
|
+
itin_arrival_point_7: "", // text
|
|
47
|
+
itin_rest_length_7: "", // text
|
|
48
|
+
itin_mileage_7: "", // text
|
|
49
|
+
itin_date_8: "", // text
|
|
50
|
+
itin_departure_point_8: "", // text
|
|
51
|
+
itin_arrival_point_8: "", // text
|
|
52
|
+
itin_rest_length_8: "", // text
|
|
53
|
+
itin_mileage_8: "", // text
|
|
54
|
+
itin_date_9: "", // text
|
|
55
|
+
itin_departure_point_9: "", // text
|
|
56
|
+
itin_arrival_point_9: "", // text
|
|
57
|
+
itin_rest_length_9: "", // text
|
|
58
|
+
itin_mileage_9: "", // text
|
|
59
|
+
itin_date_10: "", // text
|
|
60
|
+
itin_departure_point_10: "", // text
|
|
61
|
+
itin_arrival_point_10: "", // text
|
|
62
|
+
itin_rest_length_10: "", // text
|
|
63
|
+
itin_mileage_10: "", // text
|
|
64
|
+
dept_flight: "", // text
|
|
65
|
+
arrival_flight: "", // text
|
|
66
|
+
notes: "", // text
|
|
67
|
+
organization: "", // text
|
|
68
|
+
briefed_date: "", // text
|
|
69
|
+
briefee_name: "", // text
|
|
70
|
+
briefee_grade: "", // text
|
|
71
|
+
briefer_name: "", // text
|
|
72
|
+
briefer_grade: "", // text
|
|
73
|
+
emergency_contact: "", // text
|
|
7
74
|
) = render-form(
|
|
8
75
|
schema: json("FIELDS.json"),
|
|
9
76
|
backgrounds: ("page1.png",),
|
|
10
77
|
values: (
|
|
11
|
-
,
|
|
78
|
+
"mode_pmv": mode_pmv,
|
|
79
|
+
"mode_airplane": mode_airplane,
|
|
80
|
+
"mode_bus": mode_bus,
|
|
81
|
+
"mode_train": mode_train,
|
|
82
|
+
"mode_motorcycle": mode_motorcycle,
|
|
83
|
+
"mode_other": mode_other,
|
|
84
|
+
"departure_date": departure_date,
|
|
85
|
+
"final_destination": final_destination,
|
|
86
|
+
"itin_date_1": itin_date_1,
|
|
87
|
+
"itin_departure_point_1": itin_departure_point_1,
|
|
88
|
+
"itin_arrival_point_1": itin_arrival_point_1,
|
|
89
|
+
"itin_rest_length_1": itin_rest_length_1,
|
|
90
|
+
"itin_mileage_1": itin_mileage_1,
|
|
91
|
+
"itin_date_2": itin_date_2,
|
|
92
|
+
"itin_departure_point_2": itin_departure_point_2,
|
|
93
|
+
"itin_arrival_point_2": itin_arrival_point_2,
|
|
94
|
+
"itin_rest_length_2": itin_rest_length_2,
|
|
95
|
+
"itin_mileage_2": itin_mileage_2,
|
|
96
|
+
"itin_date_3": itin_date_3,
|
|
97
|
+
"itin_departure_point_3": itin_departure_point_3,
|
|
98
|
+
"itin_arrival_point_3": itin_arrival_point_3,
|
|
99
|
+
"itin_rest_length_3": itin_rest_length_3,
|
|
100
|
+
"itin_mileage_3": itin_mileage_3,
|
|
101
|
+
"itin_date_4": itin_date_4,
|
|
102
|
+
"itin_departure_point_4": itin_departure_point_4,
|
|
103
|
+
"itin_arrival_point_4": itin_arrival_point_4,
|
|
104
|
+
"itin_rest_length_4": itin_rest_length_4,
|
|
105
|
+
"itin_mileage_4": itin_mileage_4,
|
|
106
|
+
"itin_date_5": itin_date_5,
|
|
107
|
+
"itin_departure_point_5": itin_departure_point_5,
|
|
108
|
+
"itin_arrival_point_5": itin_arrival_point_5,
|
|
109
|
+
"itin_rest_length_5": itin_rest_length_5,
|
|
110
|
+
"itin_mileage_5": itin_mileage_5,
|
|
111
|
+
"itin_date_6": itin_date_6,
|
|
112
|
+
"itin_departure_point_6": itin_departure_point_6,
|
|
113
|
+
"itin_arrival_point_6": itin_arrival_point_6,
|
|
114
|
+
"itin_rest_length_6": itin_rest_length_6,
|
|
115
|
+
"itin_mileage_6": itin_mileage_6,
|
|
116
|
+
"itin_date_7": itin_date_7,
|
|
117
|
+
"itin_departure_point_7": itin_departure_point_7,
|
|
118
|
+
"itin_arrival_point_7": itin_arrival_point_7,
|
|
119
|
+
"itin_rest_length_7": itin_rest_length_7,
|
|
120
|
+
"itin_mileage_7": itin_mileage_7,
|
|
121
|
+
"itin_date_8": itin_date_8,
|
|
122
|
+
"itin_departure_point_8": itin_departure_point_8,
|
|
123
|
+
"itin_arrival_point_8": itin_arrival_point_8,
|
|
124
|
+
"itin_rest_length_8": itin_rest_length_8,
|
|
125
|
+
"itin_mileage_8": itin_mileage_8,
|
|
126
|
+
"itin_date_9": itin_date_9,
|
|
127
|
+
"itin_departure_point_9": itin_departure_point_9,
|
|
128
|
+
"itin_arrival_point_9": itin_arrival_point_9,
|
|
129
|
+
"itin_rest_length_9": itin_rest_length_9,
|
|
130
|
+
"itin_mileage_9": itin_mileage_9,
|
|
131
|
+
"itin_date_10": itin_date_10,
|
|
132
|
+
"itin_departure_point_10": itin_departure_point_10,
|
|
133
|
+
"itin_arrival_point_10": itin_arrival_point_10,
|
|
134
|
+
"itin_rest_length_10": itin_rest_length_10,
|
|
135
|
+
"itin_mileage_10": itin_mileage_10,
|
|
136
|
+
"dept_flight": dept_flight,
|
|
137
|
+
"arrival_flight": arrival_flight,
|
|
138
|
+
"notes": notes,
|
|
139
|
+
"organization": organization,
|
|
140
|
+
"briefed_date": briefed_date,
|
|
141
|
+
"briefee_name": briefee_name,
|
|
142
|
+
"briefee_grade": briefee_grade,
|
|
143
|
+
"briefer_name": briefer_name,
|
|
144
|
+
"briefer_grade": briefer_grade,
|
|
145
|
+
"emergency_contact": emergency_contact,
|
|
12
146
|
),
|
|
13
147
|
debug: debug,
|
|
14
148
|
)
|
|
@@ -1,6 +1,115 @@
|
|
|
1
1
|
// Formalizer Engine – rendering engine
|
|
2
2
|
// Renders pixel-perfect PDF form replicas from a PyMuPDF-extracted schema.
|
|
3
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
|
+
/// True when a schema field name corresponds to a date overlay.
|
|
29
|
+
#let is-date-field(name) = {
|
|
30
|
+
name.ends-with("_date") or name.starts-with("itin_date_")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Format a date value for display as `DD Mon YYYY`.
|
|
34
|
+
#let format-date(value) = {
|
|
35
|
+
if value == none {
|
|
36
|
+
none
|
|
37
|
+
} else if type(value) == datetime {
|
|
38
|
+
value.display("[day padding:zero] [month repr:short] [year]")
|
|
39
|
+
} else if type(value) == str and value == "" {
|
|
40
|
+
""
|
|
41
|
+
} else {
|
|
42
|
+
str(value)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Should this field shrink text to a single line rather than word-wrap?
|
|
47
|
+
/// True for short/narrow fields with brief content (grades, ranks, dates).
|
|
48
|
+
/// `display` may be a string or content; char-count is skipped for content.
|
|
49
|
+
#let should-shrink-to-fit(display, width, height) = {
|
|
50
|
+
let aspect = width / height
|
|
51
|
+
// content has no .len(); treat as "long" so only the dimension conditions apply
|
|
52
|
+
let char-count = if type(display) == str { display.len() } else { 99 }
|
|
53
|
+
char-count <= 10 or height < 20pt or aspect > 4.0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Render a text-like field with shrink-to-fit and word-wrap fallback.
|
|
57
|
+
#let render-text-field(display, width, height, x-inset, y-inset) = {
|
|
58
|
+
set par(leading: 0.25em)
|
|
59
|
+
context {
|
|
60
|
+
let avail-w = width - 2 * x-inset
|
|
61
|
+
let avail-h = height - 2 * y-inset
|
|
62
|
+
let shrink = should-shrink-to-fit(display, width, height)
|
|
63
|
+
|
|
64
|
+
let final-size = FORM_MIN_TEXT_SIZE
|
|
65
|
+
let current = FORM_MAX_TEXT_SIZE
|
|
66
|
+
let step = 0.5pt
|
|
67
|
+
|
|
68
|
+
while current >= FORM_MIN_TEXT_SIZE {
|
|
69
|
+
let m = if shrink {
|
|
70
|
+
// Measure as a single line (no width constraint → no wrapping)
|
|
71
|
+
measure(text(size: current, display))
|
|
72
|
+
} else {
|
|
73
|
+
// Measure with wrapping within the available width
|
|
74
|
+
measure(block(width: avail-w, text(size: current, display)))
|
|
75
|
+
}
|
|
76
|
+
let char-m = measure(text(size: current, "0" * FORM_MIN_CHARS_PER_LINE))
|
|
77
|
+
|
|
78
|
+
if m.width <= avail-w and m.height <= avail-h and char-m.width <= avail-w {
|
|
79
|
+
final-size = current
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
current = current - step
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Fallback: if shrink-to-fit hit min size and still overflows,
|
|
86
|
+
// re-try with word-wrap enabled as a last resort.
|
|
87
|
+
if shrink and current < FORM_MIN_TEXT_SIZE {
|
|
88
|
+
current = FORM_MAX_TEXT_SIZE
|
|
89
|
+
while current >= FORM_MIN_TEXT_SIZE {
|
|
90
|
+
let m = measure(block(width: avail-w, text(size: current, display)))
|
|
91
|
+
let char-m = measure(text(size: current, "0" * FORM_MIN_CHARS_PER_LINE))
|
|
92
|
+
if m.height <= avail-h and char-m.width <= avail-w {
|
|
93
|
+
final-size = current
|
|
94
|
+
break
|
|
95
|
+
}
|
|
96
|
+
current = current - step
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Default to vertically centered. Only top-align for tall "text areas".
|
|
101
|
+
let vert-align = if height >= 40pt { top } else { horizon }
|
|
102
|
+
|
|
103
|
+
box(
|
|
104
|
+
width: width,
|
|
105
|
+
height: height,
|
|
106
|
+
clip: true,
|
|
107
|
+
inset: (x: x-inset, y: y-inset),
|
|
108
|
+
align(left + vert-align, text(size: final-size, display)),
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
4
113
|
/// Render a single field's content overlay.
|
|
5
114
|
///
|
|
6
115
|
/// - field-type (str): normalised lowercase type
|
|
@@ -10,29 +119,42 @@
|
|
|
10
119
|
/// - field (dictionary): raw field entry from the schema
|
|
11
120
|
#let render-field(field-type, value, width, height, field) = {
|
|
12
121
|
if field-type == "text" {
|
|
13
|
-
if value != none
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
)
|
|
122
|
+
if value != none {
|
|
123
|
+
let display = if is-date-field(field.name) { format-date(value) } else { value }
|
|
124
|
+
// Pass content through directly so styling (bold, italic, …) is preserved;
|
|
125
|
+
// for plain strings, skip empty values.
|
|
126
|
+
if type(display) == content {
|
|
127
|
+
render-text-field(display, width, height, 1.5pt, 1pt)
|
|
128
|
+
} else if str(display) != "" {
|
|
129
|
+
render-text-field(str(display), width, height, 1.5pt, 1pt)
|
|
28
130
|
}
|
|
29
131
|
}
|
|
30
132
|
} else if field-type == "checkbox" {
|
|
31
133
|
if value == true {
|
|
134
|
+
// Drawn as vector strokes rather than a glyph so the mark renders
|
|
135
|
+
// regardless of whether the active font carries U+2713.
|
|
136
|
+
let s = calc.min(width, height) * 0.78
|
|
137
|
+
let t = calc.max(s * 0.16, 1pt)
|
|
138
|
+
let mark-stroke = (paint: black, thickness: t, cap: "round", join: "round")
|
|
32
139
|
box(
|
|
33
140
|
width: width,
|
|
34
141
|
height: height,
|
|
35
|
-
align(center + horizon,
|
|
142
|
+
align(center + horizon, box(
|
|
143
|
+
width: s,
|
|
144
|
+
height: s,
|
|
145
|
+
{
|
|
146
|
+
place(line(
|
|
147
|
+
start: (0.12 * s, 0.52 * s),
|
|
148
|
+
end: (0.40 * s, 0.80 * s),
|
|
149
|
+
stroke: mark-stroke,
|
|
150
|
+
))
|
|
151
|
+
place(line(
|
|
152
|
+
start: (0.40 * s, 0.80 * s),
|
|
153
|
+
end: (0.88 * s, 0.16 * s),
|
|
154
|
+
stroke: mark-stroke,
|
|
155
|
+
))
|
|
156
|
+
},
|
|
157
|
+
)),
|
|
36
158
|
)
|
|
37
159
|
}
|
|
38
160
|
} else if field-type == "radio" {
|
|
@@ -47,31 +169,18 @@
|
|
|
47
169
|
)
|
|
48
170
|
}
|
|
49
171
|
} else if field-type == "combobox" or field-type == "listbox" {
|
|
50
|
-
|
|
51
|
-
//
|
|
172
|
+
// Default: render the raw value, preserving content styling if present.
|
|
173
|
+
// Override with the schema's display label when the export value matches.
|
|
174
|
+
let display = value
|
|
52
175
|
if field.at("options", default: none) != none and value != none {
|
|
53
176
|
for opt in field.options {
|
|
54
|
-
if str(opt.at(0)) == str(value) {
|
|
177
|
+
if str(opt.at(0)) == to-str(value) {
|
|
55
178
|
display = str(opt.at(1))
|
|
56
179
|
}
|
|
57
180
|
}
|
|
58
181
|
}
|
|
59
|
-
if display != "" {
|
|
60
|
-
|
|
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
|
-
}
|
|
182
|
+
if display != none and to-str(display) != "" {
|
|
183
|
+
render-text-field(display, width, height, 2pt, 1pt)
|
|
75
184
|
}
|
|
76
185
|
}
|
|
77
186
|
}
|
|
@@ -86,10 +195,10 @@
|
|
|
86
195
|
if group-value == none { return false }
|
|
87
196
|
let ev = field.at("export_value", default: none)
|
|
88
197
|
if ev != none {
|
|
89
|
-
return str(ev) == str(group-value)
|
|
198
|
+
return str(ev) == to-str(group-value)
|
|
90
199
|
}
|
|
91
200
|
// Fallback: match against the stringified index
|
|
92
|
-
str(index-in-group) == str(group-value)
|
|
201
|
+
str(index-in-group) == to-str(group-value)
|
|
93
202
|
}
|
|
94
203
|
|
|
95
204
|
/// Draw a debug overlay rectangle with a label for a field.
|
|
@@ -1,86 +1,63 @@
|
|
|
1
1
|
#import "@local/quillmark-helper:0.1.0": data
|
|
2
|
+
#import "@local/daf4392page2-pkg:0.1.0": form
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
#let show-date(v) = {
|
|
5
|
-
if v == none { "" }
|
|
6
|
-
else if type(v) == datetime { v.display("[month padding:none]/[day padding:none]/[year]") }
|
|
7
|
-
else { str(v) }
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
#set page(width: 8.5in, height: 11in, margin: 0in)
|
|
11
|
-
#set text(font: "Arimo", size: 10pt)
|
|
4
|
+
#set text(font: "Arimo")
|
|
12
5
|
|
|
13
|
-
//
|
|
14
|
-
#
|
|
6
|
+
// Map snake_case Quill `data` keys to generated `form.typ` parameter names.
|
|
7
|
+
#let vals = (:)
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
#let check(dx, dy, is_checked) = if is_checked { place(dx: dx, dy: dy, text(weight: "bold", size: 12pt)[✓]) }
|
|
18
|
-
|
|
19
|
-
// Mode of Transportation
|
|
9
|
+
// Mode of Transportation — enum string drives one of six checkboxes.
|
|
20
10
|
#let mode = data.at("transportation_mode", default: "")
|
|
21
|
-
#
|
|
22
|
-
#check(173pt, 67pt, mode == "airplane")
|
|
23
|
-
#check(243pt, 67pt, mode == "bus")
|
|
24
|
-
#check(286pt, 67pt, mode == "train")
|
|
25
|
-
#check(336pt, 67pt, mode == "motorcycle")
|
|
26
|
-
#check(411pt, 67pt, mode == "other")
|
|
11
|
+
#if mode != "" { vals.insert("mode_" + mode, true) }
|
|
27
12
|
|
|
28
|
-
//
|
|
29
|
-
#
|
|
30
|
-
#
|
|
13
|
+
// Header row
|
|
14
|
+
#if "departure_date" in data { vals.insert("departure_date", data.departure_date) }
|
|
15
|
+
#if "final_destination" in data { vals.insert("final_destination", data.final_destination) }
|
|
31
16
|
|
|
32
|
-
// Itinerary
|
|
17
|
+
// Proposed Travel Itinerary — one card per row, up to 10 rows.
|
|
18
|
+
#let itin-cols = ("date", "departure_point", "arrival_point", "rest_length", "mileage")
|
|
33
19
|
#{
|
|
34
20
|
let row = 0
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
let
|
|
41
|
-
|
|
42
|
-
tf(135pt, dy)[#card.at("departure_point", default: "")]
|
|
43
|
-
tf(295pt, dy)[#card.at("arrival_point", default: "")]
|
|
44
|
-
tf(450pt, dy)[#card.at("rest_length", default: "")]
|
|
45
|
-
tf(515pt, dy)[#card.at("mileage", default: "")]
|
|
46
|
-
row = row + 1
|
|
21
|
+
for card in data.at("CARDS", default: ()) {
|
|
22
|
+
if card.CARD == "itinerary" and row < 10 {
|
|
23
|
+
let n = str(row + 1)
|
|
24
|
+
for col in itin-cols {
|
|
25
|
+
let v = card.at(col, default: none)
|
|
26
|
+
let display = if col == "date" { v } else if v == none { "" } else { str(v) }
|
|
27
|
+
if display != none and display != "" { vals.insert("itin_" + col + "_" + n, display) }
|
|
47
28
|
}
|
|
29
|
+
row = row + 1
|
|
48
30
|
}
|
|
49
31
|
}
|
|
50
32
|
}
|
|
51
33
|
|
|
52
|
-
// Flight
|
|
34
|
+
// Flight info — bold label prefixes a tentative/confirmed flight number.
|
|
53
35
|
#{
|
|
54
|
-
let d
|
|
55
|
-
if d
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if a-flight != "" { tf(450pt, 545pt)[#text(weight: "bold")[Arr Flight:] #a-flight] }
|
|
36
|
+
let d = str(data.at("dept_flight_num", default: ""))
|
|
37
|
+
if d != "" { vals.insert("dept_flight", [#text(weight: "bold")[Dept Flight:] #d]) }
|
|
38
|
+
let a = str(data.at("arrival_flight_num", default: ""))
|
|
39
|
+
if a != "" { vals.insert("arrival_flight", [#text(weight: "bold")[Arr Flight:] #a]) }
|
|
59
40
|
}
|
|
60
41
|
|
|
61
42
|
// Notes
|
|
62
|
-
#
|
|
43
|
+
#if "notes" in data { vals.insert("notes", data.notes) }
|
|
63
44
|
|
|
64
45
|
// Acknowledgements
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// Emergency Contact
|
|
46
|
+
#if "organization" in data { vals.insert("organization", data.organization) }
|
|
47
|
+
#if "briefed_date" in data { vals.insert("briefed_date", data.briefed_date) }
|
|
48
|
+
#if "briefee_name" in data { vals.insert("briefee_name", data.briefee_name) }
|
|
49
|
+
#if "briefee_grade" in data { vals.insert("briefee_grade", data.briefee_grade) }
|
|
50
|
+
#if "briefer_name" in data { vals.insert("briefer_name", data.briefer_name) }
|
|
51
|
+
#if "briefer_grade" in data { vals.insert("briefer_grade", data.briefer_grade) }
|
|
52
|
+
|
|
53
|
+
// Emergency contact — bold label prefixes a "name: phone" pair.
|
|
75
54
|
#{
|
|
76
|
-
let
|
|
77
|
-
let
|
|
78
|
-
let
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
emergency_name
|
|
82
|
-
} else {
|
|
83
|
-
emergency_phone
|
|
55
|
+
let en = str(data.at("emergency_contact_name", default: ""))
|
|
56
|
+
let ep = str(data.at("emergency_contact_phone", default: ""))
|
|
57
|
+
let contact = if en != "" and ep != "" { en + ": " + ep } else if en != "" { en } else { ep }
|
|
58
|
+
if contact != "" {
|
|
59
|
+
vals.insert("emergency_contact", [#text(weight: "bold")[EMERGENCY CONTACT:] #contact])
|
|
84
60
|
}
|
|
85
|
-
tf(40pt, 700pt)[#text(size: 10pt, weight: "bold")[EMERGENCY CONTACT:] #text(size: 10pt)[#emergency_contact]]
|
|
86
61
|
}
|
|
62
|
+
|
|
63
|
+
#form(..vals)
|
|
@@ -24,7 +24,7 @@ main:
|
|
|
24
24
|
|
|
25
25
|
memo_from:
|
|
26
26
|
type: array
|
|
27
|
-
|
|
27
|
+
default: []
|
|
28
28
|
example:
|
|
29
29
|
- ORG/SYMBOL
|
|
30
30
|
- Organization Name
|
|
@@ -32,7 +32,7 @@ main:
|
|
|
32
32
|
- City ST 12345-6789
|
|
33
33
|
ui:
|
|
34
34
|
group: Addressing
|
|
35
|
-
description: If recipients are on the same installation, use only the office symbol. For recipients on other installations, include the full mailing address to enable return correspondence.
|
|
35
|
+
description: "If recipients are on the same installation, use only the office symbol. For recipients on other installations, include the full mailing address to enable return correspondence. Omit entirely to produce a Memorandum for Record (no MEMO FROM line)."
|
|
36
36
|
|
|
37
37
|
subject:
|
|
38
38
|
type: string
|
|
@@ -91,8 +91,17 @@ main:
|
|
|
91
91
|
default: ""
|
|
92
92
|
ui:
|
|
93
93
|
group: Letterhead
|
|
94
|
+
compact: true
|
|
94
95
|
description: Organizational motto at the bottom of the page.
|
|
95
96
|
|
|
97
|
+
freedom250:
|
|
98
|
+
type: boolean
|
|
99
|
+
default: true
|
|
100
|
+
ui:
|
|
101
|
+
group: Letterhead
|
|
102
|
+
compact: true
|
|
103
|
+
description: Show the America 250 / Freedom 250 commemorative emblem in the letterhead, mirrored opposite the seal.
|
|
104
|
+
|
|
96
105
|
references:
|
|
97
106
|
type: array
|
|
98
107
|
default: []
|
|
Binary file
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
letterhead_caption: "[YOUR SQUADRON/UNIT NAME]",
|
|
20
20
|
letterhead_seal: none,
|
|
21
21
|
letterhead_seal_subtitle: none, // optional line under seal (9pt bold caps); ignored if no seal
|
|
22
|
+
letterhead_emblem: none, // optional image placed opposite the seal (right side)
|
|
22
23
|
letterhead_font: DEFAULT_LETTERHEAD_FONTS,
|
|
23
24
|
body_font: DEFAULT_BODY_FONTS,
|
|
24
25
|
font_size: 12pt,
|
|
@@ -31,7 +32,6 @@
|
|
|
31
32
|
) = {
|
|
32
33
|
assert(subject != none, message: "subject is required")
|
|
33
34
|
assert(memo_for != none, message: "memo_for is required")
|
|
34
|
-
assert(memo_from != none, message: "memo_from is required")
|
|
35
35
|
assert(
|
|
36
36
|
memo_style in ("usaf", "daf"),
|
|
37
37
|
message: "memo_style must be \"usaf\" or \"daf\"",
|
|
@@ -123,6 +123,7 @@
|
|
|
123
123
|
letterhead_font,
|
|
124
124
|
letterhead-seal: letterhead_seal,
|
|
125
125
|
letterhead-seal-subtitle: letterhead_seal_subtitle,
|
|
126
|
+
letterhead-emblem: letterhead_emblem,
|
|
126
127
|
)
|
|
127
128
|
|
|
128
129
|
// AFH 33-337 "Date": "Place the date 1 inch from the right edge, 1.75 inches from the top"
|
|
@@ -147,7 +148,7 @@
|
|
|
147
148
|
|
|
148
149
|
render-date-section(actual_date, memo-style: memo_style)
|
|
149
150
|
render-for-section(memo_for, memo_for_cols)
|
|
150
|
-
render-from-section(memo_from)
|
|
151
|
+
if not falsey(memo_from) { render-from-section(memo_from) }
|
|
151
152
|
render-subject-section(subject)
|
|
152
153
|
render-references-section(references)
|
|
153
154
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
font,
|
|
21
21
|
letterhead-seal: none,
|
|
22
22
|
letterhead-seal-subtitle: none,
|
|
23
|
+
letterhead-emblem: none, // optional image placed opposite the seal (right side)
|
|
23
24
|
) = {
|
|
24
25
|
font = ensure-array(font)
|
|
25
26
|
title = ensure-string(title)
|
|
@@ -73,6 +74,17 @@
|
|
|
73
74
|
seal-body,
|
|
74
75
|
)
|
|
75
76
|
}
|
|
77
|
+
|
|
78
|
+
if letterhead-emblem != none {
|
|
79
|
+
place(
|
|
80
|
+
right + top,
|
|
81
|
+
dx: 0.5in,
|
|
82
|
+
dy: -.5in,
|
|
83
|
+
block[
|
|
84
|
+
#fit-box(width: 2in, height: 1in, alignment: right + horizon)[#letterhead-emblem]
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
}
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
// =============================================================================
|
|
@@ -98,9 +110,9 @@
|
|
|
98
110
|
" ",
|
|
99
111
|
align(left)[
|
|
100
112
|
#if type(recipients) == array {
|
|
101
|
-
create-auto-grid(recipients, column-gutter: spacing.tab, cols: cols)
|
|
113
|
+
create-auto-grid(recipients.map(upper), column-gutter: spacing.tab, cols: cols)
|
|
102
114
|
} else {
|
|
103
|
-
recipients
|
|
115
|
+
upper(recipients)
|
|
104
116
|
}
|
|
105
117
|
],
|
|
106
118
|
)
|
|
@@ -15,14 +15,19 @@
|
|
|
15
15
|
}
|
|
16
16
|
),
|
|
17
17
|
|
|
18
|
+
// Optional Freedom 250 emblem, placed opposite the seal
|
|
19
|
+
..if data.at("freedom250", default: true) {
|
|
20
|
+
(letterhead_emblem: image("assets/freedom250.png"))
|
|
21
|
+
},
|
|
22
|
+
|
|
18
23
|
// Date
|
|
19
24
|
date: data.at("date", default: none),
|
|
20
25
|
|
|
21
26
|
// Receiver information
|
|
22
27
|
memo_for: data.memo_for,
|
|
23
28
|
|
|
24
|
-
// Sender information
|
|
25
|
-
memo_from: data.memo_from,
|
|
29
|
+
// Sender information (omitted for Memorandum for Record)
|
|
30
|
+
..if data.at("memo_from", default: ()).len() > 0 { (memo_from: data.memo_from) },
|
|
26
31
|
|
|
27
32
|
// Subject line
|
|
28
33
|
subject: data.subject,
|
|
File without changes
|