@jackens/nnn 2026.4.14 → 2026.8.24

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 (4) hide show
  1. package/nnn.d.ts +89 -89
  2. package/nnn.js +75 -76
  3. package/package.json +1 -1
  4. package/readme.md +529 -451
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # nnn
2
2
 
3
- A collection of Jackens’ JavaScript helper utilities (version: `2026.4.14`).
3
+ A collection of Jackens’ JavaScript helper utilities (version: `2026.8.24`).
4
4
 
5
5
  ## Installation
6
6
 
@@ -19,30 +19,29 @@ npm i @jackens/nnn
19
19
  ```js
20
20
  import {
21
21
  c,
22
- csvParse,
23
- fixPlTypography,
24
- h,
25
- hasOwn,
26
- isArray,
27
- isFiniteNumber,
28
- isInteger,
29
- isNumber,
30
- isRecord,
31
- isString,
32
- jsOnParse,
33
- monokai,
34
- nanolightTs,
35
- newEscape,
36
- newNounForm,
37
- newTokenizer,
38
- omit,
39
- pick,
40
- rwd,
41
- s,
42
- svgUse,
43
- uuidV1,
44
- vivify,
45
-
22
+ createEscape,
23
+ createPluralRules,
24
+ createTokenizer,
25
+ csvParse,
26
+ fixPlTypography,
27
+ h,
28
+ hasOwn,
29
+ isArray,
30
+ isFiniteNumber,
31
+ isInteger,
32
+ isNumber,
33
+ isRecord,
34
+ isString,
35
+ jsOnParse,
36
+ monokai,
37
+ nanolightTs,
38
+ omit,
39
+ pick,
40
+ rwd,
41
+ s,
42
+ svgUse,
43
+ uuidV1,
44
+ vivify,
46
45
  } from '@jackens/nnn' // or './node_modules/@jackens/nnn/nnn.js'
47
46
  ```
48
47
 
@@ -62,6 +61,16 @@ vivify,
62
61
  A minimal CSS-in-JS helper
63
62
  that converts a JavaScript object hierarchy
64
63
  into a CSS string.
