@jackens/nnn 2026.8.24 → 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 -105
  2. package/nnn.js +105 -110
  3. package/package.json +1 -4
  4. package/readme.md +27 -514
package/nnn.d.ts CHANGED
@@ -1,106 +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
- /**
68
- * Creates a function that
69
- * returns the appropriate noun form
70
- * based on a numeric value using `Intl.PluralRules`.
71
- *
72
- * Different languages have different plural rules.
73
- * The `Intl.PluralRules` API provides locale-aware
74
- * plural category selection.
75
- * Possible categories are:
76
- *
77
- * - `zero`: for zero items
78
- * (used in some languages like Arabic, Latvian)
79
- * - `one`: for singular (e.g., 1 item)
80
- * - `two`: for dual
81
- * (used in some languages like Arabic, Hebrew)
82
- * - `few`: for small plurals (e.g., 2-4 in Polish)
83
- * - `many`: for larger plurals (e.g., 5-21 in Polish)
84
- * - `other`: fallback category (used by all languages)
85
- *
86
- * @param locale
87
- *
88
- * A BCP 47 language tag (e.g., `pl`, `en`).
89
- *
90
- * @param forms
91
- *
92
- * An object mapping plural categories to noun forms.
93
- * Not all categories need to be provided;
94
- * if a category is missing,
95
- * the function falls back to `other`,
96
- * then to an empty string.
97
- *
98
- * @returns
99
- *
100
- * A function that takes a numeric value
101
- * and returns the appropriate noun form.
102
- */
103
- export declare const createPluralRules: (locale: string, forms: Partial<Record<Intl.LDMLPluralRule, string>>) => (value: number) => string;
104
1
  /**
105
2
  * A helper for building simple tokenizers
106
3
  * (see also {@link nanolightTs}).
@@ -400,7 +297,7 @@ export declare const jsOnParse: (handlers: Record<PropertyKey, Function>, text:
400
297
  * for use with the {@link c} helper
401
298
  * and {@link nanolightTs} tokenizer.
402
299
  */
403
- export declare const monokai: CRoot;
300
+ export declare const monokai: CNode;
404
301
  /**
405
302
  * A TypeScript/JavaScript syntax highlighting tokenizer
406
303
  * built using {@link createTokenizer}.
@@ -487,7 +384,7 @@ export declare const pick: <T, K extends keyof T>(ref: T, keys: unknown[]) => Pi
487
384
  * - `height` (optional, default `1`):
488
385
  * number of vertical cells the element spans.
489
386
  */
