@airmark/quiver 0.28.0 → 0.29.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 (37) hide show
  1. package/README.md +3 -2
  2. package/package.json +3 -3
  3. package/quills/daf4392/0.1.0/Quill.yaml +2 -2
  4. package/quills/usaf_memo/0.2.0/Quill.yaml +14 -11
  5. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/body.typ +37 -15
  6. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +5 -6
  7. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +5 -3
  8. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +87 -35
  9. package/quills/usaf_memo/0.2.0/packages/tonguetoquill-usaf-memo/src/utils.typ +34 -1
  10. package/quills/usaf_memo/0.3.0/Quill.yaml +321 -0
  11. package/quills/usaf_memo/0.3.0/assets/dod_seal.png +0 -0
  12. package/quills/usaf_memo/0.3.0/assets/dow_seal.png +0 -0
  13. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/.gitignore +7 -0
  14. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/LICENSE +21 -0
  15. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/Cinzel[wght].ttf +0 -0
  16. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/Cinzel/LICENSE +93 -0
  17. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Bold.otf +0 -0
  18. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/CopperplateCC-Heavy.otf +0 -0
  19. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/CopperplateCC/LICENSE.md +79 -0
  20. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/LiberationMono/LICENSE +102 -0
  21. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/LiberationMono/LiberationMono-Regular.ttf +0 -0
  22. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/GNU General Public License.txt +340 -0
  23. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Med.otf +0 -0
  24. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-MedIta.otf +0 -0
  25. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-Reg.otf +0 -0
  26. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/fonts/NimbusRomanNo9L/NimbusRomNo9L-RegIta.otf +0 -0
  27. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/backmatter.typ +29 -0
  28. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/body.typ +364 -0
  29. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/config.typ +78 -0
  30. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/frontmatter.typ +242 -0
  31. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/indorsement.typ +170 -0
  32. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/lib.typ +57 -0
  33. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/mainmatter.typ +27 -0
  34. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/primitives.typ +462 -0
  35. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/src/utils.typ +419 -0
  36. package/quills/usaf_memo/0.3.0/packages/tonguetoquill-usaf-memo/typst.toml +27 -0
  37. package/quills/usaf_memo/0.3.0/plate.typ +120 -0
package/README.md CHANGED
@@ -9,7 +9,8 @@ and aligned to the current Quillmark spec.
9
9
 
10
10
  | Quill | Version | Description |
11
11
  |--------------|---------|-----------------------------------------------------------------------------------|
12
- | `usaf_memo` | 0.2.0 | USAF / DAF Official Memorandum (AFH 33-337) |
12
+ | `usaf_memo` | 0.3.0 | USAF / DAF Official Memorandum (AFH 33-337) |
13
+ | `usaf_memo` | 0.2.0 | Previous release, retained so documents pinned to `@0.2` keep resolving |
13
14
  | `af4141` | 0.1.0 | AF Form 4141 — Individual's Record of Duties and Experience (Ground Environment) |
14
15
  | `daf1206` | 0.1.0 | DAF Form 1206 — Nomination for Award |
15
16
  | `daf4392` | 0.1.0 | DAF Form 4392 — Pre-Departure Safety Briefing (Page 2) |
@@ -46,7 +47,7 @@ const quiver = await fromDir(root);
46
47
  const engine = new Engine();
47
48
 
