@jackens/nnn 2026.2.26 → 2026.4.11

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 +21 -43
  2. package/nnn.js +49 -55
  3. package/package.json +1 -1
  4. package/readme.md +270 -105
package/nnn.d.ts CHANGED
@@ -128,6 +128,24 @@ export declare const s: {
128
128
  <N extends Node>(node: N, ...args1: HArgs1[]): N;
129
129
  (tagOrNode: string | Node, ...args1: HArgs1[]): Node;
130
130
  };
131
+ /**
132
+ * Shorthand for creating an SVG element with a `<use>` child referencing an icon by ID.
133
+ *
134
+ * Equivalent to: `s('svg', ['use', { 'xlink:href': '#' + id }], ...args)`.
135
+ *
136
+ * @param id
137
+ *
138
+ * The ID of the symbol to reference (without the `#` prefix).
139
+ *
140
+ * @param args
141
+ *
142
+ * Additional arguments passed to the outer `<svg>` element.
143
+ *
144
+ * @returns
145
+ *
146
+ * An `SVGSVGElement` containing a `<use>` element.
147
+ */
148
+ export declare const svgUse: (id: string, ...args: HArgs1[]) => SVGSVGElement;
131
149
  /**
132
150
  * Checks whether an object has the specified key as its own property.
133
151
  *
@@ -253,40 +271,18 @@ export declare const monokai: CRoot;
253
271
  * An array of {@link HArgs1} elements suitable for rendering with {@link h}.
254
272
  */
255
273
  export declare const nanolightTs: (code: string) => HArgs1[];
256
- /**
257
- * A map from value constructors (or `null`/`undefined`) to escape functions.
258
- *
259
- * Used by {@link escapeValues} and {@link newEscape}.
260
- */
261
- export type EscapeMap = Map<unknown, (value?: unknown) => string>;
262
- /**
263
- * Escapes an array of values using the provided escape map.
264
- *
265
- * @param escapeMap
266
- *
267
- * A map where keys are constructors (e.g., `String`, `Number`) and values are escape functions.
268
- *
269
- * @param values
270
- *
271
- * The array of values to escape.
272
- *
273
- * @returns
274
- *
275
- * An array of escaped strings.
276
- */
277
- export declare const escapeValues: (escapeMap: EscapeMap, values: unknown[]) => string[];
278
274
  /**
279
275
  * Creates a tag function for escaping interpolated values in template literals.
280
276
  *
281
- * @param escapeMap
277
+ * @param escapeFn
282
278
  *
283
- * A map where keys are constructors and values are escape functions.
279
+ * A function that takes a value and returns its escaped string representation.
284
280
  *
285
281
  * @returns
286
282
  *
287
283
  * A tag function that escapes interpolated values using the provided escape map.
288
284
  */
289
- export declare const newEscape: (escapeMap: EscapeMap) => (template: TemplateStringsArray, ...values: unknown[]) => string;
285
+ export declare const newEscape: (escapeFn: (value: any) => string) => (template: TemplateStringsArray, ...values: unknown[]) => string;
290
286
  /**
291
287
  * Creates a function that returns the appropriate noun form based on a numeric value using `Intl.PluralRules`.
292
288
  *
@@ -403,24 +399,6 @@ export declare const pick: <T, K extends keyof T>(ref: T, keys: K[]) => Pick<T,
403
399
  * - `height` (optional, default `1`): number of vertical cells the element spans.
404
400
  */
405
401
  export declare const rwd: (root: CRoot, selector: string, cellWidthPx: number, cellHeightPx: number, ...specs: [number, number?, number?][]) => void;
