@20syldev/api 4.7.0 → 4.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.
Files changed (191) hide show
  1. package/README.md +84 -47
  2. package/dist/app.js +6 -6
  3. package/dist/app.js.map +1 -1
  4. package/dist/config/env.js.map +1 -1
  5. package/dist/config/versions.js +22 -3
  6. package/dist/config/versions.js.map +1 -1
  7. package/dist/constants.js +27 -12
  8. package/dist/constants.js.map +1 -1
  9. package/dist/middleware/error.js +2 -2
  10. package/dist/middleware/error.js.map +1 -1
  11. package/dist/middleware/logger.js.map +1 -1
  12. package/dist/middleware/ratelimit.js +2 -2
  13. package/dist/middleware/ratelimit.js.map +1 -1
  14. package/dist/middleware/version.js.map +1 -1
  15. package/dist/modules/v3.js +1 -1
  16. package/dist/modules/v3.js.map +1 -1
  17. package/{src/modules/v4/address.ts → dist/modules/v4/address.js} +14 -46
  18. package/dist/modules/v4/address.js.map +1 -0
  19. package/{src/modules/v4/agent.ts → dist/modules/v4/agent.js} +26 -38
  20. package/dist/modules/v4/agent.js.map +1 -0
  21. package/dist/modules/v4/algorithms.js +79 -0
  22. package/dist/modules/v4/algorithms.js.map +1 -1
  23. package/dist/modules/v4/avatar.js +136 -0
  24. package/dist/modules/v4/avatar.js.map +1 -0
  25. package/dist/modules/v4/barcode.js +440 -0
  26. package/dist/modules/v4/barcode.js.map +1 -0
  27. package/dist/modules/v4/captcha.js +7 -0
  28. package/dist/modules/v4/captcha.js.map +1 -1
  29. package/dist/modules/v4/chat.js +9 -1
  30. package/dist/modules/v4/chat.js.map +1 -1
  31. package/dist/modules/v4/color.js +8 -1
  32. package/dist/modules/v4/color.js.map +1 -1
  33. package/dist/modules/v4/convert.js +9 -0
  34. package/dist/modules/v4/convert.js.map +1 -1
  35. package/dist/modules/v4/credit.js +104 -0
  36. package/dist/modules/v4/credit.js.map +1 -0
  37. package/dist/modules/v4/cron.js +202 -0
  38. package/dist/modules/v4/cron.js.map +1 -0
  39. package/dist/modules/v4/dice.js +7 -0
  40. package/dist/modules/v4/dice.js.map +1 -1
  41. package/dist/modules/v4/domain.js +6 -1
  42. package/dist/modules/v4/domain.js.map +1 -1
  43. package/dist/modules/v4/encode.js +71 -0
  44. package/dist/modules/v4/encode.js.map +1 -1
  45. package/dist/modules/v4/geo.js +11 -1
  46. package/dist/modules/v4/geo.js.map +1 -1
  47. package/dist/modules/v4/hash.js +10 -1
  48. package/dist/modules/v4/hash.js.map +1 -1
  49. package/dist/modules/v4/hyperplanning.js +9 -1
  50. package/dist/modules/v4/hyperplanning.js.map +1 -1
  51. package/{src/modules/v4/ip.ts → dist/modules/v4/ip.js} +58 -60
  52. package/dist/modules/v4/ip.js.map +1 -0
  53. package/dist/modules/v4/levenshtein.js +8 -0
  54. package/dist/modules/v4/levenshtein.js.map +1 -1
  55. package/dist/modules/v4/palette.js +9 -1
  56. package/dist/modules/v4/palette.js.map +1 -1
  57. package/{src/modules/v4/password.ts → dist/modules/v4/password.js} +25 -63
  58. package/dist/modules/v4/password.js.map +1 -0
  59. package/dist/modules/v4/personal.js +14 -18
  60. package/dist/modules/v4/personal.js.map +1 -1
  61. package/dist/modules/v4/placeholder.js +8 -0
  62. package/dist/modules/v4/placeholder.js.map +1 -1
  63. package/dist/modules/v4/qrcode.js +10 -3
  64. package/dist/modules/v4/qrcode.js.map +1 -1
  65. package/dist/modules/v4/regex.js +48 -0
  66. package/dist/modules/v4/regex.js.map +1 -0
  67. package/dist/modules/v4/statistics.js +7 -0
  68. package/dist/modules/v4/statistics.js.map +1 -1
  69. package/dist/modules/v4/text.js +30 -0
  70. package/dist/modules/v4/text.js.map +1 -1
  71. package/dist/modules/v4/tic_tac_toe.js +9 -1
  72. package/dist/modules/v4/tic_tac_toe.js.map +1 -1
  73. package/dist/modules/v4/time.js +62 -11
  74. package/dist/modules/v4/time.js.map +1 -1
  75. package/dist/modules/v4/token.js +17 -8
  76. package/dist/modules/v4/token.js.map +1 -1
  77. package/dist/modules/v4/username.js +5 -0
  78. package/dist/modules/v4/username.js.map +1 -1
  79. package/dist/modules/v4/validate.js +21 -0
  80. package/dist/modules/v4/validate.js.map +1 -1
  81. package/dist/modules/v4.js +10 -1
  82. package/dist/modules/v4.js.map +1 -1
  83. package/dist/routes/delete.js.map +1 -1
  84. package/dist/routes/get.js +208 -48
  85. package/dist/routes/get.js.map +1 -1
  86. package/dist/routes/index.js +2 -2
  87. package/dist/routes/index.js.map +1 -1
  88. package/dist/routes/patch.js.map +1 -1
  89. package/dist/routes/post.js +2 -2
  90. package/dist/routes/post.js.map +1 -1
  91. package/dist/utils/response.js.map +1 -1
  92. package/package.json +2 -1
  93. package/.github/FUNDING.yml +0 -1
  94. package/.prettierrc +0 -7
  95. package/eslint.config.js +0 -24
  96. package/robots.txt +0 -74
  97. package/src/app.ts +0 -53
  98. package/src/config/env.ts +0 -87
  99. package/src/config/plans.ts +0 -61
  100. package/src/config/versions.ts +0 -119
  101. package/src/constants.ts +0 -46
  102. package/src/favicon.ico +0 -0
  103. package/src/middleware/cors.ts +0 -23
  104. package/src/middleware/error.ts +0 -16
  105. package/src/middleware/json.ts +0 -9
  106. package/src/middleware/logger.ts +0 -32
  107. package/src/middleware/ratelimit.ts +0 -89
  108. package/src/middleware/version.ts +0 -40
  109. package/src/modules/v3/algorithms.js +0 -218
  110. package/src/modules/v3/captcha.js +0 -60
  111. package/src/modules/v3/chat.js +0 -111
  112. package/src/modules/v3/color.js +0 -60
  113. package/src/modules/v3/convert.js +0 -39
  114. package/src/modules/v3/domain.js +0 -86
  115. package/src/modules/v3/hash.js +0 -16
  116. package/src/modules/v3/hyperplanning.js +0 -56
  117. package/src/modules/v3/levenshtein.js +0 -46
  118. package/src/modules/v3/personal.js +0 -139
  119. package/src/modules/v3/qrcode.js +0 -20
  120. package/src/modules/v3/tic_tac_toe.js +0 -242
  121. package/src/modules/v3/time.js +0 -98
  122. package/src/modules/v3/token.js +0 -54
  123. package/src/modules/v3/username.js +0 -94
  124. package/src/modules/v3/utils.js +0 -77
  125. package/src/modules/v3.js +0 -15
  126. package/src/modules/v4/algorithms.ts +0 -276
  127. package/src/modules/v4/captcha.ts +0 -114
  128. package/src/modules/v4/chat.ts +0 -130
  129. package/src/modules/v4/color.ts +0 -75
  130. package/src/modules/v4/convert.ts +0 -127
  131. package/src/modules/v4/dice.ts +0 -46
  132. package/src/modules/v4/domain.ts +0 -86
  133. package/src/modules/v4/encode.ts +0 -241
  134. package/src/modules/v4/geo.ts +0 -63
  135. package/src/modules/v4/hash.ts +0 -32
  136. package/src/modules/v4/hyperplanning.ts +0 -93
  137. package/src/modules/v4/levenshtein.ts +0 -46
  138. package/src/modules/v4/palette.ts +0 -71
  139. package/src/modules/v4/personal.ts +0 -140
  140. package/src/modules/v4/placeholder.ts +0 -197
  141. package/src/modules/v4/qrcode.ts +0 -121
  142. package/src/modules/v4/statistics.ts +0 -62
  143. package/src/modules/v4/text.ts +0 -330
  144. package/src/modules/v4/tic_tac_toe.ts +0 -264
  145. package/src/modules/v4/time.ts +0 -103
  146. package/src/modules/v4/token.ts +0 -53
  147. package/src/modules/v4/username.ts +0 -94
  148. package/src/modules/v4/validate.ts +0 -76
  149. package/src/modules/v4.ts +0 -27
  150. package/src/routes/delete.ts +0 -72
  151. package/src/routes/get.ts +0 -835
  152. package/src/routes/index.ts +0 -59
  153. package/src/routes/patch.ts +0 -45
  154. package/src/routes/post.ts +0 -209
  155. package/src/storage/index.ts +0 -16
  156. package/src/types/express.d.ts +0 -17
  157. package/src/types/storage.ts +0 -48
  158. package/src/utils/colors.ts +0 -91
  159. package/src/utils/helpers.ts +0 -90
  160. package/src/utils/response.ts +0 -28
  161. package/tests/integration/api.test.ts +0 -716
  162. package/tests/tsconfig.json +0 -3
  163. package/tests/unit/address.test.ts +0 -84
  164. package/tests/unit/agent.test.ts +0 -113
  165. package/tests/unit/algorithms.test.ts +0 -121
  166. package/tests/unit/captcha.test.ts +0 -62
  167. package/tests/unit/chat.test.ts +0 -54
  168. package/tests/unit/color.test.ts +0 -54
  169. package/tests/unit/convert.test.ts +0 -68
  170. package/tests/unit/dice.test.ts +0 -58
  171. package/tests/unit/domain.test.ts +0 -48
  172. package/tests/unit/encode.test.ts +0 -109
  173. package/tests/unit/geo.test.ts +0 -46
  174. package/tests/unit/hash.test.ts +0 -45
  175. package/tests/unit/hyperplanning.test.ts +0 -97
  176. package/tests/unit/ip.test.ts +0 -140
  177. package/tests/unit/levenshtein.test.ts +0 -35
  178. package/tests/unit/palette.test.ts +0 -54
  179. package/tests/unit/password.test.ts +0 -86
  180. package/tests/unit/personal.test.ts +0 -66
  181. package/tests/unit/placeholder.test.ts +0 -58
  182. package/tests/unit/qrcode.test.ts +0 -83
  183. package/tests/unit/statistics.test.ts +0 -58
  184. package/tests/unit/text.test.ts +0 -108
  185. package/tests/unit/tic_tac_toe.test.ts +0 -56
  186. package/tests/unit/time.test.ts +0 -68
  187. package/tests/unit/token.test.ts +0 -62
  188. package/tests/unit/username.test.ts +0 -35
  189. package/tests/unit/validate.test.ts +0 -72
  190. package/tsconfig.json +0 -17
  191. package/tsconfig.test.json +0 -9
