@excofy/utils 1.0.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/dist/index.js ADDED
@@ -0,0 +1,384 @@
1
+ // src/helpers/sanitize.ts
2
+ import { FilterXSS } from "xss";
3
+ var allTags = [
4
+ "h1",
5
+ "h2",
6
+ "h3",
7
+ "h4",
8
+ "h5",
9
+ "h6",
10
+ "p",
11
+ "ul",
12
+ "ol",
13
+ "li",
14
+ "span",
15
+ "a",
16
+ "strong",
17
+ "em",
18
+ "i",
19
+ "br",
20
+ "img"
21
+ ];
22
+ var allAttributes = [
23
+ "data-lexical-text",
24
+ "style",
25
+ "dir",
26
+ "value",
27
+ "href",
28
+ "rel",
29
+ "target",
30
+ "alt",
31
+ "title",
32
+ "src",
33
+ "class"
34
+ ];
35
+ function sanitizeValue(value, allowedTags) {
36
+ let whiteList = {};
37
+ if (allowedTags === void 0) {
38
+ for (const tag of allTags) {
39
+ whiteList[tag] = [...allAttributes];
40
+ }
41
+ } else if (allowedTags.length === 0) {
42
+ whiteList = {};
43
+ } else {
44
+ for (const entry of allowedTags) {
45
+ if (typeof entry === "string") {
46
+ whiteList[entry] = [];
47
+ } else {
48
+ whiteList[entry.tag] = entry.attributes ?? [];
49
+ }
50
+ }
51
+ }
52
+ const filter = new FilterXSS({
53
+ whiteList,
54
+ stripIgnoreTag: true,
55
+ stripIgnoreTagBody: ["script"]
56
+ });
57
+ return filter.process(value);
58
+ }
59
+ function htmlEntityDecode(value) {
60
+ if (!value) {
61
+ return "";
62
+ }
63
+ return value.replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
64
+ }
65
+
66
+ // src/helpers/video.ts
67
+ var video = {
68
+ youtube: {
69
+ isValid: (url) => {
70
+ const regex = /^(?:(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11}))/g;
71
+ return regex.test(url);
72
+ },
73
+ getId: (url) => {
74
+ const regex = /^(?:(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11}))/;
75
+ const match = url?.match(regex);
76
+ if (match && match.length >= 2 && match[1]) {
77
+ return match[1];
78
+ }
79
+ return null;
80
+ },
81
+ getEmbedUrl: (id) => {
82
+ return `https://www.youtube.com/embed/${id}`;
83
+ }
84
+ }
85
+ };
86
+
87
+ // src/helpers/validator.ts
88
+ function createValidator() {
89
+ const current = {
90
+ field: "",
91
+ value: void 0,
92
+ required: false,
93
+ inputs: {},
94
+ errors: {},
95
+ typeValid: false,
96
+ setInput(field, value) {
97
+ this.field = field;
98
+ this.value = value;
99
+ },
100
+ pushError(message) {
101
+ const key = String(this.field);
102
+ if (!this.errors[key]) this.errors[key] = [];
103
+ this.errors[key].push({ message });
104
+ }
105
+ };
106
+ const validatorRequired = {
107
+ isRequired(message) {
108
+ current.required = true;
109
+ if (current.value === void 0 || current.value === null) {
110
+ current.pushError(message);
111
+ }
112
+ return validatorType;
113
+ },
114
+ isNotRequired() {
115
+ current.required = false;
116
+ return validatorType;
117
+ }
118
+ };
119
+ const validatorType = {
120
+ type(type, message) {
121
+ if (shouldSkipValidation()) {
122
+ return validator;
123
+ }
124
+ const value = current.value;
125
+ let isValidType = false;
126
+ if (type === "array") {
127
+ isValidType = Array.isArray(value);
128
+ } else if (type === "object") {
129
+ isValidType = typeof value === "object" && value !== null && !Array.isArray(value);
130
+ } else if (type === "file") {
131
+ isValidType = value instanceof File;
132
+ } else if (type === "date") {
133
+ isValidType = typeof value === "string" && !Number.isNaN(new Date(value).getTime());
134
+ } else {
135
+ isValidType = type === "string" && typeof value === "string" || type === "number" && typeof value === "number" || type === "boolean" && typeof value === "boolean";
136
+ }
137
+ current.typeValid = isValidType;
138
+ if (!isValidType) {
139
+ current.pushError(message);
140
+ }
141
+ return validator;
142
+ }
143
+ };
144
+ const shouldSkipValidation = () => {
145
+ return !current.required && (current.value === void 0 || current.value === null || current.value === "");
146
+ };
147
+ const validator = {
148
+ each(callback) {
149
+ if (shouldSkipValidation()) {
150
+ return validator;
151
+ }
152
+ if (!Array.isArray(current.value)) return validator;
153
+ const fieldName = String(current.field);
154
+ for (let index = 0; index < current.value.length; index++) {
155
+ const item = current.value[index];
156
+ const subValidator = createValidator();
157
+ subValidator.setInputs(item);
158
+ callback(item, index, subValidator);
159
+ const subErrors = subValidator.getFlatErrors();
160
+ if (subErrors) {
161
+ for (const [subField, messages] of Object.entries(subErrors)) {
162
+ const path = `${fieldName}[${index}].${subField}`;
163
+ current.errors[path] = messages;
164
+ }
165
+ }
166
+ if (Array.isArray(current.value)) {
167
+ current.value[index] = subValidator.getSanitizedInputs();
168
+ }
169
+ }
170
+ current.inputs[current.field] = current.value;
171
+ return validator;
172
+ },
173
+ email: (message) => {
174
+ if (shouldSkipValidation()) {
175
+ return validator;
176
+ }
177
+ if (typeof current.value === "string") {
178
+ const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
179
+ if (!regex.test(current.value)) {
180
+ current.pushError(message);
181
+ }
182
+ } else {
183
+ current.pushError(message);
184
+ }
185
+ return validator;
186
+ },
187
+ fileMaxSize: (maxSize, message) => {
188
+ if (shouldSkipValidation()) {
189
+ return validator;
190
+ }
191
+ const value = current.value;
192
+ if (!(value instanceof File)) {
193
+ current.pushError("Arquivo inv\xE1lido");
194
+ return validator;
195
+ }
196
+ if (value.size > maxSize) {
197
+ current.pushError(message);
198
+ }
199
+ return validator;
200
+ },
201
+ fileType: (validTypes, message) => {
202
+ if (shouldSkipValidation()) {
203
+ return validator;
204
+ }
205
+ const value = current.value;
206
+ if (!(value instanceof File)) {
207
+ current.pushError("Arquivo inv\xE1lido");
208
+ return validator;
209
+ }
210
+ if (!validTypes.includes(value.type)) {
211
+ current.pushError(message);
212
+ }
213
+ return validator;
214
+ },
215
+ length(length, message) {
216
+ if (shouldSkipValidation()) {
217
+ return validator;
218
+ }
219
+ const value = current.value;
220
+ const isValid = (typeof value === "string" || Array.isArray(value)) && value.length === length;
221
+ if (!isValid) {
222
+ current.pushError(message);
223
+ }
224
+ return validator;
225
+ },
226
+ min(min, message) {
227
+ if (shouldSkipValidation()) {
228
+ return validator;
229
+ }
230
+ if (typeof current.value === "string" && current.value.length < min)
231
+ current.pushError(message);
232
+ else if (typeof current.value === "number" && current.value < min)
233
+ current.pushError(message);
234
+ else if (Array.isArray(current.value) && current.value.length < min)
235
+ current.pushError(message);
236
+ return validator;
237
+ },
238
+ max(max, message) {
239
+ if (shouldSkipValidation()) {
240
+ return validator;
241
+ }
242
+ if (typeof current.value === "string" && current.value.length > max)
243
+ current.pushError(message);
244
+ else if (typeof current.value === "number" && current.value > max)
245
+ current.pushError(message);
246
+ return validator;
247
+ },
248
+ transform(fn) {
249
+ if (shouldSkipValidation()) return validator;
250
+ const transformed = fn(current.value);
251
+ current.value = transformed;
252
+ current.inputs[current.field] = transformed;
253
+ return validator;
254
+ },
255
+ slug(message) {
256
+ if (shouldSkipValidation()) {
257
+ return validator;
258
+ }
259
+ if (typeof current.value === "string") {
260
+ const regex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
261
+ if (!regex.test(current.value)) current.pushError(message);
262
+ }
263
+ return validator;
264
+ },
265
+ sanitize() {
266
+ if (shouldSkipValidation()) {
267
+ return validator;
268
+ }
269
+ if (typeof current.value === "string") {
270
+ const newValue = sanitizeValue(current.value, []).replace(/\s+/g, " ").trim();
271
+ current.value = newValue;
272
+ current.inputs[current.field] = newValue;
273
+ }
274
+ return validator;
275
+ },
276
+ sanitizeHTML(tags) {
277
+ if (shouldSkipValidation()) {
278
+ return validator;
279
+ }
280
+ if (typeof current.value === "string") {
281
+ const newValue = sanitizeValue(current.value, tags);
282
+ current.value = newValue;
283
+ current.inputs[current.field] = newValue;
284
+ }
285
+ return validator;
286
+ },
287
+ uuid(message) {
288
+ if (shouldSkipValidation()) {
289
+ return validator;
290
+ }
291
+ if (typeof current.value === "string") {
292
+ const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
293
+ if (!regex.test(current.value)) current.pushError(message);
294
+ }
295
+ return validator;
296
+ },
297
+ oneOf: (types, message) => {
298
+ if (shouldSkipValidation()) {
299
+ return validator;
300
+ }
301
+ if (types.includes(String(current.value))) {
302
+ return validator;
303
+ }
304
+ current.pushError(message);
305
+ return validator;
306
+ },
307
+ videoUrl: {
308
+ youtube(message) {
309
+ if (shouldSkipValidation()) {
310
+ return validator;
311
+ }
312
+ if (typeof current.value === "string" && !video.youtube.isValid(current.value)) {
313
+ current.pushError(message);
314
+ }
315
+ return validator;
316
+ }
317
+ }
318
+ };
319
+ return {
320
+ getErrors() {
321
+ return Object.keys(current.errors).length > 0 ? current.errors : void 0;
322
+ },
323
+ getFlatErrors() {
324
+ const flatErrors = {};
325
+ function flatten(prefix, obj) {
326
+ if (!obj) return;
327
+ if (Array.isArray(obj)) {
328
+ if (obj.length > 0 && typeof obj[0] === "object" && "message" in obj[0]) {
329
+ flatErrors[prefix] = obj;
330
+ } else if (obj.length > 0 && typeof obj[0] === "object") {
331
+ for (let i = 0; i < obj.length; i++) {
332
+ flatten(`${prefix}[${i}]`, obj[i]);
333
+ }
334
+ }
335
+ } else {
336
+ for (const key in obj) {
337
+ flatten(prefix ? `${prefix}.${key}` : key, obj[key]);
338
+ }
339
+ }
340
+ }
341
+ flatten("", current.errors);
342
+ return Object.keys(flatErrors).length ? flatErrors : {};
343
+ },
344
+ getSanitizedInputs() {
345
+ return current.inputs;
346
+ },
347
+ hasErrors() {
348
+ return Object.keys(current.errors).length > 0;
349
+ },
350
+ setInputs(inputs) {
351
+ current.inputs = inputs;
352
+ current.errors = {};
353
+ },
354
+ unflattenErrors(flat) {
355
+ const result = {};
356
+ for (const flatKey in flat) {
357
+ const messages = flat[flatKey].map((m) => m.message).join("\n");
358
+ const pathParts = flatKey.replace(/\[(\d+)\]/g, ".$1").split(".");
359
+ let current2 = result;
360
+ for (let i = 0; i < pathParts.length; i++) {
361
+ const part = pathParts[i];
362
+ const nextIsIndex = /^\d+$/.test(pathParts[i + 1] || "");
363
+ if (i === pathParts.length - 1) {
364
+ current2[part] = messages;
365
+ } else {
366
+ if (!(part in current2)) {
367
+ current2[part] = nextIsIndex ? [] : {};
368
+ }
369
+ current2 = current2[part];
370
+ }
371
+ }
372
+ }
373
+ return result;
374
+ },
375
+ validate(field) {
376
+ current.setInput(field, current.inputs[field]);
377
+ return validatorRequired;
378
+ }
379
+ };
380
+ }
381
+ export {
382
+ createValidator,
383
+ htmlEntityDecode
384
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@excofy/utils",
3
+ "version": "1.0.0",
4
+ "description": "Biblioteca de utilitários para o Excofy",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "require": "./dist/index.js"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "build": "tsup src/index.ts --dts --format esm,cjs --out-dir dist",
15
+ "lint": "biome lint",
16
+ "typecheck": "tsc --noEmit",
17
+ "prepublishOnly": "npm run build",
18
+ "deploy": "act"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/excofy/utils.git"
23
+ },
24
+ "keywords": [],
25
+ "author": "Excofy",
26
+ "license": "ISC",
27
+ "bugs": {
28
+ "url": "https://github.com/excofy/utils/issues"
29
+ },
30
+ "homepage": "https://github.com/excofy/utils#readme",
31
+ "devDependencies": {
32
+ "@biomejs/biome": "^1.9.4",
33
+ "@types/node": "^24.0.3",
34
+ "tsup": "^8.5.0",
35
+ "typescript": "^5.8.3"
36
+ },
37
+ "dependencies": {
38
+ "xss": "^1.0.15"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }
@@ -0,0 +1,115 @@
1
+ import { type IWhiteList, FilterXSS } from 'xss';
2
+
3
+ type Tag =
4
+ | 'h1'
5
+ | 'h2'
6
+ | 'h3'
7
+ | 'h4'
8
+ | 'h5'
9
+ | 'h6'
10
+ | 'p'
11
+ | 'ul'
12
+ | 'ol'
13
+ | 'li'
14
+ | 'span'
15
+ | 'a'
16
+ | 'strong'
17
+ | 'em'
18
+ | 'i'
19
+ | 'br'
20
+ | 'img';
21
+
22
+ type LexicalEditorAttributes = 'data-lexical-text' | 'style' | 'dir' | 'value';
23
+ type HTMLAttributes =
24
+ | 'href'
25
+ | 'rel'
26
+ | 'target'
27
+ | 'alt'
28
+ | 'title'
29
+ | 'src'
30
+ | 'class';
31
+
32
+ type Attributes = LexicalEditorAttributes | HTMLAttributes;
33
+
34
+ export type AllowedTag = Tag | { tag: Tag; attributes?: Attributes[] };
35
+
36
+ const allTags: Tag[] = [
37
+ 'h1',
38
+ 'h2',
39
+ 'h3',
40
+ 'h4',
41
+ 'h5',
42
+ 'h6',
43
+ 'p',
44
+ 'ul',
45
+ 'ol',
46
+ 'li',
47
+ 'span',
48
+ 'a',
49
+ 'strong',
50
+ 'em',
51
+ 'i',
52
+ 'br',
53
+ 'img',
54
+ ];
55
+
56
+ const allAttributes: Attributes[] = [
57
+ 'data-lexical-text',
58
+ 'style',
59
+ 'dir',
60
+ 'value',
61
+ 'href',
62
+ 'rel',
63
+ 'target',
64
+ 'alt',
65
+ 'title',
66
+ 'src',
67
+ 'class',
68
+ ];
69
+
70
+ export function sanitizeValue(
71
+ value: string,
72
+ allowedTags?: AllowedTag[]
73
+ ): string {
74
+ let whiteList: IWhiteList = {};
75
+
76
+ if (allowedTags === undefined) {
77
+ // Permitir todas as tags com todos os atributos
78
+ for (const tag of allTags) {
79
+ whiteList[tag] = [...allAttributes];
80
+ }
81
+ } else if (allowedTags.length === 0) {
82
+ // Não permitir nenhuma tag
83
+ whiteList = {};
84
+ } else {
85
+ // Construir whiteList com base nas tags e atributos permitidos explicitamente
86
+ for (const entry of allowedTags) {
87
+ if (typeof entry === 'string') {
88
+ whiteList[entry] = [];
89
+ } else {
90
+ whiteList[entry.tag] = entry.attributes ?? [];
91
+ }
92
+ }
93
+ }
94
+
95
+ const filter = new FilterXSS({
96
+ whiteList,
97
+ stripIgnoreTag: true,
98
+ stripIgnoreTagBody: ['script'],
99
+ });
100
+
101
+ return filter.process(value);
102
+ }
103
+
104
+ export function htmlEntityDecode(value?: string): string {
105
+ if (!value) {
106
+ return '';
107
+ }
108
+
109
+ return value
110
+ .replace(/&gt;/g, '>')
111
+ .replace(/&lt;/g, '<')
112
+ .replace(/&amp;/g, '&')
113
+ .replace(/&quot;/g, '"')
114
+ .replace(/&#39;/g, "'");
115
+ }