@instafy/cli 0.3.1 → 0.4.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.
- package/README.md +3 -1
- package/dist/bind/bin/instafy-bind.mjs +4 -1
- package/dist/bind/src/check.mjs +195 -8
- package/dist/bind/src/starters/compare/app.js +62 -37
- package/dist/bind/src/starters/compare/data.json +1 -1
- package/dist/bind/src/starters/ranked/app.js +38 -15
- package/dist/bind/src/starters/ranked/data.json +2 -2
- package/dist/bind/src/starters/site.js +129 -27
- package/dist/cli.js +6413 -3465
- package/package.json +1 -1
|
@@ -9,12 +9,18 @@
|
|
|
9
9
|
* - Rows: give each row id="row-<slug>" and data-ref="<kind>:<slug>" (site.rowAttrs(ref)). A "select" command from
|
|
10
10
|
* chat scrolls to the row and marks it; "highlight" fades the rows it names. "toast" and "reload" are the client's.
|
|
11
11
|
* - Facts: one shape everywhere, {"v": "8.9 oz", "src": "Maker", "url": "https://…", "date": "2026-10-17", "est": false}.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* "url" is the page it was read on and "date" the day it was read; each value cites them: "checked 17 Oct on
|
|
13
|
+
* maker.com ↗" (a price: "seen 17 Oct on shop.com ↗"). Optional "asof": the source's own date, shown in place of
|
|
14
|
+
* "date" ("seen 5 Aug on …"); a price with an "asof" over 30 days old is muted and says "may have changed", like a
|
|
15
|
+
* live price past its time. A value that only says nothing is known ("—", "n/a", "?", "unknown") is no value.
|
|
16
|
+
* site.trust(fact | record, opts) draws the one trust line: "checked 17 Oct on maker.com ↗", "at Store ↗ · checked
|
|
17
|
+
* 4 min ago", "Estimate", "Price not checked" (nobody checked it), "Not listed · checked 16 Oct on maker.com ↗" (a
|
|
18
|
+
* fact with a source and no value: the source was read and does not give it); in a table of options a value nobody
|
|
19
|
+
* found reads opts.unknown, "Unknown · why" ("Price unknown"). A price fact with "list": true is that source's
|
|
20
|
+
* list price, never a store's price now: "$900 · list price on BabyGearLab ↗ · seen 5 Aug".
|
|
21
|
+
* - Filters: site.within(value, filter) is "in", "out" or "unknown" for the page's one filter ({key, max, min}), and
|
|
22
|
+
* site.byFilter(rows, filter, valueOf) splits rows into {in, unknown, out}. Only a value the page shows counts: a
|
|
23
|
+
* row without one is never "under $140"; the starters list it apart ("Price unknown").
|
|
18
24
|
* - Live checks: site.checkAll(rows) is the page's one primary button ("Check all prices", with two or more checkable
|
|
19
25
|
* rows). trust(value, {ref, intent, store, seed}) ends a row's caption in its one quiet action: "Check price" (only
|
|
20
26
|
* without "Check all prices"), "Try again" after an error, "Open store ↗" when the store can't be read. While a
|
|
@@ -147,24 +153,82 @@
|
|
|
147
153
|
}
|
|
148
154
|
}
|
|
149
155
|
function isRecord(x) { return !!(x && typeof x === "object" && x.prov && x.type); }
|
|
150
|
-
/**
|
|
156
|
+
/** A value that only says nothing is known: blank, dashes, "?", "n/a", "unknown", "TBD". */
|
|
157
|
+
var NO_VALUE = /^(?:[\s\-\u2010-\u2015?.\u00b7\u2026/]*|n\s*\/\s*a|unknown|tb[ad])$/i;
|
|
158
|
+
/** The text of a fact or record value ("" when there is none, or it only says nothing is known: a dash in a table
|
|
159
|
+
* is never drawn as if it were a value). */
|
|
151
160
|
function valueText(x) {
|
|
152
161
|
if (!x) return "";
|
|
162
|
+
var text;
|
|
153
163
|
if (isRecord(x)) {
|
|
154
164
|
if (x.type === "offer") return money(x.price);
|
|
155
165
|
if (x.value === true) return "Yes";
|
|
156
166
|
if (x.value === false) return "No";
|
|
157
|
-
|
|
167
|
+
text = x.value == null ? "" : String(x.value);
|
|
168
|
+
} else {
|
|
169
|
+
text = x.v == null ? "" : String(x.v);
|
|
170
|
+
}
|
|
171
|
+
return NO_VALUE.test(text.trim()) ? "" : text;
|
|
172
|
+
}
|
|
173
|
+
/** The first number in a text, read the way shops write it: a ".", ",", space or "'" before exactly three digits
|
|
174
|
+
* groups thousands ("1.299", "1 299", "1'299", "1,299" are 1299), and the last "." or "," is the decimal mark
|
|
175
|
+
* otherwise ("1.299,00" and "1,299.00" are 1299, "549,99" is 549.99, "4.6" is 4.6, "0.125" is 0.125). */
|
|
176
|
+
function numberIn(text) {
|
|
177
|
+
var m = /(-?)(\d+)((?:[.,'’ ]\d+)*)/.exec(String(text == null ? "" : text));
|
|
178
|
+
if (!m) return null;
|
|
179
|
+
var head = m[2];
|
|
180
|
+
var parts = m[3] ? m[3].match(/[.,'’ ]\d+/g) : [];
|
|
181
|
+
var mark = function (p) { return p.charAt(0); };
|
|
182
|
+
var dec = -1;
|
|
183
|
+
parts.forEach(function (p, i) { if (mark(p) === "." || mark(p) === ",") dec = i; });
|
|
184
|
+
// the last "." or "," groups thousands when three digits follow it, one to three (not just 0) lead, and no other
|
|
185
|
+
// kind of decimal mark comes before it: "1.299", "1,299,000"
|
|
186
|
+
if (dec >= 0 && parts[dec].length === 4 && head.length <= 3 && head !== "0" &&
|
|
187
|
+
parts.slice(0, dec).every(function (p) { return (mark(p) !== "." && mark(p) !== ",") || mark(p) === mark(parts[dec]); })) dec = -1;
|
|
188
|
+
var whole = head;
|
|
189
|
+
var end = dec >= 0 ? dec : parts.length;
|
|
190
|
+
for (var i = 0; i < end; i++) {
|
|
191
|
+
// a mark that does not group thousands ends the number: "2 3-year plans" is 2
|
|
192
|
+
if (head.length > 3 || parts[i].length !== 4) return Number(m[1] + whole);
|
|
193
|
+
whole += parts[i].slice(1);
|
|
158
194
|
}
|
|
159
|
-
return
|
|
195
|
+
return Number(m[1] + whole + (dec >= 0 ? "." + parts[dec].slice(1) : ""));
|
|
160
196
|
}
|
|
161
|
-
/** A number to sort by: fact.n, an offer's amount, else the first number in the value text. */
|
|
197
|
+
/** A number to sort by: fact.n, an offer's amount, else the first number in the value text (numberIn). */
|
|
162
198
|
function num(x) {
|
|
163
199
|
if (!x) return null;
|
|
164
200
|
if (isRecord(x) && x.type === "offer") return x.price && typeof x.price.amount === "number" ? x.price.amount : null;
|
|
165
201
|
if (typeof x.n === "number") return x.n;
|
|
166
|
-
|
|
167
|
-
|
|
202
|
+
return numberIn(valueText(x));
|
|
203
|
+
}
|
|
204
|
+
/** The number of a value the page shows, else null. A value counts only when its trust line draws it: a live value,
|
|
205
|
+
* an estimate, or a fact with its source. An unsourced fact (drawn "Not checked"), a check that could not read it,
|
|
206
|
+
* and a dash are unknown, so no filter or sort counts them. */
|
|
207
|
+
function known(x) {
|
|
208
|
+
if (!x) return null;
|
|
209
|
+
if (isRecord(x)) {
|
|
210
|
+
if (x.type === "claim" && (x.status === "uncertain" || x.status === "not_found" || x.status === "error")) return null;
|
|
211
|
+
return valueText(x) ? num(x) : null;
|
|
212
|
+
}
|
|
213
|
+
if (!valueText(x) || !(x.src || x.est)) return null;
|
|
214
|
+
return num(x);
|
|
215
|
+
}
|
|
216
|
+
/** Where a value stands against the page's one filter ({key, max?, min?}): "in", "out", or "unknown" when the page
|
|
217
|
+
* has no number for it. Unknown is never "in": a row without a price is not "under $140". */
|
|
218
|
+
function within(x, filter) {
|
|
219
|
+
if (!filter) return "in";
|
|
220
|
+
var n = known(x);
|
|
221
|
+
if (n == null) return "unknown";
|
|
222
|
+
var max = filter.max == null || filter.max === "" ? null : Number(filter.max);
|
|
223
|
+
var min = filter.min == null || filter.min === "" ? null : Number(filter.min);
|
|
224
|
+
return (max == null || n <= max) && (min == null || n >= min) ? "in" : "out";
|
|
225
|
+
}
|
|
226
|
+
/** Rows split by the page's one filter, each part in the rows' own order: {in, unknown, out}. Without a filter every
|
|
227
|
+
* row is "in". valueOf(row) gives the row's value for the filter's key. */
|
|
228
|
+
function byFilter(rows, filter, valueOf) {
|
|
229
|
+
var parts = { "in": [], unknown: [], out: [] };
|
|
230
|
+
(rows || []).forEach(function (r) { parts[within(filter ? valueOf(r) : null, filter)].push(r); });
|
|
231
|
+
return parts;
|
|
168
232
|
}
|
|
169
233
|
|
|
170
234
|
// ---------- live values ----------
|
|
@@ -189,6 +253,30 @@
|
|
|
189
253
|
var when = x && (x.asof || x.date);
|
|
190
254
|
return when && date(when) ? "seen " + date(when) : "";
|
|
191
255
|
}
|
|
256
|
+
/** Where and when a value was read, as one caption part: "checked 17 Oct on maker.com ↗" (the host links the page it
|
|
257
|
+
* was read on). A price, or a date the source gives ("asof"), is "seen": "seen 5 Aug on reviews.example ↗", never
|
|
258
|
+
* "checked", so it does not read as today's price. Without a page link it names the source: "Maker · checked 17
|
|
259
|
+
* Oct". o.price: the value is a price. */
|
|
260
|
+
function citeParts(x, o) {
|
|
261
|
+
o = o || {};
|
|
262
|
+
if (!x) return [];
|
|
263
|
+
var when = x.asof || x.date;
|
|
264
|
+
var day = when && date(when) ? ((o.price || x.asof ? "seen " : "checked ") + date(when)) : "";
|
|
265
|
+
var site = host(x.url);
|
|
266
|
+
if (site) return [h("span", { "class": "cite" }, day ? day + " on " : "", link(x.url, site))];
|
|
267
|
+
var out = [];
|
|
268
|
+
if (x.src) out.push(String(x.src));
|
|
269
|
+
if (day) out.push(h("span", { "class": "age", text: day }));
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
/** cite() as one inline piece, for a line under a list ("Weight: checked 17 Oct on maker.com ↗"). */
|
|
273
|
+
function cite(x, o) {
|
|
274
|
+
var parts = citeParts(x, o);
|
|
275
|
+
if (!parts.length) return null;
|
|
276
|
+
var el = h("span", null);
|
|
277
|
+
parts.forEach(function (p, i) { if (i) el.appendChild(document.createTextNode(" · ")); append(el, p); });
|
|
278
|
+
return el;
|
|
279
|
+
}
|
|
192
280
|
function newest(list) {
|
|
193
281
|
return list.slice().sort(function (a, b) { return (Date.parse(b.prov.observed_at) || 0) - (Date.parse(a.prov.observed_at) || 0); })[0] || null;
|
|
194
282
|
}
|
|
@@ -233,12 +321,14 @@
|
|
|
233
321
|
|
|
234
322
|
/**
|
|
235
323
|
* The one trust line for a fact or a live record: the value, then a quiet caption.
|
|
236
|
-
* value ·
|
|
324
|
+
* value · checked 17 Oct on maker.com ↗ (a seed fact: the page it was read on and the day; "seen 5 Aug on …"
|
|
325
|
+
* for a price or the source's own date)
|
|
237
326
|
* value at Store ↗ · checked 4 min ago (a live check; " · may have changed" once it is old)
|
|
238
327
|
* value · Estimate · how ↗
|
|
239
328
|
* Price not checked · reason · where to check ↗
|
|
240
|
-
* opts: label ("Price": says what was not checked),
|
|
241
|
-
*
|
|
329
|
+
* opts: label ("Price": says what was not checked), unknown (the word for a missing value: "Unknown" in a table of
|
|
330
|
+
* options, "To confirm" on a checklist), price (a price: "at <store>"), shared (the source is said once for the
|
|
331
|
+
* whole column: value only), and for a row that can be checked live: ref, intent, store, params. Then the
|
|
242
332
|
* caption ends in the row's one action: "Check price" (only when there is no "Check all prices"), "Try again" after
|
|
243
333
|
* an error, "Open store ↗" when the store can't be read. While a check runs the value stays and the caption says
|
|
244
334
|
* "Checking…".
|
|
@@ -259,17 +349,21 @@
|
|
|
259
349
|
var value = valueText(x);
|
|
260
350
|
// "Not checked" has one meaning on every page: nobody checked it. A checklist says "To confirm" (opts.unknown),
|
|
261
351
|
// because there "checked" is the tick.
|
|
352
|
+
// A table of options says "Unknown" ("Price unknown") for a value nobody found, then why; "Not listed" when the
|
|
353
|
+
// page that was read does not give it.
|
|
354
|
+
var word = opts.unknown || "Not checked";
|
|
262
355
|
var notChecked = function (note, url) {
|
|
263
|
-
add((opts.label ? opts.label + "
|
|
356
|
+
add((opts.label ? opts.label + " " + word.toLowerCase() : word) + (usefulNote(note) ? " · " + usefulNote(note) : ""));
|
|
264
357
|
if (safeUrl(url)) add(link(url, "where to check", "act"));
|
|
265
358
|
};
|
|
266
|
-
// looked for and not there: the source was read and does not give it ("Not listed ·
|
|
267
|
-
// link to the page that was read stays even when the source is named once for the column (then without
|
|
359
|
+
// looked for and not there: the source was read and does not give it ("Not listed · checked 16 Oct on maker.com
|
|
360
|
+
// ↗"); the link to the page that was read stays even when the source is named once for the column (then without
|
|
361
|
+
// the date)
|
|
268
362
|
var notListed = function (note, src, url, fact) {
|
|
269
363
|
add((opts.label ? opts.label + " not listed" : "Not listed") + (usefulNote(note) ? " · " + usefulNote(note) : ""));
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
if (
|
|
364
|
+
if (fact && !opts.shared) citeParts(fact, { price: opts.price }).forEach(add);
|
|
365
|
+
else if (safeUrl(url)) add(link(url, host(url) || src));
|
|
366
|
+
else if (src) add(src);
|
|
273
367
|
};
|
|
274
368
|
var failed = live && !pending && checks.errors.has(opts.ref);
|
|
275
369
|
|
|
@@ -285,6 +379,9 @@
|
|
|
285
379
|
} else if (x.type === "claim" && x.status === "error") {
|
|
286
380
|
value = "";
|
|
287
381
|
add(h("span", null, icon("error"), "Couldn't check" + (usefulNote(x.note) ? " · " + usefulNote(x.note) : "")));
|
|
382
|
+
} else if (!value) {
|
|
383
|
+
// a record whose value only says nothing is known ("—") is unknown, never a caption without a value
|
|
384
|
+
notChecked(x.note);
|
|
288
385
|
} else if (ESTIMATE_BASIS.indexOf(x.basis) >= 0) {
|
|
289
386
|
add("Estimate");
|
|
290
387
|
if (safeUrl(url)) add(link(url, "how"));
|
|
@@ -293,7 +390,7 @@
|
|
|
293
390
|
add(h("span", { "class": "age", text: "checked " + ago(x.prov.observed_at) }));
|
|
294
391
|
}
|
|
295
392
|
if (expired(x) && value) { el.classList.add("is-old"); add("may have changed"); }
|
|
296
|
-
} else if (x && x.est) {
|
|
393
|
+
} else if (x && x.est && value) {
|
|
297
394
|
// an estimate names its basis: a source to check it against ("Estimate · Taiwan Railway ↗") or how ↗
|
|
298
395
|
add("Estimate");
|
|
299
396
|
if (x.src) add(link(x.url, x.src)); else if (safeUrl(x.url)) add(link(x.url, "how"));
|
|
@@ -305,9 +402,14 @@
|
|
|
305
402
|
// under the list names the source: next to "Open Amazon ↗" a bare "$900" reads as Amazon's price today
|
|
306
403
|
var list = opts.price && !atStore && x.list;
|
|
307
404
|
if (!opts.shared) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
405
|
+
if (atStore || list) {
|
|
406
|
+
add(atStore ? h("span", null, "at ", link(x.url, x.src)) : h("span", null, "list price on ", link(x.url, x.src)));
|
|
407
|
+
var when = seenText(x);
|
|
408
|
+
if (when) add(h("span", { "class": "age", text: when }));
|
|
409
|
+
} else {
|
|
410
|
+
// every value cites the page it was read on and the day: "checked 17 Oct on maker.com ↗"
|
|
411
|
+
citeParts(x, { price: opts.price }).forEach(add);
|
|
412
|
+
}
|
|
311
413
|
if (seedOld) add("may have changed");
|
|
312
414
|
} else if (list) {
|
|
313
415
|
add("list price");
|
|
@@ -872,10 +974,10 @@
|
|
|
872
974
|
}
|
|
873
975
|
|
|
874
976
|
window.site = {
|
|
875
|
-
version: "1.
|
|
977
|
+
version: "1.5.0",
|
|
876
978
|
h: h, append: append, icon: icon, link: link, slug: slug, rowAttrs: rowAttrs, safeUrl: safeUrl,
|
|
877
979
|
date: date, dayDate: dayDate, ago: ago, money: money, num: num, valueText: valueText, host: host, seen: seenText,
|
|
878
|
-
old: isOld, expired: expired,
|
|
980
|
+
cite: cite, known: known, within: within, byFilter: byFilter, old: isOld, expired: expired,
|
|
879
981
|
live: live, trust: trust, rowAction: rowAction, checkButton: checkButton, checkAll: checkAll, canCheck: canCheck,
|
|
880
982
|
anyPending: anyPending, priceNotice: priceNotice, storesBlocked: storesBlocked, blocked: blockedRecord, tidy: tidyCaptions, remember: remember, recall: recall,
|
|
881
983
|
state: state, setState: setState, onState: onState, render: render, schedule: schedule,
|