@jackens/nnn 2026.4.15 → 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 +828 -787
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # nnn
2
2
 
3
- A collection of Jackens’ JavaScript helper utilities (version: `2026.4.15`).
3
+ A collection of Jackens’ JavaScript helper utilities (version: `2026.8.24`).
4
4
 
5
5
  ## Installation
6
6
 
@@ -19,6 +19,9 @@ npm i @jackens/nnn
19
19
  ```js
20
20
  import {
21
21
  c,
22
+ createEscape,
23
+ createPluralRules,
24
+ createTokenizer,
22
25
  csvParse,
23
26
  fixPlTypography,
24
27
  h,
@@ -32,9 +35,6 @@ import {
32
35
  jsOnParse,
33
36
  monokai,
34
37
  nanolightTs,
35
- newEscape,
36
- newNounForm,
37
- newTokenizer,
38
38
  omit,
39
39
  pick,
40
40
  rwd,
@@ -61,6 +61,16 @@ import {
61
61
  A minimal CSS-in-JS helper
62
62
  that converts a JavaScript object hierarchy
63
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)).
64
74
  - [`csvParse`](#csvParse):
65
75
  Parses a CSV string
66
76
  into a two-dimensional array of strings.
@@ -98,17 +108,7 @@ import {
98
108
  and [`nanolightTs`](#nanolightTs) tokenizer.
99
109
  - [`nanolightTs`](#nanolightTs):
100
110
  A TypeScript/JavaScript syntax highlighting tokenizer
101
- built using [`newTokenizer`](#newTokenizer).
102
- - [`newEscape`](#newEscape):
103
- Creates a tag function
104
- for escaping interpolated values in template literals.
105
- - [`newNounForm`](#newNounForm):
106
- Creates a function that
107
- returns the appropriate noun form
108
- based on a numeric value using `Intl.PluralRules`.
109
- - [`newTokenizer`](#newTokenizer):
110
- A helper for building simple tokenizers
111
- (see also [`nanolightTs`](#nanolightTs)).
111
+ built using [`createTokenizer`](#createTokenizer).
112
112
  - [`omit`](#omit):
113
113
  Creates a new object excluding the specified keys
114
114
  from the source object.
@@ -215,16 +215,14 @@ A CSS string representing the compiled rules.
215
215
  #### Usage Examples
216
216
 
217
217
  ```ts