48
49
  const doc = Document.fromMarkdown(`~~~
49
- $quill: usaf_memo@0.2
50
+ $quill: usaf_memo@0.3
50
51
  $kind: main
51
52
  memo_for: ["ORG/SYMBOL"]
52
53
  memo_from: ["ORG/SYMBOL", "Organization Name", "123 Street Ave", "City ST 12345-6789"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@airmark/quiver",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Airmark Quiver — Air Force and DAF document Quills for Quillmark.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,8 +17,8 @@
17
17
  },
18
18
  "devDependencies": {
19
19
  "@quillmark/quiver": "0.21.0",
20
- "@quillmark/wasm": "^0.103.0",
21
- "quillkit": "0.2.0"
20
+ "@quillmark/wasm": "^0.104.0",
21
+ "quillkit": "0.2.2"
22
22
  },
23
23
  "scripts": {
24
24
  "test": "quillkit test",
@@ -12,8 +12,8 @@ main:
12
12
  enabled: false
13
13
  fields:
14
14
  transportation_mode:
15
- type: string
16
- enum: [pmv, airplane, bus, train, motorcycle, other]
15
+ type: enum
16
+ values: [pmv, airplane, bus, train, motorcycle, other]
17
17
  default: pmv
18
18
  description: "Matches the check box."
19
19
 
@@ -80,8 +80,8 @@ main:
80
80
  description: The full organization name of your unit.
81
81
 
82
82
  letterhead_seal:
83
- type: string
84
- enum:
83
+ type: enum
84
+ values:
85
85
  - dow
86
86
  - dod
87
87
  default: dow
@@ -108,8 +108,8 @@ main:
108
108
  description: "Organizational motto at the bottom of the page. Supports Markdown emphasis (e.g. *italics*, **bold**)."
109
109
 
110
110
  classification:
111
- type: string
112
- enum:
111
+ type: enum
112
+ values:
113
113
  - ""
114
114
  - UNCLASSIFIED
115
115
  - CUI
@@ -210,8 +210,8 @@ main:
210
210
  description: List attachments in the order they are mentioned in the memo. Briefly describe each; do not use 'as stated' or abbreviations.
211
211
 
212
212
  memo_style:
213
- type: string
214
- enum:
213
+ type: enum
214
+ values:
215
215
  - usaf
216
216
  - daf
217
217
  default: usaf
@@ -269,8 +269,8 @@ card_kinds:
269
269
  compact: true
270
270
  description: "Line 1: Name in UPPERCASE as signed, grade, and service. Line 2: Duty title."
271
271
  format:
272
- type: string
273
- enum:
272
+ type: enum
273
+ values:
274
274
  - standard
275
275
  - informal
276
276
  - separate_page
@@ -280,17 +280,20 @@ card_kinds:
280
280
  compact: true
281
281
  description: "'standard' (formal, same page), 'informal' (less formal routing), or 'separate_page' (begins on a new page)."
282
282
  action:
283
- type: string
284
- enum:
283
+ type: enum
284
+ values:
285
285
  - ""
286
286
  - undecided
287
287
  - approve
288
288
  - disapprove
289
+ - undecided_concur
290
+ - concur
291
+ - nonconcur
289
292
  default: ""
290
293
  ui:
291
294
  group: additional
292
295
  compact: true
293
- description: "Action taken by the endorser. Leave blank to hide the Approve/Disapprove line entirely, 'undecided' to display the line with neither option circled, 'approve' or 'disapprove' to circle the selected option."
296
+ description: "Decision taken by the endorser, shown as an either/or line with the choice circled. Leave blank to hide the line entirely. Approval authority: 'approve' or 'disapprove' (use 'undecided' to print Approve/Disapprove for the endorser to circle by hand). Coordinating official: 'concur' or 'nonconcur' (use 'undecided_concur' to print Concur/Nonconcur unmarked)."
294
297
  date:
295
298
  type: date
296
299
  default: ""
@@ -199,6 +199,7 @@
199
199
  // item.kind — "par", "heading", "table", or "continuation"
200
200
  context {
201
201
  let heading_buffer = none
202
+ let heading_level = 0
202
203
  // Filter out zero-width paragraphs so that an empty body (e.g. empty
203
204
  // string from a KIND with no text) emits nothing and collapses to zero
204
205
  // vertical space. Tables are always kept regardless of measured width.
@@ -226,24 +227,36 @@
226
227
  let kind = item.kind
227
228
  let item_content = item.content
228
229
 
229
- // Buffer headings for prepend to the next rendered element
230
- if kind == "heading" {
231
- heading_buffer = item_content
232
- continue
233
- }
234
-
235
- // Prepend buffered heading to the next non-heading element
230
+ // A buffered heading runs into this element only when the two belong to
231
+ // the same item: a later block of this list item ("continuation"), or,
232
+ // at top level, the next paragraph. The first block of the *next* item,
233
+ // a table (nest_level −1, so the level test alone excludes it), and
234
+ // another heading all fail the test, and each would otherwise carry the
235
+ // heading's text somewhere it was not authored. Those emit the heading
236
+ // on its own line, the treatment a heading before a table takes.
236
237
  if heading_buffer != none {
237
- if kind == "table" {
238
- // Tables cannot have inline text prepended; emit heading as
239
- // a standalone bold line above the table
240
- blank-line()
241
- strong[#heading_buffer.]
242
- heading_buffer = none
243
- } else {
238
+ let runs_in = (
239
+ kind != "heading"
240
+ and item.nest_level == heading_level
241
+ and (heading_level == 0 or kind == "continuation")
242
+ )
243
+ if runs_in {
244
244
  item_content = [#strong[#heading_buffer.] #item_content]
245
- heading_buffer = none
245
+ } else {
246
+ if any_emitted { blank-line() }
247
+ strong[#heading_buffer.]
248
+ any_emitted = true
246
249
  }
250
+ heading_buffer = none
251
+ }
252
+
253
+ // Buffer this heading for the next element to run into. The level it was
254
+ // captured at rides along: the run-in test needs it, and the buffer is
255
+ // read one iteration later, by which time `item` is the *next* element.
256
+ if kind == "heading" {
257
+ heading_buffer = item_content
258
+ heading_level = item.nest_level
259
+ continue
247
260
  }
248
261
 
249
262
  // Format based on element kind
@@ -336,6 +349,15 @@
336
349
  block[#final_par]
337
350
  }
338
351
  }
352
+
353
+ // A heading with nothing after it never ran into anything, and the buffer
354
+ // dies with the loop unless it is drained here. Sticky for the same reason
355
+ // the last item is: a standalone heading is one line, so it keeps with the
356
+ // signature block rather than opening a page alone.
357
+ if heading_buffer != none {
358
+ if any_emitted { blank-line() }
359
+ block(sticky: true)[#strong[#heading_buffer.]]
360
+ }
339
361
  }
340
362
  }
341
363
 
@@ -221,13 +221,12 @@
221
221
  render-date-section(actual_date, memo-style: memo_style)
222
222
  render-for-section(memo_for, memo_for_cols)
223
223
  if not falsey(memo_from) { render-from-section(memo_from) }
224
- let single-ref = if type(references) == array and references.len() == 1 {
225
- references.at(0)
226
- } else {
227
- none
228
- }
224
+ // Blank entries are dropped first so a stub `- ` left under `references:`
225
+ // cannot pass as the lone reference and render an empty `()` after the subject.
226
+ let refs = compact-references(references)
227
+ let single-ref = if refs.len() == 1 { refs.at(0) } else { none }
229
228
  render-subject-section(subject, inline-reference: single-ref)
230
- render-references-section(references)
229
+ render-references-section(refs)
231
230
 
232
231
  // AFH 33-337: "Begin text on second line below subject/references".
233
232
  // Emitted here (not inside body.typ) so the v() lands at the same lexical
@@ -22,9 +22,11 @@
22
22
  date: none,
23
23
  // Format of indorsement: "standard" (same page), "informal" (no header), or "separate_page" (starts on new page)
24
24
  format: "standard",
25
- // Approval action: none (default, no action line displayed), "undecided", "approve", or "disapprove".
26
- // When set to "undecided", the action line is displayed with neither option circled.
27
- // When set to "approve" or "disapprove", the action line is displayed with the selected option circled.
25
+ // Decision action. `none` (default) displays no action line at all.
26
+ // Approval authority pair: "approve" or "disapprove" circles the selected
27
+ // option; "undecided" displays the pair with neither circled.
28
+ // Coordination pair: "concur" or "nonconcur" circles the selected option;
29
+ // "undecided_concur" displays the pair with neither circled.
28
30
  action: none,
29
31
  content,
30
32
  ) = {
@@ -39,23 +39,36 @@
39
39
  let band-top = -band-height / 2 // puts the band center at dy 0
40
40
  let band-center = band-top + band-height / 2
41
41
 
42
+ // The centered title/caption is placed content (not width-constrained), so a
43
+ // long caption would otherwise render on one line and run underneath the seal.
44
+ // Bound the caption to the width that stays clear of the seal on both sides
45
+ // and auto-shrink it to fit: the seal reaches `band-height - corner-overhang`
46
+ // into the text area, so reserving that plus a gutter on each side keeps the
47
+ // centered caption from touching the seal. `layout` resolves the text-area
48
+ // width so the bound tracks the page/margins.
49
+ let caption-gutter = 0.25in
50
+ let caption-clear = (band-height - corner-overhang) + caption-gutter
42
51
  place(
43
52
  dy: 0.625in - spacing.margin,
44
53
  box(
45
54
  width: 100%,
46
55
  fill: none,
47
56
  stroke: none,
48
- [
49
- #place(
50
- center + top,
51
- align(center)[
52
- #set text(12pt, font: font, fill: LETTERHEAD_COLOR, weight: "bold")
53
- #title\
54
- #v(1pt)
55
- #text(10.5pt)[#caption]
56
- ],
57
- )
58
- ],
57
+ layout(size => place(
58
+ center + top,
59
+ align(center)[
60
+ #set text(12pt, font: font, fill: LETTERHEAD_COLOR, weight: "bold")
61
+ #title\
62
+ #v(1pt)
63
+ #if not falsey(caption) {
64
+ fit-to-width(
65
+ size.width - 2 * caption-clear,
66
+ alignment: center,
67
+ box(text(10.5pt)[#caption]),
68
+ )
69
+ }
70
+ ],
71
+ )),
59
72
  ),
60
73
  )
61
74
 
@@ -145,11 +158,32 @@
145
158
  )
146
159
  }
147
160
 
161
+ /// Whether a `references` entry carries no citation text.
162
+ ///
163
+ /// Blank entries reach the template routinely: a template author leaves a
164
+ /// stub `- ` under `references:` for the user to fill in, and the quillmark
165
+ /// helper hands back `""` for an unset or whitespace-only markdown field.
166
+ /// Such an entry must not count as a reference — otherwise a lone blank one
167
+ /// satisfies the "exactly one reference" test below and renders as an empty
168
+ /// `()` after the subject.
169
+ ///
170
+ /// - entry (any): A single `references` element
171
+ /// -> bool
172
+ #let blank-reference(entry) = falsey(entry) or entry == []
173
+
174
+ /// Drops `references` entries that carry no citation text and normalizes the
175
+ /// result to an array, so blank placeholder entries neither render on their
176
+ /// own nor affect the inline-vs-block decision.
177
+ ///
178
+ /// - references (array | none): Raw reference entries
179
+ /// -> array
180
+ #let compact-references(references) = ensure-array(references).filter(entry => not blank-reference(entry))
181
+
148
182
  // AFH 33-337 "SUBJECT:": "In all uppercase letters place 'SUBJECT:', flush with the
149
183
  // left margin, on the second line below the last line of the FROM element"
150
184
  #let render-subject-section(subject-text, inline-reference: none) = {
151
185
  blank-line()
152
- let content = if inline-reference != none {
186
+ let content = if not blank-reference(inline-reference) {
153
187
  [#subject-text (#box(inline-reference))]
154
188
  } else {
155
189
  [#subject-text]
@@ -163,7 +197,8 @@
163
197
  // AFH 33-337: only render References block for two or more references.
164
198
  // A single reference is rendered inline after the SUBJECT text instead.
165
199
  #let render-references-section(references) = {
166
- if type(references) == array and references.len() >= 2 {
200
+ let references = compact-references(references)
201
+ if references.len() >= 2 {
167
202
  blank-line()
168
203
  grid(
169
204
  columns: (auto, auto, 1fr),
@@ -245,40 +280,57 @@
245
280
  // =============================================================================
246
281
  // ACTION LINE RENDERING
247
282
  // =============================================================================
248
- // Renders the Approve / Disapprove action line for indorsement memos.
249
- // action: "undecided" = both options rendered plain (no circle),
250
- // "approve" = Approve circled, "disapprove" = Disapprove circled.
283
+ // Renders the decision line for indorsement memos — an "either / or" pair
284
+ // where the endorser's choice is circled and the rejected option struck out.
285
+ //
286
+ // Two option pairs are supported, reflecting the two roles an indorsement
287
+ // plays in a coordination chain: coordinating officials Concur / Nonconcur,
288
+ // while the final approval authority Approves / Disapproves.
289
+ //
290
+ // Each pair also has an "undecided" form that renders both options plain, for
291
+ // printing a memo the endorser circles by hand when signing.
292
+ //
251
293
  // Empty/none suppression is handled by the caller before this is invoked.
252
294
 
295
+ // Maps each `action` value to the option pair it renders and the index of the
296
+ // selected option within that pair (`none` selects neither).
297
+ #let ACTION_FORMS = (
298
+ "undecided": (("Approve", "Disapprove"), none),
299
+ "approve": (("Approve", "Disapprove"), 0),
300
+ "disapprove": (("Approve", "Disapprove"), 1),
301
+ "undecided_concur": (("Concur", "Nonconcur"), none),
302
+ "concur": (("Concur", "Nonconcur"), 0),
303
+ "nonconcur": (("Concur", "Nonconcur"), 1),
304
+ )
305
+
253
306
  #let render-action-line(action, trailing-blank-line: true) = {
254
307
  assert(
255
- action in ("undecided", "approve", "disapprove"),
256
- message: "action must be \"undecided\", \"approve\", or \"disapprove\"",
308
+ action in ACTION_FORMS,
309
+ message: "action must be one of " + ACTION_FORMS.keys().map(k => "\"" + k + "\"").join(", "),
257
310
  )
311
+ let (options, selected) = ACTION_FORMS.at(action)
312
+ // Circle the selected option using a box with rounded corners; strike the
313
+ // one it was chosen over. When neither is selected both render plain.
314
+ // Use baseline parameter to maintain vertical text alignment.
315
+ let render-option(index) = {
316
+ let option = options.at(index)
317
+ if selected == none {
318
+ option
319
+ } else if selected == index {
320
+ box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[#option]
321
+ } else {
322
+ strike(option)
323
+ }
324
+ }
258
325
  // No leading blank-line: the caller (indorsement.typ) already emits the
259
326
  // header→content gap once. The action line's `block(sticky: true)`
260
327
  // additionally inherits `block.above: spacing.line` so the visual gap
261
328
  // above matches the gap above a body's first paragraph.
262
- // Circle the selected option using a box with rounded corners
263
- // Use baseline parameter to maintain vertical text alignment
264
- let approve-text = if action == "approve" {
265
- box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[Approve]
266
- } else if action == "disapprove" {
267
- strike[Approve]
268
- } else {
269
- [Approve]
270
- }
271
- let disapprove-text = if action == "disapprove" {
272
- box(stroke: 0.5pt + black, radius: 2pt, inset: 2pt, baseline: 2pt)[Disapprove]
273
- } else if action == "approve" {
274
- strike[Disapprove]
275
- } else {
276
- [Disapprove]
277
- }
329
+ //
278
330
  // Keep the action line with the following content (body or signature block)
279
331
  // using the same sticky-block pattern that body.typ applies to the last
280
332
  // paragraph, per AFH 33-337 §11 orphan-prevention rules.
281
- block(sticky: true)[#approve-text / #disapprove-text]
333
+ block(sticky: true)[#render-option(0) / #render-option(1)]
282
334
  // Trailing blank-line places the body's first paragraph one line below
283
335
  // the action, mirroring the gap above it. Suppressed when the body is
284
336
  // empty so the signature block's own 4-line gap lands on AFH 33-337's
@@ -87,6 +87,36 @@
87
87
  ]
88
88
  }
89
89
 
90
+ /// Shrinks content to fit within a maximum width, never enlarging it.
91
+ ///
92
+ /// Measures `body` at its current font size and scales it down uniformly only
93
+ /// when it is wider than `width`; content that already fits keeps its set size
94
+ /// (the scale is capped at 100%). Unlike `fit-box`, height flows naturally and
95
+ /// small content is never scaled up. Used for the letterhead caption (unit
96
+ /// designation): a long name is shrunk to the width that clears the seal rather
97
+ /// than running underneath it. Pass a non-wrapping `box` as `body` so the
98
+ /// measured width is the single-line width and the result stays on one line.
99
+ ///
100
+ /// Empty content measures 0pt wide and has no scale to compute, so it is returned
101
+ /// as-is rather than divided by; a non-positive `width` likewise admits no scale
102
+ /// that fits, so the body is left at its set size instead of being flipped or
103
+ /// collapsed.
104
+ ///
105
+ /// - width (length): Maximum width the content may occupy
106
+ /// - alignment (alignment): Horizontal placement within the reserved width
107
+ /// - body (content): Content to fit (typically a non-wrapping `box`)
108
+ /// -> content
109
+ #let fit-to-width(width, alignment: left, body) = context {
110
+ let s = measure(body)
111
+ if s.width <= 0pt or width <= 0pt { return body }
112
+ let f = calc.min(1, width / s.width) * 100% // ratio, capped so it only shrinks
113
+ box(width: width)[
114
+ #align(alignment)[
115
+ #scale(f, reflow: true)[#body]
116
+ ]
117
+ ]
118
+ }
119
+
90
120
  /// Formats a date for the memo heading.
91
121
  ///
92
122
  /// Dispatches on the shape a date field can arrive in:
@@ -275,7 +305,10 @@
275
305
  if value == none {
276
306
  ""
277
307
  } else if type(value) == array {
278
- value.join(separator)
308
+ // `().join(sep)` is `none`, not `""` — coerce so an empty array keeps the
309
+ // documented contract and stays `falsey` for callers that test the result.
310
+ let joined = value.join(separator)
311
+ if joined == none { "" } else { joined }
279
312
  } else {
280
313
  str(value)
281
314
  }