@jackens/nnn 2026.9.9 → 2026.9.28

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 (5) hide show
  1. package/README.md +1099 -0
  2. package/nnn.d.ts +175 -204
  3. package/nnn.js +81 -92
  4. package/package.json +2 -13
  5. package/readme.md +0 -1370
package/README.md ADDED
@@ -0,0 +1,1099 @@
1
+ # nnn
2
+
3
+ A collection of Jackens’ JavaScript helper utilities (version: `2026.9.28`).
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ bun i @jackens/nnn
9
+ ```
10
+
11
+ or
12
+
13
+ ```sh
14
+ npm i @jackens/nnn
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```js
20
+ import {
21
+ CNode,
22
+ HArgs,
23
+ HArgs1,
24
+ TokenizerSpec,
25
+ TokenizerSpecOptions,
26
+ c,
27
+ createTokenizer,
28
+ h,
29
+ monokai,
30
+ nanolightTs,
31
+ rwd,
32
+ s,
33
+ svgUse,
34
+ } from '@jackens/nnn' // or './node_modules/@jackens/nnn/nnn.js'
35
+ ```
36
+
37
+ ## Exports
38
+
39
+ - [`CNode`](#CNode):
40
+ Represents a CSS rule node for the [`c`](#c) helper. Keys are CSS properties or nested selectors.
41
+ - [`HArgs`](#HArgs):
42
+ Tuple argument type for the [`h`](#h) and [`s`](#s) helpers.
43
+ - [`HArgs1`](#HArgs1):
44
+ Single argument type for the [`h`](#h) and [`s`](#s) helpers.
45
+ - [`TokenizerSpec`](#TokenizerSpec):
46
+ A [`createTokenizer`](#createTokenizer) spec: a `[metadata, pattern, options?]` tuple.
47
+ - [`TokenizerSpecOptions`](#TokenizerSpecOptions):
48
+ Options of a [`createTokenizer`](#createTokenizer) spec controlling the tokenizer state stack.
49
+ - [`c`](#c):
50
+ A minimal CSS-in-JS helper that converts a JavaScript object hierarchy into a CSS string.
51
+ - [`createTokenizer`](#createTokenizer):
52
+ A helper for building simple stateful tokenizers (see also [`nanolightTs`](#nanolightTs)).
53
+ - [`h`](#h):
54
+ 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) tokenizer.
57
+ - [`nanolightTs`](#nanolightTs):
58
+ A TypeScript/JavaScript syntax highlighting tokenizer built using [`createTokenizer`](#createTokenizer).
59
+ - [`rwd`](#rwd):
60
+ A responsive web design helper that generates CSS rules for a grid-like layout.
61
+ - [`s`](#s):
62
+ A lightweight _HyperScript_-style helper for creating and modifying `SVGElement`s (see also [`h`](#h)).
63
+ - [`svgUse`](#svgUse):
64
+ Shorthand for creating an SVG element with a `<use>` child referencing an icon by ID.
65
+
66
+ ### CNode
67
+
68
+ ```ts
69
+ type CNode = {
70
+ [attributeOrSelector: string]: string | number | CNode | undefined
71
+ }
72
+ ```
73
+
74
+ Represents a CSS rule node for the [`c`](#c) helper. Keys are CSS properties or nested selectors.
75
+
76
+ ### HArgs
77
+
78
+ ```ts
79
+ type HArgs = [string | Node, ...HArgs1[]]
80
+ ```
81
+
82
+ Tuple argument type for the [`h`](#h) and [`s`](#s) helpers.
83
+
84
+ ### HArgs1
85
+
86
+ ```ts
87
+ type HArgs1 = Record<PropertyKey, unknown> | null | undefined | Node | string | number | HArgs
88
+ ```
89
+
90
+ Single argument type for the [`h`](#h) and [`s`](#s) helpers.
91
+
92
+ ### TokenizerSpec
93
+
94
+ ```ts
95
+ type TokenizerSpec<M> = [metadata: M, pattern: string | RegExp, options?: TokenizerSpecOptions]
96
+ ```
97
+
98
+ A [`createTokenizer`](#createTokenizer) spec: a `[metadata, pattern, options?]` tuple.
99
+
100
+ ### TokenizerSpecOptions
101
+
102
+ ```ts
103
+ type TokenizerSpecOptions = {
104
+ in?: string | string[]
105
+ pop?: boolean
106
+ push?: string
107
+ }
108
+ ```
109
+
110
+ Options of a [`createTokenizer`](#createTokenizer) spec controlling the tokenizer 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
+ ### c
125
+
126
+ ```ts
127
+ const c: (node: CNode, splitter?: string) => string
128
+ ```
129
+
130
+ A minimal CSS-in-JS helper that converts a JavaScript object hierarchy into a CSS string.
131
+
132
+ #### node
133
+
134
+ An object describing CSS rules.
135
+ Keys are selectors or at-rules; values are either CSS property values or nested rule objects.
136
+ 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'`).
137
+
138
+ #### splitter
139
+
140
+ A delimiter used to create unique keys (default: `'$$'`).
141
+ The substring from `splitter` to the end of a key is ignored (e.g., `src$$1` → `src`).
142
+
143
+ #### Returns
144
+
145
+ A CSS string representing the compiled rules.
146
+
147
+ #### Usage Examples
148
+
149
+ ```ts
150
+ const actual = c({
151
+ a: {
152
+ color: 'red',
153
+ margin: 1,
154
+ '.c': { margin: 2, padding: 2 },
155
+ padding: 1,
156
+ },
157
+ })
158
+
159
+ const expected = `
160
+ a{
161
+ color:red;
162
+ margin:1
163
+ }
164
+ a.c{
165
+ margin:2;
166
+ padding:2
167
+ }
168
+ a{
169
+ padding:1
170
+ }
171
+ `.replace(/\n\s*/g, '')
172
+
173
+ expect(actual).toBe(expected)
174
+ ```
175
+
176
+ ```ts
177
+ const actual = c({
178
+ a: {
179
+ '.b': {
180
+ color: 'red',
181
+ margin: 1,
182
+ '.c': { margin: 2, padding: 2 },
183
+ padding: 1,
184
+ },
185
+ },
186
+ })
187
+
188
+ const expected = `
189
+ a.b{
190
+ color:red;
191
+ margin:1
192
+ }
193
+ a.b.c{
194
+ margin:2;
195
+ padding:2
196
+ }
197
+ a.b{
198
+ padding:1
199
+ }
200
+ `.replace(/\n\s*/g, '')
201
+
202
+ expect(actual).toBe(expected)
203
+ ```
204
+
205
+ ```ts
206
+ const actual = c({
207
+ '@font-face$$1': {
208
+ 'font-family': 'Jackens',
209
+ src$$1: 'url(otf/jackens.otf)',
210
+ src$$2: "url(otf/jackens.otf) format('opentype')," + "url(svg/jackens.svg) format('svg')",
211
+ 'font-weight': 'normal',
212
+ 'font-style': 'normal',
213
+ },
214
+ '@font-face$$2': {
215
+ 'font-family': 'C64',
216
+ src: 'url(fonts/C64_Pro_Mono-STYLE.woff)',
217
+ },
218
+ '@keyframes spin': {
219
+ '0%': { transform: 'rotate(0deg)' },
220
+ '100%': { transform: 'rotate(360deg)' },
221
+ },
222
+ div: {
223
+ border: 'solid red 1px',
224
+ '.c1': { 'background-color': '#000' },
225
+ ' .c1': { 'background-color': 'black' },
226
+ '.c2': { 'background-color': 'rgb(0,0,0)' },
227
+ },
228
+ '@media(min-width:200px)': {
229
+ div: { margin: 0, padding: 0 },
230
+ span: { color: '#000' },
231
+ },
232
+ })
233
+
234
+ const expected = `
235
+ @font-face{
236
+ font-family:Jackens;
237
+ src:url(otf/jackens.otf);
238
+ src:url(otf/jackens.otf) format('opentype'),url(svg/jackens.svg) format('svg');
239
+ font-weight:normal;
240
+ font-style:normal
241
+ }
242
+ @font-face{
243
+ font-family:C64;
244
+ src:url(fonts/C64_Pro_Mono-STYLE.woff)
245
+ }
246
+ @keyframes spin{
247
+ 0%{
248
+ transform:rotate(0deg)
249
+ }
250
+ 100%{
251
+ transform:rotate(360deg)
252
+ }
253
+ }
254
+ div{
255
+ border:solid red 1px
256
+ }
257
+ div.c1{
258
+ background-color:#000
259
+ }
260
+ div .c1{
261
+ background-color:black
262
+ }
263
+ div.c2{
264
+ background-color:rgb(0,0,0)
265
+ }
266
+ @media(min-width:200px){
267
+ div{
268
+ margin:0;
269
+ padding:0
270
+ }
271
+ span{
272
+ color:#000
273
+ }
274
+ }
275
+ `.replace(/\n\s*/g, '')
276
+
277
+ expect(actual).toBe(expected)
278
+ ```
279
+
280
+ ```ts
281
+ const actual = c({
282
+ a: {
283
+ '.b,.c': {
284
+ margin: 1,
285
+ '.d': {
286
+ margin: 2,
287
+ },
288
+ },
289
+ },
290
+ })
291
+
292
+ const expected = `
293
+ a.b,a.c{
294
+ margin:1
295
+ }
296
+ a.b.d,a.c.d{
297
+ margin:2
298
+ }
299
+ `.replace(/\n\s*/g, '')
300
+
301
+ expect(actual).toBe(expected)
302
+ ```
303
+
304
+ ```ts
305
+ const actual = c({
306
+ '.b,.c': {
307
+ margin: 1,
308
+ '.d': {
309
+ margin: 2,
310
+ },
311
+ },
312
+ })
313
+
314
+ const expected = `
315
+ .b,.c{
316
+ margin:1
317
+ }
318
+ .b.d,.c.d{
319
+ margin:2
320
+ }
321
+ `.replace(/\n\s*/g, '')
322
+
323
+ expect(actual).toBe(expected)
324
+ ```
325
+
326
+ ```ts
327
+ const actual = c({
328
+ '.a,.b': {
329
+ margin: 1,
330
+ '.c,.d': {
331
+ margin: 2,
332
+ },
333
+ },
334
+ })
335
+
336
+ const expected = `
337
+ .a,.b{
338
+ margin:1
339
+ }
340
+ .a.c,.a.d,.b.c,.b.d{
341
+ margin:2
342
+ }
343
+ `.replace(/\n\s*/g, '')
344
+
345
+ expect(actual).toBe(expected)
346
+ ```
347
+
348
+ ```ts
349
+ const actual = c({
350
+ ':root$$monokai': {
351
+ '--': {
352
+ bg: '#faf4f2',
353
+ fg: '#29242a',
354
+ comment: '#918c8e',
355
+ identifier: {
356
+ 1: '#7058be',
357
+ 2: '#269d69',
358
+ 3: '#1c8ca8',
359
+ 4: '#29242a',
360
+ },
361
+ keyword: {
362
+ 1: '#e14775',
363
+ 2: '#7058be',
364
+ 3: '#1c8ca8',
365
+ },
366
+ number: '#7058be',
367
+ operator: '#e14775',
368
+ punctuation: '#918c8e',
369
+ string: '#cc7a0a',
370
+ },
371
+ },
372
+ '@media only screen and (prefers-color-scheme: dark)$$monokai': {
373
+ ':root': {
374
+ '--': {
375
+ bg: '#2d2a2e',
376
+ fg: '#fcfcfa',
377
+ comment: '#727072',
378
+ identifier: {
379
+ 1: '#ae81ff',
380
+ 2: '#a9dc76',
381
+ 3: '#66d9ef',
382
+ 4: '#fcfcfa',
383
+ },
384
+ keyword: {
385
+ 1: '#ff6188',
386
+ 2: '#ae81ff',
387
+ 3: '#66d9ef',
388
+ },
389
+ number: '#ae81ff',
390
+ operator: '#ff6188',
391
+ punctuation: '#727072',
392
+ string: '#ffd866',
393
+ },
394
+ },
395
+ },
396
+ })
397
+
398
+ const expected = `
399
+ :root{
400
+ --bg:#faf4f2;
401
+ --fg:#29242a;
402
+ --comment:#918c8e;
403
+ --identifier1:#7058be;
404
+ --identifier2:#269d69;
405
+ --identifier3:#1c8ca8;
406
+ --identifier4:#29242a;
407
+ --keyword1:#e14775;
408
+ --keyword2:#7058be;
409
+ --keyword3:#1c8ca8;
410
+ --number:#7058be;
411
+ --operator:#e14775;
412
+ --punctuation:#918c8e;
413
+ --string:#cc7a0a
414
+ }
415
+ @media only screen and (prefers-color-scheme: dark){
416
+ :root{
417
+ --bg:#2d2a2e;
418
+ --fg:#fcfcfa;
419
+ --comment:#727072;
420
+ --identifier1:#ae81ff;
421
+ --identifier2:#a9dc76;
422
+ --identifier3:#66d9ef;
423
+ --identifier4:#fcfcfa;
424
+ --keyword1:#ff6188;
425
+ --keyword2:#ae81ff;
426
+ --keyword3:#66d9ef;
427
+ --number:#ae81ff;
428
+ --operator:#ff6188;
429
+ --punctuation:#727072;
430
+ --string:#ffd866
431
+ }
432
+ }
433
+ `.replace(/\n\s*/g, '')
434
+
435
+ expect(actual).toBe(expected)
436
+ ```
437
+
438
+ ### createTokenizer
439
+
440
+ ```ts
441
+ const createTokenizer: <M, T>(
442
+ decorator: (chunk: string, metadata?: M) => T,
443
+ ...specs: TokenizerSpec<M>[]
444
+ ) => (code: string) => T[]
445
+ ```
446
+
447
+ A helper for building simple stateful tokenizers (see also [`nanolightTs`](#nanolightTs)).
448
+
449
+ #### Remarks
450
+
451
+ 1. Matches starting at an earlier position take precedence.
452
+ 2. Among matches at the same position, the longer one wins.
453
+ 3. Among matches of the same position and length, the one defined earlier wins.
454
+ 4. Only specs active in the current state (the top of the state stack, initially `'root'`) are considered.
455
+ 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.
456
+ 6. Empty matches are ignored.
457
+
458
+ #### decorator
459
+
460
+ A function that wraps each matched chunk.
461
+ It receives the matched text (`chunk`) and optionally the `metadata` associated with the pattern that produced the match.
462
+ For unmatched text between patterns, `metadata` is `undefined`.
463
+
464
+ #### specs
465
+
466
+ An array of tuples `[metadata, pattern, options?]` where:
467
+
468
+ - `metadata`: arbitrary data (e.g., a CSS class name) passed to `decorator` when the pattern matches.
469
+ - `pattern`: a `string` or `RegExp` to match against the input.
470
+ - `options`: optional [`TokenizerSpecOptions`](#TokenizerSpecOptions) (states in which the spec is active and state transitions).
471
+
472
+ #### Returns
473
+
474
+ A tokenizer function that accepts a code string and returns an array of decorated tokens.
475
+
476
+ #### Usage Examples
477
+
478
+ ```ts
479
+ const tokenizer = createTokenizer(
480
+ (chunk, metadata) => ({ chunk, metadata }),
481
+ ['keyword', /\b(if|else|for)\b/],
482
+ ['string', /"[^"]*"/]
483
+ )
484
+
485
+ const result = tokenizer('if "hello" else "world"')
486
+
487
+ expect(result).toEqual([
488
+ { chunk: 'if', metadata: 'keyword' },
489
+ { chunk: ' ', metadata: undefined },
490
+ { chunk: '"hello"', metadata: 'string' },
491
+ { chunk: ' ', metadata: undefined },
492
+ { chunk: 'else', metadata: 'keyword' },
493
+ { chunk: ' ', metadata: undefined },
494
+ { chunk: '"world"', metadata: 'string' },
495
+ ])
496
+ ```
497
+
498
+ ```ts
499
+ const tokenizer = createTokenizer((chunk, metadata) => `${metadata}:${chunk}`, ['tag', 'BEGIN'], ['end', 'END'])
500
+
501
+ const result = tokenizer('aBEGINbENDc')
502
+
503
+ expect(result).toEqual(['undefined:a', 'tag:BEGIN', 'undefined:b', 'end:END', 'undefined:c'])
504
+ ```
505
+
506
+ ```ts
507
+ const tokenizer = createTokenizer(chunk => chunk, ['test', /test/])
508
+
509
+ const result = tokenizer('')
510
+
511
+ expect(result).toEqual([])
512
+ ```
513
+
514
+ ```ts
515
+ const tokenizer = createTokenizer((chunk, metadata) => ({ chunk, metadata }), ['start', /^test/])
516
+
517
+ const result = tokenizer('test here')
518
+
519
+ expect(result).toEqual([
520
+ { chunk: 'test', metadata: 'start' },
521
+ { chunk: ' here', metadata: undefined },
522
+ ])
523
+ ```
524
+
525
+ ```ts
526
+ const tokenizer = createTokenizer((chunk, metadata) => metadata, ['later', /x/], ['earlier', /y/])
527
+
528
+ const result = tokenizer('yx')
529
+
530
+ expect(result).toEqual(['earlier', 'later'])
531
+ ```
532
+
533
+ ```ts
534
+ const tokenizer = createTokenizer(chunk => chunk, ['short', 'a'], ['long', 'abc'])
535
+
536
+ const result = tokenizer('abc')
537
+
538
+ expect(result).toEqual(['abc'])
539
+ ```
540
+
541
+ ```ts
542
+ const tokenizer = createTokenizer(chunk => chunk, ['empty', ''], ['word', /\w+/])
543
+
544
+ const result = tokenizer('hello')
545
+
546
+ expect(result).toEqual(['hello'])
547
+ ```
548
+
549
+ ```ts
550
+ const tokenizer = createTokenizer(chunk => chunk, ['test', /xyz/])
551
+
552
+ const result = tokenizer('abc')
553
+
554
+ expect(result).toEqual(['abc'])
555
+ ```
556
+
557
+ ```ts
558
+ const tokenizer = createTokenizer((_chunk, metadata) => metadata, ['a', 'a'], ['b', 'b'])
559
+
560
+ const result = tokenizer('aabb')
561
+
562
+ expect(result).toEqual(['a', 'a', 'b', 'b'])
563
+ ```
564
+
565
+ ```ts
566
+ const tokenizer = createTokenizer(
567
+ (chunk, metadata) => `${metadata}:${chunk}`,
568
+ ['open', '(', { push: 'inner' }],
569
+ ['word', /\w+/, { in: 'inner' }],
570
+ ['close', ')', { in: 'inner', pop: true }]
571
+ )
572
+
573
+ const result = tokenizer('a(b)c)')
574
+
575
+ expect(result).toEqual(['undefined:a', 'open:(', 'word:b', 'close:)', 'undefined:c)'])
576
+ ```
577
+
578
+ ```ts
579
+ const tokenizer = createTokenizer(
580
+ (chunk, metadata) => `${metadata}:${chunk}`,
581
+ ['open', '{', { in: ['root', 'nested'], push: 'nested' }],
582
+ ['close', '}', { in: 'nested', pop: true }]
583
+ )
584
+
585
+ const result = tokenizer('{{}}}')
586
+
587
+ expect(result).toEqual(['open:{', 'open:{', 'close:}', 'close:}', 'undefined:}'])
588
+ ```
589
+
590
+ ```ts
591
+ const tokenizer = createTokenizer((chunk, metadata) => `${metadata}:${chunk}`, ['a', 'a'], ['b', /(?<=a)b/])
592
+
593
+ const result = tokenizer('abcb')
594
+
595
+ expect(result).toEqual(['a:a', 'b:b', 'undefined:cb'])
596
+ ```
597
+
598
+ ```ts
599
+ const tokenizer = createTokenizer((chunk, metadata) => `${metadata}:${chunk}`, ['x', /x/y], ['y', 'y'])
600
+
601
+ const result = tokenizer('xyx.x')
602
+
603
+ expect(result).toEqual(['x:x', 'y:y', 'x:x', 'undefined:.x'])
604
+ ```
605
+
606
+ ```ts
607
+ const tokenizer = createTokenizer((chunk, metadata) => `${metadata}:${chunk}`, ['digits', /\d*/])
608
+
609
+ const result = tokenizer('ab12')
610
+
611
+ expect(result).toEqual(['undefined:ab', 'digits:12'])
612
+ ```
613
+
614
+ ### h
615
+
616
+ ```ts
617
+ const h: {
618
+ <T extends keyof HTMLElementTagNameMap>(tag: T, ...args1: HArgs1[]): HTMLElementTagNameMap[T]
619
+ <N extends Node>(node: N, ...args1: HArgs1[]): N
620
+ (tagOrNode: string | Node, ...args1: HArgs1[]): Node
621
+ }
622
+ ```
623
+
624
+ A lightweight _HyperScript_-style helper for creating and modifying `HTMLElement`s (see also [`s`](#s)).
625
+
626
+ #### tagOrNode
627
+
628
+ If a `string`, it is treated as the tag name for a new element.
629
+ If a `Node`, that node is modified in place.
630
+
631
+ #### args
632
+
633
+ Additional arguments processed as follows:
634
+
635
+ - `Object`: maps attributes/properties.
636
+ 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`).
637
+ Other keys set attributes: a key prefixed with `xlink:` sets the attribute via `setAttributeNS` in the XLink namespace; any other key (without a colon) sets it via `setAttribute`.
638
+ A value of `false` removes the attribute.
639
+ - `null`/`undefined`: ignored.
640
+ - `Node`: appended as a child.
641
+ - `string`/`number`: converted to a `Text` node and appended.
642
+ - [`HArgs`](#HArgs) array: processed recursively.
643
+
644
+ #### Returns
645
+
646
+ The created or modified `HTMLElement`.
647
+
648
+ #### Usage Examples
649
+
650
+ ```ts
651
+ const b = h('b')
652
+
653
+ expect(b.outerHTML).toBe('<b></b>')
654
+
655
+ const i = h('i', 'text')
656
+
657
+ h(b, i)
658
+
659
+ expect(i.outerHTML).toBe('<i>text</i>')
660
+
661
+ expect(b.outerHTML).toBe('<b><i>text</i></b>')
662
+
663
+ h(i, { $className: 'some class' })
664
+
665
+ expect(i.outerHTML).toBe('<i class="some class">text</i>')
666
+
667
+ expect(b.outerHTML).toBe('<b><i class="some class">text</i></b>')
668
+ ```
669
+
670
+ ```ts
671
+ expect(h('span', 'text').outerHTML).toBe('<span>text</span>')
672
+
673
+ expect(h('span', { $innerText: 'text' }).outerHTML).toBe('<span>text</span>')
674
+
675
+ expect(h('span', '42').outerHTML).toBe('<span>42</span>')
676
+
677
+ expect(h('span', 42).outerHTML).toBe('<span>42</span>')
678
+ ```
679
+
680
+ ```ts
681
+ expect(h('div', { style: 'margin:0;padding:0' }).outerHTML).toBe('<div style="margin:0;padding:0"></div>')
682
+
683
+ expect(h('div', { $style: 'margin:0;padding:0' }).outerHTML).toBe('<div style="margin: 0px; padding: 0px;"></div>')
684
+
685
+ expect(h('div', { $style: { margin: 0, padding: 0 } }).outerHTML).toBe(
686
+ '<div style="margin: 0px; padding: 0px;"></div>'
687
+ )
688
+ ```
689
+
690
+ ```ts
691
+ const input1 = h('input', { value: 42 })
692
+ const input2 = h('input', { $value: '42' })
693
+
694
+ expect(input1.value).toBe('42')
695
+ expect(input2.value).toBe('42')
696
+
697
+ expect(input1.outerHTML).toBe('<input value="42">')
698
+ expect(input2.outerHTML).toBe('<input>')
699
+ ```
700
+
701
+ ```ts
702
+ const checkbox1 = h('input', {
703
+ type: 'checkbox',
704
+ checked: true,
705
+ })
706
+
707
+ const checkbox2 = h('input', {
708
+ type: 'checkbox',
709
+ $checked: true,
710
+ })
711
+
712
+ expect(checkbox1.checked).toBe(true)
713
+ expect(checkbox2.checked).toBe(true)
714
+
715
+ expect(checkbox1.outerHTML).toBe('<input type="checkbox" checked="">')
716
+
717
+ expect(checkbox2.outerHTML).toBe('<input type="checkbox">')
718
+ ```
719
+
720
+ ```ts
721
+ const div = h('div')
722
+
723
+ expect(div.key).toBeUndefined()
724
+
725
+ h(div, { $key: { one: 1 } })
726
+
727
+ expect(div.key).toEqual({ one: 1 })
728
+
729
+ h(div, { $key: { two: 2 } })
730
+
731
+ expect(div.key).toEqual({ one: 1, two: 2 })
732
+ ```
733
+
734
+ ```ts
735
+ const elemWithClass = h('div', { class: 'test' })
736
+
737
+ expect(elemWithClass.getAttribute('class')).toBe('test')
738
+
739
+ const elemWithText = h('div', 'initial')
740
+
741
+ h(elemWithText, ' more')
742
+
743
+ expect(elemWithText.outerHTML).toBe('<div>initial more</div>')
744
+
745
+ const elemWithNested = h('div', ['span', 'hello'], ['b', 'world'])
746
+
747
+ expect(elemWithNested.outerHTML).toBe('<div><span>hello</span><b>world</b></div>')
748
+ ```
749
+
750
+ ### monokai
751
+
752
+ ```ts
753
+ const monokai: CNode
754
+ ```
755
+
756
+ A Monokai-inspired color scheme for use with the [`c`](#c) helper and [`nanolightTs`](#nanolightTs) tokenizer.
757
+
758
+ ### nanolightTs
759
+
760
+ ```ts
761
+ const nanolightTs: (code: string) => HArgs1[]
762
+ ```
763
+
764
+ A TypeScript/JavaScript syntax highlighting tokenizer built using [`createTokenizer`](#createTokenizer).
765
+
766
+ #### code
767
+
768
+ The source code string to tokenize.
769
+
770
+ #### Returns
771
+
772
+ An array of [`HArgs1`](#HArgs1) elements suitable for rendering with [`h`](#h).
773
+
774
+ #### Usage Examples
775
+
776
+ ```ts
777
+ const codeJs = 'const answerToLifeTheUniverseAndEverything = ' + "{ 42: 42 }['42'] /* 42 */"
778
+
779
+ expect(nanolightTs(codeJs)).toEqual([
780
+ ['span', { class: 'keyword1' }, 'const'],
781
+ ' ',
782
+ ['span', { class: 'identifier4' }, 'answerToLifeTheUniverseAndEverything'],
783
+ ' ',
784
+ ['span', { class: 'operator' }, '='],
785
+ ' ',
786
+ ['span', { class: 'punctuation' }, '{'],
787
+ ' ',
788
+ ['span', { class: 'number' }, '42'],
789
+ ['span', { class: 'operator' }, ':'],
790
+ ' ',
791
+ ['span', { class: 'number' }, '42'],
792
+ ' ',
793
+ ['span', { class: 'punctuation' }, '}'],
794
+ ['span', { class: 'punctuation' }, '['],
795
+ ['span', { class: 'string' }, "'42'"],
796
+ ['span', { class: 'punctuation' }, ']'],
797
+ ' ',
798
+ ['span', { class: 'comment' }, '/* 42 */'],
799
+ ])
800
+ ```
801
+
802
+ ```ts
803
+ expect(classes('x = /a\\/b[/]c/gi.test(y) / 2')).toEqual([
804
+ 'identifier4:x',
805
+ ' ',
806
+ 'operator:=',
807
+ ' ',
808
+ 'string:/a\\/b[/]c/gi',
809
+ 'punctuation:.',
810
+ 'identifier2:test',
811
+ 'punctuation:(',
812
+ 'identifier4:y',
813
+ 'punctuation:)',
814
+ ' ',
815
+ 'operator:/',
816
+ ' ',
817
+ 'number:2',
818
+ ])
819
+
820
+ expect(classes('a / b / c')).toEqual([
821
+ 'identifier4:a',
822
+ ' ',
823
+ 'operator:/',
824
+ ' ',
825
+ 'identifier4:b',
826
+ ' ',
827
+ 'operator:/',
828
+ ' ',
829
+ 'identifier4:c',
830
+ ])
831
+
832
+ expect(classes('return / x /')).toEqual(['keyword1:return', ' ', 'string:/ x /'])
833
+ ```
834
+
835
+ ```ts
836
+ expect(classes('`a ${b + `c ${ {d}.d }`} \\${e} $f`')).toEqual([
837
+ 'string:`a ',
838
+ 'punctuation:${',
839
+ 'identifier4:b',
840
+ ' ',
841
+ 'operator:+',
842
+ ' ',
843
+ 'string:`c ',
844
+ 'punctuation:${',
845
+ ' ',
846
+ 'punctuation:{',
847
+ 'identifier4:d',
848
+ 'punctuation:}',
849
+ 'punctuation:.',
850
+ 'identifier4:d',
851
+ ' ',
852
+ 'punctuation:}',
853
+ 'string:`',
854
+ 'punctuation:}',
855
+ 'string: \\${e} $f`',
856
+ ])
857
+
858
+ expect(classes('`${x}` / 2')).toEqual([
859
+ 'string:`',
860
+ 'punctuation:${',
861
+ 'identifier4:x',
862
+ 'punctuation:}',
863
+ 'string:`',
864
+ ' ',
865
+ 'operator:/',
866
+ ' ',
867
+ 'number:2',
868
+ ])
869
+ ```
870
+
871
+ ```ts
872
+ expect(classes('"a // b" // c')).toEqual(['string:"a // b"', ' ', 'comment:// c'])
873
+ expect(classes("'it\\'s' /* x")).toEqual(["string:'it\\'s'", ' ', 'comment:/* x'])
874
+ ```
875
+
876
+ ### rwd
877
+
878
+ ```ts
879
+ const rwd: (
880
+ root: CNode,
881
+ selector: string,
882
+ cellWidthPx: number,
883
+ cellHeightPx: number,
884
+ ...specs: [number, number?, number?][]
885
+ ) => void
886
+ ```
887
+
888
+ A responsive web design helper that generates CSS rules for a grid-like layout.
889
+
890
+ #### root
891
+
892
+ The CSS root object to populate (see [`c`](#c)).
893
+
894
+ #### selector
895
+
896
+ The CSS selector for the grid item.
897
+
898
+ #### cellWidthPx
899
+
900
+ The base cell width in pixels.
901
+
902
+ #### cellHeightPx
903
+
904
+ The base cell height in pixels.
905
+
906
+ #### specs
907
+
908
+ An array of breakpoint specifications, each a tuple of:
909
+
910
+ - `maxWidth`: maximum number of cells per row (defines the viewport breakpoint).
911
+ - `width` (optional, default `1`): number of horizontal cells the element spans.
912
+ - `height` (optional, default `1`): number of vertical cells the element spans.
913
+
914
+ #### Usage Examples
915
+
916
+ ```ts
917
+ const style: CNode = {
918
+ body: {
919
+ margin: 0,
920
+ },
921
+ '.r6': {
922
+ border: 'solid red 1px',
923
+ '.no-border': {
924
+ border: 'none',
925
+ },
926
+ },
927
+ }
928
+
929
+ rwd(style, '.r6', 200, 50, [6], [3], [1, 1, 2])
930
+
931
+ expect(style).toEqual({
932
+ body: {
933
+ margin: 0,
934
+ },
935
+ '.r6': {
936
+ border: 'solid red 1px',
937
+ '.no-border': {
938
+ border: 'none',
939
+ },
940
+ boxSizing: 'border-box',
941
+ display: 'block',
942
+ float: 'left',
943
+ width: '100%',
944
+ height: '100px',
945
+ },
946
+ '@media(min-width:600px)': {
947
+ '.r6': {
948
+ width: 'calc(100% / 3)',
949
+ height: '50px',
950
+ },
951
+ },
952
+ '@media(min-width:1200px)': {
953
+ '.r6': {
954
+ width: 'calc(50% / 3)',
955
+ height: '50px',
956
+ },
957
+ },
958
+ })
959
+ ```
960
+
961
+ ### s
962
+
963
+ ```ts
964
+ const s: {
965
+ <T extends keyof SVGElementTagNameMap>(tag: T, ...args1: HArgs1[]): SVGElementTagNameMap[T]
966
+ <N extends Node>(node: N, ...args1: HArgs1[]): N
967
+ (tagOrNode: string | Node, ...args1: HArgs1[]): Node
968
+ }
969
+ ```
970
+
971
+ A lightweight _HyperScript_-style helper for creating and modifying `SVGElement`s (see also [`h`](#h)).
972
+
973
+ #### tagOrNode
974
+
975
+ If a `string`, it is treated as the tag name for a new element.
976
+ If a `Node`, that node is modified in place.
977
+
978
+ #### args
979
+
980
+ Additional arguments processed as follows:
981
+
982
+ - `Object`: maps attributes/properties.
983
+ 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.
984
+ Other keys set attributes: a key prefixed with `xlink:` sets the attribute via `setAttributeNS` in the XLink namespace; any other key (without a colon) sets it via `setAttribute`.
985
+ A value of `false` removes the attribute.
986
+ - `null`/`undefined`: ignored.
987
+ - `Node`: appended as a child.
988
+ - `string`/`number`: converted to a `Text` node and appended.
989
+ - [`HArgs`](#HArgs) array: processed recursively.
990
+
991
+ #### Returns
992
+
993
+ The created or modified `SVGElement`.
994
+
995
+ #### Usage Examples
996
+
997
+ ```ts
998
+ const svg = s('svg', { 'xlink:href': true })
999
+
1000
+ expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('')
1001
+ ```
1002
+
1003
+ ```ts
1004
+ const svg = s('svg', { 'xlink:href': false })
1005
+
1006
+ expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBeNull()
1007
+ ```
1008
+
1009
+ ```ts
1010
+ const svg = s('svg', { 'xlink:href': 'http://example.com' })
1011
+
1012
+ expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('http://example.com')
1013
+ ```
1014
+
1015
+ ```ts
1016
+ const svg = s('svg', { 'xlink:title': 42 })
1017
+
1018
+ expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'title')).toBe('42')
1019
+ ```
1020
+
1021
+ ### svgUse
1022
+
1023
+ ```ts
1024
+ const svgUse: (id: string, ...args: HArgs1[]) => SVGSVGElement
1025
+ ```
1026
+
1027
+ Shorthand for creating an SVG element with a `<use>` child referencing an icon by ID.
1028
+
1029
+ Equivalent to:
1030
+
1031
+ ```ts
1032
+ s('svg', ['use', { 'xlink:href': '#' + id }], ...args)
1033
+ ```
1034
+
1035
+ #### id
1036
+
1037
+ The ID of the symbol to reference (without the `#` prefix).
1038
+
1039
+ #### args
1040
+
1041
+ Additional arguments passed to the outer `<svg>` element.
1042
+
1043
+ #### Returns
1044
+
1045
+ An `SVGSVGElement` containing a `<use>` element.
1046
+
1047
+ #### Usage Examples
1048
+
1049
+ ```ts
1050
+ const svg = svgUse('icon-home')
1051
+
1052
+ expect(svg.children.length).toBe(1)
1053
+
1054
+ const useElement = svg.children[0]
1055
+
1056
+ expect(useElement.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('#icon-home')
1057
+
1058
+ const svgWithViewBox = svgUse('icon-star', { viewBox: '0 0 24 24' })
1059
+
1060
+ expect(svgWithViewBox.getAttribute('viewBox')).toBe('0 0 24 24')
1061
+
1062
+ const useViewBox = svgWithViewBox.children[0]
1063
+
1064
+ expect(useViewBox.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('#icon-star')
1065
+
1066
+ const svgWithClass = svgUse('icon-menu', { class: 'icon-btn' })
1067
+
1068
+ expect(svgWithClass.getAttribute('class')).toBe('icon-btn')
1069
+
1070
+ expect(svgWithClass.children.length).toBe(1)
1071
+
1072
+ const useClass = svgWithClass.children[0]
1073
+
1074
+ expect(useClass.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('#icon-menu')
1075
+
1076
+ const svgWithMultipleAttrs = svgUse('icon-settings', { width: 24, height: 24, class: 'icon' })
1077
+
1078
+ expect(svgWithMultipleAttrs.getAttribute('width')).toBe('24')
1079
+
1080
+ expect(svgWithMultipleAttrs.getAttribute('height')).toBe('24')
1081
+
1082
+ expect(svgWithMultipleAttrs.getAttribute('class')).toBe('icon')
1083
+
1084
+ const useMultiple = svgWithMultipleAttrs.children[0]
1085
+
1086
+ expect(useMultiple.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('#icon-settings')
1087
+ ```
1088
+
1089
+ ## License
1090
+
1091
+ The MIT License (MIT)
1092
+
1093
+ Copyright (c) 2016+ Jackens
1094
+
1095
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
1096
+
1097
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
1098
+
1099
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.