406
- /**
407
- * Shorthand for creating an SVG element with a `<use>` child referencing an icon by ID.
408
- *
409
- * Equivalent to: `s('svg', ['use', { 'xlink:href': '#' + id }], ...args)`.
410
- *
411
- * @param id
412
- *
413
- * The ID of the symbol to reference (without the `#` prefix).
414
- *
415
- * @param args
416
- *
417
- * Additional arguments passed to the outer `<svg>` element.
418
- *
419
- * @returns
420
- *
421
- * An `SVGSVGElement` containing a `<use>` element.
422
- */
423
- export declare const svgUse: (id: string, ...args: HArgs1[]) => SVGSVGElement;
424
402
  /**
425
403
  * Generates a UUID v1 (time-based) identifier.
426
404
  *
package/nnn.js CHANGED
@@ -66,66 +66,64 @@ var csvParse = (csv, separator = ",") => {
66
66
  var isRecord = (arg) => typeof arg === "object" && arg != null && !isArray(arg);
67
67
 
68
68
  // src/nnn/h.ts
69
- var _h = (namespaceUri) => {
69
+ var XLINK_NS = "http://www.w3.org/1999/xlink";
70
+ var _h = (namespaceUri, tagOrNode, ...args) => {
70
71
  const createElement = namespaceUri == null ? (tag) => document.createElement(tag) : (tag) => document.createElementNS(namespaceUri, tag);
71
- const h = (tagOrNode, ...args) => {
72
- const node = isString(tagOrNode) ? createElement(tagOrNode) : tagOrNode;
73
- args.forEach((arg) => {
74
- let child = null;
75
- if (arg instanceof Node) {
76
- child = arg;
77
- } else if (isArray(arg)) {
78
- child = h(...arg);
79
- } else if (isRecord(arg)) {
80
- for (const name in arg) {
81
- const value = arg[name];
82
- if (name[0] === "$") {
83
- const name1 = name.slice(1);
84
- if (isRecord(value)) {
85
- node[name1] ??= {};
86
- Object.assign(node[name1], value);
87
- } else {
88
- node[name1] = value;
89
- }
90
- } else if (node instanceof Element) {
91
- const indexOfColon = name.indexOf(":");
92
- if (indexOfColon >= 0) {
93
- const nsKey = name.slice(0, indexOfColon);
94
- if (nsKey === "xlink") {
95
- const ns = "http://www.w3.org/1999/xlink";
96
- const basename = name.slice(indexOfColon + 1);
97
- if (value === true) {
98
- node.setAttributeNS(ns, basename, "");
99
- } else if (value === false) {
100
- node.removeAttributeNS(ns, basename);
101
- } else {
102
- node.setAttributeNS(ns, basename, "" + value);
103
- }
104
- }
105
- } else {
72
+ const node = isString(tagOrNode) ? createElement(tagOrNode) : tagOrNode;
73
+ args.forEach((arg) => {
74
+ let child = null;
75
+ if (arg instanceof Node) {
76
+ child = arg;
77
+ } else if (isArray(arg)) {
78
+ child = _h(namespaceUri, ...arg);
79
+ } else if (isRecord(arg)) {
80
+ for (const name in arg) {
81
+ const value = arg[name];
82
+ if (name[0] === "$") {
83
+ const name1 = name.slice(1);
84
+ if (isRecord(value)) {
85
+ node[name1] ??= {};
86
+ Object.assign(node[name1], value);
87
+ } else {
88
+ node[name1] = value;
89
+ }
90
+ } else if (node instanceof Element) {
91
+ const indexOfColon = name.indexOf(":");
92
+ if (indexOfColon >= 0) {
93
+ const nsKey = name.slice(0, indexOfColon);
94
+ if (nsKey === "xlink") {
95
+ const basename = name.slice(indexOfColon + 1);
106
96
  if (value === true) {
107
- node.setAttribute(name, "");
97
+ node.setAttributeNS(XLINK_NS, basename, "");
108
98
  } else if (value === false) {
109
- node.removeAttribute(name);
99
+ node.removeAttributeNS(XLINK_NS, basename);
110
100
  } else {
111
- node.setAttribute(name, "" + value);
101
+ node.setAttributeNS(XLINK_NS, basename, "" + value);
112
102
  }
113
103
  }
104
+ } else {
105
+ if (value === true) {
106
+ node.setAttribute(name, "");
107
+ } else if (value === false) {
108
+ node.removeAttribute(name);
109
+ } else {
110
+ node.setAttribute(name, "" + value);
111
+ }
114
112
  }
115
113
  }
116
- } else if (isString(arg) || isNumber(arg)) {
117
- child = document.createTextNode(arg);
118
- }
119
- if (child != null) {
120
- node.appendChild(child);
121
114
  }
122
- });
123
- return node;
124
- };
125
- return h;
115
+ } else if (isString(arg) || isNumber(arg)) {
116
+ child = document.createTextNode(arg);
117
+ }
118
+ if (child != null) {
119
+ node.appendChild(child);
120
+ }
121
+ });
122
+ return node;
126
123
  };
127
- var h = /* @__PURE__ */ _h();
128
- var s = /* @__PURE__ */ _h("http://www.w3.org/2000/svg");
124
+ var h = _h.bind(null, null);
125
+ var s = _h.bind(null, "http://www.w3.org/2000/svg");
126
+ var svgUse = (id, ...args) => s("svg", ["use", { "xlink:href": "#" + id }], ...args);
129
127
 
130
128
  // src/nnn/fixPlTypography.ts
131
129
  var TAGS_TO_SKIP = ["IFRAME", "NOSCRIPT", "PRE", "SCRIPT", "STYLE", "TEXTAREA"];
@@ -298,8 +296,7 @@ var PUNCTUATION = "punctuation";
298
296
  var STRING = "string";