490
- 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;
491
388
  /**
492
389
  * Generates a UUID v1 (time-based) identifier.
493
390
  *
package/nnn.js CHANGED
@@ -6,59 +6,59 @@ var isString = (arg) => typeof arg === "string";
6
6
  var isRecord = (arg) => typeof arg === "object" && arg != null && !isArray(arg);
7
7
 
8
8
  // src/nnn/c.ts
9
- var _c = (node, prefix, result, splitter) => {
10
- const queue = [[node, prefix]];
11
- while (queue.length > 0) {
12
- const [style0, prefix0] = queue.shift();
13
- if (isArray(style0)) {
14
- result.push(prefix0, prefix0 !== "" ? "{" : "", style0.join(";"), prefix0 !== "" ? "}" : "");
15
- } else {
16
- const todo = [];
17
- let attributes = [];
18
- let attributesPushed = false;
19
- for (const key in style0) {
20
- const value = style0[key];
21
- if (isString(value) || isNumber(value)) {
22
- if (!attributesPushed) {
23
- attributesPushed = true;
24
- attributes = [];
25
- todo.push([attributes, prefix0]);
26
- }
27
- const attribute = key.split(splitter)[0].replace(/_/g, "-").replace(/([A-Z])/g, (_, letter) => "-" + letter.toLowerCase());
28
- attributes.push(`${attribute}:${value}`);
29
- } else if (value != null) {
30
- attributesPushed = false;
31
- const prefixN = [];
32
- const keyChunks = key.split(",");
33
- prefix0.split(",").forEach((prefixChunk) => keyChunks.forEach((keyChunk) => prefixN.push(prefixChunk + keyChunk)));
34
- todo.push([value, prefixN.join()]);
35
- }
36
- }
37
- queue.unshift(...todo);
9
+ var DEFAULT_SPLITTER = "$$";
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);
38
17
  }
39
18
  }
19
+ return combined.join(",");
40
20
  };
41
- var c = (root, splitter = "$$") => {
42
- const chunks = [];
43
- for (const key in root) {
44
- const value = root[key];
45
- if (value != null) {
46
- if (key[0] === "@") {
47
- chunks.push(key.split(splitter)[0] + "{");
48
- _c(value, "", chunks, splitter);
49
- 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}`);
50
40
  } else {
51
- _c(value, key.split(splitter)[0], chunks, splitter);
41
+ buffer.push(`${name}:${value}`);
52
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));
53
54
  }
54
55
  }
55
- return chunks.join("");
56
+ flush();
57
+ return out.join(flatten ? ";" : "");
56
58
  };
59
+ var c = (node, splitter = DEFAULT_SPLITTER) => _c(node, splitter);
57
60
  // src/nnn/createEscape.ts
58
61
  var createEscape = (escapeFn) => (template, ...values) => String.raw(template, ...values.map(escapeFn));
59
- // src/nnn/createPluralRules.ts
60
- var PLURAL_RULES = {};
61
- var createPluralRules = (locale, forms) => (value) => forms[(PLURAL_RULES[locale] ??= new Intl.PluralRules(locale)).select(value)] ?? forms.other ?? "";
62
62
  // src/nnn/createTokenizer.ts
63
63
  var createTokenizer = (decorator, ...specs) => (code) => {
64
64
  const result = [];
@@ -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
@@ -358,7 +355,6 @@ var vivify = (ref) => _vivify({ _: ref }, "_");
358
355
  export {
359
356
  c,
360
357
  createEscape,
361
- createPluralRules as createNounForm,
362
358
  createTokenizer,
363
359
  csvParse,
364
360
  fixPlTypography,
@@ -369,7 +365,6 @@ export {
369
365
  isNumber,
370
366
  isRecord,
371
367
  isString,
372
- jsOnParse,
373
368
  monokai,
374
369
  nanolightTs,
375
370
  omit,
package/package.json CHANGED
@@ -20,12 +20,9 @@
20
20
  "isRecord",
21
21
  "isString",
22
22
  "JS-to-CSS",
23
- "jsOnParse",
24
- "JSON",
25
23
  "Monokai",
26
24
  "nanolight",
27
25
  "nnn",
28
- "noun form",
29
26
  "omit",
30
27
  "pick",
31
28
  "Responsive Web Design",
@@ -44,5 +41,5 @@
44
41
  "name": "@jackens/nnn",
45
42
  "type": "module",
46
43
  "types": "nnn.d.ts",
47
- "version": "2026.8.24"
44
+ "version": "2026.9.1"
48
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.24`).
3
+ A collection of Jackens’ JavaScript helper utilities (version: `2026.9.1`).
4
4
 
5
5
  ## Installation
6
6
 
@@ -18,9 +18,6 @@ npm i @jackens/nnn
18
18
 
19
19
  ```js
20
20
  import {
21
- c,
22
- createEscape,
23
- createPluralRules,
24
21
  createTokenizer,
25
22
  csvParse,
26
23
  fixPlTypography,
@@ -47,27 +44,12 @@ import {
47
44
 
48
45
  ## Exports
49
46
 
50
- - [`CNode`](#CNode):
51
- Represents a CSS rule node for the [`c`](#c) helper.
52
- - [`CRoot`](#CRoot):
53
- Represents the root CSS object for the [`c`](#c) helper.
54
47
  - [`HArgs`](#HArgs):
55
48
  Tuple argument type for the [`h`](#h) and [`s`](#s)
56
49
  helpers.
57
50
  - [`HArgs1`](#HArgs1):
58
51
  Single argument type for the [`h`](#h) and [`s`](#s)
59
52
  helpers.
60
- - [`c`](#c):
61
- A minimal CSS-in-JS helper
62
- that converts a JavaScript object hierarchy
63
- into a CSS string.
64
- - [`createEscape`](#createEscape):
65
- Creates a tag function
66
- for escaping interpolated values in template literals.
67
- - [`createPluralRules`](#createPluralRules):
68
- Creates a function that
69
- returns the appropriate noun form
70
- based on a numeric value using `Intl.PluralRules`.
71
53
  - [`createTokenizer`](#createTokenizer):
72
54
  A helper for building simple tokenizers
73
55
  (see also [`nanolightTs`](#nanolightTs)).
@@ -104,7 +86,7 @@ import {
104
86
  handler-based value transformation (“JavaScript ON”).
105
87
  - [`monokai`](#monokai):
106
88
  A Monokai-inspired color scheme
107
- for use with the [`c`](#c) helper
89
+ for use with the {@link c} helper
108
90
  and [`nanolightTs`](#nanolightTs) tokenizer.
109
91
  - [`nanolightTs`](#nanolightTs):
110
92
  A TypeScript/JavaScript syntax highlighting tokenizer
@@ -131,26 +113,6 @@ import {
131
113
  A Proxy-based helper for auto-vivification
132
114
  of nested object structures.
133
115
 
134
- ### CNode
135
-
136
- ```ts
137
- type CNode = {
138
- [attributeOrSelector: string]: string | number | CNode | undefined;
139
- };
140
- ```
141
-
142
- Represents a CSS rule node for the [`c`](#c) helper.
143
- Keys are CSS properties or nested selectors.
144
-
145
- ### CRoot
146
-
147
- ```ts
148
- type CRoot = Record<PropertyKey, CNode>;
149
- ```
150
-
151
- Represents the root CSS object for the [`c`](#c) helper.
152
- Keys are top-level selectors or at-rules.
153
-
154
116
  ### HArgs
155
117
 
156
118
  ```ts
@@ -169,376 +131,6 @@ type HArgs1 = Record<PropertyKey, unknown> | null | undefined | Node | string |
169
131
  Single argument type for the [`h`](#h) and [`s`](#s)
170
132
  helpers.
171
133
 
172
- ### c
173
-
174
- ```ts
175
- const c: (root: CRoot, splitter?: string) => string;
176
- ```
177
-
178
- A minimal CSS-in-JS helper
179
- that converts a JavaScript object hierarchy
180
- into a CSS string.
181
-
182
- #### root
183
-
184
- An object describing CSS rules.
185
- Keys are selectors or at-rules;
186
- values are either CSS property values
187
- or nested rule objects.
188
-
189
- #### splitter
190
-
191
- A delimiter used to create unique keys (default: `'$$'`).
192
- The substring from `splitter` to the end of a key
193
- is ignored (e.g., `src$$1` → `src`).
194
-
195
- #### Returns
196
-
197
- A CSS string representing the compiled rules.
198
-
199
- #### Remarks
200
-
201
- - Keys whose values are primitives (`string` | `number`)
202
- are treated as CSS properties.
203
- - In property keys, uppercase letters
204
- become lowercase with a `-` prefix
205
- (e.g., `fontFamily` → `font-family`);
206
- underscores become hyphens
207
- (e.g., `font_family` → `font-family`).
208
- - Comma-separated selector keys
209
- expand into multiple selectors
210
- (e.g., `{ div: { '.a,.b': { margin: 1 } } }` →
211
- `div.a,div.b{margin:1}`).
212
- - Top-level keys starting with `@` (at-rules)
213
- are not concatenated with child selectors.
214
-
215
- #### Usage Examples
216
-
217
- ```ts
218
- const actual1 = c({
219
- a: {
220
- color: 'red',
221
- margin: 1,
222
- '.c': { margin: 2, padding: 2 },
223
- padding: 1,
224
- },
225
- })
226
-
227
- const expected1 = `
228
- a{
229
- color:red;
230
- margin:1
231
- }
232
- a.c{
233
- margin:2;
234
- padding:2
235
- }
236
- a{
237
- padding:1
238
- }`.replace(/\n\s*/g, '')
239
-
240
- expect(actual1).to.equal(expected1)
241
-
242
- const actual2 = c({
243
- a: {
244
- '.b': {
245
- color: 'red',
246
- margin: 1,
247
- '.c': { margin: 2, padding: 2 },
248
- padding: 1,
249
- },
250
- },
251
- })
252
-
253
- const expected2 = `
254
- a.b{
255
- color:red;
256
- margin:1
257
- }
258
- a.b.c{
259
- margin:2;
260
- padding:2
261
- }
262
- a.b{
263
- padding:1
264
- }`.replace(/\n\s*/g, '')
265
-
266
- expect(actual2).to.equal(expected2)
267
-
268
- const actual3 = c({
269
- '@font-face$$1': {
270
- fontFamily: 'Jackens',
271
- src$$1: 'url(otf/jackens.otf)',
272
- src$$2:
273
- "url(otf/jackens.otf) format('opentype')," +
274
- "url(svg/jackens.svg) format('svg')",
275
- font_weight: 'normal',
276
- 'font-style': 'normal',
277
- },
278
- '@font-face$$2': {
279
- font_family: 'C64',
280
- src: 'url(fonts/C64_Pro_Mono-STYLE.woff)',
281
- },
282
- '@keyframes spin': {
283
- '0%': { transform: 'rotate(0deg)' },
284
- '100%': { transform: 'rotate(360deg)' },
285
- },
286
- div: {
287
- border: 'solid red 1px',
288
- '.c1': { 'background-color': '#000' },
289
- ' .c1': { background_color: 'black' },
290
- '.c2': { backgroundColor: 'rgb(0,0,0)' },
291
- },
292
- '@media(min-width:200px)': {
293
- div: { margin: 0, padding: 0 },
294
- span: { color: '#000' },
295
- },
296
- })
297
-
298
- const expected3 = `
299
- @font-face{
300
- font-family:Jackens;
301
- src:url(otf/jackens.otf);
302
- src:url(otf/jackens.otf) format('opentype'),url(svg/jackens.svg) format('svg');
303
- font-weight:normal;
304
- font-style:normal
305
- }
306
- @font-face{
307
- font-family:C64;
308
- src:url(fonts/C64_Pro_Mono-STYLE.woff)
309
- }
310
- @keyframes spin{
311
- 0%{
312
- transform:rotate(0deg)
313
- }
314
- 100%{
315
- transform:rotate(360deg)
316
- }
317
- }
318
- div{
319
- border:solid red 1px
320
- }
321
- div.c1{
322
- background-color:#000
323
- }
324
- div .c1{
325
- background-color:black
326
- }
327
- div.c2{
328
- background-color:rgb(0,0,0)
329
- }
330
- @media(min-width:200px){
331
- div{
332
- margin:0;
333
- padding:0
334
- }
335
- span{
336
- color:#000
337
- }
338
- }`.replace(/\n\s*/g, '')
339
-
340
- expect(actual3).to.equal(expected3)
341
-
342
- const actual4 = c({
343
- a: {
344
- '.b,.c': {
345
- margin: 1,
346
- '.d': {
347
- margin: 2,
348
- },
349
- },
350
- },
351
- })
352
-
353
- const expected4 = `
354
- a.b,a.c{
355
- margin:1
356
- }
357
- a.b.d,a.c.d{
358
- margin:2
359
- }`.replace(/\n\s*/g, '')
360
-
361
- expect(actual4).to.equal(expected4)
362
-
363
- const actual5 = c({
364
- '.b,.c': {
365
- margin: 1,
366
- '.d': {
367
- margin: 2,
368
- },
369
- },
370
- })
371
-
372
- const expected5 = `
373
- .b,.c{
374
- margin:1
375
- }
376
- .b.d,.c.d{
377
- margin:2
378
- }`.replace(/\n\s*/g, '')
379
-
380
- expect(actual5).to.equal(expected5)
381
-
382
- const actual6 = c({
383
- '.a,.b': {
384
- margin: 1,
385
- '.c,.d': {
386
- margin: 2,
387
- },
388
- },
389
- })
390
-
391
- const expected6 = `
392
- .a,.b{
393
- margin:1
394
- }
395
- .a.c,.a.d,.b.c,.b.d{
396
- margin:2
397
- }`.replace(/\n\s*/g, '')
398
-
399
- expect(actual6).to.equal(expected6)
400
- ```
401
-
402
- ### createEscape
403
-
404
- ```ts
405
- const createEscape: (escapeFn: (value: any) => string) => (template: TemplateStringsArray, ...values: unknown[]) => string;
406
- ```
407
-
408
- Creates a tag function
409
- for escaping interpolated values in template literals.
410
-
411
- #### escapeFn
412
-
413
- A function that takes a value
414
- and returns its escaped string representation.
415
-
416
- #### Returns
417
-
418
- A tag function that escapes interpolated values
419
- using the provided escape map.
420
-
421
- #### Usage Examples
422
-
423
- ```ts
424
- const escapeFn = (value: any): string =>
425
- isArray(value)
426
- ? value.map(escapeFn).join(', ')
427
- : value === true || value === false
428
- ? `b'${+value}'`
429
- : value instanceof Date
430
- ? `'${value
431
- .toISOString()
432
- .replace(/^(.+)T(.+)\..*$/, '$1 $2')}'`
433
- : isFiniteNumber(value)
434
- ? `${value}`
435
- : isString(value)
436
- ? `'${value.replace(/'/g, "''")}'`
437
- : 'NULL'
438
-
439
- const sql = createEscape(escapeFn)
440
-
441
- const actual = sql`
442
- SELECT *
443
- FROM table_name
444
- WHERE column_name IN (
445
- ${[
446
- true,
447
- null,
448
- undefined,
449
- NaN,
450
- Infinity,
451
- 42,
452
- '42',
453
- "4'2",
454
- /42/,
455
- new Date(323325000000),
456
- ]}
457
- )`
458
-
459
- const expected = `
460
- SELECT *
461
- FROM table_name
462
- WHERE column_name IN (
463
- b'1', NULL, NULL, NULL, NULL, 42, '42', '4''2', NULL, '1980-03-31 04:30:00'
464
- )`
465
-
466
- expect(actual).to.equal(expected)
467
- ```
468
-
469
- ### createPluralRules
470
-
471
- ```ts
472
- const createPluralRules: (locale: string, forms: Partial<Record<Intl.LDMLPluralRule, string>>) => (value: number) => string;
473
- ```
474
-
475
- Creates a function that
476
- returns the appropriate noun form
477
- based on a numeric value using `Intl.PluralRules`.
478
-
479
- Different languages have different plural rules.
480
- The `Intl.PluralRules` API provides locale-aware
481
- plural category selection.
482
- Possible categories are:
483
-
484
- - `zero`: for zero items
485
- (used in some languages like Arabic, Latvian)
486
- - `one`: for singular (e.g., 1 item)
487
- - `two`: for dual
488
- (used in some languages like Arabic, Hebrew)
489
- - `few`: for small plurals (e.g., 2-4 in Polish)
490
- - `many`: for larger plurals (e.g., 5-21 in Polish)
491
- - `other`: fallback category (used by all languages)
492
-
493
- #### locale
494
-
495
- A BCP 47 language tag (e.g., `pl`, `en`).
496
-
497
- #### forms
498
-
499
- An object mapping plural categories to noun forms.
500
- Not all categories need to be provided;
501
- if a category is missing,
502
- the function falls back to `other`,
503
- then to an empty string.
504
-
505
- #### Returns
506
-
507
- A function that takes a numeric value
508
- and returns the appropriate noun form.
509
-
510
- #### Usage Examples
511
-
512
- ```ts
513
- const auto = createPluralRules('pl', {
514
- one: 'auto',
515
- few: 'auta',
516
- other: 'aut',
517
- })
518
-
519
- expect(auto(0)).to.equal('aut')
520
- expect(auto(1)).to.equal('auto')
521
- expect(auto(17)).to.equal('aut')
522
- expect(auto(42)).to.equal('auta')
523
-
524
- const car = createPluralRules('en', {
525
- one: 'car',
526
- other: 'cars',
527
- })
528
-
529
- expect(car(0)).to.equal('cars')
530
- expect(car(1)).to.equal('car')
531
- expect(car(17)).to.equal('cars')
532
- expect(car(42)).to.equal('cars')
533
-
534
- const empty = createPluralRules('en', {})
535
-
536
- expect(empty(0)).to.equal('')
537
- expect(empty(1)).to.equal('')
538
- expect(empty(17)).to.equal('')
539
- expect(empty(42)).to.equal('')
540
- ```
541
-
542
134
  ### createTokenizer
543
135
 
544
136
  ```ts
