@jackens/nnn 2023.9.18 → 2023.9.23
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/nnn.js +919 -0
- package/package.json +1 -1
package/nnn.js
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
|
|
3
|
+
export const _test = {}
|
|
4
|
+
export const _version = '2023.9.23'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ```ts
|
|
8
|
+
* export type EscapeMap = Map<any, (value?: any) => string>;
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
/**
|
|
2
13
|
* @typedef {Map<any, (value?: any) => string>} EscapeMap
|
|
14
|
+
*
|
|
15
|
+
* The type of arguments of the `escapeValues` and `escape` helpers.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* ```ts
|
|
20
|
+
* export type HArgs = [
|
|
21
|
+
* string | Node,
|
|
22
|
+
* ...(Record<string, any> | null | undefined | Node | string | number | HArgs)[]
|
|
23
|
+
* ];
|
|
24
|
+
* ```
|
|
3
25
|
*/
|
|
4
26
|
|
|
5
27
|
/**
|
|
@@ -7,21 +29,84 @@
|
|
|
7
29
|
* string | Node,
|
|
8
30
|
* ...(Record<string, any> | null | undefined | Node | string | number | HArgs)[]
|
|
9
31
|
* ]} HArgs
|
|
32
|
+
*
|
|
33
|
+
* The type of arguments of the `h` and `s` helpers.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* ```ts
|
|
38
|
+
* export type JcssNode = {
|
|
39
|
+
* [attributeOrSelector: string]: string | number | JcssNode;
|
|
40
|
+
* };
|
|
41
|
+
* ```
|
|
10
42
|
*/
|
|
11
43
|
|
|
12
44
|
/**
|
|
13
45
|
* @typedef {{
|
|
14
46
|
* [attributeOrSelector: string]: string | number | JcssNode;
|
|
15
47
|
* }} JcssNode
|
|
48
|
+
*
|
|
49
|
+
* The type of arguments of the `jcss` helper.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* ```ts
|
|
54
|
+
* export type JcssRoot = Record<string, JcssNode>;
|
|
55
|
+
* ```
|
|
16
56
|
*/
|
|
17
57
|
|
|
18
58
|
/**
|
|
19
59
|
* @typedef {Record<string, JcssNode>} JcssRoot
|
|
60
|
+
*
|
|
61
|
+
* The type of arguments of the `jcss` helper.
|
|
20
62
|
*/
|
|
21
63
|
|
|
22
64
|
const _COLORS = ['#e22', '#e73', '#fc3', '#ad4', '#4d9', '#3be', '#45d', '#c3e']
|
|
23
65
|
|
|
24
66
|
/**
|
|
67
|
+
* ```ts
|
|
68
|
+
* export function chartable(options?: {
|
|
69
|
+
* bottom?: number;
|
|
70
|
+
* gapX?: number;
|
|
71
|
+
* gapY?: number;
|
|
72
|
+
* headerColumn?: boolean;
|
|
73
|
+
* headerRow?: boolean;
|
|
74
|
+
* id?: string;
|
|
75
|
+
* left?: number;
|
|
76
|
+
* maxY?: number;
|
|
77
|
+
* right?: number;
|
|
78
|
+
* singleScale?: boolean;
|
|
79
|
+
* table?: HTMLTableElement;
|
|
80
|
+
* title?: string;
|
|
81
|
+
* top?: number;
|
|
82
|
+
* xLabels?: string[];
|
|
83
|
+
* zLabels?: string[];
|
|
84
|
+
* zxY?: number[][];
|
|
85
|
+
* }): SVGSVGElement;
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A helper for creating a chart based on a table (conf. <https://jackens.github.io/nnn/chartable/>).
|
|
91
|
+
*
|
|
92
|
+
* Options:
|
|
93
|
+
* - `bottom`: bottom padding (for X axis labels)
|
|
94
|
+
* - `gapX`: X axis spacing
|
|
95
|
+
* - `gapY`: Y axis spacing
|
|
96
|
+
* - `headerColumn`: flag indicating that `table` has a header column (with X axis labels)
|
|
97
|
+
* - `headerRow`: flag indicating that `table` has a header row (with data series labels)
|
|
98
|
+
* - `id`: chart id
|
|
99
|
+
* - `left`: left padding (for data series labels)
|
|
100
|
+
* - `maxY`: number of Y axis lines
|
|
101
|
+
* - `right`: right padding (for data series labels)
|
|
102
|
+
* - `singleScale`: flag to force single scale
|
|
103
|
+
* - `table`: `HTMLTableElement` to extract data, data series labels and X axis labels
|
|
104
|
+
* - `title`: chart title
|
|
105
|
+
* - `top`: top padding (for the title)
|
|
106
|
+
* - `xLabels`: X axis labels
|
|
107
|
+
* - `zLabels`: data series labels
|
|
108
|
+
* - `zxY`: chart data
|
|
109
|
+
*
|
|
25
110
|
* @param {{
|
|
26
111
|
* bottom?: number;
|
|
27
112
|
* gapX?: number;
|
|
@@ -211,6 +296,15 @@ export const chartable = ({
|
|
|
211
296
|
)
|
|
212
297
|
}
|
|
213
298
|
|
|
299
|
+
/**
|
|
300
|
+
* ```ts
|
|
301
|
+
* export function eq(x: any, y: any): boolean;
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* A helper that checks equality of the given arguments.
|
|
307
|
+
*/
|
|
214
308
|
export const eq = /** @returns {boolean} */ (/** @type {any} */ x, /** @type {any} */ y) => {
|
|
215
309
|
if (x === y) {
|
|
216
310
|
return true
|
|
@@ -253,12 +347,93 @@ export const eq = /** @returns {boolean} */ (/** @type {any} */ x, /** @type {an
|
|
|
253
347
|
return false
|
|
254
348
|
}
|
|
255
349
|
|
|
350
|
+
_test.eq = () => {
|
|
351
|
+
console.assert(eq(true, true))
|
|
352
|
+
console.assert(eq(NaN, NaN))
|
|
353
|
+
console.assert(!eq(null, undefined))
|
|
354
|
+
console.assert(eq(42, 42))
|
|
355
|
+
// eslint-disable-next-line no-new-wrappers
|
|
356
|
+
console.assert(eq(42, new Number(42)))
|
|
357
|
+
console.assert(eq(42, Number(42)))
|
|
358
|
+
// eslint-disable-next-line no-new-wrappers
|
|
359
|
+
console.assert(eq(new Number(42), Number(42)))
|
|
360
|
+
console.assert(!eq(42, '42'))
|
|
361
|
+
console.assert(eq('42', '42'))
|
|
362
|
+
// eslint-disable-next-line no-new-wrappers
|
|
363
|
+
console.assert(eq('42', new String('42')))
|
|
364
|
+
console.assert(eq('42', String('42')))
|
|
365
|
+
// eslint-disable-next-line no-new-wrappers
|
|
366
|
+
console.assert(eq(String('42'), new String('42')))
|
|
367
|
+
console.assert(eq(/42/, /42/))
|
|
368
|
+
console.assert(!eq(/42/, /42/g))
|
|
369
|
+
console.assert(eq(new Date(42), new Date(42)))
|
|
370
|
+
console.assert(!eq(new Date(), new Date(42)))
|
|
371
|
+
console.assert(eq({ j: '42', c: 42 }, { c: 42, j: '42' }))
|
|
372
|
+
console.assert(eq([42, '42'], [42, '42']))
|
|
373
|
+
console.assert(eq(new Set(['42', 42]), new Set([42, '42'])))
|
|
374
|
+
console.assert(!eq(new Set(['42', 42]), new Set([42])))
|
|
375
|
+
console.assert(!eq(new Set([42, undefined]), new Set([42])))
|
|
376
|
+
console.assert(eq(
|
|
377
|
+
new Map([[{ j: 42 }, { J: '42' }], [{ c: 42 }, { C: '42' }]]),
|
|
378
|
+
new Map([[{ c: 42 }, { C: '42' }], [{ j: 42 }, { J: '42' }]])
|
|
379
|
+
))
|
|
380
|
+
console.assert(!eq(
|
|
381
|
+
new Map([[{ j: 42 }, { J: '42' }], [{ c: 42 }, { C: '42' }]]),
|
|
382
|
+
new Map([[{ j: '42' }, { J: 42 }], [{ c: '42' }, { C: 42 }]])
|
|
383
|
+
))
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* ```ts
|
|
388
|
+
* export function escape(escapeMap: EscapeMap, template: TemplateStringsArray, ...values: any[]): string;
|
|
389
|
+
* ```
|
|
390
|
+
*/
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* A generic helper for escaping `values` by given `escapeMap` (in *TemplateStrings* flavor).
|
|
394
|
+
*/
|
|
256
395
|
export const escape = (
|
|
257
396
|
/** @type {EscapeMap} */ escapeMap,
|
|
258
397
|
/** @type {TemplateStringsArray} */ template,
|
|
259
398
|
/** @type {any[]} */ ...values
|
|
260
399
|
) => String.raw(template, ...escapeValues(escapeMap, values))
|
|
261
400
|
|
|
401
|
+
_test.escape = () => {
|
|
402
|
+
// @ts-expect-error
|
|
403
|
+
const /** @type {EscapeMap} */ escapeMap = new Map([
|
|
404
|
+
[undefined, () => 'NULL'],
|
|
405
|
+
[Array, (/** @type {any[]} */ values) => escapeValues(escapeMap, values).join(', ')],
|
|
406
|
+
[Boolean, (/** @type {boolean} */ value) => `b'${+value}'`],
|
|
407
|
+
[Date, (/** @type {Date} */ value) => `'${value.toISOString().replace(/^(.+)T(.+)\..*$/, '$1 $2')}'`],
|
|
408
|
+
[Number, (/** @type {number} */ value) => `${value}`],
|
|
409
|
+
[String, (/** @type {string} */ value) => `'${value.replace(/'/g, "''")}'`]
|
|
410
|
+
])
|
|
411
|
+
|
|
412
|
+
// @ts-expect-error
|
|
413
|
+
const sql = escape.bind(null, escapeMap)
|
|
414
|
+
|
|
415
|
+
const actual = sql`
|
|
416
|
+
SELECT *
|
|
417
|
+
FROM table_name
|
|
418
|
+
WHERE column_name IN (${[true, null, undefined, 42, '42', "4'2", /42/, new Date(323325e6)]})`
|
|
419
|
+
|
|
420
|
+
const expected = `
|
|
421
|
+
SELECT *
|
|
422
|
+
FROM table_name
|
|
423
|
+
WHERE column_name IN (b'1', NULL, NULL, 42, '42', '4''2', NULL, '1980-03-31 04:30:00')`
|
|
424
|
+
|
|
425
|
+
console.assert(actual === expected)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* ```ts
|
|
430
|
+
* export function escapeValues(escapeMap: EscapeMap, values: any[]): string[];
|
|
431
|
+
* ```
|
|
432
|
+
*/
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* A generic helper for escaping `values` by given `escapeMap`.
|
|
436
|
+
*/
|
|
262
437
|
export const escapeValues = (
|
|
263
438
|
/** @type {EscapeMap} */ escapeMap,
|
|
264
439
|
/** @type {any[]} */ values
|
|
@@ -266,6 +441,15 @@ export const escapeValues = (
|
|
|
266
441
|
|
|
267
442
|
const _TAGS_TO_SKIP = { IFRAME: 1, NOSCRIPT: 1, PRE: 1, SCRIPT: 1, STYLE: 1, TEXTAREA: 1 }
|
|
268
443
|
|
|
444
|
+
/**
|
|
445
|
+
* ```ts
|
|
446
|
+
* export function fixTypography(node: Node): void;
|
|
447
|
+
* ```
|
|
448
|
+
*/
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* A helper that implements typographic corrections specific to Polish typography.
|
|
452
|
+
*/
|
|
269
453
|
export const fixTypography = (/** @type {Node} */ node) => {
|
|
270
454
|
const /** @type {Node[]} */ queue = [node]
|
|
271
455
|
|
|
@@ -306,6 +490,25 @@ export const fixTypography = (/** @type {Node} */ node) => {
|
|
|
306
490
|
}
|
|
307
491
|
}
|
|
308
492
|
|
|
493
|
+
_test.fixTypography = () => {
|
|
494
|
+
const p = h('p', 'Pchnąć w tę łódź jeża lub ośm skrzyń fig (zob. https://pl.wikipedia.org/wiki/Pangram).')
|
|
495
|
+
|
|
496
|
+
fixTypography(p)
|
|
497
|
+
|
|
498
|
+
console.assert(p.innerHTML ===
|
|
499
|
+
'Pchnąć <span style="white-space:nowrap">w </span>tę łódź jeża lub ośm skrzyń fig ' +
|
|
500
|
+
'(zob. https://\u200Bpl.\u200Bwikipedia.\u200Borg/\u200Bwiki/\u200BPangram).')
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* ```ts
|
|
505
|
+
* export function get(defaultValue: any, ref: any, ...keys: (string | number | symbol)[]): any;
|
|
506
|
+
* ```
|
|
507
|
+
*/
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* A convenient helper for getting values of nested objects.
|
|
511
|
+
*/
|
|
309
512
|
export const get = (
|
|
310
513
|
/** @type {any} */ defaultValue,
|
|
311
514
|
/** @type {any} */ ref,
|
|
@@ -322,6 +525,23 @@ export const get = (
|
|
|
322
525
|
return ref
|
|
323
526
|
}
|
|
324
527
|
|
|
528
|
+
_test.get = () => {
|
|
529
|
+
const ref = { one: { two: { 3: 'value' } } }
|
|
530
|
+
|
|
531
|
+
console.assert(get('default', ref, 'one', 'two', 3) === 'value')
|
|
532
|
+
|
|
533
|
+
// @ts-expect-error
|
|
534
|
+
ref.one.two[3] = undefined
|
|
535
|
+
|
|
536
|
+
console.assert(get('default', ref, 'one', 'two', 3) === undefined)
|
|
537
|
+
|
|
538
|
+
// @ts-expect-error
|
|
539
|
+
ref.one.two = {}
|
|
540
|
+
|
|
541
|
+
console.assert(get('default', ref, 'one', 'two', 3) === 'default')
|
|
542
|
+
console.assert(get('default', ref) === ref)
|
|
543
|
+
}
|
|
544
|
+
|
|
325
545
|
const /** @type {Record<string, string>} */ _NS = {
|
|
326
546
|
xlink: 'http://www.w3.org/1999/xlink'
|
|
327
547
|
}
|
|
@@ -416,12 +636,158 @@ const _h = (/** @type {string?=} */ namespaceURI) => {
|
|
|
416
636
|
return h
|
|
417
637
|
}
|
|
418
638
|
|
|
639
|
+
/**
|
|
640
|
+
* ```ts
|
|
641
|
+
* export function h<T extends keyof HTMLElementTagNameMap>(tag: T, ...args: HArgs[1][]): HTMLElementTagNameMap[T];
|
|
642
|
+
* export function h<N extends Node>(node: N, ...args: HArgs[1][]): N;
|
|
643
|
+
* export function h(...args: HArgs): Node;
|
|
644
|
+
* ```
|
|
645
|
+
*/
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* A lightweight [HyperScript](https://github.com/hyperhype/hyperscript)-style helper for creating and modifying `HTMLElement`s (see also `s`).
|
|
649
|
+
*
|
|
650
|
+
* - The first argument of type `string` specifies the tag of the element to be created.
|
|
651
|
+
* - The first argument of type `Node` specifies the element to be modified.
|
|
652
|
+
* - All other arguments of type `Record<string, any>` are mappings of attributes and properties.
|
|
653
|
+
* Keys starting with `$` specify *properties* (without the leading `$`) to be set on the element being created or modified.
|
|
654
|
+
* (Note that `$` is not a valid attribute name character.)
|
|
655
|
+
* All other keys specify *attributes* to be set by `setAttribute`.
|
|
656
|
+
* An attribute equal to `false` causes the attribute to be removed by `removeAttribute`.
|
|
657
|
+
* - All other arguments of type `null` or `undefined` are simply ignored.
|
|
658
|
+
* - All other arguments of type `Node` are appended to the element being created or modified.
|
|
659
|
+
* - All other arguments of type `string`/`number` are converted to `Text` nodes and appended to the element being created or modified.
|
|
660
|
+
* - All other arguments of type `HArgs` are passed to `h` and the results are appended to the element being created or modified.
|
|
661
|
+
*/
|
|
419
662
|
export const h = _h()
|
|
420
663
|
|
|
664
|
+
_test.h = () => {
|
|
665
|
+
const b = h('b')
|
|
666
|
+
|
|
667
|
+
console.assert(b.outerHTML === '<b></b>')
|
|
668
|
+
|
|
669
|
+
const i = h('i', 'text')
|
|
670
|
+
|
|
671
|
+
h(b, i)
|
|
672
|
+
|
|
673
|
+
console.assert(i.outerHTML === '<i>text</i>')
|
|
674
|
+
console.assert(b.outerHTML === '<b><i>text</i></b>')
|
|
675
|
+
|
|
676
|
+
h(i, { $className: 'some class' })
|
|
677
|
+
|
|
678
|
+
console.assert(i.outerHTML === '<i class="some class">text</i>')
|
|
679
|
+
console.assert(b.outerHTML === '<b><i class="some class">text</i></b>')
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
_test['h: innerText vs items'] = () => {
|
|
683
|
+
console.assert(h('span', 'text').outerHTML === '<span>text</span>')
|
|
684
|
+
console.assert(h('span', { $innerText: 'text' }).outerHTML === '<span>text</span>')
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
_test['h: style'] = () => {
|
|
688
|
+
console.assert(h('div', { style: 'margin:0;padding:0' }).outerHTML ===
|
|
689
|
+
'<div style="margin:0;padding:0"></div>')
|
|
690
|
+
console.assert(h('div', { $style: 'margin:0;padding:0' }).outerHTML ===
|
|
691
|
+
'<div style="margin: 0px; padding: 0px;"></div>')
|
|
692
|
+
console.assert(h('div', { $style: { margin: 0, padding: 0 } }).outerHTML ===
|
|
693
|
+
'<div style="margin: 0px; padding: 0px;"></div>')
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
_test['h: attributes vs properties'] = () => {
|
|
697
|
+
const input1 = h('input', { value: 42 })
|
|
698
|
+
const input2 = h('input', { $value: '42' })
|
|
699
|
+
|
|
700
|
+
console.assert(input1.value === '42')
|
|
701
|
+
console.assert(input2.value === '42')
|
|
702
|
+
|
|
703
|
+
console.assert(input1.outerHTML === '<input value="42">')
|
|
704
|
+
console.assert(input2.outerHTML === '<input>')
|
|
705
|
+
|
|
706
|
+
const checkbox1 = h('input', { type: 'checkbox', checked: true })
|
|
707
|
+
const checkbox2 = h('input', { type: 'checkbox', $checked: true })
|
|
708
|
+
|
|
709
|
+
console.assert(checkbox1.checked === true)
|
|
710
|
+
console.assert(checkbox2.checked === true)
|
|
711
|
+
|
|
712
|
+
console.assert(checkbox1.outerHTML === '<input type="checkbox" checked="">')
|
|
713
|
+
console.assert(checkbox2.outerHTML === '<input type="checkbox">')
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
_test['h: nested properties'] = () => {
|
|
717
|
+
const div = h('div')
|
|
718
|
+
|
|
719
|
+
// @ts-expect-error
|
|
720
|
+
console.assert(div.key === undefined)
|
|
721
|
+
|
|
722
|
+
h(div, { $key: { one: 1 } })
|
|
723
|
+
|
|
724
|
+
// @ts-expect-error
|
|
725
|
+
console.assert(eq(div.key, { one: 1 }))
|
|
726
|
+
|
|
727
|
+
h(div, { $key: { two: 2 } })
|
|
728
|
+
|
|
729
|
+
// @ts-expect-error
|
|
730
|
+
console.assert(eq(div.key, { one: 1, two: 2 }))
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* ```ts
|
|
735
|
+
* export function has(key: any, ref: any): boolean;
|
|
736
|
+
* ```
|
|
737
|
+
*/
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* A replacement for the `in` operator (not to be confused with the `for-in` loop) that works properly.
|
|
741
|
+
*/
|
|
421
742
|
export const has = (/** @type {any} */ key, /** @type {any} */ ref) =>
|
|
422
743
|
(is(String, key) || is(Number, key) || is(Symbol, key)) && Object.hasOwnProperty.call(ref ?? Object, key)
|
|
423
744
|
|
|
745
|
+
_test.has = () => {
|
|
746
|
+
const obj = { key: 'K', null: 'N' }
|
|
747
|
+
|
|
748
|
+
console.assert('key' in obj)
|
|
749
|
+
console.assert(has('key', obj))
|
|
750
|
+
|
|
751
|
+
console.assert('null' in obj)
|
|
752
|
+
console.assert(has('null', obj))
|
|
753
|
+
|
|
754
|
+
// @ts-expect-error
|
|
755
|
+
console.assert(null in obj)
|
|
756
|
+
console.assert(!has(null, obj))
|
|
757
|
+
|
|
758
|
+
console.assert('toString' in obj)
|
|
759
|
+
console.assert(!has('toString', obj))
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
_test['has: null'] = () => {
|
|
763
|
+
let typeError
|
|
764
|
+
|
|
765
|
+
try {
|
|
766
|
+
// @ts-expect-error
|
|
767
|
+
console.assert('key' in null)
|
|
768
|
+
} catch (error) {
|
|
769
|
+
typeError = error
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
console.assert(typeError instanceof TypeError) // Cannot use 'in' operator to search for 'key' in null
|
|
773
|
+
console.assert(!has('key', null))
|
|
774
|
+
}
|
|
775
|
+
|
|
424
776
|
/**
|
|
777
|
+
* ```ts
|
|
778
|
+
* export function is(type: BigIntConstructor, arg: any): arg is bigint;
|
|
779
|
+
* export function is(type: BooleanConstructor, arg: any): arg is boolean;
|
|
780
|
+
* export function is(type: NumberConstructor, arg: any): arg is number;
|
|
781
|
+
* export function is(type: StringConstructor, arg: any): arg is string;
|
|
782
|
+
* export function is(type: SymbolConstructor, arg: any): arg is symbol;
|
|
783
|
+
* export function is(type: undefined, arg: any): arg is null | undefined;
|
|
784
|
+
* export function is<T extends abstract new (...args: any[]) => any>(type: T, arg: any): arg is InstanceType<T>;
|
|
785
|
+
* ```
|
|
786
|
+
*/
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* A helper that checks if the given argument is of a certain type.
|
|
790
|
+
*
|
|
425
791
|
* @template {abstract new (...args: any[]) => any} T
|
|
426
792
|
*
|
|
427
793
|
* @type {{
|
|
@@ -438,6 +804,39 @@ export const has = (/** @type {any} */ key, /** @type {any} */ ref) =>
|
|
|
438
804
|
*/
|
|
439
805
|
export const is = (/** @type {T} */ type, /** @type {any} */ arg) => arg?.constructor === type
|
|
440
806
|
|
|
807
|
+
_test.is = () => {
|
|
808
|
+
console.assert(is(Number, 42))
|
|
809
|
+
console.assert(is(Number, Number(42)))
|
|
810
|
+
// eslint-disable-next-line no-new-wrappers
|
|
811
|
+
console.assert(is(Number, new Number(42)))
|
|
812
|
+
console.assert(is(Number, NaN))
|
|
813
|
+
console.assert(is(String, '42'))
|
|
814
|
+
console.assert(is(String, String('42')))
|
|
815
|
+
// eslint-disable-next-line no-new-wrappers
|
|
816
|
+
console.assert(is(String, new String('42')))
|
|
817
|
+
console.assert(is(Symbol, Symbol('42')))
|
|
818
|
+
console.assert(is(Symbol, Object(Symbol('42'))))
|
|
819
|
+
console.assert(is(undefined, undefined))
|
|
820
|
+
console.assert(is(undefined, null))
|
|
821
|
+
console.assert(is(Object, {}))
|
|
822
|
+
console.assert(is(Array, []))
|
|
823
|
+
console.assert(is(RegExp, /42/))
|
|
824
|
+
console.assert(is(Date, new Date(42)))
|
|
825
|
+
console.assert(is(Set, new Set(['42', 42])))
|
|
826
|
+
console.assert(is(Map, new Map([[{ j: 42 }, { J: '42' }], [{ c: 42 }, { C: '42' }]])))
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
_test['is vs ‘toString.call’'] = () => {
|
|
830
|
+
class FooBar { }
|
|
831
|
+
|
|
832
|
+
console.assert(is(FooBar, new FooBar()))
|
|
833
|
+
|
|
834
|
+
const fakeFooBar = { [Symbol.toStringTag]: 'FooBar' }
|
|
835
|
+
|
|
836
|
+
console.assert(({}).toString.call(new FooBar()) === '[object Object]')
|
|
837
|
+
console.assert(({}).toString.call(fakeFooBar) === '[object FooBar]')
|
|
838
|
+
}
|
|
839
|
+
|
|
441
840
|
const _jcss = (
|
|
442
841
|
/** @type {JcssNode} */ node,
|
|
443
842
|
/** @type {string} */ prefix,
|
|
@@ -492,6 +891,23 @@ const _jcss = (
|
|
|
492
891
|
}
|
|
493
892
|
}
|
|
494
893
|
|
|
894
|
+
/**
|
|
895
|
+
* ```ts
|
|
896
|
+
* export function jcss(root: JcssRoot, splitter?: string): string;
|
|
897
|
+
* ```
|
|
898
|
+
*/
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* A simple CSS-in-JS helper.
|
|
902
|
+
*
|
|
903
|
+
* The `root` parameter provides a hierarchical description of CSS rules.
|
|
904
|
+
*
|
|
905
|
+
* - Keys of sub-objects whose values are NOT objects are treated as CSS attribute, and values are treated as values of those CSS attributes; the concatenation of keys of all parent objects is a CSS rule.
|
|
906
|
+
* - All keys ignore the part starting with a splitter (default: `$$`) sign until the end of the key (e.g. `src$$1` → `src`, `@font-face$$1` → `@font-face`).
|
|
907
|
+
* - In keys specifying CSS attribute, all uppercase letters are replaced by lowercase letters with an additional `-` character preceding them (e.g. `fontFamily` → `font-family`).
|
|
908
|
+
* - Commas in keys that makes a CSS rule cause it to “split” and create separate rules for each part (e.g. `{div:{margin:1,'.a,.b,.c':{margin:2}}}` → `div{margin:1}div.a,div.b,div.c{margin:2}`).
|
|
909
|
+
* - Top-level keys that begin with `@` are not concatenated with sub-object keys.
|
|
910
|
+
*/
|
|
495
911
|
export const jcss = (/** @type {JcssRoot} */ root, splitter = '$$') => {
|
|
496
912
|
const split = (/** @type {string} */ text) => text.split(splitter)[0]
|
|
497
913
|
const /** @type {string[]} */ result = []
|
|
@@ -511,6 +927,221 @@ export const jcss = (/** @type {JcssRoot} */ root, splitter = '$$') => {
|
|
|
511
927
|
return result.join('')
|
|
512
928
|
}
|
|
513
929
|
|
|
930
|
+
_test['jcss: #1'] = () => {
|
|
931
|
+
const actual = jcss({
|
|
932
|
+
a: {
|
|
933
|
+
color: 'red',
|
|
934
|
+
margin: 1,
|
|
935
|
+
'.c': { margin: 2, padding: 2 },
|
|
936
|
+
padding: 1
|
|
937
|
+
}
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
const expected = `
|
|
941
|
+
a{
|
|
942
|
+
color:red;
|
|
943
|
+
margin:1
|
|
944
|
+
}
|
|
945
|
+
a.c{
|
|
946
|
+
margin:2;
|
|
947
|
+
padding:2
|
|
948
|
+
}
|
|
949
|
+
a{
|
|
950
|
+
padding:1
|
|
951
|
+
}`.replace(/\n\s*/g, '')
|
|
952
|
+
|
|
953
|
+
console.assert(actual === expected)
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
_test['jcss: #2'] = () => {
|
|
957
|
+
const actual = jcss({
|
|
958
|
+
a: {
|
|
959
|
+
'.b': {
|
|
960
|
+
color: 'red',
|
|
961
|
+
margin: 1,
|
|
962
|
+
'.c': { margin: 2, padding: 2 },
|
|
963
|
+
padding: 1
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
})
|
|
967
|
+
|
|
968
|
+
const expected = `
|
|
969
|
+
a.b{
|
|
970
|
+
color:red;
|
|
971
|
+
margin:1
|
|
972
|
+
}
|
|
973
|
+
a.b.c{
|
|
974
|
+
margin:2;
|
|
975
|
+
padding:2
|
|
976
|
+
}
|
|
977
|
+
a.b{
|
|
978
|
+
padding:1
|
|
979
|
+
}`.replace(/\n\s*/g, '')
|
|
980
|
+
|
|
981
|
+
console.assert(actual === expected)
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
_test['jcss: #3'] = () => {
|
|
985
|
+
const actual = jcss({
|
|
986
|
+
'@font-face$$1': {
|
|
987
|
+
fontFamily: 'Jackens',
|
|
988
|
+
src$$1: 'url(otf/jackens.otf)',
|
|
989
|
+
src$$2: "url(otf/jackens.otf) format('opentype')," +
|
|
990
|
+
"url(svg/jackens.svg) format('svg')",
|
|
991
|
+
fontWeight: 'normal',
|
|
992
|
+
fontStyle: 'normal'
|
|
993
|
+
},
|
|
994
|
+
'@font-face$$2': {
|
|
995
|
+
fontFamily: 'C64',
|
|
996
|
+
src: 'url(fonts/C64_Pro_Mono-STYLE.woff)'
|
|
997
|
+
},
|
|
998
|
+
'@keyframes spin': {
|
|
999
|
+
'0%': { transform: 'rotate(0deg)' },
|
|
1000
|
+
'100%': { transform: 'rotate(360deg)' }
|
|
1001
|
+
},
|
|
1002
|
+
div: {
|
|
1003
|
+
border: 'solid red 1px',
|
|
1004
|
+
'.c1': { 'background-color': '#000' },
|
|
1005
|
+
' .c1': { backgroundColor: 'black' },
|
|
1006
|
+
'.c2': { backgroundColor: 'rgb(0,0,0)' }
|
|
1007
|
+
},
|
|
1008
|
+
'@media(min-width:200px)': {
|
|
1009
|
+
div: { margin: 0, padding: 0 },
|
|
1010
|
+
span: { color: '#000' }
|
|
1011
|
+
}
|
|
1012
|
+
})
|
|
1013
|
+
|
|
1014
|
+
const expected = `
|
|
1015
|
+
@font-face{
|
|
1016
|
+
font-family:Jackens;
|
|
1017
|
+
src:url(otf/jackens.otf);
|
|
1018
|
+
src:url(otf/jackens.otf) format('opentype'),url(svg/jackens.svg) format('svg');
|
|
1019
|
+
font-weight:normal;
|
|
1020
|
+
font-style:normal
|
|
1021
|
+
}
|
|
1022
|
+
@font-face{
|
|
1023
|
+
font-family:C64;
|
|
1024
|
+
src:url(fonts/C64_Pro_Mono-STYLE.woff)
|
|
1025
|
+
}
|
|
1026
|
+
@keyframes spin{
|
|
1027
|
+
0%{
|
|
1028
|
+
transform:rotate(0deg)
|
|
1029
|
+
}
|
|
1030
|
+
100%{
|
|
1031
|
+
transform:rotate(360deg)
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
div{
|
|
1035
|
+
border:solid red 1px
|
|
1036
|
+
}
|
|
1037
|
+
div.c1{
|
|
1038
|
+
background-color:#000
|
|
1039
|
+
}
|
|
1040
|
+
div .c1{
|
|
1041
|
+
background-color:black
|
|
1042
|
+
}
|
|
1043
|
+
div.c2{
|
|
1044
|
+
background-color:rgb(0,0,0)
|
|
1045
|
+
}
|
|
1046
|
+
@media(min-width:200px){
|
|
1047
|
+
div{
|
|
1048
|
+
margin:0;
|
|
1049
|
+
padding:0
|
|
1050
|
+
}
|
|
1051
|
+
span{
|
|
1052
|
+
color:#000
|
|
1053
|
+
}
|
|
1054
|
+
}`.replace(/\n\s*/g, '')
|
|
1055
|
+
|
|
1056
|
+
console.assert(actual === expected)
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
_test['jcss: #4'] = () => {
|
|
1060
|
+
const actual = jcss({
|
|
1061
|
+
a: {
|
|
1062
|
+
'.b,.c': {
|
|
1063
|
+
margin: 1,
|
|
1064
|
+
'.d': {
|
|
1065
|
+
margin: 2
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
})
|
|
1070
|
+
|
|
1071
|
+
const expected = `
|
|
1072
|
+
a.b,a.c{
|
|
1073
|
+
margin:1
|
|
1074
|
+
}
|
|
1075
|
+
a.b.d,a.c.d{
|
|
1076
|
+
margin:2
|
|
1077
|
+
}`.replace(/\n\s*/g, '')
|
|
1078
|
+
|
|
1079
|
+
console.assert(actual === expected)
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
_test['jcss: #5'] = () => {
|
|
1083
|
+
const actual = jcss({
|
|
1084
|
+
'.b,.c': {
|
|
1085
|
+
margin: 1,
|
|
1086
|
+
'.d': {
|
|
1087
|
+
margin: 2
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
})
|
|
1091
|
+
|
|
1092
|
+
const expected = `
|
|
1093
|
+
.b,.c{
|
|
1094
|
+
margin:1
|
|
1095
|
+
}
|
|
1096
|
+
.b.d,.c.d{
|
|
1097
|
+
margin:2
|
|
1098
|
+
}`.replace(/\n\s*/g, '')
|
|
1099
|
+
|
|
1100
|
+
console.assert(actual === expected)
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
_test['jcss: #6'] = () => {
|
|
1104
|
+
const actual = jcss({
|
|
1105
|
+
'.a,.b': {
|
|
1106
|
+
margin: 1,
|
|
1107
|
+
'.c,.d': {
|
|
1108
|
+
margin: 2
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
})
|
|
1112
|
+
|
|
1113
|
+
const expected = `
|
|
1114
|
+
.a,.b{
|
|
1115
|
+
margin:1
|
|
1116
|
+
}
|
|
1117
|
+
.a.c,.a.d,.b.c,.b.d{
|
|
1118
|
+
margin:2
|
|
1119
|
+
}`.replace(/\n\s*/g, '')
|
|
1120
|
+
|
|
1121
|
+
console.assert(actual === expected)
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* ```ts
|
|
1126
|
+
* export function jsOnParse(handlers: Record<string, Function>, text: string): any;
|
|
1127
|
+
* ```
|
|
1128
|
+
*/
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* `JSON.parse` with “JavaScript turned on”.
|
|
1132
|
+
*
|
|
1133
|
+
* Objects having *exactly* one property which is present in the `handlers` map, i.e. objects of the form:
|
|
1134
|
+
*
|
|
1135
|
+
* ```js
|
|
1136
|
+
* { "«handlerName»": [«params»] }
|
|
1137
|
+
* ```
|
|
1138
|
+
*
|
|
1139
|
+
* are replaced by the result of call
|
|
1140
|
+
*
|
|
1141
|
+
* ```js
|
|
1142
|
+
* handlers['«handlerName»'](...«params»)
|
|
1143
|
+
* ```
|
|
1144
|
+
*/
|
|
514
1145
|
export const jsOnParse = (
|
|
515
1146
|
/** @type {Record<string, Function>} */ handlers,
|
|
516
1147
|
/** @type {string} */ text
|
|
@@ -533,6 +1164,49 @@ export const jsOnParse = (
|
|
|
533
1164
|
return value
|
|
534
1165
|
})
|
|
535
1166
|
|
|
1167
|
+
_test.jsOnParse = () => {
|
|
1168
|
+
const handlers = {
|
|
1169
|
+
$hello: (/** @type {string} */ name) => `Hello ${name}!`,
|
|
1170
|
+
$foo: () => 'bar'
|
|
1171
|
+
}
|
|
1172
|
+
const actual = jsOnParse(handlers, `
|
|
1173
|
+
[
|
|
1174
|
+
{
|
|
1175
|
+
"$hello": ["World"]
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
"nested": {
|
|
1179
|
+
"$hello": ["nested World"]
|
|
1180
|
+
},
|
|
1181
|
+
"one": 1,
|
|
1182
|
+
"two": 2
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
"$foo": []
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
"$foo": ["The parent object does not have exactly one property!"],
|
|
1189
|
+
"one": 1,
|
|
1190
|
+
"two": 2
|
|
1191
|
+
}
|
|
1192
|
+
]`)
|
|
1193
|
+
const expected = [
|
|
1194
|
+
'Hello World!',
|
|
1195
|
+
{
|
|
1196
|
+
nested: 'Hello nested World!',
|
|
1197
|
+
one: 1,
|
|
1198
|
+
two: 2
|
|
1199
|
+
},
|
|
1200
|
+
'bar',
|
|
1201
|
+
{
|
|
1202
|
+
$foo: ['The parent object does not have exactly one property!'],
|
|
1203
|
+
one: 1,
|
|
1204
|
+
two: 2
|
|
1205
|
+
}]
|
|
1206
|
+
|
|
1207
|
+
console.assert(eq(actual, expected))
|
|
1208
|
+
}
|
|
1209
|
+
|
|
536
1210
|
const _locale = (
|
|
537
1211
|
/** @type {Record<string, Record<string, string | Record<string, string>>>} */ locales,
|
|
538
1212
|
/** @type {string} */ language,
|
|
@@ -551,6 +1225,19 @@ const _locale = (
|
|
|
551
1225
|
return is(String, t) ? t : text
|
|
552
1226
|
}
|
|
553
1227
|
|
|
1228
|
+
/**
|
|
1229
|
+
* ```ts
|
|
1230
|
+
* export function locale(
|
|
1231
|
+
* locales: Record<string, Record<string, string | Record<string, string>>>,
|
|
1232
|
+
* defaultLanguage: string,
|
|
1233
|
+
* languages?: readonly string[]
|
|
1234
|
+
* ): (text?: string, version?: string) => string;
|
|
1235
|
+
* ```
|
|
1236
|
+
*/
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Language translations helper.
|
|
1240
|
+
*/
|
|
554
1241
|
export const locale = (
|
|
555
1242
|
/** @type {Record<string, Record<string, string | Record<string, string>>>} */ locales,
|
|
556
1243
|
/** @type {string} */ defaultLanguage,
|
|
@@ -567,6 +1254,42 @@ export const locale = (
|
|
|
567
1254
|
return _locale.bind(0, locales, defaultLanguage)
|
|
568
1255
|
}
|
|
569
1256
|
|
|
1257
|
+
_test.locale = () => {
|
|
1258
|
+
const locales = {
|
|
1259
|
+
pl: {
|
|
1260
|
+
Password: 'Hasło',
|
|
1261
|
+
button: { Login: 'Zaloguj' }
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
const _ = locale(locales, 'pl', [])
|
|
1265
|
+
|
|
1266
|
+
console.assert(_('Login') === 'Login')
|
|
1267
|
+
console.assert(_('Password') === 'Hasło')
|
|
1268
|
+
|
|
1269
|
+
console.assert(_('Undefined text') === 'Undefined text')
|
|
1270
|
+
|
|
1271
|
+
console.assert(_('Login', 'button') === 'Zaloguj')
|
|
1272
|
+
|
|
1273
|
+
console.assert(_('Password', 'undefined_version') === 'Hasło')
|
|
1274
|
+
console.assert(_('Undefined text', 'undefined_version') === 'Undefined text')
|
|
1275
|
+
|
|
1276
|
+
console.assert(_('toString') === 'toString')
|
|
1277
|
+
console.assert(_('toString', 'undefined_version') === 'toString')
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* ```ts
|
|
1282
|
+
* export function nanolight(
|
|
1283
|
+
* pattern: RegExp,
|
|
1284
|
+
* highlighters: ((chunk: string, index: number) => HArgs[1])[],
|
|
1285
|
+
* code: string
|
|
1286
|
+
* ): HArgs[1][];
|
|
1287
|
+
* ```
|
|
1288
|
+
*/
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* A generic helper for syntax highlighting (see also `nanolightJs`).
|
|
1292
|
+
*/
|
|
570
1293
|
export const nanolight = (
|
|
571
1294
|
/** @type {RegExp} */ pattern,
|
|
572
1295
|
/** @type {((chunk: string, index: number) => HArgs[1])[]} */ highlighters,
|
|
@@ -584,6 +1307,15 @@ export const nanolight = (
|
|
|
584
1307
|
return result
|
|
585
1308
|
}
|
|
586
1309
|
|
|
1310
|
+
/**
|
|
1311
|
+
* ```ts
|
|
1312
|
+
* export function nanolightJs(codeJs: string): HArgs[1][];
|
|
1313
|
+
* ```
|
|
1314
|
+
*/
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* A helper for highlighting JavaScript.
|
|
1318
|
+
*/
|
|
587
1319
|
// @ts-expect-error
|
|
588
1320
|
export const nanolightJs = nanolight.bind(0,
|
|
589
1321
|
/('.*?'|".*?"|`[\s\S]*?`)|(\/\/.*?\n|\/\*[\s\S]*?\*\/)|(break|case|catch|const|continue|debugger|default|delete|do|else|eval|export\s+type|export|extends|false|finally|for|from|function|goto|if|import|in|instanceof|is|keyof|let|NaN|new|null|package|return|super|switch|this|throw|true|try|typeof|undefined|var|void|while|with|yield)(?!\w)|([<>=.?:&|!~*/%+-])|(0x[\dabcdef]+|0o[01234567]+|0b[01]+|\d+(?:\.[\d_]+)?(?:e[+-]?[\d_]+)?)|([$\w]+)(?=\()|([$\wąćęłńóśżźĄĆĘŁŃÓŚŻŹ]+)/,
|
|
@@ -599,6 +1331,22 @@ export const nanolightJs = nanolight.bind(0,
|
|
|
599
1331
|
]
|
|
600
1332
|
)
|
|
601
1333
|
|
|
1334
|
+
_test.nanolightJs = () => {
|
|
1335
|
+
const codeJs = 'const answerToLifeTheUniverseAndEverything = 42'
|
|
1336
|
+
|
|
1337
|
+
console.assert(h('pre', ['code', ...nanolightJs(codeJs)]).outerHTML ===
|
|
1338
|
+
'<pre><code><b>const</b> <i>answerToLifeTheUniverseAndEverything</i> <b>=</b> <u>42</u></code></pre>')
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* ```ts
|
|
1343
|
+
* export function plUral(singular: string, plural2: string, plural5: string, value: number): string;
|
|
1344
|
+
* ```
|
|
1345
|
+
*/
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* A helper for choosing the correct singular and plural.
|
|
1349
|
+
*/
|
|
602
1350
|
export const plUral = (
|
|
603
1351
|
/** @type {string} */ singular,
|
|
604
1352
|
/** @type {string} */ plural2,
|
|
@@ -617,6 +1365,35 @@ export const plUral = (
|
|
|
617
1365
|
: plural5
|
|
618
1366
|
}
|
|
619
1367
|
|
|
1368
|
+
_test.plUral = () => {
|
|
1369
|
+
// @ts-expect-error
|
|
1370
|
+
const auto = plUral.bind(null, 'auto', 'auta', 'aut')
|
|
1371
|
+
|
|
1372
|
+
console.assert(auto(0) === 'aut')
|
|
1373
|
+
console.assert(auto(1) === 'auto')
|
|
1374
|
+
console.assert(auto(17) === 'aut')
|
|
1375
|
+
console.assert(auto(42) === 'auta')
|
|
1376
|
+
|
|
1377
|
+
// @ts-expect-error
|
|
1378
|
+
const car = plUral.bind(null, 'car', 'cars', 'cars')
|
|
1379
|
+
|
|
1380
|
+
console.assert(car(0) === 'cars')
|
|
1381
|
+
console.assert(car(1) === 'car')
|
|
1382
|
+
console.assert(car(17) === 'cars')
|
|
1383
|
+
console.assert(car(42) === 'cars')
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* ```ts
|
|
1388
|
+
* export function refsInfo(...refs: any[]): [string, string, string[]][];
|
|
1389
|
+
* ```
|
|
1390
|
+
*/
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* A helper that provides information about the given `refs`.
|
|
1394
|
+
*
|
|
1395
|
+
* It returns an array of triples: `[«name», «prototype-name», «array-of-own-property-names»]`.
|
|
1396
|
+
*/
|
|
620
1397
|
export const refsInfo = (/** @type {any[]} */ ...refs) => {
|
|
621
1398
|
const /** @type {Set<Function>} */ fns = new Set()
|
|
622
1399
|
|
|
@@ -634,8 +1411,72 @@ export const refsInfo = (/** @type {any[]} */ ...refs) => {
|
|
|
634
1411
|
]).sort((a, b) => -(a[0] < b[0]))
|
|
635
1412
|
}
|
|
636
1413
|
|
|
1414
|
+
_test.refsInfo = () => {
|
|
1415
|
+
const info = refsInfo(Array, Function)
|
|
1416
|
+
|
|
1417
|
+
console.assert(info.find(([name]) => name === 'Array')?.[2]?.includes?.('length'))
|
|
1418
|
+
console.assert(info.find(([name]) => name === 'Function')?.[2]?.includes?.('length'))
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
_test['refsInfo: browserFingerprint'] = () => {
|
|
1422
|
+
const browserFingerprint = () => {
|
|
1423
|
+
// @ts-expect-error
|
|
1424
|
+
const refs = Object.getOwnPropertyNames(window).map(name => window[name])
|
|
1425
|
+
const info = refsInfo(...refs)
|
|
1426
|
+
const json = JSON.stringify(info)
|
|
1427
|
+
const hash = Array(32).fill(0)
|
|
1428
|
+
let j = 0
|
|
1429
|
+
|
|
1430
|
+
for (let i = 0; i < json.length; i++) {
|
|
1431
|
+
let charCode = json.charCodeAt(i)
|
|
1432
|
+
|
|
1433
|
+
while (charCode > 0) {
|
|
1434
|
+
hash[j] = hash[j] ^ (charCode & 15)
|
|
1435
|
+
charCode >>= 4
|
|
1436
|
+
j = (j + 1) & 31
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
return hash.map(x => x.toString(16)).join('')
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
console.log(browserFingerprint())
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* ```ts
|
|
1448
|
+
* export function s<T extends keyof SVGElementTagNameMap>(tag: T, ...args: HArgs[1][]): SVGElementTagNameMap[T];
|
|
1449
|
+
* export function s<N extends Node>(node: N, ...args: HArgs[1][]): N;
|
|
1450
|
+
* export function s(...args: HArgs): Node;
|
|
1451
|
+
* ```
|
|
1452
|
+
*/
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* A lightweight [HyperScript](https://github.com/hyperhype/hyperscript)-style helper for creating and modifying `SVGElement`s (see also `h`).
|
|
1456
|
+
*
|
|
1457
|
+
* - The first argument of type `string` specifies the tag of the element to be created.
|
|
1458
|
+
* - The first argument of type `Node` specifies the element to be modified.
|
|
1459
|
+
* - All other arguments of type `Record<string, any>` are mappings of attributes and properties.
|
|
1460
|
+
* Keys starting with `$` specify *properties* (without the leading `$`) to be set on the element being created or modified.
|
|
1461
|
+
* (Note that `$` is not a valid attribute name character.)
|
|
1462
|
+
* All other keys specify *attributes* to be set by `setAttributeNS`.
|
|
1463
|
+
* An attribute equal to `false` causes the attribute to be removed by `removeAttributeNS`.
|
|
1464
|
+
* - All other arguments of type `null` or `undefined` are simply ignored.
|
|
1465
|
+
* - All other arguments of type `Node` are appended to the element being created or modified.
|
|
1466
|
+
* - All other arguments of type `string`/`number` are converted to `Text` nodes and appended to the element being created or modified.
|
|
1467
|
+
* - All other arguments of type `HArgs` are passed to `s` and the results are appended to the element being created or modified.
|
|
1468
|
+
*/
|
|
637
1469
|
export const s = _h('http://www.w3.org/2000/svg')
|
|
638
1470
|
|
|
1471
|
+
/**
|
|
1472
|
+
* ```ts
|
|
1473
|
+
* export function set(value: any, ref: any, ...keys: (string | number | symbol)[]): void;
|
|
1474
|
+
* ```
|
|
1475
|
+
*/
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* A convenient helper for setting values of nested objects.
|
|
1479
|
+
*/
|
|
639
1480
|
export const set = (
|
|
640
1481
|
/** @type {any} */ value,
|
|
641
1482
|
/** @type {any} */ ref,
|
|
@@ -660,9 +1501,64 @@ export const set = (
|
|
|
660
1501
|
ref[keys[last]] = value
|
|
661
1502
|
}
|
|
662
1503
|
|
|
1504
|
+
_test.set = () => {
|
|
1505
|
+
const ref = {}
|
|
1506
|
+
|
|
1507
|
+
set('value', ref, 'one', 'two', 3, 4)
|
|
1508
|
+
|
|
1509
|
+
console.assert(eq(ref, {
|
|
1510
|
+
one: {
|
|
1511
|
+
two: [undefined, undefined, undefined, [
|
|
1512
|
+
undefined, undefined, undefined, undefined, 'value']
|
|
1513
|
+
]
|
|
1514
|
+
}
|
|
1515
|
+
}))
|
|
1516
|
+
|
|
1517
|
+
set(undefined, ref, 'one', 'two', 3, 4)
|
|
1518
|
+
|
|
1519
|
+
console.assert(eq(ref, {
|
|
1520
|
+
one: {
|
|
1521
|
+
two: [undefined, undefined, undefined, [
|
|
1522
|
+
undefined, undefined, undefined, undefined, undefined]
|
|
1523
|
+
]
|
|
1524
|
+
}
|
|
1525
|
+
}))
|
|
1526
|
+
|
|
1527
|
+
set({}, ref, 'one', 'two', 3)
|
|
1528
|
+
|
|
1529
|
+
console.assert(eq(ref, {
|
|
1530
|
+
one: {
|
|
1531
|
+
two: [undefined, undefined, undefined, {}]
|
|
1532
|
+
}
|
|
1533
|
+
}))
|
|
1534
|
+
|
|
1535
|
+
set('value', ref)
|
|
1536
|
+
|
|
1537
|
+
console.assert(eq(ref, {
|
|
1538
|
+
one: {
|
|
1539
|
+
two: [undefined, undefined, undefined, {}]
|
|
1540
|
+
}
|
|
1541
|
+
}))
|
|
1542
|
+
}
|
|
1543
|
+
|
|
663
1544
|
const _ZEROS = '0'.repeat(16)
|
|
664
1545
|
let _counter = 0
|
|
665
1546
|
|
|
1547
|
+
/**
|
|
1548
|
+
* ```ts
|
|
1549
|
+
* export function uuid1(options?: {
|
|
1550
|
+
* date?: Date;
|
|
1551
|
+
* node?: string;
|
|
1552
|
+
* }): string;
|
|
1553
|
+
* ```
|
|
1554
|
+
*/
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* A helper that generates a UUID v1 identifier (with a creation timestamp).
|
|
1558
|
+
*
|
|
1559
|
+
* - The optional `node` parameter should have the format `/^[0123456789abcdef]+$/`.
|
|
1560
|
+
* Its value will be trimmed to last 12 characters and left padded with zeros.
|
|
1561
|
+
*/
|
|
666
1562
|
export const uuid1 = ({
|
|
667
1563
|
date = new Date(),
|
|
668
1564
|
node = Math.random().toString(16).slice(2)
|
|
@@ -683,3 +1579,26 @@ export const uuid1 = ({
|
|
|
683
1579
|
'-',
|
|
684
1580
|
(_ZEROS + node).slice(-12))
|
|
685
1581
|
}
|
|
1582
|
+
|
|
1583
|
+
_test.uuid1 = () => {
|
|
1584
|
+
for (let counter = 1; counter <= 0x5678; ++counter) {
|
|
1585
|
+
const uuid = uuid1()
|
|
1586
|
+
|
|
1587
|
+
counter === 0x0001 && console.assert(uuid.split('-')[3] === '8001')
|
|
1588
|
+
counter === 0x0fff && console.assert(uuid.split('-')[3] === '8fff')
|
|
1589
|
+
counter === 0x1000 && console.assert(uuid.split('-')[3] === '9000')
|
|
1590
|
+
counter === 0x2345 && console.assert(uuid.split('-')[3] === 'a345')
|
|
1591
|
+
counter === 0x3456 && console.assert(uuid.split('-')[3] === 'b456')
|
|
1592
|
+
counter === 0x4000 && console.assert(uuid.split('-')[3] === '8000')
|
|
1593
|
+
counter === 0x4567 && console.assert(uuid.split('-')[3] === '8567')
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
_test['uuid1: node'] = () => {
|
|
1598
|
+
console.assert(uuid1({ node: '000123456789abc' }).split('-')[4] === '123456789abc')
|
|
1599
|
+
console.assert(uuid1({ node: '123456789' }).split('-')[4] === '000123456789')
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
_test['uuid1: date'] = () => {
|
|
1603
|
+
console.assert(uuid1({ date: new Date(323325e6) }).startsWith('c1399400-9a71-11bd'))
|
|
1604
|
+
}
|
package/package.json
CHANGED