218
- const actual1 = c(
219
- {
220
- a: {
221
- color: 'red',
222
- margin: 1,
223
- '.c': { margin: 2, padding: 2 },
224
- padding: 1,
225
- },
218
+ const actual1 = c({
219
+ a: {
220
+ color: 'red',
221
+ margin: 1,
222
+ '.c': { margin: 2, padding: 2 },
223
+ padding: 1,
226
224
  },
227
- )
225
+ })
228
226
 
229
227
  const expected1 = `
230
228
  a{
@@ -241,18 +239,16 @@ a{
241
239
 
242
240
  expect(actual1).to.equal(expected1)
243
241
 
244
- const actual2 = c(
245
- {
246
- a: {
247
- '.b': {
248
- color: 'red',
249
- margin: 1,
250
- '.c': { margin: 2, padding: 2 },
251
- padding: 1,
252
- },
242
+ const actual2 = c({
243
+ a: {
244
+ '.b': {
245
+ color: 'red',
246
+ margin: 1,
247
+ '.c': { margin: 2, padding: 2 },
248
+ padding: 1,
253
249
  },
254
250
  },
255
- )
251
+ })
256
252
 
257
253
  const expected2 = `
258
254
  a.b{
@@ -269,36 +265,35 @@ a.b{
269
265
 
270
266
  expect(actual2).to.equal(expected2)
271
267
 
272
- const actual3 = c(
273
- {
274
- '@font-face$$1': {
275
- fontFamily: 'Jackens',
276
- src$$1: 'url(otf/jackens.otf)',
277
- src$$2: "url(otf/jackens.otf) format('opentype')," +
268
+ const actual3 = c({
269
+ '@font-face$$1': {
270
+ fontFamily: 'Jackens',
271
+ src$$1: 'url(otf/jackens.otf)',
272
+ src$$2:
273
+ "url(otf/jackens.otf) format('opentype')," +
278
274
  "url(svg/jackens.svg) format('svg')",
279
- font_weight: 'normal',
280
- 'font-style': 'normal',
281
- },
282
- '@font-face$$2': {
283
- font_family: 'C64',
284
- src: 'url(fonts/C64_Pro_Mono-STYLE.woff)',
285
- },
286
- '@keyframes spin': {
287
- '0%': { transform: 'rotate(0deg)' },
288
- '100%': { transform: 'rotate(360deg)' },
289
- },
290
- div: {
291
- border: 'solid red 1px',
292
- '.c1': { 'background-color': '#000' },
293
- ' .c1': { background_color: 'black' },
294
- '.c2': { backgroundColor: 'rgb(0,0,0)' },
295
- },
296
- '@media(min-width:200px)': {
297
- div: { margin: 0, padding: 0 },
298
- span: { color: '#000' },
299
- },
275
+ font_weight: 'normal',
276
+ 'font-style': 'normal',
300
277
  },
301
- )
278
+ '@font-face$$2': {
279
+ font_family: 'C64',
280
+ src: 'url(fonts/C64_Pro_Mono-STYLE.woff)',
281
+ },
282
+ '@keyframes spin': {
283
+ '0%': { transform: 'rotate(0deg)' },
284
+ '100%': { transform: 'rotate(360deg)' },
285
+ },
286
+ div: {
287
+ border: 'solid red 1px',
288
+ '.c1': { 'background-color': '#000' },
289
+ ' .c1': { background_color: 'black' },
290
+ '.c2': { backgroundColor: 'rgb(0,0,0)' },
291
+ },
292
+ '@media(min-width:200px)': {
293
+ div: { margin: 0, padding: 0 },
294
+ span: { color: '#000' },
295
+ },
296
+ })
302
297
 
303
298
  const expected3 = `
304
299
  @font-face{
@@ -344,18 +339,16 @@ div.c2{
344
339
 
345
340
  expect(actual3).to.equal(expected3)
346
341
 
347
- const actual4 = c(
348
- {
349
- a: {
350
- '.b,.c': {
351
- margin: 1,
352
- '.d': {
353
- margin: 2,
354
- },
342
+ const actual4 = c({
343
+ a: {
344
+ '.b,.c': {
345
+ margin: 1,
346
+ '.d': {
347
+ margin: 2,
355
348
  },
356
349
  },
357
350
  },
358
- )
351
+ })
359
352
 
360
353
  const expected4 = `
361
354
  a.b,a.c{
@@ -367,16 +360,14 @@ a.b.d,a.c.d{
367
360
 
368
361
  expect(actual4).to.equal(expected4)
369
362
 
370
- const actual5 = c(
371
- {
372
- '.b,.c': {
373
- margin: 1,
374
- '.d': {
375
- margin: 2,
376
- },
363
+ const actual5 = c({
364
+ '.b,.c': {
365
+ margin: 1,
366
+ '.d': {
367
+ margin: 2,
377
368
  },
378
369
  },
379
- )
370
+ })
380
371
 
381
372
  const expected5 = `
382
373
  .b,.c{
@@ -388,16 +379,14 @@ const expected5 = `
388
379
 
389
380
  expect(actual5).to.equal(expected5)
390
381
 
391
- const actual6 = c(
392
- {
393
- '.a,.b': {
394
- margin: 1,
395
- '.c,.d': {
396
- margin: 2,
397
- },
382
+ const actual6 = c({
383
+ '.a,.b': {
384
+ margin: 1,
385
+ '.c,.d': {
386
+ margin: 2,
398
387
  },
399
388
  },
400
- )
389
+ })
401
390
 
402
391
  const expected6 = `
403
392
  .a,.b{
@@ -410,204 +399,504 @@ const expected6 = `
410
399
  expect(actual6).to.equal(expected6)
411
400
  ```
412
401
 
413
- ### csvParse
402
+ ### createEscape
414
403
 
415
404
  ```ts
416
- const csvParse: (csv: string, separator?: string) => string[][];
405
+ const createEscape: (escapeFn: (value: any) => string) => (template: TemplateStringsArray, ...values: unknown[]) => string;
417
406
  ```
418
407
 
419
- Parses a CSV string
420
- into a two-dimensional array of strings.
421
-
422
- Supports quoted fields with escaped double quotes (`""`).
423
- Carriage returns are normalized.
424
-
425
- #### csv
426
-
427
- The CSV string to parse.
408
+ Creates a tag function
409
+ for escaping interpolated values in template literals.
428
410
 
429
- #### separator
411
+ #### escapeFn
430
412
 
431
- The field delimiter (default: `','`).
413
+ A function that takes a value
414
+ and returns its escaped string representation.
432
415
 
433
416
  #### Returns
434
417
 
435
- A 2D array where each inner array
436
- represents a row of fields.
418
+ A tag function that escapes interpolated values
419
+ using the provided escape map.
437
420
 
438
421
  #### Usage Examples
439
422
 
440
423
  ```ts
441
- const text = `"aaa
442
- ""aaa""
443
- aaa",bbb, "ccc,ccc"
444
- "xxx,xxx", "yyy
445
- yyy",zzz
446
- 42 , "42" , 17
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'
447
438
 
448
- `
439
+ const sql = createEscape(escapeFn)
449
440
 
450
- expect(csvParse(text)).to.deep.equal(
451
- [
452
- ['aaa\n"aaa"\naaa', 'bbb', 'ccc,ccc'],
453
- ['xxx,xxx', 'yyy\nyyy', 'zzz'],
454
- [' 42 ', '42', ' 17'],
455
- ],
456
- )
457
- ```
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
458
 
459
- ### fixPlTypography
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
+ )`
460
465
 
461
- ```ts
462
- const fixPlTypography: (node: Node) => void;
466
+ expect(actual).to.equal(expected)
463
467
  ```
464
468
 
465
- Applies Polish-specific typographic corrections
466
- to a DOM subtree.
467
-
468
- This function prevents orphaned conjunctions
469
- (single-letter words like “a”, “i”, “o”, “u”, “w”, “z”)
470
- from appearing at the end of a line
471
- by wrapping them with the following word
472
- in a non-breaking span.
473
- It also inserts zero-width spaces
474
- after slashes and dots to allow line breaks.
475
-
476
- #### node
477
-
478
- The root DOM node to process.
479
- All descendant text nodes are corrected recursively,
480
- except those inside `IFRAME`, `NOSCRIPT`, `PRE`,
481
- `SCRIPT`, `STYLE`, or `TEXTAREA` elements.
482
-
483
- #### Usage Examples
469
+ ### createPluralRules
484
470
 
485
471
  ```ts
486
- const p = h('p',
487
- 'Pchnąć w tę łódź jeża lub ośm skrzyń fig ' +
488
- '(zob. https://pl.wikipedia.org/wiki/Pangram).',
489
- ['br'],
490
- ['b', 'Zażółć gęślą jaźń.'],
491
- )
492
-
493
- fixPlTypography(p)
494
-
495
- expect(p.innerHTML).to.deep.equal(
496
- 'Pchnąć ' +
497
- '<span style="white-space:nowrap">w </span>' +
498
- 'tę łódź jeża lub ośm skrzyń fig ' +
499
- '(zob. https://\u200Bpl.\u200Bwikipedia.\u200Borg/' +
500
- '\u200Bwiki/\u200BPangram).' +
501
- '<br>' +
502
- '<b>Zażółć gęślą jaźń.</b>')
472
+ const createPluralRules: (locale: string, forms: Partial<Record<Intl.LDMLPluralRule, string>>) => (value: number) => string;
503
473
  ```
504
474
 
505
- ### h
475
+ Creates a function that
476
+ returns the appropriate noun form
477
+ based on a numeric value using `Intl.PluralRules`.
506
478
 
507
- ```ts
508
- const h: {
509
- <T extends keyof HTMLElementTagNameMap>(tag: T, ...args1: HArgs1[]): HTMLElementTagNameMap[T];
510
- <N extends Node>(node: N, ...args1: HArgs1[]): N;
511
- (tagOrNode: string | Node, ...args1: HArgs1[]): Node;
512
- };
513
- ```
479
+ Different languages have different plural rules.
480
+ The `Intl.PluralRules` API provides locale-aware
481
+ plural category selection.
482
+ Possible categories are:
514
483
 
515
- A lightweight *HyperScript*-style helper
516
- for creating and modifying `HTMLElement`s
517
- (see also [`s`](#s)).
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)
518
492
 
519
- #### tagOrNode
493
+ #### locale
520
494
 
521
- If a `string`, it is treated as the tag name
522
- for a new element.
523
- If a `Node`, that node is modified in place.
495
+ A BCP 47 language tag (e.g., `pl`, `en`).
524
496
 
525
- #### args
497
+ #### forms
526
498
 
527
- Additional arguments processed as follows:
528
- - `Object`: maps attributes/properties.
529
- Keys starting with `$` set element properties
530
- (without the `$` prefix);
531
- other keys set attributes via `setAttribute`.
532
- A value of `false` removes the attribute.
533
- - `null`/`undefined`: ignored.
534
- - `Node`: appended as a child.
535
- - `string`/`number`: converted to a `Text` node
536
- and appended.
537
- - [`HArgs`](#HArgs) array: processed recursively.
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.
538
504
 
539
505
  #### Returns
540
506
 
541
- The created or modified `HTMLElement`.
507
+ A function that takes a numeric value
508
+ and returns the appropriate noun form.
542
509
 
543
510
  #### Usage Examples
544
511
 
545
512
  ```ts
546
- const b = h('b')
513
+ const auto = createPluralRules('pl', {
514
+ one: 'auto',
515
+ few: 'auta',
516
+ other: 'aut',
517
+ })
547
518
 
548
- expect(b.outerHTML).to.equal('<b></b>')
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')
549
523
 
550
- const i = h('i', 'text')
524
+ const car = createPluralRules('en', {
525
+ one: 'car',
526
+ other: 'cars',
527
+ })
551
528
 
552
- h(b, i)
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')
553
533
 
554
- expect(i.outerHTML).to.equal('<i>text</i>')
555
- expect(b.outerHTML).to.equal('<b><i>text</i></b>')
534
+ const empty = createPluralRules('en', {})
556
535
 
557
- h(i, { $className: 'some class' })
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
+ ```
558
541
 
559
- expect(i.outerHTML)
560
- .to.equal('<i class="some class">text</i>')
561
- expect(b.outerHTML)
562
- .to.equal('<b><i class="some class">text</i></b>')
542
+ ### createTokenizer
563
543
 
564
- expect(h('span', 'text').outerHTML)
565
- .to.equal('<span>text</span>')
566
- expect(h('span', { $innerText: 'text' }).outerHTML)
567
- .to.equal('<span>text</span>')
544
+ ```ts
545
+ const createTokenizer: <M, T>(decorator: (chunk: string, metadata?: M) => T, ...specs: [M, string | RegExp][]) => (code: string) => T[];
546
+ ```
568
547
 
569
- expect(h('span', '42').outerHTML)
570
- .to.equal('<span>42</span>')
571
- expect(h('span', 42).outerHTML).to.equal('<span>42</span>')
548
+ A helper for building simple tokenizers
549
+ (see also [`nanolightTs`](#nanolightTs)).
572
550
 
573
- expect(h('div', { style: 'margin:0;padding:0' }).outerHTML)
574
- .to.equal('<div style="margin:0;padding:0"></div>')
575
- expect(h('div', { $style: 'margin:0;padding:0' }).outerHTML)
576
- .to.equal(
577
- '<div style="margin: 0px; padding: 0px;"></div>',
578
- )
579
- expect(
580
- h(
581
- 'div',
582
- { $style: { margin: 0, padding: 0 } },
583
- ).outerHTML,
584
- ).to.equal('<div style="margin: 0px; padding: 0px;"></div>')
551
+ #### decorator
585
552
 
586
- const input1 = h('input', { value: 42 })
587
- const input2 = h('input', { $value: '42' })
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`.
588
559
 
589
- expect(input1.value).to.equal('42')
590
- expect(input2.value).to.equal('42')
560
+ #### specs
591
561
 
592
- expect(input1.outerHTML).to.equal('<input value="42">')
593
- expect(input2.outerHTML).to.equal('<input>')
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.
594
567
 
595
- const checkbox1 = h(
596
- 'input',
597
- { type: 'checkbox', checked: true },
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
+
690
+ ### csvParse
691
+
692
+ ```ts
693
+ const csvParse: (csv: string, separator?: string) => string[][];
694
+ ```
695
+
696
+ Parses a CSV string
697
+ into a two-dimensional array of strings.
698
+
699
+ Supports quoted fields with escaped double quotes (`""`).
700
+ Carriage returns are normalized.
701
+
702
+ #### csv
703
+
704
+ The CSV string to parse.
705
+
706
+ #### separator
707
+
708
+ The field delimiter (default: `','`).
709
+
710
+ #### Returns
711
+
712
+ A 2D array where each inner array
713
+ represents a row of fields.
714
+
715
+ #### Usage Examples
716
+
717
+ ```ts
718
+ const text = `"aaa
719
+ ""aaa""
720
+ aaa",bbb, "ccc,ccc"
721
+ "xxx,xxx", "yyy
722
+ yyy",zzz
723
+ 42 , "42" , 17
724
+
725
+ `
726
+
727
+ expect(csvParse(text)).to.deep.equal([
728
+ ['aaa\n"aaa"\naaa', 'bbb', 'ccc,ccc'],
729
+ ['xxx,xxx', 'yyy\nyyy', 'zzz'],
730
+ [' 42 ', '42', ' 17'],
731
+ ])
732
+ ```
733
+
734
+ ### fixPlTypography
735
+
736
+ ```ts
737
+ const fixPlTypography: (node: Node) => void;
738
+ ```
739
+
740
+ Applies Polish-specific typographic corrections
741
+ to a DOM subtree.
742
+
743
+ This function prevents orphaned conjunctions
744
+ (single-letter words like “a”, “i”, “o”, “u”, “w”, “z”)
745
+ from appearing at the end of a line
746
+ by wrapping them with the following word
747
+ in a non-breaking span.
748
+ It also inserts zero-width spaces
749
+ after slashes and dots to allow line breaks.
750
+
751
+ #### node
752
+
753
+ The root DOM node to process.
754
+ All descendant text nodes are corrected recursively,
755
+ except those inside `IFRAME`, `NOSCRIPT`, `PRE`,
756
+ `SCRIPT`, `STYLE`, or `TEXTAREA` elements.
757
+
758
+ #### Usage Examples
759
+
760
+ ```ts
761
+ const p = h(
762
+ 'p',
763
+ 'Pchnąć w tę łódź jeża lub ośm skrzyń fig ' +
764
+ '(zob. https://pl.wikipedia.org/wiki/Pangram).',
765
+ ['br'],
766
+ ['b', 'Zażółć gęślą jaźń.']
767
+ )
768
+
769
+ fixPlTypography(p)
770
+
771
+ expect(p.innerHTML).to.deep.equal(
772
+ 'Pchnąć ' +
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
+ )
780
+ ```
781
+
782
+ ### h
783
+
784
+ ```ts
785
+ const h: {
786
+ <T extends keyof HTMLElementTagNameMap>(tag: T, ...args1: HArgs1[]): HTMLElementTagNameMap[T];
787
+ <N extends Node>(node: N, ...args1: HArgs1[]): N;
788
+ (tagOrNode: string | Node, ...args1: HArgs1[]): Node;
789
+ };
790
+ ```
791
+
792
+ A lightweight *HyperScript*-style helper
793
+ for creating and modifying `HTMLElement`s
794
+ (see also [`s`](#s)).
795
+
796
+ #### tagOrNode
797
+
798
+ If a `string`, it is treated as the tag name
799
+ for a new element.
800
+ If a `Node`, that node is modified in place.
801
+
802
+ #### args
803
+
804
+ Additional arguments processed as follows:
805
+ - `Object`: maps attributes/properties.
806
+ Keys starting with `$` set element properties
807
+ (without the `$` prefix);
808
+ other keys set attributes via `setAttribute`.
809
+ A value of `false` removes the attribute.
810
+ - `null`/`undefined`: ignored.
811
+ - `Node`: appended as a child.
812
+ - `string`/`number`: converted to a `Text` node
813
+ and appended.
814
+ - [`HArgs`](#HArgs) array: processed recursively.
815
+
816
+ #### Returns
817
+
818
+ The created or modified `HTMLElement`.
819
+
820
+ #### Usage Examples
821
+
822
+ ```ts
823
+ const b = h('b')
824
+
825
+ expect(b.outerHTML).to.equal('<b></b>')
826
+
827
+ const i = h('i', 'text')
828
+
829
+ h(b, i)
830
+
831
+ expect(i.outerHTML).to.equal('<i>text</i>')
832
+
833
+ expect(b.outerHTML).to.equal('<b><i>text</i></b>')
834
+
835
+ h(i, { $className: 'some class' })
836
+
837
+ expect(i.outerHTML).to.equal(
838
+ '<i class="some class">text</i>'
839
+ )
840
+
841
+ expect(b.outerHTML).to.equal(
842
+ '<b><i class="some class">text</i></b>'
843
+ )
844
+
845
+ expect(h('span', 'text').outerHTML).to.equal(
846
+ '<span>text</span>'
598
847
  )
599
- const checkbox2 = h(
600
- 'input',
601
- { type: 'checkbox', $checked: true },
848
+
849
+ expect(
850
+ h('span', { $innerText: 'text' }).outerHTML
851
+ ).to.equal('<span>text</span>')
852
+
853
+ expect(h('span', '42').outerHTML).to.equal(
854
+ '<span>42</span>'
602
855
  )
603
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 })
872
+ const input2 = h('input', { $value: '42' })
873
+
874
+ expect(input1.value).to.equal('42')
875
+ expect(input2.value).to.equal('42')
876
+
877
+ expect(input1.outerHTML).to.equal('<input value="42">')
878
+ expect(input2.outerHTML).to.equal('<input>')
879
+
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
+ })
889
+
604
890
  expect(checkbox1.checked).to.be.true
605
891
  expect(checkbox2.checked).to.be.true
606
892
 
607
- expect(checkbox1.outerHTML)
608
- .to.equal('<input type="checkbox" checked="">')
609
- expect(checkbox2.outerHTML)
610
- .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
+ )
611
900
 
612
901
  const div = h('div')
613
902
 
@@ -628,15 +917,20 @@ expect(elemWithClass.getAttribute('class')).to.equal('test')
628
917
  const elemWithText = h('div', 'initial')
629
918
 
630
919
  h(elemWithText, ' more')
631
- expect(elemWithText.outerHTML)
632
- .to.equal('<div>initial more</div>')
633
920
 
634
- const elemWithNested = h('div',
921
+ expect(elemWithText.outerHTML).to.equal(
922
+ '<div>initial more</div>'
923
+ )
924
+
925
+ const elemWithNested = h(
926
+ 'div',
635
927
  ['span', 'hello'],
636
- ['b', 'world'])
928
+ ['b', 'world']
929
+ )
637
930
 
638
- expect(elemWithNested.outerHTML)
639
- .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
+ )
640
934
  ```
641
935
 
642
936
  ### hasOwn
@@ -721,8 +1015,9 @@ const fakeArray1 = Object.create({ constructor: Array })
721
1015
 
722
1016
  expect(isArray(fakeArray1)).to.be.false
723
1017
 
724
- const fakeArray2 =
725
- Object.create({ [Symbol.toStringTag]: Array.name })
1018
+ const fakeArray2 = Object.create({
1019
+ [Symbol.toStringTag]: Array.name,
1020
+ })
726
1021
 
727
1022
  expect(isArray(fakeArray2)).to.be.false
728
1023
  ```
@@ -748,9 +1043,13 @@ The value to check.
748
1043
 
749
1044
  ```ts
750
1045
  expect(isFiniteNumber(42)).to.be.true
1046
+
751
1047
  expect(isFiniteNumber(Number(42))).to.be.true
1048
+
752
1049
  expect(isFiniteNumber(new Number(42))).to.be.false
1050
+
753
1051
  expect(isFiniteNumber(NaN)).to.be.false
1052
+
754
1053
  expect(isFiniteNumber(Infinity)).to.be.false
755
1054
  ```
756
1055
 
@@ -774,12 +1073,18 @@ The value to check.
774
1073
 
775
1074
  ```ts
776
1075
  expect(isInteger(42)).to.be.true
1076
+
777
1077
  expect(isInteger(42.00000000000001)).to.be.false
1078
+
778
1079
  expect(isInteger(42.000000000000001)).to.be.true
779
1080
  // ^because of loss of precision
1081
+
780
1082
  expect(isInteger(Number(42))).to.be.true
1083
+
781
1084
  expect(isInteger(new Number(42))).to.be.false
1085
+
782
1086
  expect(isInteger(NaN)).to.be.false
1087
+
783
1088
  expect(isInteger(Infinity)).to.be.false
784
1089
  ```
785
1090
 
@@ -804,9 +1109,13 @@ The value to check.
804
1109
 
805
1110
  ```ts
806
1111
  expect(isNumber(42)).to.be.true
1112
+
807
1113
  expect(isNumber(Number(42))).to.be.true
1114
+
808
1115
  expect(isNumber(new Number(42))).to.be.false
1116
+
809
1117
  expect(isNumber(NaN)).to.be.true
1118
+
810
1119
  expect(isNumber(Infinity)).to.be.true
811
1120
  ```
812
1121
 
@@ -831,495 +1140,220 @@ The value to check.
831
1140
 
832
1141
  ```ts
833
1142
  expect(isRecord({})).to.be.true
834
- expect(isRecord([])).to.be.false
835
- expect(isRecord(Object.create(null))).to.be.true
836
-
837
- const fakeNumber1 = Object.create({ constructor: Number })
838
-
839
- expect(isRecord(fakeNumber1)).to.be.true
840
-
841
- const fakeNumber2 =
842
- Object.create({ [Symbol.toStringTag]: Number.name })
843
-
844
- expect(isRecord(fakeNumber2)).to.be.true
845
-
846
- expect(isRecord(new Number(42))).to.be.true
847
- expect(isRecord(new String('42'))).to.be.true
848
-
849
- class FooBar { }
850
-
851
- expect(isRecord(new FooBar())).to.be.true
852
- ```
853
-
854
- ### isString
855
-
856
- ```ts
857
- const isString: (arg: unknown) => arg is string;
858
- ```
859
-
860
- Checks whether the argument is a string.
861
-
862
- #### arg
863
-
864
- The value to check.
865
-
866
- #### Returns
867
-
868
- `true` if `typeof arg === 'string'`, `false` otherwise.
869
-
870
- #### Usage Examples
871
-
872
- ```ts
873
- expect(isString('42')).to.be.true
874
- expect(isString(String('42'))).to.be.true
875
- expect(isString(new String('42'))).to.be.false
876
- ```
877
-
878
- ### jsOnParse
879
-
880
- ```ts
881
- const jsOnParse: (handlers: Record<PropertyKey, Function>, text: string) => any;
882
- ```
883
-
884
- Parses JSON with support for
885
- handler-based value transformation (“JavaScript ON”).
886
-
887
- Objects with exactly one property
888
- whose key exists in `handlers`
889
- and whose value is an array
890
- are replaced by invoking the corresponding handler
891
- with the array elements as arguments.
892
-
893
- #### handlers
894
-
895
- An object mapping handler names to functions.
896
-
897
- #### text
898
-
899
- The JSON string to parse.
900
-
901
- #### Returns
902
-
903
- The parsed value with handler substitutions applied.
904
-
905
- #### Usage Examples
906
-
907
- ```ts
908
- const handlers = {
909
- $add: (a: number, b: number) => a + b,
910
- $hello: (name: string) => `Hello ${name}!`,
911
- $foo: () => 'bar',
912
- }
913
1143
 
914
- const actual1 = jsOnParse(handlers, `[
915
- {
916
- "$add": [1, 2]
917
- },
918
- {
919
- "$hello": ["World"]
920
- },
921
- {
922
- "nested": {
923
- "$hello": ["nested World"]
924
- },
925
- "one": 1,
926
- "two": 2
927
- },
928
- {
929
- "$foo": []
930
- },
931
- {
932
- "$foo": ["The parent object does not have exactly one property!"],
933
- "one": 1,
934
- "two": 2
935
- }
936
- ]`)
937
-
938
- expect(actual1).to.deep.equal(
939
- [
940
- 3,
941
- 'Hello World!',
942
- {
943
- nested: 'Hello nested World!',
944
- one: 1,
945
- two: 2,
946
- },
947
- 'bar',
948
- {
949
- $foo: [
950
- 'The parent object does not have ' +
951
- 'exactly one property!',
952
- ],
953
- one: 1,
954
- two: 2,
955
- },
956
- ],
957
- )
958
-
959
- const actual2 = jsOnParse(
960
- { $notFunc: 'handler not being a function' } as any,
961
- '{"$notFunc": [1, 2]}',
962
- )
963
-
964
- expect(actual2).to.deep.equal({ $notFunc: [1, 2] })
965
-
966
- const actual3 = jsOnParse(
967
- handlers,
968
- '{"$unknown_handler_key": [1, 2]}',
969
- )
970
-
971
- expect(actual3)
972
- .to.deep.equal({ $unknown_handler_key: [1, 2] })
973
-
974
- const actual4 = jsOnParse(
975
- handlers,
976
- '{"$add": {"not": "array"}}',
977
- )
978
-
979
- expect(actual4)
980
- .to.deep.equal({ $add: { not: 'array' } })
981
- ```
982
-
983
- ### monokai
984
-
985
- ```ts
986
- const monokai: CRoot;
987
- ```
988
-
989
- A Monokai-inspired color scheme
990
- for use with the [`c`](#c) helper
991
- and [`nanolightTs`](#nanolightTs) tokenizer.
992
-
993
- ### nanolightTs
994
-
995
- ```ts
996
- const nanolightTs: (code: string) => HArgs1[];
997
- ```
998
-
999
- A TypeScript/JavaScript syntax highlighting tokenizer
1000
- built using [`newTokenizer`](#newTokenizer).
1001
-
1002
- #### code
1003
-
1004
- The source code string to tokenize.
1005
-
1006
- #### Returns
1007
-
1008
- An array of [`HArgs1`](#HArgs1) elements
1009
- suitable for rendering with [`h`](#h).
1010
-
1011
- #### Usage Examples
1012
-
1013
- ```ts
1014
- const codeJs =
1015
- 'const answerToLifeTheUniverseAndEverything = ' +
1016
- "{ 42: 42 }['42'] /* 42 */"
1017
-
1018
- expect(nanolightTs(codeJs)).to.deep.equal(
1019
- [
1020
- ['span', { class: 'keyword-1' }, 'const'],
1021
- ' ',
1022
- ['span',
1023
- { class: 'identifier-4' },
1024
- 'answerToLifeTheUniverseAndEverything',
1025
- ],
1026
- ' ',
1027
- ['span', { class: 'operator' }, '='],
1028
- ' ',
1029
- ['span', { class: 'punctuation' }, '{'],
1030
- ' ',
1031
- ['span', { class: 'number' }, '42'],
1032
- ['span', { class: 'operator' }, ':'],
1033
- ' ',
1034
- ['span', { class: 'number' }, '42'],
1035
- ' ',
1036
- ['span', { class: 'punctuation' }, '}'],
1037
- ['span', { class: 'punctuation' }, '['],
1038
- ['span', { class: 'string' }, "'42'"],
1039
- ['span', { class: 'punctuation' }, ']'],
1040
- ' ',
1041
- ['span', { class: 'comment' }, '/* 42 */'],
1042
- ],
1043
- )
1044
- ```
1045
-
1046
- ### newEscape
1047
-
1048
- ```ts
1049
- const newEscape: (escapeFn: (value: any) => string) => (template: TemplateStringsArray, ...values: unknown[]) => string;
1050
- ```
1051
-
1052
- Creates a tag function
1053
- for escaping interpolated values in template literals.
1054
-
1055
- #### escapeFn
1056
-
1057
- A function that takes a value
1058
- and returns its escaped string representation.
1059
-
1060
- #### Returns
1061
-
1062
- A tag function that escapes interpolated values
1063
- using the provided escape map.
1064
-
1065
- #### Usage Examples
1066
-
1067
- ```ts
1068
- const escapeFn = (value: any): string =>
1069
- isArray(value)
1070
- ? value.map(escapeFn).join(', ')
1071
- : value === true || value === false
1072
- ? `b'${+value}'`
1073
- : value instanceof Date
1074
- ? `'${value.toISOString().replace(/^(.+)T(.+)\..*$/, '$1 $2')}'`
1075
- : isFiniteNumber(value)
1076
- ? `${value}`
1077
- : isString(value)
1078
- ? `'${value.replace(/'/g, "''")}'`
1079
- : 'NULL'
1080
-
1081
- const sql = newEscape(escapeFn)
1082
-
1083
- const actual = sql`
1084
- SELECT *
1085
- FROM table_name
1086
- WHERE column_name IN (
1087
- ${[
1088
- true,
1089
- null,
1090
- undefined,
1091
- NaN,
1092
- Infinity,
1093
- 42,
1094
- '42',
1095
- "4'2",
1096
- /42/,
1097
- new Date(323325000000),
1098
- ]}
1099
- )`
1100
-
1101
- const expected = `
1102
- SELECT *
1103
- FROM table_name
1104
- WHERE column_name IN (
1105
- b'1', NULL, NULL, NULL, NULL, 42, '42', '4''2', NULL, '1980-03-31 04:30:00'
1106
- )`
1107
-
1108
- expect(actual).to.equal(expected)
1109
- ```
1110
-
1111
- ### newNounForm
1112
-
1113
- ```ts
1114
- const newNounForm: (locale: string, forms: Partial<Record<Intl.LDMLPluralRule, string>>) => (value: number) => string;
1115
- ```
1116
-
1117
- Creates a function that
1118
- returns the appropriate noun form
1119
- based on a numeric value using `Intl.PluralRules`.
1120
-
1121
- Different languages have different plural rules.
1122
- The `Intl.PluralRules` API provides locale-aware
1123
- plural category selection.
1124
- Possible categories are:
1125
-
1126
- - `zero`: for zero items
1127
- (used in some languages like Arabic, Latvian)
1128
- - `one`: for singular (e.g., 1 item)
1129
- - `two`: for dual
1130
- (used in some languages like Arabic, Hebrew)
1131
- - `few`: for small plurals (e.g., 2-4 in Polish)
1132
- - `many`: for larger plurals (e.g., 5-21 in Polish)
1133
- - `other`: fallback category (used by all languages)
1144
+ expect(isRecord([])).to.be.false
1134
1145
 
1135
- #### locale
1146
+ expect(isRecord(Object.create(null))).to.be.true
1136
1147
 
1137
- A BCP 47 language tag (e.g., `pl`, `en`).
1148
+ const fakeNumber1 = Object.create({ constructor: Number })
1138
1149
 
1139
- #### forms
1150
+ expect(isRecord(fakeNumber1)).to.be.true
1140
1151
 
1141
- An object mapping plural categories to noun forms.
1142
- Not all categories need to be provided;
1143
- if a category is missing,
1144
- the function falls back to `other`,
1145
- then to an empty string.
1152
+ const fakeNumber2 = Object.create({
1153
+ [Symbol.toStringTag]: Number.name,
1154
+ })
1146
1155
 
1147
- #### Returns
1156
+ expect(isRecord(fakeNumber2)).to.be.true
1148
1157
 
1149
- A function that takes a numeric value
1150
- and returns the appropriate noun form.
1158
+ expect(isRecord(new Number(42))).to.be.true
1151
1159
 
1152
- #### Usage Examples
1160
+ expect(isRecord(new String('42'))).to.be.true
1161
+
1162
+ class FooBar {}
1163
+
1164
+ expect(isRecord(new FooBar())).to.be.true
1165
+ ```
1166
+
1167
+ ### isString
1153
1168
 
1154
1169
  ```ts
1155
- const auto = newNounForm(
1156
- 'pl',
1157
- { one: 'auto', few: 'auta', other: 'aut' },
1158
- )
1170
+ const isString: (arg: unknown) => arg is string;
1171
+ ```
1159
1172
 
1160
- expect(auto(0)).to.equal('aut')
1161
- expect(auto(1)).to.equal('auto')
1162
- expect(auto(17)).to.equal('aut')
1163
- expect(auto(42)).to.equal('auta')
1173
+ Checks whether the argument is a string.
1164
1174
 
1165
- const car = newNounForm('en', { one: 'car', other: 'cars' })
1175
+ #### arg
1166
1176
 
1167
- expect(car(0)).to.equal('cars')
1168
- expect(car(1)).to.equal('car')
1169
- expect(car(17)).to.equal('cars')
1170
- expect(car(42)).to.equal('cars')
1177
+ The value to check.
1171
1178
 
1172
- const empty = newNounForm('en', {})
1179
+ #### Returns
1173
1180
 
1174
- expect(empty(0)).to.equal('')
1175
- expect(empty(1)).to.equal('')
1176
- expect(empty(17)).to.equal('')
1177
- expect(empty(42)).to.equal('')
1178
- ```
1181
+ `true` if `typeof arg === 'string'`, `false` otherwise.
1179
1182
 
1180
- ### newTokenizer
1183
+ #### Usage Examples
1181
1184
 
1182
1185
  ```ts
1183
- const newTokenizer: <M, T>(decorator: (chunk: string, metadata?: M) => T, ...specs: [M, string | RegExp][]) => (code: string) => T[];
1186
+ expect(isString('42')).to.be.true
1187
+ expect(isString(String('42'))).to.be.true
1188
+ expect(isString(new String('42'))).to.be.false
1184
1189
  ```
1185
1190
 
1186
- A helper for building simple tokenizers
1187
- (see also [`nanolightTs`](#nanolightTs)).
1191
+ ### jsOnParse
1188
1192
 
1189
- #### decorator
1193
+ ```ts
1194
+ const jsOnParse: (handlers: Record<PropertyKey, Function>, text: string) => any;
1195
+ ```
1190
1196
 
1191
- A function that wraps each matched chunk.
1192
- It receives the matched text (`chunk`)
1193
- and optionally the `metadata`
1194
- associated with the pattern that produced the match.
1195
- For unmatched text between patterns,
1196
- `metadata` is `undefined`.
1197
+ Parses JSON with support for
1198
+ handler-based value transformation (“JavaScript ON”).
1197
1199
 
1198
- #### specs
1200
+ Objects with exactly one property
1201
+ whose key exists in `handlers`
1202
+ and whose value is an array
1203
+ are replaced by invoking the corresponding handler
1204
+ with the array elements as arguments.
1199
1205
 
1200
- An array of tuples `[metadata, pattern]` where:
1201
- - `metadata`: arbitrary data (e.g., a CSS class name)
1202
- passed to `decorator` when the pattern matches.
1203
- - `pattern`: a `string` or `RegExp`
1204
- to match against the input.
1206
+ #### handlers
1205
1207
 
1206
- #### Returns
1208
+ An object mapping handler names to functions.
1207
1209
 
1208
- A tokenizer function that accepts a code string
1209
- and returns an array of decorated tokens.
1210
+ #### text
1210
1211
 
1211
- #### Remarks
1212
+ The JSON string to parse.
1212
1213
 
1213
- 1. Matches starting at an earlier position
1214
- take precedence.
1215
- 2. Among matches at the same position,
1216
- the longer one wins.
1217
- 3. Among matches of the same position and length,
1218
- the one defined earlier wins.
1214
+ #### Returns
1215
+
1216
+ The parsed value with handler substitutions applied.
1219
1217
 
1220
1218
  #### Usage Examples
1221
1219
 
1222
1220
  ```ts
1223
- const tokenizer1 = newTokenizer(
1224
- (chunk, metadata) => ({ chunk, metadata }),
1225
- ['keyword', /\b(if|else|for)\b/],
1226
- ['string', /"[^"]*"/],
1227
- )
1228
- const result1 = tokenizer1('if "hello" else "world"')
1221
+ const handlers = {
1222
+ $add: (a: number, b: number) => a + b,
1223
+ $hello: (name: string) => `Hello ${name}!`,
1224
+ $foo: () => 'bar',
1225
+ }
1229
1226
 
1230
- expect(result1).to.deep.equal(
1231
- [
1232
- { chunk: 'if', metadata: 'keyword' },
1233
- { chunk: ' ', metadata: undefined },
1234
- { chunk: '"hello"', metadata: 'string' },
1235
- { chunk: ' ', metadata: undefined },
1236
- { chunk: 'else', metadata: 'keyword' },
1237
- { chunk: ' ', metadata: undefined },
1238
- { chunk: '"world"', metadata: 'string' },
1239
- ],
1227
+ const actual1 = jsOnParse(
1228
+ handlers,
1229
+ `[
1230
+ {
1231
+ "$add": [1, 2]
1232
+ },
1233
+ {
1234
+ "$hello": ["World"]
1235
+ },
1236
+ {
1237
+ "nested": {
1238
+ "$hello": ["nested World"]
1239
+ },
1240
+ "one": 1,
1241
+ "two": 2
1242
+ },
1243
+ {
1244
+ "$foo": []
1245
+ },
1246
+ {
1247
+ "$foo": ["The parent object does not have exactly one property!"],
1248
+ "one": 1,
1249
+ "two": 2
1250
+ }
1251
+ ]`
1240
1252
  )
1241
1253
 
1242
- const tokenizer2 = newTokenizer(
1243
- (chunk, metadata) => `${metadata}:${chunk}`,
1244
- ['tag', 'BEGIN'],
1245
- ['end', 'END'],
1246
- )
1247
- const result2 = tokenizer2('aBEGINbENDc')
1254
+ expect(actual1).to.deep.equal([
1255
+ 3,
1256
+ 'Hello World!',
1257
+ {
1258
+ nested: 'Hello nested World!',
1259
+ one: 1,
1260
+ two: 2,
1261
+ },
1262
+ 'bar',
1263
+ {
1264
+ $foo: [
1265
+ 'The parent object does not have ' +
1266
+ 'exactly one property!',
1267
+ ],
1268
+ one: 1,
1269
+ two: 2,
1270
+ },
1271
+ ])
1248
1272
 
1249
- expect(result2).to.deep.equal(
1250
- [
1251
- 'undefined:a',
1252
- 'tag:BEGIN',
1253
- 'undefined:b',
1254
- 'end:END',
1255
- 'undefined:c',
1256
- ],
1273
+ const actual2 = jsOnParse(
1274
+ { $notFunc: 'handler not being a function' } as any,
1275
+ '{"$notFunc": [1, 2]}'
1257
1276
  )
1258
1277
 
1259
- const tokenizer3 = newTokenizer(
1260
- (chunk) => chunk,
1261
- ['test', /test/],
1278
+ expect(actual2).to.deep.equal({ $notFunc: [1, 2] })
1279
+
1280
+ const actual3 = jsOnParse(
1281
+ handlers,
1282
+ '{"$unknown_handler_key": [1, 2]}'
1262
1283
  )
1263
- const result3 = tokenizer3('')
1264
1284
 
1265
- expect(result3).to.deep.equal([])
1285
+ expect(actual3).to.deep.equal({
1286
+ $unknown_handler_key: [1, 2],
1287
+ })
1266
1288
 
1267
- const tokenizer4 = newTokenizer(
1268
- (chunk, metadata) => ({ chunk, metadata }),
1269
- ['start', /^test/],
1289
+ const actual4 = jsOnParse(
1290
+ handlers,
1291
+ '{"$add": {"not": "array"}}'
1270
1292
  )
1271
- const result4 = tokenizer4('test here')
1272
1293
 
1273
- expect(result4).to.deep.equal(
1274
- [
1275
- { chunk: 'test', metadata: 'start' },
1276
- { chunk: ' here', metadata: undefined },
1277
- ],
1278
- )
1294
+ expect(actual4).to.deep.equal({ $add: { not: 'array' } })
1295
+ ```
1279
1296
 
1280
- const tokenizer5 = newTokenizer(
1281
- (chunk, metadata) => metadata,
1282
- ['later', /x/],
1283
- ['earlier', /y/],
1284
- )
1285
- const result5 = tokenizer5('yx')
1297
+ ### monokai
1286
1298
 
1287
- expect(result5).to.deep.equal(['earlier', 'later'])
1299
+ ```ts
1300
+ const monokai: CRoot;
1301
+ ```
1288
1302
 
1289
- const tokenizer6 = newTokenizer(
1290
- (chunk) => chunk,
1291
- ['short', 'a'],
1292
- ['long', 'abc'],
1293
- )
1294
- const result6 = tokenizer6('abc')
1303
+ A Monokai-inspired color scheme
1304
+ for use with the [`c`](#c) helper
1305
+ and [`nanolightTs`](#nanolightTs) tokenizer.
1295
1306
 
1296
- expect(result6).to.deep.equal(['abc'])
1307
+ ### nanolightTs
1297
1308
 
1298
- const tokenizer7 = newTokenizer(
1299
- (chunk) => chunk,
1300
- ['empty', ''],
1301
- ['word', /\w+/],
1302
- )
1303
- const result7 = tokenizer7('hello')
1309
+ ```ts
1310
+ const nanolightTs: (code: string) => HArgs1[];
1311
+ ```
1304
1312
 
1305
- expect(result7).to.deep.equal(['hello'])
1313
+ A TypeScript/JavaScript syntax highlighting tokenizer
1314
+ built using [`createTokenizer`](#createTokenizer).
1306
1315
 
1307
- const tokenizer8 = newTokenizer(
1308
- (chunk) => chunk,
1309
- ['test', /xyz/],
1310
- )
1311
- const result8 = tokenizer8('abc')
1316
+ #### code
1312
1317
 
1313
- expect(result8).to.deep.equal(['abc'])
1318
+ The source code string to tokenize.
1314
1319
 
1315
- const tokenizer9 = newTokenizer(
1316
- (_chunk, metadata) => metadata,
1317
- ['a', 'a'],
1318
- ['b', 'b'],
1319
- )
1320
- const result9 = tokenizer9('aabb')
1320
+ #### Returns
1321
1321
 
1322
- expect(result9).to.deep.equal(['a', 'a', 'b', 'b'])
1322
+ An array of [`HArgs1`](#HArgs1) elements
1323
+ suitable for rendering with [`h`](#h).
1324
+
1325
+ #### Usage Examples
1326
+
1327
+ ```ts
1328
+ const codeJs =
1329
+ 'const answerToLifeTheUniverseAndEverything = ' +
1330
+ "{ 42: 42 }['42'] /* 42 */"
1331
+
1332
+ expect(nanolightTs(codeJs)).to.deep.equal([
1333
+ ['span', { class: 'keyword-1' }, 'const'],
1334
+ ' ',
1335
+ [
1336
+ 'span',
1337
+ { class: 'identifier-4' },
1338
+ 'answerToLifeTheUniverseAndEverything',
1339
+ ],
1340
+ ' ',
1341
+ ['span', { class: 'operator' }, '='],
1342
+ ' ',
1343
+ ['span', { class: 'punctuation' }, '{'],
1344
+ ' ',
1345
+ ['span', { class: 'number' }, '42'],
1346
+ ['span', { class: 'operator' }, ':'],
1347
+ ' ',
1348
+ ['span', { class: 'number' }, '42'],
1349
+ ' ',
1350
+ ['span', { class: 'punctuation' }, '}'],
1351
+ ['span', { class: 'punctuation' }, '['],
1352
+ ['span', { class: 'string' }, "'42'"],
1353
+ ['span', { class: 'punctuation' }, ']'],
1354
+ ' ',
1355
+ ['span', { class: 'comment' }, '/* 42 */'],
1356
+ ])
1323
1357
  ```
1324
1358
 
1325
1359
  ### omit
@@ -1385,9 +1419,10 @@ A new object with only the specified keys.
1385
1419
  ```ts
1386
1420
  const ref = { a: 42, b: '42', c: 17 }
1387
1421
 
1388
- expect(pick(ref, ['a', 'b'])).to.deep.equal(
1389
- { a: 42, b: '42' },
1390
- )
1422
+ expect(pick(ref, ['a', 'b'])).to.deep.equal({
1423
+ a: 42,
1424
+ b: '42',
1425
+ })
1391
1426
  ```
1392
1427
 
1393
1428
  ### rwd
@@ -1442,36 +1477,34 @@ const style: CRoot = {
1442
1477
 
1443
1478
  rwd(style, '.r6', 200, 50, [6], [3], [1, 1, 2])
1444
1479
 
1445
- expect(style).to.deep.equal(
1446
- {
1447
- body: {
1448
- 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',
1449
1488
  },
1489
+ boxSizing: 'border-box',
1490
+ display: 'block',
1491
+ float: 'left',
1492
+ width: '100%',
1493
+ height: '100px',
1494
+ },
1495
+ '@media(min-width:600px)': {
1450
1496
  '.r6': {
1451
- border: 'solid red 1px',
1452
- '.no-border': {
1453
- border: 'none',
1454
- },
1455
- boxSizing: 'border-box',
1456
- display: 'block',
1457
- float: 'left',
1458
- width: '100%',
1459
- height: '100px',
1460
- },
1461
- '@media(min-width:600px)': {
1462
- '.r6': {
1463
- width: 'calc(100% / 3)',
1464
- height: '50px',
1465
- },
1497
+ width: 'calc(100% / 3)',
1498
+ height: '50px',
1466
1499
  },
1467
- '@media(min-width:1200px)': {
1468
- '.r6': {
1469
- width: 'calc(50% / 3)',
1470
- height: '50px',
1471
- },
1500
+ },
1501
+ '@media(min-width:1200px)': {
1502
+ '.r6': {
1503
+ width: 'calc(50% / 3)',
1504
+ height: '50px',
1472
1505
  },
1473
1506
  },
1474
- )
1507
+ })
1475
1508
  ```
1476
1509
 
1477
1510
  ### s
@@ -1525,18 +1558,19 @@ const svg2 = s('svg', { 'xlink:href': false })
1525
1558
 
1526
1559
  expect(svg2.getAttributeNS(XLINK_NS, 'href')).to.be.null
1527
1560
 
1528
- const svg3 = s(
1529
- 'svg',
1530
- { 'xlink:href': 'http://example.com' },
1531
- )
1561
+ const svg3 = s('svg', {
1562
+ 'xlink:href': 'http://example.com',
1563
+ })
1532
1564
 
1533
- expect(svg3.getAttributeNS(XLINK_NS, 'href'))
1534
- .to.equal('http://example.com')
1565
+ expect(svg3.getAttributeNS(XLINK_NS, 'href')).to.equal(
1566
+ 'http://example.com'
1567
+ )
1535
1568
 
1536
1569
  const svg4 = s('svg', { 'xlink:title': 42 })
1537
1570
 
1538
- expect(svg4.getAttributeNS(XLINK_NS, 'title'))
1539
- .to.equal('42')
1571
+ expect(svg4.getAttributeNS(XLINK_NS, 'title')).to.equal(
1572
+ '42'
1573
+ )
1540
1574
  ```
1541
1575
 
1542
1576
  ### svgUse
@@ -1575,51 +1609,63 @@ expect(svg.children.length).to.equal(1)
1575
1609
 
1576
1610
  const useElement = svg.children[0]
1577
1611
 
1578
- expect(useElement.getAttributeNS(XLINK_NS, 'href'))
1579
- .to.equal('#icon-home')
1612
+ expect(
1613
+ useElement.getAttributeNS(XLINK_NS, 'href')
1614
+ ).to.equal('#icon-home')
1580
1615
 
1581
- const svgWithViewBox =
1582
- svgUse('icon-star', { viewBox: '0 0 24 24' })
1616
+ const svgWithViewBox = svgUse('icon-star', {
1617
+ viewBox: '0 0 24 24',
1618
+ })
1583
1619
 
1584
- expect(svgWithViewBox.getAttribute('viewBox'))
1585
- .to.equal('0 0 24 24')
1620
+ expect(svgWithViewBox.getAttribute('viewBox')).to.equal(
1621
+ '0 0 24 24'
1622
+ )
1586
1623
 
1587
1624
  const useViewBox = svgWithViewBox.children[0]
1588
1625
 
1589
- expect(useViewBox.getAttributeNS(XLINK_NS, 'href'))
1590
- .to.equal('#icon-star')
1626
+ expect(
1627
+ useViewBox.getAttributeNS(XLINK_NS, 'href')
1628
+ ).to.equal('#icon-star')
1591
1629
 
1592
- const svgWithClass =
1593
- svgUse('icon-menu', { class: 'icon-btn' })
1630
+ const svgWithClass = svgUse('icon-menu', {
1631
+ class: 'icon-btn',
1632
+ })
1594
1633
 
1595
- expect(svgWithClass.getAttribute('class'))
1596
- .to.equal('icon-btn')
1634
+ expect(svgWithClass.getAttribute('class')).to.equal(
1635
+ 'icon-btn'
1636
+ )
1597
1637
 
1598
1638
  expect(svgWithClass.children.length).to.equal(1)
1599
1639
 
1600
1640
  const useClass = svgWithClass.children[0]
1601
1641
 
1602
- expect(useClass.getAttributeNS(XLINK_NS, 'href'))
1603
- .to.equal('#icon-menu')
1604
-
1605
- const svgWithMultipleAttrs = svgUse(
1606
- 'icon-settings',
1607
- { width: 24, height: 24, class: 'icon' },
1642
+ expect(useClass.getAttributeNS(XLINK_NS, 'href')).to.equal(
1643
+ '#icon-menu'
1608
1644
  )
1609
1645
 
1610
- expect(svgWithMultipleAttrs.getAttribute('width'))
1611
- .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
+ )
1612
1655
 
1613
- expect(svgWithMultipleAttrs.getAttribute('height'))
1614
- .to.equal('24')
1656
+ expect(
1657
+ svgWithMultipleAttrs.getAttribute('height')
1658
+ ).to.equal('24')
1615
1659
 
1616
- expect(svgWithMultipleAttrs.getAttribute('class'))
1617
- .to.equal('icon')
1660
+ expect(svgWithMultipleAttrs.getAttribute('class')).to.equal(
1661
+ 'icon'
1662
+ )
1618
1663
 
1619
1664
  const useMultiple = svgWithMultipleAttrs.children[0]
1620
1665
 
1621
- expect(useMultiple.getAttributeNS(XLINK_NS, 'href'))
1622
- .to.equal('#icon-settings')
1666
+ expect(
1667
+ useMultiple.getAttributeNS(XLINK_NS, 'href')
1668
+ ).to.equal('#icon-settings')
1623
1669
  ```
1624
1670
 
1625
1671
  ### uuidV1
@@ -1683,17 +1729,19 @@ for (let i = 1; i <= 22136; ++i) {
1683
1729
  }
1684
1730
  }
1685
1731
 
1686
- expect(uuidV1(new Date(), '000123456789abc').split('-')[4])
1687
- .to.equal('123456789abc')
1732
+ expect(
1733
+ uuidV1(new Date(), '000123456789abc').split('-')[4]
1734
+ ).to.equal('123456789abc')
1688
1735
 
1689
- expect(uuidV1(new Date(), '123456789').split('-')[4])
1690
- .to.equal('000123456789')
1736
+ expect(
1737
+ uuidV1(new Date(), '123456789').split('-')[4]
1738
+ ).to.equal('000123456789')
1691
1739
 
1692
1740
  expect(
1693
- uuidV1(new Date(323325000000))
1694
- .startsWith('c1399400-9a71-11bd'),
1695
- )
1696
- .to.be.true
1741
+ uuidV1(new Date(323325000000)).startsWith(
1742
+ 'c1399400-9a71-11bd'
1743
+ )
1744
+ ).to.be.true
1697
1745
  ```
1698
1746
 
1699
1747
  ### vivify
@@ -1749,58 +1797,48 @@ const ref: any = {}
1749
1797
 
1750
1798
  vivify(ref).one.two[3][4]
1751
1799
 
1752
- expect(ref).to.deep.equal(
1753
- {
1754
- one: {
1755
- two: [undefined, undefined, undefined, []],
1756
- },
1800
+ expect(ref).to.deep.equal({
1801
+ one: {
1802
+ two: [undefined, undefined, undefined, []],
1757
1803
  },
1758
- )
1804
+ })
1759
1805
 
1760
1806
  vivify(ref).one.two[3][4] = 5
1761
1807
 
1762
- expect(ref).to.deep.equal(
1763
- {
1764
- one: {
1765
- two: [
1766
- undefined,
1767
- undefined,
1768
- undefined,
1769
- [undefined, undefined, undefined, undefined, 5],
1770
- ],
1771
- },
1808
+ expect(ref).to.deep.equal({
1809
+ one: {
1810
+ two: [
1811
+ undefined,
1812
+ undefined,
1813
+ undefined,
1814
+ [undefined, undefined, undefined, undefined, 5],
1815
+ ],
1772
1816
  },
1773
- )
1817
+ })
1774
1818
 
1775
1819
  vivify(ref).one.two[3].length = 1
1776
1820
 
1777
- expect(ref).to.deep.equal(
1778
- {
1779
- one: {
1780
- two: [undefined, undefined, undefined, [undefined]],
1781
- },
1821
+ expect(ref).to.deep.equal({
1822
+ one: {
1823
+ two: [undefined, undefined, undefined, [undefined]],
1782
1824
  },
1783
- )
1825
+ })
1784
1826
 
1785
1827
  vivify(ref).one.two[3] = 4
1786
1828
 
1787
- expect(ref).to.deep.equal(
1788
- {
1789
- one: {
1790
- two: [undefined, undefined, undefined, 4],
1791
- },
1829
+ expect(ref).to.deep.equal({
1830
+ one: {
1831
+ two: [undefined, undefined, undefined, 4],
1792
1832
  },
1793
- )
1833
+ })
1794
1834
 
1795
1835
  expect(vivify(ref).one.two.length).to.equal(4)
1796
1836
 
1797
- expect(ref).to.deep.equal(
1798
- {
1799
- one: {
1800
- two: [undefined, undefined, undefined, 4],
1801
- },
1837
+ expect(ref).to.deep.equal({
1838
+ one: {
1839
+ two: [undefined, undefined, undefined, 4],
1802
1840
  },
1803
- )
1841
+ })
1804
1842
 
1805
1843
  vivify(ref).one.two = 3
1806
1844
 
@@ -1814,8 +1852,8 @@ delete vivify(ref).one.two
1814
1852
 
1815
1853
  expect(ref).to.deep.equal({ one: {} })
1816
1854
 
1817
- expect(vivify(ref).one.toString instanceof Function)
1818
- .to.be.true
1855
+ expect(vivify(ref).one.toString instanceof Function).to.be
1856
+ .true
1819
1857
 
1820
1858
  expect(vivify(ref).one.toString.name).to.equal('toString')
1821
1859
 
@@ -1835,14 +1873,17 @@ expect(ref).to.deep.equal({ one: { two: { three: {} } } })
1835
1873
 
1836
1874
  vivify(ref).one.two.three.four = 5
1837
1875
 
1838
- expect(ref)
1839
- .to.deep.equal({ one: { two: { three: { four: 5 } } } })
1876
+ expect(ref).to.deep.equal({
1877
+ one: { two: { three: { four: 5 } } },
1878
+ })
1840
1879
 
1841
- expect(vivify(ref).one.two.three.four.toString.name)
1842
- .to.equal('toString')
1880
+ expect(
1881
+ vivify(ref).one.two.three.four.toString.name
1882
+ ).to.equal('toString')
1843
1883
 
1844
- expect(ref)
1845
- .to.deep.equal({ one: { two: { three: { four: 5 } } } })
1884
+ expect(ref).to.deep.equal({
1885
+ one: { two: { three: { four: 5 } } },
1886
+ })
1846
1887
 
1847
1888
  const u = undefined
1848
1889