@ham2k/extension-sdk 0.5.0 → 0.5.2

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // @ham2k/extension-sdk 0.5.0
1
+ // @ham2k/extension-sdk 0.5.2
2
2
  /** Experimental v1 scene contract. All coordinates are in the scene's viewBox. */
3
3
  export interface SvgScene {
4
4
  version: 1;
@@ -60,6 +60,7 @@ function qsoValues(qso) {
60
60
  const their = qso.their ?? {};
61
61
  const our = qso.our ?? {};
62
62
  const startMillis = Number(qso.startAtMillis ?? 0);
63
+ const serialSent = refField(qso.refs, "ourSerial");
63
64
  return {
64
65
  call: their.call ?? "",
65
66
  their,
@@ -71,12 +72,31 @@ function qsoValues(qso) {
71
72
  // sides — `qso.our.sent` is what WE sent, i.e. the report they received.
72
73
  rstSent: our.sent ?? "",
73
74
  rstRcvd: their.sent ?? "",
75
+ // Unpadded, as stored: `001` or `1` is the operator's call, not the
76
+ // contest's, so it is left to the `pad` filter.
77
+ serialSent,
78
+ serialRcvd: refField(qso.refs, "theirSerial"),
79
+ // The short forms are the SENT side: a CW message is what we send, and
80
+ // that is where these get typed.
81
+ rst: our.sent ?? "",
82
+ serial: serialSent,
74
83
  notes: qso.notes ?? "",
75
84
  refs: qso.refs ?? [],
76
85
  ...startMillis > 0 ? dateValues(startMillis) : { date: "", dateCompact: "", time: "", at: "" },
77
86
  startAtMillis: startMillis
78
87
  };
79
88
  }
89
+ function refField(refs, field) {
90
+ if (!Array.isArray(refs)) return "";
91
+ for (const ref of refs) {
92
+ if (ref === null || typeof ref !== "object" || Array.isArray(ref)) continue;
93
+ const value = ref[field];
94
+ if (typeof value !== "string" && typeof value !== "number") continue;
95
+ const text = String(value).trim();
96
+ if (text !== "") return text;
97
+ }
98
+ return "";
99
+ }
80
100
  function logValues(values) {
81
101
  return {
82
102
  station: values.station ?? "",
package/dist/templates.js CHANGED
@@ -30,7 +30,29 @@ function registerFilters(liquid2) {
30
30
  (value) => String(value ?? "").replace(/[^A-Za-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")
31
31
  );
32
32
  liquid2.registerFilter("alnum", (value) => String(value ?? "").replace(/[^A-Za-z0-9]/g, ""));
33
+ liquid2.registerFilter("pad", (value, width = 3) => {
34
+ const text = String(value ?? "").trim();
35
+ return text === "" ? "" : text.padStart(Number(width) || 0, "0");
36
+ });
37
+ liquid2.registerFilter("cut", (value, digits = "09") => {
38
+ const chosen = String(digits ?? "");
39
+ return String(value ?? "").replace(/[0159]/g, (digit) => chosen.includes(digit) ? CUT_NUMBERS[digit] : digit);
40
+ });
41
+ liquid2.registerFilter(
42
+ "refLabels",
43
+ (value) => (Array.isArray(value) ? value : []).flatMap((ref) => {
44
+ const label = [ref?.shortLabel, ref?.label, ref?.ref].map((text) => typeof text === "string" || typeof text === "number" ? String(text).trim() : "").find((text) => text !== "");
45
+ return label === void 0 ? [] : [label];
46
+ })
47
+ );
48
+ liquid2.registerFilter("sentence", (value, separator = ", ", final = " & ") => {
49
+ if (!Array.isArray(value)) return String(value ?? "");
50
+ const items = value.map((item) => String(item ?? "").trim()).filter((item) => item !== "");
51
+ if (items.length < 2) return items.join("");
52
+ return `${items.slice(0, -1).join(String(separator))}${String(final)}${items[items.length - 1]}`;
53
+ });
33
54
  }
55
+ const CUT_NUMBERS = { "0": "T", "1": "A", "5": "E", "9": "N" };
34
56
  class TemplateError extends Error {
35
57
  constructor(message, options) {
36
58
  super(message);
package/docs/templates.md CHANGED
@@ -34,7 +34,9 @@ about 2 µs each.
34
34
  the namespaces are deliberately polo's. The syntax around them does not:
35
35
  `{{#if}}` is `{% if %}{% endif %}`, `{{#each}}` is `{% for %}{% endfor %}`,
36
36
  `{{> Partial}}` has no equivalent, and helpers are filters
37
- (`{{ dash x }}` → `{{ x | dash }}`).
37
+ (`{{ dash x }}` → `{{ x | dash }}`). polo's
38
+ `{{#join op.refs separator=", " final=" & "}}{{or shortLabel label key}}{{/join}}`
39
+ is `{{ op.refs | refLabels | sentence }}`.
38
40
 
39
41
  ## What a template can name
40
42
 
@@ -74,12 +76,22 @@ that contact's length.
74
76
 
75
77
  ### `qso` — one contact
76
78
  `call`, `their`, `our` (both whole, so `qso.their.guess.name` reaches the
77
- lookup's answer), `band`, `mode`, `freq`, `rstSent`, `rstRcvd`, `notes`,
78
- `date`, `dateCompact`, `time`, `at`, `startAtMillis`, `refs`.
79
+ lookup's answer), `band`, `mode`, `freq`, `rstSent`, `rstRcvd`, `serialSent`,
80
+ `serialRcvd`, `notes`, `date`, `dateCompact`, `time`, `at`, `startAtMillis`,
81
+ `refs` — and two short forms for the sent side, `rst` (= `rstSent`) and
82
+ `serial` (= `serialSent`), since a CW message is what we send and that is
83
+ where these get typed: `{{ qso.call }} {{ qso.rst | cut }} {{ qso.serial | pad: 3 | cut }}`.
79
84
 
80
85
  `rstSent` is what WE sent — QSON stores each side's report under its own
81
86
  `sent`, and these are named for the operator's view of the contact.
82
87
 
88
+ `serialSent` / `serialRcvd` are a contest's serial numbers, blank outside
89
+ one. A contest keeps them on its own ref, so they are found by field name —
90
+ the first ref carrying `ourSerial` / `theirSerial` — which is what lets one
91
+ message serve every serial contest (docs/design/contests.md §5.8). In a CW
92
+ message `serialSent` is the number the serial field is SHOWING, the one the
93
+ QSO will be logged with. It is unpadded (`7`, not `007`): use `pad`.
94
+
83
95
  ### `config` — a panel's own form values
84
96
  Whatever that panel's `form` declared, under the keys it used.
85
97
 
@@ -112,12 +124,27 @@ with nothing to say it was invented.
112
124
 
113
125
  All of Liquid's own (`date`, `downcase`, `upcase`, `strip`, `default`,
114
126
  `join`, `size`, `first`, `last`, `round`, `truncate`, `replace`, `map`,
115
- `where`, …), plus two:
127
+ `where`, …), plus six:
116
128
 
117
129
  | filter | does | example |
118
130
  |---|---|---|
119
131
  | `dash` | non-alphanumerics → `-`, collapsed and trimmed, **case preserved** | `N0CALL/P` → `N0CALL-P` |
120
132
  | `alnum` | strip to alphanumerics | `2026-07-27` → `20260727` |
133
+ | `pad: n` | zero-pad on the left to `n` characters (default 3); **blank stays blank** | `7` → `007` |
134
+ | `cut: "digits"` | CW cut numbers for the listed digits, of `0`→`T` `1`→`A` `5`→`E` `9`→`N`; default `"09"` | `599` → `5NN`, `{{ 9 \| pad: 3 \| cut }}` → `TTN` |
135
+ | `refLabels` | a list of refs → each one's `shortLabel`, else `label`, else `ref`; a ref with none of them is skipped — an operation's contest ref HAS a label (`CQ WPX CW`) and is listed | `op.refs` → `POTA US-1234`, `KFF-9999` |
136
+ | `sentence: "sep", "final"` | joins a list with a different LAST separator (defaults `", "` and `" & "`); blanks are dropped | `A, B & C`, `A & B` |
137
+
138
+ `pad` leaves a blank alone because a CW message can be keyed before the
139
+ serial field has a number, and `000` sent in that gap is a serial the log
140
+ will never agree with. Order matters with `cut`: pad first, or the padding
141
+ zeros go out uncut.
142
+
143
+ `refLabels` exists because `map` reads one field: `map: "shortLabel"` drops a
144
+ reference that resolved to nothing, since it carries no labels — an
145
+ operation at an unlisted park would print as though it had none. Liquid's own
146
+ `array_to_sentence_string` is the near miss for `sentence`: it always writes
147
+ the serial comma (`A, B, & C`).
121
148
 
122
149
  `alnum` is app-polo's `compact` helper under a different name: Liquid already
123
150
  has a `compact` (it drops nils from an **array**, and `op.refs`/`qso.refs` are
@@ -140,6 +167,10 @@ Each of these was measured against the shipped runtime, not assumed.
140
167
  number as SECONDS, so epoch millis render in the year 58567. Use the `…At`
141
168
  strings.
142
169
 
170
+ **`cut`'s digit list must be quoted.** Liquid reads a bare `0159` as the
171
+ number 159 before the filter sees it, so `{{ n | cut: 09 }}` cuts only the
172
+ nines — the zero is gone with no error. Write `cut: "09"`.
173
+
143
174
  **Unknown names are silent.** An unknown variable renders empty and an
144
175
  unknown filter passes its value through. That is deliberate — an operator's
145
176
  typo costs one line, not the document — but it means a misspelled
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ham2k/extension-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Write extensions for the Ham2K Logger: typed hook contracts and the host API",
5
5
  "keywords": [
6
6
  "ham2k",