@jackens/nnn 2026.8.31 → 2026.9.1

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 +2 -68
  2. package/nnn.js +104 -108
  3. package/package.json +1 -3
  4. package/readme.md +27 -436
package/nnn.d.ts CHANGED
@@ -1,69 +1,3 @@
1
- /**
2
- * Represents a CSS rule node for the {@link c} helper.
3
- * Keys are CSS properties or nested selectors.
4
- */
5
- export type CNode = {
6
- [attributeOrSelector: string]: string | number | CNode | undefined;
7
- };
8
- /**
9
- * Represents the root CSS object for the {@link c} helper.
10
- * Keys are top-level selectors or at-rules.
11
- */
12
- export type CRoot = Record<PropertyKey, CNode>;
13
- /**
14
- * A minimal CSS-in-JS helper
15
- * that converts a JavaScript object hierarchy
16
- * into a CSS string.
17
- *
18
- * @param root
19
- *
20
- * An object describing CSS rules.
21
- * Keys are selectors or at-rules;
22
- * values are either CSS property values
23
- * or nested rule objects.
24
- *
25
- * @param splitter
26
- *
27
- * A delimiter used to create unique keys (default: `'$$'`).
28
- * The substring from `splitter` to the end of a key
29
- * is ignored (e.g., `src$$1` → `src`).
30
- *
31
- * @returns
32
- *
33
- * A CSS string representing the compiled rules.
34
- *
35
- * @remarks
36
- *
37
- * - Keys whose values are primitives (`string` | `number`)
38
- * are treated as CSS properties.
39
- * - In property keys, uppercase letters
40
- * become lowercase with a `-` prefix
41
- * (e.g., `fontFamily` → `font-family`);
42
- * underscores become hyphens
43
- * (e.g., `font_family` → `font-family`).
44
- * - Comma-separated selector keys
45
- * expand into multiple selectors
46
- * (e.g., `{ div: { '.a,.b': { margin: 1 } } }` →
47
- * `div.a,div.b{margin:1}`).
48
- * - Top-level keys starting with `@` (at-rules)
49
- * are not concatenated with child selectors.
50
- */
51
- export declare const c: (root: CRoot, splitter?: string) => string;
52
- /**
53
- * Creates a tag function
54
- * for escaping interpolated values in template literals.
55
- *
56
- * @param escapeFn
57
- *
58
- * A function that takes a value
59
- * and returns its escaped string representation.
60
- *
61
- * @returns
62
- *
63
- * A tag function that escapes interpolated values
64
- * using the provided escape map.
65
- */
66
- export declare const createEscape: (escapeFn: (value: any) => string) => (template: TemplateStringsArray, ...values: unknown[]) => string;
67
1
  /**
68
2
  * A helper for building simple tokenizers
69
3
  * (see also {@link nanolightTs}).
@@ -363,7 +297,7 @@ export declare const jsOnParse: (handlers: Record<PropertyKey, Function>, text:
363
297
  * for use with the {@link c} helper
364
298
  * and {@link nanolightTs} tokenizer.
365
299
  */
366
- export declare const monokai: CRoot;
300
+ export declare const monokai: CNode;
367
301
  /**
368
302
  * A TypeScript/JavaScript syntax highlighting tokenizer
369
303
  * built using {@link createTokenizer}.
@@ -450,7 +384,7 @@ export declare const pick: <T, K extends keyof T>(ref: T, keys: unknown[]) => Pi
450
384
  * - `height` (optional, default `1`):
451
385
  * number of vertical cells the element spans.
452
386
  */
