@jackens/nnn 2023.8.28 → 2023.9.20
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.d.ts +1 -1
- package/nnn.js +895 -0
- package/package.json +1 -1
package/nnn.d.ts
CHANGED
package/nnn.js
CHANGED
|
@@ -1,27 +1,107 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
|
|
3
|
+
export const _test = {}
|
|
4
|
+
export const _version = '2023.9.20'
|
|
5
|
+
|
|
1
6
|
/**
|
|
7
|
+
* <!--
|
|
8
|
+
* ```ts
|
|
9
|
+
* export type EscapeMap = Map<any, (value?: any) => string>;
|
|
10
|
+
* ```
|
|
11
|
+
* -->
|
|
2
12
|
* @typedef {Map<any, (value?: any) => string>} EscapeMap
|
|
13
|
+
*
|
|
14
|
+
* The type of arguments of the `escapeValues` and `escape` helpers.
|
|
3
15
|
*/
|
|
4
16
|
|
|
5
17
|
/**
|
|
18
|
+
* <!--
|
|
19
|
+
* ```ts
|
|
20
|
+
* export type HArgs = [
|
|
21
|
+
* string | Node,
|
|
22
|
+
* ...(Record<string, any> | null | undefined | Node | string | number | HArgs)[]
|
|
23
|
+
* ];
|
|
24
|
+
* ```
|
|
25
|
+
* -->
|
|
6
26
|
* @typedef {[
|
|
7
27
|
* string | Node,
|
|
8
28
|
* ...(Record<string, any> | null | undefined | Node | string | number | HArgs)[]
|
|
9
29
|
* ]} HArgs
|
|
30
|
+
*
|
|
31
|
+
* The type of arguments of the `h` and `s` helpers.
|
|
10
32
|
*/
|
|
11
33
|
|
|
12
34
|
/**
|
|
35
|
+
* <!--
|
|
36
|
+
* ```ts
|
|
37
|
+
* export type JcssNode = {
|
|
38
|
+
* [attributeOrSelector: string]: string | number | JcssNode;
|
|
39
|
+
* };
|
|
40
|
+
* ```
|
|
41
|
+
* -->
|
|
13
42
|
* @typedef {{
|
|
14
43
|
* [attributeOrSelector: string]: string | number | JcssNode;
|
|
15
44
|
* }} JcssNode
|
|
45
|
+
*
|
|
46
|
+
* The type of arguments of the `jcss` helper.
|
|
16
47
|
*/
|
|
17
48
|
|
|
18
49
|
/**
|
|
50
|
+
* <!--
|
|
51
|
+
* ```ts
|
|
52
|
+
* export type JcssRoot = Record<string, JcssNode>;
|
|
53
|
+
* ```
|
|
54
|
+
* -->
|
|
19
55
|
* @typedef {Record<string, JcssNode>} JcssRoot
|
|
56
|
+
*
|
|
57
|
+
* The type of arguments of the `jcss` helper.
|
|
20
58
|
*/
|
|
21
59
|
|
|
22
60
|
const _COLORS = ['#e22', '#e73', '#fc3', '#ad4', '#4d9', '#3be', '#45d', '#c3e']
|
|
23
61
|
|
|
24
62
|
/**
|
|
63
|
+
* <!--
|
|
64
|
+
* ```ts
|
|
65
|
+
* export function chartable(options?: {
|
|
66
|
+
* bottom?: number;
|
|
67
|
+
* gapX?: number;
|
|
68
|
+
* gapY?: number;
|
|
69
|
+
* headerColumn?: boolean;
|
|
70
|
+
* headerRow?: boolean;
|
|
71
|
+
* id?: string;
|
|
72
|
+
* left?: number;
|
|
73
|
+
* maxY?: number;
|
|
74
|
+
* right?: number;
|
|
75
|
+
* singleScale?: boolean;
|
|
76
|
+
* table?: HTMLTableElement;
|
|
77
|
+
* title?: string;
|
|
78
|
+
* top?: number;
|
|
79
|
+
* xLabels?: string[];
|
|
80
|
+
* zLabels?: string[];
|
|
81
|
+
* zxY?: number[][];
|
|
82
|
+
* }): SVGSVGElement;
|
|
83
|
+
* ```
|
|
84
|
+
* -->
|
|
85
|
+
* A helper for creating a chart based on a table (conf. <https://jackens.github.io/nnn/chartable/>).
|
|
86
|
+
*
|
|
87
|
+
* Options:
|
|
88
|
+
* - `bottom`: bottom padding (for X axis labels)
|
|
89
|
+
* - `gapX`: X axis spacing
|
|
90
|
+
* - `gapY`: Y axis spacing
|
|
91
|
+
* - `headerColumn`: flag indicating that `table` has a header column (with X axis labels)
|
|
92
|
+
* - `headerRow`: flag indicating that `table` has a header row (with data series labels)
|
|
93
|
+
* - `id`: chart id
|
|
94
|
+
* - `left`: left padding (for data series labels)
|
|
95
|
+
* - `maxY`: number of Y axis lines
|
|
96
|
+
* - `right`: right padding (for data series labels)
|
|
97
|
+
* - `singleScale`: flag to force single scale
|
|
98
|
+
* - `table`: `HTMLTableElement` to extract data, data series labels and X axis labels
|
|
99
|
+
* - `title`: chart title
|
|
100
|
+
* - `top`: top padding (for the title)
|
|
101
|
+
* - `xLabels`: X axis labels
|
|
102
|
+
* - `zLabels`: data series labels
|
|
103
|
+
* - `zxY`: chart data
|
|
104
|
+
*
|
|
25
105
|
* @param {{
|
|
26
106
|
* bottom?: number;
|
|
27
107
|
* gapX?: number;
|
|
@@ -211,6 +291,14 @@ export const chartable = ({
|
|
|
211
291
|
)
|
|
212
292
|
}
|
|
213
293
|
|
|
294
|
+
/**
|
|
295
|
+
* <!--
|
|
296
|
+
* ```ts
|
|
297
|
+
* export function eq(x: any, y: any): boolean;
|
|
298
|
+
* ```
|
|
299
|
+
* -->
|
|
300
|
+
* A helper that checks equality of the given arguments.
|
|
301
|
+
*/
|
|
214
302
|
export const eq = /** @returns {boolean} */ (/** @type {any} */ x, /** @type {any} */ y) => {
|
|
215
303
|
if (x === y) {
|
|
216
304
|
return true
|
|
@@ -253,12 +341,91 @@ export const eq = /** @returns {boolean} */ (/** @type {any} */ x, /** @type {an
|
|
|
253
341
|
return false
|
|
254
342
|
}
|
|
255
343
|
|
|
344
|
+
_test.eq = () => {
|
|
345
|
+
console.assert(eq(true, true))
|
|
346
|
+
console.assert(eq(NaN, NaN))
|
|
347
|
+
console.assert(!eq(null, undefined))
|
|
348
|
+
console.assert(eq(42, 42))
|
|
349
|
+
// eslint-disable-next-line no-new-wrappers
|
|
350
|
+
console.assert(eq(42, new Number(42)))
|
|
351
|
+
console.assert(eq(42, Number(42)))
|
|
352
|
+
// eslint-disable-next-line no-new-wrappers
|
|
353
|
+
console.assert(eq(new Number(42), Number(42)))
|
|
354
|
+
console.assert(!eq(42, '42'))
|
|
355
|
+
console.assert(eq('42', '42'))
|
|
356
|
+
// eslint-disable-next-line no-new-wrappers
|
|
357
|
+
console.assert(eq('42', new String('42')))
|
|
358
|
+
console.assert(eq('42', String('42')))
|
|
359
|
+
// eslint-disable-next-line no-new-wrappers
|
|
360
|
+
console.assert(eq(String('42'), new String('42')))
|
|
361
|
+
console.assert(eq(/42/, /42/))
|
|
362
|
+
console.assert(!eq(/42/, /42/g))
|
|
363
|
+
console.assert(eq(new Date(42), new Date(42)))
|
|
364
|
+
console.assert(!eq(new Date(), new Date(42)))
|
|
365
|
+
console.assert(eq({ j: '42', c: 42 }, { c: 42, j: '42' }))
|
|
366
|
+
console.assert(eq([42, '42'], [42, '42']))
|
|
367
|
+
console.assert(eq(new Set(['42', 42]), new Set([42, '42'])))
|
|
368
|
+
console.assert(!eq(new Set(['42', 42]), new Set([42])))
|
|
369
|
+
console.assert(!eq(new Set([42, undefined]), new Set([42])))
|
|
370
|
+
console.assert(eq(
|
|
371
|
+
new Map([[{ j: 42 }, { J: '42' }], [{ c: 42 }, { C: '42' }]]),
|
|
372
|
+
new Map([[{ c: 42 }, { C: '42' }], [{ j: 42 }, { J: '42' }]])
|
|
373
|
+
))
|
|
374
|
+
console.assert(!eq(
|
|
375
|
+
new Map([[{ j: 42 }, { J: '42' }], [{ c: 42 }, { C: '42' }]]),
|
|
376
|
+
new Map([[{ j: '42' }, { J: 42 }], [{ c: '42' }, { C: 42 }]])
|
|
377
|
+
))
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* <!--
|
|
382
|
+
* ```ts
|
|
383
|
+
* export function escape(escapeMap: EscapeMap, template: TemplateStringsArray, ...values: any[]): string;
|
|
384
|
+
* ```
|
|
385
|
+
* -->
|
|
386
|
+
* A generic helper for escaping `values` by given `escapeMap` (in *TemplateStrings* flavor).
|
|
387
|
+
*/
|
|
256
388
|
export const escape = (
|
|
257
389
|
/** @type {EscapeMap} */ escapeMap,
|
|
258
390
|
/** @type {TemplateStringsArray} */ template,
|
|
259
391
|
/** @type {any[]} */ ...values
|
|
260
392
|
) => String.raw(template, ...escapeValues(escapeMap, values))
|
|
261
393
|
|
|
394
|
+
_test.escape = () => {
|
|
395
|
+
// @ts-expect-error
|
|
396
|
+
const /** @type {EscapeMap} */ escapeMap = new Map([
|
|
397
|
+
[undefined, () => 'NULL'],
|
|
398
|
+
[Array, (/** @type {any[]} */ values) => escapeValues(escapeMap, values).join(', ')],
|
|
399
|
+
[Boolean, (/** @type {boolean} */ value) => `b'${+value}'`],
|
|
400
|
+
[Date, (/** @type {Date} */ value) => `'${value.toISOString().replace(/^(.+)T(.+)\..*$/, '$1 $2')}'`],
|
|
401
|
+
[Number, (/** @type {number} */ value) => `${value}`],
|
|
402
|
+
[String, (/** @type {string} */ value) => `'${value.replace(/'/g, "''")}'`]
|
|
403
|
+
])
|
|
404
|
+
|
|
405
|
+
// @ts-expect-error
|
|
406
|
+
const sql = escape.bind(null, escapeMap)
|
|
407
|
+
|
|
408
|
+
const actual = sql`
|
|
409
|
+
SELECT *
|
|
410
|
+
FROM table_name
|
|
411
|
+
WHERE column_name IN (${[true, null, undefined, 42, '42', "4'2", /42/, new Date(323325e6)]})`
|
|
412
|
+
|
|
413
|
+
const expected = `
|
|
414
|
+
SELECT *
|
|
415
|
+
FROM table_name
|
|
416
|
+
WHERE column_name IN (b'1', NULL, NULL, 42, '42', '4''2', NULL, '1980-03-31 04:30:00')`
|
|
417
|
+
|
|
418
|
+
console.assert(actual === expected)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* <!--
|
|
423
|
+
* ```ts
|
|
424
|
+
* export function escapeValues(escapeMap: EscapeMap, values: any[]): string[];
|
|
425
|
+
* ```
|
|
426
|
+
* -->
|
|
427
|
+
* A generic helper for escaping `values` by given `escapeMap`.
|
|
428
|
+
*/
|
|
262
429
|
export const escapeValues = (
|
|
263
430
|
/** @type {EscapeMap} */ escapeMap,
|
|
264
431
|
/** @type {any[]} */ values
|
|
@@ -266,6 +433,14 @@ export const escapeValues = (
|
|
|
266
433
|
|
|
267
434
|
const _TAGS_TO_SKIP = { IFRAME: 1, NOSCRIPT: 1, PRE: 1, SCRIPT: 1, STYLE: 1, TEXTAREA: 1 }
|
|
268
435
|
|
|
436
|
+
/**
|
|
437
|
+
* <!--
|
|
438
|
+
* ```ts
|
|
439
|
+
* export function fixTypography(node: Node): void;
|
|
440
|
+
* ```
|
|
441
|
+
* -->
|
|
442
|
+
* A helper that implements typographic corrections specific to Polish typography.
|
|
443
|
+
*/
|
|
269
444
|
export const fixTypography = (/** @type {Node} */ node) => {
|
|
270
445
|
const /** @type {Node[]} */ queue = [node]
|
|
271
446
|
|
|
@@ -306,6 +481,24 @@ export const fixTypography = (/** @type {Node} */ node) => {
|
|
|
306
481
|
}
|
|
307
482
|
}
|
|
308
483
|
|
|
484
|
+
_test.fixTypography = () => {
|
|
485
|
+
const p = h('p', 'Pchnąć w tę łódź jeża lub ośm skrzyń fig (zob. https://pl.wikipedia.org/wiki/Pangram).')
|
|
486
|
+
|
|
487
|
+
fixTypography(p)
|
|
488
|
+
|
|
489
|
+
console.assert(p.innerHTML ===
|
|
490
|
+
'Pchnąć <span style="white-space:nowrap">w </span>tę łódź jeża lub ośm skrzyń fig ' +
|
|
491
|
+
'(zob. https://\u200Bpl.\u200Bwikipedia.\u200Borg/\u200Bwiki/\u200BPangram).')
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* <!--
|
|
496
|
+
* ```ts
|
|
497
|
+
* export function get(defaultValue: any, ref: any, ...keys: (string | number | symbol)[]): any;
|
|
498
|
+
* ```
|
|
499
|
+
* -->
|
|
500
|
+
* A convenient helper for getting values of nested objects.
|
|
501
|
+
*/
|
|
309
502
|
export const get = (
|
|
310
503
|
/** @type {any} */ defaultValue,
|
|
311
504
|
/** @type {any} */ ref,
|
|
@@ -322,6 +515,23 @@ export const get = (
|
|
|
322
515
|
return ref
|
|
323
516
|
}
|
|
324
517
|
|
|
518
|
+
_test.get = () => {
|
|
519
|
+
const ref = { one: { two: { 3: 'value' } } }
|
|
520
|
+
|
|
521
|
+
console.assert(get('default', ref, 'one', 'two', 3) === 'value')
|
|
522
|
+
|
|
523
|
+
// @ts-expect-error
|
|
524
|
+
ref.one.two[3] = undefined
|
|
525
|
+
|
|
526
|
+
console.assert(get('default', ref, 'one', 'two', 3) === undefined)
|
|
527
|
+
|
|
528
|
+
// @ts-expect-error
|
|
529
|
+
ref.one.two = {}
|
|
530
|
+
|
|
531
|
+
console.assert(get('default', ref, 'one', 'two', 3) === 'default')
|
|
532
|
+
console.assert(get('default', ref) === ref)
|
|
533
|
+
}
|
|
534
|
+
|
|
325
535
|
const /** @type {Record<string, string>} */ _NS = {
|
|
326
536
|
xlink: 'http://www.w3.org/1999/xlink'
|
|
327
537
|
}
|
|
@@ -416,12 +626,155 @@ const _h = (/** @type {string?=} */ namespaceURI) => {
|
|
|
416
626
|
return h
|
|
417
627
|
}
|
|
418
628
|
|
|
629
|
+
/**
|
|
630
|
+
* <!--
|
|
631
|
+
* ```ts
|
|
632
|
+
* export function h<T extends keyof HTMLElementTagNameMap>(tag: T, ...args: HArgs[1][]): HTMLElementTagNameMap[T];
|
|
633
|
+
* export function h<N extends Node>(node: N, ...args: HArgs[1][]): N;
|
|
634
|
+
* export function h(...args: HArgs): Node;
|
|
635
|
+
* ```
|
|
636
|
+
* -->
|
|
637
|
+
* A lightweight [HyperScript](https://github.com/hyperhype/hyperscript)-style helper for creating and modifying `HTMLElement`s (see also `s`).
|
|
638
|
+
*
|
|
639
|
+
* - The first argument of type `string` specifies the tag of the element to be created.
|
|
640
|
+
* - The first argument of type `Node` specifies the element to be modified.
|
|
641
|
+
* - All other arguments of type `Record<string, any>` are mappings of attributes and properties.
|
|
642
|
+
* Keys starting with `$` specify *properties* (without the leading `$`) to be set on the element being created or modified.
|
|
643
|
+
* (Note that `$` is not a valid attribute name character.)
|
|
644
|
+
* All other keys specify *attributes* to be set by `setAttribute`.
|
|
645
|
+
* An attribute equal to `false` causes the attribute to be removed by `removeAttribute`.
|
|
646
|
+
* - All other arguments of type `null` or `undefined` are simply ignored.
|
|
647
|
+
* - All other arguments of type `Node` are appended to the element being created or modified.
|
|
648
|
+
* - All other arguments of type `string`/`number` are converted to `Text` nodes and appended to the element being created or modified.
|
|
649
|
+
* - All other arguments of type `HArgs` are passed to `h` and the results are appended to the element being created or modified.
|
|
650
|
+
*/
|
|
419
651
|
export const h = _h()
|
|
420
652
|
|
|
653
|
+
_test.h = () => {
|
|
654
|
+
const b = h('b')
|
|
655
|
+
|
|
656
|
+
console.assert(b.outerHTML === '<b></b>')
|
|
657
|
+
|
|
658
|
+
const i = h('i', 'text')
|
|
659
|
+
|
|
660
|
+
h(b, i)
|
|
661
|
+
|
|
662
|
+
console.assert(i.outerHTML === '<i>text</i>')
|
|
663
|
+
console.assert(b.outerHTML === '<b><i>text</i></b>')
|
|
664
|
+
|
|
665
|
+
h(i, { $className: 'some class' })
|
|
666
|
+
|
|
667
|
+
console.assert(i.outerHTML === '<i class="some class">text</i>')
|
|
668
|
+
console.assert(b.outerHTML === '<b><i class="some class">text</i></b>')
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
_test['h: innerText vs items'] = () => {
|
|
672
|
+
console.assert(h('span', 'text').outerHTML === '<span>text</span>')
|
|
673
|
+
console.assert(h('span', { $innerText: 'text' }).outerHTML === '<span>text</span>')
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
_test['h: style'] = () => {
|
|
677
|
+
console.assert(h('div', { style: 'margin:0;padding:0' }).outerHTML ===
|
|
678
|
+
'<div style="margin:0;padding:0"></div>')
|
|
679
|
+
console.assert(h('div', { $style: 'margin:0;padding:0' }).outerHTML ===
|
|
680
|
+
'<div style="margin: 0px; padding: 0px;"></div>')
|
|
681
|
+
console.assert(h('div', { $style: { margin: 0, padding: 0 } }).outerHTML ===
|
|
682
|
+
'<div style="margin: 0px; padding: 0px;"></div>')
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
_test['h: attributes vs properties'] = () => {
|
|
686
|
+
const input1 = h('input', { value: 42 })
|
|
687
|
+
const input2 = h('input', { $value: '42' })
|
|
688
|
+
|
|
689
|
+
console.assert(input1.value === '42')
|
|
690
|
+
console.assert(input2.value === '42')
|
|
691
|
+
|
|
692
|
+
console.assert(input1.outerHTML === '<input value="42">')
|
|
693
|
+
console.assert(input2.outerHTML === '<input>')
|
|
694
|
+
|
|
695
|
+
const checkbox1 = h('input', { type: 'checkbox', checked: true })
|
|
696
|
+
const checkbox2 = h('input', { type: 'checkbox', $checked: true })
|
|
697
|
+
|
|
698
|
+
console.assert(checkbox1.checked === true)
|
|
699
|
+
console.assert(checkbox2.checked === true)
|
|
700
|
+
|
|
701
|
+
console.assert(checkbox1.outerHTML === '<input type="checkbox" checked="">')
|
|
702
|
+
console.assert(checkbox2.outerHTML === '<input type="checkbox">')
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
_test['h: nested properties'] = () => {
|
|
706
|
+
const div = h('div')
|
|
707
|
+
|
|
708
|
+
// @ts-expect-error
|
|
709
|
+
console.assert(div.key === undefined)
|
|
710
|
+
|
|
711
|
+
h(div, { $key: { one: 1 } })
|
|
712
|
+
|
|
713
|
+
// @ts-expect-error
|
|
714
|
+
console.assert(eq(div.key, { one: 1 }))
|
|
715
|
+
|
|
716
|
+
h(div, { $key: { two: 2 } })
|
|
717
|
+
|
|
718
|
+
// @ts-expect-error
|
|
719
|
+
console.assert(eq(div.key, { one: 1, two: 2 }))
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* <!--
|
|
724
|
+
* ```ts
|
|
725
|
+
* export function has(key: any, ref: any): boolean;
|
|
726
|
+
* ```
|
|
727
|
+
* -->
|
|
728
|
+
* A replacement for the `in` operator (not to be confused with the `for-in` loop) that works properly.
|
|
729
|
+
*/
|
|
421
730
|
export const has = (/** @type {any} */ key, /** @type {any} */ ref) =>
|
|
422
731
|
(is(String, key) || is(Number, key) || is(Symbol, key)) && Object.hasOwnProperty.call(ref ?? Object, key)
|
|
423
732
|
|
|
733
|
+
_test.has = () => {
|
|
734
|
+
const obj = { key: 'K', null: 'N' }
|
|
735
|
+
|
|
736
|
+
console.assert('key' in obj)
|
|
737
|
+
console.assert(has('key', obj))
|
|
738
|
+
|
|
739
|
+
console.assert('null' in obj)
|
|
740
|
+
console.assert(has('null', obj))
|
|
741
|
+
|
|
742
|
+
// @ts-expect-error
|
|
743
|
+
console.assert(null in obj)
|
|
744
|
+
console.assert(!has(null, obj))
|
|
745
|
+
|
|
746
|
+
console.assert('toString' in obj)
|
|
747
|
+
console.assert(!has('toString', obj))
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
_test['has: null'] = () => {
|
|
751
|
+
let typeError
|
|
752
|
+
|
|
753
|
+
try {
|
|
754
|
+
// @ts-expect-error
|
|
755
|
+
console.assert('key' in null)
|
|
756
|
+
} catch (error) {
|
|
757
|
+
typeError = error
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
console.assert(typeError instanceof TypeError) // Cannot use 'in' operator to search for 'key' in null
|
|
761
|
+
console.assert(!has('key', null))
|
|
762
|
+
}
|
|
763
|
+
|
|
424
764
|
/**
|
|
765
|
+
* <!--
|
|
766
|
+
* ```ts
|
|
767
|
+
* export function is(type: BigIntConstructor, arg: any): arg is bigint;
|
|
768
|
+
* export function is(type: BooleanConstructor, arg: any): arg is boolean;
|
|
769
|
+
* export function is(type: NumberConstructor, arg: any): arg is number;
|
|
770
|
+
* export function is(type: StringConstructor, arg: any): arg is string;
|
|
771
|
+
* export function is(type: SymbolConstructor, arg: any): arg is symbol;
|
|
772
|
+
* export function is(type: undefined, arg: any): arg is null | undefined;
|
|
773
|
+
* export function is<T extends abstract new (...args: any[]) => any>(type: T, arg: any): arg is InstanceType<T>;
|
|
774
|
+
* ```
|
|
775
|
+
* -->
|
|
776
|
+
* A helper that checks if the given argument is of a certain type.
|
|
777
|
+
*
|
|
425
778
|
* @template {abstract new (...args: any[]) => any} T
|
|
426
779
|
*
|
|
427
780
|
* @type {{
|
|
@@ -438,6 +791,39 @@ export const has = (/** @type {any} */ key, /** @type {any} */ ref) =>
|
|
|
438
791
|
*/
|
|
439
792
|
export const is = (/** @type {T} */ type, /** @type {any} */ arg) => arg?.constructor === type
|
|
440
793
|
|
|
794
|
+
_test.is = () => {
|
|
795
|
+
console.assert(is(Number, 42))
|
|
796
|
+
console.assert(is(Number, Number(42)))
|
|
797
|
+
// eslint-disable-next-line no-new-wrappers
|
|
798
|
+
console.assert(is(Number, new Number(42)))
|
|
799
|
+
console.assert(is(Number, NaN))
|
|
800
|
+
console.assert(is(String, '42'))
|
|
801
|
+
console.assert(is(String, String('42')))
|
|
802
|
+
// eslint-disable-next-line no-new-wrappers
|
|
803
|
+
console.assert(is(String, new String('42')))
|
|
804
|
+
console.assert(is(Symbol, Symbol('42')))
|
|
805
|
+
console.assert(is(Symbol, Object(Symbol('42'))))
|
|
806
|
+
console.assert(is(undefined, undefined))
|
|
807
|
+
console.assert(is(undefined, null))
|
|
808
|
+
console.assert(is(Object, {}))
|
|
809
|
+
console.assert(is(Array, []))
|
|
810
|
+
console.assert(is(RegExp, /42/))
|
|
811
|
+
console.assert(is(Date, new Date(42)))
|
|
812
|
+
console.assert(is(Set, new Set(['42', 42])))
|
|
813
|
+
console.assert(is(Map, new Map([[{ j: 42 }, { J: '42' }], [{ c: 42 }, { C: '42' }]])))
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
_test['is vs ‘toString.call’'] = () => {
|
|
817
|
+
class FooBar { }
|
|
818
|
+
|
|
819
|
+
console.assert(is(FooBar, new FooBar()))
|
|
820
|
+
|
|
821
|
+
const fakeFooBar = { [Symbol.toStringTag]: 'FooBar' }
|
|
822
|
+
|
|
823
|
+
console.assert(({}).toString.call(new FooBar()) === '[object Object]')
|
|
824
|
+
console.assert(({}).toString.call(fakeFooBar) === '[object FooBar]')
|
|
825
|
+
}
|
|
826
|
+
|
|
441
827
|
const _jcss = (
|
|
442
828
|
/** @type {JcssNode} */ node,
|
|
443
829
|
/** @type {string} */ prefix,
|
|
@@ -492,6 +878,22 @@ const _jcss = (
|
|
|
492
878
|
}
|
|
493
879
|
}
|
|
494
880
|
|
|
881
|
+
/**
|
|
882
|
+
* <!--
|
|
883
|
+
* ```ts
|
|
884
|
+
* export function jcss(root: JcssRoot, splitter?: string): string;
|
|
885
|
+
* ```
|
|
886
|
+
* -->
|
|
887
|
+
* A simple CSS-in-JS helper.
|
|
888
|
+
*
|
|
889
|
+
* The `root` parameter provides a hierarchical description of CSS rules.
|
|
890
|
+
*
|
|
891
|
+
* - 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.
|
|
892
|
+
* - 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`).
|
|
893
|
+
* - In keys specifying CSS attribute, all uppercase letters are replaced by lowercase letters with an additional `-` character preceding them (e.g. `fontFamily` → `font-family`).
|
|
894
|
+
* - 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}`).
|
|
895
|
+
* - Top-level keys that begin with `@` are not concatenated with sub-object keys.
|
|
896
|
+
*/
|
|
495
897
|
export const jcss = (/** @type {JcssRoot} */ root, splitter = '$$') => {
|
|
496
898
|
const split = (/** @type {string} */ text) => text.split(splitter)[0]
|
|
497
899
|
const /** @type {string[]} */ result = []
|
|
@@ -511,6 +913,220 @@ export const jcss = (/** @type {JcssRoot} */ root, splitter = '$$') => {
|
|
|
511
913
|
return result.join('')
|
|
512
914
|
}
|
|
513
915
|
|
|
916
|
+
_test['jcss: #1'] = () => {
|
|
917
|
+
const actual = jcss({
|
|
918
|
+
a: {
|
|
919
|
+
color: 'red',
|
|
920
|
+
margin: 1,
|
|
921
|
+
'.c': { margin: 2, padding: 2 },
|
|
922
|
+
padding: 1
|
|
923
|
+
}
|
|
924
|
+
})
|
|
925
|
+
|
|
926
|
+
const expected = `
|
|
927
|
+
a{
|
|
928
|
+
color:red;
|
|
929
|
+
margin:1
|
|
930
|
+
}
|
|
931
|
+
a.c{
|
|
932
|
+
margin:2;
|
|
933
|
+
padding:2
|
|
934
|
+
}
|
|
935
|
+
a{
|
|
936
|
+
padding:1
|
|
937
|
+
}`.replace(/\n\s*/g, '')
|
|
938
|
+
|
|
939
|
+
console.assert(actual === expected)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
_test['jcss: #2'] = () => {
|
|
943
|
+
const actual = jcss({
|
|
944
|
+
a: {
|
|
945
|
+
'.b': {
|
|
946
|
+
color: 'red',
|
|
947
|
+
margin: 1,
|
|
948
|
+
'.c': { margin: 2, padding: 2 },
|
|
949
|
+
padding: 1
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
})
|
|
953
|
+
|
|
954
|
+
const expected = `
|
|
955
|
+
a.b{
|
|
956
|
+
color:red;
|
|
957
|
+
margin:1
|
|
958
|
+
}
|
|
959
|
+
a.b.c{
|
|
960
|
+
margin:2;
|
|
961
|
+
padding:2
|
|
962
|
+
}
|
|
963
|
+
a.b{
|
|
964
|
+
padding:1
|
|
965
|
+
}`.replace(/\n\s*/g, '')
|
|
966
|
+
|
|
967
|
+
console.assert(actual === expected)
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
_test['jcss: #3'] = () => {
|
|
971
|
+
const actual = jcss({
|
|
972
|
+
'@font-face$$1': {
|
|
973
|
+
fontFamily: 'Jackens',
|
|
974
|
+
src$$1: 'url(otf/jackens.otf)',
|
|
975
|
+
src$$2: "url(otf/jackens.otf) format('opentype')," +
|
|
976
|
+
"url(svg/jackens.svg) format('svg')",
|
|
977
|
+
fontWeight: 'normal',
|
|
978
|
+
fontStyle: 'normal'
|
|
979
|
+
},
|
|
980
|
+
'@font-face$$2': {
|
|
981
|
+
fontFamily: 'C64',
|
|
982
|
+
src: 'url(fonts/C64_Pro_Mono-STYLE.woff)'
|
|
983
|
+
},
|
|
984
|
+
'@keyframes spin': {
|
|
985
|
+
'0%': { transform: 'rotate(0deg)' },
|
|
986
|
+
'100%': { transform: 'rotate(360deg)' }
|
|
987
|
+
},
|
|
988
|
+
div: {
|
|
989
|
+
border: 'solid red 1px',
|
|
990
|
+
'.c1': { 'background-color': '#000' },
|
|
991
|
+
' .c1': { backgroundColor: 'black' },
|
|
992
|
+
'.c2': { backgroundColor: 'rgb(0,0,0)' }
|
|
993
|
+
},
|
|
994
|
+
'@media(min-width:200px)': {
|
|
995
|
+
div: { margin: 0, padding: 0 },
|
|
996
|
+
span: { color: '#000' }
|
|
997
|
+
}
|
|
998
|
+
})
|
|
999
|
+
|
|
1000
|
+
const expected = `
|
|
1001
|
+
@font-face{
|
|
1002
|
+
font-family:Jackens;
|
|
1003
|
+
src:url(otf/jackens.otf);
|
|
1004
|
+
src:url(otf/jackens.otf) format('opentype'),url(svg/jackens.svg) format('svg');
|
|
1005
|
+
font-weight:normal;
|
|
1006
|
+
font-style:normal
|
|
1007
|
+
}
|
|
1008
|
+
@font-face{
|
|
1009
|
+
font-family:C64;
|
|
1010
|
+
src:url(fonts/C64_Pro_Mono-STYLE.woff)
|
|
1011
|
+
}
|
|
1012
|
+
@keyframes spin{
|
|
1013
|
+
0%{
|
|
1014
|
+
transform:rotate(0deg)
|
|
1015
|
+
}
|
|
1016
|
+
100%{
|
|
1017
|
+
transform:rotate(360deg)
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
div{
|
|
1021
|
+
border:solid red 1px
|
|
1022
|
+
}
|
|
1023
|
+
div.c1{
|
|
1024
|
+
background-color:#000
|
|
1025
|
+
}
|
|
1026
|
+
div .c1{
|
|
1027
|
+
background-color:black
|
|
1028
|
+
}
|
|
1029
|
+
div.c2{
|
|
1030
|
+
background-color:rgb(0,0,0)
|
|
1031
|
+
}
|
|
1032
|
+
@media(min-width:200px){
|
|
1033
|
+
div{
|
|
1034
|
+
margin:0;
|
|
1035
|
+
padding:0
|
|
1036
|
+
}
|
|
1037
|
+
span{
|
|
1038
|
+
color:#000
|
|
1039
|
+
}
|
|
1040
|
+
}`.replace(/\n\s*/g, '')
|
|
1041
|
+
|
|
1042
|
+
console.assert(actual === expected)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
_test['jcss: #4'] = () => {
|
|
1046
|
+
const actual = jcss({
|
|
1047
|
+
a: {
|
|
1048
|
+
'.b,.c': {
|
|
1049
|
+
margin: 1,
|
|
1050
|
+
'.d': {
|
|
1051
|
+
margin: 2
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
const expected = `
|
|
1058
|
+
a.b,a.c{
|
|
1059
|
+
margin:1
|
|
1060
|
+
}
|
|
1061
|
+
a.b.d,a.c.d{
|
|
1062
|
+
margin:2
|
|
1063
|
+
}`.replace(/\n\s*/g, '')
|
|
1064
|
+
|
|
1065
|
+
console.assert(actual === expected)
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
_test['jcss: #5'] = () => {
|
|
1069
|
+
const actual = jcss({
|
|
1070
|
+
'.b,.c': {
|
|
1071
|
+
margin: 1,
|
|
1072
|
+
'.d': {
|
|
1073
|
+
margin: 2
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
})
|
|
1077
|
+
|
|
1078
|
+
const expected = `
|
|
1079
|
+
.b,.c{
|
|
1080
|
+
margin:1
|
|
1081
|
+
}
|
|
1082
|
+
.b.d,.c.d{
|
|
1083
|
+
margin:2
|
|
1084
|
+
}`.replace(/\n\s*/g, '')
|
|
1085
|
+
|
|
1086
|
+
console.assert(actual === expected)
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
_test['jcss: #6'] = () => {
|
|
1090
|
+
const actual = jcss({
|
|
1091
|
+
'.a,.b': {
|
|
1092
|
+
margin: 1,
|
|
1093
|
+
'.c,.d': {
|
|
1094
|
+
margin: 2
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
})
|
|
1098
|
+
|
|
1099
|
+
const expected = `
|
|
1100
|
+
.a,.b{
|
|
1101
|
+
margin:1
|
|
1102
|
+
}
|
|
1103
|
+
.a.c,.a.d,.b.c,.b.d{
|
|
1104
|
+
margin:2
|
|
1105
|
+
}`.replace(/\n\s*/g, '')
|
|
1106
|
+
|
|
1107
|
+
console.assert(actual === expected)
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* <!--
|
|
1112
|
+
* ```ts
|
|
1113
|
+
* export function jsOnParse(handlers: Record<string, Function>, text: string): any;
|
|
1114
|
+
* ```
|
|
1115
|
+
* -->
|
|
1116
|
+
* `JSON.parse` with “JavaScript turned on”.
|
|
1117
|
+
*
|
|
1118
|
+
* Objects having *exactly* one property which is present in the `handlers` map, i.e. objects of the form:
|
|
1119
|
+
*
|
|
1120
|
+
* ```js
|
|
1121
|
+
* { "«handlerName»": [«params»] }
|
|
1122
|
+
* ```
|
|
1123
|
+
*
|
|
1124
|
+
* are replaced by the result of call
|
|
1125
|
+
*
|
|
1126
|
+
* ```js
|
|
1127
|
+
* handlers['«handlerName»'](...«params»)
|
|
1128
|
+
* ```
|
|
1129
|
+
*/
|
|
514
1130
|
export const jsOnParse = (
|
|
515
1131
|
/** @type {Record<string, Function>} */ handlers,
|
|
516
1132
|
/** @type {string} */ text
|
|
@@ -533,6 +1149,49 @@ export const jsOnParse = (
|
|
|
533
1149
|
return value
|
|
534
1150
|
})
|
|
535
1151
|
|
|
1152
|
+
_test.jsOnParse = () => {
|
|
1153
|
+
const handlers = {
|
|
1154
|
+
$hello: (/** @type {string} */ name) => `Hello ${name}!`,
|
|
1155
|
+
$foo: () => 'bar'
|
|
1156
|
+
}
|
|
1157
|
+
const actual = jsOnParse(handlers, `
|
|
1158
|
+
[
|
|
1159
|
+
{
|
|
1160
|
+
"$hello": ["World"]
|
|
1161
|
+
},
|
|
1162
|
+
{
|
|
1163
|
+
"nested": {
|
|
1164
|
+
"$hello": ["nested World"]
|
|
1165
|
+
},
|
|
1166
|
+
"one": 1,
|
|
1167
|
+
"two": 2
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
"$foo": []
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
"$foo": ["The parent object does not have exactly one property!"],
|
|
1174
|
+
"one": 1,
|
|
1175
|
+
"two": 2
|
|
1176
|
+
}
|
|
1177
|
+
]`)
|
|
1178
|
+
const expected = [
|
|
1179
|
+
'Hello World!',
|
|
1180
|
+
{
|
|
1181
|
+
nested: 'Hello nested World!',
|
|
1182
|
+
one: 1,
|
|
1183
|
+
two: 2
|
|
1184
|
+
},
|
|
1185
|
+
'bar',
|
|
1186
|
+
{
|
|
1187
|
+
$foo: ['The parent object does not have exactly one property!'],
|
|
1188
|
+
one: 1,
|
|
1189
|
+
two: 2
|
|
1190
|
+
}]
|
|
1191
|
+
|
|
1192
|
+
console.assert(eq(actual, expected))
|
|
1193
|
+
}
|
|
1194
|
+
|
|
536
1195
|
const _locale = (
|
|
537
1196
|
/** @type {Record<string, Record<string, string | Record<string, string>>>} */ locales,
|
|
538
1197
|
/** @type {string} */ language,
|
|
@@ -551,6 +1210,18 @@ const _locale = (
|
|
|
551
1210
|
return is(String, t) ? t : text
|
|
552
1211
|
}
|
|
553
1212
|
|
|
1213
|
+
/**
|
|
1214
|
+
* <!--
|
|
1215
|
+
* ```ts
|
|
1216
|
+
* export function locale(
|
|
1217
|
+
* locales: Record<string, Record<string, string | Record<string, string>>>,
|
|
1218
|
+
* defaultLanguage: string,
|
|
1219
|
+
* languages?: readonly string[]
|
|
1220
|
+
* ): (text?: string, version?: string) => string;
|
|
1221
|
+
* ```
|
|
1222
|
+
* -->
|
|
1223
|
+
* Language translations helper.
|
|
1224
|
+
*/
|
|
554
1225
|
export const locale = (
|
|
555
1226
|
/** @type {Record<string, Record<string, string | Record<string, string>>>} */ locales,
|
|
556
1227
|
/** @type {string} */ defaultLanguage,
|
|
@@ -567,6 +1238,41 @@ export const locale = (
|
|
|
567
1238
|
return _locale.bind(0, locales, defaultLanguage)
|
|
568
1239
|
}
|
|
569
1240
|
|
|
1241
|
+
_test.locale = () => {
|
|
1242
|
+
const locales = {
|
|
1243
|
+
pl: {
|
|
1244
|
+
Password: 'Hasło',
|
|
1245
|
+
button: { Login: 'Zaloguj' }
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
const _ = locale(locales, 'pl', [])
|
|
1249
|
+
|
|
1250
|
+
console.assert(_('Login') === 'Login')
|
|
1251
|
+
console.assert(_('Password') === 'Hasło')
|
|
1252
|
+
|
|
1253
|
+
console.assert(_('Undefined text') === 'Undefined text')
|
|
1254
|
+
|
|
1255
|
+
console.assert(_('Login', 'button') === 'Zaloguj')
|
|
1256
|
+
|
|
1257
|
+
console.assert(_('Password', 'undefined_version') === 'Hasło')
|
|
1258
|
+
console.assert(_('Undefined text', 'undefined_version') === 'Undefined text')
|
|
1259
|
+
|
|
1260
|
+
console.assert(_('toString') === 'toString')
|
|
1261
|
+
console.assert(_('toString', 'undefined_version') === 'toString')
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* <!--
|
|
1266
|
+
* ```ts
|
|
1267
|
+
* export function nanolight(
|
|
1268
|
+
* pattern: RegExp,
|
|
1269
|
+
* highlighters: ((chunk: string, index: number) => HArgs[1])[],
|
|
1270
|
+
* code: string
|
|
1271
|
+
* ): HArgs[1][];
|
|
1272
|
+
* ```
|
|
1273
|
+
* -->
|
|
1274
|
+
* A generic helper for syntax highlighting (see also `nanolightJs`).
|
|
1275
|
+
*/
|
|
570
1276
|
export const nanolight = (
|
|
571
1277
|
/** @type {RegExp} */ pattern,
|
|
572
1278
|
/** @type {((chunk: string, index: number) => HArgs[1])[]} */ highlighters,
|
|
@@ -584,6 +1290,14 @@ export const nanolight = (
|
|
|
584
1290
|
return result
|
|
585
1291
|
}
|
|
586
1292
|
|
|
1293
|
+
/**
|
|
1294
|
+
* <!--
|
|
1295
|
+
* ```ts
|
|
1296
|
+
* export function nanolightJs(codeJs: string): HArgs[1][];
|
|
1297
|
+
* ```
|
|
1298
|
+
* -->
|
|
1299
|
+
* A helper for highlighting JavaScript.
|
|
1300
|
+
*/
|
|
587
1301
|
// @ts-expect-error
|
|
588
1302
|
export const nanolightJs = nanolight.bind(0,
|
|
589
1303
|
/('.*?'|".*?"|`[\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 +1313,21 @@ export const nanolightJs = nanolight.bind(0,
|
|
|
599
1313
|
]
|
|
600
1314
|
)
|
|
601
1315
|
|
|
1316
|
+
_test.nanolightJs = () => {
|
|
1317
|
+
const codeJs = 'const answerToLifeTheUniverseAndEverything = 42'
|
|
1318
|
+
|
|
1319
|
+
console.assert(h('pre', ['code', ...nanolightJs(codeJs)]).outerHTML ===
|
|
1320
|
+
'<pre><code><b>const</b> <i>answerToLifeTheUniverseAndEverything</i> <b>=</b> <u>42</u></code></pre>')
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* <!--
|
|
1325
|
+
* ```ts
|
|
1326
|
+
* export function plUral(singular: string, plural2: string, plural5: string, value: number): string;
|
|
1327
|
+
* ```
|
|
1328
|
+
* -->
|
|
1329
|
+
* A helper for choosing the correct singular and plural.
|
|
1330
|
+
*/
|
|
602
1331
|
export const plUral = (
|
|
603
1332
|
/** @type {string} */ singular,
|
|
604
1333
|
/** @type {string} */ plural2,
|
|
@@ -617,6 +1346,34 @@ export const plUral = (
|
|
|
617
1346
|
: plural5
|
|
618
1347
|
}
|
|
619
1348
|
|
|
1349
|
+
_test.plUral = () => {
|
|
1350
|
+
// @ts-expect-error
|
|
1351
|
+
const auto = plUral.bind(null, 'auto', 'auta', 'aut')
|
|
1352
|
+
|
|
1353
|
+
console.assert(auto(0) === 'aut')
|
|
1354
|
+
console.assert(auto(1) === 'auto')
|
|
1355
|
+
console.assert(auto(17) === 'aut')
|
|
1356
|
+
console.assert(auto(42) === 'auta')
|
|
1357
|
+
|
|
1358
|
+
// @ts-expect-error
|
|
1359
|
+
const car = plUral.bind(null, 'car', 'cars', 'cars')
|
|
1360
|
+
|
|
1361
|
+
console.assert(car(0) === 'cars')
|
|
1362
|
+
console.assert(car(1) === 'car')
|
|
1363
|
+
console.assert(car(17) === 'cars')
|
|
1364
|
+
console.assert(car(42) === 'cars')
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* <!--
|
|
1369
|
+
* ```ts
|
|
1370
|
+
* export function refsInfo(...refs: any[]): [string, string, string[]][];
|
|
1371
|
+
* ```
|
|
1372
|
+
* -->
|
|
1373
|
+
* A helper that provides information about the given `refs`.
|
|
1374
|
+
*
|
|
1375
|
+
* It returns an array of triples: `[«name», «prototype-name», «array-of-own-property-names»]`.
|
|
1376
|
+
*/
|
|
620
1377
|
export const refsInfo = (/** @type {any[]} */ ...refs) => {
|
|
621
1378
|
const /** @type {Set<Function>} */ fns = new Set()
|
|
622
1379
|
|
|
@@ -634,8 +1391,69 @@ export const refsInfo = (/** @type {any[]} */ ...refs) => {
|
|
|
634
1391
|
]).sort((a, b) => -(a[0] < b[0]))
|
|
635
1392
|
}
|
|
636
1393
|
|
|
1394
|
+
_test.refsInfo = () => {
|
|
1395
|
+
const info = refsInfo(Array, Function)
|
|
1396
|
+
|
|
1397
|
+
console.assert(info.find(([name]) => name === 'Array')?.[2]?.includes?.('length'))
|
|
1398
|
+
console.assert(info.find(([name]) => name === 'Function')?.[2]?.includes?.('length'))
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
_test['refsInfo: browserFingerprint'] = () => {
|
|
1402
|
+
const browserFingerprint = () => {
|
|
1403
|
+
// @ts-expect-error
|
|
1404
|
+
const info = refsInfo(...Object.getOwnPropertyNames(window).map(name => window[name]))
|
|
1405
|
+
const json = JSON.stringify(info)
|
|
1406
|
+
const hash = Array(32).fill(0)
|
|
1407
|
+
let j = 0
|
|
1408
|
+
|
|
1409
|
+
for (let i = 0; i < json.length; i++) {
|
|
1410
|
+
let charCode = json.charCodeAt(i)
|
|
1411
|
+
|
|
1412
|
+
while (charCode > 0) {
|
|
1413
|
+
hash[j] = hash[j] ^ (charCode & 15)
|
|
1414
|
+
charCode >>= 4
|
|
1415
|
+
j = (j + 1) & 31
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
return hash.map(x => x.toString(16)).join('')
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
console.log(browserFingerprint())
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* <!--
|
|
1427
|
+
* ```ts
|
|
1428
|
+
* export function s<T extends keyof SVGElementTagNameMap>(tag: T, ...args: HArgs[1][]): SVGElementTagNameMap[T];
|
|
1429
|
+
* export function s<N extends Node>(node: N, ...args: HArgs[1][]): N;
|
|
1430
|
+
* export function s(...args: HArgs): Node;
|
|
1431
|
+
* ```
|
|
1432
|
+
* -->
|
|
1433
|
+
* A lightweight [HyperScript](https://github.com/hyperhype/hyperscript)-style helper for creating and modifying `SVGElement`s (see also `h`).
|
|
1434
|
+
*
|
|
1435
|
+
* - The first argument of type `string` specifies the tag of the element to be created.
|
|
1436
|
+
* - The first argument of type `Node` specifies the element to be modified.
|
|
1437
|
+
* - All other arguments of type `Record<string, any>` are mappings of attributes and properties.
|
|
1438
|
+
* Keys starting with `$` specify *properties* (without the leading `$`) to be set on the element being created or modified.
|
|
1439
|
+
* (Note that `$` is not a valid attribute name character.)
|
|
1440
|
+
* All other keys specify *attributes* to be set by `setAttributeNS`.
|
|
1441
|
+
* An attribute equal to `false` causes the attribute to be removed by `removeAttributeNS`.
|
|
1442
|
+
* - All other arguments of type `null` or `undefined` are simply ignored.
|
|
1443
|
+
* - All other arguments of type `Node` are appended to the element being created or modified.
|
|
1444
|
+
* - All other arguments of type `string`/`number` are converted to `Text` nodes and appended to the element being created or modified.
|
|
1445
|
+
* - All other arguments of type `HArgs` are passed to `s` and the results are appended to the element being created or modified.
|
|
1446
|
+
*/
|
|
637
1447
|
export const s = _h('http://www.w3.org/2000/svg')
|
|
638
1448
|
|
|
1449
|
+
/**
|
|
1450
|
+
* <!--
|
|
1451
|
+
* ```ts
|
|
1452
|
+
* export function set(value: any, ref: any, ...keys: (string | number | symbol)[]): void;
|
|
1453
|
+
* ```
|
|
1454
|
+
* -->
|
|
1455
|
+
* A convenient helper for setting values of nested objects.
|
|
1456
|
+
*/
|
|
639
1457
|
export const set = (
|
|
640
1458
|
/** @type {any} */ value,
|
|
641
1459
|
/** @type {any} */ ref,
|
|
@@ -660,9 +1478,63 @@ export const set = (
|
|
|
660
1478
|
ref[keys[last]] = value
|
|
661
1479
|
}
|
|
662
1480
|
|
|
1481
|
+
_test.set = () => {
|
|
1482
|
+
const ref = {}
|
|
1483
|
+
|
|
1484
|
+
set('value', ref, 'one', 'two', 3, 4)
|
|
1485
|
+
|
|
1486
|
+
console.assert(eq(ref, {
|
|
1487
|
+
one: {
|
|
1488
|
+
two: [undefined, undefined, undefined, [
|
|
1489
|
+
undefined, undefined, undefined, undefined, 'value']
|
|
1490
|
+
]
|
|
1491
|
+
}
|
|
1492
|
+
}))
|
|
1493
|
+
|
|
1494
|
+
set(undefined, ref, 'one', 'two', 3, 4)
|
|
1495
|
+
|
|
1496
|
+
console.assert(eq(ref, {
|
|
1497
|
+
one: {
|
|
1498
|
+
two: [undefined, undefined, undefined, [
|
|
1499
|
+
undefined, undefined, undefined, undefined, undefined]
|
|
1500
|
+
]
|
|
1501
|
+
}
|
|
1502
|
+
}))
|
|
1503
|
+
|
|
1504
|
+
set({}, ref, 'one', 'two', 3)
|
|
1505
|
+
|
|
1506
|
+
console.assert(eq(ref, {
|
|
1507
|
+
one: {
|
|
1508
|
+
two: [undefined, undefined, undefined, {}]
|
|
1509
|
+
}
|
|
1510
|
+
}))
|
|
1511
|
+
|
|
1512
|
+
set('value', ref)
|
|
1513
|
+
|
|
1514
|
+
console.assert(eq(ref, {
|
|
1515
|
+
one: {
|
|
1516
|
+
two: [undefined, undefined, undefined, {}]
|
|
1517
|
+
}
|
|
1518
|
+
}))
|
|
1519
|
+
}
|
|
1520
|
+
|
|
663
1521
|
const _ZEROS = '0'.repeat(16)
|
|
664
1522
|
let _counter = 0
|
|
665
1523
|
|
|
1524
|
+
/**
|
|
1525
|
+
* <!--
|
|
1526
|
+
* ```ts
|
|
1527
|
+
* export function uuid1(options?: {
|
|
1528
|
+
* date?: Date;
|
|
1529
|
+
* node?: string;
|
|
1530
|
+
* }): string;
|
|
1531
|
+
* ```
|
|
1532
|
+
* -->
|
|
1533
|
+
* A helper that generates a UUID v1 identifier (with a creation timestamp).
|
|
1534
|
+
*
|
|
1535
|
+
* - The optional `node` parameter should have the format `/^[0123456789abcdef]+$/`.
|
|
1536
|
+
* Its value will be trimmed to last 12 characters and left padded with zeros.
|
|
1537
|
+
*/
|
|
666
1538
|
export const uuid1 = ({
|
|
667
1539
|
date = new Date(),
|
|
668
1540
|
node = Math.random().toString(16).slice(2)
|
|
@@ -683,3 +1555,26 @@ export const uuid1 = ({
|
|
|
683
1555
|
'-',
|
|
684
1556
|
(_ZEROS + node).slice(-12))
|
|
685
1557
|
}
|
|
1558
|
+
|
|
1559
|
+
_test.uuid1 = () => {
|
|
1560
|
+
for (let counter = 1; counter <= 0x5678; ++counter) {
|
|
1561
|
+
const uuid = uuid1()
|
|
1562
|
+
|
|
1563
|
+
counter === 0x0001 && console.assert(uuid.split('-')[3] === '8001')
|
|
1564
|
+
counter === 0x0fff && console.assert(uuid.split('-')[3] === '8fff')
|
|
1565
|
+
counter === 0x1000 && console.assert(uuid.split('-')[3] === '9000')
|
|
1566
|
+
counter === 0x2345 && console.assert(uuid.split('-')[3] === 'a345')
|
|
1567
|
+
counter === 0x3456 && console.assert(uuid.split('-')[3] === 'b456')
|
|
1568
|
+
counter === 0x4000 && console.assert(uuid.split('-')[3] === '8000')
|
|
1569
|
+
counter === 0x4567 && console.assert(uuid.split('-')[3] === '8567')
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
_test['uuid1: node'] = () => {
|
|
1574
|
+
console.assert(uuid1({ node: '000123456789abc' }).split('-')[4] === '123456789abc')
|
|
1575
|
+
console.assert(uuid1({ node: '123456789' }).split('-')[4] === '000123456789')
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
_test['uuid1: date'] = () => {
|
|
1579
|
+
console.assert(uuid1({ date: new Date(323325e6) }).startsWith('c1399400-9a71-11bd'))
|
|
1580
|
+
}
|
package/package.json
CHANGED