299
297
  var nanolightTs = /* @__PURE__ */ newTokenizer((chunk, name) => name != null ? ["span", { class: name }, chunk] : chunk, [COMMENT, /\/\*.*?\*\//s], [COMMENT, /(?<!\\)\/\/.*?(?=\n)/], [STRING, /".*?"/], [STRING, /'.*?'/], [STRING, /`.*?`/s], [STRING, /\/[^\s]*[^\\]\/[dgimsuvy]*/], [NUMBER, /0b[01_]+/], [NUMBER, /0o[01234567_]+/], [NUMBER, /0x[\dabcdef_]+/], [NUMBER, /\d[\d_]*(\.[\d_]+)?(e[+-]?[\d_]+)?/], [OPERATOR, "!"], [OPERATOR, "!="], [OPERATOR, "!=="], [OPERATOR, "%"], [OPERATOR, "%="], [OPERATOR, "&&"], [OPERATOR, "&&="], [OPERATOR, "&"], [OPERATOR, "&="], [OPERATOR, "*"], [OPERATOR, "**"], [OPERATOR, "**="], [OPERATOR, "*="], [OPERATOR, "+"], [OPERATOR, "++"], [OPERATOR, "+="], [OPERATOR, "-"], [OPERATOR, "--"], [OPERATOR, "-="], [OPERATOR, "..."], [OPERATOR, "/"], [OPERATOR, "/="], [OPERATOR, ":"], [OPERATOR, "<"], [OPERATOR, "<<"], [OPERATOR, "<<="], [OPERATOR, "<="], [OPERATOR, "="], [OPERATOR, "=="], [OPERATOR, "==="], [OPERATOR, "=>"], [OPERATOR, ">"], [OPERATOR, ">="], [OPERATOR, ">>"], [OPERATOR, ">>="], [OPERATOR, ">>>"], [OPERATOR, ">>>="], [OPERATOR, "?"], [OPERATOR, "?"], [OPERATOR, "??"], [OPERATOR, "??="], [OPERATOR, "^"], [OPERATOR, "^="], [OPERATOR, "|"], [OPERATOR, "|="], [OPERATOR, "||"], [OPERATOR, "||="], [OPERATOR, "~"], [OPERATOR, "~="], [OPERATOR, /(?<=\s):/], [PUNCTUATION, "("], [PUNCTUATION, ")"], [PUNCTUATION, ","], [PUNCTUATION, "."], [PUNCTUATION, ":"], [PUNCTUATION, ";"], [PUNCTUATION, "?."], [PUNCTUATION, "["], [PUNCTUATION, "]"], [PUNCTUATION, "{"], [PUNCTUATION, "}"], [KEYWORD_1, "as"], [KEYWORD_1, "async"], [KEYWORD_1, "await"], [KEYWORD_1, "break"], [KEYWORD_1, "case"], [KEYWORD_1, "catch"], [KEYWORD_1, "class"], [KEYWORD_1, "const"], [KEYWORD_1, "continue"], [KEYWORD_1, "debugger"], [KEYWORD_1, "default"], [KEYWORD_1, "delete"], [KEYWORD_1, "do"], [KEYWORD_1, "else"], [KEYWORD_1, "export"], [KEYWORD_1, "extends"], [KEYWORD_1, "finally"], [KEYWORD_1, "for"], [KEYWORD_1, "from"], [KEYWORD_1, "function"], [KEYWORD_1, "function*"], [KEYWORD_1, "goto"], [KEYWORD_1, "if"], [KEYWORD_1, "import"], [KEYWORD_1, "in"], [KEYWORD_1, "instanceof"], [KEYWORD_1, "is"], [KEYWORD_1, "keyof"], [KEYWORD_1, "let"], [KEYWORD_1, "new"], [KEYWORD_1, "of"], [KEYWORD_1, "package"], [KEYWORD_1, "return"], [KEYWORD_1, "super"], [KEYWORD_1, "switch"], [KEYWORD_1, "this"], [KEYWORD_1, "throw"], [KEYWORD_1, "try"], [KEYWORD_1, "type"], [KEYWORD_1, "typeof"], [KEYWORD_1, "var"], [KEYWORD_1, "void"], [KEYWORD_1, "while"], [KEYWORD_1, "with"], [KEYWORD_1, "yield"], [KEYWORD_1, "yield*"], [KEYWORD_2, "false"], [KEYWORD_2, "Infinity"], [KEYWORD_2, "NaN"], [KEYWORD_2, "null"], [KEYWORD_2, "true"], [KEYWORD_2, "undefined"], [KEYWORD_3, "any"], [KEYWORD_3, "bigint"], [KEYWORD_3, "boolean"], [KEYWORD_3, "eval"], [KEYWORD_3, "number"], [KEYWORD_3, "string"], [KEYWORD_3, "symbol"], [KEYWORD_3, "unknown"], [IDENTIFIER_1, /[\p{Lu}_$][\p{Lu}\d_$]*/u], [IDENTIFIER_2, /[\p{L}_$][\p{L}\d_$]*(?=[(`])/u], [IDENTIFIER_3, /\p{Lu}[\p{L}\d_$]*/u], [IDENTIFIER_4, /[\p{L}_$][\p{L}\d_$]*/u]);
300
298
  // src/nnn/newEscape.ts
301
- var escapeValues = (escapeMap, values) => values.map((value) => (value == null ? escapeMap.get(value) : escapeMap.get(value?.constructor))?.(value) ?? "");
302
- var newEscape = (escapeMap) => (template, ...values) => String.raw(template, ...escapeValues(escapeMap, values));
299
+ var newEscape = (escapeFn) => (template, ...values) => String.raw(template, ...values.map(escapeFn));
303
300
  // src/nnn/newNounForm.ts
304
301
  var PLURAL_RULES = {};
305
302
  var newNounForm = (locale, forms) => (value) => forms[(PLURAL_RULES[locale] ??= new Intl.PluralRules(locale)).select(value)] ?? forms.other ?? "";
@@ -352,8 +349,6 @@ var rwd = (root, selector, cellWidthPx, cellHeightPx, ...specs) => {
352
349
  node.height = `${cellHeightPx * height}px`;
353
350
  }
354
351
  };
355
- // src/nnn/svgUse.ts
356
- var svgUse = (id, ...args) => s("svg", ["use", { "xlink:href": "#" + id }], ...args);
357
352
  // src/nnn/uuidV1.ts
358
353
  var ZEROS = /* @__PURE__ */ "0".repeat(16);
359
354
  var counter = 0;
@@ -384,7 +379,6 @@ export {
384
379
  hasOwn,
385
380
  h,
386
381
  fixPlTypography,
387
- escapeValues,
388
382
  csvParse,
389
383
  c
390
384
  };
package/package.json CHANGED
@@ -44,5 +44,5 @@
44
44
  "name": "@jackens/nnn",
45
45
  "type": "module",
46
46
  "types": "nnn.d.ts",
47
- "version": "2026.2.26"
47
+ "version": "2026.4.11"
48
48
  }
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # nnn
2
2
 
