@20syldev/api 3.5.0 → 4.1.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/.prettierrc +7 -0
- package/README.md +32 -33
- package/dist/app.js +36 -0
- package/dist/app.js.map +1 -0
- package/dist/config/env.js +73 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/versions.js +72 -0
- package/dist/config/versions.js.map +1 -0
- package/dist/constants.js +42 -0
- package/dist/constants.js.map +1 -0
- package/dist/middleware/cors.js +15 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/error.js +6 -0
- package/dist/middleware/error.js.map +1 -0
- package/dist/middleware/json.js +8 -0
- package/dist/middleware/json.js.map +1 -0
- package/dist/middleware/logger.js +26 -0
- package/dist/middleware/logger.js.map +1 -0
- package/dist/middleware/ratelimit.js +58 -0
- package/dist/middleware/ratelimit.js.map +1 -0
- package/dist/middleware/version.js +30 -0
- package/dist/middleware/version.js.map +1 -0
- package/dist/modules/v3/algorithms.js +221 -0
- package/dist/modules/v3/algorithms.js.map +1 -0
- package/{modules → dist/modules}/v3/captcha.js +4 -13
- package/dist/modules/v3/captcha.js.map +1 -0
- package/dist/modules/v3/chat.js +103 -0
- package/dist/modules/v3/chat.js.map +1 -0
- package/{modules → dist/modules}/v3/color.js +12 -13
- package/dist/modules/v3/color.js.map +1 -0
- package/dist/modules/v3/convert.js +41 -0
- package/dist/modules/v3/convert.js.map +1 -0
- package/dist/modules/v3/domain.js +83 -0
- package/dist/modules/v3/domain.js.map +1 -0
- package/dist/modules/v3/hash.js +16 -0
- package/dist/modules/v3/hash.js.map +1 -0
- package/dist/modules/v3/hyperplanning.js +50 -0
- package/dist/modules/v3/hyperplanning.js.map +1 -0
- package/dist/modules/v3/levenshtein.js +38 -0
- package/dist/modules/v3/levenshtein.js.map +1 -0
- package/dist/modules/v3/personal.js +129 -0
- package/dist/modules/v3/personal.js.map +1 -0
- package/dist/modules/v3/qrcode.js +21 -0
- package/dist/modules/v3/qrcode.js.map +1 -0
- package/dist/modules/v3/tic_tac_toe.js +221 -0
- package/dist/modules/v3/tic_tac_toe.js.map +1 -0
- package/dist/modules/v3/time.js +88 -0
- package/dist/modules/v3/time.js.map +1 -0
- package/{modules → dist/modules}/v3/token.js +10 -11
- package/dist/modules/v3/token.js.map +1 -0
- package/dist/modules/v3/username.js +92 -0
- package/dist/modules/v3/username.js.map +1 -0
- package/dist/modules/v3/utils.js +70 -0
- package/dist/modules/v3/utils.js.map +1 -0
- package/dist/modules/v3.js +16 -0
- package/dist/modules/v3.js.map +1 -0
- package/dist/modules/v4/algorithms.js +200 -0
- package/dist/modules/v4/algorithms.js.map +1 -0
- package/dist/modules/v4/captcha.js +35 -0
- package/dist/modules/v4/captcha.js.map +1 -0
- package/dist/modules/v4/chat.js +92 -0
- package/dist/modules/v4/chat.js.map +1 -0
- package/dist/modules/v4/color.js +43 -0
- package/dist/modules/v4/color.js.map +1 -0
- package/dist/modules/v4/convert.js +33 -0
- package/dist/modules/v4/convert.js.map +1 -0
- package/dist/modules/v4/dice.js +30 -0
- package/dist/modules/v4/dice.js.map +1 -0
- package/dist/modules/v4/domain.js +78 -0
- package/dist/modules/v4/domain.js.map +1 -0
- package/dist/modules/v4/encode.js +165 -0
- package/dist/modules/v4/encode.js.map +1 -0
- package/dist/modules/v4/geo.js +41 -0
- package/dist/modules/v4/geo.js.map +1 -0
- package/dist/modules/v4/hash.js +9 -0
- package/dist/modules/v4/hash.js.map +1 -0
- package/dist/modules/v4/hyperplanning.js +42 -0
- package/dist/modules/v4/hyperplanning.js.map +1 -0
- package/dist/modules/v4/levenshtein.js +29 -0
- package/dist/modules/v4/levenshtein.js.map +1 -0
- package/dist/modules/v4/palette.js +89 -0
- package/dist/modules/v4/palette.js.map +1 -0
- package/dist/modules/v4/personal.js +125 -0
- package/dist/modules/v4/personal.js.map +1 -0
- package/dist/modules/v4/placeholder.js +96 -0
- package/dist/modules/v4/placeholder.js.map +1 -0
- package/dist/modules/v4/qrcode.js +12 -0
- package/dist/modules/v4/qrcode.js.map +1 -0
- package/dist/modules/v4/statistics.js +41 -0
- package/dist/modules/v4/statistics.js.map +1 -0
- package/dist/modules/v4/text.js +293 -0
- package/dist/modules/v4/text.js.map +1 -0
- package/dist/modules/v4/tic_tac_toe.js +206 -0
- package/dist/modules/v4/tic_tac_toe.js.map +1 -0
- package/dist/modules/v4/time.js +73 -0
- package/dist/modules/v4/time.js.map +1 -0
- package/dist/modules/v4/token.js +36 -0
- package/dist/modules/v4/token.js.map +1 -0
- package/dist/modules/v4/username.js +87 -0
- package/dist/modules/v4/username.js.map +1 -0
- package/dist/modules/v4/utils.js +26 -0
- package/dist/modules/v4/utils.js.map +1 -0
- package/dist/modules/v4/validate.js +54 -0
- package/dist/modules/v4/validate.js.map +1 -0
- package/dist/modules/v4.js +18 -0
- package/dist/modules/v4.js.map +1 -0
- package/dist/routes/delete.js +65 -0
- package/dist/routes/delete.js.map +1 -0
- package/dist/routes/get.js +441 -0
- package/dist/routes/get.js.map +1 -0
- package/dist/routes/index.js +50 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/patch.js +41 -0
- package/dist/routes/patch.js.map +1 -0
- package/dist/routes/post.js +185 -0
- package/dist/routes/post.js.map +1 -0
- package/dist/storage/index.js +13 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/types/storage.js +2 -0
- package/dist/types/storage.js.map +1 -0
- package/dist/utils/response.js +10 -0
- package/dist/utils/response.js.map +1 -0
- package/dist/utils/version.js +3 -0
- package/dist/utils/version.js.map +1 -0
- package/docs/changelog.md +333 -0
- package/eslint.config.js +20 -0
- package/package.json +31 -15
- package/src/app.ts +48 -0
- package/src/config/env.ts +81 -0
- package/src/config/versions.ts +92 -0
- package/src/constants.ts +47 -0
- package/src/middleware/cors.ts +17 -0
- package/src/middleware/error.ts +7 -0
- package/src/middleware/json.ts +9 -0
- package/src/middleware/logger.ts +31 -0
- package/src/middleware/ratelimit.ts +66 -0
- package/src/middleware/version.ts +39 -0
- package/{modules → src/modules}/v3/algorithms.js +1 -1
- package/src/modules/v3/captcha.js +60 -0
- package/{modules → src/modules}/v3/chat.js +6 -4
- package/src/modules/v3/color.js +60 -0
- package/{modules → src/modules}/v3/convert.js +5 -5
- package/{modules → src/modules}/v3/domain.js +51 -4
- package/{modules → src/modules}/v3/hash.js +1 -1
- package/{modules → src/modules}/v3/hyperplanning.js +11 -10
- package/{modules → src/modules}/v3/levenshtein.js +3 -3
- package/{modules → src/modules}/v3/personal.js +20 -8
- package/{modules → src/modules}/v3/qrcode.js +1 -1
- package/{modules → src/modules}/v3/tic_tac_toe.js +24 -18
- package/{modules → src/modules}/v3/time.js +22 -11
- package/src/modules/v3/token.js +54 -0
- package/src/modules/v3/username.js +94 -0
- package/{modules → src/modules}/v3/utils.js +2 -2
- package/{modules → src/modules}/v3.js +1 -1
- package/src/modules/v4/algorithms.ts +197 -0
- package/src/modules/v4/captcha.ts +49 -0
- package/src/modules/v4/chat.ts +122 -0
- package/src/modules/v4/color.ts +64 -0
- package/src/modules/v4/convert.ts +38 -0
- package/src/modules/v4/dice.ts +39 -0
- package/src/modules/v4/domain.ts +81 -0
- package/src/modules/v4/encode.ts +170 -0
- package/src/modules/v4/geo.ts +53 -0
- package/src/modules/v4/hash.ts +9 -0
- package/src/modules/v4/hyperplanning.ts +58 -0
- package/src/modules/v4/levenshtein.ts +38 -0
- package/src/modules/v4/palette.ts +103 -0
- package/src/modules/v4/personal.ts +147 -0
- package/src/modules/v4/placeholder.ts +122 -0
- package/src/modules/v4/qrcode.ts +11 -0
- package/src/modules/v4/statistics.ts +55 -0
- package/src/modules/v4/text.ts +300 -0
- package/src/modules/v4/tic_tac_toe.ts +255 -0
- package/src/modules/v4/time.ts +92 -0
- package/src/modules/v4/token.ts +43 -0
- package/src/modules/v4/username.ts +89 -0
- package/src/modules/v4/utils.ts +38 -0
- package/src/modules/v4/validate.ts +55 -0
- package/src/modules/v4.ts +17 -0
- package/src/routes/delete.ts +72 -0
- package/src/routes/get.ts +508 -0
- package/src/routes/index.ts +58 -0
- package/src/routes/patch.ts +45 -0
- package/src/routes/post.ts +200 -0
- package/src/storage/index.ts +16 -0
- package/src/types/express.d.ts +17 -0
- package/src/types/storage.ts +48 -0
- package/src/utils/response.ts +11 -0
- package/src/utils/version.ts +5 -0
- package/tests/integration/api.test.ts +395 -0
- package/tests/tsconfig.json +3 -0
- package/tests/unit/algorithms.test.ts +120 -0
- package/tests/unit/color.test.ts +33 -0
- package/tests/unit/convert.test.ts +42 -0
- package/tests/unit/dice.test.ts +57 -0
- package/tests/unit/domain.test.ts +47 -0
- package/tests/unit/encode.test.ts +108 -0
- package/tests/unit/geo.test.ts +45 -0
- package/tests/unit/hash.test.ts +37 -0
- package/tests/unit/hyperplanning.test.ts +77 -0
- package/tests/unit/levenshtein.test.ts +34 -0
- package/tests/unit/palette.test.ts +53 -0
- package/tests/unit/personal.test.ts +65 -0
- package/tests/unit/statistics.test.ts +57 -0
- package/tests/unit/text.test.ts +107 -0
- package/tests/unit/time.test.ts +67 -0
- package/tests/unit/token.test.ts +61 -0
- package/tests/unit/username.test.ts +34 -0
- package/tests/unit/validate.test.ts +71 -0
- package/tsconfig.json +17 -0
- package/tsconfig.test.json +9 -0
- package/app.js +0 -865
- package/modules/v3/username.js +0 -32
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createCanvas } from 'canvas';
|
|
2
|
+
|
|
3
|
+
export interface PlaceholderOptions {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
bg?: string;
|
|
7
|
+
color?: string;
|
|
8
|
+
text?: string;
|
|
9
|
+
rows?: number;
|
|
10
|
+
avatar?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseSize(value: string | undefined, name: string, def: number): number {
|
|
14
|
+
if (value === undefined) return def;
|
|
15
|
+
const n = Number(value);
|
|
16
|
+
if (isNaN(n)) throw new Error(`${name} must be a number`);
|
|
17
|
+
if (n < 1 || n > 4000) throw new Error(`${name} must be between 1 and 4000`);
|
|
18
|
+
return Math.floor(n);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeColor(value: string | undefined, def: string): string {
|
|
22
|
+
if (!value) return def;
|
|
23
|
+
const clean = value.startsWith('#') ? value : `#${value}`;
|
|
24
|
+
if (!/^#[0-9a-fA-F]{3,6}$/.test(clean)) throw new Error('Invalid color (use hex like #ff6600)');
|
|
25
|
+
return clean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function generateImage(opts: PlaceholderOptions): Buffer {
|
|
29
|
+
const { width, height } = opts;
|
|
30
|
+
const bg = normalizeColor(opts.bg, '#cccccc');
|
|
31
|
+
const color = normalizeColor(opts.color, '#333333');
|
|
32
|
+
const text = opts.text ?? `${width}×${height}`;
|
|
33
|
+
|
|
34
|
+
const canvas = createCanvas(width, height);
|
|
35
|
+
const ctx = canvas.getContext('2d');
|
|
36
|
+
|
|
37
|
+
ctx.fillStyle = bg;
|
|
38
|
+
ctx.fillRect(0, 0, width, height);
|
|
39
|
+
|
|
40
|
+
const fontSize = Math.max(12, Math.min(width, height) / 8);
|
|
41
|
+
ctx.font = `bold ${fontSize}px sans-serif`;
|
|
42
|
+
ctx.fillStyle = color;
|
|
43
|
+
ctx.textAlign = 'center';
|
|
44
|
+
ctx.textBaseline = 'middle';
|
|
45
|
+
ctx.fillText(text, width / 2, height / 2);
|
|
46
|
+
|
|
47
|
+
return canvas.toBuffer('image/png');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function generateSkeleton(opts: PlaceholderOptions): string {
|
|
51
|
+
const { width, height } = opts;
|
|
52
|
+
const bg = normalizeColor(opts.bg, '#e2e8f0');
|
|
53
|
+
const shimmer = normalizeColor(opts.color, '#f1f5f9');
|
|
54
|
+
const rows = Math.max(1, Math.min(20, opts.rows ?? 3));
|
|
55
|
+
const avatar = !!opts.avatar;
|
|
56
|
+
|
|
57
|
+
const padding = Math.min(width, height) * 0.05;
|
|
58
|
+
const avatarSize = avatar ? Math.min(width, height) * 0.2 : 0;
|
|
59
|
+
const lineHeight = (height - padding * 2 - avatarSize - (avatar ? padding : 0)) / rows;
|
|
60
|
+
const lineThickness = Math.max(8, lineHeight * 0.5);
|
|
61
|
+
const radius = lineThickness / 2;
|
|
62
|
+
|
|
63
|
+
const lines: string[] = [];
|
|
64
|
+
const startY = padding + (avatar ? avatarSize + padding : 0);
|
|
65
|
+
for (let i = 0; i < rows; i++) {
|
|
66
|
+
const y = startY + i * lineHeight + (lineHeight - lineThickness) / 2;
|
|
67
|
+
const lineWidth = (width - padding * 2) * (i === rows - 1 ? 0.6 : 0.95);
|
|
68
|
+
lines.push(
|
|
69
|
+
`<rect x="${padding}" y="${y}" width="${lineWidth}" height="${lineThickness}" rx="${radius}" fill="url(#shimmer)" />`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const avatarShape = avatar
|
|
74
|
+
? `<circle cx="${padding + avatarSize / 2}" cy="${padding + avatarSize / 2}" r="${avatarSize / 2}" fill="url(#shimmer)" />`
|
|
75
|
+
: '';
|
|
76
|
+
|
|
77
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
|
78
|
+
<defs>
|
|
79
|
+
<linearGradient id="shimmer" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
80
|
+
<stop offset="0%" stop-color="${bg}">
|
|
81
|
+
<animate attributeName="offset" values="-2; 1" dur="1.5s" repeatCount="indefinite" />
|
|
82
|
+
</stop>
|
|
83
|
+
<stop offset="50%" stop-color="${shimmer}">
|
|
84
|
+
<animate attributeName="offset" values="-1.5; 1.5" dur="1.5s" repeatCount="indefinite" />
|
|
85
|
+
</stop>
|
|
86
|
+
<stop offset="100%" stop-color="${bg}">
|
|
87
|
+
<animate attributeName="offset" values="-1; 2" dur="1.5s" repeatCount="indefinite" />
|
|
88
|
+
</stop>
|
|
89
|
+
</linearGradient>
|
|
90
|
+
</defs>
|
|
91
|
+
<rect width="${width}" height="${height}" fill="${bg}" />
|
|
92
|
+
${avatarShape}
|
|
93
|
+
${lines.join('\n ')}
|
|
94
|
+
</svg>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface PlaceholderResult {
|
|
98
|
+
type: string;
|
|
99
|
+
contentType: string;
|
|
100
|
+
body: Buffer | string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default function placeholder(type: string, query: Record<string, string | undefined>): PlaceholderResult {
|
|
104
|
+
const opts: PlaceholderOptions = {
|
|
105
|
+
width: parseSize(query.width, 'width', 800),
|
|
106
|
+
height: parseSize(query.height, 'height', 600),
|
|
107
|
+
bg: query.bg,
|
|
108
|
+
color: query.color,
|
|
109
|
+
text: query.text,
|
|
110
|
+
rows: query.rows ? parseInt(query.rows, 10) : undefined,
|
|
111
|
+
avatar: query.avatar === 'true' || query.avatar === '1',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
switch (type) {
|
|
115
|
+
case 'image':
|
|
116
|
+
return { type, contentType: 'image/png', body: generateImage(opts) };
|
|
117
|
+
case 'skeleton':
|
|
118
|
+
return { type, contentType: 'image/svg+xml', body: generateSkeleton(opts) };
|
|
119
|
+
default:
|
|
120
|
+
throw new Error('Type must be one of: image, skeleton');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { toDataURL } from 'qrcode';
|
|
2
|
+
|
|
3
|
+
export default async function qrcode(url: string): Promise<string> {
|
|
4
|
+
if (!url || typeof url !== 'string') throw new Error('Please provide a valid URL');
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
return await toDataURL(url);
|
|
8
|
+
} catch (error) {
|
|
9
|
+
throw new Error(`Failed to generate QR code: ${(error as Error).message}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { MAX_STRING_LENGTH } from '../../constants.js';
|
|
2
|
+
|
|
3
|
+
export interface StatisticsResult {
|
|
4
|
+
count: number;
|
|
5
|
+
sum: number;
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
range: number;
|
|
9
|
+
mean: number;
|
|
10
|
+
median: number;
|
|
11
|
+
mode: number[];
|
|
12
|
+
variance: number;
|
|
13
|
+
stddev: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function statistics(values: string): StatisticsResult {
|
|
17
|
+
if (!values) throw new Error('A list of values is required');
|
|
18
|
+
if (typeof values !== 'string') throw new Error('Values must be a comma-separated string');
|
|
19
|
+
|
|
20
|
+
const arr = values.split(',').map((v) => Number(v.trim()));
|
|
21
|
+
|
|
22
|
+
if (arr.some(isNaN)) throw new Error('Values must contain only numbers');
|
|
23
|
+
if (arr.length < 1) throw new Error('At least one value is required');
|
|
24
|
+
if (arr.length > MAX_STRING_LENGTH) throw new Error(`Cannot process more than ${MAX_STRING_LENGTH} values`);
|
|
25
|
+
|
|
26
|
+
const count = arr.length;
|
|
27
|
+
const sum = arr.reduce((a, b) => a + b, 0);
|
|
28
|
+
const min = Math.min(...arr);
|
|
29
|
+
const max = Math.max(...arr);
|
|
30
|
+
const mean = sum / count;
|
|
31
|
+
|
|
32
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
33
|
+
const median = count % 2 === 0 ? (sorted[count / 2 - 1]! + sorted[count / 2]!) / 2 : sorted[Math.floor(count / 2)]!;
|
|
34
|
+
|
|
35
|
+
const counts = new Map<number, number>();
|
|
36
|
+
for (const n of arr) counts.set(n, (counts.get(n) ?? 0) + 1);
|
|
37
|
+
const maxCount = Math.max(...counts.values());
|
|
38
|
+
const mode = maxCount === 1 ? [] : [...counts.entries()].filter(([, c]) => c === maxCount).map(([n]) => n);
|
|
39
|
+
|
|
40
|
+
const variance = arr.reduce((acc, n) => acc + (n - mean) ** 2, 0) / count;
|
|
41
|
+
const stddev = Math.sqrt(variance);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
count,
|
|
45
|
+
sum: +sum.toFixed(6),
|
|
46
|
+
min,
|
|
47
|
+
max,
|
|
48
|
+
range: max - min,
|
|
49
|
+
mean: +mean.toFixed(6),
|
|
50
|
+
median: +median.toFixed(6),
|
|
51
|
+
mode,
|
|
52
|
+
variance: +variance.toFixed(6),
|
|
53
|
+
stddev: +stddev.toFixed(6),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { MAX_STRING_LENGTH } from '../../constants.js';
|
|
2
|
+
|
|
3
|
+
const LOREM_WORDS = [
|
|
4
|
+
'lorem',
|
|
5
|
+
'ipsum',
|
|
6
|
+
'dolor',
|
|
7
|
+
'sit',
|
|
8
|
+
'amet',
|
|
9
|
+
'consectetur',
|
|
10
|
+
'adipiscing',
|
|
11
|
+
'elit',
|
|
12
|
+
'sed',
|
|
13
|
+
'do',
|
|
14
|
+
'eiusmod',
|
|
15
|
+
'tempor',
|
|
16
|
+
'incididunt',
|
|
17
|
+
'ut',
|
|
18
|
+
'labore',
|
|
19
|
+
'et',
|
|
20
|
+
'dolore',
|
|
21
|
+
'magna',
|
|
22
|
+
'aliqua',
|
|
23
|
+
'enim',
|
|
24
|
+
'ad',
|
|
25
|
+
'minim',
|
|
26
|
+
'veniam',
|
|
27
|
+
'quis',
|
|
28
|
+
'nostrud',
|
|
29
|
+
'exercitation',
|
|
30
|
+
'ullamco',
|
|
31
|
+
'laboris',
|
|
32
|
+
'nisi',
|
|
33
|
+
'aliquip',
|
|
34
|
+
'ex',
|
|
35
|
+
'ea',
|
|
36
|
+
'commodo',
|
|
37
|
+
'consequat',
|
|
38
|
+
'duis',
|
|
39
|
+
'aute',
|
|
40
|
+
'irure',
|
|
41
|
+
'in',
|
|
42
|
+
'reprehenderit',
|
|
43
|
+
'voluptate',
|
|
44
|
+
'velit',
|
|
45
|
+
'esse',
|
|
46
|
+
'cillum',
|
|
47
|
+
'eu',
|
|
48
|
+
'fugiat',
|
|
49
|
+
'nulla',
|
|
50
|
+
'pariatur',
|
|
51
|
+
'excepteur',
|
|
52
|
+
'sint',
|
|
53
|
+
'occaecat',
|
|
54
|
+
'cupidatat',
|
|
55
|
+
'non',
|
|
56
|
+
'proident',
|
|
57
|
+
'sunt',
|
|
58
|
+
'culpa',
|
|
59
|
+
'qui',
|
|
60
|
+
'officia',
|
|
61
|
+
'deserunt',
|
|
62
|
+
'mollit',
|
|
63
|
+
'anim',
|
|
64
|
+
'id',
|
|
65
|
+
'est',
|
|
66
|
+
'laborum',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const FR_UNITS = [
|
|
70
|
+
'zéro',
|
|
71
|
+
'un',
|
|
72
|
+
'deux',
|
|
73
|
+
'trois',
|
|
74
|
+
'quatre',
|
|
75
|
+
'cinq',
|
|
76
|
+
'six',
|
|
77
|
+
'sept',
|
|
78
|
+
'huit',
|
|
79
|
+
'neuf',
|
|
80
|
+
'dix',
|
|
81
|
+
'onze',
|
|
82
|
+
'douze',
|
|
83
|
+
'treize',
|
|
84
|
+
'quatorze',
|
|
85
|
+
'quinze',
|
|
86
|
+
'seize',
|
|
87
|
+
'dix-sept',
|
|
88
|
+
'dix-huit',
|
|
89
|
+
'dix-neuf',
|
|
90
|
+
];
|
|
91
|
+
const FR_TENS = [
|
|
92
|
+
'',
|
|
93
|
+
'',
|
|
94
|
+
'vingt',
|
|
95
|
+
'trente',
|
|
96
|
+
'quarante',
|
|
97
|
+
'cinquante',
|
|
98
|
+
'soixante',
|
|
99
|
+
'soixante',
|
|
100
|
+
'quatre-vingt',
|
|
101
|
+
'quatre-vingt',
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const EN_UNITS = [
|
|
105
|
+
'zero',
|
|
106
|
+
'one',
|
|
107
|
+
'two',
|
|
108
|
+
'three',
|
|
109
|
+
'four',
|
|
110
|
+
'five',
|
|
111
|
+
'six',
|
|
112
|
+
'seven',
|
|
113
|
+
'eight',
|
|
114
|
+
'nine',
|
|
115
|
+
'ten',
|
|
116
|
+
'eleven',
|
|
117
|
+
'twelve',
|
|
118
|
+
'thirteen',
|
|
119
|
+
'fourteen',
|
|
120
|
+
'fifteen',
|
|
121
|
+
'sixteen',
|
|
122
|
+
'seventeen',
|
|
123
|
+
'eighteen',
|
|
124
|
+
'nineteen',
|
|
125
|
+
];
|
|
126
|
+
const EN_TENS = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
|
|
127
|
+
|
|
128
|
+
function checkText(value: string): void {
|
|
129
|
+
if (!value) throw new Error('A value is required');
|
|
130
|
+
if (typeof value !== 'string') throw new Error('Value must be a string');
|
|
131
|
+
if (value.length > MAX_STRING_LENGTH)
|
|
132
|
+
throw new Error(`Value must be less than ${MAX_STRING_LENGTH} characters long`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface TextStats {
|
|
136
|
+
characters: number;
|
|
137
|
+
charactersNoSpaces: number;
|
|
138
|
+
words: number;
|
|
139
|
+
sentences: number;
|
|
140
|
+
paragraphs: number;
|
|
141
|
+
readingTime: string;
|
|
142
|
+
mostFrequentChar: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function stats(value: string): TextStats {
|
|
146
|
+
checkText(value);
|
|
147
|
+
|
|
148
|
+
const characters = value.length;
|
|
149
|
+
const charactersNoSpaces = value.replace(/\s/g, '').length;
|
|
150
|
+
const words = value.trim().split(/\s+/).filter(Boolean).length;
|
|
151
|
+
const sentences = value.split(/[.!?]+/).filter((s) => s.trim().length > 0).length;
|
|
152
|
+
const paragraphs = value.split(/\n\s*\n/).filter((p) => p.trim().length > 0).length || 1;
|
|
153
|
+
|
|
154
|
+
const minutes = words / 200;
|
|
155
|
+
const readingTime = minutes < 1 ? `${Math.ceil(minutes * 60)}s` : `${Math.round(minutes)}min`;
|
|
156
|
+
|
|
157
|
+
const counts = new Map<string, number>();
|
|
158
|
+
for (const c of value.replace(/\s/g, '').toLowerCase()) counts.set(c, (counts.get(c) ?? 0) + 1);
|
|
159
|
+
let mostFrequentChar = '';
|
|
160
|
+
let maxCount = 0;
|
|
161
|
+
for (const [char, count] of counts) {
|
|
162
|
+
if (count > maxCount) {
|
|
163
|
+
mostFrequentChar = char;
|
|
164
|
+
maxCount = count;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { characters, charactersNoSpaces, words, sentences, paragraphs, readingTime, mostFrequentChar };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function slug(value: string): string {
|
|
172
|
+
checkText(value);
|
|
173
|
+
return value
|
|
174
|
+
.normalize('NFD')
|
|
175
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
176
|
+
.toLowerCase()
|
|
177
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
178
|
+
.trim()
|
|
179
|
+
.replace(/[\s-]+/g, '-');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function lorem(type: string, count: string): string {
|
|
183
|
+
const n = Number(count) || 5;
|
|
184
|
+
if (n < 1 || n > 500) throw new Error('Count must be between 1 and 500');
|
|
185
|
+
|
|
186
|
+
const randomWord = (): string => LOREM_WORDS[Math.floor(Math.random() * LOREM_WORDS.length)]!;
|
|
187
|
+
const randomSentence = (): string => {
|
|
188
|
+
const length = 5 + Math.floor(Math.random() * 10);
|
|
189
|
+
const words = Array.from({ length }, randomWord);
|
|
190
|
+
words[0] = words[0]!.charAt(0).toUpperCase() + words[0]!.slice(1);
|
|
191
|
+
return words.join(' ') + '.';
|
|
192
|
+
};
|
|
193
|
+
const randomParagraph = (): string => {
|
|
194
|
+
const length = 3 + Math.floor(Math.random() * 5);
|
|
195
|
+
return Array.from({ length }, randomSentence).join(' ');
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
switch (type) {
|
|
199
|
+
case 'words':
|
|
200
|
+
return Array.from({ length: n }, randomWord).join(' ');
|
|
201
|
+
case 'sentences':
|
|
202
|
+
return Array.from({ length: n }, randomSentence).join(' ');
|
|
203
|
+
case 'paragraphs':
|
|
204
|
+
return Array.from({ length: n }, randomParagraph).join('\n\n');
|
|
205
|
+
default:
|
|
206
|
+
throw new Error('Type must be one of: words, sentences, paragraphs');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function frBelowHundred(n: number): string {
|
|
211
|
+
if (n < 20) return FR_UNITS[n]!;
|
|
212
|
+
const t = Math.floor(n / 10);
|
|
213
|
+
const u = n % 10;
|
|
214
|
+
if (t === 7 || t === 9) {
|
|
215
|
+
const base = FR_TENS[t]!;
|
|
216
|
+
const sub = 10 + u;
|
|
217
|
+
return base + (base.endsWith('-') ? '' : '-') + FR_UNITS[sub];
|
|
218
|
+
}
|
|
219
|
+
if (t === 8 && u === 0) return 'quatre-vingts';
|
|
220
|
+
if (u === 0) return FR_TENS[t]!;
|
|
221
|
+
if (u === 1 && t < 8) return FR_TENS[t] + ' et un';
|
|
222
|
+
return FR_TENS[t] + '-' + FR_UNITS[u];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function frBelowThousand(n: number): string {
|
|
226
|
+
if (n < 100) return frBelowHundred(n);
|
|
227
|
+
const h = Math.floor(n / 100);
|
|
228
|
+
const r = n % 100;
|
|
229
|
+
let result = '';
|
|
230
|
+
if (h === 1) result = 'cent';
|
|
231
|
+
else result = FR_UNITS[h] + ' cent' + (r === 0 ? 's' : '');
|
|
232
|
+
if (r > 0) result += ' ' + frBelowHundred(r);
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function numberToFrench(n: number): string {
|
|
237
|
+
if (n === 0) return 'zéro';
|
|
238
|
+
if (n < 0) return 'moins ' + numberToFrench(-n);
|
|
239
|
+
if (n < 1000) return frBelowThousand(n);
|
|
240
|
+
if (n < 1_000_000) {
|
|
241
|
+
const t = Math.floor(n / 1000);
|
|
242
|
+
const r = n % 1000;
|
|
243
|
+
const thousands = t === 1 ? 'mille' : frBelowThousand(t) + ' mille';
|
|
244
|
+
return r === 0 ? thousands : thousands + ' ' + frBelowThousand(r);
|
|
245
|
+
}
|
|
246
|
+
if (n < 1_000_000_000) {
|
|
247
|
+
const m = Math.floor(n / 1_000_000);
|
|
248
|
+
const r = n % 1_000_000;
|
|
249
|
+
const millions = m === 1 ? 'un million' : frBelowThousand(m) + ' millions';
|
|
250
|
+
return r === 0 ? millions : millions + ' ' + numberToFrench(r);
|
|
251
|
+
}
|
|
252
|
+
throw new Error('Number must be less than 1 billion');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function enBelowHundred(n: number): string {
|
|
256
|
+
if (n < 20) return EN_UNITS[n]!;
|
|
257
|
+
const t = Math.floor(n / 10);
|
|
258
|
+
const u = n % 10;
|
|
259
|
+
return u === 0 ? EN_TENS[t]! : EN_TENS[t] + '-' + EN_UNITS[u];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function enBelowThousand(n: number): string {
|
|
263
|
+
if (n < 100) return enBelowHundred(n);
|
|
264
|
+
const h = Math.floor(n / 100);
|
|
265
|
+
const r = n % 100;
|
|
266
|
+
return r === 0 ? EN_UNITS[h] + ' hundred' : EN_UNITS[h] + ' hundred ' + enBelowHundred(r);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function numberToEnglish(n: number): string {
|
|
270
|
+
if (n === 0) return 'zero';
|
|
271
|
+
if (n < 0) return 'minus ' + numberToEnglish(-n);
|
|
272
|
+
if (n < 1000) return enBelowThousand(n);
|
|
273
|
+
if (n < 1_000_000) {
|
|
274
|
+
const t = Math.floor(n / 1000);
|
|
275
|
+
const r = n % 1000;
|
|
276
|
+
return r === 0 ? enBelowThousand(t) + ' thousand' : enBelowThousand(t) + ' thousand ' + enBelowThousand(r);
|
|
277
|
+
}
|
|
278
|
+
if (n < 1_000_000_000) {
|
|
279
|
+
const m = Math.floor(n / 1_000_000);
|
|
280
|
+
const r = n % 1_000_000;
|
|
281
|
+
return r === 0 ? enBelowThousand(m) + ' million' : enBelowThousand(m) + ' million ' + numberToEnglish(r);
|
|
282
|
+
}
|
|
283
|
+
throw new Error('Number must be less than 1 billion');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function number(value: string, lang: string): string {
|
|
287
|
+
const n = Number(value);
|
|
288
|
+
if (isNaN(n)) throw new Error('Value must be a number');
|
|
289
|
+
if (!Number.isInteger(n)) throw new Error('Value must be an integer');
|
|
290
|
+
if (Math.abs(n) >= 1_000_000_000) throw new Error('Number must be less than 1 billion');
|
|
291
|
+
|
|
292
|
+
switch (lang) {
|
|
293
|
+
case 'fr':
|
|
294
|
+
return numberToFrench(n);
|
|
295
|
+
case 'en':
|
|
296
|
+
return numberToEnglish(n);
|
|
297
|
+
default:
|
|
298
|
+
throw new Error('Lang must be one of: fr, en');
|
|
299
|
+
}
|
|
300
|
+
}
|