@jackens/nnn 2026.9.30 → 2026.11.16
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.
- package/README.md +219 -600
- package/nnn.d.ts +26 -88
- package/nnn.js +135 -270
- package/package.json +14 -10
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# nnn
|
|
2
2
|
|
|
3
|
-
A collection of Jackens’ JavaScript helper utilities (version: `2026.
|
|
3
|
+
A collection of Jackens’ JavaScript helper utilities (version: `2026.11.16`).
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -18,16 +18,11 @@ npm i @jackens/nnn
|
|
|
18
18
|
|
|
19
19
|
```js
|
|
20
20
|
import {
|
|
21
|
-
CNode,
|
|
22
|
-
HArgs,
|
|
23
|
-
HArgs1,
|
|
24
|
-
LexerSpec,
|
|
25
|
-
LexerSpecOptions,
|
|
21
|
+
type CNode,
|
|
22
|
+
type HArgs,
|
|
23
|
+
type HArgs1,
|
|
26
24
|
c,
|
|
27
|
-
createLexer,
|
|
28
25
|
h,
|
|
29
|
-
monokai,
|
|
30
|
-
nanolightTs,
|
|
31
26
|
rwd,
|
|
32
27
|
s,
|
|
33
28
|
svgUse,
|
|
@@ -42,20 +37,10 @@ import {
|
|
|
42
37
|
Tuple argument type for the [`h`](#h) and [`s`](#s) helpers.
|
|
43
38
|
- [`HArgs1`](#HArgs1):
|
|
44
39
|
Single argument type for the [`h`](#h) and [`s`](#s) helpers.
|
|
45
|
-
- [`LexerSpec`](#LexerSpec):
|
|
46
|
-
A [`createLexer`](#createLexer) spec: a `[metadata, pattern, options?]` tuple.
|
|
47
|
-
- [`LexerSpecOptions`](#LexerSpecOptions):
|
|
48
|
-
Options of a [`createLexer`](#createLexer) spec controlling the lexer state stack.
|
|
49
40
|
- [`c`](#c):
|
|
50
41
|
A minimal CSS-in-JS helper that converts a JavaScript object hierarchy into a CSS string.
|
|
51
|
-
- [`createLexer`](#createLexer):
|
|
52
|
-
A helper for building simple stateful lexers (see also [`nanolightTs`](#nanolightTs)).
|
|
53
42
|
- [`h`](#h):
|
|
54
43
|
A lightweight _HyperScript_-style helper for creating and modifying `HTMLElement`s (see also [`s`](#s)).
|
|
55
|
-
- [`monokai`](#monokai):
|
|
56
|
-
A Monokai-inspired color scheme for use with the [`c`](#c) helper and [`nanolightTs`](#nanolightTs) lexer.
|
|
57
|
-
- [`nanolightTs`](#nanolightTs):
|
|
58
|
-
A TypeScript/JavaScript syntax highlighting lexer built using [`createLexer`](#createLexer).
|
|
59
44
|
- [`rwd`](#rwd):
|
|
60
45
|
A responsive web design helper that generates CSS rules for a grid-like layout of floating elements.
|
|
61
46
|
- [`s`](#s):
|
|
@@ -67,7 +52,7 @@ import {
|
|
|
67
52
|
|
|
68
53
|
```ts
|
|
69
54
|
type CNode = {
|
|
70
|
-
[attributeOrSelector: string]: string | number | CNode | undefined
|
|
55
|
+
[attributeOrSelector: string]: string | number | CNode | null | undefined
|
|
71
56
|
}
|
|
72
57
|
```
|
|
73
58
|
|
|
@@ -89,38 +74,6 @@ type HArgs1 = Record<PropertyKey, unknown> | null | undefined | Node | string |
|
|
|
89
74
|
|
|
90
75
|
Single argument type for the [`h`](#h) and [`s`](#s) helpers.
|
|
91
76
|
|
|
92
|
-
### LexerSpec
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
type LexerSpec<M> = [metadata: M, pattern: string | RegExp, options?: LexerSpecOptions]
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
A [`createLexer`](#createLexer) spec: a `[metadata, pattern, options?]` tuple.
|
|
99
|
-
|
|
100
|
-
### LexerSpecOptions
|
|
101
|
-
|
|
102
|
-
```ts
|
|
103
|
-
type LexerSpecOptions = {
|
|
104
|
-
in?: string | string[]
|
|
105
|
-
pop?: boolean
|
|
106
|
-
push?: string
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
Options of a [`createLexer`](#createLexer) spec controlling the lexer state stack.
|
|
111
|
-
|
|
112
|
-
#### in
|
|
113
|
-
|
|
114
|
-
State(s) in which the spec is active (default: `'root'`).
|
|
115
|
-
|
|
116
|
-
#### pop
|
|
117
|
-
|
|
118
|
-
Whether to pop the current state off the stack after a match (the `'root'` state at the bottom is never popped).
|
|
119
|
-
|
|
120
|
-
#### push
|
|
121
|
-
|
|
122
|
-
State to push onto the stack after a match (applied after `pop`).
|
|
123
|
-
|
|
124
77
|
### c
|
|
125
78
|
|
|
126
79
|
```ts
|
|
@@ -134,14 +87,19 @@ A minimal CSS-in-JS helper that converts a JavaScript object hierarchy into a CS
|
|
|
134
87
|
An object describing CSS rules.
|
|
135
88
|
Keys are selectors or at-rules; values are either CSS property values or nested rule objects.
|
|
136
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
|
+
If a nested selector contains `&`, every `&` is replaced with the parent selector instead (e.g. `'&:hover'` → `a:hover`, `'.c &'` → `.c a`).
|
|
137
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'`).
|
|
138
|
-
|
|
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`).
|
|
93
|
+
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
|
+
Inside a top-level `:root` rule nested keys are concatenated into property names (e.g. `{ ':root': { '--': { bg: 'red' } } }` → `:root{--bg:red}`).
|
|
95
|
+
Properties without a selector (top-level or directly in a top-level at-rule, e.g. `@font-face`) are emitted as bare declarations.
|
|
139
96
|
`null`/`undefined` values are skipped.
|
|
140
97
|
|
|
141
98
|
#### splitter
|
|
142
99
|
|
|
143
100
|
A delimiter used to create unique keys (default: `'$$'`).
|
|
144
101
|
The substring from `splitter` to the end of a key is ignored (e.g. `src$$1` → `src`).
|
|
102
|
+
An empty `splitter` disables this feature.
|
|
145
103
|
|
|
146
104
|
#### Returns
|
|
147
105
|
|
|
@@ -216,7 +174,7 @@ const actual = c({
|
|
|
216
174
|
'@font-face$$1': {
|
|
217
175
|
'font-family': 'Jackens',
|
|
218
176
|
src$$1: 'url(otf/jackens.otf)',
|
|
219
|
-
src$$2: "url(otf/jackens.otf) format('opentype'),
|
|
177
|
+
src$$2: "url(otf/jackens.otf) format('opentype'),url(svg/jackens.svg) format('svg')",
|
|
220
178
|
'font-weight': 'normal',
|
|
221
179
|
'font-style': 'normal',
|
|
222
180
|
},
|
|
@@ -452,208 +410,120 @@ const expected = `
|
|
|
452
410
|
expect(actual).toBe(expected)
|
|
453
411
|
```
|
|
454
412
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
```ts
|
|
458
|
-
const createLexer: <M, T>(
|
|
459
|
-
decorator: (chunk: string, metadata?: M) => T,
|
|
460
|
-
...specs: LexerSpec<M>[]
|
|
461
|
-
) => (code: string) => T[]
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
A helper for building simple stateful lexers (see also [`nanolightTs`](#nanolightTs)).
|
|
465
|
-
|
|
466
|
-
#### Remarks
|
|
467
|
-
|
|
468
|
-
1. Matches starting at an earlier position take precedence.
|
|
469
|
-
2. Among matches at the same position, the longer one wins.
|
|
470
|
-
3. Among matches of the same position and length, the one defined earlier wins.
|
|
471
|
-
4. Only specs active in the current state (the top of the state stack, initially `'root'`) are considered.
|
|
472
|
-
5. Patterns are matched against the whole input, so `^`, `$`, `\b` and lookbehinds see the text around the current position; a sticky (`y`) `RegExp` matches only at the current position.
|
|
473
|
-
6. Empty matches are ignored.
|
|
474
|
-
|
|
475
|
-
#### decorator
|
|
476
|
-
|
|
477
|
-
A function that wraps each chunk of the input.
|
|
478
|
-
It receives the matched text (`chunk`) and the `metadata` of the spec whose pattern produced the match.
|
|
479
|
-
For unmatched text between matches, `metadata` is `undefined`.
|
|
480
|
-
|
|
481
|
-
#### specs
|
|
482
|
-
|
|
483
|
-
An array of tuples `[metadata, pattern, options?]` where:
|
|
484
|
-
|
|
485
|
-
- `metadata`: arbitrary data (e.g., a CSS class name) passed to `decorator` when the pattern matches.
|
|
486
|
-
- `pattern`: a `string` (matched literally) or `RegExp` to match against the input.
|
|
487
|
-
- `options`: optional [`LexerSpecOptions`](#LexerSpecOptions) (states in which the spec is active and state transitions).
|
|
488
|
-
|
|
489
|
-
#### Returns
|
|
490
|
-
|
|
491
|
-
A lexer function that accepts an input string and returns an array of decorated chunks covering the whole input (in order).
|
|
492
|
-
|
|
493
|
-
#### Usage Examples
|
|
494
|
-
|
|
495
|
-
Tokenizing keywords and strings with `RegExp` patterns:
|
|
496
|
-
|
|
497
|
-
```ts
|
|
498
|
-
const lexer = createLexer(
|
|
499
|
-
(chunk, metadata) => ({ chunk, metadata }),
|
|
500
|
-
['keyword', /\b(if|else|for)\b/],
|
|
501
|
-
['string', /"[^"]*"/]
|
|
502
|
-
)
|
|
503
|
-
|
|
504
|
-
const result = lexer('if "hello" else "world"')
|
|
505
|
-
|
|
506
|
-
expect(result).toEqual([
|
|
507
|
-
{ chunk: 'if', metadata: 'keyword' },
|
|
508
|
-
{ chunk: ' ', metadata: undefined },
|
|
509
|
-
{ chunk: '"hello"', metadata: 'string' },
|
|
510
|
-
{ chunk: ' ', metadata: undefined },
|
|
511
|
-
{ chunk: 'else', metadata: 'keyword' },
|
|
512
|
-
{ chunk: ' ', metadata: undefined },
|
|
513
|
-
{ chunk: '"world"', metadata: 'string' },
|
|
514
|
-
])
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
String patterns are matched literally and unmatched text gets `undefined` metadata:
|
|
518
|
-
|
|
519
|
-
```ts
|
|
520
|
-
const lexer = createLexer((chunk, metadata) => `${metadata}:${chunk}`, ['tag', 'BEGIN'], ['end', 'END'])
|
|
521
|
-
|
|
522
|
-
const result = lexer('aBEGINbENDc')
|
|
523
|
-
|
|
524
|
-
expect(result).toEqual(['undefined:a', 'tag:BEGIN', 'undefined:b', 'end:END', 'undefined:c'])
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
Empty input produces an empty result:
|
|
528
|
-
|
|
529
|
-
```ts
|
|
530
|
-
const lexer = createLexer(chunk => chunk, ['test', /test/])
|
|
531
|
-
|
|
532
|
-
const result = lexer('')
|
|
533
|
-
|
|
534
|
-
expect(result).toEqual([])
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
`^` anchors to the start of the whole input:
|
|
538
|
-
|
|
539
|
-
```ts
|
|
540
|
-
const lexer = createLexer((chunk, metadata) => ({ chunk, metadata }), ['start', /^test/])
|
|
541
|
-
|
|
542
|
-
const result = lexer('test here')
|
|
543
|
-
|
|
544
|
-
expect(result).toEqual([
|
|
545
|
-
{ chunk: 'test', metadata: 'start' },
|
|
546
|
-
{ chunk: ' here', metadata: undefined },
|
|
547
|
-
])
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
Matches starting earlier take precedence regardless of spec order:
|
|
551
|
-
|
|
552
|
-
```ts
|
|
553
|
-
const lexer = createLexer((chunk, metadata) => metadata, ['later', /x/], ['earlier', /y/])
|
|
554
|
-
|
|
555
|
-
const result = lexer('yx')
|
|
556
|
-
|
|
557
|
-
expect(result).toEqual(['earlier', 'later'])
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
The longer match wins at the same position:
|
|
413
|
+
`&` is replaced with the parent selector:
|
|
561
414
|
|
|
562
415
|
```ts
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
```ts
|
|
573
|
-
const lexer = createLexer(chunk => chunk, ['empty', ''], ['word', /\w+/])
|
|
574
|
-
|
|
575
|
-
const result = lexer('hello')
|
|
576
|
-
|
|
577
|
-
expect(result).toEqual(['hello'])
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
Input without any match is returned as a single chunk:
|
|
581
|
-
|
|
582
|
-
```ts
|
|
583
|
-
const lexer = createLexer(chunk => chunk, ['test', /xyz/])
|
|
584
|
-
|
|
585
|
-
const result = lexer('abc')
|
|
586
|
-
|
|
587
|
-
expect(result).toEqual(['abc'])
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
Consecutive matches are returned as separate chunks:
|
|
591
|
-
|
|
592
|
-
```ts
|
|
593
|
-
const lexer = createLexer((_chunk, metadata) => metadata, ['a', 'a'], ['b', 'b'])
|
|
416
|
+
const actual = c({
|
|
417
|
+
a: {
|
|
418
|
+
color: 'red',
|
|
419
|
+
'&:hover': { color: 'blue' },
|
|
420
|
+
'.dark &': { color: 'white' },
|
|
421
|
+
'.b,& .c': { margin: 1 },
|
|
422
|
+
},
|
|
423
|
+
})
|
|
594
424
|
|
|
595
|
-
const
|
|
425
|
+
const expected = `
|
|
426
|
+
a{
|
|
427
|
+
color:red
|
|
428
|
+
}
|
|
429
|
+
a:hover{
|
|
430
|
+
color:blue
|
|
431
|
+
}
|
|
432
|
+
.dark a{
|
|
433
|
+
color:white
|
|
434
|
+
}
|
|
435
|
+
a.b,a .c{
|
|
436
|
+
margin:1
|
|
437
|
+
}
|
|
438
|
+
`.replace(/\n\s*/g, '')
|
|
596
439
|
|
|
597
|
-
expect(
|
|
440
|
+
expect(actual).toBe(expected)
|
|
598
441
|
```
|
|
599
442
|
|
|
600
|
-
|
|
443
|
+
Commas inside brackets or quotes do not split selectors and whitespace after a comma is ignored:
|
|
601
444
|
|
|
602
445
|
```ts
|
|
603
|
-
const
|
|
604
|
-
(
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
)
|
|
446
|
+
const actual = c({
|
|
447
|
+
':is(.a,.b)': {
|
|
448
|
+
'.c, .d': { margin: 1 },
|
|
449
|
+
'[title="x,y"]': { margin: 2 },
|
|
450
|
+
},
|
|
451
|
+
})
|
|
609
452
|
|
|
610
|
-
const
|
|
453
|
+
const expected = `
|
|
454
|
+
:is(.a,.b).c,:is(.a,.b).d{
|
|
455
|
+
margin:1
|
|
456
|
+
}
|
|
457
|
+
:is(.a,.b)[title="x,y"]{
|
|
458
|
+
margin:2
|
|
459
|
+
}
|
|
460
|
+
`.replace(/\n\s*/g, '')
|
|
611
461
|
|
|
612
|
-
expect(
|
|
462
|
+
expect(actual).toBe(expected)
|
|
613
463
|
```
|
|
614
464
|
|
|
615
|
-
|
|
465
|
+
At-rules nested in selectors wrap these selectors:
|
|
616
466
|
|
|
617
467
|
```ts
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
468
|
+
const actual = c({
|
|
469
|
+
a: {
|
|
470
|
+
color: 'red',
|
|
471
|
+
'@media(min-width:600px)': {
|
|
472
|
+
color: 'blue',
|
|
473
|
+
'.b': { margin: 1 },
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
})
|
|
623
477
|
|
|
624
|
-
const
|
|
478
|
+
const expected = `
|
|
479
|
+
a{
|
|
480
|
+
color:red
|
|
481
|
+
}
|
|
482
|
+
@media(min-width:600px){
|
|
483
|
+
a{
|
|
484
|
+
color:blue
|
|
485
|
+
}
|
|
486
|
+
a.b{
|
|
487
|
+
margin:1
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
`.replace(/\n\s*/g, '')
|
|
625
491
|
|
|
626
|
-
expect(
|
|
492
|
+
expect(actual).toBe(expected)
|
|
627
493
|
```
|
|
628
494
|
|
|
629
|
-
|
|
495
|
+
Declarations without a selector are separated from subsequent rules:
|
|
630
496
|
|
|
631
497
|
```ts
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const result = lexer('abcb')
|
|
498
|
+
expect(c({ color: 'red', margin: 0 })).toBe('color:red;margin:0')
|
|
635
499
|
|
|
636
|
-
expect(
|
|
500
|
+
expect(c({ color: 'red', a: { margin: 0 }, padding: 0 })).toBe('color:red;a{margin:0}padding:0')
|
|
637
501
|
```
|
|
638
502
|
|
|
639
|
-
|
|
503
|
+
`null` and `undefined` values are skipped:
|
|
640
504
|
|
|
641
505
|
```ts
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
506
|
+
const actual = c({
|
|
507
|
+
a: {
|
|
508
|
+
color: 'red',
|
|
509
|
+
margin: null,
|
|
510
|
+
padding: undefined,
|
|
511
|
+
'.b': null,
|
|
512
|
+
},
|
|
513
|
+
':root': {
|
|
514
|
+
'--': { bg: null, fg: 'black' },
|
|
515
|
+
},
|
|
516
|
+
})
|
|
645
517
|
|
|
646
|
-
expect(
|
|
518
|
+
expect(actual).toBe('a{color:red}:root{--fg:black}')
|
|
647
519
|
```
|
|
648
520
|
|
|
649
|
-
|
|
521
|
+
Custom splitter (an empty one disables splitting):
|
|
650
522
|
|
|
651
523
|
```ts
|
|
652
|
-
|
|
524
|
+
expect(c({ a: { 'src#1': 'url(a.otf)', 'src#2': 'url(a.woff)' } }, '#')).toBe('a{src:url(a.otf);src:url(a.woff)}')
|
|
653
525
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
expect(result).toEqual(['undefined:ab', 'digits:12'])
|
|
526
|
+
expect(c({ a: { b$$c: 1 } }, '')).toBe('a{b$$c:1}')
|
|
657
527
|
```
|
|
658
528
|
|
|
659
529
|
### h
|
|
@@ -661,6 +531,7 @@ expect(result).toEqual(['undefined:ab', 'digits:12'])
|
|
|
661
531
|
```ts
|
|
662
532
|
const h: {
|
|
663
533
|
<T extends keyof HTMLElementTagNameMap>(tag: T, ...args: HArgs1[]): HTMLElementTagNameMap[T]
|
|
534
|
+
(tag: string, ...args: HArgs1[]): HTMLElement
|
|
664
535
|
<N extends Node>(node: N, ...args: HArgs1[]): N
|
|
665
536
|
(tagOrNode: string | Node, ...args: HArgs1[]): Node
|
|
666
537
|
}
|
|
@@ -678,9 +549,9 @@ If a `Node`, that node is modified in place.
|
|
|
678
549
|
Additional arguments processed as follows:
|
|
679
550
|
|
|
680
551
|
- `Object`: maps attributes/properties.
|
|
681
|
-
- Keys starting with `$` set element properties (without the `$` prefix); if the value is a plain object, it is shallow-merged into the existing property instead of replacing it (useful e.g. for `$style`).
|
|
682
|
-
- Other keys set attributes: a key prefixed with `xlink:` sets the attribute via `setAttributeNS` in the
|
|
683
|
-
A value of `true` sets an empty attribute (e.g. `checked=""`), a value of `false` removes the attribute.
|
|
552
|
+
- Keys starting with `$` set element properties (without the `$` prefix); if the value is a plain object (created by `{…}` or `Object.create(null)`), it is shallow-merged into the existing property instead of replacing it (useful e.g. for `$style`), other values (including arrays and class instances) replace the property.
|
|
553
|
+
- Other keys set attributes: a key prefixed with `xlink:`, `xml:` or `xmlns:` sets the attribute via `setAttributeNS` in the corresponding namespace; any other key sets it via `setAttribute`.
|
|
554
|
+
A value of `true` sets an empty attribute (e.g. `checked=""`), a value of `false` removes the attribute, a value of `null`/`undefined` leaves the attribute unchanged.
|
|
684
555
|
- `null`/`undefined`: ignored.
|
|
685
556
|
- `Node`: appended as a child.
|
|
686
557
|
- `string`/`number`: converted to a `Text` node and appended.
|
|
@@ -806,356 +677,44 @@ const elemWithNested = h('div', ['span', 'hello'], ['b', 'world'])
|
|
|
806
677
|
expect(elemWithNested.outerHTML).toBe('<div><span>hello</span><b>world</b></div>')
|
|
807
678
|
```
|
|
808
679
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
```ts
|
|
812
|
-
const monokai: CNode
|
|
813
|
-
```
|
|
814
|
-
|
|
815
|
-
A Monokai-inspired color scheme for use with the [`c`](#c) helper and [`nanolightTs`](#nanolightTs) lexer.
|
|
816
|
-
It styles `pre` and `code>span` elements with the class names produced by [`nanolightTs`](#nanolightTs) (with light, dark and print variants).
|
|
817
|
-
|
|
818
|
-
#### Usage Examples
|
|
819
|
-
|
|
820
|
-
Styling `nanolightTs` output:
|
|
821
|
-
|
|
822
|
-
```ts
|
|
823
|
-
const style = h('style', c(monokai))
|
|
824
|
-
const pre = h('pre', ['code', ...nanolightTs('const x = 42')])
|
|
825
|
-
|
|
826
|
-
expect(style.textContent).toContain('code>span.keyword1{color:var(--keyword1)}')
|
|
827
|
-
expect(style.textContent).toContain('code>span.regexp{color:var(--regexp)}')
|
|
828
|
-
expect(style.textContent).toContain('@media only screen and (prefers-color-scheme: dark){:root{--bg:#2d2a2e;')
|
|
829
|
-
expect(pre.outerHTML).toBe(
|
|
830
|
-
'<pre><code>' +
|
|
831
|
-
'<span class="keyword1">const</span> ' +
|
|
832
|
-
'<span class="identifier4">x</span> ' +
|
|
833
|
-
'<span class="operator">=</span> ' +
|
|
834
|
-
'<span class="number">42</span>' +
|
|
835
|
-
'</code></pre>'
|
|
836
|
-
)
|
|
837
|
-
```
|
|
838
|
-
|
|
839
|
-
### nanolightTs
|
|
840
|
-
|
|
841
|
-
```ts
|
|
842
|
-
const nanolightTs: (code: string) => HArgs1[]
|
|
843
|
-
```
|
|
844
|
-
|
|
845
|
-
A TypeScript/JavaScript syntax highlighting lexer built using [`createLexer`](#createLexer).
|
|
846
|
-
|
|
847
|
-
#### code
|
|
848
|
-
|
|
849
|
-
The source code string to tokenize.
|
|
850
|
-
|
|
851
|
-
#### Returns
|
|
852
|
-
|
|
853
|
-
An array of [`HArgs1`](#HArgs1) elements suitable for rendering with [`h`](#h): plain `string`s for unrecognized text (e.g. whitespace) and `['span', { class }, chunk]` tuples for recognized tokens (class names are styled by [`monokai`](#monokai)).
|
|
854
|
-
|
|
855
|
-
#### Usage Examples
|
|
856
|
-
|
|
857
|
-
Keywords, identifiers, operators, numbers, punctuation, strings and comments:
|
|
680
|
+
`false` removes an attribute, `null` and `undefined` leave it unchanged:
|
|
858
681
|
|
|
859
682
|
```ts
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
expect(nanolightTs(codeJs)).toEqual([
|
|
863
|
-
['span', { class: 'keyword1' }, 'const'],
|
|
864
|
-
' ',
|
|
865
|
-
['span', { class: 'identifier4' }, 'answerToLifeTheUniverseAndEverything'],
|
|
866
|
-
' ',
|
|
867
|
-
['span', { class: 'operator' }, '='],
|
|
868
|
-
' ',
|
|
869
|
-
['span', { class: 'punctuation' }, '{'],
|
|
870
|
-
' ',
|
|
871
|
-
['span', { class: 'number' }, '42'],
|
|
872
|
-
['span', { class: 'operator' }, ':'],
|
|
873
|
-
' ',
|
|
874
|
-
['span', { class: 'number' }, '42'],
|
|
875
|
-
' ',
|
|
876
|
-
['span', { class: 'punctuation' }, '}'],
|
|
877
|
-
['span', { class: 'punctuation' }, '['],
|
|
878
|
-
['span', { class: 'string' }, "'42'"],
|
|
879
|
-
['span', { class: 'punctuation' }, ']'],
|
|
880
|
-
' ',
|
|
881
|
-
['span', { class: 'comment' }, '/* 42 */'],
|
|
882
|
-
])
|
|
883
|
-
```
|
|
683
|
+
const input = h('input', { type: 'checkbox', checked: true, title: 'title' })
|
|
884
684
|
|
|
885
|
-
|
|
685
|
+
h(input, { checked: false, title: null, type: undefined })
|
|
886
686
|
|
|
887
|
-
|
|
888
|
-
const codeJs = 'const x = 42 // answer'
|
|
889
|
-
|
|
890
|
-
expect(nanolightTs(codeJs)).toEqual([
|
|
891
|
-
['span', { class: 'keyword1' }, 'const'],
|
|
892
|
-
' ',
|
|
893
|
-
['span', { class: 'identifier4' }, 'x'],
|
|
894
|
-
' ',
|
|
895
|
-
['span', { class: 'operator' }, '='],
|
|
896
|
-
' ',
|
|
897
|
-
['span', { class: 'number' }, '42'],
|
|
898
|
-
' ',
|
|
899
|
-
['span', { class: 'comment' }, '// answer'],
|
|
900
|
-
])
|
|
687
|
+
expect(input.outerHTML).toBe('<input type="checkbox" title="title">')
|
|
901
688
|
```
|
|
902
689
|
|
|
903
|
-
|
|
690
|
+
Only plain objects are merged into `$`-prefixed properties:
|
|
904
691
|
|
|
905
692
|
```ts
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
' ',
|
|
909
|
-
['span', { class: 'operator' }, '='],
|
|
910
|
-
' ',
|
|
911
|
-
['span', { class: 'regexp' }, '/'],
|
|
912
|
-
['span', { class: 'regexp' }, 'a'],
|
|
913
|
-
['span', { class: 'keyword2' }, '\\/'],
|
|
914
|
-
['span', { class: 'regexp' }, 'b'],
|
|
915
|
-
['span', { class: 'punctuation' }, '['],
|
|
916
|
-
['span', { class: 'regexp' }, '/'],
|
|
917
|
-
['span', { class: 'punctuation' }, ']'],
|
|
918
|
-
['span', { class: 'regexp' }, 'c'],
|
|
919
|
-
['span', { class: 'regexp' }, '/'],
|
|
920
|
-
['span', { class: 'keyword1' }, 'gi'],
|
|
921
|
-
['span', { class: 'punctuation' }, '.'],
|
|
922
|
-
['span', { class: 'identifier2' }, 'test'],
|
|
923
|
-
['span', { class: 'punctuation' }, '('],
|
|
924
|
-
['span', { class: 'identifier4' }, 'y'],
|
|
925
|
-
['span', { class: 'punctuation' }, ')'],
|
|
926
|
-
' ',
|
|
927
|
-
['span', { class: 'operator' }, '/'],
|
|
928
|
-
' ',
|
|
929
|
-
['span', { class: 'number' }, '2'],
|
|
930
|
-
])
|
|
931
|
-
|
|
932
|
-
expect(nanolightTs('a / b / c')).toEqual([
|
|
933
|
-
['span', { class: 'identifier4' }, 'a'],
|
|
934
|
-
' ',
|
|
935
|
-
['span', { class: 'operator' }, '/'],
|
|
936
|
-
' ',
|
|
937
|
-
['span', { class: 'identifier4' }, 'b'],
|
|
938
|
-
' ',
|
|
939
|
-
['span', { class: 'operator' }, '/'],
|
|
940
|
-
' ',
|
|
941
|
-
['span', { class: 'identifier4' }, 'c'],
|
|
942
|
-
])
|
|
943
|
-
|
|
944
|
-
expect(nanolightTs('return / x /')).toEqual([
|
|
945
|
-
['span', { class: 'keyword1' }, 'return'],
|
|
946
|
-
' ',
|
|
947
|
-
['span', { class: 'regexp' }, '/'],
|
|
948
|
-
['span', { class: 'regexp' }, ' x '],
|
|
949
|
-
['span', { class: 'regexp' }, '/'],
|
|
950
|
-
])
|
|
951
|
-
|
|
952
|
-
expect(nanolightTs('x = /=/g')).toEqual([
|
|
953
|
-
['span', { class: 'identifier4' }, 'x'],
|
|
954
|
-
' ',
|
|
955
|
-
['span', { class: 'operator' }, '='],
|
|
956
|
-
' ',
|
|
957
|
-
['span', { class: 'regexp' }, '/='],
|
|
958
|
-
['span', { class: 'regexp' }, '/'],
|
|
959
|
-
['span', { class: 'keyword1' }, 'g'],
|
|
960
|
-
])
|
|
961
|
-
```
|
|
693
|
+
const date = new Date(0)
|
|
694
|
+
const div = h('div', { $date: date, $list: [1, 2] })
|
|
962
695
|
|
|
963
|
-
|
|
696
|
+
expect(div.date).toBe(date)
|
|
964
697
|
|
|
965
|
-
|
|
966
|
-
expect(nanolightTs('`a ${b + `c ${ {d}.d }`} \\${e} $f`')).toEqual([
|
|
967
|
-
['span', { class: 'string' }, '`a '],
|
|
968
|
-
['span', { class: 'punctuation' }, '${'],
|
|
969
|
-
['span', { class: 'identifier4' }, 'b'],
|
|
970
|
-
' ',
|
|
971
|
-
['span', { class: 'operator' }, '+'],
|
|
972
|
-
' ',
|
|
973
|
-
['span', { class: 'string' }, '`c '],
|
|
974
|
-
['span', { class: 'punctuation' }, '${'],
|
|
975
|
-
' ',
|
|
976
|
-
['span', { class: 'punctuation' }, '{'],
|
|
977
|
-
['span', { class: 'identifier4' }, 'd'],
|
|
978
|
-
['span', { class: 'punctuation' }, '}'],
|
|
979
|
-
['span', { class: 'punctuation' }, '.'],
|
|
980
|
-
['span', { class: 'identifier4' }, 'd'],
|
|
981
|
-
' ',
|
|
982
|
-
['span', { class: 'punctuation' }, '}'],
|
|
983
|
-
['span', { class: 'string' }, '`'],
|
|
984
|
-
['span', { class: 'punctuation' }, '}'],
|
|
985
|
-
['span', { class: 'string' }, ' \\${e} $f`'],
|
|
986
|
-
])
|
|
987
|
-
|
|
988
|
-
expect(nanolightTs('`${x}` / 2')).toEqual([
|
|
989
|
-
['span', { class: 'string' }, '`'],
|
|
990
|
-
['span', { class: 'punctuation' }, '${'],
|
|
991
|
-
['span', { class: 'identifier4' }, 'x'],
|
|
992
|
-
['span', { class: 'punctuation' }, '}'],
|
|
993
|
-
['span', { class: 'string' }, '`'],
|
|
994
|
-
' ',
|
|
995
|
-
['span', { class: 'operator' }, '/'],
|
|
996
|
-
' ',
|
|
997
|
-
['span', { class: 'number' }, '2'],
|
|
998
|
-
])
|
|
999
|
-
```
|
|
1000
|
-
|
|
1001
|
-
Comment markers inside strings and unterminated comments:
|
|
1002
|
-
|
|
1003
|
-
```ts
|
|
1004
|
-
expect(nanolightTs('"a // b" // c')).toEqual([
|
|
1005
|
-
['span', { class: 'string' }, '"a // b"'],
|
|
1006
|
-
' ',
|
|
1007
|
-
['span', { class: 'comment' }, '// c'],
|
|
1008
|
-
])
|
|
1009
|
-
|
|
1010
|
-
expect(nanolightTs("'it\\'s' /* x")).toEqual([
|
|
1011
|
-
['span', { class: 'string' }, "'it\\'s'"],
|
|
1012
|
-
' ',
|
|
1013
|
-
['span', { class: 'comment' }, '/* x'],
|
|
1014
|
-
])
|
|
1015
|
-
```
|
|
1016
|
-
|
|
1017
|
-
Hexadecimal, BigInt, numeric separators and exponents:
|
|
1018
|
-
|
|
1019
|
-
```ts
|
|
1020
|
-
expect(nanolightTs('0xFF + 1_000n + .5e-3')).toEqual([
|
|
1021
|
-
['span', { class: 'number' }, '0xFF'],
|
|
1022
|
-
' ',
|
|
1023
|
-
['span', { class: 'operator' }, '+'],
|
|
1024
|
-
' ',
|
|
1025
|
-
['span', { class: 'number' }, '1_000n'],
|
|
1026
|
-
' ',
|
|
1027
|
-
['span', { class: 'operator' }, '+'],
|
|
1028
|
-
' ',
|
|
1029
|
-
['span', { class: 'number' }, '.5e-3'],
|
|
1030
|
-
])
|
|
1031
|
-
```
|
|
698
|
+
h(div, { $list: [3] })
|
|
1032
699
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
```ts
|
|
1036
|
-
expect(nanolightTs('a++ / 2 / b')).toEqual([
|
|
1037
|
-
['span', { class: 'identifier4' }, 'a'],
|
|
1038
|
-
['span', { class: 'operator' }, '++'],
|
|
1039
|
-
' ',
|
|
1040
|
-
['span', { class: 'operator' }, '/'],
|
|
1041
|
-
' ',
|
|
1042
|
-
['span', { class: 'number' }, '2'],
|
|
1043
|
-
' ',
|
|
1044
|
-
['span', { class: 'operator' }, '/'],
|
|
1045
|
-
' ',
|
|
1046
|
-
['span', { class: 'identifier4' }, 'b'],
|
|
1047
|
-
])
|
|
1048
|
-
|
|
1049
|
-
expect(nanolightTs('x?.5:a?.b')).toEqual([
|
|
1050
|
-
['span', { class: 'identifier4' }, 'x'],
|
|
1051
|
-
['span', { class: 'operator' }, '?'],
|
|
1052
|
-
['span', { class: 'number' }, '.5'],
|
|
1053
|
-
['span', { class: 'operator' }, ':'],
|
|
1054
|
-
['span', { class: 'identifier4' }, 'a'],
|
|
1055
|
-
['span', { class: 'punctuation' }, '?.'],
|
|
1056
|
-
['span', { class: 'identifier4' }, 'b'],
|
|
1057
|
-
])
|
|
700
|
+
expect(div.list).toEqual([3])
|
|
1058
701
|
```
|
|
1059
702
|
|
|
1060
|
-
|
|
703
|
+
Namespaced attributes:
|
|
1061
704
|
|
|
1062
705
|
```ts
|
|
1063
|
-
|
|
1064
|
-
['span', { class: 'identifier4' }, 'p'],
|
|
1065
|
-
['span', { class: 'punctuation' }, '.'],
|
|
1066
|
-
['span', { class: 'identifier2' }, 'catch'],
|
|
1067
|
-
['span', { class: 'punctuation' }, '('],
|
|
1068
|
-
['span', { class: 'identifier4' }, 'obj'],
|
|
1069
|
-
['span', { class: 'punctuation' }, '.'],
|
|
1070
|
-
['span', { class: 'identifier4' }, 'in'],
|
|
1071
|
-
['span', { class: 'punctuation' }, ','],
|
|
1072
|
-
' ',
|
|
1073
|
-
['span', { class: 'operator' }, '...'],
|
|
1074
|
-
['span', { class: 'keyword1' }, 'this'],
|
|
1075
|
-
['span', { class: 'punctuation' }, ')'],
|
|
1076
|
-
])
|
|
1077
|
-
```
|
|
1078
|
-
|
|
1079
|
-
Hashbang, decorators and private class members:
|
|
706
|
+
const div = h('div', { 'xml:lang': 'pl', 'ng:x': 1 })
|
|
1080
707
|
|
|
1081
|
-
|
|
1082
|
-
expect(
|
|
1083
|
-
['span', { class: 'comment' }, '#!/usr/bin/env bun'],
|
|
1084
|
-
'\n',
|
|
1085
|
-
['span', { class: 'identifier2' }, '@sealed'],
|
|
1086
|
-
' ',
|
|
1087
|
-
['span', { class: 'keyword1' }, 'class'],
|
|
1088
|
-
' ',
|
|
1089
|
-
['span', { class: 'identifier1' }, 'A'],
|
|
1090
|
-
' ',
|
|
1091
|
-
['span', { class: 'punctuation' }, '{'],
|
|
1092
|
-
' ',
|
|
1093
|
-
['span', { class: 'identifier4' }, '#x'],
|
|
1094
|
-
' ',
|
|
1095
|
-
['span', { class: 'operator' }, '='],
|
|
1096
|
-
' ',
|
|
1097
|
-
['span', { class: 'number' }, '1'],
|
|
1098
|
-
' ',
|
|
1099
|
-
['span', { class: 'punctuation' }, '}'],
|
|
1100
|
-
])
|
|
708
|
+
expect(div.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')).toBe('pl')
|
|
709
|
+
expect(div.getAttribute('ng:x')).toBe('1')
|
|
1101
710
|
```
|
|
1102
711
|
|
|
1103
|
-
|
|
712
|
+
Return types:
|
|
1104
713
|
|
|
1105
714
|
```ts
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
['span', { class: 'punctuation' }, '(?:'],
|
|
1110
|
-
['span', { class: 'keyword3' }, '\\d'],
|
|
1111
|
-
['span', { class: 'operator' }, '{1,3}'],
|
|
1112
|
-
['span', { class: 'operator' }, '|'],
|
|
1113
|
-
['span', { class: 'punctuation' }, '['],
|
|
1114
|
-
['span', { class: 'operator' }, '^'],
|
|
1115
|
-
['span', { class: 'regexp' }, 'a'],
|
|
1116
|
-
['span', { class: 'operator' }, '-'],
|
|
1117
|
-
['span', { class: 'regexp' }, 'z'],
|
|
1118
|
-
['span', { class: 'keyword2' }, '\\]'],
|
|
1119
|
-
['span', { class: 'punctuation' }, ']'],
|
|
1120
|
-
['span', { class: 'punctuation' }, ')'],
|
|
1121
|
-
['span', { class: 'operator' }, '+?'],
|
|
1122
|
-
['span', { class: 'keyword1' }, '\\b'],
|
|
1123
|
-
['span', { class: 'punctuation' }, '(?<'],
|
|
1124
|
-
['span', { class: 'identifier2' }, 'year'],
|
|
1125
|
-
['span', { class: 'punctuation' }, '>'],
|
|
1126
|
-
['span', { class: 'keyword3' }, '\\d'],
|
|
1127
|
-
['span', { class: 'operator' }, '{4}'],
|
|
1128
|
-
['span', { class: 'punctuation' }, ')'],
|
|
1129
|
-
['span', { class: 'regexp' }, '-'],
|
|
1130
|
-
['span', { class: 'identifier2' }, '\\k<year>'],
|
|
1131
|
-
['span', { class: 'identifier2' }, '\\1'],
|
|
1132
|
-
['span', { class: 'keyword1' }, '$'],
|
|
1133
|
-
['span', { class: 'regexp' }, '/'],
|
|
1134
|
-
['span', { class: 'keyword1' }, 'u'],
|
|
1135
|
-
])
|
|
1136
|
-
|
|
1137
|
-
expect(nanolightTs('/(?<=\\$)(?!0)\\p{L}+\\.\\u{1F600}|\\x41[.\\b-]/v')).toEqual([
|
|
1138
|
-
['span', { class: 'regexp' }, '/'],
|
|
1139
|
-
['span', { class: 'punctuation' }, '(?<='],
|
|
1140
|
-
['span', { class: 'keyword2' }, '\\$'],
|
|
1141
|
-
['span', { class: 'punctuation' }, ')'],
|
|
1142
|
-
['span', { class: 'punctuation' }, '(?!'],
|
|
1143
|
-
['span', { class: 'regexp' }, '0'],
|
|
1144
|
-
['span', { class: 'punctuation' }, ')'],
|
|
1145
|
-
['span', { class: 'keyword3' }, '\\p{L}'],
|
|
1146
|
-
['span', { class: 'operator' }, '+'],
|
|
1147
|
-
['span', { class: 'keyword2' }, '\\.'],
|
|
1148
|
-
['span', { class: 'keyword2' }, '\\u{1F600}'],
|
|
1149
|
-
['span', { class: 'operator' }, '|'],
|
|
1150
|
-
['span', { class: 'keyword2' }, '\\x41'],
|
|
1151
|
-
['span', { class: 'punctuation' }, '['],
|
|
1152
|
-
['span', { class: 'regexp' }, '.'],
|
|
1153
|
-
['span', { class: 'keyword2' }, '\\b'],
|
|
1154
|
-
['span', { class: 'regexp' }, '-'],
|
|
1155
|
-
['span', { class: 'punctuation' }, ']'],
|
|
1156
|
-
['span', { class: 'regexp' }, '/'],
|
|
1157
|
-
['span', { class: 'keyword1' }, 'v'],
|
|
1158
|
-
])
|
|
715
|
+
expectTypeOf(h('input')).toEqualTypeOf<HTMLInputElement>()
|
|
716
|
+
expectTypeOf(h('my-element')).toEqualTypeOf<HTMLElement>()
|
|
717
|
+
expectTypeOf(h(document.createTextNode(''))).toEqualTypeOf<Text>()
|
|
1159
718
|
```
|
|
1160
719
|
|
|
1161
720
|
### rwd
|
|
@@ -1166,7 +725,7 @@ const rwd: (
|
|
|
1166
725
|
selector: string,
|
|
1167
726
|
cellWidthPx: number,
|
|
1168
727
|
cellHeightPx: number,
|
|
1169
|
-
...specs: [number, number
|
|
728
|
+
...specs: [columns: number, width?: number, height?: number][]
|
|
1170
729
|
) => void
|
|
1171
730
|
```
|
|
1172
731
|
|
|
@@ -1183,20 +742,24 @@ The CSS selector for the grid item.
|
|
|
1183
742
|
|
|
1184
743
|
#### cellWidthPx
|
|
1185
744
|
|
|
1186
|
-
The base cell width in pixels.
|
|
745
|
+
The base cell width in pixels (a positive number).
|
|
1187
746
|
|
|
1188
747
|
#### cellHeightPx
|
|
1189
748
|
|
|
1190
|
-
The base cell height in pixels.
|
|
749
|
+
The base cell height in pixels (a positive number).
|
|
1191
750
|
|
|
1192
751
|
#### specs
|
|
1193
752
|
|
|
1194
|
-
An array of breakpoint specifications, each a tuple of:
|
|
753
|
+
An array of breakpoint specifications, each a tuple of positive integers:
|
|
1195
754
|
|
|
1196
|
-
- `
|
|
1197
|
-
- `width` (optional, default `1`): number of horizontal cells the element spans.
|
|
755
|
+
- `columns`: number of cells per row; the breakpoint is `min-width: cellWidthPx * columns` (`1` modifies the base rule).
|
|
756
|
+
- `width` (optional, default `1`, at most `columns`): number of horizontal cells the element spans.
|
|
1198
757
|
- `height` (optional, default `1`): number of vertical cells the element spans.
|
|
1199
758
|
|
|
759
|
+
#### Throws
|
|
760
|
+
|
|
761
|
+
A `RangeError` if any of the numeric arguments is out of range.
|
|
762
|
+
|
|
1200
763
|
#### Usage Examples
|
|
1201
764
|
|
|
1202
765
|
Responsive grid cell widths and heights with media queries:
|
|
@@ -1246,11 +809,57 @@ expect(style).toEqual({
|
|
|
1246
809
|
})
|
|
1247
810
|
```
|
|
1248
811
|
|
|
812
|
+
Widths that divide evenly do not use `calc`; existing at-rules are extended:
|
|
813
|
+
|
|
814
|
+
```ts
|
|
815
|
+
const style: CNode = {
|
|
816
|
+
'@media(min-width:400px)': {
|
|
817
|
+
'.r2': { margin: 0 },
|
|
818
|
+
},
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
rwd(style, '.r2', 100, 40, [4, 2], [2, 2, 3])
|
|
822
|
+
|
|
823
|
+
expect(style).toEqual({
|
|
824
|
+
'@media(min-width:400px)': {
|
|
825
|
+
'.r2': {
|
|
826
|
+
margin: 0,
|
|
827
|
+
width: '50%',
|
|
828
|
+
height: '40px',
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
'.r2': {
|
|
832
|
+
'box-sizing': 'border-box',
|
|
833
|
+
display: 'block',
|
|
834
|
+
float: 'left',
|
|
835
|
+
width: '100%',
|
|
836
|
+
height: '40px',
|
|
837
|
+
},
|
|
838
|
+
'@media(min-width:200px)': {
|
|
839
|
+
'.r2': {
|
|
840
|
+
width: '100%',
|
|
841
|
+
height: '120px',
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
})
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
Invalid arguments throw a `RangeError`:
|
|
848
|
+
|
|
849
|
+
```ts
|
|
850
|
+
expect(() => rwd({}, '.x', 0, 50)).toThrow(RangeError)
|
|
851
|
+
expect(() => rwd({}, '.x', 100, Infinity)).toThrow(RangeError)
|
|
852
|
+
expect(() => rwd({}, '.x', 100, 50, [0])).toThrow(RangeError)
|
|
853
|
+
expect(() => rwd({}, '.x', 100, 50, [2, 3])).toThrow(RangeError)
|
|
854
|
+
expect(() => rwd({}, '.x', 100, 50, [2, 1, 1.5])).toThrow(RangeError)
|
|
855
|
+
```
|
|
856
|
+
|
|
1249
857
|
### s
|
|
1250
858
|
|
|
1251
859
|
```ts
|
|
1252
860
|
const s: {
|
|
1253
861
|
<T extends keyof SVGElementTagNameMap>(tag: T, ...args: HArgs1[]): SVGElementTagNameMap[T]
|
|
862
|
+
(tag: string, ...args: HArgs1[]): SVGElement
|
|
1254
863
|
<N extends Node>(node: N, ...args: HArgs1[]): N
|
|
1255
864
|
(tagOrNode: string | Node, ...args: HArgs1[]): Node
|
|
1256
865
|
}
|
|
@@ -1268,9 +877,9 @@ If a `Node`, that node is modified in place.
|
|
|
1268
877
|
Additional arguments processed as follows:
|
|
1269
878
|
|
|
1270
879
|
- `Object`: maps attributes/properties.
|
|
1271
|
-
- Keys starting with `$` set element properties (without the `$` prefix); if the value is a plain object, it is shallow-merged into the existing property instead of replacing it.
|
|
1272
|
-
- Other keys set attributes: a key prefixed with `xlink:` sets the attribute via `setAttributeNS` in the
|
|
1273
|
-
A value of `true` sets an empty attribute
|
|
880
|
+
- Keys starting with `$` set element properties (without the `$` prefix); if the value is a plain object (created by `{…}` or `Object.create(null)`), it is shallow-merged into the existing property instead of replacing it, other values (including arrays and class instances) replace the property.
|
|
881
|
+
- Other keys set attributes: a key prefixed with `xlink:`, `xml:` or `xmlns:` sets the attribute via `setAttributeNS` in the corresponding namespace (e.g. `xml:lang`); any other key sets it via `setAttribute`.
|
|
882
|
+
A value of `true` sets an empty attribute, a value of `false` removes the attribute, a value of `null`/`undefined` leaves the attribute unchanged.
|
|
1274
883
|
- `null`/`undefined`: ignored.
|
|
1275
884
|
- `Node`: appended as a child.
|
|
1276
885
|
- `string`/`number`: converted to a `Text` node and appended.
|
|
@@ -1282,6 +891,16 @@ The created `SVGElement` or the modified node.
|
|
|
1282
891
|
|
|
1283
892
|
#### Usage Examples
|
|
1284
893
|
|
|
894
|
+
Elements (including nested ones) are created in the SVG namespace:
|
|
895
|
+
|
|
896
|
+
```ts
|
|
897
|
+
const svg = s('svg', ['g', ['circle', { r: 1 }]])
|
|
898
|
+
|
|
899
|
+
expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg')
|
|
900
|
+
expect(svg.querySelector('circle')?.namespaceURI).toBe('http://www.w3.org/2000/svg')
|
|
901
|
+
expect(svg.outerHTML).toBe('<svg><g><circle r="1"></circle></g></svg>')
|
|
902
|
+
```
|
|
903
|
+
|
|
1285
904
|
Namespaced `xlink:` attribute set to `true` gets an empty value:
|
|
1286
905
|
|
|
1287
906
|
```ts
|
|
@@ -1290,10 +909,12 @@ const svg = s('svg', { 'xlink:href': true })
|
|
|
1290
909
|
expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('')
|
|
1291
910
|
```
|
|
1292
911
|
|
|
1293
|
-
Namespaced `xlink:` attribute set to `false` is
|
|
912
|
+
Namespaced `xlink:` attribute set to `false` is removed:
|
|
1294
913
|
|
|
1295
914
|
```ts
|
|
1296
|
-
const svg = s('svg', { 'xlink:href':
|
|
915
|
+
const svg = s('svg', { 'xlink:href': '#id' })
|
|
916
|
+
|
|
917
|
+
s(svg, { 'xlink:href': false })
|
|
1297
918
|
|
|
1298
919
|
expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBeNull()
|
|
1299
920
|
```
|
|
@@ -1314,6 +935,22 @@ const svg = s('svg', { 'xlink:title': 42 })
|
|
|
1314
935
|
expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'title')).toBe('42')
|
|
1315
936
|
```
|
|
1316
937
|
|
|
938
|
+
Namespaced `xml:` and `xmlns:` attributes:
|
|
939
|
+
|
|
940
|
+
```ts
|
|
941
|
+
const svg = s('svg', { 'xml:lang': 'pl', 'xmlns:xlink': 'http://www.w3.org/1999/xlink' })
|
|
942
|
+
|
|
943
|
+
expect(svg.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')).toBe('pl')
|
|
944
|
+
expect(svg.getAttributeNS('http://www.w3.org/2000/xmlns/', 'xlink')).toBe('http://www.w3.org/1999/xlink')
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
Return types:
|
|
948
|
+
|
|
949
|
+
```ts
|
|
950
|
+
expectTypeOf(s('circle')).toEqualTypeOf<SVGCircleElement>()
|
|
951
|
+
expectTypeOf(s('my-element')).toEqualTypeOf<SVGElement>()
|
|
952
|
+
```
|
|
953
|
+
|
|
1317
954
|
### svgUse
|
|
1318
955
|
|
|
1319
956
|
```ts
|
|
@@ -1325,7 +962,7 @@ Shorthand for creating an `<svg>` element with a `<use>` child referencing a sym
|
|
|
1325
962
|
Equivalent to:
|
|
1326
963
|
|
|
1327
964
|
```ts
|
|
1328
|
-
s('svg', ['use', {
|
|
965
|
+
s('svg', ['use', { href: '#' + id }], ...args)
|
|
1329
966
|
```
|
|
1330
967
|
|
|
1331
968
|
#### id
|
|
@@ -1342,46 +979,28 @@ An `SVGSVGElement` containing a `<use>` element.
|
|
|
1342
979
|
|
|
1343
980
|
#### Usage Examples
|
|
1344
981
|
|
|
1345
|
-
SVG `use` element referencing a symbol
|
|
982
|
+
SVG `use` element referencing a symbol:
|
|
1346
983
|
|
|
1347
984
|
```ts
|
|
1348
985
|
const svg = svgUse('icon-home')
|
|
1349
986
|
|
|
1350
|
-
expect(svg.
|
|
1351
|
-
|
|
1352
|
-
const useElement = svg.children[0]
|
|
1353
|
-
|
|
1354
|
-
expect(useElement.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('#icon-home')
|
|
1355
|
-
|
|
1356
|
-
const svgWithViewBox = svgUse('icon-star', { viewBox: '0 0 24 24' })
|
|
1357
|
-
|
|
1358
|
-
expect(svgWithViewBox.getAttribute('viewBox')).toBe('0 0 24 24')
|
|
1359
|
-
|
|
1360
|
-
const useViewBox = svgWithViewBox.children[0]
|
|
1361
|
-
|
|
1362
|
-
expect(useViewBox.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('#icon-star')
|
|
1363
|
-
|
|
1364
|
-
const svgWithClass = svgUse('icon-menu', { class: 'icon-btn' })
|
|
1365
|
-
|
|
1366
|
-
expect(svgWithClass.getAttribute('class')).toBe('icon-btn')
|
|
1367
|
-
|
|
1368
|
-
expect(svgWithClass.children.length).toBe(1)
|
|
1369
|
-
|
|
1370
|
-
const useClass = svgWithClass.children[0]
|
|
1371
|
-
|
|
1372
|
-
expect(useClass.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('#icon-menu')
|
|
987
|
+
expect(svg.outerHTML).toBe('<svg><use href="#icon-home"></use></svg>')
|
|
988
|
+
```
|
|
1373
989
|
|
|
1374
|
-
|
|
990
|
+
Additional attributes are set on the outer `svg` element:
|
|
1375
991
|
|
|
1376
|
-
|
|
992
|
+
```ts
|
|
993
|
+
const svg = svgUse('icon-settings', { width: 24, height: 24, class: 'icon' })
|
|
1377
994
|
|
|
1378
|
-
expect(
|
|
995
|
+
expect(svg.outerHTML).toBe('<svg width="24" height="24" class="icon"><use href="#icon-settings"></use></svg>')
|
|
996
|
+
```
|
|
1379
997
|
|
|
1380
|
-
|
|
998
|
+
Additional children are appended to the outer `svg` element:
|
|
1381
999
|
|
|
1382
|
-
|
|
1000
|
+
```ts
|
|
1001
|
+
const svg = svgUse('icon-star', ['title', 'Star'])
|
|
1383
1002
|
|
|
1384
|
-
expect(
|
|
1003
|
+
expect(svg.outerHTML).toBe('<svg><use href="#icon-star"></use><title>Star</title></svg>')
|
|
1385
1004
|
```
|
|
1386
1005
|
|
|
1387
1006
|
## License
|