@@ -1,46 +0,0 @@
1
- import { MAX_LEVENSHTEIN_LENGTH } from '../../constants.js';
2
-
3
- /**
4
- * Computes the Levenshtein edit distance between two strings.
5
- *
6
- * @param str1 - First string
7
- * @param str2 - Second string
8
- * @returns Object containing both strings and the minimum number of single-character edits to transform one into the other
9
- * @throws Error if either string is missing, not a string, or exceeds the maximum length
10
- */
11
- export default function levenshtein(str1: string, str2: string): { str1: string; str2: string; distance: number } {
12
- if (!str1 || typeof str1 !== 'string') {
13
- throw new Error('Please provide a valid first string');
14
- }
15
-
16
- if (!str2 || typeof str2 !== 'string') {
17
- throw new Error('Please provide a valid second string');
18
- }
19
-
20
- if (str1.length > MAX_LEVENSHTEIN_LENGTH) {
21
- throw new Error('First string exceeds 1000 characters');
22
- }
23
-
24
- if (str2.length > MAX_LEVENSHTEIN_LENGTH) {
25
- throw new Error('Second string exceeds 1000 characters');
26
- }
27
-
28
- const m: number[][] = Array.from({ length: str1.length + 1 }, (_, i) => [i]);
29
- for (let j = 0; j <= str2.length; j++) m[0]![j] = j;
30
-
31
- for (let i = 1; i <= str1.length; i++) {
32
- for (let j = 1; j <= str2.length; j++) {
33
- m[i]![j] = Math.min(
34
- m[i - 1]![j]! + 1,
35
- m[i]![j - 1]! + 1,
36
- m[i - 1]![j - 1]! + (str1[i - 1] !== str2[j - 1] ? 1 : 0),
37
- );
38
- }
39
- }
40
-
41
- return {
42
- str1,
43
- str2,
44
- distance: m[str1.length]![str2.length]!,
45
- };
46
- }
@@ -1,71 +0,0 @@
1
- import { hexToRgb, hslToRgb, rgbToHex, rgbToHsl } from '../../utils/colors.js';
2
-
3
- export interface PaletteColor {
4
- hex: string;
5
- rgb: string;
6
- hsl: string;
7
- }
8
-
9
- export interface PaletteResult {
10
- base: PaletteColor;
11
- type: string;
12
- colors: PaletteColor[];
13
- }
14
-
15
- function format(r: number, g: number, b: number): PaletteColor {
16
- const [h, s, l] = rgbToHsl(r, g, b);
17
- return {
18
- hex: rgbToHex(r, g, b),
19
- rgb: `rgb(${r}, ${g}, ${b})`,
20
- hsl: `hsl(${h.toFixed(1)}, ${(s * 100).toFixed(1)}%, ${(l * 100).toFixed(1)}%)`,
21
- };
22
- }
23
-
24
- function fromHueShifts(base: [number, number, number], shifts: number[]): PaletteColor[] {
25
- const [h, s, l] = base;
26
- return shifts.map((shift) => {
27
- const newH = (h + shift + 360) % 360;
28
- const [r, g, b] = hslToRgb(newH, s, l);
29
- return format(r, g, b);
30
- });
31
- }
32
-
33
- /**
34
- * Generates a color palette from a base hex color using a specified harmony type.
35
- *
36
- * @param color - Base hex color (e.g. "#ff5733")
37
- * @param type - Palette type: "complementary", "triadic", "analogous", "tetradic", or "split-complementary"
38
- * @returns Object containing the base color and the derived palette colors in hex, RGB, and HSL
39
- * @throws Error if color or type is missing, or the type is not one of the accepted values
40
- */
41
- export default function palette(color: string, type: string): PaletteResult {
42
- if (!color) throw new Error('A base color is required');
43
- if (!type) throw new Error('A palette type is required');
44
-
45
- const [r, g, b] = hexToRgb(color);
46
- const base = format(r, g, b);
47
- const hsl = rgbToHsl(r, g, b);
48
-
49
- let colors: PaletteColor[];
50
- switch (type) {
51
- case 'complementary':
52
- colors = fromHueShifts(hsl, [0, 180]);
53
- break;
54
- case 'triadic':
55
- colors = fromHueShifts(hsl, [0, 120, 240]);
56
- break;
57
- case 'analogous':
58
- colors = fromHueShifts(hsl, [-60, -30, 0, 30, 60]);
59
- break;
60
- case 'tetradic':
61
- colors = fromHueShifts(hsl, [0, 90, 180, 270]);
62
- break;
63
- case 'split-complementary':
64
- colors = fromHueShifts(hsl, [0, 150, 210]);
65
- break;
66
- default:
67
- throw new Error('Type must be one of: complementary, triadic, analogous, tetradic, split-complementary');
68
- }
69
-
70
- return { base, type, colors };
71
- }
@@ -1,140 +0,0 @@
1
- import { random, randomNumber } from '../../utils/helpers.js';
2
- import addressFn from './address.js';
3
-
4
- interface Person {
5
- name: string;
6
- social: string;
7
- email: string;
8
- country: string;
9
- }
10
-
11
- interface CountryInfo {
12
- tel: string;
13
- code: string;
14
- lang: string;
15
- }
16
-
17
- /**
18
- * Generates a fictional personal profile with realistic demographic and contact data.
19
- *
20
- * @returns Object containing name, email, phone, address, job, hobbies, and other personal attributes
21
- */
22
- export default function personal(): Record<string, unknown> {
23
- const people: Person[] = [
24
- { name: 'John Doe', social: 'john_doe', email: 'john@example.com', country: 'US' },
25
- { name: 'Jane Martin', social: 'jane_martin', email: 'jane@example.com', country: 'FR' },
26
- { name: 'Michael Johnson', social: 'mike_johnson', email: 'michael@example.com', country: 'UK' },
27
- { name: 'Emily Davis', social: 'emily_davis', email: 'emily@example.com', country: 'ES' },
28
- { name: 'Alexis Barbos', social: 'alexis_barbos', email: 'alexis@example.com', country: 'DE' },
29
- { name: 'Sarah Williams', social: 'sarah_williams', email: 'sarah@example.com', country: 'IT' },
30
- { name: 'Daniel Brown', social: 'daniel_brown', email: 'daniel@example.com', country: 'JP' },
31
- { name: 'Sophia Wilson', social: 'sophia_wilson', email: 'sophia@example.com', country: 'BR' },
32
- { name: 'James Taylor', social: 'james_taylor', email: 'james@example.com', country: 'CA' },
33
- { name: 'Olivia Thomas', social: 'olivia_thomas', email: 'olivia@example.com', country: 'AU' },
34
- ];
35
-
36
- const countries: Record<string, CountryInfo> = {
37
- US: { tel: '123-456-7890', code: '1', lang: 'English' },
38
- FR: { tel: '06 78 90 12 34', code: '33', lang: 'French' },
39
- UK: { tel: '7911 123456', code: '44', lang: 'English' },
40
- ES: { tel: '678 901 234', code: '34', lang: 'Spanish' },
41
- DE: { tel: '163 555 1584', code: '49', lang: 'German' },
42
- IT: { tel: '345 678 9012', code: '39', lang: 'Italian' },
43
- JP: { tel: '080-1234-5678', code: '81', lang: 'Japanese' },
44
- BR: { tel: '(11) 98765-4321', code: '55', lang: 'Portuguese' },
45
- CA: { tel: '416-123-4567', code: '1', lang: 'English' },
46
- AU: { tel: '0412 345 678', code: '61', lang: 'English' },
47
- };
48
-
49
- const jobs = ['Writer', 'Artist', 'Musician', 'Explorer', 'Scientist', 'Engineer', 'Athlete', 'Doctor'];
50
- const hobbies = ['Reading', 'Traveling', 'Gaming', 'Cooking', 'Fitness', 'Music', 'Photography', 'Writing'];
51
- const card = Array.from({ length: 4 }, () => randomNumber(1000, 9999)).join(' ');
52
- const cvc = randomNumber(100, 999);
53
- const expiration = `${String(randomNumber(1, 12)).padStart(2, '0')}/${(new Date().getFullYear() + randomNumber(0, 3)).toString().slice(-2)}`;
54
-
55
- const person = random(people);
56
- const social = person.social;
57
- const country = person.country;
58
- const countryCodes: Record<string, string> = { US: 'us', FR: 'fr', UK: 'uk', ES: 'es', DE: 'de' };
59
- const addrCountry = countryCodes[country] ?? 'us';
60
- const countryInfo = countries[country]!;
61
- const phone = countryInfo.tel;
62
- const lang = countryInfo.lang;
63
-
64
- const age = randomNumber(18, 67);
65
- const birthday = new Date(Date.now() - randomNumber(18, 67) * 365.25 * 24 * 60 * 60 * 1000).toISOString();
66
-
67
- const emergencyContacts: { name: string; relationship: string; phone: string }[] = [];
68
- const yearIncome = randomNumber(20000, 100000);
69
- const subscriptions: string[] = [];
70
- const pets: string[] = [];
71
- const vehicles: string[] = [];
72
- let civilStatus = 'Single';
73
- let children = 0;
74
-
75
- if (age >= 21 && Math.random() > 0.7) civilStatus = 'Married';
76
- if (civilStatus === 'Married' && age >= 25) children = randomNumber(0, 3);
77
-
78
- while (emergencyContacts.length < randomNumber(1, 3)) {
79
- let emergencyContact = random(people);
80
- while (
81
- emergencyContact.email === person.email ||
82
- emergencyContacts.some((e) => e.name === emergencyContact.name)
83
- ) {
84
- emergencyContact = random(people);
85
- }
86
- const emergencyPhone = `${randomNumber(100, 999)}-${randomNumber(100, 999)}-${randomNumber(1000, 9999)}`;
87
- emergencyContacts.push({
88
- name: emergencyContact.name,
89
- relationship: random(['Spouse', 'Parent', 'Sibling', 'Friend']),
90
- phone: `+${countryInfo.code} ${emergencyPhone}`,
91
- });
92
- }
93
-
94
- while (subscriptions.length < randomNumber(1, 3)) {
95
- const subscription = random(['Netflix', 'Spotify', 'Amazon Prime', 'Disney+', 'Hulu']);
96
- if (!subscriptions.includes(subscription)) subscriptions.push(subscription);
97
- }
98
-
99
- while (pets.length < randomNumber(1, 3)) {
100
- const pet = random(['Dog', 'Cat', 'Fish', 'Bird', 'None']);
101
- if (!pets.includes(pet)) pets.push(pet);
102
- }
103
-
104
- while (vehicles.length < randomNumber(1, 3)) {
105
- const vehicle = random(['Car', 'Bike', 'Motorcycle', 'Bus', 'None']);
106
- if (!vehicles.includes(vehicle)) vehicles.push(vehicle);
107
- }
108
-
109
- return {
110
- name: person.name,
111
- email: person.email,
112
- localisation: country,
113
- phone: `+${countryInfo.code} ${phone}`,
114
- job: random(jobs),
115
- hobbies: random(hobbies),
116
- language: lang,
117
- card,
118
- cvc,
119
- expiration,
120
- address: addressFn(addrCountry).addresses[0]!,
121
- birthday,
122
- civil_status: civilStatus,
123
- children,
124
- vehicle: vehicles,
125
- social_profiles: {
126
- twitter: `@${social}`,
127
- facebook: `facebook.com/${social}`,
128
- linkedin: `linkedin.com/in/${social}`,
129
- instagram: `instagram.com/${social}`,
130
- },
131
- year_income: `${yearIncome} USD/year`,
132
- month_income: `${(yearIncome / 12).toFixed(2)} USD/month`,
133
- education: random(['High School', "Bachelor's", "Master's", 'PhD']),
134
- work_experience: `${randomNumber(0, 20)} years`,
135
- health_status: random(['Healthy', 'Minor Issues', 'Chronic Conditions']),
136
- emergency_contacts: emergencyContacts,
137
- subscriptions,
138
- pets,
139
- };
140
- }
@@ -1,197 +0,0 @@
1
- import { normalizeColor } from '../../utils/colors.js';
2
-
3
- type AvatarShape = 'circle' | 'rounded' | 'square';
4
- type AnimateMode = 'shimmer' | 'pulse' | 'none';
5
-
6
- export interface PlaceholderOptions {
7
- width: number;
8
- height: number;
9
- bg?: string;
10
- color?: string;
11
- text?: string;
12
- rows?: number;
13
- avatar?: AvatarShape;
14
- lines?: number;
15
- animate?: AnimateMode;
16
- speed?: number;
17
- radius?: number;
18
- }
19
-
20
- function parseSize(value: string | undefined, name: string, def: number): number {
21
- if (value === undefined) return def;
22
- const n = Number(value);
23
- if (isNaN(n)) throw new Error(`${name} must be a number`);
24
- if (n < 1 || n > 4000) throw new Error(`${name} must be between 1 and 4000`);
25
- return Math.floor(n);
26
- }
27
-
28
- function escapeXml(str: string): string {
29
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
30
- }
31
-
32
- function generateImage(opts: PlaceholderOptions): string {
33
- const { width, height } = opts;
34
- const bg = normalizeColor(opts.bg, '#cccccc');
35
- const color = normalizeColor(opts.color, '#333333');
36
- const text = escapeXml(opts.text ?? `${width}\u00d7${height}`);
37
- const fontSize = Math.max(12, Math.min(width, height) / 8);
38
-
39
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
40
- <rect width="${width}" height="${height}" fill="${bg}" rx="4" />
41
- <text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-weight="600" font-size="${fontSize}" fill="${color}">${text}</text>
42
- </svg>`;
43
- }
44
-
45
- function avatarShape(shape: AvatarShape, x: number, y: number, size: number): string {
46
- const r = size / 2;
47
- switch (shape) {
48
- case 'circle':
49
- return `<circle cx="${x + r}" cy="${y + r}" r="${r}" />`;
50
- case 'rounded':
51
- return `<rect x="${x}" y="${y}" width="${size}" height="${size}" rx="${Math.round(size * 0.25)}" />`;
52
- case 'square':
53
- return `<rect x="${x}" y="${y}" width="${size}" height="${size}" rx="2" />`;
54
- }
55
- }
56
-
57
- function animationDefs(mode: AnimateMode, bg: string, highlight: string, speed: number): string {
58
- if (mode === 'none') return '';
59
-
60
- if (mode === 'pulse') {
61
- return `<style>
62
- @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
63
- .sk { animation: pulse ${speed}s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
64
- </style>`;
65
- }
66
-
67
- return `<linearGradient id="g">
68
- <stop offset="0.1" stop-color="${bg}" />
69
- <stop offset="0.5" stop-color="${highlight}" />
70
- <stop offset="0.9" stop-color="${bg}" />
71
- <animateTransform attributeName="gradientTransform" type="translate" values="-2 0; 2 0" dur="${speed}s" repeatCount="indefinite" />
72
- </linearGradient>`;
73
- }
74
-
75
- function generateSkeleton(opts: PlaceholderOptions): string {
76
- const { width, height } = opts;
77
- const bg = normalizeColor(opts.bg, '#e5e5e5');
78
- const highlight = normalizeColor(opts.color, '#f5f5f5');
79
- const rows = Math.max(0, Math.min(20, opts.rows ?? 3));
80
- const avatar = opts.avatar;
81
- const animate = opts.animate ?? 'shimmer';
82
- const speed = opts.speed ?? 1.5;
83
- const rx = opts.radius ?? 4;
84
-
85
- const pad = Math.round(width * 0.06);
86
- const slots = rows + (avatar ? 4 : 0);
87
- const available = height - pad * 2;
88
- const lineH = Math.round(Math.max(8, (available / Math.max(1, slots)) * 0.65));
89
- const gap = Math.round(Math.max(6, (available / Math.max(1, slots)) * 0.35));
90
-
91
- const shapes: string[] = [];
92
- let y = pad;
93
-
94
- if (avatar) {
95
- const size = Math.round(Math.min(width * 0.14, height * 0.24, 48));
96
- shapes.push(avatarShape(avatar, pad, y, size));
97
-
98
- const lx = pad + size + gap;
99
- const lw = width - lx - pad;
100
- const count = Math.max(0, Math.min(2, opts.lines ?? 2));
101
-
102
- if (count === 2) {
103
- const nameY = Math.max(pad, Math.round(y + size / 2 - (lineH * 2 + gap) / 2));
104
- shapes.push(`<rect x="${lx}" y="${nameY}" width="${lw * 0.55}" height="${lineH}" rx="${rx}" />`);
105
- shapes.push(
106
- `<rect x="${lx}" y="${nameY + lineH + gap}" width="${lw * 0.35}" height="${lineH}" rx="${rx}" />`,
107
- );
108
- } else if (count === 1) {
109
- const nameY = Math.max(pad, Math.round(y + size / 2 - lineH / 2));
110
- shapes.push(`<rect x="${lx}" y="${nameY}" width="${lw * 0.55}" height="${lineH}" rx="${rx}" />`);
111
- }
112
-
113
- y += size + gap * 2;
114
- }
115
-
116
- for (let i = 0; i < rows; i++) {
117
- const ratio = i === rows - 1 && rows > 1 ? 0.55 : i % 2 === 0 ? 0.92 : 0.78;
118
- const w = Math.round((width - pad * 2) * ratio);
119
- shapes.push(`<rect x="${pad}" y="${y}" width="${w}" height="${lineH}" rx="${rx}" />`);
120
- y += lineH + gap;
121
- }
122
-
123
- const defs = animationDefs(animate, bg, highlight, speed);
124
- const fill = animate === 'shimmer' ? 'url(#g)' : bg;
125
- const cls = animate === 'pulse' ? ' class="sk"' : '';
126
-
127
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
128
- <defs>
129
- <clipPath id="c">
130
- ${shapes.join('\n ')}
131
- </clipPath>
132
- ${defs}
133
- </defs>
134
- <rect width="${width}" height="${height}" clip-path="url(#c)" fill="${fill}"${cls} />
135
- </svg>`;
136
- }
137
-
138
- export interface PlaceholderResult {
139
- type: string;
140
- contentType: string;
141
- body: string;
142
- }
143
-
144
- const AVATAR_SHAPES = new Set(['circle', 'rounded', 'square']);
145
- const ANIMATE_MODES = new Set(['shimmer', 'pulse', 'none']);
146
-
147
- function parseAvatar(value: string | undefined): AvatarShape | undefined {
148
- if (!value) return undefined;
149
- if (value === 'true' || value === '1') return 'circle';
150
- if (AVATAR_SHAPES.has(value)) return value as AvatarShape;
151
- throw new Error('avatar must be one of: circle, rounded, square');
152
- }
153
-
154
- function parseAnimate(value: string | undefined): AnimateMode {
155
- if (!value) return 'shimmer';
156
- if (ANIMATE_MODES.has(value)) return value as AnimateMode;
157
- throw new Error('animate must be one of: shimmer, pulse, none');
158
- }
159
-
160
- /**
161
- * Generates an SVG placeholder image or skeleton loader with configurable dimensions and style.
162
- *
163
- * @param type - Placeholder type: "image" or "skeleton"
164
- * @param query - Query parameters including width, height, bg, color, text, rows, avatar, animate, speed, and radius
165
- * @returns Object containing the SVG body, content type, and placeholder type
166
- * @throws Error if any parameter is invalid or out of range
167
- */
168
- export default function placeholder(type: string, query: Record<string, string | undefined>): PlaceholderResult {
169
- const speed = query.speed ? parseFloat(query.speed) : 1.5;
170
- if (isNaN(speed) || speed < 0.1 || speed > 10) throw new Error('speed must be between 0.1 and 10');
171
-
172
- const radius = query.radius ? parseInt(query.radius, 10) : 4;
173
- if (isNaN(radius) || radius < 0 || radius > 50) throw new Error('radius must be between 0 and 50');
174
-
175
- const opts: PlaceholderOptions = {
176
- width: parseSize(query.width, 'width', 800),
177
- height: parseSize(query.height, 'height', 600),
178
- bg: query.bg,
179
- color: query.color,
180
- text: query.text,
181
- rows: query.rows ? parseInt(query.rows, 10) : undefined,
182
- avatar: parseAvatar(query.avatar),
183
- lines: query.lines ? parseInt(query.lines, 10) : 2,
184
- animate: parseAnimate(query.animate),
185
- speed,
186
- radius,
187
- };
188
-
189
- switch (type) {
190
- case 'image':
191
- return { type, contentType: 'image/svg+xml', body: generateImage(opts) };
192
- case 'skeleton':
193
- return { type, contentType: 'image/svg+xml', body: generateSkeleton(opts) };
194
- default:
195
- throw new Error('Type must be one of: image, skeleton');
196
- }
197
- }
@@ -1,121 +0,0 @@
1
- import { createCanvas, loadImage } from 'canvas';
2
- import { toBuffer, toCanvas as qrToCanvas, toDataURL } from 'qrcode';
3
-
4
- import { normalizeColor } from '../../utils/colors.js';
5
-
6
- export interface QRCodeOptions {
7
- url: string;
8
- size?: number;
9
- margin?: number;
10
- correction?: 'L' | 'M' | 'Q' | 'H';
11
- dark?: string;
12
- light?: string;
13
- icon?: string;
14
- iconSize?: number;
15
- iconPadding?: number;
16
- iconRadius?: number;
17
- format?: 'png' | 'base64';
18
- }
19
-
20
- export interface QRCodeResult {
21
- contentType: string;
22
- body: Buffer | string;
23
- }
24
-
25
- const CORRECTIONS = new Set(['L', 'M', 'Q', 'H']);
26
- const FORMATS = new Set(['png', 'base64']);
27
- const MAX_LOGO_BYTES = 2 * 1024 * 1024;
28
-
29
- function clamp(value: number | undefined, name: string, def: number, min: number, max: number): number {
30
- if (value === undefined) return def;
31
- if (isNaN(value)) throw new Error(`${name} must be a number`);
32
- if (value < min || value > max) throw new Error(`${name} must be between ${min} and ${max}`);
33
- return Math.floor(value);
34
- }
35
-
36
- async function fetchIcon(url: string): Promise<Buffer> {
37
- if (!/^https:\/\//i.test(url)) throw new Error('Icon URL must use HTTPS');
38
-
39
- const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
40
-
41
- if (!response.ok) throw new Error(`Failed to fetch icon: HTTP ${response.status}`);
42
-
43
- const contentType = response.headers.get('content-type') ?? '';
44
- if (!contentType.startsWith('image/')) throw new Error('Icon URL must point to an image');
45
-
46
- const buffer = Buffer.from(await response.arrayBuffer());
47
- if (buffer.length > MAX_LOGO_BYTES) throw new Error('Icon must be under 2MB');
48
-
49
- return buffer;
50
- }
51
-
52
- /**
53
- * Generates a QR code image for a given URL, with optional icon overlay and color customization.
54
- *
55
- * @param options - QR code generation options including URL, size, margin, correction level, colors, and optional icon
56
- * @returns Object containing the QR code as a PNG buffer or Base64 string, and the content type
57
- * @throws Error if the URL is missing, any option is invalid, or the icon URL cannot be fetched
58
- */
59
- export default async function qrcode(options: QRCodeOptions): Promise<QRCodeResult> {
60
- const { url } = options;
61
- if (!url || typeof url !== 'string') throw new Error('Please provide a valid URL');
62
-
63
- const format = options.format ?? 'png';
64
- if (!FORMATS.has(format)) throw new Error('format must be one of: png, base64');
65
-
66
- const size = clamp(options.size, 'size', 200, 50, 2000);
67
- const margin = clamp(options.margin, 'margin', 4, 0, 10);
68
- const dark = normalizeColor(options.dark, '#000000');
69
- const light = normalizeColor(options.light, '#ffffff');
70
-
71
- let correction = options.correction ?? 'M';
72
- if (!CORRECTIONS.has(correction)) throw new Error('correction must be one of: L, M, Q, H');
73
- if (options.icon) correction = 'H';
74
-
75
- const qrOpts = {
76
- width: size,
77
- margin,
78
- errorCorrectionLevel: correction as 'L' | 'M' | 'Q' | 'H',
79
- color: { dark, light },
80
- };
81
-
82
- if (!options.icon) {
83
- if (format === 'base64') {
84
- return { contentType: 'application/json', body: await toDataURL(url, qrOpts) };
85
- }
86
- return { contentType: 'image/png', body: await toBuffer(url, qrOpts) };
87
- }
88
-
89
- const canvas = createCanvas(size, size);
90
- await qrToCanvas(canvas as unknown as HTMLCanvasElement, url, qrOpts);
91
-
92
- const iconBuffer = await fetchIcon(options.icon);
93
- const iconImg = await loadImage(iconBuffer);
94
-
95
- const iconSize = clamp(options.iconSize, 'iconSize', Math.round(size * 0.2), 10, Math.round(size * 0.4));
96
- const iconPadding = clamp(options.iconPadding, 'iconPadding', 5, 0, 50);
97
- const maxRadius = Math.floor((iconSize + iconPadding * 2) / 2);
98
- const iconRadius = clamp(options.iconRadius, 'iconRadius', 0, 0, maxRadius);
99
-
100
- const ctx = canvas.getContext('2d');
101
- const totalSize = iconSize + iconPadding * 2;
102
- const x = (canvas.width - totalSize) / 2;
103
- const y = (canvas.height - totalSize) / 2;
104
-
105
- ctx.beginPath();
106
- ctx.roundRect(x, y, totalSize, totalSize, iconRadius);
107
- ctx.fillStyle = light;
108
- ctx.fill();
109
-
110
- ctx.save();
111
- ctx.beginPath();
112
- ctx.roundRect(x + iconPadding, y + iconPadding, iconSize, iconSize, Math.max(0, iconRadius - iconPadding));
113
- ctx.clip();
114
- ctx.drawImage(iconImg, x + iconPadding, y + iconPadding, iconSize, iconSize);
115
- ctx.restore();
116
-
117
- if (format === 'base64') {
118
- return { contentType: 'application/json', body: canvas.toDataURL('image/png') };
119
- }
120
- return { contentType: 'image/png', body: canvas.toBuffer('image/png') };
121
- }
@@ -1,62 +0,0 @@
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
- /**
17
- * Computes descriptive statistics for a comma-separated list of numbers.
18
- *
19
- * @param values - Comma-separated numeric string (e.g. "1,2,3,4,5")
20
- * @returns Object containing count, sum, min, max, range, mean, median, mode, variance, and standard deviation
21
- * @throws Error if values is missing, contains non-numeric entries, or exceeds the maximum count
22
- */
23
- export default function statistics(values: string): StatisticsResult {
24
- if (!values) throw new Error('A list of values is required');
25
- if (typeof values !== 'string') throw new Error('Values must be a comma-separated string');
26
-
27
- const arr = values.split(',').map((v) => Number(v.trim()));
28
-
29
- if (arr.some(isNaN)) throw new Error('Values must contain only numbers');
30
- if (arr.length < 1) throw new Error('At least one value is required');
31
- if (arr.length > MAX_STRING_LENGTH) throw new Error(`Cannot process more than ${MAX_STRING_LENGTH} values`);
32
-
33
- const count = arr.length;
34
- const sum = arr.reduce((a, b) => a + b, 0);
35
- const min = Math.min(...arr);
36
- const max = Math.max(...arr);
37
- const mean = sum / count;
38
-
39
- const sorted = [...arr].sort((a, b) => a - b);
40
- const median = count % 2 === 0 ? (sorted[count / 2 - 1]! + sorted[count / 2]!) / 2 : sorted[Math.floor(count / 2)]!;
41
-
42
- const counts = new Map<number, number>();
43
- for (const n of arr) counts.set(n, (counts.get(n) ?? 0) + 1);
44
- const maxCount = Math.max(...counts.values());
45
- const mode = maxCount === 1 ? [] : [...counts.entries()].filter(([, c]) => c === maxCount).map(([n]) => n);
46
-
47
- const variance = arr.reduce((acc, n) => acc + (n - mean) ** 2, 0) / count;
48
- const stddev = Math.sqrt(variance);
49
-
50
- return {
51
- count,
52
- sum: +sum.toFixed(6),
53
- min,
54
- max,
55
- range: max - min,
56
- mean: +mean.toFixed(6),
57
- median: +median.toFixed(6),
58
- mode,
59
- variance: +variance.toFixed(6),
60
- stddev: +stddev.toFixed(6),
61
- };
62
- }