@jackens/nnn 2026.11.16 → 2026.11.17

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/README.md +37 -17
  2. package/nnn.d.ts +6 -5
  3. package/nnn.js +2 -2
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # nnn
2
2
 
3
- A collection of Jackens’ JavaScript helper utilities (version: `2026.11.16`).
3
+ A collection of Jackens’ JavaScript helper utilities (version: `2026.11.17`).
4
4
 
5
5
  ## Installation
6
6
 
@@ -89,7 +89,8 @@ Keys are selectors or at-rules; values are either CSS property values or nested
89
89
  Nested selectors are appended to the parent selector as is (e.g. `'.c'` inside `a` → `a.c`), so a leading space is needed for a descendant combinator (e.g. `' .c'` → `a .c`).
90
90
  If a nested selector contains `&`, every `&` is replaced with the parent selector instead (e.g. `'&:hover'` → `a:hover`, `'.c &'` → `.c a`).
91
91
  A selector key may contain comma-separated alternatives (e.g. `'.b,.c'`); when nested, each alternative is combined with each parent alternative (cartesian product), producing a comma-joined selector list (e.g. `'a.b,a.c'`).
92
- Commas inside brackets or quotes (e.g. `':is(.b,.c)'`, `'[title="b,c"]'`) do not separate alternatives; whitespace after a separating comma is ignored (use `&` for a descendant combinator in a non-first alternative, e.g. `'.b,& .c'` → `a.b,a .c`).
92
+ Commas inside brackets or quotes (e.g. `':is(.b,.c)'`, `'[title="b,c"]'`) do not separate alternatives.
93
+ Whitespace after a separating comma belongs to the next alternative, so like a leading space of the first one it makes a descendant combinator when nested (e.g. `' .b, .c'` → `a .b,a .c`, `'.b, .c'` → `a.b,a .c`); leading whitespace of a resulting selector is ignored (e.g. `'h1, h2'` → `h1,h2`, `'.b, &:hover'` → `a.b,a:hover`).
93
94
  At-rules (e.g. `'@media …'`) wrap their nested rules; an at-rule nested in a selector wraps that selector (e.g. `{ a: { '@media(…)': { color: 'red' } } }` → `@media(…){a{color:red}}`).
94
95
  Inside a top-level `:root` rule nested keys are concatenated into property names (e.g. `{ ':root': { '--': { bg: 'red' } } }` → `:root{--bg:red}`).
95
96
  Properties without a selector (top-level or directly in a top-level at-rule, e.g. `@font-face`) are emitted as bare declarations.
@@ -440,12 +441,12 @@ const expected = `
440
441
  expect(actual).toBe(expected)
441
442
  ```
442
443
 
443
- Commas inside brackets or quotes do not split selectors and whitespace after a comma is ignored:
444
+ Commas inside brackets or quotes do not split selectors:
444
445
 
445
446
  ```ts
446
447
  const actual = c({
447
448
  ':is(.a,.b)': {
448
- '.c, .d': { margin: 1 },
449
+ '.c,.d': { margin: 1 },
449
450
  '[title="x,y"]': { margin: 2 },
450
451
  },
451
452
  })
@@ -462,6 +463,32 @@ const expected = `
462
463
  expect(actual).toBe(expected)
463
464
  ```
464
465
 
466
+ Whitespace after a comma is kept, but leading whitespace of a resulting selector is ignored:
467
+
468
+ ```ts
469
+ const actual = c({
470
+ 'h1, h2': {
471
+ margin: 0,
472
+ ' .a, .b': { margin: 1 },
473
+ '.c, .d , &:hover': { margin: 2 },
474
+ },
475
+ })
476
+
477
+ const expected = `
478
+ h1,h2{
479
+ margin:0
480
+ }
481
+ h1 .a,h1 .b,h2 .a,h2 .b{
482
+ margin:1
483
+ }
484
+ h1.c,h1 .d,h1:hover,h2.c,h2 .d,h2:hover{
485
+ margin:2
486
+ }
487
+ `.replace(/\n\s*/g, '')
488
+
489
+ expect(actual).toBe(expected)
490
+ ```
491
+
465
492
  At-rules nested in selectors wrap these selectors:
466
493
 
467
494
  ```ts