3
- A collection of Jackens’ JavaScript helper utilities (version: `2026.2.26`).
3
+ A collection of Jackens’ JavaScript helper utilities (version: `2026.4.11`).
4
4
 
5
5
  ## Installation
6
6
 
@@ -20,7 +20,6 @@ npm i @jackens/nnn
20
20
  import {
21
21
  c,
22
22
  csvParse,
23
- escapeValues,
24
23
  fixPlTypography,
25
24
  h,
26
25
  hasOwn,
@@ -50,12 +49,10 @@ import {
50
49
 
51
50
  - [`CNode`](#CNode): Represents a CSS rule node for the [`c`](#c) helper. Keys are CSS properties or nested selectors.
52
51
  - [`CRoot`](#CRoot): Represents the root CSS object for the [`c`](#c) helper. Keys are top-level selectors or at-rules.
53
- - [`EscapeMap`](#EscapeMap): A map from value constructors (or `null`/`undefined`) to escape functions.
54
52
  - [`HArgs`](#HArgs): Tuple argument type for the [`h`](#h) and [`s`](#s) helpers.
55
53
  - [`HArgs1`](#HArgs1): Single argument type for the [`h`](#h) and [`s`](#s) helpers.
56
54
  - [`c`](#c): A minimal CSS-in-JS helper that converts a JavaScript object hierarchy into a CSS string.
57
55
  - [`csvParse`](#csvParse): Parses a CSV string into a two-dimensional array of strings.
58
- - [`escapeValues`](#escapeValues): Escapes an array of values using the provided escape map.
59
56
  - [`fixPlTypography`](#fixPlTypography): Applies Polish-specific typographic corrections to a DOM subtree.
60
57
  - [`h`](#h): A lightweight [HyperScript](https://github.com/hyperhype/hyperscript)-style helper for creating and modifying `HTMLElement`s (see also [`s`](#s)).
61
58
  - [`hasOwn`](#hasOwn): Checks whether an object has the specified key as its own property.
@@ -97,16 +94,6 @@ type CRoot = Record<PropertyKey, CNode>;
97
94
 
98
95
  Represents the root CSS object for the [`c`](#c) helper. Keys are top-level selectors or at-rules.
99
96
 
100
- ### EscapeMap
101
-
102
- ```ts
103
- type EscapeMap = Map<unknown, (value?: unknown) => string>;
104
- ```
105
-
106
- A map from value constructors (or `null`/`undefined`) to escape functions.
107
-
108
- Used by [`escapeValues`](#escapeValues) and [`newEscape`](#newEscape).
109
-
110
97
  ### HArgs
111
98
 
112
99
  ```ts
@@ -162,8 +149,8 @@ const actual1 = c({
162
149
  color: 'red',
163
150
  margin: 1,
164
151
  '.c': { margin: 2, padding: 2 },
165
- padding: 1
166
- }
152
+ padding: 1,
153
+ },
167
154
  })
168
155
 
169
156
  const expected1 = `
@@ -187,9 +174,9 @@ const actual2 = c({
187
174
  color: 'red',
188
175
  margin: 1,
189
176
  '.c': { margin: 2, padding: 2 },
190
- padding: 1
191
- }
192
- }
177
+ padding: 1,
178
+ },
179
+ },
193
180
  })
194
181
 
195
182
  const expected2 = `
@@ -214,26 +201,26 @@ const actual3 = c({
214
201
  src$$2: "url(otf/jackens.otf) format('opentype')," +
215
202
  "url(svg/jackens.svg) format('svg')",
216
203
  font_weight: 'normal',
217
- 'font-style': 'normal'
204
+ 'font-style': 'normal',
218
205
  },
219
206
  '@font-face$$2': {
220
207
  font_family: 'C64',
221
- src: 'url(fonts/C64_Pro_Mono-STYLE.woff)'
208
+ src: 'url(fonts/C64_Pro_Mono-STYLE.woff)',
222
209
  },
223
210
  '@keyframes spin': {
224
211
  '0%': { transform: 'rotate(0deg)' },
225
- '100%': { transform: 'rotate(360deg)' }
212
+ '100%': { transform: 'rotate(360deg)' },
226
213
  },
227
214
  div: {
228
215
  border: 'solid red 1px',
229
216
  '.c1': { 'background-color': '#000' },
230
217
  ' .c1': { background_color: 'black' },
231
- '.c2': { backgroundColor: 'rgb(0,0,0)' }
218
+ '.c2': { backgroundColor: 'rgb(0,0,0)' },
232
219
  },
233
220
  '@media(min-width:200px)': {
234
221
  div: { margin: 0, padding: 0 },
235
- span: { color: '#000' }
236
- }
222
+ span: { color: '#000' },
223
+ },
237
224
  })
238
225
 
239
226
  const expected3 = `
@@ -285,10 +272,10 @@ const actual4 = c({
285
272
  '.b,.c': {
286
273
  margin: 1,
287
274
  '.d': {
288
- margin: 2
289
- }
290
- }
291
- }
275
+ margin: 2,
276
+ },
277
+ },
278
+ },
292
279
  })
293
280
 
294
281
  const expected4 = `
@@ -305,9 +292,9 @@ const actual5 = c({
305
292
  '.b,.c': {
306
293
  margin: 1,
307
294
  '.d': {
308
- margin: 2
309
- }
310
- }
295
+ margin: 2,
296
+ },
297
+ },
311
298
  })
312
299
 
313
300
  const expected5 = `
@@ -324,9 +311,9 @@ const actual6 = c({
324
311
  '.a,.b': {
325
312
  margin: 1,
326
313
  '.c,.d': {
327
- margin: 2
328
- }
329
- }
314
+ margin: 2,
315
+ },
316
+ },
330
317
  })
331
318
 
332
319
  const expected6 = `
@@ -377,30 +364,10 @@ yyy",zzz
377
364
  expect(csvParse(text)).to.deep.equal([
378
365
  ['aaa\n"aaa"\naaa', 'bbb', 'ccc,ccc'],
379
366
  ['xxx,xxx', 'yyy\nyyy', 'zzz'],
380
- [' 42 ', '42', ' 17']
367
+ [' 42 ', '42', ' 17'],
381
368
  ])
382
369
  ```
383
370
 
384
- ### escapeValues
385
-
386
- ```ts
387
- const escapeValues: (escapeMap: EscapeMap, values: unknown[]) => string[];
388
- ```
389
-
390
- Escapes an array of values using the provided escape map.
391
-
392
- #### escapeMap
393
-
394
- A map where keys are constructors (e.g., `String`, `Number`) and values are escape functions.
395
-
396
- #### values
397
-
398
- The array of values to escape.
399
-
400
- #### Returns
401
-
402
- An array of escaped strings.
403
-
404
371
  ### fixPlTypography
405
372
 
406
373
  ```ts
@@ -421,13 +388,18 @@ except those inside `IFRAME`, `NOSCRIPT`, `PRE`, `SCRIPT`, `STYLE`, or `TEXTAREA
421
388
  #### Usage Examples
422
389
 
423
390
  ```ts
424
- const p = h('p', 'Pchnąć w tę łódź jeża lub ośm skrzyń fig (zob. https://pl.wikipedia.org/wiki/Pangram).')
391
+ const p = h('p',
392
+ 'Pchnąć w tę łódź jeża lub ośm skrzyń fig (zob. https://pl.wikipedia.org/wiki/Pangram).',
393
+ ['br'],
394
+ ['b', 'Zażółć gęślą jaźń.'],
395
+ )
425
396
 
426
397
  fixPlTypography(p)
427
398
 
428
399
  expect(p.innerHTML).to.deep.equal(
429
400
  'Pchnąć <span style="white-space:nowrap">w </span>tę łódź jeża lub ośm skrzyń fig ' +
430
- '(zob. https://\u200Bpl.\u200Bwikipedia.\u200Borg/\u200Bwiki/\u200BPangram).')
401
+ '(zob. https://\u200Bpl.\u200Bwikipedia.\u200Borg/\u200Bwiki/\u200BPangram).' +
402
+ '<br><b>Zażółć gęślą jaźń.</b>')
431
403
  ```
432
404
 
433
405
  ### h
@@ -522,6 +494,19 @@ expect(div.key).to.deep.equal({ one: 1 })
522
494
  h(div, { $key: { two: 2 } })
523
495
 
524
496
  expect(div.key).to.deep.equal({ one: 1, two: 2 })
497
+
498
+ const elemWithClass = h('div', { class: 'test' })
499
+
500
+ expect(elemWithClass.getAttribute('class')).to.equal('test')
501
+
502
+ const elemWithText = h('div', 'initial')
503
+
504
+ h(elemWithText, ' more')
505
+ expect(elemWithText.outerHTML).to.equal('<div>initial more</div>')
506
+
507
+ const elemWithNested = h('div', ['span', 'hello'], ['b', 'world'])
508
+
509
+ expect(elemWithNested.outerHTML).to.equal('<div><span>hello</span><b>world</b></div>')
525
510
  ```
526
511
 
527
512
  ### hasOwn
@@ -768,10 +753,10 @@ The parsed value with handler substitutions applied.
768
753
  const handlers = {
769
754
  $add: (a: number, b: number) => a + b,
770
755
  $hello: (name: string) => `Hello ${name}!`,
771
- $foo: () => 'bar'
756
+ $foo: () => 'bar',
772
757
  }
773
758
 
774
- const actual = jsOnParse(handlers, `[
759
+ const actual1 = jsOnParse(handlers, `[
775
760
  {
776
761
  "$add": [1, 2]
777
762
  },
@@ -795,23 +780,33 @@ const actual = jsOnParse(handlers, `[
795
780
  }
796
781
  ]`)
797
782
 
798
- const expected = [
783
+ expect(actual1).to.deep.equal([
799
784
  3,
800
785
  'Hello World!',
801
786
  {
802
787
  nested: 'Hello nested World!',
803
788
  one: 1,
804
- two: 2
789
+ two: 2,
805
790
  },
806
791
  'bar',
807
792
  {
808
793
  $foo: ['The parent object does not have exactly one property!'],
809
794
  one: 1,
810
- two: 2
811
- }
812
- ]
795
+ two: 2,
796
+ },
797
+ ])
798
+
799
+ const actual2 = jsOnParse({ $notFunc: 'handler not being a function' } as any, '{"$notFunc": [1, 2]}')
813
800
 
814
- expect(actual).to.deep.equal(expected)
801
+ expect(actual2).to.deep.equal({ $notFunc: [1, 2] })
802
+
803
+ const actual3 = jsOnParse(handlers, '{"$unknown_handler_key": [1, 2]}')
804
+
805
+ expect(actual3).to.deep.equal({ $unknown_handler_key: [1, 2] })
806
+
807
+ const actual4 = jsOnParse(handlers, '{"$add": {"not": "array"}}')
808
+
809
+ expect(actual4).to.deep.equal({ $add: { not: 'array' } })
815
810
  ```
816
811
 
817
812
  ### monokai
@@ -862,21 +857,21 @@ expect(nanolightTs(codeJs)).to.deep.equal([
862
857
  ['span', { class: 'string' }, "'42'"],
863
858
  ['span', { class: 'punctuation' }, ']'],
864
859
  ' ',
865
- ['span', { class: 'comment' }, '/* 42 */']
860
+ ['span', { class: 'comment' }, '/* 42 */'],
866
861
  ])
867
862
  ```
868
863
 
869
864
  ### newEscape
870
865
 
871
866
  ```ts
872
- const newEscape: (escapeMap: EscapeMap) => (template: TemplateStringsArray, ...values: unknown[]) => string;
867
+ const newEscape: (escapeFn: (value: any) => string) => (template: TemplateStringsArray, ...values: unknown[]) => string;
873
868
  ```
874
869
 
875
870
  Creates a tag function for escaping interpolated values in template literals.
876
871
 
877
- #### escapeMap
872
+ #### escapeFn
878
873
 
879
- A map where keys are constructors and values are escape functions.
874
+ A function that takes a value and returns its escaped string representation.
880
875
 
881
876
  #### Returns
882
877
 
@@ -885,27 +880,30 @@ A tag function that escapes interpolated values using the provided escape map.
885
880
  #### Usage Examples
886
881
 
887
882
  ```ts
888
- const escapeMap: EscapeMap = new Map([
889
- [null, () => 'NULL'],
890
- [undefined, () => 'NULL'],
891
- [Array, (values: unknown[]) => escapeValues(escapeMap, values).join(', ')],
892
- [Boolean, (value: boolean) => `b'${+value}'`],
893
- [Date, (value: Date) => `'${value.toISOString().replace(/^(.+)T(.+)\..*$/, '$1 $2')}'`],
894
- [Number, (value: number) => `${value}`],
895
- [String, (value: string) => `'${value.replace(/'/g, "''")}'`]
896
- ])
897
-
898
- const sql = newEscape(escapeMap)
883
+ const escapeFn = (value: any): string =>
884
+ isArray(value)
885
+ ? value.map(escapeFn).join(', ')
886
+ : value === true || value === false
887
+ ? `b'${+value}'`
888
+ : value instanceof Date
889
+ ? `'${value.toISOString().replace(/^(.+)T(.+)\..*$/, '$1 $2')}'`
890
+ : isFiniteNumber(value)
891
+ ? `${value}`
892
+ : isString(value)
893
+ ? `'${value.replace(/'/g, "''")}'`
894
+ : 'NULL'
895
+
896
+ const sql = newEscape(escapeFn)
899
897
 
900
898
  const actual = sql`
901
899
  SELECT *
902
900
  FROM table_name
903
- WHERE column_name IN (${[true, null, undefined, 42, '42', "4'2", new Date(323325000000)]})`
901
+ WHERE column_name IN (${[true, null, undefined, NaN, Infinity, 42, '42', "4'2", /42/, new Date(323325000000)]})`
904
902
 
905
903
  const expected = `
906
904
  SELECT *
907
905
  FROM table_name
908
- WHERE column_name IN (b'1', NULL, NULL, 42, '42', '4''2', '1980-03-31 04:30:00')`
906
+ WHERE column_name IN (b'1', NULL, NULL, NULL, NULL, 42, '42', '4''2', NULL, '1980-03-31 04:30:00')`
909
907
 
910
908
  expect(actual).to.equal(expected)
911
909
  ```
@@ -957,6 +955,13 @@ expect(car(0)).to.equal('cars')
957
955
  expect(car(1)).to.equal('car')
958
956
  expect(car(17)).to.equal('cars')
959
957
  expect(car(42)).to.equal('cars')
958
+
959
+ const empty = newNounForm('en', {})
960
+
961
+ expect(empty(0)).to.equal('')
962
+ expect(empty(1)).to.equal('')
963
+ expect(empty(17)).to.equal('')
964
+ expect(empty(42)).to.equal('')
960
965
  ```
961
966
 
962
967
  ### newTokenizer
@@ -989,6 +994,99 @@ A tokenizer function that accepts a code string and returns an array of decorate
989
994
  2. Among matches at the same position, the longer one wins.
990
995
  3. Among matches of the same position and length, the one defined earlier wins.
991
996
 
997
+ #### Usage Examples
998
+
999
+ ```ts
1000
+ const tokenizer1 = newTokenizer(
1001
+ (chunk, metadata) => ({ chunk, metadata }),
1002
+ ['keyword', /\b(if|else|for)\b/],
1003
+ ['string', /"[^"]*"/],
1004
+ )
1005
+ const result1 = tokenizer1('if "hello" else "world"')
1006
+
1007
+ expect(result1).to.deep.equal([
1008
+ { chunk: 'if', metadata: 'keyword' },
1009
+ { chunk: ' ', metadata: undefined },
1010
+ { chunk: '"hello"', metadata: 'string' },
1011
+ { chunk: ' ', metadata: undefined },
1012
+ { chunk: 'else', metadata: 'keyword' },
1013
+ { chunk: ' ', metadata: undefined },
1014
+ { chunk: '"world"', metadata: 'string' },
1015
+ ])
1016
+
1017
+ const tokenizer2 = newTokenizer(
1018
+ (chunk, metadata) => `${metadata}:${chunk}`,
1019
+ ['tag', 'BEGIN'],
1020
+ ['end', 'END'],
1021
+ )
1022
+ const result2 = tokenizer2('aBEGINbENDc')
1023
+
1024
+ expect(result2).to.deep.equal(['undefined:a', 'tag:BEGIN', 'undefined:b', 'end:END', 'undefined:c'])
1025
+
1026
+ const tokenizer3 = newTokenizer(
1027
+ (chunk) => chunk,
1028
+ ['test', /test/],
1029
+ )
1030
+ const result3 = tokenizer3('')
1031
+
1032
+ expect(result3).to.deep.equal([])
1033
+
1034
+ const tokenizer4 = newTokenizer(
1035
+ (chunk, metadata) => ({ chunk, metadata }),
1036
+ ['start', /^test/],
1037
+ )
1038
+ const result4 = tokenizer4('test here')
1039
+
1040
+ expect(result4).to.deep.equal([
1041
+ { chunk: 'test', metadata: 'start' },
1042
+ { chunk: ' here', metadata: undefined },
1043
+ ])
1044
+
1045
+ const tokenizer5 = newTokenizer(
1046
+ (chunk, metadata) => metadata,
1047
+ ['later', /x/],
1048
+ ['earlier', /y/],
1049
+ )
1050
+ const result5 = tokenizer5('yx')
1051
+
1052
+ expect(result5).to.deep.equal(['earlier', 'later'])
1053
+
1054
+ const tokenizer6 = newTokenizer(
1055
+ (chunk) => chunk,
1056
+ ['short', 'a'],
1057
+ ['long', 'abc'],
1058
+ )
1059
+ const result6 = tokenizer6('abc')
1060
+
1061
+ expect(result6).to.deep.equal(['abc'])
1062
+
1063
+ const tokenizer7 = newTokenizer(
1064
+ (chunk) => chunk,
1065
+ ['empty', ''],
1066
+ ['word', /\w+/],
1067
+ )
1068
+ const result7 = tokenizer7('hello')
1069
+
1070
+ expect(result7).to.deep.equal(['hello'])
1071
+
1072
+ const tokenizer8 = newTokenizer(
1073
+ (chunk) => chunk,
1074
+ ['test', /xyz/],
1075
+ )
1076
+ const result8 = tokenizer8('abc')
1077
+
1078
+ expect(result8).to.deep.equal(['abc'])
1079
+
1080
+ const tokenizer9 = newTokenizer(
1081
+ (chunk, metadata) => metadata,
1082
+ ['a', 'a'],
1083
+ ['b', 'b'],
1084
+ )
1085
+ const result9 = tokenizer9('aabb')
1086
+
1087
+ expect(result9).to.deep.equal(['a', 'a', 'b', 'b'])
1088
+ ```
1089
+
992
1090
  ### omit
993
1091
 
994
1092
  ```ts
@@ -1085,45 +1183,45 @@ An array of breakpoint specifications, each a tuple of:
1085
1183
  ```ts
1086
1184
  const style: CRoot = {
1087
1185
  body: {
1088
- margin: 0
1186
+ margin: 0,
1089
1187
  },
1090
1188
  '.r6': {
1091
1189
  border: 'solid red 1px',
1092
1190
  '.no-border': {
1093
- border: 'none'
1094
- }
1095
- }
1191
+ border: 'none',
1192
+ },
1193
+ },
1096
1194
  }
1097
1195
 
1098
1196
  rwd(style, '.r6', 200, 50, [6], [3], [1, 1, 2])
1099
1197
 
1100
1198
  expect(style).to.deep.equal({
1101
1199
  body: {
1102
- margin: 0
1200
+ margin: 0,
1103
1201
  },
1104
1202
  '.r6': {
1105
1203
  border: 'solid red 1px',
1106
1204
  '.no-border': {
1107
- border: 'none'
1205
+ border: 'none',
1108
1206
  },
1109
1207
  boxSizing: 'border-box',
1110
1208
  display: 'block',
1111
1209
  float: 'left',
1112
1210
  width: '100%',
1113
- height: '100px'
1211
+ height: '100px',
1114
1212
  },
1115
1213
  '@media(min-width:600px)': {
1116
1214
  '.r6': {
1117
1215
  width: 'calc(100% / 3)',
1118
- height: '50px'
1119
- }
1216
+ height: '50px',
1217
+ },
1120
1218
  },
1121
1219
  '@media(min-width:1200px)': {
1122
1220
  '.r6': {
1123
1221
  width: 'calc(50% / 3)',
1124
- height: '50px'
1125
- }
1126
- }
1222
+ height: '50px',
1223
+ },
1224
+ },
1127
1225
  })
1128
1226
  ```
1129
1227
 
@@ -1158,6 +1256,30 @@ Additional arguments processed as follows:
1158
1256
 
1159
1257
  The created or modified `SVGElement`.
1160
1258
 
1259
+ #### Usage Examples
1260
+
1261
+ ```ts
1262
+ const svg = s('svg')
1263
+
1264
+ s(svg, { 'xlink:href': true })
1265
+ expect(svg.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).to.equal('')
1266
+
1267
+ const svg2 = s('svg')
1268
+
1269
+ s(svg2, { 'xlink:href': false })
1270
+ expect(svg2.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).to.be.null
1271
+
1272
+ const svg3 = s('svg')
1273
+
1274
+ s(svg3, { 'xlink:href': 'http://example.com' })
1275
+ expect(svg3.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).to.equal('http://example.com')
1276
+
1277
+ const svg4 = s('svg')
1278
+
1279
+ s(svg4, { 'xlink:title': 42 })
1280
+ expect(svg4.getAttributeNS('http://www.w3.org/1999/xlink', 'title')).to.equal('42')
1281
+ ```
1282
+
1161
1283
  ### svgUse
1162
1284
 
1163
1285
  ```ts
@@ -1180,6 +1302,49 @@ Additional arguments passed to the outer `<svg>` element.
1180
1302
 
1181
1303
  An `SVGSVGElement` containing a `<use>` element.
1182
1304
 
1305
+ #### Usage Examples
1306
+
1307
+ ```ts
1308
+ const XLINK_NS = 'http://www.w3.org/1999/xlink'
1309
+
1310
+ const svg = svgUse('icon-home')
1311
+
1312
+ expect(svg.tagName).to.equal('SVG')
1313
+ expect(svg.children.length).to.equal(1)
1314
+
1315
+ const useElement = svg.children[0]
1316
+
1317
+ expect(useElement.tagName).to.equal('USE')
1318
+ expect(useElement.getAttributeNS(XLINK_NS, 'href')).to.equal('#icon-home')
1319
+
1320
+ const svgWithViewBox = svgUse('icon-star', { viewBox: '0 0 24 24' })
1321
+
1322
+ expect(svgWithViewBox.getAttribute('viewBox')).to.equal('0 0 24 24')
1323
+
1324
+ const useViewBox = svgWithViewBox.children[0]
1325
+
1326
+ expect(useViewBox.getAttributeNS(XLINK_NS, 'href')).to.equal('#icon-star')
1327
+
1328
+ const svgWithClass = svgUse('icon-menu', { class: 'icon-btn' })
1329
+
1330
+ expect(svgWithClass.getAttribute('class')).to.equal('icon-btn')
1331
+ expect(svgWithClass.children.length).to.equal(1)
1332
+
1333
+ const useClass = svgWithClass.children[0]
1334
+
1335
+ expect(useClass.getAttributeNS(XLINK_NS, 'href')).to.equal('#icon-menu')
1336
+
1337
+ const svgWithMultipleAttrs = svgUse('icon-settings', { width: 24, height: 24, class: 'icon' })
1338
+
1339
+ expect(svgWithMultipleAttrs.getAttribute('width')).to.equal('24')
1340
+ expect(svgWithMultipleAttrs.getAttribute('height')).to.equal('24')
1341
+ expect(svgWithMultipleAttrs.getAttribute('class')).to.equal('icon')
1342
+
1343
+ const useMultiple = svgWithMultipleAttrs.children[0]
1344
+
1345
+ expect(useMultiple.getAttributeNS(XLINK_NS, 'href')).to.equal('#icon-settings')
1346
+ ```
1347
+
1183
1348
  ### uuidV1
1184
1349
 
1185
1350
  ```ts
@@ -1282,40 +1447,40 @@ vivify(ref).one.two[3][4]
1282
1447
 
1283
1448
  expect(ref).to.deep.equal({
1284
1449
  one: {
1285
- two: [undefined, undefined, undefined, []]
1286
- }
1450
+ two: [undefined, undefined, undefined, []],
1451
+ },
1287
1452
  })
1288
1453
 
1289
1454
  vivify(ref).one.two[3][4] = 5
1290
1455
 
1291
1456
  expect(ref).to.deep.equal({
1292
1457
  one: {
1293
- two: [undefined, undefined, undefined, [undefined, undefined, undefined, undefined, 5]]
1294
- }
1458
+ two: [undefined, undefined, undefined, [undefined, undefined, undefined, undefined, 5]],
1459
+ },
1295
1460
  })
1296
1461
 
1297
1462
  vivify(ref).one.two[3].length = 1
1298
1463
 
1299
1464
  expect(ref).to.deep.equal({
1300
1465
  one: {
1301
- two: [undefined, undefined, undefined, [undefined]]
1302
- }
1466
+ two: [undefined, undefined, undefined, [undefined]],
1467
+ },
1303
1468
  })
1304
1469
 
1305
1470
  vivify(ref).one.two[3] = 4
1306
1471
 
1307
1472
  expect(ref).to.deep.equal({
1308
1473
  one: {
1309
- two: [undefined, undefined, undefined, 4]
1310
- }
1474
+ two: [undefined, undefined, undefined, 4],
1475
+ },
1311
1476
  })
1312
1477
 
1313
1478
  expect(vivify(ref).one.two.length).to.equal(4)
1314
1479
 
1315
1480
  expect(ref).to.deep.equal({
1316
1481
  one: {
1317
- two: [undefined, undefined, undefined, 4]
1318
- }
1482
+ two: [undefined, undefined, undefined, 4],
1483
+ },
1319
1484
  })
1320
1485
 
1321
1486
  vivify(ref).one.two = 3