@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.
Files changed (3) hide show
  1. package/nnn.d.ts +1 -1
  2. package/nnn.js +895 -0
  3. package/package.json +1 -1
package/nnn.d.ts CHANGED
@@ -221,4 +221,4 @@ export function set(value: any, ref: any, ...keys: (string | number | symbol)[])
221
221
  export function uuid1(options?: {
222
222
  date?: Date;
223
223
  node?: string;
224
- }): string;
224
+ }): string;
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
@@ -40,5 +40,5 @@
40
40
  "types": "nnn.d.ts",
41
41
  "name": "@jackens/nnn",
42
42
  "type": "module",
43
- "version": "2023.8.28"
43
+ "version": "2023.9.20"
44
44
  }