@jackens/nnn 2023.8.17

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 +180 -0
  2. package/nnn.js +638 -0
  3. package/package.json +33 -0
  4. package/readme.md +5 -0
package/nnn.d.ts ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * The type of arguments of the `$c` helper.
3
+ */
4
+ export type $CNode = {
5
+ [attributeOrSelector: string]: string | number | $CNode;
6
+ };
7
+
8
+ /**
9
+ * The type of arguments of the `$c` helper.
10
+ */
11
+ export type $CRoot = Record<string, $CNode>;
12
+
13
+ /**
14
+ * The type of arguments of the `$escapeValues` and `$escape` helpers.
15
+ */
16
+ export type $EscapeMap = Map<any, (value?: any) => string>;
17
+
18
+ /**
19
+ * The type of arguments of the `$h` and `$s` helpers.
20
+ */
21
+ export type $HArgs = [
22
+ string | Node,
23
+ ...(Record<string, any> | null | undefined | Node | string | number | $HArgs)[]
24
+ ];
25
+
26
+ /**
27
+ * A simple CSS-in-JS helper.
28
+ *
29
+ * The `root` parameter provides a hierarchical description of CSS rules.
30
+ *
31
+ * - Keys of sub-objects whose values are NOT objects are treated as CSS attribute, and values are treated as values of those CSS attributes; the concatenation of keys of all parent objects is a CSS rule.
32
+ * - All keys ignore the part starting with a splitter (default: `$$`) sign until the end of the key (e.g. `src$$1` → `src`, `@font-face$$1` → `@font-face`).
33
+ * - In keys specifying CSS attribute, all uppercase letters are replaced by lowercase letters with an additional `-` character preceding them (e.g. `fontFamily` → `font-family`).
34
+ * - Commas in keys that makes a CSS rule cause it to “split” and create separate rules for each part (e.g. `{div:{margin:1,'.a,.b,.c':{margin:2}}}` → `div{margin:1}div.a,div.b,div.c{margin:2}`).
35
+ * - Top-level keys that begin with `@` are not concatenated with sub-object keys.
36
+ */
37
+ export function $c(root: $CRoot, splitter?: string): string;
38
+
39
+ /**
40
+ * A helper for creating a chart based on a table (conf. <https://jackens.github.io/chartable/>).
41
+ */
42
+ export function $chartable(options?: {
43
+ bottomPx?: number;
44
+ headerColumn?: boolean;
45
+ headerRow?: boolean;
46
+ leftPx?: number;
47
+ rightPx?: number;
48
+ series?: number[][];
49
+ seriesLabels?: string[];
50
+ table?: HTMLTableElement;
51
+ title?: string;
52
+ topPx?: number;
53
+ xGapPx?: number;
54
+ xLabels?: string[];
55
+ yGapPx?: number;
56
+ yMax?: number;
57
+ }): SVGSVGElement;
58
+
59
+ /**
60
+ * A helper that checks equality of the given arguments.
61
+ */
62
+ export function $eq(x: any, y: any): boolean;
63
+
64
+ /**
65
+ * A generic helper for escaping `values` by given `escapeMap` (in *TemplateStrings* flavor).
66
+ */
67
+ export function $escape(escapeMap: $EscapeMap, template: TemplateStringsArray, ...values: any[]): string;
68
+
69
+ /**
70
+ * A generic helper for escaping `values` by given `escapeMap`.
71
+ */
72
+ export function $escapeValues(escapeMap: $EscapeMap, values: any[]): string[];
73
+
74
+ /**
75
+ * A helper that implements typographic corrections specific to Polish typography.
76
+ */
77
+ export function $fixTypography(node: Node): void;
78
+
79
+ /**
80
+ * A convenient helper for getting values of nested objects.
81
+ */
82
+ export function $get(defaultValue: any, ref: any, ...keys: (string | number | symbol)[]): any;
83
+
84
+ /**
85
+ * A lightweight [HyperScript](https://github.com/hyperhype/hyperscript)-style helper for creating and modifying `HTMLElement`s (see also `$s`).
86
+ *
87
+ * - The first argument of type `string` specifies the tag of the element to be created.
88
+ * - The first argument of type `Node` specifies the element to be modified.
89
+ * - All other arguments of type `Record<string, any>` are mappings of attributes and properties.
90
+ * Keys starting with `$` specify *properties* (without the leading `$`) to be set on the element being created or modified.
91
+ * (Note that `$` is not a valid attribute name character.)
92
+ * All other keys specify *attributes* to be set by `setAttribute`.
93
+ * An attribute equal to `false` causes the attribute to be removed by `removeAttribute`.
94
+ * - All other arguments of type `null` or `undefined` are simply ignored.
95
+ * - All other arguments of type `Node` are appended to the element being created or modified.
96
+ * - All other arguments of type `string`/`number` are converted to `Text` nodes and appended to the element being created or modified.
97
+ * - All other arguments of type `$HArgs` are passed to `$h` and the results are appended to the element being created or modified.
98
+ */
99
+ export function $h<T extends keyof HTMLElementTagNameMap>(tag: T, ...args: $HArgs[1][]): HTMLElementTagNameMap[T];
100
+ export function $h<N extends Node>(node: N, ...args: $HArgs[1][]): N;
101
+ export function $h(...args: $HArgs): Node;
102
+
103
+ /**
104
+ * A replacement for the `in` operator (not to be confused with the `for-in` loop) that works properly.
105
+ */
106
+ export function $in(key: any, ref: any): boolean;
107
+
108
+ /**
109
+ * A helper that checks if the given argument is of a certain type.
110
+ */
111
+ export function $is(type: BigIntConstructor, arg: any): arg is bigint;
112
+ export function $is(type: BooleanConstructor, arg: any): arg is boolean;
113
+ export function $is(type: NumberConstructor, arg: any): arg is number;
114
+ export function $is(type: StringConstructor, arg: any): arg is string;
115
+ export function $is(type: SymbolConstructor, arg: any): arg is symbol;
116
+ export function $is(type: undefined, arg: any): arg is null | undefined;
117
+ export function $is<T extends abstract new (...args: any[]) => any>(type: T, arg: any): arg is InstanceType<T>;
118
+
119
+ /**
120
+ * Language translations helper.
121
+ */
122
+ export function $locale(
123
+ locales: Record<string, Record<string, string | Record<string, string>>>,
124
+ defaultLanguage: string,
125
+ languages?: readonly string[]
126
+ ): (text?: string, version?: string) => string;
127
+
128
+ /**
129
+ * A generic helper for syntax highlighting (see also `$nanolightJs`).
130
+ */
131
+ export function $nanolight(
132
+ pattern: RegExp,
133
+ highlighters: ((chunk: string, index: number) => $HArgs[1])[],
134
+ code: string
135
+ ): $HArgs[1][];
136
+
137
+ /**
138
+ * A helper for highlighting JavaScript.
139
+ */
140
+ export function $nanolightJs(codeJs: string): $HArgs[1][];
141
+
142
+ /**
143
+ * A helper for choosing the correct singular and plural.
144
+ */
145
+ export function $plUral(singular: string, plural2: string, plural5: string, value: number): string;
146
+
147
+ /**
148
+ * A lightweight [HyperScript](https://github.com/hyperhype/hyperscript)-style helper for creating and modifying `SVGElement`s (see also `$h`).
149
+ *
150
+ * - The first argument of type `string` specifies the tag of the element to be created.
151
+ * - The first argument of type `Node` specifies the element to be modified.
152
+ * - All other arguments of type `Record<string, any>` are mappings of attributes and properties.
153
+ * Keys starting with `$` specify *properties* (without the leading `$`) to be set on the element being created or modified.
154
+ * (Note that `$` is not a valid attribute name character.)
155
+ * All other keys specify *attributes* to be set by `setAttributeNS`.
156
+ * An attribute equal to `false` causes the attribute to be removed by `removeAttributeNS`.
157
+ * - All other arguments of type `null` or `undefined` are simply ignored.
158
+ * - All other arguments of type `Node` are appended to the element being created or modified.
159
+ * - All other arguments of type `string`/`number` are converted to `Text` nodes and appended to the element being created or modified.
160
+ * - All other arguments of type `$HArgs` are passed to `$s` and the results are appended to the element being created or modified.
161
+ */
162
+ export function $s<T extends keyof SVGElementTagNameMap>(tag: T, ...args: $HArgs[1][]): SVGElementTagNameMap[T];
163
+ export function $s<N extends Node>(node: N, ...args: $HArgs[1][]): N;
164
+ export function $s(...args: $HArgs): Node;
165
+
166
+ /**
167
+ * A convenient helper for setting values of nested objects.
168
+ */
169
+ export function $set(value: any, ref: any, ...keys: (string | number | symbol)[]): void;
170
+
171
+ /**
172
+ * A helper that generates a UUID v1 identifier (with a creation timestamp).
173
+ *
174
+ * - The optional `node` parameter should have the format `/^[0123456789abcdef]+$/`.
175
+ * Its value will be trimmed to last 12 characters and left padded with zeros.
176
+ */
177
+ export function $uuid1(options?: {
178
+ date?: Date;
179
+ node?: string;
180
+ }): string;
package/nnn.js ADDED
@@ -0,0 +1,638 @@
1
+ /**
2
+ * @typedef {[
3
+ * string | Node,
4
+ * ...(Record<string, any> | null | undefined | Node | string | number | $HArgs)[]
5
+ * ]} $HArgs
6
+ */
7
+
8
+ /**
9
+ * @typedef {{
10
+ * [attributeOrSelector: string]: string | number | $CNode;
11
+ * }} $CNode
12
+ */
13
+
14
+ /**
15
+ * @typedef {Record<string, $CNode>} $CRoot
16
+ */
17
+
18
+ /**
19
+ * @typedef {Map<any, (value?: any) => string>} $EscapeMap
20
+ */
21
+
22
+ const _c = (
23
+ /** @type {$CNode} */ node,
24
+ /** @type {string} */ prefix,
25
+ /** @type {string[]} */ result,
26
+ /** @type {(text: string) => string} */ split
27
+ ) => {
28
+ const /** @type {[$CNode | string[], string][]} */ queue = [[node, prefix]]
29
+
30
+ while (queue.length) {
31
+ const [style2, prefix2] = queue.shift() ?? []
32
+
33
+ if (style2 == null || prefix2 == null) {
34
+ continue
35
+ }
36
+
37
+ if ($is(Array, style2)) {
38
+ result.push(prefix2, prefix2 !== '' ? '{' : '', style2.join(';'), prefix2 !== '' ? '}' : '')
39
+ } else {
40
+ const /** @type {[$CNode | string[], string][]} */ todo = []
41
+ let /** @type {string[]} */ attributes = []
42
+ let attributesPushed = false
43
+
44
+ for (const key in style2) {
45
+ const value = style2[key]
46
+
47
+ if ($is(String, value) || $is(Number, value)) {
48
+ if (!attributesPushed) {
49
+ attributesPushed = true
50
+ attributes = []
51
+ todo.push([attributes, prefix2])
52
+ }
53
+
54
+ attributes.push(`${split(key).replace(/([A-Z])/g, (_, letter) => '-' + letter.toLowerCase())}:${value}`)
55
+ } else {
56
+ attributesPushed = false
57
+
58
+ const /** @type {string[]} */ newPrefix = []
59
+ const keySplitted = key.split(',')
60
+
61
+ for (const prefixItem of prefix2.split(',')) {
62
+ for (const keyItem of keySplitted) {
63
+ newPrefix.push(prefixItem + keyItem)
64
+ }
65
+ }
66
+
67
+ todo.push([value, newPrefix.join(',')])
68
+ }
69
+ }
70
+
71
+ queue.unshift(...todo)
72
+ }
73
+ }
74
+ }
75
+
76
+ export const $c = (/** @type {$CRoot} */ root, splitter = '$$') => {
77
+ const split = (/** @type {string} */ text) => text.split(splitter)[0]
78
+ const /** @type {string[]} */ result = []
79
+
80
+ for (const key in root) {
81
+ const value = root[key]
82
+
83
+ if (key[0] === '@') {
84
+ result.push(split(key) + '{')
85
+ _c(value, '', result, split)
86
+ result.push('}')
87
+ } else {
88
+ _c(value, split(key), result, split)
89
+ }
90
+ }
91
+
92
+ return result.join('')
93
+ }
94
+
95
+ const _COLORS = ['#e22', '#e73', '#fc3', '#ad4', '#4d9', '#3be', '#45d', '#c3e']
96
+
97
+ /**
98
+ * @param {{
99
+ * bottomPx?: number;
100
+ * headerColumn?: boolean;
101
+ * headerRow?: boolean;
102
+ * leftPx?: number;
103
+ * rightPx?: number;
104
+ * series?: number[][];
105
+ * seriesLabels?: string[];
106
+ * table?: HTMLTableElement;
107
+ * title?: string;
108
+ * topPx?: number;
109
+ * xGapPx?: number;
110
+ * xLabels?: string[];
111
+ * yGapPx?: number;
112
+ * yMax?: number;
113
+ * }} options
114
+ */
115
+ export const $chartable = ({
116
+ bottomPx = 50,
117
+ headerColumn = false,
118
+ headerRow = true,
119
+ leftPx = 200,
120
+ rightPx = 200,
121
+ series = [],
122
+ seriesLabels = [],
123
+ table,
124
+ title,
125
+ topPx = 70,
126
+ xGapPx = 70,
127
+ xLabels = [],
128
+ yGapPx = 30,
129
+ yMax = 10
130
+ } = {}) => {
131
+ if (table != null) {
132
+ series = []
133
+ seriesLabels = []
134
+ xLabels = []
135
+ table.querySelectorAll('tr').forEach((row, r) => row.querySelectorAll('td,th').forEach((col, c) => {
136
+ const s = c - +headerColumn
137
+ const v = r - +headerRow
138
+ // @ts-expect-error
139
+ const value = col.innerText
140
+
141
+ if (s >= 0 && v >= 0) {
142
+ $set(parseFloat(value), series, s, v)
143
+ } else if (s >= 0 && v < 0) {
144
+ seriesLabels[s] = value
145
+ } else if (s < 0 && v >= 0) {
146
+ xLabels[v] = value
147
+ }
148
+ }))
149
+ }
150
+
151
+ const ranges = series.map(values => {
152
+ const validValues = values.filter(value => !isNaN(value))
153
+ return [Math.min(...validValues), Math.max(...validValues)]
154
+ })
155
+
156
+ let bestScales = [Infinity, -Infinity, Infinity, -Infinity, Infinity]
157
+
158
+ ranges.forEach(([min1, max0]) => ranges.forEach(([min0, max1]) => {
159
+ if (min0 >= min1 && max1 >= max0) {
160
+ let min2 = Infinity
161
+ let max2 = -Infinity
162
+
163
+ ranges.forEach(([min, max]) => {
164
+ if (min < min1 || max1 < max) {
165
+ if (min2 > min) {
166
+ min2 = min
167
+ }
168
+ if (max2 < max) {
169
+ max2 = max
170
+ }
171
+ }
172
+ })
173
+
174
+ const cost = ranges.reduce((result, [min, max]) =>
175
+ (result += 1 - (max - min) / (min1 <= min && max <= max1 ? max1 - min1 : max2 - min2)), 0)
176
+
177
+ if (bestScales[4] > cost) {
178
+ bestScales = [min1, max1, min2, max2, cost]
179
+ }
180
+ }
181
+ }))
182
+
183
+ const [min1, max1, min2, max2] = bestScales
184
+ const xMax = series[0].length
185
+ const xi = Array.from({ length: xMax }, (_, x) => x * xGapPx)
186
+ const yi = Array.from({ length: yMax }, (_, y) => y * yGapPx)
187
+ const w = xGapPx * (xMax - 1)
188
+ const h = yGapPx * (yMax - 1)
189
+ const svgW = leftPx + w + rightPx
190
+ const svgH = topPx + h + bottomPx
191
+ const /** @type {$HArgs} */ graph = ['g',
192
+ { transform: `translate(${leftPx} ${topPx})` },
193
+ ...yi.map(y => ['line', { x1: 0, x2: w, y1: y, y2: y, stroke: '#8888' }]),
194
+ ...xi.map(x => ['line', { x1: x, x2: x, y1: 0, y2: h, stroke: '#8888' }])]
195
+ const /** @type {$HArgs} */ leftDots = ['g',
196
+ { class: 'chartable-left-legend-dots', transform: 'translate(-80 0)' }]
197
+ const /** @type {$HArgs} */ leftLabels = ['g',
198
+ { class: 'chartable-left-legend-labels', transform: 'translate(-95 0)' }]
199
+ const /** @type {$HArgs} */ rightDots = ['g',
200
+ { class: 'chartable-right-legend-dots', transform: 'translate(80 0)' }]
201
+ const /** @type {$HArgs} */ rightLabels = ['g',
202
+ { class: 'chartable-right-legend-labels', transform: 'translate(95 0)' }]
203
+ const yLabel = (/** @type {number} */ min, /** @type {number} */ max, /** @type {number} */ y) =>
204
+ (min + (max - min) * (yMax - 1 - y) / (yMax - 1)).toFixed(2).replace(/\.?0+$/, '')
205
+
206
+ series.forEach((values, s) => {
207
+ const cls = `chartable-series-${s + 1}`
208
+ const color = _COLORS[s % _COLORS.length]
209
+ let [min, max] = ranges[s]
210
+
211
+ if (min1 <= min && max <= max1) {
212
+ min = min1
213
+ max = max1
214
+ leftDots.push(['circle', { class: cls, cx: 0, cy: yGapPx * (leftDots.length - 2), r: 5, fill: color }])
215
+ leftLabels.push(['text', { x: 0, y: yGapPx * (leftLabels.length - 2), 'text-anchor': 'end', 'alignment-baseline': 'middle' }, seriesLabels[s]])
216
+ } else {
217
+ min = min2
218
+ max = max2
219
+ rightDots.push(['circle', { class: cls, cx: 0, cy: yGapPx * (rightDots.length - 2), r: 5, fill: color }])
220
+ rightLabels.push(['text', { x: 0, y: yGapPx * (rightLabels.length - 2), 'text-anchor': 'start', 'alignment-baseline': 'middle' }, seriesLabels[s]])
221
+ }
222
+
223
+ const y = values.map(v => isNaN(v) ? v : h * (max - v) / (max - min))
224
+
225
+ xi.forEach((x, i) => {
226
+ if (!isNaN(y[i])) {
227
+ graph.push(['circle', { class: cls, cx: x, cy: y[i], r: 5, fill: color }])
228
+
229
+ if (!isNaN(y[i - 1])) {
230
+ graph.push(['line', { class: cls, x1: x, x2: xi[i - 1], y1: y[i], y2: y[i - 1], stroke: color }])
231
+ }
232
+ }
233
+ })
234
+ })
235
+
236
+ return $s('svg', {
237
+ viewBox: `0 0 ${svgW} ${svgH}`,
238
+ width: `${svgW}px`,
239
+ height: `${svgH}px`,
240
+ class: 'chartable'
241
+ }, title != null
242
+ ? ['text',
243
+ { x: svgW / 2, y: topPx / 2, 'text-anchor': 'middle', 'alignment-baseline': 'middle', class: 'chartable-title' },
244
+ title]
245
+ : null,
246
+ graph,
247
+ min1 < Infinity
248
+ ? ['g',
249
+ { transform: `translate(${leftPx} ${topPx})` },
250
+ ['g',
251
+ { class: 'chartable-left-y-labels', transform: 'translate(-15 0)' },
252
+ ...yi.map((y, i) => ['text',
253
+ { x: 0, y, 'text-anchor': 'end', 'alignment-baseline': 'middle' },
254
+ yLabel(min1, max1, i)])],
255
+ leftDots, leftLabels]
256
+ : null,
257
+ min2 < Infinity
258
+ ? ['g',
259
+ { transform: `translate(${leftPx + w} ${topPx})` },
260
+ ['g',
261
+ { class: 'chartable-right-y-labels', transform: 'translate(15 0)' },
262
+ ...yi.map((y, i) => ['text',
263
+ { x: 0, y, 'text-anchor': 'start', 'alignment-baseline': 'middle' },
264
+ yLabel(min2, max2, i)])],
265
+ rightDots, rightLabels]
266
+ : null,
267
+ xLabels.length > 0
268
+ ? ['g',
269
+ { transform: `translate(${leftPx} ${topPx + h})` },
270
+ ['g',
271
+ { class: 'chartable-x-labels', transform: 'translate(0 15)' },
272
+ ...xi.map((x, i) => ['text',
273
+ { x, y: 0, 'text-anchor': 'middle', 'alignment-baseline': 'hanging' },
274
+ xLabels[i]])]]
275
+ : null
276
+ )
277
+ }
278
+
279
+ export const $eq = /** @returns {boolean} */ (/** @type {any} */ x, /** @type {any} */ y) => {
280
+ if (x === y) {
281
+ return true
282
+ }
283
+
284
+ const xConstructor = x?.constructor
285
+
286
+ if (xConstructor === y?.constructor) {
287
+ if (xConstructor === Number) {
288
+ return (isNaN(x) && isNaN(y)) || +x === +y
289
+ }
290
+
291
+ if (xConstructor === Date) {
292
+ return +x === +y
293
+ }
294
+
295
+ if (xConstructor === String || xConstructor === RegExp) {
296
+ return '' + x === '' + y
297
+ }
298
+
299
+ if (xConstructor === Array) {
300
+ return x.length === y.length && x.every((/** @type {any} */ item, /** @type {number} */ index) => $eq(item, y[index]))
301
+ }
302
+
303
+ if (xConstructor === Object) {
304
+ const keysOfX = Object.keys(x)
305
+ return keysOfX.length === Object.keys(y).length && keysOfX.every(key => $eq(x[key], y[key]))
306
+ }
307
+
308
+ if (xConstructor === Set || xConstructor === Map) {
309
+ if (x.size !== y.size) {
310
+ return false
311
+ }
312
+ const xa = [...x]
313
+ const ya = [...y]
314
+ return xa.every(xv => ya.find(yv => $eq(xv, yv)))
315
+ }
316
+ }
317
+
318
+ return false
319
+ }
320
+
321
+ export const $escape = (
322
+ /** @type {$EscapeMap} */ escapeMap,
323
+ /** @type {TemplateStringsArray} */ template,
324
+ /** @type {any[]} */ ...values
325
+ ) => String.raw(template, ...$escapeValues(escapeMap, values))
326
+
327
+ export const $escapeValues = (
328
+ /** @type {$EscapeMap} */ escapeMap,
329
+ /** @type {any[]} */ values
330
+ ) => values.map(value => (escapeMap.get(value?.constructor) ?? escapeMap.get(undefined))?.(value) ?? '')
331
+
332
+ const TAGS_TO_SKIP = { IFRAME: 1, NOSCRIPT: 1, PRE: 1, SCRIPT: 1, STYLE: 1, TEXTAREA: 1 }
333
+
334
+ export const $fixTypography = (/** @type {Node} */ node) => {
335
+ const /** @type {Node[]} */ queue = [node]
336
+
337
+ while (queue.length > 0) {
338
+ const node0 = queue.shift()
339
+
340
+ if (node0 instanceof Element) {
341
+ for (let i = 0; i < node0.childNodes.length; ++i) {
342
+ const childNode = node0.childNodes[i]
343
+
344
+ if (childNode instanceof Text) {
345
+ queue.push(childNode)
346
+ } else if (childNode instanceof Element && !$in(childNode.tagName, TAGS_TO_SKIP)) {
347
+ queue.push(childNode)
348
+ }
349
+ }
350
+ } else if (node0 instanceof Text && node0.nodeValue != null && node0.nodeValue.trim() !== '') {
351
+ let /** @type {Node} */ previousNode = node0
352
+
353
+ node0.nodeValue.split(/(\s|\(|„)([aiouwz—]\s)/gi).forEach((chunk, i) => {
354
+ i %= 3
355
+
356
+ const currentNode = i === 2
357
+ ? $h('span', { style: 'white-space:nowrap' }, chunk)
358
+ : i === 1
359
+ ? document.createTextNode(chunk)
360
+ : document.createTextNode(chunk.replace(/(\/(?=[^/\s])|\.(?=[^\s]))/g, '$1\u200B'))
361
+
362
+ if (node0.parentNode != null) {
363
+ node0.parentNode.insertBefore(currentNode, previousNode.nextSibling)
364
+ }
365
+
366
+ previousNode = currentNode
367
+ })
368
+
369
+ node0.parentNode?.removeChild(node0)
370
+ }
371
+ }
372
+ }
373
+
374
+ export const $get = (
375
+ /** @type {any} */ defaultValue,
376
+ /** @type {any} */ ref,
377
+ /** @type {(string | number | symbol)[]} */ ...keys
378
+ ) => {
379
+ for (const key of keys) {
380
+ if (!$in(key, ref)) {
381
+ return defaultValue
382
+ }
383
+
384
+ ref = ref[key]
385
+ }
386
+
387
+ return ref
388
+ }
389
+
390
+ const /** @type {Record<string, string>} */ NS = {
391
+ xlink: 'http://www.w3.org/1999/xlink'
392
+ }
393
+
394
+ /**
395
+ * @type {{
396
+ * (namespaceURI?: null | undefined): {
397
+ * <T extends keyof HTMLElementTagNameMap>(tag: T, ...args: $HArgs[1][]): HTMLElementTagNameMap[T];
398
+ * <N extends Node> (node: N, ...args: $HArgs[1][]): N;
399
+ * (...args: $HArgs): Node;
400
+ * };
401
+ * (namespaceURI: 'http://www.w3.org/2000/svg'): {
402
+ * <T extends keyof SVGElementTagNameMap> (tag: T, ...args: $HArgs[1][]): SVGElementTagNameMap[T];
403
+ * <N extends Node> (node: N, ...args: $HArgs[1][]): N;
404
+ * (...args: $HArgs): Node;
405
+ * };
406
+ * }}
407
+ */
408
+ const _h = (/** @type {string?=} */ namespaceURI) => {
409
+ const createElement = namespaceURI == null
410
+ ? (/** @type {string } */ tag) => document.createElement(tag)
411
+ : (/** @type {string } */ tag) => document.createElementNS(namespaceURI, tag)
412
+
413
+ const h = (/** @type {$HArgs[0]} */ tagOrNode, /** @type {$HArgs[1][]} */ ...args) => {
414
+ const node = $is(String, tagOrNode) ? createElement(tagOrNode) : tagOrNode
415
+
416
+ for (const arg of args) {
417
+ let child = null
418
+
419
+ if (arg instanceof Node) {
420
+ child = arg
421
+ } else if ($is(String, arg) || $is(Number, arg)) {
422
+ // @ts-expect-error
423
+ child = new Text(arg)
424
+ } else if ($is(Array, arg)) {
425
+ // @ts-expect-error
426
+ child = h(...arg)
427
+ } else if (arg != null) {
428
+ for (const name in arg) {
429
+ const value = arg[name]
430
+
431
+ if (name[0] === '$') {
432
+ const name1 = name.slice(1)
433
+
434
+ if ($is(Object, value)) {
435
+ // @ts-expect-error
436
+ node[name1] = node[name1] ?? {}
437
+ // @ts-expect-error
438
+ Object.assign(node[name1], value)
439
+ } else {
440
+ // @ts-expect-error
441
+ node[name1] = value
442
+ }
443
+ } else if (node instanceof Element) {
444
+ const indexOfColon = name.indexOf(':')
445
+
446
+ if (indexOfColon >= 0) {
447
+ const /** @type {string=} */ ns = NS[name.slice(0, indexOfColon)]
448
+
449
+ if (ns != null) {
450
+ const basename = name.slice(indexOfColon + 1)
451
+
452
+ if (value === true) {
453
+ node.setAttributeNS(ns, basename, '')
454
+ } else if (value === false) {
455
+ node.removeAttributeNS(ns, basename)
456
+ } else {
457
+ node.setAttributeNS(ns, basename, $is(String, value) ? value : '' + value)
458
+ }
459
+ }
460
+ } else {
461
+ if (value === true) {
462
+ node.setAttribute(name, '')
463
+ } else if (value === false) {
464
+ node.removeAttribute(name)
465
+ } else {
466
+ node.setAttribute(name, $is(String, value) ? value : '' + value)
467
+ }
468
+ }
469
+ }
470
+ }
471
+ }
472
+
473
+ if (child != null) {
474
+ node.appendChild(child)
475
+ }
476
+ }
477
+
478
+ return node
479
+ }
480
+
481
+ return h
482
+ }
483
+
484
+ export const $h = _h()
485
+
486
+ export const $in = (/** @type {any} */ key, /** @type {any} */ ref) =>
487
+ ($is(String, key) || $is(Number, key) || $is(Symbol, key)) && Object.hasOwnProperty.call(ref ?? Object, key)
488
+
489
+ /**
490
+ * @template {abstract new (...args: any[]) => any} T
491
+ *
492
+ * @type {{
493
+ * (type: BigIntConstructor, arg: any): arg is bigint;
494
+ * (type: BooleanConstructor, arg: any): arg is boolean;
495
+ * (type: NumberConstructor, arg: any): arg is number;
496
+ * (type: StringConstructor, arg: any): arg is string;
497
+ * (type: SymbolConstructor, arg: any): arg is symbol;
498
+ * (type: undefined, arg: any): arg is undefined | null;
499
+ * <T extends abstract new (...args: any[]) => any>(type: T, arg: any): arg is InstanceType<T>;
500
+ * }}
501
+ *
502
+ * @returns {arg is bigint | boolean | number | string | symbol | undefined | null | InstanceType<T>}
503
+ */
504
+ export const $is = (/** @type {T} */ type, /** @type {any} */ arg) => arg?.constructor === type
505
+
506
+ const _locale = (
507
+ /** @type {Record<string, Record<string, string | Record<string, string>>>} */ locales,
508
+ /** @type {string} */ language,
509
+ /** @type {string} */ text,
510
+ /** @type {string} */ version
511
+ ) => {
512
+ // @ts-expect-error
513
+ const v = locales?.[language]?.[version]?.[text]
514
+
515
+ if ($is(String, v)) {
516
+ return v
517
+ }
518
+
519
+ const t = locales?.[language]?.[text]
520
+
521
+ return $is(String, t) ? t : text
522
+ }
523
+
524
+ export const $locale = (
525
+ /** @type {Record<string, Record<string, string | Record<string, string>>>} */ locales,
526
+ /** @type {string} */ defaultLanguage,
527
+ languages = navigator.languages
528
+ ) => {
529
+ for (const language of languages) {
530
+ if ($in(language, locales)) {
531
+ // @ts-expect-error
532
+ return _locale.bind(0, locales, language)
533
+ }
534
+ }
535
+
536
+ // @ts-expect-error
537
+ return _locale.bind(0, locales, defaultLanguage)
538
+ }
539
+
540
+ export const $nanolight = (
541
+ /** @type {RegExp} */ pattern,
542
+ /** @type {((chunk: string, index: number) => $HArgs[1])[]} */ highlighters,
543
+ /** @type {string} */ code
544
+ ) => {
545
+ const /** @type {$HArgs[1][]} */ result = []
546
+
547
+ code.split(pattern).forEach((chunk, index) => {
548
+ index %= highlighters.length
549
+ if (chunk != null) {
550
+ result.push(highlighters[index](chunk, index))
551
+ }
552
+ })
553
+
554
+ return result
555
+ }
556
+
557
+ // @ts-expect-error
558
+ export const $nanolightJs = $nanolight.bind(0,
559
+ /('.*?'|".*?"|`[\s\S]*?`)|(\/\/.*?\n|\/\*[\s\S]*?\*\/)|(break|case|catch|const|continue|debugger|default|delete|do|else|eval|export\s+type|export|extends|false|finally|for|from|function|goto|if|import|in|instanceof|is|keyof|let|NaN|new|null|package|return|super|switch|this|throw|true|try|typeof|undefined|var|void|while|with|yield)(?!\w)|([<>=.?:&|!~*/%+-])|(0x[\dabcdef]+|0o[01234567]+|0b[01]+|\d+(?:\.[\d_]+)?(?:e[+-]?[\d_]+)?)|([$\w]+)(?=\()|([$\wąćęłńóśżźĄĆĘŁŃÓŚŻŹ]+)/,
560
+ [
561
+ chunk => chunk, // no match
562
+ chunk => ['u', chunk], // string literals
563
+ chunk => ['em', chunk], // comments
564
+ chunk => ['b', chunk], // keywords
565
+ chunk => ['b', chunk], // operators
566
+ chunk => ['u', chunk], // number literals
567
+ chunk => ['u', chunk], // function calls
568
+ chunk => ['i', chunk] // literals
569
+ ]
570
+ )
571
+
572
+ export const $plUral = (
573
+ /** @type {string} */ singular,
574
+ /** @type {string} */ plural2,
575
+ /** @type {string} */ plural5,
576
+ /** @type {number} */ value
577
+ ) => {
578
+ const absValue = Math.abs(value)
579
+ const absValueMod10 = absValue % 10
580
+
581
+ return value === 1
582
+ ? singular
583
+ : absValue === 12 || absValue === 13 || absValue === 14
584
+ ? plural5
585
+ : absValueMod10 === 2 || absValueMod10 === 3 || absValueMod10 === 4
586
+ ? plural2
587
+ : plural5
588
+ }
589
+
590
+ export const $s = _h('http://www.w3.org/2000/svg')
591
+
592
+ export const $set = (
593
+ /** @type {any} */ value,
594
+ /** @type {any} */ ref,
595
+ /** @type {(string | number | symbol)[]} */ ...keys
596
+ ) => {
597
+ const last = keys.length - 1
598
+
599
+ if (last < 0) {
600
+ return
601
+ }
602
+
603
+ for (let k = 0; k < last; ++k) {
604
+ const key = keys[k]
605
+
606
+ if (!$in(key, ref)) {
607
+ ref[key] = $is(Number, keys[k + 1]) ? [] : {}
608
+ }
609
+
610
+ ref = ref[key]
611
+ }
612
+
613
+ ref[keys[last]] = value
614
+ }
615
+
616
+ const _ZEROS = '0'.repeat(16)
617
+ let _counter = 0
618
+
619
+ export const $uuid1 = ({
620
+ date = new Date(),
621
+ node = Math.random().toString(16).slice(2)
622
+ } = {}) => {
623
+ const time = _ZEROS + (10_000 * (+date + 12_219_292_800_000)).toString(16)
624
+
625
+ _counter = (_counter + 1) & 0x3fff
626
+
627
+ return time.slice(-8).concat(
628
+ '-',
629
+ time.slice(-12, -8),
630
+ // @ts-expect-error
631
+ -1,
632
+ time.slice(-15, -12),
633
+ '-',
634
+ (8 | (_counter >> 12)).toString(16),
635
+ (_ZEROS + (_counter & 0xfff).toString(16)).slice(-3),
636
+ '-',
637
+ (_ZEROS + node).slice(-12))
638
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "author": "Jackens",
3
+ "description": "Jackens’ JavaScript helpers.",
4
+ "homepage": "https://jackens.github.io/doc/",
5
+ "keywords": [
6
+ "CSS-in-JS",
7
+ "DOM",
8
+ "HyperScript",
9
+ "RWD",
10
+ "deepEqual",
11
+ "equal",
12
+ "escape",
13
+ "h",
14
+ "highlight",
15
+ "in",
16
+ "is",
17
+ "locale",
18
+ "localization",
19
+ "nanolight",
20
+ "nnn",
21
+ "translation",
22
+ "typography",
23
+ "uuid",
24
+ "uuid1",
25
+ "uuidv1"
26
+ ],
27
+ "license": "MIT",
28
+ "main": "nnn.js",
29
+ "types": "nnn.d.ts",
30
+ "name": "@jackens/nnn",
31
+ "type": "module",
32
+ "version": "2023.8.17"
33
+ }
package/readme.md ADDED
@@ -0,0 +1,5 @@
1
+ # nnn
2
+
3
+ Jackens’ JavaScript helpers.
4
+
5
+ <https://jackens.github.io/doc/>