@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.
- package/nnn.d.ts +180 -0
- package/nnn.js +638 -0
- package/package.json +33 -0
- 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
|
+
}
|