@@ -531,9 +558,9 @@ expect(c({ a: { b$$c: 1 } }, '')).toBe('a{b$$c:1}')
531
558
  ```ts
532
559
  const h: {
533
560
  <T extends keyof HTMLElementTagNameMap>(tag: T, ...args: HArgs1[]): HTMLElementTagNameMap[T]
534
- (tag: string, ...args: HArgs1[]): HTMLElement
535
561
  <N extends Node>(node: N, ...args: HArgs1[]): N
536
- (tagOrNode: string | Node, ...args: HArgs1[]): Node
562
+ (tag: string, ...args: HArgs1[]): HTMLElement
563
+ (node: Node, ...args: HArgs1[]): Node
537
564
  }
538
565
  ```
539
566
 
@@ -575,13 +602,11 @@ const i = h('i', 'text')
575
602
  h(b, i)
576
603
 
577
604
  expect(i.outerHTML).toBe('<i>text</i>')
578
-
579
605
  expect(b.outerHTML).toBe('<b><i>text</i></b>')
580
606
 
581
607
  h(i, { $className: 'some class' })
582
608
 
583
609
  expect(i.outerHTML).toBe('<i class="some class">text</i>')
584
-
585
610
  expect(b.outerHTML).toBe('<b><i class="some class">text</i></b>')
586
611
  ```
587
612
 
@@ -589,11 +614,9 @@ Text content from strings, numbers and `$innerText`:
589
614
 
590
615
  ```ts
591
616
  expect(h('span', 'text').outerHTML).toBe('<span>text</span>')
592
-
593
617
  expect(h('span', { $innerText: 'text' }).outerHTML).toBe('<span>text</span>')
594
618
 
595
619
  expect(h('span', '42').outerHTML).toBe('<span>42</span>')
596
-
597
620
  expect(h('span', 42).outerHTML).toBe('<span>42</span>')
598
621
  ```
599
622
 
@@ -601,9 +624,7 @@ expect(h('span', 42).outerHTML).toBe('<span>42</span>')
601
624
 
602
625
  ```ts
603
626
  expect(h('div', { style: 'margin:0;padding:0' }).outerHTML).toBe('<div style="margin:0;padding:0"></div>')
604
-
605
627
  expect(h('div', { $style: 'margin:0;padding:0' }).outerHTML).toBe('<div style="margin: 0px; padding: 0px;"></div>')
606
-
607
628
  expect(h('div', { $style: { margin: 0, padding: 0 } }).outerHTML).toBe(
608
629
  '<div style="margin: 0px; padding: 0px;"></div>'
609
630
  )
@@ -639,14 +660,13 @@ expect(checkbox1.checked).toBe(true)
639
660
  expect(checkbox2.checked).toBe(true)
640
661
 
641
662
  expect(checkbox1.outerHTML).toBe('<input type="checkbox" checked="">')
642
-
643
663
  expect(checkbox2.outerHTML).toBe('<input type="checkbox">')
644
664
  ```
645
665
 
646
666
  Object values of `$`-prefixed properties are merged:
647
667
 
648
668
  ```ts
649
- const div = h('div')
669
+ const div = h('div') as any
650
670
 
651
671
  expect(div.key).toBeUndefined()
652
672
 
@@ -691,7 +711,7 @@ Only plain objects are merged into `$`-prefixed properties:
691
711
 
692
712
  ```ts
693
713
  const date = new Date(0)
694
- const div = h('div', { $date: date, $list: [1, 2] })
714
+ const div = h('div', { $date: date, $list: [1, 2] }) as any
695
715
 
696
716
  expect(div.date).toBe(date)
697
717
 
@@ -859,9 +879,9 @@ expect(() => rwd({}, '.x', 100, 50, [2, 1, 1.5])).toThrow(RangeError)
859
879
  ```ts
860
880
  const s: {
861
881
  <T extends keyof SVGElementTagNameMap>(tag: T, ...args: HArgs1[]): SVGElementTagNameMap[T]
862
- (tag: string, ...args: HArgs1[]): SVGElement
863
882
  <N extends Node>(node: N, ...args: HArgs1[]): N
864
- (tagOrNode: string | Node, ...args: HArgs1[]): Node
883
+ (tag: string, ...args: HArgs1[]): SVGElement
884
+ (node: Node, ...args: HArgs1[]): Node
865
885
  }
