@archznn/xavva 2.6.0 → 2.8.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/README.md +55 -0
- package/package.json +4 -2
- package/src/commands/CompletionCommand.ts +212 -0
- package/src/commands/ConfigCommand.ts +184 -0
- package/src/commands/DeployCommand.ts +2 -1
- package/src/commands/EncodingCommand.ts +351 -0
- package/src/commands/HealthCommand.ts +302 -0
- package/src/commands/HelpCommand.ts +42 -0
- package/src/commands/HistoryCommand.ts +49 -0
- package/src/commands/InitCommand.ts +148 -0
- package/src/commands/RedoCommand.ts +36 -0
- package/src/config/versions.ts +63 -0
- package/src/di/container.ts +249 -0
- package/src/errors/ErrorHandler.ts +249 -0
- package/src/errors/XavvaError.ts +273 -0
- package/src/index.ts +136 -96
- package/src/services/AuditService.ts +3 -2
- package/src/services/BrowserService.ts +127 -16
- package/src/services/DeployWatcher.ts +183 -0
- package/src/services/EmbeddedTomcatService.ts +67 -37
- package/src/services/EncodingService.ts +548 -0
- package/src/services/FileWatcher.ts +243 -0
- package/src/services/HistoryService.ts +73 -0
- package/src/services/NotificationService.ts +145 -0
- package/src/services/TomcatService.ts +59 -26
- package/src/types/args.ts +151 -0
- package/src/types/config.ts +6 -0
- package/src/types/index.ts +7 -0
- package/src/utils/PathUtils.ts +221 -0
- package/src/utils/ProgressBar.ts +182 -0
- package/src/utils/config.ts +6 -0
- package/src/utils/parsers/JavaParser.ts +413 -0
- package/src/utils/platform.ts +2 -2
- package/src/services/WatcherService.ts +0 -117
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { promises as fs, existsSync, mkdirSync, createReadStream, createWriteStream } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { Logger } from "../utils/ui";
|
|
4
|
+
|
|
5
|
+
// Mapeamento de encoding aliases
|
|
6
|
+
const ENCODING_ALIASES: Record<string, string> = {
|
|
7
|
+
"utf8": "utf-8",
|
|
8
|
+
"utf-8": "utf-8",
|
|
9
|
+
"cp1252": "windows-1252",
|
|
10
|
+
"windows-1252": "windows-1252",
|
|
11
|
+
"latin1": "iso-8859-1",
|
|
12
|
+
"iso-8859-1": "iso-8859-1",
|
|
13
|
+
"iso88591": "iso-8859-1",
|
|
14
|
+
"cp850": "ibm850",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Encodings suportados
|
|
18
|
+
const SUPPORTED_ENCODINGS = ["utf-8", "windows-1252", "iso-8859-1"];
|
|
19
|
+
|
|
20
|
+
// Bytes caracteristicos de UTF-8 para deteccao
|
|
21
|
+
const UTF8_BOM = Buffer.from([0xEF, 0xBB, 0xBF]);
|
|
22
|
+
|
|
23
|
+
export interface DetectedEncoding {
|
|
24
|
+
encoding: string;
|
|
25
|
+
confidence: number;
|
|
26
|
+
hasBOM: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class EncodingService {
|
|
30
|
+
private backupDir: string;
|
|
31
|
+
|
|
32
|
+
constructor() {
|
|
33
|
+
this.backupDir = path.join(process.cwd(), ".xavva", "encoding-backups");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Normaliza o nome do encoding
|
|
38
|
+
*/
|
|
39
|
+
normalizeEncoding(encoding: string): string {
|
|
40
|
+
const normalized = encoding.toLowerCase().replace(/[_\s]/g, "-");
|
|
41
|
+
return ENCODING_ALIASES[normalized] || normalized;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Verifica se um encoding e suportado
|
|
46
|
+
*/
|
|
47
|
+
isValidEncoding(encoding: string): boolean {
|
|
48
|
+
const normalized = this.normalizeEncoding(encoding);
|
|
49
|
+
return SUPPORTED_ENCODINGS.includes(normalized);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Retorna lista de encodings suportados
|
|
54
|
+
*/
|
|
55
|
+
getSupportedEncodings(): string[] {
|
|
56
|
+
return [...SUPPORTED_ENCODINGS];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Detecta o encoding de um buffer
|
|
61
|
+
* Implementacao simplificada baseada em heuristicas
|
|
62
|
+
*/
|
|
63
|
+
detectEncoding(buffer: Buffer): DetectedEncoding {
|
|
64
|
+
// Verifica BOM
|
|
65
|
+
if (buffer.length >= 3) {
|
|
66
|
+
if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
|
|
67
|
+
return { encoding: "utf-8", confidence: 1, hasBOM: true };
|
|
68
|
+
}
|
|
69
|
+
if (buffer[0] === 0xFE && buffer[1] === 0xFF) {
|
|
70
|
+
return { encoding: "utf-16be", confidence: 1, hasBOM: true };
|
|
71
|
+
}
|
|
72
|
+
if (buffer[0] === 0xFF && buffer[1] === 0xFE) {
|
|
73
|
+
return { encoding: "utf-16le", confidence: 1, hasBOM: true };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Heuristica: conta bytes validos UTF-8 vs invalidos
|
|
78
|
+
let utf8Valid = 0;
|
|
79
|
+
let utf8Invalid = 0;
|
|
80
|
+
let highBytes = 0; // Bytes > 127
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
83
|
+
const byte = buffer[i];
|
|
84
|
+
|
|
85
|
+
if (byte > 127) {
|
|
86
|
+
highBytes++;
|
|
87
|
+
|
|
88
|
+
// Verifica se e inicio de sequencia UTF-8 multi-byte
|
|
89
|
+
if ((byte & 0xE0) === 0xC0) {
|
|
90
|
+
// 2-byte sequence
|
|
91
|
+
if (i + 1 < buffer.length && (buffer[i + 1] & 0xC0) === 0x80) {
|
|
92
|
+
utf8Valid++;
|
|
93
|
+
i++;
|
|
94
|
+
} else {
|
|
95
|
+
utf8Invalid++;
|
|
96
|
+
}
|
|
97
|
+
} else if ((byte & 0xF0) === 0xE0) {
|
|
98
|
+
// 3-byte sequence
|
|
99
|
+
if (i + 2 < buffer.length &&
|
|
100
|
+
(buffer[i + 1] & 0xC0) === 0x80 &&
|
|
101
|
+
(buffer[i + 2] & 0xC0) === 0x80) {
|
|
102
|
+
utf8Valid++;
|
|
103
|
+
i += 2;
|
|
104
|
+
} else {
|
|
105
|
+
utf8Invalid++;
|
|
106
|
+
}
|
|
107
|
+
} else if ((byte & 0xF8) === 0xF0) {
|
|
108
|
+
// 4-byte sequence
|
|
109
|
+
if (i + 3 < buffer.length &&
|
|
110
|
+
(buffer[i + 1] & 0xC0) === 0x80 &&
|
|
111
|
+
(buffer[i + 2] & 0xC0) === 0x80 &&
|
|
112
|
+
(buffer[i + 3] & 0xC0) === 0x80) {
|
|
113
|
+
utf8Valid++;
|
|
114
|
+
i += 3;
|
|
115
|
+
} else {
|
|
116
|
+
utf8Invalid++;
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
utf8Invalid++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Se tem bytes altos e todos sao validos UTF-8, provavelmente e UTF-8
|
|
125
|
+
if (highBytes > 0 && utf8Invalid === 0 && utf8Valid > 0) {
|
|
126
|
+
return { encoding: "utf-8", confidence: 0.95, hasBOM: false };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Se tem bytes altos invalidos em UTF-8, provavelmente e single-byte encoding
|
|
130
|
+
if (highBytes > 0 && utf8Invalid > 0) {
|
|
131
|
+
// No contexto brasileiro/Windows, provavelmente e Windows-1252
|
|
132
|
+
return { encoding: "windows-1252", confidence: 0.7, hasBOM: false };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Se nao tem bytes altos, pode ser ASCII/UTF-8/Latin1
|
|
136
|
+
return { encoding: "utf-8", confidence: 0.5, hasBOM: false };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Detecta encoding de um arquivo
|
|
141
|
+
*/
|
|
142
|
+
async detectFileEncoding(filePath: string): Promise<DetectedEncoding | null> {
|
|
143
|
+
try {
|
|
144
|
+
const buffer = await fs.readFile(filePath);
|
|
145
|
+
return this.detectEncoding(buffer);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Cria backup de um arquivo antes da conversao
|
|
153
|
+
*/
|
|
154
|
+
async createBackup(filePath: string): Promise<string> {
|
|
155
|
+
if (!existsSync(this.backupDir)) {
|
|
156
|
+
mkdirSync(this.backupDir, { recursive: true });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const fileName = path.basename(filePath);
|
|
160
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
161
|
+
const backupPath = path.join(this.backupDir, `${fileName}.${timestamp}.bak`);
|
|
162
|
+
|
|
163
|
+
await fs.copyFile(filePath, backupPath);
|
|
164
|
+
return backupPath;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Converte arquivo de um encoding para outro
|
|
169
|
+
*/
|
|
170
|
+
async convertFile(
|
|
171
|
+
filePath: string,
|
|
172
|
+
fromEncoding: string,
|
|
173
|
+
toEncoding: string,
|
|
174
|
+
options: { backup?: boolean; dryRun?: boolean } = {}
|
|
175
|
+
): Promise<{ success: boolean; message: string; unsupportedChars?: string[] }> {
|
|
176
|
+
try {
|
|
177
|
+
if (!existsSync(filePath)) {
|
|
178
|
+
return { success: false, message: `Arquivo nao encontrado: ${filePath}` };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const normalizedFrom = this.normalizeEncoding(fromEncoding);
|
|
182
|
+
const normalizedTo = this.normalizeEncoding(toEncoding);
|
|
183
|
+
|
|
184
|
+
if (normalizedFrom === normalizedTo) {
|
|
185
|
+
return { success: true, message: "Encodings iguais, nenhuma conversao necessaria" };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Le o arquivo como buffer
|
|
189
|
+
const buffer = await fs.readFile(filePath);
|
|
190
|
+
|
|
191
|
+
// Se tem BOM UTF-8, remove para processamento
|
|
192
|
+
let contentBuffer = buffer;
|
|
193
|
+
let hadBOM = false;
|
|
194
|
+
if (buffer.length >= 3 &&
|
|
195
|
+
buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
|
|
196
|
+
contentBuffer = buffer.slice(3);
|
|
197
|
+
hadBOM = true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Decodifica do encoding de origem
|
|
201
|
+
let content: string;
|
|
202
|
+
try {
|
|
203
|
+
// Usa TextDecoder para encodings padrao ou iconv-lite para Windows-1252
|
|
204
|
+
if (normalizedFrom === "utf-8") {
|
|
205
|
+
content = contentBuffer.toString("utf-8");
|
|
206
|
+
} else {
|
|
207
|
+
// Para Windows-1252 e outros single-byte, usamos decoding manual
|
|
208
|
+
content = this.decodeSingleByte(contentBuffer, normalizedFrom);
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {
|
|
211
|
+
return { success: false, message: `Erro ao decodificar de ${fromEncoding}: ${e}` };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (options.dryRun) {
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
message: `[DRY RUN] Converteria de ${fromEncoding} para ${toEncoding}`
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Cria backup se solicitado
|
|
222
|
+
if (options.backup) {
|
|
223
|
+
const backupPath = await this.createBackup(filePath);
|
|
224
|
+
Logger.debug(`Backup criado: ${backupPath}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Codifica para o encoding destino
|
|
228
|
+
let outputBuffer: Buffer;
|
|
229
|
+
const unsupportedChars: string[] = [];
|
|
230
|
+
|
|
231
|
+
if (normalizedTo === "utf-8") {
|
|
232
|
+
outputBuffer = Buffer.from(content, "utf-8");
|
|
233
|
+
// Adiciona BOM se o original tinha
|
|
234
|
+
if (hadBOM) {
|
|
235
|
+
outputBuffer = Buffer.concat([UTF8_BOM, outputBuffer]);
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
const result = this.encodeSingleByte(content, normalizedTo);
|
|
239
|
+
outputBuffer = result.buffer;
|
|
240
|
+
unsupportedChars.push(...result.unsupportedChars);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await fs.writeFile(filePath, outputBuffer);
|
|
244
|
+
|
|
245
|
+
let message = `Convertido: ${fromEncoding} -> ${toEncoding}`;
|
|
246
|
+
if (unsupportedChars.length > 0) {
|
|
247
|
+
const unique = [...new Set(unsupportedChars)].slice(0, 5);
|
|
248
|
+
// Mostra o código Unicode em vez do caractere (evita ? no terminal)
|
|
249
|
+
const codes = unique.map(c => `U+${c.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')} (${c})`);
|
|
250
|
+
message += ` (${unsupportedChars.length} caractere(s) nao suportado(s): ${codes.join(", ")})`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
success: true,
|
|
255
|
+
message,
|
|
256
|
+
unsupportedChars: unsupportedChars.length > 0 ? unsupportedChars : undefined
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
} catch (e) {
|
|
260
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
261
|
+
return { success: false, message: `Erro: ${error}` };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Tenta corrigir mojibake (texto corrompido por encoding errado)
|
|
267
|
+
* Ex: A�o -> Acao
|
|
268
|
+
*/
|
|
269
|
+
async fixMojibake(
|
|
270
|
+
filePath: string,
|
|
271
|
+
options: { backup?: boolean; dryRun?: boolean; force?: boolean } = {}
|
|
272
|
+
): Promise<{ success: boolean; message: string; detectedFrom?: string }> {
|
|
273
|
+
try {
|
|
274
|
+
if (!existsSync(filePath)) {
|
|
275
|
+
return { success: false, message: `Arquivo nao encontrado: ${filePath}` };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const buffer = await fs.readFile(filePath);
|
|
279
|
+
const detection = this.detectEncoding(buffer);
|
|
280
|
+
|
|
281
|
+
// Se ja e UTF-8, nao precisa converter (a menos que force=true)
|
|
282
|
+
if (detection.encoding === "utf-8" && detection.confidence > 0.8 && !options.force) {
|
|
283
|
+
// Mas verifica se tem padrões de mojibake comuns
|
|
284
|
+
const content = buffer.toString("utf-8");
|
|
285
|
+
const hasMojibake = /[\u00EF\u00BF\u00BD\u00C3\u00A0-\u00C3\u00B9]/.test(content);
|
|
286
|
+
|
|
287
|
+
if (!hasMojibake) {
|
|
288
|
+
return { success: true, message: "Arquivo ja esta em UTF-8", detectedFrom: "utf-8" };
|
|
289
|
+
}
|
|
290
|
+
// Se tem mojibake, continua com a correção mesmo sendo UTF-8
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Tenta converter assumindo que esta em Windows-1252 mas foi lido como UTF-8
|
|
294
|
+
// Ou vice-versa
|
|
295
|
+
let fixed = false;
|
|
296
|
+
let usedEncoding = "";
|
|
297
|
+
|
|
298
|
+
// Tenta interpretar como Windows-1252
|
|
299
|
+
try {
|
|
300
|
+
const asLatin1 = buffer.toString("latin1");
|
|
301
|
+
const reencoded = Buffer.from(asLatin1, "utf-8");
|
|
302
|
+
const redecoded = reencoded.toString("utf-8");
|
|
303
|
+
|
|
304
|
+
// Se o resultado tem caracteres legiveis comuns em portugues, provavelmente funcionou
|
|
305
|
+
if (this.looksLikePortuguese(redecoded)) {
|
|
306
|
+
if (!options.dryRun) {
|
|
307
|
+
if (options.backup) {
|
|
308
|
+
await this.createBackup(filePath);
|
|
309
|
+
}
|
|
310
|
+
await fs.writeFile(filePath, reencoded);
|
|
311
|
+
}
|
|
312
|
+
fixed = true;
|
|
313
|
+
usedEncoding = "windows-1252";
|
|
314
|
+
}
|
|
315
|
+
} catch (e) {
|
|
316
|
+
// ignora
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (fixed) {
|
|
320
|
+
return {
|
|
321
|
+
success: true,
|
|
322
|
+
message: options.dryRun ?
|
|
323
|
+
`[DRY RUN] Corrigiria mojibake (detectado: ${usedEncoding})` :
|
|
324
|
+
`Mojibake corrigido! (detectado: ${usedEncoding})`,
|
|
325
|
+
detectedFrom: usedEncoding
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
success: false,
|
|
331
|
+
message: "Nao foi possivel detectar/corrigir o mojibake automaticamente",
|
|
332
|
+
detectedFrom: detection.encoding
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
} catch (e) {
|
|
336
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
337
|
+
return { success: false, message: `Erro: ${error}` };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Verifica se uma string parece portugues valido
|
|
343
|
+
*/
|
|
344
|
+
private looksLikePortuguese(text: string): boolean {
|
|
345
|
+
// Conta caracteres comuns em portugues
|
|
346
|
+
const portugueseChars = /[\u00E1\u00E0\u00E2\u00E3\u00E9\u00EA\u00ED\u00F3\u00F4\u00F5\u00FA\u00FC\u00E7\u00C1\u00C0\u00C2\u00C3\u00C9\u00CA\u00CD\u00D3\u00D4\u00D5\u00DA\u00DC\u00C7]/g;
|
|
347
|
+
const matches = text.match(portugueseChars);
|
|
348
|
+
|
|
349
|
+
// Se tem caracteres acentuados brasileiros, provavelmente e portugues
|
|
350
|
+
if (matches && matches.length > 0) {
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Verifica sequencias suspeitas de mojibake
|
|
355
|
+
const mojibakePatterns = /[\u00EF\u00BF\u00BD\u00C3\u00A1]/;
|
|
356
|
+
return !mojibakePatterns.test(text);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Decodifica buffer single-byte (Windows-1252, ISO-8859-1)
|
|
361
|
+
*/
|
|
362
|
+
private decodeSingleByte(buffer: Buffer, encoding: string): string {
|
|
363
|
+
// Mapeamento Windows-1252 para Unicode
|
|
364
|
+
const windows1252Map: Record<number, string> = {
|
|
365
|
+
0x80: "\u20AC", 0x82: "\u201A", 0x83: "\u0192", 0x84: "\u201E", 0x85: "\u2026",
|
|
366
|
+
0x86: "\u2020", 0x87: "\u2021", 0x88: "\u02C6", 0x89: "\u2030", 0x8A: "\u0160",
|
|
367
|
+
0x8B: "\u2039", 0x8C: "\u0152", 0x8E: "\u017D", 0x91: "\u2018", 0x92: "\u2019",
|
|
368
|
+
0x93: "\u201C", 0x94: "\u201D", 0x95: "\u2022", 0x96: "\u2013", 0x97: "\u2014",
|
|
369
|
+
0x98: "\u02DC", 0x99: "\u2122", 0x9A: "\u0161", 0x9B: "\u203A", 0x9C: "\u0153",
|
|
370
|
+
0x9E: "\u017E", 0x9F: "\u0178",
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
let result = "";
|
|
374
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
375
|
+
const byte = buffer[i];
|
|
376
|
+
if (encoding === "windows-1252" && byte >= 0x80 && windows1252Map[byte]) {
|
|
377
|
+
result += windows1252Map[byte];
|
|
378
|
+
} else {
|
|
379
|
+
result += String.fromCharCode(byte);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Codifica string para single-byte encoding
|
|
387
|
+
* Retorna o buffer e lista de caracteres nao suportados
|
|
388
|
+
*/
|
|
389
|
+
private encodeSingleByte(text: string, encoding: string): { buffer: Buffer; unsupportedChars: string[] } {
|
|
390
|
+
// Mapeamento COMPLETO Unicode -> Windows-1252 (usando escapes Unicode)
|
|
391
|
+
const toWindows1252: Record<string, number> = {
|
|
392
|
+
// Range 0x80-0x9F (caracteres especiais do Windows-1252)
|
|
393
|
+
"\u20AC": 0x80, "\u201A": 0x82, "\u0192": 0x83, "\u201E": 0x84, "\u2026": 0x85,
|
|
394
|
+
"\u2020": 0x86, "\u2021": 0x87, "\u02C6": 0x88, "\u2030": 0x89, "\u0160": 0x8A,
|
|
395
|
+
"\u2039": 0x8B, "\u0152": 0x8C, "\u017D": 0x8E, "\u2018": 0x91, "\u2019": 0x92,
|
|
396
|
+
"\u201C": 0x93, "\u201D": 0x94, "\u2022": 0x95, "\u2013": 0x96, "\u2014": 0x97,
|
|
397
|
+
"\u02DC": 0x98, "\u2122": 0x99, "\u0161": 0x9A, "\u203A": 0x9B, "\u0153": 0x9C,
|
|
398
|
+
"\u017E": 0x9E, "\u0178": 0x9F,
|
|
399
|
+
|
|
400
|
+
// Range 0xA0-0xBF
|
|
401
|
+
"\u00A0": 0xA0, "\u00A1": 0xA1, "\u00A2": 0xA2, "\u00A3": 0xA3, "\u00A4": 0xA4,
|
|
402
|
+
"\u00A5": 0xA5, "\u00A6": 0xA6, "\u00A7": 0xA7, "\u00A8": 0xA8, "\u00A9": 0xA9,
|
|
403
|
+
"\u00AA": 0xAA, "\u00AB": 0xAB, "\u00AC": 0xAC, "\u00AD": 0xAD, "\u00AE": 0xAE,
|
|
404
|
+
"\u00AF": 0xAF, "\u00B0": 0xB0, "\u00B1": 0xB1, "\u00B2": 0xB2, "\u00B3": 0xB3,
|
|
405
|
+
"\u00B4": 0xB4, "\u00B5": 0xB5, "\u00B6": 0xB6, "\u00B7": 0xB7, "\u00B8": 0xB8,
|
|
406
|
+
"\u00B9": 0xB9, "\u00BA": 0xBA, "\u00BB": 0xBB, "\u00BC": 0xBC, "\u00BD": 0xBD,
|
|
407
|
+
"\u00BE": 0xBE, "\u00BF": 0xBF,
|
|
408
|
+
|
|
409
|
+
// Letras maiusculas acentuadas (0xC0-0xDF)
|
|
410
|
+
"\u00C0": 0xC0, "\u00C1": 0xC1, "\u00C2": 0xC2, "\u00C3": 0xC3, "\u00C4": 0xC4,
|
|
411
|
+
"\u00C5": 0xC5, "\u00C6": 0xC6, "\u00C7": 0xC7, "\u00C8": 0xC8, "\u00C9": 0xC9,
|
|
412
|
+
"\u00CA": 0xCA, "\u00CB": 0xCB, "\u00CC": 0xCC, "\u00CD": 0xCD, "\u00CE": 0xCE,
|
|
413
|
+
"\u00CF": 0xCF, "\u00D0": 0xD0, "\u00D1": 0xD1, "\u00D2": 0xD2, "\u00D3": 0xD3,
|
|
414
|
+
"\u00D4": 0xD4, "\u00D5": 0xD5, "\u00D6": 0xD6, "\u00D7": 0xD7, "\u00D8": 0xD8,
|
|
415
|
+
"\u00D9": 0xD9, "\u00DA": 0xDA, "\u00DB": 0xDB, "\u00DC": 0xDC, "\u00DD": 0xDD,
|
|
416
|
+
"\u00DE": 0xDE, "\u00DF": 0xDF,
|
|
417
|
+
|
|
418
|
+
// Letras minusculas acentuadas (0xE0-0xFF)
|
|
419
|
+
"\u00E0": 0xE0, "\u00E1": 0xE1, "\u00E2": 0xE2, "\u00E3": 0xE3, "\u00E4": 0xE4,
|
|
420
|
+
"\u00E5": 0xE5, "\u00E6": 0xE6, "\u00E7": 0xE7, "\u00E8": 0xE8, "\u00E9": 0xE9,
|
|
421
|
+
"\u00EA": 0xEA, "\u00EB": 0xEB, "\u00EC": 0xEC, "\u00ED": 0xED, "\u00EE": 0xEE,
|
|
422
|
+
"\u00EF": 0xEF, "\u00F0": 0xF0, "\u00F1": 0xF1, "\u00F2": 0xF2, "\u00F3": 0xF3,
|
|
423
|
+
"\u00F4": 0xF4, "\u00F5": 0xF5, "\u00F6": 0xF6, "\u00F7": 0xF7, "\u00F8": 0xF8,
|
|
424
|
+
"\u00F9": 0xF9, "\u00FA": 0xFA, "\u00FB": 0xFB, "\u00FC": 0xFC, "\u00FD": 0xFD,
|
|
425
|
+
"\u00FE": 0xFE, "\u00FF": 0xFF,
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const bytes: number[] = [];
|
|
429
|
+
const unsupportedChars: string[] = [];
|
|
430
|
+
|
|
431
|
+
for (const char of text) {
|
|
432
|
+
const code = char.charCodeAt(0);
|
|
433
|
+
|
|
434
|
+
if (code < 128) {
|
|
435
|
+
// ASCII direto
|
|
436
|
+
bytes.push(code);
|
|
437
|
+
} else if (encoding === "windows-1252" && toWindows1252[char] !== undefined) {
|
|
438
|
+
bytes.push(toWindows1252[char]);
|
|
439
|
+
} else if (encoding === "iso-8859-1" && code >= 0xA0 && code <= 0xFF) {
|
|
440
|
+
// ISO-8859-1 suporta diretamente 0xA0-0xFF
|
|
441
|
+
bytes.push(code);
|
|
442
|
+
} else {
|
|
443
|
+
// Caractere nao suportado no encoding
|
|
444
|
+
unsupportedChars.push(char);
|
|
445
|
+
bytes.push(0x3F); // ?
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return { buffer: Buffer.from(bytes), unsupportedChars };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Lista arquivos de texto em um diretorio
|
|
454
|
+
*/
|
|
455
|
+
async findTextFiles(dir: string, extensions: string[] = [".java", ".xml", ".properties", ".txt", ".jsp", ".html", ".js", ".css"]): Promise<string[]> {
|
|
456
|
+
const results: string[] = [];
|
|
457
|
+
|
|
458
|
+
const scan = async (currentDir: string) => {
|
|
459
|
+
try {
|
|
460
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
461
|
+
|
|
462
|
+
for (const entry of entries) {
|
|
463
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
464
|
+
|
|
465
|
+
if (entry.isDirectory()) {
|
|
466
|
+
// Ignora diretorios comuns de build
|
|
467
|
+
if (["node_modules", "target", "build", ".git", ".xavva", "dist", "bin"].includes(entry.name)) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
await scan(fullPath);
|
|
471
|
+
} else if (entry.isFile()) {
|
|
472
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
473
|
+
if (extensions.includes(ext)) {
|
|
474
|
+
results.push(fullPath);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} catch (e) {
|
|
479
|
+
// ignora diretorios sem permissao
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
await scan(dir);
|
|
484
|
+
return results;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Converte todos os arquivos de um diretorio
|
|
489
|
+
*/
|
|
490
|
+
async convertDirectory(
|
|
491
|
+
dir: string,
|
|
492
|
+
fromEncoding: string,
|
|
493
|
+
toEncoding: string,
|
|
494
|
+
options: {
|
|
495
|
+
backup?: boolean;
|
|
496
|
+
dryRun?: boolean;
|
|
497
|
+
extensions?: string[];
|
|
498
|
+
} = {}
|
|
499
|
+
): Promise<{ success: number; failed: number; total: number; totalUnsupported: number }> {
|
|
500
|
+
const files = await this.findTextFiles(dir, options.extensions);
|
|
501
|
+
let success = 0;
|
|
502
|
+
let failed = 0;
|
|
503
|
+
let totalUnsupported = 0;
|
|
504
|
+
|
|
505
|
+
Logger.info("Arquivos encontrados", String(files.length));
|
|
506
|
+
|
|
507
|
+
for (const file of files) {
|
|
508
|
+
const result = await this.convertFile(file, fromEncoding, toEncoding, {
|
|
509
|
+
backup: options.backup,
|
|
510
|
+
dryRun: options.dryRun
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
if (result.success) {
|
|
514
|
+
success++;
|
|
515
|
+
if (result.unsupportedChars) {
|
|
516
|
+
totalUnsupported += result.unsupportedChars.length;
|
|
517
|
+
}
|
|
518
|
+
if (!options.dryRun) {
|
|
519
|
+
Logger.success(`${path.relative(dir, file)}: ${result.message}`);
|
|
520
|
+
} else {
|
|
521
|
+
Logger.info(`${path.relative(dir, file)}`, result.message);
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
failed++;
|
|
525
|
+
Logger.warn(`${path.relative(dir, file)}: ${result.message}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return { success, failed, total: files.length, totalUnsupported };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Detecta encoding de todos os arquivos em um diretorio
|
|
534
|
+
*/
|
|
535
|
+
async detectDirectoryEncodings(dir: string): Promise<Map<string, DetectedEncoding>> {
|
|
536
|
+
const files = await this.findTextFiles(dir);
|
|
537
|
+
const results = new Map<string, DetectedEncoding>();
|
|
538
|
+
|
|
539
|
+
for (const file of files) {
|
|
540
|
+
const detection = await this.detectFileEncoding(file);
|
|
541
|
+
if (detection) {
|
|
542
|
+
results.set(file, detection);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return results;
|
|
547
|
+
}
|
|
548
|
+
}
|