@@ -561,9 +153,9 @@ For unmatched text between patterns,
561
153
 
562
154
  An array of tuples `[metadata, pattern]` where:
563
155
  - `metadata`: arbitrary data (e.g., a CSS class name)
564
- passed to `decorator` when the pattern matches.
156
+ passed to `decorator` when the pattern matches.
565
157
  - `pattern`: a `string` or `RegExp`
566
- to match against the input.
158
+ to match against the input.
567
159
 
568
160
  #### Returns
569
161
 
@@ -573,11 +165,11 @@ and returns an array of decorated tokens.
573
165
  #### Remarks
574
166
 
575
167
  1. Matches starting at an earlier position
576
- take precedence.
168
+ take precedence.
577
169
  2. Among matches at the same position,
578
- the longer one wins.
170
+ the longer one wins.
579
171
  3. Among matches of the same position and length,
580
- the one defined earlier wins.
172
+ the one defined earlier wins.
581
173
 
582
174
  #### Usage Examples
583
175
 
@@ -803,14 +395,14 @@ If a `Node`, that node is modified in place.
803
395
 
804
396
  Additional arguments processed as follows:
805
397
  - `Object`: maps attributes/properties.
806
- Keys starting with `$` set element properties
807
- (without the `$` prefix);
808
- other keys set attributes via `setAttribute`.
809
- 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.
810
402
  - `null`/`undefined`: ignored.