453
- export declare const rwd: (root: CRoot, selector: string, cellWidthPx: number, cellHeightPx: number, ...specs: [number, number?, number?][]) => void;
387
+ export declare const rwd: (root: CNode, selector: string, cellWidthPx: number, cellHeightPx: number, ...specs: [number, number?, number?][]) => void;
454
388
  /**
455
389
  * Generates a UUID v1 (time-based) identifier.
456
390
  *
package/nnn.js CHANGED
@@ -7,56 +7,56 @@ var isRecord = (arg) => typeof arg === "object" && arg != null && !isArray(arg);
7
7
 
8
8
  // src/nnn/c.ts
9
9
  var DEFAULT_SPLITTER = "$$";
10
- var getName = (key, splitter = DEFAULT_SPLITTER) => key.split(splitter)[0].replace(/_/g, "-").replace(/([A-Z])/g, (_, letter) => "-" + letter.toLowerCase());
11
- var _c = (node, prefix, result, splitter) => {
12
- const queue = [[node, prefix]];
13
- while (queue.length > 0) {
14
- const [style0, prefix0] = queue.shift();
15
- if (isArray(style0)) {
16
- result.push(prefix0, prefix0 !== "" ? "{" : "", style0.join(";"), prefix0 !== "" ? "}" : "");
17
- } else {
18
- const todo = [];
19
- let attributes = [];
20
- let attributesPushed = false;
21
- for (const key in style0) {
22
- const value = style0[key];
23
- const name = getName(key, splitter);
24
- if (isString(value) || isNumber(value)) {
25
- if (!attributesPushed) {
26
- attributesPushed = true;
27
- attributes = [];
28
- todo.push([attributes, prefix0]);
29
- }
30
- attributes.push(`${name}:${value}`);
31
- } else if (value != null) {
32
- attributesPushed = false;
33
- const prefixN = [];
34
- const nameChunks = name.split(",");
35
- prefix0.split(",").forEach((prefixChunk) => nameChunks.forEach((nameChunk) => prefixN.push(prefixChunk + nameChunk)));
36
- todo.push([value, prefixN.join()]);
37
- }
38
- }
39
- queue.unshift(...todo);
10
+ var combinePrefixAndName = (prefix, name) => {
11
+ const prefixes = prefix === "" ? [""] : prefix.split(",");
12
+ const names = name.split(",");
13
+ const combined = [];
14
+ for (const p of prefixes) {
15
+ for (const n of names) {
16
+ combined.push(p + n);
40
17
  }
41
18
  }
19
+ return combined.join(",");
42
20
  };
43
- var c = (root, splitter = DEFAULT_SPLITTER) => {
44
- const chunks = [];
45
- for (const key in root) {
46
- const value = root[key];
47
- const name = getName(key, splitter);
48
- if (value != null) {
49
- if (key[0] === "@") {
50
- chunks.push(name + "{");
51
- _c(value, "", chunks, splitter);
52
- chunks.push("}");
21
+ var _c = (node, splitter = DEFAULT_SPLITTER, prefix = "", flatten = false) => {
22
+ const out = [];
23
+ let buffer = [];
24
+ const flush = () => {
25
+ if (buffer.length === 0) {
26
+ return;
27
+ }
28
+ out.push(prefix === "" ? buffer.join(";") : `${prefix}{${buffer.join(";")}}`);
29
+ buffer = [];
30
+ };
31
+ for (const [rawKey, value] of Object.entries(node)) {
32
+ if (value == null) {
33
+ continue;
34
+ }
35
+ const splitterIndex = rawKey.indexOf(splitter);
36
+ const name = splitterIndex < 0 ? rawKey : rawKey.slice(0, splitterIndex);
37
+ if (isNumber(value) || isString(value)) {
38
+ if (flatten) {
39
+ out.push(`${prefix + name}:${value}`);
53
40
  } else {
54
- _c(value, name, chunks, splitter);
41
+ buffer.push(`${name}:${value}`);
55
42
  }
43
+ continue;
44
+ }
45
+ if (flatten) {
46
+ out.push(_c(value, splitter, prefix + name, true));
47
+ continue;
48
+ }
49
+ flush();
50
+ if (prefix === "" && (name === ":root" || name[0] === "@")) {
51
+ out.push(`${name}{${_c(value, splitter, "", name === ":root")}}`);
52
+ } else {
53
+ out.push(_c(value, splitter, combinePrefixAndName(prefix, name), false));
56
54
  }
57
55
  }
58
- return chunks.join("");
56
+ flush();
57
+ return out.join(flatten ? ";" : "");
59
58
  };
59
+ var c = (node, splitter = DEFAULT_SPLITTER) => _c(node, splitter);
60
60
  // src/nnn/createEscape.ts
61
61
  var createEscape = (escapeFn) => (template, ...values) => String.raw(template, ...values.map(escapeFn));
62
62
  // src/nnn/createTokenizer.ts
@@ -201,51 +201,38 @@ var fixPlTypography = (node) => {
201
201
  };
202
202
  // src/nnn/hasOwn.ts
203
203
  var hasOwn = (ref, key) => ref != null && Object.hasOwn(ref, key);
204
- // src/nnn/jsOnParse.ts
205
- var jsOnParse = (handlers, text) => JSON.parse(text, (key, value) => {
206
- if (isRecord(value)) {
207
- let isSecondKey = false;
208
- for (key in value) {
209
- if (isSecondKey) {
210
- return value;
211
- }
212
- isSecondKey = true;
213
- }
214
- const handler = handlers[key];
215
- const params = value[key];
216
- if (handler instanceof Function && isArray(params)) {
217
- return handler(...params);
218
- }
219
- }
220
- return value;
221
- });
222
204
  // src/nnn/monokai.ts
223
- var darkKey = "@media only screen and (prefers-color-scheme: dark)" + "$$monokai";
224
205
  var monokai = {
225
206
  ":root$$monokai": {
226
- __bg: "#faf4f2",
227
- __fg: "#29242a",
228
- __comment: "#918c8e",
229
- __identifier_1: "#7058be",
230
- __identifier_2: "#269d69",
231
- __identifier_3: "#1c8ca8",
232
- __identifier_4: "#29242a",
233
- __keyword_1: "#e14775",
234
- __keyword_2: "#7058be",
235
- __keyword_3: "#1c8ca8",
236
- __number: "#7058be",
237
- __operator: "#e14775",
238
- __punctuation: "#918c8e",
239
- __string: "#cc7a0a"
207
+ "--": {
208
+ bg: "#faf4f2",
209
+ fg: "#29242a",
210
+ comment: "#918c8e",
211
+ identifier: {
212
+ 1: "#7058be",
213
+ 2: "#269d69",
214
+ 3: "#1c8ca8",
215
+ 4: "#29242a"
216
+ },
217
+ keyword: {
218
+ 1: "#e14775",
219
+ 2: "#7058be",
220
+ 3: "#1c8ca8"
221
+ },
222
+ number: "#7058be",
223
+ operator: "#e14775",
224
+ punctuation: "#918c8e",
225
+ string: "#cc7a0a"
226
+ }
240
227
  },
241
228
  pre$$monokai: {
242
- backgroundColor: "var(--bg)",
229
+ "background-color": "var(--bg)",
243
230
  color: "var(--fg)",
244
231
  margin: 0,
245
232
  padding: "1em",
246
233
  overflow: "visible",
247
234
  " code": {
248
- fontFamily: '"Source Code Pro"',
235
+ "font-family": '"Source Code Pro"',
249
236
  padding: 0
250
237
  }
251
238
  },
@@ -253,51 +240,61 @@ var monokai = {
253
240
  bg: { color: "var(--bg)" },
254
241
  fg: { color: "var(--fg)" },
255
242
  comment: { color: "var(--comment)" },
256
- identifier_1: { color: "var(--identifier-1)" },
257
- identifier_2: { color: "var(--identifier-2)" },
258
- identifier_3: { color: "var(--identifier-3)" },
259
- identifier_4: { color: "var(--identifier-4)" },
260
- keyword_1: { color: "var(--keyword-1)" },
261
- keyword_2: { color: "var(--keyword-2)" },
262
- keyword_3: { color: "var(--keyword-3)" },
243
+ identifier: {
244
+ 1: { color: "var(--identifier1)" },
245
+ 2: { color: "var(--identifier2)" },
246
+ 3: { color: "var(--identifier3)" },
247
+ 4: { color: "var(--identifier4)" }
248
+ },
249
+ keyword: {
250
+ 1: { color: "var(--keyword1)" },
251
+ 2: { color: "var(--keyword2)" },
252
+ 3: { color: "var(--keyword3)" }
253
+ },
263
254
  number: { color: "var(--number)" },
264
255
  operator: { color: "var(--operator)" },
265
256
  punctuation: { color: "var(--punctuation)" },
266
257
  string: { color: "var(--string)" }
267
258
  },
268
- [darkKey]: {
259
+ "@media only screen and (prefers-color-scheme: dark)$$monokai": {
269
260
  ":root": {
270
- __bg: "#2d2a2e",
271
- __fg: "#fcfcfa",
272
- __comment: "#727072",
273
- __identifier_1: "#ae81ff",
274
- __identifier_2: "#a9dc76",
275
- __identifier_3: "#66d9ef",
276
- __identifier_4: "#fcfcfa",
277
- __keyword_1: "#ff6188",
278
- __keyword_2: "#ae81ff",
279
- __keyword_3: "#66d9ef",
280
- __number: "#ae81ff",
281
- __operator: "#ff6188",
282
- __punctuation: "#727072",
283
- __string: "#ffd866"
261
+ "--": {
262
+ bg: "#2d2a2e",
263
+ fg: "#fcfcfa",
264
+ comment: "#727072",
265
+ identifier: {
266
+ 1: "#ae81ff",
267
+ 2: "#a9dc76",
268
+ 3: "#66d9ef",
269
+ 4: "#fcfcfa"
270
+ },
271
+ keyword: {
272
+ 1: "#ff6188",
273
+ 2: "#ae81ff",
274
+ 3: "#66d9ef"
275
+ },
276
+ number: "#ae81ff",
277
+ operator: "#ff6188",
278
+ punctuation: "#727072",
279
+ string: "#ffd866"
280
+ }
284
281
  }
285
282
  }
286
283
  };
287
284
  // src/nnn/nanolightTs.ts
288
285
  var COMMENT = "comment";
289
- var IDENTIFIER_1 = "identifier-1";
290
- var IDENTIFIER_2 = "identifier-2";
291
- var IDENTIFIER_3 = "identifier-3";
292
- var IDENTIFIER_4 = "identifier-4";
293
- var KEYWORD_1 = "keyword-1";
294
- var KEYWORD_2 = "keyword-2";
295
- var KEYWORD_3 = "keyword-3";
286
+ var IDENTIFIER1 = "identifier1";
287
+ var IDENTIFIER2 = "identifier2";
288
+ var IDENTIFIER3 = "identifier3";
289
+ var IDENTIFIER4 = "identifier4";
290
+ var KEYWORD1 = "keyword1";
291
+ var KEYWORD2 = "keyword2";
292
+ var KEYWORD3 = "keyword3";
296
293
  var NUMBER = "number";
297
294
  var OPERATOR = "operator";
298
295
  var PUNCTUATION = "punctuation";
299
296
  var STRING = "string";
300
- var nanolightTs = /* @__PURE__ */ createTokenizer((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]);
297
+ var nanolightTs = /* @__PURE__ */ createTokenizer((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, "}"], [KEYWORD1, "as"], [KEYWORD1, "async"], [KEYWORD1, "await"], [KEYWORD1, "break"], [KEYWORD1, "case"], [KEYWORD1, "catch"], [KEYWORD1, "class"], [KEYWORD1, "const"], [KEYWORD1, "continue"], [KEYWORD1, "debugger"], [KEYWORD1, "default"], [KEYWORD1, "delete"], [KEYWORD1, "do"], [KEYWORD1, "else"], [KEYWORD1, "export"], [KEYWORD1, "extends"], [KEYWORD1, "finally"], [KEYWORD1, "for"], [KEYWORD1, "from"], [KEYWORD1, "function"], [KEYWORD1, "function*"], [KEYWORD1, "goto"], [KEYWORD1, "if"], [KEYWORD1, "import"], [KEYWORD1, "in"], [KEYWORD1, "instanceof"], [KEYWORD1, "is"], [KEYWORD1, "keyof"], [KEYWORD1, "let"], [KEYWORD1, "new"], [KEYWORD1, "of"], [KEYWORD1, "package"], [KEYWORD1, "return"], [KEYWORD1, "super"], [KEYWORD1, "switch"], [KEYWORD1, "this"], [KEYWORD1, "throw"], [KEYWORD1, "try"], [KEYWORD1, "type"], [KEYWORD1, "typeof"], [KEYWORD1, "var"], [KEYWORD1, "void"], [KEYWORD1, "while"], [KEYWORD1, "with"], [KEYWORD1, "yield"], [KEYWORD1, "yield*"], [KEYWORD2, "false"], [KEYWORD2, "Infinity"], [KEYWORD2, "NaN"], [KEYWORD2, "null"], [KEYWORD2, "true"], [KEYWORD2, "undefined"], [KEYWORD3, "any"], [KEYWORD3, "bigint"], [KEYWORD3, "boolean"], [KEYWORD3, "eval"], [KEYWORD3, "number"], [KEYWORD3, "string"], [KEYWORD3, "symbol"], [KEYWORD3, "unknown"], [IDENTIFIER1, /[\p{Lu}_$][\p{Lu}\d_$]*/u], [IDENTIFIER2, /[\p{L}_$][\p{L}\d_$]*(?=[(`])/u], [IDENTIFIER3, /\p{Lu}[\p{L}\d_$]*/u], [IDENTIFIER4, /[\p{L}_$][\p{L}\d_$]*/u]);
301
298
  // src/nnn/omit.ts
302
299
  var omit = (ref, keys) => Object.fromEntries(Object.entries(ref).filter(([key]) => !keys.includes(key)));
303
300
  // src/nnn/pick.ts
@@ -368,7 +365,6 @@ export {
368
365
  isNumber,
369
366
  isRecord,
370
367
  isString,
371
- jsOnParse,
372
368
  monokai,
373
369
  nanolightTs,
374
370
  omit,
package/package.json CHANGED
@@ -20,8 +20,6 @@
20
20
  "isRecord",
21
21
  "isString",
22
22
  "JS-to-CSS",
23
- "jsOnParse",
24
- "JSON",
25
23
  "Monokai",
26
24
  "nanolight",
27
25
  "nnn",
@@ -43,5 +41,5 @@
43
41
  "name": "@jackens/nnn",
44
42
  "type": "module",
45
43
  "types": "nnn.d.ts",
46
- "version": "2026.8.31"
44
+ "version": "2026.9.1"
47
45
  }
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # nnn
2
2
 
3
- A collection of Jackens’ JavaScript helper utilities (version: `2026.8.31`).
3
+ A collection of Jackens’ JavaScript helper utilities (version: `2026.9.1`).
4
4
 
5
5
  ## Installation
6
6
 
@@ -18,8 +18,6 @@ npm i @jackens/nnn
18
18
 
19
19
  ```js
20
20
  import {
21
- c,
22
- createEscape,
23
21
  createTokenizer,
24
22
  csvParse,
25
23
  fixPlTypography,
@@ -46,23 +44,12 @@ import {
46
44
 
47
45
  ## Exports
48
46
 
49
- - [`CNode`](#CNode):
50
- Represents a CSS rule node for the [`c`](#c) helper.
51
- - [`CRoot`](#CRoot):
52
- Represents the root CSS object for the [`c`](#c) helper.
53
47
  - [`HArgs`](#HArgs):
54
48
  Tuple argument type for the [`h`](#h) and [`s`](#s)
55
49
  helpers.
56
50
  - [`HArgs1`](#HArgs1):
57
51
  Single argument type for the [`h`](#h) and [`s`](#s)
58
52
  helpers.
59
- - [`c`](#c):
60
- A minimal CSS-in-JS helper
61
- that converts a JavaScript object hierarchy
62
- into a CSS string.
63
- - [`createEscape`](#createEscape):
64
- Creates a tag function
65
- for escaping interpolated values in template literals.
66
53
  - [`createTokenizer`](#createTokenizer):
67
54
  A helper for building simple tokenizers
68
55
  (see also [`nanolightTs`](#nanolightTs)).
@@ -99,7 +86,7 @@ import {
99
86
  handler-based value transformation (“JavaScript ON”).
100
87
  - [`monokai`](#monokai):
101
88
  A Monokai-inspired color scheme
102
- for use with the [`c`](#c) helper
89
+ for use with the {@link c} helper
103
90
  and [`nanolightTs`](#nanolightTs) tokenizer.
104
91
  - [`nanolightTs`](#nanolightTs):
105
92
  A TypeScript/JavaScript syntax highlighting tokenizer
@@ -126,26 +113,6 @@ import {
126
113
  A Proxy-based helper for auto-vivification
127
114
  of nested object structures.
128
115
 
129
- ### CNode
130
-
131
- ```ts
132
- type CNode = {
133
- [attributeOrSelector: string]: string | number | CNode | undefined;
134
- };
135
- ```
136
-
137
- Represents a CSS rule node for the [`c`](#c) helper.
138
- Keys are CSS properties or nested selectors.
139
-
140
- ### CRoot
141
-
142
- ```ts
143
- type CRoot = Record<PropertyKey, CNode>;
144
- ```
145
-
146
- Represents the root CSS object for the [`c`](#c) helper.
147
- Keys are top-level selectors or at-rules.
148
-
149
116
  ### HArgs
150
117
 
151
118
  ```ts
@@ -164,303 +131,6 @@ type HArgs1 = Record<PropertyKey, unknown> | null | undefined | Node | string |
164
131
  Single argument type for the [`h`](#h) and [`s`](#s)
165
132
  helpers.
166
133
 
167
- ### c
168
-
169
- ```ts
170
- const c: (root: CRoot, splitter?: string) => string;
171
- ```
172
-
173
- A minimal CSS-in-JS helper
174
- that converts a JavaScript object hierarchy
175
- into a CSS string.
176
-
177
- #### root
178
-
179
- An object describing CSS rules.
180
- Keys are selectors or at-rules;
181
- values are either CSS property values
182
- or nested rule objects.
183
-
184
- #### splitter
185
-
186
- A delimiter used to create unique keys (default: `'$$'`).
187
- The substring from `splitter` to the end of a key
188
- is ignored (e.g., `src$$1` → `src`).
189
-
190
- #### Returns
191
-
192
- A CSS string representing the compiled rules.
193
-
194
- #### Remarks
195
-
196
- - Keys whose values are primitives (`string` | `number`)
197
- are treated as CSS properties.
198
- - In property keys, uppercase letters
199
- become lowercase with a `-` prefix
200
- (e.g., `fontFamily` → `font-family`);
201
- underscores become hyphens
202
- (e.g., `font_family` → `font-family`).
203
- - Comma-separated selector keys
204
- expand into multiple selectors
205
- (e.g., `{ div: { '.a,.b': { margin: 1 } } }` →
206
- `div.a,div.b{margin:1}`).
207
- - Top-level keys starting with `@` (at-rules)
208
- are not concatenated with child selectors.
209
-
210
- #### Usage Examples
211
-
212
- ```ts
213
- const actual1 = c({
214
- a: {
215
- color: 'red',
216
- margin: 1,
217
- '.c': { margin: 2, padding: 2 },
218
- padding: 1,
219
- },
220
- })
221
-
222
- const expected1 = `
223
- a{
224
- color:red;
225
- margin:1
226
- }
227
- a.c{
228
- margin:2;
229
- padding:2
230
- }
231
- a{
232
- padding:1
233
- }`.replace(/\n\s*/g, '')
234
-
235
- expect(actual1).to.equal(expected1)
236
-
237
- const actual2 = c({
238
- a: {
239
- '.b': {
240
- color: 'red',
241
- margin: 1,
242
- '.c': { margin: 2, padding: 2 },
243
- padding: 1,
244
- },
245
- },
246
- })
247
-
248
- const expected2 = `
249
- a.b{
250
- color:red;
251
- margin:1
252
- }
253
- a.b.c{
254
- margin:2;
255
- padding:2
256
- }
257
- a.b{
258
- padding:1
259
- }`.replace(/\n\s*/g, '')
260
-
261
- expect(actual2).to.equal(expected2)
262
-
263
- const actual3 = c({
264
- '@font-face$$1': {
265
- fontFamily: 'Jackens',
266
- src$$1: 'url(otf/jackens.otf)',
267
- src$$2:
268
- "url(otf/jackens.otf) format('opentype')," +
269
- "url(svg/jackens.svg) format('svg')",
270
- font_weight: 'normal',
271
- 'font-style': 'normal',
272
- },
273
- '@font-face$$2': {
274
- font_family: 'C64',
275
- src: 'url(fonts/C64_Pro_Mono-STYLE.woff)',
276
- },
277
- '@keyframes spin': {
278
- '0%': { transform: 'rotate(0deg)' },
279
- '100%': { transform: 'rotate(360deg)' },
280
- },
281
- div: {
282
- border: 'solid red 1px',
283
- '.c1': { 'background-color': '#000' },
284
- ' .c1': { background_color: 'black' },
285
- '.c2': { backgroundColor: 'rgb(0,0,0)' },
286
- },
287
- '@media(min-width:200px)': {
288
- div: { margin: 0, padding: 0 },
289
- span: { color: '#000' },
290
- },
291
- })
292
-
293
- const expected3 = `
294
- @font-face{
295
- font-family:Jackens;
296
- src:url(otf/jackens.otf);
297
- src:url(otf/jackens.otf) format('opentype'),url(svg/jackens.svg) format('svg');
298
- font-weight:normal;
299
- font-style:normal
300
- }
301
- @font-face{
302
- font-family:C64;
303
- src:url(fonts/C64_Pro_Mono-STYLE.woff)
304
- }
305
- @keyframes spin{
306
- 0%{
307
- transform:rotate(0deg)
308
- }
309
- 100%{
310
- transform:rotate(360deg)
311
- }
312
- }
313
- div{
314
- border:solid red 1px
315
- }
316
- div.c1{
317
- background-color:#000
318
- }
319
- div .c1{
320
- background-color:black
321
- }
322
- div.c2{
323
- background-color:rgb(0,0,0)
324
- }
325
- @media(min-width:200px){
326
- div{
327
- margin:0;
328
- padding:0
329
- }
330
- span{
331
- color:#000
332
- }
333
- }`.replace(/\n\s*/g, '')
334
-
335
- expect(actual3).to.equal(expected3)
336
-
337
- const actual4 = c({
338
- a: {
339
- '.b,.c': {
340
- margin: 1,
341
- '.d': {
342
- margin: 2,
343
- },
344
- },
345
- },
346
- })
347
-
348
- const expected4 = `
349
- a.b,a.c{
350
- margin:1
351
- }
352
- a.b.d,a.c.d{
353
- margin:2
354
- }`.replace(/\n\s*/g, '')
355
-
356
- expect(actual4).to.equal(expected4)
357
-
358
- const actual5 = c({
359
- '.b,.c': {
360
- margin: 1,
361
- '.d': {
362
- margin: 2,
363
- },
364
- },
365
- })
366
-
367
- const expected5 = `
368
- .b,.c{
369
- margin:1
370
- }
371
- .b.d,.c.d{
372
- margin:2
373
- }`.replace(/\n\s*/g, '')
374
-
375
- expect(actual5).to.equal(expected5)
376
-
377
- const actual6 = c({
378
- '.a,.b': {
379
- margin: 1,
380
- '.c,.d': {
381
- margin: 2,
382
- },
383
- },
384
- })
385
-
386
- const expected6 = `
387
- .a,.b{
388
- margin:1
389
- }
390
- .a.c,.a.d,.b.c,.b.d{
391
- margin:2
392
- }`.replace(/\n\s*/g, '')
393
-
394
- expect(actual6).to.equal(expected6)
395
- ```
396
-
397
- ### createEscape
398
-
399
- ```ts
400
- const createEscape: (escapeFn: (value: any) => string) => (template: TemplateStringsArray, ...values: unknown[]) => string;
401
- ```
402
-
403
- Creates a tag function
404
- for escaping interpolated values in template literals.
405
-
406
- #### escapeFn
407
-
408
- A function that takes a value
409
- and returns its escaped string representation.
410
-
411
- #### Returns
412
-
413
- A tag function that escapes interpolated values
414
- using the provided escape map.
415
-
416
- #### Usage Examples
417
-
418
- ```ts
419
- const escapeFn = (value: any): string =>
420
- isArray(value)
421
- ? value.map(escapeFn).join(', ')
422
- : value === true || value === false
423
- ? `b'${+value}'`
424
- : value instanceof Date
425
- ? `'${value
426
- .toISOString()
427
- .replace(/^(.+)T(.+)\..*$/, '$1 $2')}'`
428
- : isFiniteNumber(value)
429
- ? `${value}`
430
- : isString(value)
431
- ? `'${value.replace(/'/g, "''")}'`
432
- : 'NULL'
433
-
434
- const sql = createEscape(escapeFn)
435
-
436
- const actual = sql`
437
- SELECT *
438
- FROM table_name
439
- WHERE column_name IN (
440
- ${[
441
- true,
442
- null,
443
- undefined,
444
- NaN,
445
- Infinity,
446
- 42,
447
- '42',
448
- "4'2",
449
- /42/,
450
- new Date(323325000000),
451
- ]}
452
- )`
453
-
454
- const expected = `
455
- SELECT *
456
- FROM table_name
457
- WHERE column_name IN (
458
- b'1', NULL, NULL, NULL, NULL, 42, '42', '4''2', NULL, '1980-03-31 04:30:00'
459
- )`
460
-
461
- expect(actual).to.equal(expected)
462
- ```
463
-
464
134
  ### createTokenizer
465
135
 
466
136
  ```ts
@@ -483,9 +153,9 @@ For unmatched text between patterns,
483
153
 
484
154
  An array of tuples `[metadata, pattern]` where:
485
155
  - `metadata`: arbitrary data (e.g., a CSS class name)
486
- passed to `decorator` when the pattern matches.
156
+ passed to `decorator` when the pattern matches.
487
157
  - `pattern`: a `string` or `RegExp`
488
- to match against the input.
158
+ to match against the input.
489
159
 
490
160
  #### Returns
491
161
 
@@ -495,11 +165,11 @@ and returns an array of decorated tokens.
495
165
  #### Remarks
496
166
 
497
167
  1. Matches starting at an earlier position
498
- take precedence.
168
+ take precedence.
499
169
  2. Among matches at the same position,
500
- the longer one wins.
170
+ the longer one wins.
501
171
  3. Among matches of the same position and length,
502
- the one defined earlier wins.
172
+ the one defined earlier wins.
503
173
 
504
174
  #### Usage Examples
505
175
 
@@ -725,14 +395,14 @@ If a `Node`, that node is modified in place.
725
395
 
726
396
  Additional arguments processed as follows:
727
397
  - `Object`: maps attributes/properties.
728
- Keys starting with `$` set element properties
729
- (without the `$` prefix);
730
- other keys set attributes via `setAttribute`.
731
- A value of `false` removes the attribute.
398
+ Keys starting with `$` set element properties
399
+ (without the `$` prefix);
400
+ other keys set attributes via `setAttribute`.
401
+ A value of `false` removes the attribute.
732
402
  - `null`/`undefined`: ignored.
733
403
  - `Node`: appended as a child.
734
404
  - `string`/`number`: converted to a `Text` node
735
- and appended.
405
+ and appended.
736
406
  - [`HArgs`](#HArgs) array: processed recursively.
737
407
 
738
408
  #### Returns
@@ -1137,93 +807,14 @@ The JSON string to parse.
1137
807
 
1138
808
  The parsed value with handler substitutions applied.
1139
809
 
1140
- #### Usage Examples
1141
-
1142
- ```ts
1143
- const handlers = {
1144
- $add: (a: number, b: number) => a + b,
1145
- $hello: (name: string) => `Hello ${name}!`,
1146
- $foo: () => 'bar',
1147
- }
1148
-
1149
- const actual1 = jsOnParse(
1150
- handlers,
1151
- `[
1152
- {
1153
- "$add": [1, 2]
1154
- },
1155
- {
1156
- "$hello": ["World"]
1157
- },
1158
- {
1159
- "nested": {
1160
- "$hello": ["nested World"]
1161
- },
1162
- "one": 1,
1163
- "two": 2
1164
- },
1165
- {
1166
- "$foo": []
1167
- },
1168
- {
1169
- "$foo": ["The parent object does not have exactly one property!"],
1170
- "one": 1,
1171
- "two": 2
1172
- }
1173
- ]`
1174
- )
1175
-
1176
- expect(actual1).to.deep.equal([
1177
- 3,
1178
- 'Hello World!',
1179
- {
1180
- nested: 'Hello nested World!',
1181
- one: 1,
1182
- two: 2,
1183
- },
1184
- 'bar',
1185
- {
1186
- $foo: [
1187
- 'The parent object does not have ' +
1188
- 'exactly one property!',
1189
- ],
1190
- one: 1,
1191
- two: 2,
1192
- },
1193
- ])
1194
-
1195
- const actual2 = jsOnParse(
1196
- { $notFunc: 'handler not being a function' } as any,
1197
- '{"$notFunc": [1, 2]}'
1198
- )
1199
-
1200
- expect(actual2).to.deep.equal({ $notFunc: [1, 2] })
1201
-
1202
- const actual3 = jsOnParse(
1203
- handlers,
1204
- '{"$unknown_handler_key": [1, 2]}'
1205
- )
1206
-
1207
- expect(actual3).to.deep.equal({
1208
- $unknown_handler_key: [1, 2],
1209
- })
1210
-
1211
- const actual4 = jsOnParse(
1212
- handlers,
1213
- '{"$add": {"not": "array"}}'
1214
- )
1215
-
1216
- expect(actual4).to.deep.equal({ $add: { not: 'array' } })
1217
- ```
1218
-
1219
810
  ### monokai
1220
811
 
1221
812
  ```ts
1222
- const monokai: CRoot;
813
+ const monokai: CNode;
1223
814
  ```
1224
815
 
1225
816
  A Monokai-inspired color scheme
1226
- for use with the [`c`](#c) helper
817
+ for use with the {@link c} helper
1227
818
  and [`nanolightTs`](#nanolightTs) tokenizer.
1228
819
 
1229
820
  ### nanolightTs
@@ -1252,11 +843,11 @@ const codeJs =
1252
843
  "{ 42: 42 }['42'] /* 42 */"
1253
844
 
1254
845
  expect(nanolightTs(codeJs)).to.deep.equal([
1255
- ['span', { class: 'keyword-1' }, 'const'],
846
+ ['span', { class: 'keyword1' }, 'const'],
1256
847
  ' ',
1257
848
  [
1258
849
  'span',
1259
- { class: 'identifier-4' },
850
+ { class: 'identifier4' },
1260
851
  'answerToLifeTheUniverseAndEverything',
1261
852
  ],
1262
853
  ' ',
@@ -1350,7 +941,7 @@ expect(pick(ref, ['a', 'b'])).to.deep.equal({
1350
941
  ### rwd
1351
942
 
1352
943
  ```ts
1353
- const rwd: (root: CRoot, selector: string, cellWidthPx: number, cellHeightPx: number, ...specs: [number, number?, number?][]) => void;
944
+ const rwd: (root: CNode, selector: string, cellWidthPx: number, cellHeightPx: number, ...specs: [number, number?, number?][]) => void;
1354
945
  ```
1355
946
 
1356
947
  A responsive web design helper
@@ -1358,7 +949,7 @@ that generates CSS rules for a grid-like layout.
1358
949
 
1359
950
  #### root
1360
951
 
1361
- The CSS root object to populate (see [`c`](#c)).
952
+ The CSS root object to populate (see {@link c}).
1362
953
 
1363
954
  #### selector
1364
955
 
@@ -1376,16 +967,16 @@ The base cell height in pixels.
1376
967
 
1377
968
  An array of breakpoint specifications, each a tuple of:
1378
969
  - `maxWidth`: maximum number of cells per row
1379
- (defines the viewport breakpoint).
970
+ (defines the viewport breakpoint).
1380
971
  - `width` (optional, default `1`):
1381
- number of horizontal cells the element spans.
972
+ number of horizontal cells the element spans.
1382
973
  - `height` (optional, default `1`):
1383
- number of vertical cells the element spans.
974
+ number of vertical cells the element spans.
1384
975
 
1385
976
  #### Usage Examples
1386
977
 
1387
978
  ```ts
1388
- const style: CRoot = {
979
+ const style: CNode = {
1389
980
  body: {
1390
981
  margin: 0,
1391
982
  },
@@ -1453,14 +1044,14 @@ If a `Node`, that node is modified in place.
1453
1044
 
1454
1045
  Additional arguments processed as follows:
1455
1046
  - `Object`: maps attributes/properties.
1456
- Keys starting with `$` set element properties
1457
- (without the `$` prefix);
1458
- other keys set attributes via `setAttributeNS`.
1459
- A value of `false` removes the attribute.
1047
+ Keys starting with `$` set element properties
1048
+ (without the `$` prefix);
1049
+ other keys set attributes via `setAttributeNS`.
1050
+ A value of `false` removes the attribute.
1460
1051
  - `null`/`undefined`: ignored.
1461
1052
  - `Node`: appended as a child.
1462
1053
  - `string`/`number`: converted to a `Text` node
1463
- and appended.
1054
+ and appended.
1464
1055
  - [`HArgs`](#HArgs) array: processed recursively.
1465
1056
 
1466
1057
  #### Returns