tova 0.1.1 → 0.2.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/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/tova.js +811 -154
- package/package.json +8 -2
- package/src/analyzer/analyzer.js +297 -58
- package/src/analyzer/scope.js +38 -1
- package/src/analyzer/type-registry.js +72 -0
- package/src/analyzer/types.js +478 -0
- package/src/codegen/base-codegen.js +371 -0
- package/src/codegen/client-codegen.js +62 -10
- package/src/codegen/codegen.js +111 -2
- package/src/codegen/server-codegen.js +175 -3
- package/src/config/edit-toml.js +100 -0
- package/src/config/package-json.js +52 -0
- package/src/config/resolve.js +100 -0
- package/src/config/toml.js +209 -0
- package/src/lexer/lexer.js +2 -2
- package/src/lsp/server.js +284 -30
- package/src/parser/ast.js +105 -0
- package/src/parser/parser.js +202 -2
- package/src/runtime/ai.js +305 -0
- package/src/runtime/devtools.js +228 -0
- package/src/runtime/embedded.js +3 -1
- package/src/runtime/io.js +240 -0
- package/src/runtime/reactivity.js +264 -19
- package/src/runtime/ssr.js +196 -24
- package/src/runtime/table.js +522 -0
- package/src/stdlib/collections.js +245 -0
- package/src/stdlib/core.js +87 -0
- package/src/stdlib/datetime.js +88 -0
- package/src/stdlib/encoding.js +35 -0
- package/src/stdlib/functional.js +82 -0
- package/src/stdlib/inline.js +334 -67
- package/src/stdlib/math.js +93 -0
- package/src/stdlib/string.js +95 -0
- package/src/stdlib/url.js +33 -0
- package/src/stdlib/validation.js +29 -0
|
@@ -88,3 +88,248 @@ export function values(obj) {
|
|
|
88
88
|
export function merge(...objects) {
|
|
89
89
|
return Object.assign({}, ...objects);
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
export function partition(arr, fn) {
|
|
93
|
+
const y = [], n = [];
|
|
94
|
+
for (const v of arr) { (fn(v) ? y : n).push(v); }
|
|
95
|
+
return [y, n];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function zip_with(a, b, fn) {
|
|
99
|
+
const m = Math.min(a.length, b.length);
|
|
100
|
+
const r = [];
|
|
101
|
+
for (let i = 0; i < m; i++) r.push(fn(a[i], b[i]));
|
|
102
|
+
return r;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function frequencies(arr) {
|
|
106
|
+
const r = {};
|
|
107
|
+
for (const v of arr) {
|
|
108
|
+
const k = String(v);
|
|
109
|
+
r[k] = (r[k] || 0) + 1;
|
|
110
|
+
}
|
|
111
|
+
return r;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function scan(arr, fn, init) {
|
|
115
|
+
const r = [];
|
|
116
|
+
let acc = init;
|
|
117
|
+
for (const v of arr) { acc = fn(acc, v); r.push(acc); }
|
|
118
|
+
return r;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function min_by(arr, fn) {
|
|
122
|
+
if (arr.length === 0) return null;
|
|
123
|
+
let best = arr[0], bestK = fn(arr[0]);
|
|
124
|
+
for (let i = 1; i < arr.length; i++) {
|
|
125
|
+
const k = fn(arr[i]);
|
|
126
|
+
if (k < bestK) { best = arr[i]; bestK = k; }
|
|
127
|
+
}
|
|
128
|
+
return best;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function max_by(arr, fn) {
|
|
132
|
+
if (arr.length === 0) return null;
|
|
133
|
+
let best = arr[0], bestK = fn(arr[0]);
|
|
134
|
+
for (let i = 1; i < arr.length; i++) {
|
|
135
|
+
const k = fn(arr[i]);
|
|
136
|
+
if (k > bestK) { best = arr[i]; bestK = k; }
|
|
137
|
+
}
|
|
138
|
+
return best;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function sum_by(arr, fn) {
|
|
142
|
+
let s = 0;
|
|
143
|
+
for (const v of arr) s += fn(v);
|
|
144
|
+
return s;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function product(arr) {
|
|
148
|
+
return arr.reduce((a, b) => a * b, 1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function from_entries(pairs) {
|
|
152
|
+
return Object.fromEntries(pairs);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function has_key(obj, key) {
|
|
156
|
+
return obj != null && Object.prototype.hasOwnProperty.call(obj, key);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function get(obj, path, def) {
|
|
160
|
+
const keys = Array.isArray(path) ? path : String(path).split('.');
|
|
161
|
+
let cur = obj;
|
|
162
|
+
for (const k of keys) {
|
|
163
|
+
if (cur == null || typeof cur !== 'object') return def !== undefined ? def : null;
|
|
164
|
+
cur = cur[k];
|
|
165
|
+
}
|
|
166
|
+
return cur !== undefined ? cur : (def !== undefined ? def : null);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function pick(obj, ks) {
|
|
170
|
+
const r = {};
|
|
171
|
+
for (const k of ks) { if (k in obj) r[k] = obj[k]; }
|
|
172
|
+
return r;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function omit(obj, ks) {
|
|
176
|
+
const s = new Set(ks);
|
|
177
|
+
const r = {};
|
|
178
|
+
for (const k of Object.keys(obj)) { if (!s.has(k)) r[k] = obj[k]; }
|
|
179
|
+
return r;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function map_values(obj, fn) {
|
|
183
|
+
const r = {};
|
|
184
|
+
for (const [k, v] of Object.entries(obj)) r[k] = fn(v, k);
|
|
185
|
+
return r;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function sliding_window(arr, n) {
|
|
189
|
+
if (n <= 0 || n > arr.length) return [];
|
|
190
|
+
const r = [];
|
|
191
|
+
for (let i = 0; i <= arr.length - n; i++) r.push(arr.slice(i, i + n));
|
|
192
|
+
return r;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Set Operations ────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
export function intersection(a, b) {
|
|
198
|
+
const s = new Set(b);
|
|
199
|
+
return a.filter(x => s.has(x));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function difference(a, b) {
|
|
203
|
+
const s = new Set(b);
|
|
204
|
+
return a.filter(x => !s.has(x));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function symmetric_difference(a, b) {
|
|
208
|
+
const sa = new Set(a);
|
|
209
|
+
const sb = new Set(b);
|
|
210
|
+
return [...a.filter(x => !sb.has(x)), ...b.filter(x => !sa.has(x))];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function is_subset(a, b) {
|
|
214
|
+
const s = new Set(b);
|
|
215
|
+
return a.every(x => s.has(x));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function is_superset(a, b) {
|
|
219
|
+
const s = new Set(a);
|
|
220
|
+
return b.every(x => s.has(x));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Itertools ─────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
export function pairwise(arr) {
|
|
226
|
+
const r = [];
|
|
227
|
+
for (let i = 0; i < arr.length - 1; i++) r.push([arr[i], arr[i + 1]]);
|
|
228
|
+
return r;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function combinations(arr, r) {
|
|
232
|
+
const result = [];
|
|
233
|
+
const combo = [];
|
|
234
|
+
function gen(start, depth) {
|
|
235
|
+
if (depth === r) { result.push([...combo]); return; }
|
|
236
|
+
for (let i = start; i < arr.length; i++) {
|
|
237
|
+
combo.push(arr[i]);
|
|
238
|
+
gen(i + 1, depth + 1);
|
|
239
|
+
combo.pop();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
gen(0, 0);
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function permutations(arr, r) {
|
|
247
|
+
const n = r === undefined ? arr.length : r;
|
|
248
|
+
const result = [];
|
|
249
|
+
const perm = [];
|
|
250
|
+
const used = new Array(arr.length).fill(false);
|
|
251
|
+
function gen() {
|
|
252
|
+
if (perm.length === n) { result.push([...perm]); return; }
|
|
253
|
+
for (let i = 0; i < arr.length; i++) {
|
|
254
|
+
if (!used[i]) {
|
|
255
|
+
used[i] = true;
|
|
256
|
+
perm.push(arr[i]);
|
|
257
|
+
gen();
|
|
258
|
+
perm.pop();
|
|
259
|
+
used[i] = false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
gen();
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function intersperse(arr, sep) {
|
|
268
|
+
if (arr.length <= 1) return [...arr];
|
|
269
|
+
const r = [arr[0]];
|
|
270
|
+
for (let i = 1; i < arr.length; i++) { r.push(sep, arr[i]); }
|
|
271
|
+
return r;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function interleave(...arrs) {
|
|
275
|
+
const m = Math.max(...arrs.map(a => a.length));
|
|
276
|
+
const r = [];
|
|
277
|
+
for (let i = 0; i < m; i++) {
|
|
278
|
+
for (const a of arrs) { if (i < a.length) r.push(a[i]); }
|
|
279
|
+
}
|
|
280
|
+
return r;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function repeat_value(val, n) {
|
|
284
|
+
return Array(n).fill(val);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Array Utilities ───────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
export function binary_search(arr, target, keyFn) {
|
|
290
|
+
let lo = 0, hi = arr.length - 1;
|
|
291
|
+
while (lo <= hi) {
|
|
292
|
+
const mid = (lo + hi) >> 1;
|
|
293
|
+
const val = keyFn ? keyFn(arr[mid]) : arr[mid];
|
|
294
|
+
if (val === target) return mid;
|
|
295
|
+
if (val < target) lo = mid + 1; else hi = mid - 1;
|
|
296
|
+
}
|
|
297
|
+
return -1;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function is_sorted(arr, keyFn) {
|
|
301
|
+
for (let i = 1; i < arr.length; i++) {
|
|
302
|
+
const a = keyFn ? keyFn(arr[i - 1]) : arr[i - 1];
|
|
303
|
+
const b = keyFn ? keyFn(arr[i]) : arr[i];
|
|
304
|
+
if (a > b) return false;
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function compact(arr) {
|
|
310
|
+
return arr.filter(v => v != null);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function rotate(arr, n) {
|
|
314
|
+
if (arr.length === 0) return [];
|
|
315
|
+
const k = ((n % arr.length) + arr.length) % arr.length;
|
|
316
|
+
return [...arr.slice(k), ...arr.slice(0, k)];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function insert_at(arr, idx, val) {
|
|
320
|
+
const r = [...arr];
|
|
321
|
+
r.splice(idx, 0, val);
|
|
322
|
+
return r;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function remove_at(arr, idx) {
|
|
326
|
+
const r = [...arr];
|
|
327
|
+
r.splice(idx, 1);
|
|
328
|
+
return r;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function update_at(arr, idx, val) {
|
|
332
|
+
const r = [...arr];
|
|
333
|
+
r[idx] = val;
|
|
334
|
+
return r;
|
|
335
|
+
}
|
package/src/stdlib/core.js
CHANGED
|
@@ -96,3 +96,90 @@ export function any(arr, fn) {
|
|
|
96
96
|
export function all(arr, fn) {
|
|
97
97
|
return arr.every(fn || Boolean);
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
// ── Randomness ──────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
export function random_int(lo, hi) {
|
|
103
|
+
return Math.floor(Math.random() * (hi - lo + 1)) + lo;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function random_float(lo, hi) {
|
|
107
|
+
return Math.random() * (hi - lo) + lo;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function choice(arr) {
|
|
111
|
+
return arr.length === 0 ? null : arr[Math.floor(Math.random() * arr.length)];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function sample(arr, n) {
|
|
115
|
+
const c = [...arr];
|
|
116
|
+
for (let i = c.length - 1; i > 0; i--) {
|
|
117
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
118
|
+
[c[i], c[j]] = [c[j], c[i]];
|
|
119
|
+
}
|
|
120
|
+
return c.slice(0, n);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function shuffle(arr) {
|
|
124
|
+
const c = [...arr];
|
|
125
|
+
for (let i = c.length - 1; i > 0; i--) {
|
|
126
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
127
|
+
[c[i], c[j]] = [c[j], c[i]];
|
|
128
|
+
}
|
|
129
|
+
return c;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Type Conversion ─────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
export function to_int(v) {
|
|
135
|
+
if (typeof v === 'boolean') return v ? 1 : 0;
|
|
136
|
+
const n = typeof v === 'string' ? parseInt(v, 10) : Math.trunc(Number(v));
|
|
137
|
+
return isNaN(n) ? null : n;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function to_float(v) {
|
|
141
|
+
if (typeof v === 'boolean') return v ? 1.0 : 0.0;
|
|
142
|
+
const n = Number(v);
|
|
143
|
+
return isNaN(n) ? null : n;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function to_string(v) {
|
|
147
|
+
if (v == null) return 'nil';
|
|
148
|
+
if (v && v.__tag) return v.__tag + (v.value !== undefined ? '(' + String(v.value) + ')' : '');
|
|
149
|
+
return String(v);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function to_bool(v) {
|
|
153
|
+
if (typeof v === 'string') return v !== '' && v !== '0' && v !== 'false';
|
|
154
|
+
return Boolean(v);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── General Utilities ────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
export function is_empty(v) {
|
|
160
|
+
if (v == null) return true;
|
|
161
|
+
if (typeof v === 'string' || Array.isArray(v)) return v.length === 0;
|
|
162
|
+
if (typeof v === 'object') return Object.keys(v).length === 0;
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Date/Time ────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
export function now() {
|
|
169
|
+
return Date.now();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function now_iso() {
|
|
173
|
+
return new Date().toISOString();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── UUID ──────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
export function uuid() {
|
|
179
|
+
return typeof crypto !== 'undefined' && crypto.randomUUID
|
|
180
|
+
? crypto.randomUUID()
|
|
181
|
+
: 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
182
|
+
var r = Math.random() * 16 | 0;
|
|
183
|
+
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Tova standard library — date/time utilities
|
|
2
|
+
|
|
3
|
+
export function date_parse(s) {
|
|
4
|
+
const d = new Date(s);
|
|
5
|
+
if (isNaN(d.getTime())) {
|
|
6
|
+
return { __tag: 'Err', error: 'Invalid date: ' + s, map(_) { return this; }, unwrap() { throw new Error(this.error); }, isOk() { return false; }, isErr() { return true; } };
|
|
7
|
+
}
|
|
8
|
+
return { __tag: 'Ok', value: d, map(fn) { return { __tag: 'Ok', value: fn(d), unwrap() { return fn(d); }, isOk() { return true; }, isErr() { return false; } }; }, unwrap() { return d; }, isOk() { return true; }, isErr() { return false; } };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function date_format(d, fmt) {
|
|
12
|
+
if (typeof d === 'number') d = new Date(d);
|
|
13
|
+
if (fmt === 'iso') return d.toISOString();
|
|
14
|
+
if (fmt === 'date') return d.toISOString().slice(0, 10);
|
|
15
|
+
if (fmt === 'time') return d.toTimeString().slice(0, 8);
|
|
16
|
+
if (fmt === 'datetime') return d.toISOString().slice(0, 10) + ' ' + d.toTimeString().slice(0, 8);
|
|
17
|
+
return fmt
|
|
18
|
+
.replace('YYYY', String(d.getFullYear()))
|
|
19
|
+
.replace('MM', String(d.getMonth() + 1).padStart(2, '0'))
|
|
20
|
+
.replace('DD', String(d.getDate()).padStart(2, '0'))
|
|
21
|
+
.replace('HH', String(d.getHours()).padStart(2, '0'))
|
|
22
|
+
.replace('mm', String(d.getMinutes()).padStart(2, '0'))
|
|
23
|
+
.replace('ss', String(d.getSeconds()).padStart(2, '0'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function date_add(d, amount, unit) {
|
|
27
|
+
if (typeof d === 'number') d = new Date(d);
|
|
28
|
+
const r = new Date(d.getTime());
|
|
29
|
+
if (unit === 'years') r.setFullYear(r.getFullYear() + amount);
|
|
30
|
+
else if (unit === 'months') r.setMonth(r.getMonth() + amount);
|
|
31
|
+
else if (unit === 'days') r.setDate(r.getDate() + amount);
|
|
32
|
+
else if (unit === 'hours') r.setHours(r.getHours() + amount);
|
|
33
|
+
else if (unit === 'minutes') r.setMinutes(r.getMinutes() + amount);
|
|
34
|
+
else if (unit === 'seconds') r.setSeconds(r.getSeconds() + amount);
|
|
35
|
+
return r;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function date_diff(d1, d2, unit) {
|
|
39
|
+
if (typeof d1 === 'number') d1 = new Date(d1);
|
|
40
|
+
if (typeof d2 === 'number') d2 = new Date(d2);
|
|
41
|
+
const ms = d2.getTime() - d1.getTime();
|
|
42
|
+
if (unit === 'seconds') return Math.floor(ms / 1000);
|
|
43
|
+
if (unit === 'minutes') return Math.floor(ms / 60000);
|
|
44
|
+
if (unit === 'hours') return Math.floor(ms / 3600000);
|
|
45
|
+
if (unit === 'days') return Math.floor(ms / 86400000);
|
|
46
|
+
if (unit === 'months') return (d2.getFullYear() - d1.getFullYear()) * 12 + (d2.getMonth() - d1.getMonth());
|
|
47
|
+
if (unit === 'years') return d2.getFullYear() - d1.getFullYear();
|
|
48
|
+
return ms;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function date_from(parts) {
|
|
52
|
+
return new Date(
|
|
53
|
+
parts.year || 0,
|
|
54
|
+
(parts.month || 1) - 1,
|
|
55
|
+
parts.day || 1,
|
|
56
|
+
parts.hour || 0,
|
|
57
|
+
parts.minute || 0,
|
|
58
|
+
parts.second || 0
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function date_part(d, part) {
|
|
63
|
+
if (typeof d === 'number') d = new Date(d);
|
|
64
|
+
if (part === 'year') return d.getFullYear();
|
|
65
|
+
if (part === 'month') return d.getMonth() + 1;
|
|
66
|
+
if (part === 'day') return d.getDate();
|
|
67
|
+
if (part === 'hour') return d.getHours();
|
|
68
|
+
if (part === 'minute') return d.getMinutes();
|
|
69
|
+
if (part === 'second') return d.getSeconds();
|
|
70
|
+
if (part === 'weekday') return d.getDay();
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function time_ago(d) {
|
|
75
|
+
if (typeof d === 'number') d = new Date(d);
|
|
76
|
+
const s = Math.floor((Date.now() - d.getTime()) / 1000);
|
|
77
|
+
if (s < 60) return s + ' seconds ago';
|
|
78
|
+
const m = Math.floor(s / 60);
|
|
79
|
+
if (m < 60) return m + (m === 1 ? ' minute ago' : ' minutes ago');
|
|
80
|
+
const h = Math.floor(m / 60);
|
|
81
|
+
if (h < 24) return h + (h === 1 ? ' hour ago' : ' hours ago');
|
|
82
|
+
const dy = Math.floor(h / 24);
|
|
83
|
+
if (dy < 30) return dy + (dy === 1 ? ' day ago' : ' days ago');
|
|
84
|
+
const mo = Math.floor(dy / 30);
|
|
85
|
+
if (mo < 12) return mo + (mo === 1 ? ' month ago' : ' months ago');
|
|
86
|
+
const yr = Math.floor(mo / 12);
|
|
87
|
+
return yr + (yr === 1 ? ' year ago' : ' years ago');
|
|
88
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Tova standard library — encoding utilities
|
|
2
|
+
|
|
3
|
+
export function base64_encode(s) {
|
|
4
|
+
return typeof btoa === 'function'
|
|
5
|
+
? btoa(unescape(encodeURIComponent(s)))
|
|
6
|
+
: Buffer.from(s, 'utf-8').toString('base64');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function base64_decode(s) {
|
|
10
|
+
return typeof atob === 'function'
|
|
11
|
+
? decodeURIComponent(escape(atob(s)))
|
|
12
|
+
: Buffer.from(s, 'base64').toString('utf-8');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function url_encode(s) {
|
|
16
|
+
return encodeURIComponent(s);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function url_decode(s) {
|
|
20
|
+
return decodeURIComponent(s);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── Hex Encoding ──────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export function hex_encode(s) {
|
|
26
|
+
let r = '';
|
|
27
|
+
for (let i = 0; i < s.length; i++) r += s.charCodeAt(i).toString(16).padStart(2, '0');
|
|
28
|
+
return r;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function hex_decode(s) {
|
|
32
|
+
let r = '';
|
|
33
|
+
for (let i = 0; i < s.length; i += 2) r += String.fromCharCode(parseInt(s.substr(i, 2), 16));
|
|
34
|
+
return r;
|
|
35
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Tova standard library — functional utilities
|
|
2
|
+
|
|
3
|
+
export function compose(...fns) {
|
|
4
|
+
return (x) => fns.reduceRight((v, fn) => fn(v), x);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function pipe_fn(...fns) {
|
|
8
|
+
return (x) => fns.reduce((v, fn) => fn(v), x);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function identity(x) {
|
|
12
|
+
return x;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function memoize(fn) {
|
|
16
|
+
const cache = new Map();
|
|
17
|
+
return function(...args) {
|
|
18
|
+
const key = JSON.stringify(args);
|
|
19
|
+
if (cache.has(key)) return cache.get(key);
|
|
20
|
+
const result = fn.apply(this, args);
|
|
21
|
+
cache.set(key, result);
|
|
22
|
+
return result;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function debounce(fn, ms) {
|
|
27
|
+
let timer;
|
|
28
|
+
return function(...args) {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
timer = setTimeout(() => fn.apply(this, args), ms);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function throttle(fn, ms) {
|
|
35
|
+
let last = 0;
|
|
36
|
+
return function(...args) {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
if (now - last >= ms) {
|
|
39
|
+
last = now;
|
|
40
|
+
return fn.apply(this, args);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function once(fn) {
|
|
46
|
+
let called = false, result;
|
|
47
|
+
return function(...args) {
|
|
48
|
+
if (!called) {
|
|
49
|
+
called = true;
|
|
50
|
+
result = fn.apply(this, args);
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function negate(fn) {
|
|
57
|
+
return function(...args) {
|
|
58
|
+
return !fn.apply(this, args);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Extended Functional ───────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export function partial(fn, ...bound) {
|
|
65
|
+
return function(...args) {
|
|
66
|
+
return fn(...bound, ...args);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function curry(fn, arity) {
|
|
71
|
+
const n = arity || fn.length;
|
|
72
|
+
return function curried(...args) {
|
|
73
|
+
if (args.length >= n) return fn(...args);
|
|
74
|
+
return function(...more) { return curried(...args, ...more); };
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function flip(fn) {
|
|
79
|
+
return function(a, b, ...rest) {
|
|
80
|
+
return fn(b, a, ...rest);
|
|
81
|
+
};
|
|
82
|
+
}
|