@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.
Files changed (2) hide show
  1. package/nnn.js +919 -0
  2. 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
@@ -40,5 +40,5 @@
40
40
  "types": "nnn.d.ts",
41
41
  "name": "@jackens/nnn",
42
42
  "type": "module",
43
- "version": "2023.9.18"
43
+ "version": "2023.9.23"
44
44
  }