64
+ - [`createEscape`](#createEscape):
65
+ Creates a tag function
66
+ for escaping interpolated values in template literals.
67
+ - [`createPluralRules`](#createPluralRules):
68
+ Creates a function that
69
+ returns the appropriate noun form
70
+ based on a numeric value using `Intl.PluralRules`.
71
+ - [`createTokenizer`](#createTokenizer):
72
+ A helper for building simple tokenizers
73
+ (see also [`nanolightTs`](#nanolightTs)).
65
74
  - [`csvParse`](#csvParse):
66
75
  Parses a CSV string
67
76
  into a two-dimensional array of strings.
@@ -99,17 +108,7 @@ vivify,
99
108
  and [`nanolightTs`](#nanolightTs) tokenizer.
100
109
  - [`nanolightTs`](#nanolightTs):
101
110
  A TypeScript/JavaScript syntax highlighting tokenizer
102
- built using [`newTokenizer`](#newTokenizer).
103
- - [`newEscape`](#newEscape):
104
- Creates a tag function
105
- for escaping interpolated values in template literals.
106
- - [`newNounForm`](#newNounForm):
107
- Creates a function that
108
- returns the appropriate noun form
109
- based on a numeric value using `Intl.PluralRules`.
110
- - [`newTokenizer`](#newTokenizer):
111
- A helper for building simple tokenizers
112
- (see also [`nanolightTs`](#nanolightTs)).
111
+ built using [`createTokenizer`](#createTokenizer).
113
112
  - [`omit`](#omit):
114
113
  Creates a new object excluding the specified keys
115
114
  from the source object.
@@ -270,7 +269,8 @@ const actual3 = c({
270
269
  '@font-face$$1': {
271
270
  fontFamily: 'Jackens',
272
271
  src$$1: 'url(otf/jackens.otf)',
273
- src$$2: "url(otf/jackens.otf) format('opentype')," +
272
+ src$$2:
273
+ "url(otf/jackens.otf) format('opentype')," +
274
274
  "url(svg/jackens.svg) format('svg')",
275
275
  font_weight: 'normal',
276
276
  'font-style': 'normal',
@@ -399,6 +399,294 @@ const expected6 = `
399
399
  expect(actual6).to.equal(expected6)
400
400
  ```
401
401
 
402
+ ### createEscape
403
+
404
+ ```ts
405
+ const createEscape: (escapeFn: (value: any) => string) => (template: TemplateStringsArray, ...values: unknown[]) => string;
406
+ ```
407
+
408
+ Creates a tag function
409
+ for escaping interpolated values in template literals.
410
+
411
+ #### escapeFn
412
+
413
+ A function that takes a value
414
+ and returns its escaped string representation.
415
+
416
+ #### Returns
417
+
418
+ A tag function that escapes interpolated values
419
+ using the provided escape map.
420
+
421
+ #### Usage Examples
422
+
423
+ ```ts
424
+ const escapeFn = (value: any): string =>
425
+ isArray(value)
426
+ ? value.map(escapeFn).join(', ')
427
+ : value === true || value === false
428
+ ? `b'${+value}'`
429
+ : value instanceof Date
430
+ ? `'${value
431
+ .toISOString()
432
+ .replace(/^(.+)T(.+)\..*$/, '$1 $2')}'`
433
+ : isFiniteNumber(value)
434
+ ? `${value}`
435
+ : isString(value)
436
+ ? `'${value.replace(/'/g, "''")}'`
437
+ : 'NULL'
438
+
439
+ const sql = createEscape(escapeFn)
440
+
441
+ const actual = sql`
442
+ SELECT *
443
+ FROM table_name
444
+ WHERE column_name IN (
445
+ ${[
446
+ true,
447
+ null,
448
+ undefined,
449
+ NaN,
450
+ Infinity,
451
+ 42,
452
+ '42',
453
+ "4'2",
454
+ /42/,
455
+ new Date(323325000000),
456
+ ]}
457
+ )`
458
+
459
+ const expected = `
460
+ SELECT *
461
+ FROM table_name
462
+ WHERE column_name IN (
463
+ b'1', NULL, NULL, NULL, NULL, 42, '42', '4''2', NULL, '1980-03-31 04:30:00'
464
+ )`
465
+
466
+ expect(actual).to.equal(expected)
467
+ ```
468
+
469
+ ### createPluralRules
470
+
471
+ ```ts
472
+ const createPluralRules: (locale: string, forms: Partial<Record<Intl.LDMLPluralRule, string>>) => (value: number) => string;
473
+ ```
474
+
475
+ Creates a function that
476
+ returns the appropriate noun form
477
+ based on a numeric value using `Intl.PluralRules`.
478
+
479
+ Different languages have different plural rules.
480
+ The `Intl.PluralRules` API provides locale-aware
481
+ plural category selection.
482
+ Possible categories are:
483
+
484
+ - `zero`: for zero items
485
+ (used in some languages like Arabic, Latvian)
486
+ - `one`: for singular (e.g., 1 item)
487
+ - `two`: for dual
488
+ (used in some languages like Arabic, Hebrew)
489
+ - `few`: for small plurals (e.g., 2-4 in Polish)
490
+ - `many`: for larger plurals (e.g., 5-21 in Polish)
491
+ - `other`: fallback category (used by all languages)
492
+
493
+ #### locale
494
+
495
+ A BCP 47 language tag (e.g., `pl`, `en`).
496
+
497
+ #### forms
498
+
499
+ An object mapping plural categories to noun forms.
500
+ Not all categories need to be provided;
501
+ if a category is missing,
502
+ the function falls back to `other`,
503
+ then to an empty string.
504
+
505
+ #### Returns
506
+
507
+ A function that takes a numeric value
508
+ and returns the appropriate noun form.
509
+
510
+ #### Usage Examples
511
+
512
+ ```ts
513
+ const auto = createPluralRules('pl', {
514
+ one: 'auto',
515
+ few: 'auta',
516
+ other: 'aut',
517
+ })
518
+
519
+ expect(auto(0)).to.equal('aut')
520
+ expect(auto(1)).to.equal('auto')
521
+ expect(auto(17)).to.equal('aut')
522
+ expect(auto(42)).to.equal('auta')
523
+
524
+ const car = createPluralRules('en', {
525
+ one: 'car',
526
+ other: 'cars',
527
+ })
528
+
529
+ expect(car(0)).to.equal('cars')
530
+ expect(car(1)).to.equal('car')
531
+ expect(car(17)).to.equal('cars')
532
+ expect(car(42)).to.equal('cars')
533
+
534
+ const empty = createPluralRules('en', {})
535
+
536
+ expect(empty(0)).to.equal('')
537
+ expect(empty(1)).to.equal('')
538
+ expect(empty(17)).to.equal('')
539
+ expect(empty(42)).to.equal('')
540
+ ```
541
+
542
+ ### createTokenizer
543
+
544
+ ```ts
545
+ const createTokenizer: <M, T>(decorator: (chunk: string, metadata?: M) => T, ...specs: [M, string | RegExp][]) => (code: string) => T[];
546
+ ```
547
+
548
+ A helper for building simple tokenizers
549
+ (see also [`nanolightTs`](#nanolightTs)).
550
+
551
+ #### decorator
552
+
553
+ A function that wraps each matched chunk.
554
+ It receives the matched text (`chunk`)
555
+ and optionally the `metadata`
556
+ associated with the pattern that produced the match.
557
+ For unmatched text between patterns,
558
+ `metadata` is `undefined`.
559
+
560
+ #### specs
561
+
562
+ An array of tuples `[metadata, pattern]` where:
563
+ - `metadata`: arbitrary data (e.g., a CSS class name)
564
+ passed to `decorator` when the pattern matches.
565
+ - `pattern`: a `string` or `RegExp`
566
+ to match against the input.
567
+
568
+ #### Returns
569
+
570
+ A tokenizer function that accepts a code string
571
+ and returns an array of decorated tokens.
572
+
573
+ #### Remarks
574
+
575
+ 1. Matches starting at an earlier position
576
+ take precedence.
577
+ 2. Among matches at the same position,
578
+ the longer one wins.
579
+ 3. Among matches of the same position and length,
580
+ the one defined earlier wins.
581
+
582
+ #### Usage Examples
583
+
584
+ ```ts
585
+ const tokenizer1 = createTokenizer(
586
+ (chunk, metadata) => ({ chunk, metadata }),
587
+ ['keyword', /\b(if|else|for)\b/],
588
+ ['string', /"[^"]*"/]
589
+ )
590
+
591
+ const result1 = tokenizer1('if "hello" else "world"')
592
+
593
+ expect(result1).to.deep.equal([
594
+ { chunk: 'if', metadata: 'keyword' },
595
+ { chunk: ' ', metadata: undefined },
596
+ { chunk: '"hello"', metadata: 'string' },
597
+ { chunk: ' ', metadata: undefined },
598
+ { chunk: 'else', metadata: 'keyword' },
599
+ { chunk: ' ', metadata: undefined },
600
+ { chunk: '"world"', metadata: 'string' },
601
+ ])
602
+
603
+ const tokenizer2 = createTokenizer(
604
+ (chunk, metadata) => `${metadata}:${chunk}`,
605
+ ['tag', 'BEGIN'],
606
+ ['end', 'END']
607
+ )
608
+
609
+ const result2 = tokenizer2('aBEGINbENDc')
610
+
611
+ expect(result2).to.deep.equal([
612
+ 'undefined:a',
613
+ 'tag:BEGIN',
614
+ 'undefined:b',
615
+ 'end:END',
616
+ 'undefined:c',
617
+ ])
618
+
619
+ const tokenizer3 = createTokenizer(
620
+ chunk => chunk,
621
+ ['test', /test/]
622
+ )
623
+
624
+ const result3 = tokenizer3('')
625
+
626
+ expect(result3).to.deep.equal([])
627
+
628
+ const tokenizer4 = createTokenizer(
629
+ (chunk, metadata) => ({ chunk, metadata }),
630
+ ['start', /^test/]
631
+ )
632
+
633
+ const result4 = tokenizer4('test here')
634
+
635
+ expect(result4).to.deep.equal([
636
+ { chunk: 'test', metadata: 'start' },
637
+ { chunk: ' here', metadata: undefined },
638
+ ])
639
+
640
+ const tokenizer5 = createTokenizer(
641
+ (chunk, metadata) => metadata,
642
+ ['later', /x/],
643
+ ['earlier', /y/]
644
+ )
645
+
646
+ const result5 = tokenizer5('yx')
647
+
648
+ expect(result5).to.deep.equal(['earlier', 'later'])
649
+
650
+ const tokenizer6 = createTokenizer(
651
+ chunk => chunk,
652
+ ['short', 'a'],
653
+ ['long', 'abc']
654
+ )
655
+
656
+ const result6 = tokenizer6('abc')
657
+
658
+ expect(result6).to.deep.equal(['abc'])
659
+
660
+ const tokenizer7 = createTokenizer(
661
+ chunk => chunk,
662
+ ['empty', ''],
663
+ ['word', /\w+/]
664
+ )
665
+
666
+ const result7 = tokenizer7('hello')
667
+
668
+ expect(result7).to.deep.equal(['hello'])
669
+
670
+ const tokenizer8 = createTokenizer(
671
+ chunk => chunk,
672
+ ['test', /xyz/]
673
+ )
674
+
675
+ const result8 = tokenizer8('abc')
676
+
677
+ expect(result8).to.deep.equal(['abc'])
678
+
679
+ const tokenizer9 = createTokenizer(
680
+ (_chunk, metadata) => metadata,
681
+ ['a', 'a'],
682
+ ['b', 'b']
683
+ )
684
+
685
+ const result9 = tokenizer9('aabb')
686
+
687
+ expect(result9).to.deep.equal(['a', 'a', 'b', 'b'])
688
+ ```
689
+
402
690
  ### csvParse
403
691
 
404
692
  ```ts
@@ -470,23 +758,25 @@ except those inside `IFRAME`, `NOSCRIPT`, `PRE`,
470
758
  #### Usage Examples
471
759
 
472
760
  ```ts
473
- const p = h('p',
761
+ const p = h(
762
+ 'p',
474
763
  'Pchnąć w tę łódź jeża lub ośm skrzyń fig ' +
475
- '(zob. https://pl.wikipedia.org/wiki/Pangram).',
764
+ '(zob. https://pl.wikipedia.org/wiki/Pangram).',
476
765
  ['br'],
477
- ['b', 'Zażółć gęślą jaźń.'],
766
+ ['b', 'Zażółć gęślą jaźń.']
478
767
  )
479
768
 
480
769
  fixPlTypography(p)
481
770
 
482
771
  expect(p.innerHTML).to.deep.equal(
483
772
  'Pchnąć ' +
484
- '<span style="white-space:nowrap">w </span>' +
485
- 'tę łódź jeża lub ośm skrzyń fig ' +
486
- '(zob. https://\u200Bpl.\u200Bwikipedia.\u200Borg/' +
487
- '\u200Bwiki/\u200BPangram).' +
488
- '<br>' +
489
- '<b>Zażółć gęślą jaźń.</b>')
773
+ '<span style="white-space:nowrap">w </span>' +
774
+ 'tę łódź jeża lub ośm skrzyń fig ' +
775
+ '(zob. https://\u200Bpl.\u200Bwikipedia.\u200Borg/' +
776
+ '\u200Bwiki/\u200BPangram).' +
777
+ '<br>' +
778
+ '<b>Zażółć gęślą jaźń.</b>'
779
+ )
490
780
  ```
491
781
 
492
782
  ### h
@@ -539,38 +829,46 @@ const i = h('i', 'text')
539
829
  h(b, i)
540
830
 
541
831
  expect(i.outerHTML).to.equal('<i>text</i>')
832
+
542
833
  expect(b.outerHTML).to.equal('<b><i>text</i></b>')
543
834
 
544
835
  h(i, { $className: 'some class' })
545
836
 
546
- expect(i.outerHTML)
547
- .to.equal('<i class="some class">text</i>')
548
- expect(b.outerHTML)
549
- .to.equal('<b><i class="some class">text</i></b>')
837
+ expect(i.outerHTML).to.equal(
838
+ '<i class="some class">text</i>'
839
+ )
550
840
 
551
- expect(h('span', 'text').outerHTML)
552
- .to.equal('<span>text</span>')
553
- expect(h('span', { $innerText: 'text' }).outerHTML)
554
- .to.equal('<span>text</span>')
841
+ expect(b.outerHTML).to.equal(
842
+ '<b><i class="some class">text</i></b>'
843
+ )
555
844
 
556
- expect(h('span', '42').outerHTML)
557
- .to.equal('<span>42</span>')
558
- expect(h('span', 42).outerHTML).to.equal('<span>42</span>')
845
+ expect(h('span', 'text').outerHTML).to.equal(
846
+ '<span>text</span>'
847
+ )
559
848
 
560
- expect(h('div', { style: 'margin:0;padding:0' }).outerHTML)
561
- .to.equal('<div style="margin:0;padding:0"></div>')
562
- expect(h('div', { $style: 'margin:0;padding:0' }).outerHTML)
563
- .to.equal(
564
- '<div style="margin: 0px; padding: 0px;"></div>',
565
- )
566
849
  expect(
567
- h(
568
- 'div',
569
- { $style: { margin: 0, padding: 0 } },
570
- ).outerHTML,
571
- ).to.equal('<div style="margin: 0px; padding: 0px;"></div>')
850
+ h('span', { $innerText: 'text' }).outerHTML
851
+ ).to.equal('<span>text</span>')
572
852
 
573
- const input1 = h('input', { value: 42 })
853
+ expect(h('span', '42').outerHTML).to.equal(
854
+ '<span>42</span>'
855
+ )
856
+
857
+ expect(h('span', 42).outerHTML).to.equal('<span>42</span>')
858
+
859
+ expect(
860
+ h('div', { style: 'margin:0;padding:0' }).outerHTML
861
+ ).to.equal('<div style="margin:0;padding:0"></div>')
862
+
863
+ expect(
864
+ h('div', { $style: 'margin:0;padding:0' }).outerHTML
865
+ ).to.equal('<div style="margin: 0px; padding: 0px;"></div>')
866
+
867
+ expect(
868
+ h('div', { $style: { margin: 0, padding: 0 } }).outerHTML
869
+ ).to.equal('<div style="margin: 0px; padding: 0px;"></div>')
870
+
871
+ const input1 = h('input', { value: 42 })
574
872
  const input2 = h('input', { $value: '42' })
575
873
 
576
874
  expect(input1.value).to.equal('42')
@@ -579,22 +877,26 @@ expect(input2.value).to.equal('42')
579
877
  expect(input1.outerHTML).to.equal('<input value="42">')
580
878
  expect(input2.outerHTML).to.equal('<input>')
581
879
 
582
- const checkbox1 = h(
583
- 'input',
584
- { type: 'checkbox', checked: true },
585
- )
586
- const checkbox2 = h(
587
- 'input',
588
- { type: 'checkbox', $checked: true },
589
- )
880
+ const checkbox1 = h('input', {
881
+ type: 'checkbox',
882
+ checked: true,
883
+ })
884
+
885
+ const checkbox2 = h('input', {
886
+ type: 'checkbox',
887
+ $checked: true,
888
+ })
590
889
 
591
890
  expect(checkbox1.checked).to.be.true
592
891
  expect(checkbox2.checked).to.be.true
593
892
 
594
- expect(checkbox1.outerHTML)
595
- .to.equal('<input type="checkbox" checked="">')
596
- expect(checkbox2.outerHTML)
597
- .to.equal('<input type="checkbox">')
893
+ expect(checkbox1.outerHTML).to.equal(
894
+ '<input type="checkbox" checked="">'
895
+ )
896
+
897
+ expect(checkbox2.outerHTML).to.equal(
898
+ '<input type="checkbox">'
899
+ )
598
900
 
599
901
  const div = h('div')
600
902
 
@@ -615,15 +917,20 @@ expect(elemWithClass.getAttribute('class')).to.equal('test')
615
917
  const elemWithText = h('div', 'initial')
616
918
 
617
919
  h(elemWithText, ' more')
618
- expect(elemWithText.outerHTML)
619
- .to.equal('<div>initial more</div>')
620
920
 
621
- const elemWithNested = h('div',
921
+ expect(elemWithText.outerHTML).to.equal(
922
+ '<div>initial more</div>'
923
+ )
924
+
925
+ const elemWithNested = h(
926
+ 'div',
622
927
  ['span', 'hello'],
623
- ['b', 'world'])
928
+ ['b', 'world']
929
+ )
624
930
 
625
- expect(elemWithNested.outerHTML)
626
- .to.equal('<div><span>hello</span><b>world</b></div>')
931
+ expect(elemWithNested.outerHTML).to.equal(
932
+ '<div><span>hello</span><b>world</b></div>'
933
+ )
627
934
  ```
628
935
 
629
936
  ### hasOwn
@@ -708,8 +1015,9 @@ const fakeArray1 = Object.create({ constructor: Array })
708
1015
 
709
1016
  expect(isArray(fakeArray1)).to.be.false
710
1017
 
711
- const fakeArray2 =
712
- Object.create({ [Symbol.toStringTag]: Array.name })
1018
+ const fakeArray2 = Object.create({
1019
+ [Symbol.toStringTag]: Array.name,
1020
+ })
713
1021
 
714
1022
  expect(isArray(fakeArray2)).to.be.false
715
1023
  ```
@@ -735,9 +1043,13 @@ The value to check.
735
1043
 
736
1044
  ```ts
737
1045
  expect(isFiniteNumber(42)).to.be.true
1046
+
738
1047
  expect(isFiniteNumber(Number(42))).to.be.true
1048
+
739
1049
  expect(isFiniteNumber(new Number(42))).to.be.false
1050
+
740
1051
  expect(isFiniteNumber(NaN)).to.be.false
1052
+
741
1053
  expect(isFiniteNumber(Infinity)).to.be.false
742
1054
  ```
743
1055
 
@@ -761,12 +1073,18 @@ The value to check.
761
1073
 
762
1074
  ```ts
763
1075
  expect(isInteger(42)).to.be.true
1076
+
764
1077
  expect(isInteger(42.00000000000001)).to.be.false
1078
+
765
1079
  expect(isInteger(42.000000000000001)).to.be.true
766
1080
  // ^because of loss of precision
1081
+
767
1082
  expect(isInteger(Number(42))).to.be.true
1083
+
768
1084
  expect(isInteger(new Number(42))).to.be.false
1085
+
769
1086
  expect(isInteger(NaN)).to.be.false
1087
+
770
1088
  expect(isInteger(Infinity)).to.be.false
771
1089
  ```
772
1090
 
@@ -791,9 +1109,13 @@ The value to check.
791
1109
 
792
1110
  ```ts
793
1111
  expect(isNumber(42)).to.be.true
1112
+
794
1113
  expect(isNumber(Number(42))).to.be.true
1114
+
795
1115
  expect(isNumber(new Number(42))).to.be.false
1116
+
796
1117
  expect(isNumber(NaN)).to.be.true
1118
+
797
1119
  expect(isNumber(Infinity)).to.be.true
798
1120
  ```
799
1121
 
@@ -818,22 +1140,26 @@ The value to check.
818
1140
 
819
1141
  ```ts
820
1142
  expect(isRecord({})).to.be.true
1143
+
821
1144
  expect(isRecord([])).to.be.false
1145
+
822
1146
  expect(isRecord(Object.create(null))).to.be.true
823
1147
 
824
1148
  const fakeNumber1 = Object.create({ constructor: Number })
825
1149
 
826
1150
  expect(isRecord(fakeNumber1)).to.be.true
827
1151
 
828
- const fakeNumber2 =
829
- Object.create({ [Symbol.toStringTag]: Number.name })
1152
+ const fakeNumber2 = Object.create({
1153
+ [Symbol.toStringTag]: Number.name,
1154
+ })
830
1155
 
831
1156
  expect(isRecord(fakeNumber2)).to.be.true
832
1157
 
833
1158
  expect(isRecord(new Number(42))).to.be.true
1159
+
834
1160
  expect(isRecord(new String('42'))).to.be.true
835
1161
 
836
- class FooBar { }
1162
+ class FooBar {}
837
1163
 
838
1164
  expect(isRecord(new FooBar())).to.be.true
839
1165
  ```
@@ -898,7 +1224,9 @@ const handlers = {
898
1224
  $foo: () => 'bar',
899
1225
  }
900
1226
 
901
- const actual1 = jsOnParse(handlers, `[
1227
+ const actual1 = jsOnParse(
1228
+ handlers,
1229
+ `[
902
1230
  {
903
1231
  "$add": [1, 2]
904
1232
  },
@@ -920,7 +1248,8 @@ const actual1 = jsOnParse(handlers, `[
920
1248
  "one": 1,
921
1249
  "two": 2
922
1250
  }
923
- ]`)
1251
+ ]`
1252
+ )
924
1253
 
925
1254
  expect(actual1).to.deep.equal([
926
1255
  3,
@@ -934,7 +1263,7 @@ expect(actual1).to.deep.equal([
934
1263
  {
935
1264
  $foo: [
936
1265
  'The parent object does not have ' +
937
- 'exactly one property!',
1266
+ 'exactly one property!',
938
1267
  ],
939
1268
  one: 1,
940
1269
  two: 2,
@@ -943,26 +1272,26 @@ expect(actual1).to.deep.equal([
943
1272
 
944
1273
  const actual2 = jsOnParse(
945
1274
  { $notFunc: 'handler not being a function' } as any,
946
- '{"$notFunc": [1, 2]}',
1275
+ '{"$notFunc": [1, 2]}'
947
1276
  )
948
1277
 
949
1278
  expect(actual2).to.deep.equal({ $notFunc: [1, 2] })
950
1279
 
951
1280
  const actual3 = jsOnParse(
952
1281
  handlers,
953
- '{"$unknown_handler_key": [1, 2]}',
1282
+ '{"$unknown_handler_key": [1, 2]}'
954
1283
  )
955
1284
 
956
- expect(actual3)
957
- .to.deep.equal({ $unknown_handler_key: [1, 2] })
1285
+ expect(actual3).to.deep.equal({
1286
+ $unknown_handler_key: [1, 2],
1287
+ })
958
1288
 
959
1289
  const actual4 = jsOnParse(
960
1290
  handlers,
961
- '{"$add": {"not": "array"}}',
1291
+ '{"$add": {"not": "array"}}'
962
1292
  )
963
1293
 
964
- expect(actual4)
965
- .to.deep.equal({ $add: { not: 'array' } })
1294
+ expect(actual4).to.deep.equal({ $add: { not: 'array' } })
966
1295
  ```
967
1296
 
968
1297
  ### monokai
@@ -982,7 +1311,7 @@ const nanolightTs: (code: string) => HArgs1[];
982
1311
  ```
983
1312
 
984
1313
  A TypeScript/JavaScript syntax highlighting tokenizer
985
- built using [`newTokenizer`](#newTokenizer).
1314
+ built using [`createTokenizer`](#createTokenizer).
986
1315
 
987
1316
  #### code
988
1317
 
@@ -1003,7 +1332,8 @@ const codeJs =
1003
1332
  expect(nanolightTs(codeJs)).to.deep.equal([
1004
1333
  ['span', { class: 'keyword-1' }, 'const'],
1005
1334
  ' ',
1006
- ['span',
1335
+ [
1336
+ 'span',
1007
1337
  { class: 'identifier-4' },
1008
1338
  'answerToLifeTheUniverseAndEverything',
1009
1339
  ],
@@ -1026,275 +1356,6 @@ expect(nanolightTs(codeJs)).to.deep.equal([
1026
1356
  ])
1027
1357
  ```
1028
1358
 
1029
- ### newEscape
1030
-
1031
- ```ts
1032
- const newEscape: (escapeFn: (value: any) => string) => (template: TemplateStringsArray, ...values: unknown[]) => string;
1033
- ```
1034
-
1035
- Creates a tag function
1036
- for escaping interpolated values in template literals.
1037
-
1038
- #### escapeFn
1039
-
1040
- A function that takes a value
1041
- and returns its escaped string representation.
1042
-
1043
- #### Returns
1044
-
1045
- A tag function that escapes interpolated values
1046
- using the provided escape map.
1047
-
1048
- #### Usage Examples
1049
-
1050
- ```ts
1051
- const escapeFn = (value: any): string =>
1052
- isArray(value)
1053
- ? value.map(escapeFn).join(', ')
1054
- : value === true || value === false
1055
- ? `b'${+value}'`
1056
- : value instanceof Date
1057
- ? `'${value.toISOString().replace(/^(.+)T(.+)\..*$/, '$1 $2')}'`
1058
- : isFiniteNumber(value)
1059
- ? `${value}`
1060
- : isString(value)
1061
- ? `'${value.replace(/'/g, "''")}'`
1062
- : 'NULL'
1063
-
1064
- const sql = newEscape(escapeFn)
1065
-
1066
- const actual = sql`
1067
- SELECT *
1068
- FROM table_name
1069
- WHERE column_name IN (${[
1070
- true,
1071
- null,
1072
- undefined,
1073
- NaN,
1074
- Infinity,
1075
- 42,
1076
- '42',
1077
- "4'2",
1078
- /42/,
1079
- new Date(323325000000),
1080
- ]})`
1081
-
1082
- const expected = `
1083
- SELECT *
1084
- FROM table_name
1085
- WHERE column_name IN (b'1', NULL, NULL, NULL, NULL, 42, '42', '4''2', NULL, '1980-03-31 04:30:00')`
1086
-
1087
- expect(actual).to.equal(expected)
1088
- ```
1089
-
1090
- ### newNounForm
1091
-
1092
- ```ts
1093
- const newNounForm: (locale: string, forms: Partial<Record<Intl.LDMLPluralRule, string>>) => (value: number) => string;
1094
- ```
1095
-
1096
- Creates a function that
1097
- returns the appropriate noun form
1098
- based on a numeric value using `Intl.PluralRules`.
1099
-
1100
- Different languages have different plural rules.
1101
- The `Intl.PluralRules` API provides locale-aware
1102
- plural category selection.
1103
- Possible categories are:
1104
-
1105
- - `zero`: for zero items
1106
- (used in some languages like Arabic, Latvian)
1107
- - `one`: for singular (e.g., 1 item)
1108
- - `two`: for dual
1109
- (used in some languages like Arabic, Hebrew)
1110
- - `few`: for small plurals (e.g., 2-4 in Polish)
1111
- - `many`: for larger plurals (e.g., 5-21 in Polish)
1112
- - `other`: fallback category (used by all languages)
1113
-
1114
- #### locale
1115
-
1116
- A BCP 47 language tag (e.g., `pl`, `en`).
1117
-
1118
- #### forms
1119
-
1120
- An object mapping plural categories to noun forms.
1121
- Not all categories need to be provided;
1122
- if a category is missing,
1123
- the function falls back to `other`,
1124
- then to an empty string.
1125
-
1126
- #### Returns
1127
-
1128
- A function that takes a numeric value
1129
- and returns the appropriate noun form.
1130
-
1131
- #### Usage Examples
1132
-
1133
- ```ts
1134
- const auto = newNounForm(
1135
- 'pl',
1136
- { one: 'auto', few: 'auta', other: 'aut' },
1137
- )
1138
-
1139
- expect(auto(0)).to.equal('aut')
1140
- expect(auto(1)).to.equal('auto')
1141
- expect(auto(17)).to.equal('aut')
1142
- expect(auto(42)).to.equal('auta')
1143
-
1144
- const car = newNounForm('en', { one: 'car', other: 'cars' })
1145
-
1146
- expect(car(0)).to.equal('cars')
1147
- expect(car(1)).to.equal('car')
1148
- expect(car(17)).to.equal('cars')
1149
- expect(car(42)).to.equal('cars')
1150
-
1151
- const empty = newNounForm('en', {})
1152
-
1153
- expect(empty(0)).to.equal('')
1154
- expect(empty(1)).to.equal('')
1155
- expect(empty(17)).to.equal('')
1156
- expect(empty(42)).to.equal('')
1157
- ```
1158
-
1159
- ### newTokenizer
1160
-
1161
- ```ts
1162
- const newTokenizer: <M, T>(decorator: (chunk: string, metadata?: M) => T, ...specs: [M, string | RegExp][]) => (code: string) => T[];
1163
- ```
1164
-
1165
- A helper for building simple tokenizers
1166
- (see also [`nanolightTs`](#nanolightTs)).
1167
-
1168
- #### decorator
1169
-
1170
- A function that wraps each matched chunk.
1171
- It receives the matched text (`chunk`)
1172
- and optionally the `metadata`
1173
- associated with the pattern that produced the match.
1174
- For unmatched text between patterns,
1175
- `metadata` is `undefined`.
1176
-
1177
- #### specs
1178
-
1179
- An array of tuples `[metadata, pattern]` where:
1180
- - `metadata`: arbitrary data (e.g., a CSS class name)
1181
- passed to `decorator` when the pattern matches.
1182
- - `pattern`: a `string` or `RegExp`
1183
- to match against the input.
1184
-
1185
- #### Returns
1186
-
1187
- A tokenizer function that accepts a code string
1188
- and returns an array of decorated tokens.
1189
-
1190
- #### Remarks
1191
-
1192
- 1. Matches starting at an earlier position
1193
- take precedence.
1194
- 2. Among matches at the same position,
1195
- the longer one wins.
1196
- 3. Among matches of the same position and length,
1197
- the one defined earlier wins.
1198
-
1199
- #### Usage Examples
1200
-
1201
- ```ts
1202
- const tokenizer1 = newTokenizer(
1203
- (chunk, metadata) => ({ chunk, metadata }),
1204
- ['keyword', /\b(if|else|for)\b/],
1205
- ['string', /"[^"]*"/],
1206
- )
1207
- const result1 = tokenizer1('if "hello" else "world"')
1208
-
1209
- expect(result1).to.deep.equal([
1210
- { chunk: 'if', metadata: 'keyword' },
1211
- { chunk: ' ', metadata: undefined },
1212
- { chunk: '"hello"', metadata: 'string' },
1213
- { chunk: ' ', metadata: undefined },
1214
- { chunk: 'else', metadata: 'keyword' },
1215
- { chunk: ' ', metadata: undefined },
1216
- { chunk: '"world"', metadata: 'string' },
1217
- ])
1218
-
1219
- const tokenizer2 = newTokenizer(
1220
- (chunk, metadata) => `${metadata}:${chunk}`,
1221
- ['tag', 'BEGIN'],
1222
- ['end', 'END'],
1223
- )
1224
- const result2 = tokenizer2('aBEGINbENDc')
1225
-
1226
- expect(result2).to.deep.equal([
1227
- 'undefined:a',
1228
- 'tag:BEGIN',
1229
- 'undefined:b',
1230
- 'end:END',
1231
- 'undefined:c',
1232
- ])
1233
-
1234
- const tokenizer3 = newTokenizer(
1235
- (chunk) => chunk,
1236
- ['test', /test/],
1237
- )
1238
- const result3 = tokenizer3('')
1239
-
1240
- expect(result3).to.deep.equal([])
1241
-
1242
- const tokenizer4 = newTokenizer(
1243
- (chunk, metadata) => ({ chunk, metadata }),
1244
- ['start', /^test/],
1245
- )
1246
- const result4 = tokenizer4('test here')
1247
-
1248
- expect(result4).to.deep.equal([
1249
- { chunk: 'test', metadata: 'start' },
1250
- { chunk: ' here', metadata: undefined },
1251
- ])
1252
-
1253
- const tokenizer5 = newTokenizer(
1254
- (chunk, metadata) => metadata,
1255
- ['later', /x/],
1256
- ['earlier', /y/],
1257
- )
1258
- const result5 = tokenizer5('yx')
1259
-
1260
- expect(result5).to.deep.equal(['earlier', 'later'])
1261
-
1262
- const tokenizer6 = newTokenizer(
1263
- (chunk) => chunk,
1264
- ['short', 'a'],
1265
- ['long', 'abc'],
1266
- )
1267
- const result6 = tokenizer6('abc')
1268
-
1269
- expect(result6).to.deep.equal(['abc'])
1270
-
1271
- const tokenizer7 = newTokenizer(
1272
- (chunk) => chunk,
1273
- ['empty', ''],
1274
- ['word', /\w+/],
1275
- )
1276
- const result7 = tokenizer7('hello')
1277
-
1278
- expect(result7).to.deep.equal(['hello'])
1279
-
1280
- const tokenizer8 = newTokenizer(
1281
- (chunk) => chunk,
1282
- ['test', /xyz/],
1283
- )
1284
- const result8 = tokenizer8('abc')
1285
-
1286
- expect(result8).to.deep.equal(['abc'])
1287
-
1288
- const tokenizer9 = newTokenizer(
1289
- (_chunk, metadata) => metadata,
1290
- ['a', 'a'],
1291
- ['b', 'b'],
1292
- )
1293
- const result9 = tokenizer9('aabb')
1294
-
1295
- expect(result9).to.deep.equal(['a', 'a', 'b', 'b'])
1296
- ```
1297
-
1298
1359
  ### omit
1299
1360
 
1300
1361
  ```ts
@@ -1358,9 +1419,10 @@ A new object with only the specified keys.
1358
1419
  ```ts
1359
1420
  const ref = { a: 42, b: '42', c: 17 }
1360
1421
 
1361
- expect(pick(ref, ['a', 'b'])).to.deep.equal(
1362
- { a: 42, b: '42' },
1363
- )
1422
+ expect(pick(ref, ['a', 'b'])).to.deep.equal({
1423
+ a: 42,
1424
+ b: '42',
1425
+ })
1364
1426
  ```
1365
1427
 
1366
1428
  ### rwd
@@ -1415,36 +1477,34 @@ const style: CRoot = {
1415
1477
 
1416
1478
  rwd(style, '.r6', 200, 50, [6], [3], [1, 1, 2])
1417
1479
 
1418
- expect(style).to.deep.equal(
1419
- {
1420
- body: {
1421
- margin: 0,
1480
+ expect(style).to.deep.equal({
1481
+ body: {
1482
+ margin: 0,
1483
+ },
1484
+ '.r6': {
1485
+ border: 'solid red 1px',
1486
+ '.no-border': {
1487
+ border: 'none',
1422
1488
  },
1489
+ boxSizing: 'border-box',
1490
+ display: 'block',
1491
+ float: 'left',
1492
+ width: '100%',
1493
+ height: '100px',
1494
+ },
1495
+ '@media(min-width:600px)': {
1423
1496
  '.r6': {
1424
- border: 'solid red 1px',
1425
- '.no-border': {
1426
- border: 'none',
1427
- },
1428
- boxSizing: 'border-box',
1429
- display: 'block',
1430
- float: 'left',
1431
- width: '100%',
1432
- height: '100px',
1433
- },
1434
- '@media(min-width:600px)': {
1435
- '.r6': {
1436
- width: 'calc(100% / 3)',
1437
- height: '50px',
1438
- },
1497
+ width: 'calc(100% / 3)',
1498
+ height: '50px',
1439
1499
  },
1440
- '@media(min-width:1200px)': {
1441
- '.r6': {
1442
- width: 'calc(50% / 3)',
1443
- height: '50px',
1444
- },
1500
+ },
1501
+ '@media(min-width:1200px)': {
1502
+ '.r6': {
1503
+ width: 'calc(50% / 3)',
1504
+ height: '50px',
1445
1505
  },
1446
1506
  },
1447
- )
1507
+ })
1448
1508
  ```
1449
1509
 
1450
1510
  ### s
@@ -1498,18 +1558,19 @@ const svg2 = s('svg', { 'xlink:href': false })
1498
1558
 
1499
1559
  expect(svg2.getAttributeNS(XLINK_NS, 'href')).to.be.null
1500
1560
 
1501
- const svg3 = s(
1502
- 'svg',
1503
- { 'xlink:href': 'http://example.com' },
1504
- )
1561
+ const svg3 = s('svg', {
1562
+ 'xlink:href': 'http://example.com',
1563
+ })
1505
1564
 
1506
- expect(svg3.getAttributeNS(XLINK_NS, 'href'))
1507
- .to.equal('http://example.com')
1565
+ expect(svg3.getAttributeNS(XLINK_NS, 'href')).to.equal(
1566
+ 'http://example.com'
1567
+ )
1508
1568
 
1509
1569
  const svg4 = s('svg', { 'xlink:title': 42 })
1510
1570
 
1511
- expect(svg4.getAttributeNS(XLINK_NS, 'title'))
1512
- .to.equal('42')
1571
+ expect(svg4.getAttributeNS(XLINK_NS, 'title')).to.equal(
1572
+ '42'
1573
+ )
1513
1574
  ```
1514
1575
 
1515
1576
  ### svgUse
@@ -1548,51 +1609,63 @@ expect(svg.children.length).to.equal(1)
1548
1609
 
1549
1610
  const useElement = svg.children[0]
1550
1611
 
1551
- expect(useElement.getAttributeNS(XLINK_NS, 'href'))
1552
- .to.equal('#icon-home')
1612
+ expect(
1613
+ useElement.getAttributeNS(XLINK_NS, 'href')
1614
+ ).to.equal('#icon-home')
1553
1615
 
1554
- const svgWithViewBox =
1555
- svgUse('icon-star', { viewBox: '0 0 24 24' })
1616
+ const svgWithViewBox = svgUse('icon-star', {
1617
+ viewBox: '0 0 24 24',
1618
+ })
1556
1619
 
1557
- expect(svgWithViewBox.getAttribute('viewBox'))
1558
- .to.equal('0 0 24 24')
1620
+ expect(svgWithViewBox.getAttribute('viewBox')).to.equal(
1621
+ '0 0 24 24'
1622
+ )
1559
1623
 
1560
1624
  const useViewBox = svgWithViewBox.children[0]
1561
1625
 
1562
- expect(useViewBox.getAttributeNS(XLINK_NS, 'href'))
1563
- .to.equal('#icon-star')
1626
+ expect(
1627
+ useViewBox.getAttributeNS(XLINK_NS, 'href')
1628
+ ).to.equal('#icon-star')
1564
1629
 
1565
- const svgWithClass =
1566
- svgUse('icon-menu', { class: 'icon-btn' })
1630
+ const svgWithClass = svgUse('icon-menu', {
1631
+ class: 'icon-btn',
1632
+ })
1567
1633
 
1568
- expect(svgWithClass.getAttribute('class'))
1569
- .to.equal('icon-btn')
1634
+ expect(svgWithClass.getAttribute('class')).to.equal(
1635
+ 'icon-btn'
1636
+ )
1570
1637
 
1571
1638
  expect(svgWithClass.children.length).to.equal(1)
1572
1639
 
1573
1640
  const useClass = svgWithClass.children[0]
1574
1641
 
1575
- expect(useClass.getAttributeNS(XLINK_NS, 'href'))
1576
- .to.equal('#icon-menu')
1577
-
1578
- const svgWithMultipleAttrs = svgUse(
1579
- 'icon-settings',
1580
- { width: 24, height: 24, class: 'icon' },
1642
+ expect(useClass.getAttributeNS(XLINK_NS, 'href')).to.equal(
1643
+ '#icon-menu'
1581
1644
  )
1582
1645
 
1583
- expect(svgWithMultipleAttrs.getAttribute('width'))
1584
- .to.equal('24')
1646
+ const svgWithMultipleAttrs = svgUse('icon-settings', {
1647
+ width: 24,
1648
+ height: 24,
1649
+ class: 'icon',
1650
+ })
1651
+
1652
+ expect(svgWithMultipleAttrs.getAttribute('width')).to.equal(
1653
+ '24'
1654
+ )
1585
1655
 
1586
- expect(svgWithMultipleAttrs.getAttribute('height'))
1587
- .to.equal('24')
1656
+ expect(
1657
+ svgWithMultipleAttrs.getAttribute('height')
1658
+ ).to.equal('24')
1588
1659
 
1589
- expect(svgWithMultipleAttrs.getAttribute('class'))
1590
- .to.equal('icon')
1660
+ expect(svgWithMultipleAttrs.getAttribute('class')).to.equal(
1661
+ 'icon'
1662
+ )
1591
1663
 
1592
1664
  const useMultiple = svgWithMultipleAttrs.children[0]
1593
1665
 
1594
- expect(useMultiple.getAttributeNS(XLINK_NS, 'href'))
1595
- .to.equal('#icon-settings')
1666
+ expect(
1667
+ useMultiple.getAttributeNS(XLINK_NS, 'href')
1668
+ ).to.equal('#icon-settings')
1596
1669
  ```
1597
1670
 
1598
1671
  ### uuidV1
@@ -1656,17 +1729,19 @@ for (let i = 1; i <= 22136; ++i) {
1656
1729
  }
1657
1730
  }
1658
1731
 
1659
- expect(uuidV1(new Date(), '000123456789abc').split('-')[4])
1660
- .to.equal('123456789abc')
1732
+ expect(
1733
+ uuidV1(new Date(), '000123456789abc').split('-')[4]
1734
+ ).to.equal('123456789abc')
1661
1735
 
1662
- expect(uuidV1(new Date(), '123456789').split('-')[4])
1663
- .to.equal('000123456789')
1736
+ expect(
1737
+ uuidV1(new Date(), '123456789').split('-')[4]
1738
+ ).to.equal('000123456789')
1664
1739
 
1665
1740
  expect(
1666
- uuidV1(new Date(323325000000))
1667
- .startsWith('c1399400-9a71-11bd'),
1668
- )
1669
- .to.be.true
1741
+ uuidV1(new Date(323325000000)).startsWith(
1742
+ 'c1399400-9a71-11bd'
1743
+ )
1744
+ ).to.be.true
1670
1745
  ```
1671
1746
 
1672
1747
  ### vivify
@@ -1777,8 +1852,8 @@ delete vivify(ref).one.two
1777
1852
 
1778
1853
  expect(ref).to.deep.equal({ one: {} })
1779
1854
 
1780
- expect(vivify(ref).one.toString instanceof Function)
1781
- .to.be.true
1855
+ expect(vivify(ref).one.toString instanceof Function).to.be
1856
+ .true
1782
1857
 
1783
1858
  expect(vivify(ref).one.toString.name).to.equal('toString')
1784
1859
 
@@ -1798,14 +1873,17 @@ expect(ref).to.deep.equal({ one: { two: { three: {} } } })
1798
1873
 
1799
1874
  vivify(ref).one.two.three.four = 5
1800
1875
 
1801
- expect(ref)
1802
- .to.deep.equal({ one: { two: { three: { four: 5 } } } })
1876
+ expect(ref).to.deep.equal({
1877
+ one: { two: { three: { four: 5 } } },
1878
+ })
1803
1879
 
1804
- expect(vivify(ref).one.two.three.four.toString.name)
1805
- .to.equal('toString')
1880
+ expect(
1881
+ vivify(ref).one.two.three.four.toString.name
1882
+ ).to.equal('toString')
1806
1883
 
1807
- expect(ref)
1808
- .to.deep.equal({ one: { two: { three: { four: 5 } } } })
1884
+ expect(ref).to.deep.equal({
1885
+ one: { two: { three: { four: 5 } } },
1886
+ })
1809
1887
 
1810
1888
  const u = undefined
1811
1889