811
403
  - `Node`: appended as a child.
812
404
  - `string`/`number`: converted to a `Text` node
813
- and appended.
405
+ and appended.
814
406
  - [`HArgs`](#HArgs) array: processed recursively.
815
407
 
816
408
  #### Returns
@@ -1215,93 +807,14 @@ The JSON string to parse.
1215
807
 
1216
808
  The parsed value with handler substitutions applied.
1217
809
 
1218
- #### Usage Examples
1219
-
1220
- ```ts
1221
- const handlers = {
1222
- $add: (a: number, b: number) => a + b,
1223
- $hello: (name: string) => `Hello ${name}!`,
1224
- $foo: () => 'bar',
1225
- }
1226
-
1227
- const actual1 = jsOnParse(
1228
- handlers,
1229
- `[
1230
- {
1231
- "$add": [1, 2]
1232
- },
1233
- {
1234
- "$hello": ["World"]
1235
- },
1236
- {
1237
- "nested": {
1238
- "$hello": ["nested World"]
1239
- },
1240
- "one": 1,
1241
- "two": 2
1242
- },
1243
- {
1244
- "$foo": []
1245
- },
1246
- {
1247
- "$foo": ["The parent object does not have exactly one property!"],
1248
- "one": 1,
1249
- "two": 2
1250
- }
1251
- ]`
1252
- )
1253
-
1254
- expect(actual1).to.deep.equal([
1255
- 3,
1256
- 'Hello World!',
1257
- {
1258
- nested: 'Hello nested World!',
1259
- one: 1,
1260
- two: 2,
1261
- },
1262
- 'bar',
1263
- {
1264
- $foo: [
1265
- 'The parent object does not have ' +
1266
- 'exactly one property!',
1267
- ],
1268
- one: 1,
1269
- two: 2,
1270
- },
1271
- ])
1272
-
1273
- const actual2 = jsOnParse(
1274
- { $notFunc: 'handler not being a function' } as any,
1275
- '{"$notFunc": [1, 2]}'
1276
- )
1277
-
1278
- expect(actual2).to.deep.equal({ $notFunc: [1, 2] })
1279
-
1280
- const actual3 = jsOnParse(
1281
- handlers,
1282
- '{"$unknown_handler_key": [1, 2]}'
1283
- )
1284
-
1285
- expect(actual3).to.deep.equal({
1286
- $unknown_handler_key: [1, 2],
1287
- })
1288
-
1289
- const actual4 = jsOnParse(
1290
- handlers,
1291
- '{"$add": {"not": "array"}}'
1292
- )
1293
-
1294
- expect(actual4).to.deep.equal({ $add: { not: 'array' } })
1295
- ```
1296
-
1297
810
  ### monokai
1298
811
 
1299
812
  ```ts
1300
- const monokai: CRoot;
813
+ const monokai: CNode;
1301
814
  ```
1302
815
 
1303
816
  A Monokai-inspired color scheme
1304
- for use with the [`c`](#c) helper
817
+ for use with the {@link c} helper
1305
818
  and [`nanolightTs`](#nanolightTs) tokenizer.
1306
819
 
1307
820
  ### nanolightTs
@@ -1330,11 +843,11 @@ const codeJs =
1330
843
  "{ 42: 42 }['42'] /* 42 */"
1331
844
 
1332
845
  expect(nanolightTs(codeJs)).to.deep.equal([
1333
- ['span', { class: 'keyword-1' }, 'const'],
846
+ ['span', { class: 'keyword1' }, 'const'],
1334
847
  ' ',
1335
848
  [
1336
849
  'span',
1337
- { class: 'identifier-4' },
850
+ { class: 'identifier4' },
1338
851
  'answerToLifeTheUniverseAndEverything',
1339
852
  ],
1340
853
  ' ',
@@ -1428,7 +941,7 @@ expect(pick(ref, ['a', 'b'])).to.deep.equal({
1428
941
  ### rwd
1429
942
 
1430
943
  ```ts
1431
- 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;
1432
945
  ```
1433
946
 
1434
947
  A responsive web design helper
@@ -1436,7 +949,7 @@ that generates CSS rules for a grid-like layout.
1436
949
 
1437
950
  #### root
1438
951
 
1439
- The CSS root object to populate (see [`c`](#c)).
952
+ The CSS root object to populate (see {@link c}).
1440
953
 
1441
954
  #### selector
1442
955
 
@@ -1454,16 +967,16 @@ The base cell height in pixels.
1454
967
 
1455
968
  An array of breakpoint specifications, each a tuple of:
1456
969
  - `maxWidth`: maximum number of cells per row
1457
- (defines the viewport breakpoint).
970
+ (defines the viewport breakpoint).
1458
971
  - `width` (optional, default `1`):
1459
- number of horizontal cells the element spans.
972
+ number of horizontal cells the element spans.
1460
973
  - `height` (optional, default `1`):
1461
- number of vertical cells the element spans.
974
+ number of vertical cells the element spans.
1462
975
 
1463
976
  #### Usage Examples
1464
977
 
1465
978
  ```ts
1466
- const style: CRoot = {
979
+ const style: CNode = {
1467
980
  body: {
1468
981
  margin: 0,
1469
982
  },
@@ -1531,14 +1044,14 @@ If a `Node`, that node is modified in place.
1531
1044
 
1532
1045
  Additional arguments processed as follows:
1533
1046
  - `Object`: maps attributes/properties.
1534
- Keys starting with `$` set element properties
1535
- (without the `$` prefix);
1536
- other keys set attributes via `setAttributeNS`.
1537
- 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.
1538
1051
  - `null`/`undefined`: ignored.
1539
1052
  - `Node`: appended as a child.
1540
1053
  - `string`/`number`: converted to a `Text` node
1541
- and appended.
1054
+ and appended.
1542
1055
  - [`HArgs`](#HArgs) array: processed recursively.
1543
1056
 
1544
1057
  #### Returns