cr-static-shared-components 9.9.0
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/CHANGELOG.md +53 -0
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/index.d.ts +58 -0
- package/index.js +36 -0
- package/package.json +49 -0
- package/src/async.js +229 -0
- package/src/crypto.js +216 -0
- package/src/errors.js +79 -0
- package/src/formatting.js +299 -0
- package/src/network.js +216 -0
- package/src/transformation.js +273 -0
- package/src/validation.js +403 -0
package/src/crypto.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cryptography & Hashing Utilities Module
|
|
5
|
+
* Secure hashing, encoding, and basic cryptographic operations
|
|
6
|
+
*
|
|
7
|
+
* @module crypto
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hash data with specified algorithm
|
|
14
|
+
*/
|
|
15
|
+
function hash(data, algorithm = 'sha256', encoding = 'hex') {
|
|
16
|
+
return crypto
|
|
17
|
+
.createHash(algorithm)
|
|
18
|
+
.update(data)
|
|
19
|
+
.digest(encoding);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* MD5 hash
|
|
24
|
+
*/
|
|
25
|
+
function md5(data) {
|
|
26
|
+
return hash(data, 'md5');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* SHA1 hash
|
|
31
|
+
*/
|
|
32
|
+
function sha1(data) {
|
|
33
|
+
return hash(data, 'sha1');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* SHA256 hash
|
|
38
|
+
*/
|
|
39
|
+
function sha256(data) {
|
|
40
|
+
return hash(data, 'sha256');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* SHA512 hash
|
|
45
|
+
*/
|
|
46
|
+
function sha512(data) {
|
|
47
|
+
return hash(data, 'sha512');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* HMAC with specified algorithm
|
|
52
|
+
*/
|
|
53
|
+
function hmac(data, secret, algorithm = 'sha256', encoding = 'hex') {
|
|
54
|
+
return crypto
|
|
55
|
+
.createHmac(algorithm, secret)
|
|
56
|
+
.update(data)
|
|
57
|
+
.digest(encoding);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generate random bytes
|
|
62
|
+
*/
|
|
63
|
+
function randomBytes(size = 16, encoding = 'hex') {
|
|
64
|
+
return crypto.randomBytes(size).toString(encoding);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generate UUID v4
|
|
69
|
+
*/
|
|
70
|
+
function uuid() {
|
|
71
|
+
return randomBytes(16, 'hex').replace(
|
|
72
|
+
/^(.{8})(.{4})(.{4})(.{4})(.{12})$/,
|
|
73
|
+
'$1-$2-$3-$4-$5'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generate random string
|
|
79
|
+
*/
|
|
80
|
+
function randomString(length = 32, charset = 'alphanumeric') {
|
|
81
|
+
const charsets = {
|
|
82
|
+
alphanumeric: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
|
|
83
|
+
alpha: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
|
84
|
+
numeric: '0123456789',
|
|
85
|
+
hex: '0123456789abcdef'
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const chars = charsets[charset] || charsets.alphanumeric;
|
|
89
|
+
let result = '';
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < length; i++) {
|
|
92
|
+
const randomIndex = crypto.randomInt(0, chars.length);
|
|
93
|
+
result += chars[randomIndex];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Encrypt data with AES-256-CBC
|
|
101
|
+
*/
|
|
102
|
+
function encrypt(data, key) {
|
|
103
|
+
const iv = crypto.randomBytes(16);
|
|
104
|
+
const keyBuffer = crypto.scryptSync(key, 'salt', 32);
|
|
105
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', keyBuffer, iv);
|
|
106
|
+
|
|
107
|
+
let encrypted = cipher.update(data, 'utf8', 'hex');
|
|
108
|
+
encrypted += cipher.final('hex');
|
|
109
|
+
|
|
110
|
+
return `${iv.toString('hex')}:${encrypted}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Decrypt data with AES-256-CBC
|
|
115
|
+
*/
|
|
116
|
+
function decrypt(encrypted, key) {
|
|
117
|
+
const [ivHex, encryptedData] = encrypted.split(':');
|
|
118
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
119
|
+
const keyBuffer = crypto.scryptSync(key, 'salt', 32);
|
|
120
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', keyBuffer, iv);
|
|
121
|
+
|
|
122
|
+
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
|
|
123
|
+
decrypted += decipher.final('utf8');
|
|
124
|
+
|
|
125
|
+
return decrypted;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Base64 encode
|
|
130
|
+
*/
|
|
131
|
+
function base64Encode(data) {
|
|
132
|
+
return Buffer.from(data).toString('base64');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Base64 decode
|
|
137
|
+
*/
|
|
138
|
+
function base64Decode(data) {
|
|
139
|
+
return Buffer.from(data, 'base64').toString('utf8');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* URL-safe Base64 encode
|
|
144
|
+
*/
|
|
145
|
+
function base64URLEncode(data) {
|
|
146
|
+
return base64Encode(data)
|
|
147
|
+
.replace(/\+/g, '-')
|
|
148
|
+
.replace(/\//g, '_')
|
|
149
|
+
.replace(/=+$/, '');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* URL-safe Base64 decode
|
|
154
|
+
*/
|
|
155
|
+
function base64URLDecode(data) {
|
|
156
|
+
let padded = data.replace(/-/g, '+').replace(/_/g, '/');
|
|
157
|
+
while (padded.length % 4) {
|
|
158
|
+
padded += '=';
|
|
159
|
+
}
|
|
160
|
+
return base64Decode(padded);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Constant-time string comparison
|
|
165
|
+
*/
|
|
166
|
+
function timingSafeEqual(a, b) {
|
|
167
|
+
if (a.length !== b.length) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const bufA = Buffer.from(a);
|
|
172
|
+
const bufB = Buffer.from(b);
|
|
173
|
+
|
|
174
|
+
return crypto.timingSafeEqual(bufA, bufB);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Hash password with bcrypt-like algorithm (PBKDF2)
|
|
179
|
+
*/
|
|
180
|
+
function hashPassword(password, salt = null, iterations = 100000) {
|
|
181
|
+
const saltBuffer = salt ? Buffer.from(salt, 'hex') : crypto.randomBytes(16);
|
|
182
|
+
const hash = crypto.pbkdf2Sync(password, saltBuffer, iterations, 64, 'sha512');
|
|
183
|
+
|
|
184
|
+
return `${saltBuffer.toString('hex')}:${hash.toString('hex')}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Verify password hash
|
|
189
|
+
*/
|
|
190
|
+
function verifyPassword(password, hashedPassword) {
|
|
191
|
+
const [salt, hash] = hashedPassword.split(':');
|
|
192
|
+
const verifyHash = crypto.pbkdf2Sync(password, Buffer.from(salt, 'hex'), 100000, 64, 'sha512');
|
|
193
|
+
|
|
194
|
+
return timingSafeEqual(hash, verifyHash.toString('hex'));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
hash,
|
|
199
|
+
md5,
|
|
200
|
+
sha1,
|
|
201
|
+
sha256,
|
|
202
|
+
sha512,
|
|
203
|
+
hmac,
|
|
204
|
+
randomBytes,
|
|
205
|
+
uuid,
|
|
206
|
+
randomString,
|
|
207
|
+
encrypt,
|
|
208
|
+
decrypt,
|
|
209
|
+
base64Encode,
|
|
210
|
+
base64Decode,
|
|
211
|
+
base64URLEncode,
|
|
212
|
+
base64URLDecode,
|
|
213
|
+
timingSafeEqual,
|
|
214
|
+
hashPassword,
|
|
215
|
+
verifyPassword
|
|
216
|
+
};
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error Handling Module
|
|
5
|
+
* Provides structured error types and handling utilities
|
|
6
|
+
*
|
|
7
|
+
* @module errors
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validation Error
|
|
12
|
+
*/
|
|
13
|
+
class ValidationError extends Error {
|
|
14
|
+
constructor(message, errors = []) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'ValidationError';
|
|
17
|
+
this.errors = errors;
|
|
18
|
+
Error.captureStackTrace(this, ValidationError);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format Error
|
|
24
|
+
*/
|
|
25
|
+
class FormatError extends Error {
|
|
26
|
+
constructor(message, value) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'FormatError';
|
|
29
|
+
this.value = value;
|
|
30
|
+
Error.captureStackTrace(this, FormatError);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Transform Error
|
|
36
|
+
*/
|
|
37
|
+
class TransformError extends Error {
|
|
38
|
+
constructor(message, input) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'TransformError';
|
|
41
|
+
this.input = input;
|
|
42
|
+
Error.captureStackTrace(this, TransformError);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Error handler
|
|
48
|
+
*/
|
|
49
|
+
function handle(error) {
|
|
50
|
+
const response = {
|
|
51
|
+
code: error.name || 'Error',
|
|
52
|
+
message: error.message
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (error instanceof ValidationError) {
|
|
56
|
+
response.errors = error.errors;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (error instanceof FormatError) {
|
|
60
|
+
response.value = error.value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (error instanceof TransformError) {
|
|
64
|
+
response.input = error.input;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
68
|
+
response.stack = error.stack;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return response;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
ValidationError,
|
|
76
|
+
FormatError,
|
|
77
|
+
TransformError,
|
|
78
|
+
handle
|
|
79
|
+
};
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Formatting Module
|
|
5
|
+
* Provides utilities for formatting dates, numbers, currency, and other data types
|
|
6
|
+
*
|
|
7
|
+
* @module formatting
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Date formatting with timezone support
|
|
12
|
+
*/
|
|
13
|
+
function date(input, pattern = 'YYYY-MM-DD', timezone = null) {
|
|
14
|
+
const d = input instanceof Date ? input : new Date(input);
|
|
15
|
+
|
|
16
|
+
if (isNaN(d.getTime())) {
|
|
17
|
+
throw new Error('Invalid date input');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Timezone offset adjustment
|
|
21
|
+
let workingDate = new Date(d);
|
|
22
|
+
if (timezone) {
|
|
23
|
+
// Simple timezone offset (real implementation would use Intl.DateTimeFormat)
|
|
24
|
+
const offset = getTimezoneOffset(timezone);
|
|
25
|
+
workingDate = new Date(d.getTime() + offset * 60000);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const tokens = {
|
|
29
|
+
'YYYY': workingDate.getFullYear(),
|
|
30
|
+
'YY': String(workingDate.getFullYear()).slice(-2),
|
|
31
|
+
'MM': String(workingDate.getMonth() + 1).padStart(2, '0'),
|
|
32
|
+
'M': workingDate.getMonth() + 1,
|
|
33
|
+
'DD': String(workingDate.getDate()).padStart(2, '0'),
|
|
34
|
+
'D': workingDate.getDate(),
|
|
35
|
+
'HH': String(workingDate.getHours()).padStart(2, '0'),
|
|
36
|
+
'H': workingDate.getHours(),
|
|
37
|
+
'hh': String(workingDate.getHours() % 12 || 12).padStart(2, '0'),
|
|
38
|
+
'h': workingDate.getHours() % 12 || 12,
|
|
39
|
+
'mm': String(workingDate.getMinutes()).padStart(2, '0'),
|
|
40
|
+
'm': workingDate.getMinutes(),
|
|
41
|
+
'ss': String(workingDate.getSeconds()).padStart(2, '0'),
|
|
42
|
+
's': workingDate.getSeconds(),
|
|
43
|
+
'SSS': String(workingDate.getMilliseconds()).padStart(3, '0'),
|
|
44
|
+
'A': workingDate.getHours() >= 12 ? 'PM' : 'AM',
|
|
45
|
+
'a': workingDate.getHours() >= 12 ? 'pm' : 'am'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
let formatted = pattern;
|
|
49
|
+
for (const [token, value] of Object.entries(tokens)) {
|
|
50
|
+
formatted = formatted.replace(new RegExp(token, 'g'), String(value));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return formatted;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get timezone offset (simplified)
|
|
58
|
+
*/
|
|
59
|
+
function getTimezoneOffset(timezone) {
|
|
60
|
+
// Simplified mapping - real implementation would use proper timezone database
|
|
61
|
+
const offsets = {
|
|
62
|
+
'UTC': 0,
|
|
63
|
+
'America/New_York': -300,
|
|
64
|
+
'America/Chicago': -360,
|
|
65
|
+
'America/Denver': -420,
|
|
66
|
+
'America/Los_Angeles': -480,
|
|
67
|
+
'Europe/London': 0,
|
|
68
|
+
'Europe/Paris': 60,
|
|
69
|
+
'Asia/Tokyo': 540,
|
|
70
|
+
'Australia/Sydney': 660
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return offsets[timezone] || 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Currency formatting
|
|
78
|
+
*/
|
|
79
|
+
function currency(amount, currencyCode = 'USD', locale = 'en-US') {
|
|
80
|
+
if (typeof amount !== 'number' || isNaN(amount)) {
|
|
81
|
+
throw new Error('Invalid amount');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
return new Intl.NumberFormat(locale, {
|
|
86
|
+
style: 'currency',
|
|
87
|
+
currency: currencyCode
|
|
88
|
+
}).format(amount);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// Fallback
|
|
91
|
+
const symbols = {
|
|
92
|
+
'USD': '$',
|
|
93
|
+
'EUR': '€',
|
|
94
|
+
'GBP': '£',
|
|
95
|
+
'JPY': '¥',
|
|
96
|
+
'CNY': '¥'
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const symbol = symbols[currencyCode] || currencyCode;
|
|
100
|
+
return `${symbol}${amount.toFixed(2)}`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Number formatting
|
|
106
|
+
*/
|
|
107
|
+
function number(value, options = {}) {
|
|
108
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
109
|
+
throw new Error('Invalid number');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const defaults = {
|
|
113
|
+
precision: 2,
|
|
114
|
+
grouping: true,
|
|
115
|
+
percentage: false
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const opts = { ...defaults, ...options };
|
|
119
|
+
|
|
120
|
+
let formatted = value;
|
|
121
|
+
|
|
122
|
+
if (opts.percentage) {
|
|
123
|
+
formatted = value * 100;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (opts.precision !== null) {
|
|
127
|
+
formatted = formatted.toFixed(opts.precision);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (opts.grouping) {
|
|
131
|
+
const parts = String(formatted).split('.');
|
|
132
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
133
|
+
formatted = parts.join('.');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (opts.percentage) {
|
|
137
|
+
formatted += '%';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return formatted;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Bytes formatting
|
|
145
|
+
*/
|
|
146
|
+
function bytes(bytesValue, decimals = 2) {
|
|
147
|
+
if (typeof bytesValue !== 'number' || bytesValue < 0) {
|
|
148
|
+
throw new Error('Invalid bytes value');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (bytesValue === 0) return '0 Bytes';
|
|
152
|
+
|
|
153
|
+
const k = 1024;
|
|
154
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
155
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
156
|
+
|
|
157
|
+
const i = Math.floor(Math.log(bytesValue) / Math.log(k));
|
|
158
|
+
|
|
159
|
+
return parseFloat((bytesValue / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Percentage formatting
|
|
164
|
+
*/
|
|
165
|
+
function percentage(value, decimals = 2) {
|
|
166
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
167
|
+
throw new Error('Invalid percentage value');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return (value * 100).toFixed(decimals) + '%';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Ordinal number formatting (1st, 2nd, 3rd, etc.)
|
|
175
|
+
*/
|
|
176
|
+
function ordinal(value) {
|
|
177
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
178
|
+
throw new Error('Invalid ordinal value');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const suffixes = ['th', 'st', 'nd', 'rd'];
|
|
182
|
+
const v = value % 100;
|
|
183
|
+
|
|
184
|
+
return value + (suffixes[(v - 20) % 10] || suffixes[v] || suffixes[0]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Duration formatting (milliseconds to human-readable)
|
|
189
|
+
*/
|
|
190
|
+
function duration(ms) {
|
|
191
|
+
if (typeof ms !== 'number' || ms < 0) {
|
|
192
|
+
throw new Error('Invalid duration');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const units = {
|
|
196
|
+
day: 86400000,
|
|
197
|
+
hour: 3600000,
|
|
198
|
+
minute: 60000,
|
|
199
|
+
second: 1000,
|
|
200
|
+
millisecond: 1
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const parts = [];
|
|
204
|
+
let remaining = ms;
|
|
205
|
+
|
|
206
|
+
for (const [unit, value] of Object.entries(units)) {
|
|
207
|
+
const count = Math.floor(remaining / value);
|
|
208
|
+
if (count > 0) {
|
|
209
|
+
parts.push(`${count} ${unit}${count > 1 ? 's' : ''}`);
|
|
210
|
+
remaining %= value;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return parts.length > 0 ? parts.join(', ') : '0 milliseconds';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Pluralize words
|
|
219
|
+
*/
|
|
220
|
+
function pluralize(count, singular, plural = null) {
|
|
221
|
+
if (typeof count !== 'number') {
|
|
222
|
+
throw new Error('Invalid count');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const word = count === 1 ? singular : (plural || singular + 's');
|
|
226
|
+
return `${count} ${word}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Titlecase formatting
|
|
231
|
+
*/
|
|
232
|
+
function titleCase(str) {
|
|
233
|
+
if (typeof str !== 'string') {
|
|
234
|
+
throw new Error('Invalid string');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const smallWords = ['a', 'an', 'and', 'as', 'at', 'but', 'by', 'for', 'if', 'in', 'of', 'on', 'or', 'the', 'to', 'via'];
|
|
238
|
+
|
|
239
|
+
return str.toLowerCase().split(' ').map((word, index) => {
|
|
240
|
+
if (index === 0 || !smallWords.includes(word)) {
|
|
241
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
242
|
+
}
|
|
243
|
+
return word;
|
|
244
|
+
}).join(' ');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Truncate string with ellipsis
|
|
249
|
+
*/
|
|
250
|
+
function truncate(str, maxLength, suffix = '...') {
|
|
251
|
+
if (typeof str !== 'string') {
|
|
252
|
+
throw new Error('Invalid string');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (str.length <= maxLength) {
|
|
256
|
+
return str;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return str.slice(0, maxLength - suffix.length) + suffix;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Pad string
|
|
264
|
+
*/
|
|
265
|
+
function pad(str, length, char = ' ', direction = 'right') {
|
|
266
|
+
if (typeof str !== 'string') {
|
|
267
|
+
str = String(str);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (str.length >= length) {
|
|
271
|
+
return str;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const padding = char.repeat(length - str.length);
|
|
275
|
+
|
|
276
|
+
if (direction === 'left') {
|
|
277
|
+
return padding + str;
|
|
278
|
+
} else if (direction === 'center') {
|
|
279
|
+
const leftPad = Math.floor(padding.length / 2);
|
|
280
|
+
const rightPad = padding.length - leftPad;
|
|
281
|
+
return char.repeat(leftPad) + str + char.repeat(rightPad);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return str + padding;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = {
|
|
288
|
+
date,
|
|
289
|
+
currency,
|
|
290
|
+
number,
|
|
291
|
+
bytes,
|
|
292
|
+
percentage,
|
|
293
|
+
ordinal,
|
|
294
|
+
duration,
|
|
295
|
+
pluralize,
|
|
296
|
+
titleCase,
|
|
297
|
+
truncate,
|
|
298
|
+
pad
|
|
299
|
+
};
|