866
886
  ```
867
887
 
package/nnn.d.ts CHANGED
@@ -12,7 +12,8 @@ export type CNode = {
12
12
  * Nested selectors are appended to the parent selector as is (e.g. `'.c'` inside `a` → `a.c`), so a leading space is needed for a descendant combinator (e.g. `' .c'` → `a .c`).
13
13
  * If a nested selector contains `&`, every `&` is replaced with the parent selector instead (e.g. `'&:hover'` → `a:hover`, `'.c &'` → `.c a`).
14
14
  * A selector key may contain comma-separated alternatives (e.g. `'.b,.c'`); when nested, each alternative is combined with each parent alternative (cartesian product), producing a comma-joined selector list (e.g. `'a.b,a.c'`).
15
- * Commas inside brackets or quotes (e.g. `':is(.b,.c)'`, `'[title="b,c"]'`) do not separate alternatives; whitespace after a separating comma is ignored (use `&` for a descendant combinator in a non-first alternative, e.g. `'.b,& .c'` → `a.b,a .c`).
15
+ * Commas inside brackets or quotes (e.g. `':is(.b,.c)'`, `'[title="b,c"]'`) do not separate alternatives.
16
+ * Whitespace after a separating comma belongs to the next alternative, so like a leading space of the first one it makes a descendant combinator when nested (e.g. `' .b, .c'` → `a .b,a .c`, `'.b, .c'` → `a.b,a .c`); leading whitespace of a resulting selector is ignored (e.g. `'h1, h2'` → `h1,h2`, `'.b, &:hover'` → `a.b,a:hover`).
16
17
  * At-rules (e.g. `'@media …'`) wrap their nested rules; an at-rule nested in a selector wraps that selector (e.g. `{ a: { '@media(…)': { color: 'red' } } }` → `@media(…){a{color:red}}`).
17
18
  * Inside a top-level `:root` rule nested keys are concatenated into property names (e.g. `{ ':root': { '--': { bg: 'red' } } }` → `:root{--bg:red}`).
18
19
  * Properties without a selector (top-level or directly in a top-level at-rule, e.g. `@font-face`) are emitted as bare declarations.
@@ -60,9 +61,9 @@ export type HArgs = [string | Node, ...HArgs1[]]
60
61
  */
61
62
  export declare const h: {
62
63
  <T extends keyof HTMLElementTagNameMap>(tag: T, ...args: HArgs1[]): HTMLElementTagNameMap[T]
63
- (tag: string, ...args: HArgs1[]): HTMLElement
64
64
  <N extends Node>(node: N, ...args: HArgs1[]): N
65
- (tagOrNode: string | Node, ...args: HArgs1[]): Node
65
+ (tag: string, ...args: HArgs1[]): HTMLElement
66
+ (node: Node, ...args: HArgs1[]): Node
66
67
  }
67
68
  /**
68
69
  * A lightweight _HyperScript_-style helper for creating and modifying `SVGElement`s (see also {@link h}).
@@ -91,9 +92,9 @@ export declare const h: {
91
92
  */
92
93
  export declare const s: {
93
94
  <T extends keyof SVGElementTagNameMap>(tag: T, ...args: HArgs1[]): SVGElementTagNameMap[T]
94
- (tag: string, ...args: HArgs1[]): SVGElement
95
95
  <N extends Node>(node: N, ...args: HArgs1[]): N
96
- (tagOrNode: string | Node, ...args: HArgs1[]): Node
96
+ (tag: string, ...args: HArgs1[]): SVGElement
97
+ (node: Node, ...args: HArgs1[]): Node
97
98
  }
98
99
  /**
99
100
  * Shorthand for creating an `<svg>` element with a `<use>` child referencing a symbol (e.g. an icon) by ID.
package/nnn.js CHANGED
@@ -29,7 +29,7 @@ var splitSelectorList = (selectorList) => {
29
29
  selectors[selectors.length - 1] += char;
30
30
  }
31
31
  });
32
- return selectors.map((selector, index) => (index === 0 ? selector : selector.trimStart()).trimEnd());
32
+ return selectors.map((selector) => selector.trimEnd());
33
33
  };
34
34
  var nestSelector = (parent, selector) => {
35
35
  let nested = "";
@@ -42,7 +42,7 @@ var nestSelector = (parent, selector) => {
42
42
  nested += char;
43
43
  }
44
44
  });
45
- return ampersandFound ? nested : parent + selector;
45
+ return (ampersandFound ? nested : parent + selector).trimStart();
46
46
  };
47
47
  var stripSplitter = (key, splitter) => {
48
48
  const splitterIndex = splitter === "" ? -1 : key.indexOf(splitter);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackens/nnn",
3
- "version": "2026.11.16",
3
+ "version": "2026.11.17",
4
4
  "description": "Jackens’ JavaScript helpers.",
5
5
  "keywords": [
6
6
  "CSS-in-JS",