rezo 1.0.67 → 1.0.68

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 (37) hide show
  1. package/dist/adapters/index.cjs +6 -6
  2. package/dist/cache/index.cjs +9 -9
  3. package/dist/crawler/index.cjs +40 -40
  4. package/dist/entries/crawler.cjs +4 -4
  5. package/dist/index.cjs +27 -27
  6. package/dist/internal/agents/index.cjs +10 -10
  7. package/dist/proxy/index.cjs +4 -4
  8. package/dist/queue/index.cjs +8 -8
  9. package/dist/responses/universal/index.cjs +11 -11
  10. package/dist/wget/asset-extractor.cjs +556 -0
  11. package/dist/wget/asset-extractor.js +553 -0
  12. package/dist/wget/asset-organizer.cjs +230 -0
  13. package/dist/wget/asset-organizer.js +227 -0
  14. package/dist/wget/download-cache.cjs +221 -0
  15. package/dist/wget/download-cache.js +218 -0
  16. package/dist/wget/downloader.cjs +607 -0
  17. package/dist/wget/downloader.js +604 -0
  18. package/dist/wget/file-writer.cjs +349 -0
  19. package/dist/wget/file-writer.js +346 -0
  20. package/dist/wget/filter-lists.cjs +1330 -0
  21. package/dist/wget/filter-lists.js +1330 -0
  22. package/dist/wget/index.cjs +633 -0
  23. package/dist/wget/index.d.ts +8486 -0
  24. package/dist/wget/index.js +614 -0
  25. package/dist/wget/link-converter.cjs +297 -0
  26. package/dist/wget/link-converter.js +294 -0
  27. package/dist/wget/progress.cjs +271 -0
  28. package/dist/wget/progress.js +266 -0
  29. package/dist/wget/resume.cjs +166 -0
  30. package/dist/wget/resume.js +163 -0
  31. package/dist/wget/robots.cjs +303 -0
  32. package/dist/wget/robots.js +300 -0
  33. package/dist/wget/types.cjs +200 -0
  34. package/dist/wget/types.js +197 -0
  35. package/dist/wget/url-filter.cjs +351 -0
  36. package/dist/wget/url-filter.js +348 -0
  37. package/package.json +6 -1
@@ -0,0 +1,349 @@
1
+ const { promises: fs, createWriteStream } = require("node:fs");
2
+ const { dirname, join, basename, extname } = require("node:path");
3
+ const { AssetOrganizer } = require('./asset-organizer.cjs');
4
+ const MIME_EXTENSIONS = {
5
+ "text/html": ".html",
6
+ "text/css": ".css",
7
+ "text/javascript": ".js",
8
+ "application/javascript": ".js",
9
+ "application/json": ".json",
10
+ "application/xml": ".xml",
11
+ "text/xml": ".xml",
12
+ "image/png": ".png",
13
+ "image/jpeg": ".jpg",
14
+ "image/gif": ".gif",
15
+ "image/webp": ".webp",
16
+ "image/svg+xml": ".svg",
17
+ "application/pdf": ".pdf",
18
+ "font/woff": ".woff",
19
+ "font/woff2": ".woff2",
20
+ "application/font-woff": ".woff",
21
+ "application/font-woff2": ".woff2",
22
+ "font/ttf": ".ttf",
23
+ "application/x-font-ttf": ".ttf",
24
+ "font/otf": ".otf"
25
+ };
26
+ const UNSAFE_CHARS = {
27
+ unix: /[\x00-\x1f\/]/g,
28
+ windows: /[\x00-\x1f\/\\:*?"<>|]/g,
29
+ ascii: /[^\x20-\x7e]/g
30
+ };
31
+
32
+ class FileWriter {
33
+ options;
34
+ outputDir;
35
+ writtenFiles = new Map;
36
+ assetOrganizer = null;
37
+ entryUrls = new Set;
38
+ constructor(options) {
39
+ this.options = options;
40
+ this.outputDir = options.outputDir || ".";
41
+ if (options.organizeAssets) {
42
+ this.assetOrganizer = new AssetOrganizer(options);
43
+ }
44
+ }
45
+ markAsEntry(url) {
46
+ this.entryUrls.add(url);
47
+ }
48
+ isEntryUrl(url) {
49
+ return this.entryUrls.has(url);
50
+ }
51
+ async write(url, content, mimeType) {
52
+ const buffer = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
53
+ let outputPath;
54
+ if (this.options.organizeAssets && this.isEntryUrl(url)) {
55
+ outputPath = this.getEntryFilePath(url, mimeType);
56
+ } else if (this.assetOrganizer?.shouldOrganize(mimeType, url)) {
57
+ const result = this.assetOrganizer.getOrganizedPath(url, buffer, mimeType);
58
+ if (result.isDuplicate) {
59
+ const fullPath = join(this.outputDir, result.path);
60
+ this.writtenFiles.set(url, fullPath);
61
+ return fullPath;
62
+ }
63
+ outputPath = join(this.outputDir, result.path);
64
+ } else {
65
+ outputPath = this.getOutputPath(url);
66
+ if (this.options.adjustExtension) {
67
+ outputPath = this.adjustExtension(outputPath, mimeType);
68
+ }
69
+ }
70
+ if (this.options.noClobber) {
71
+ const exists = await this.fileExists(outputPath);
72
+ if (exists) {
73
+ this.writtenFiles.set(url, outputPath);
74
+ return outputPath;
75
+ }
76
+ }
77
+ if (this.options.backups && this.options.backups > 0) {
78
+ await this.createBackup(outputPath);
79
+ }
80
+ await this.ensureDir(dirname(outputPath));
81
+ await fs.writeFile(outputPath, buffer);
82
+ this.writtenFiles.set(url, outputPath);
83
+ return outputPath;
84
+ }
85
+ async createWriteStream(url, mimeType) {
86
+ let outputPath = this.getOutputPath(url);
87
+ if (mimeType && this.options.adjustExtension) {
88
+ outputPath = this.adjustExtension(outputPath, mimeType);
89
+ }
90
+ if (this.options.noClobber) {
91
+ const exists = await this.fileExists(outputPath);
92
+ if (exists) {
93
+ throw new Error(`File exists and noClobber is enabled: ${outputPath}`);
94
+ }
95
+ }
96
+ if (this.options.backups && this.options.backups > 0) {
97
+ await this.createBackup(outputPath);
98
+ }
99
+ await this.ensureDir(dirname(outputPath));
100
+ const stream = createWriteStream(outputPath);
101
+ stream.on("close", () => {
102
+ this.writtenFiles.set(url, outputPath);
103
+ });
104
+ return { stream, path: outputPath };
105
+ }
106
+ getOutputPath(url) {
107
+ if (this.options.output) {
108
+ return join(this.outputDir, this.options.output);
109
+ }
110
+ let parsed;
111
+ try {
112
+ parsed = new URL(url);
113
+ } catch {
114
+ return join(this.outputDir, this.hashUrl(url));
115
+ }
116
+ const parts = [];
117
+ if (this.options.protocolDirectories) {
118
+ parts.push(parsed.protocol.replace(":", ""));
119
+ }
120
+ if (!this.options.noHostDirectories && !this.options.noDirectories) {
121
+ parts.push(this.sanitizeFilename(parsed.hostname));
122
+ }
123
+ if (!this.options.noDirectories) {
124
+ let pathname = parsed.pathname;
125
+ if (this.options.cutDirs && this.options.cutDirs > 0) {
126
+ const pathParts = pathname.split("/").filter((p) => p);
127
+ pathname = "/" + pathParts.slice(this.options.cutDirs).join("/");
128
+ }
129
+ const pathParts = pathname.split("/").filter((p) => p);
130
+ for (const part of pathParts) {
131
+ parts.push(this.sanitizeFilename(part));
132
+ }
133
+ } else {
134
+ const filename = basename(parsed.pathname) || "index.html";
135
+ parts.push(this.sanitizeFilename(filename));
136
+ }
137
+ if (parts.length === 0 || parts[parts.length - 1] === "") {
138
+ parts.push("index.html");
139
+ }
140
+ const lastPart = parts[parts.length - 1];
141
+ if (!lastPart.includes(".") && !parsed.pathname.endsWith("/")) {}
142
+ if (parsed.search) {
143
+ const lastIndex = parts.length - 1;
144
+ const filename = parts[lastIndex];
145
+ const queryHash = this.hashString(parsed.search).substring(0, 8);
146
+ const ext = extname(filename);
147
+ const base = basename(filename, ext);
148
+ parts[lastIndex] = `${base}_${queryHash}${ext}`;
149
+ }
150
+ return join(this.outputDir, ...parts);
151
+ }
152
+ getRelativePath(fullPath) {
153
+ if (fullPath.startsWith(this.outputDir)) {
154
+ return fullPath.substring(this.outputDir.length).replace(/^[\/\\]/, "");
155
+ }
156
+ return fullPath;
157
+ }
158
+ isDocument(mimeType, url) {
159
+ if (mimeType) {
160
+ const normalizedMime = mimeType.split(";")[0].trim().toLowerCase();
161
+ if (normalizedMime === "text/html" || normalizedMime === "application/xhtml+xml") {
162
+ return true;
163
+ }
164
+ }
165
+ try {
166
+ const urlPath = new URL(url).pathname;
167
+ const ext = extname(urlPath).toLowerCase();
168
+ if (ext === ".html" || ext === ".htm" || ext === ".xhtml" || ext === "") {
169
+ return true;
170
+ }
171
+ } catch {}
172
+ return false;
173
+ }
174
+ getEntryFilePath(url, mimeType) {
175
+ try {
176
+ const parsed = new URL(url);
177
+ let filename = basename(parsed.pathname);
178
+ if (!filename || filename === "/") {
179
+ filename = this.isDocument(mimeType, url) ? "index.html" : "asset";
180
+ }
181
+ if (this.isDocument(mimeType, url)) {
182
+ const ext = extname(filename).toLowerCase();
183
+ if (!ext || ![".html", ".htm", ".xhtml"].includes(ext)) {
184
+ if (!ext) {
185
+ filename = filename + ".html";
186
+ }
187
+ }
188
+ }
189
+ return join(this.outputDir, this.sanitizeFilename(filename));
190
+ } catch {
191
+ return join(this.outputDir, this.isDocument(mimeType, url) ? "index.html" : "asset");
192
+ }
193
+ }
194
+ adjustExtension(path, mimeType) {
195
+ const currentExt = extname(path).toLowerCase();
196
+ const baseMime = mimeType.split(";")[0].trim().toLowerCase();
197
+ const expectedExt = MIME_EXTENSIONS[baseMime];
198
+ if (!expectedExt) {
199
+ return path;
200
+ }
201
+ if (!currentExt) {
202
+ return path + expectedExt;
203
+ }
204
+ if (currentExt === expectedExt) {
205
+ return path;
206
+ }
207
+ if (baseMime === "text/html") {
208
+ const htmlExts = [".html", ".htm", ".php", ".asp", ".aspx", ".jsp", ".xhtml"];
209
+ if (htmlExts.includes(currentExt)) {
210
+ return path;
211
+ }
212
+ if (![".txt", ".md", ".shtml"].includes(currentExt)) {
213
+ return path + ".html";
214
+ }
215
+ }
216
+ return path;
217
+ }
218
+ sanitizeFilename(name) {
219
+ const mode = this.options.restrictFileNames || "unix";
220
+ let sanitized = name;
221
+ if (mode === "windows") {
222
+ sanitized = sanitized.replace(UNSAFE_CHARS.windows, "_");
223
+ } else if (mode === "ascii") {
224
+ sanitized = sanitized.replace(UNSAFE_CHARS.ascii, "_");
225
+ } else {
226
+ sanitized = sanitized.replace(UNSAFE_CHARS.unix, "_");
227
+ }
228
+ if (mode === "lowercase") {
229
+ sanitized = sanitized.toLowerCase();
230
+ } else if (mode === "uppercase") {
231
+ sanitized = sanitized.toUpperCase();
232
+ }
233
+ try {
234
+ sanitized = decodeURIComponent(sanitized);
235
+ } catch {}
236
+ if (mode === "windows") {
237
+ sanitized = sanitized.replace(UNSAFE_CHARS.windows, "_");
238
+ } else if (mode === "ascii") {
239
+ sanitized = sanitized.replace(UNSAFE_CHARS.ascii, "_");
240
+ } else {
241
+ sanitized = sanitized.replace(UNSAFE_CHARS.unix, "_");
242
+ }
243
+ sanitized = sanitized.replace(/_+/g, "_");
244
+ sanitized = sanitized.replace(/^[_\s]+|[_\s]+$/g, "");
245
+ if (!sanitized || sanitized === "." || sanitized === "..") {
246
+ return "unnamed";
247
+ }
248
+ if (sanitized.length > 200) {
249
+ const ext = extname(sanitized);
250
+ const base = basename(sanitized, ext);
251
+ sanitized = base.substring(0, 200 - ext.length) + ext;
252
+ }
253
+ return sanitized;
254
+ }
255
+ async createBackup(path) {
256
+ const exists = await this.fileExists(path);
257
+ if (!exists)
258
+ return;
259
+ const maxBackups = this.options.backups || 1;
260
+ for (let i = maxBackups - 1;i >= 1; i--) {
261
+ const oldBackup = `${path}.${i}`;
262
+ const newBackup = `${path}.${i + 1}`;
263
+ const oldExists = await this.fileExists(oldBackup);
264
+ if (oldExists) {
265
+ if (i + 1 > maxBackups) {
266
+ await fs.unlink(oldBackup);
267
+ } else {
268
+ await fs.rename(oldBackup, newBackup);
269
+ }
270
+ }
271
+ }
272
+ await fs.rename(path, `${path}.1`);
273
+ }
274
+ async ensureDir(dir) {
275
+ try {
276
+ await fs.mkdir(dir, { recursive: true });
277
+ } catch (error) {
278
+ if (error.code !== "EEXIST") {
279
+ throw error;
280
+ }
281
+ }
282
+ }
283
+ async fileExists(path) {
284
+ try {
285
+ await fs.access(path);
286
+ return true;
287
+ } catch {
288
+ return false;
289
+ }
290
+ }
291
+ async getFileStats(path) {
292
+ try {
293
+ const stats = await fs.stat(path);
294
+ return {
295
+ size: stats.size,
296
+ mtime: stats.mtime
297
+ };
298
+ } catch {
299
+ return null;
300
+ }
301
+ }
302
+ async setMtime(path, mtime) {
303
+ try {
304
+ await fs.utimes(path, mtime, mtime);
305
+ } catch {}
306
+ }
307
+ hashUrl(url) {
308
+ return this.hashString(url) + ".html";
309
+ }
310
+ hashString(str) {
311
+ let hash = 0;
312
+ for (let i = 0;i < str.length; i++) {
313
+ const char = str.charCodeAt(i);
314
+ hash = (hash << 5) - hash + char;
315
+ hash = hash & hash;
316
+ }
317
+ return Math.abs(hash).toString(36);
318
+ }
319
+ getUrlMap() {
320
+ return new Map(this.writtenFiles);
321
+ }
322
+ getPathForUrl(url) {
323
+ return this.writtenFiles.get(url);
324
+ }
325
+ hasUrl(url) {
326
+ return this.writtenFiles.has(url);
327
+ }
328
+ async deleteFile(path) {
329
+ try {
330
+ await fs.unlink(path);
331
+ } catch {}
332
+ }
333
+ setOutputDir(dir) {
334
+ this.outputDir = dir;
335
+ }
336
+ getOutputDir() {
337
+ return this.outputDir;
338
+ }
339
+ getAssetStats() {
340
+ return this.assetOrganizer?.getStats() ?? null;
341
+ }
342
+ getAssetOrganizer() {
343
+ return this.assetOrganizer;
344
+ }
345
+ }
346
+
347
+ exports.FileWriter = FileWriter;
348
+ exports.default = FileWriter;
349
+ module.exports = Object.assign(FileWriter, exports);
@@ -0,0 +1,346 @@
1
+ import { promises as fs, createWriteStream } from "node:fs";
2
+ import { dirname, join, basename, extname } from "node:path";
3
+ import { AssetOrganizer } from './asset-organizer.js';
4
+ const MIME_EXTENSIONS = {
5
+ "text/html": ".html",
6
+ "text/css": ".css",
7
+ "text/javascript": ".js",
8
+ "application/javascript": ".js",
9
+ "application/json": ".json",
10
+ "application/xml": ".xml",
11
+ "text/xml": ".xml",
12
+ "image/png": ".png",
13
+ "image/jpeg": ".jpg",
14
+ "image/gif": ".gif",
15
+ "image/webp": ".webp",
16
+ "image/svg+xml": ".svg",
17
+ "application/pdf": ".pdf",
18
+ "font/woff": ".woff",
19
+ "font/woff2": ".woff2",
20
+ "application/font-woff": ".woff",
21
+ "application/font-woff2": ".woff2",
22
+ "font/ttf": ".ttf",
23
+ "application/x-font-ttf": ".ttf",
24
+ "font/otf": ".otf"
25
+ };
26
+ const UNSAFE_CHARS = {
27
+ unix: /[\x00-\x1f\/]/g,
28
+ windows: /[\x00-\x1f\/\\:*?"<>|]/g,
29
+ ascii: /[^\x20-\x7e]/g
30
+ };
31
+
32
+ export class FileWriter {
33
+ options;
34
+ outputDir;
35
+ writtenFiles = new Map;
36
+ assetOrganizer = null;
37
+ entryUrls = new Set;
38
+ constructor(options) {
39
+ this.options = options;
40
+ this.outputDir = options.outputDir || ".";
41
+ if (options.organizeAssets) {
42
+ this.assetOrganizer = new AssetOrganizer(options);
43
+ }
44
+ }
45
+ markAsEntry(url) {
46
+ this.entryUrls.add(url);
47
+ }
48
+ isEntryUrl(url) {
49
+ return this.entryUrls.has(url);
50
+ }
51
+ async write(url, content, mimeType) {
52
+ const buffer = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
53
+ let outputPath;
54
+ if (this.options.organizeAssets && this.isEntryUrl(url)) {
55
+ outputPath = this.getEntryFilePath(url, mimeType);
56
+ } else if (this.assetOrganizer?.shouldOrganize(mimeType, url)) {
57
+ const result = this.assetOrganizer.getOrganizedPath(url, buffer, mimeType);
58
+ if (result.isDuplicate) {
59
+ const fullPath = join(this.outputDir, result.path);
60
+ this.writtenFiles.set(url, fullPath);
61
+ return fullPath;
62
+ }
63
+ outputPath = join(this.outputDir, result.path);
64
+ } else {
65
+ outputPath = this.getOutputPath(url);
66
+ if (this.options.adjustExtension) {
67
+ outputPath = this.adjustExtension(outputPath, mimeType);
68
+ }
69
+ }
70
+ if (this.options.noClobber) {
71
+ const exists = await this.fileExists(outputPath);
72
+ if (exists) {
73
+ this.writtenFiles.set(url, outputPath);
74
+ return outputPath;
75
+ }
76
+ }
77
+ if (this.options.backups && this.options.backups > 0) {
78
+ await this.createBackup(outputPath);
79
+ }
80
+ await this.ensureDir(dirname(outputPath));
81
+ await fs.writeFile(outputPath, buffer);
82
+ this.writtenFiles.set(url, outputPath);
83
+ return outputPath;
84
+ }
85
+ async createWriteStream(url, mimeType) {
86
+ let outputPath = this.getOutputPath(url);
87
+ if (mimeType && this.options.adjustExtension) {
88
+ outputPath = this.adjustExtension(outputPath, mimeType);
89
+ }
90
+ if (this.options.noClobber) {
91
+ const exists = await this.fileExists(outputPath);
92
+ if (exists) {
93
+ throw new Error(`File exists and noClobber is enabled: ${outputPath}`);
94
+ }
95
+ }
96
+ if (this.options.backups && this.options.backups > 0) {
97
+ await this.createBackup(outputPath);
98
+ }
99
+ await this.ensureDir(dirname(outputPath));
100
+ const stream = createWriteStream(outputPath);
101
+ stream.on("close", () => {
102
+ this.writtenFiles.set(url, outputPath);
103
+ });
104
+ return { stream, path: outputPath };
105
+ }
106
+ getOutputPath(url) {
107
+ if (this.options.output) {
108
+ return join(this.outputDir, this.options.output);
109
+ }
110
+ let parsed;
111
+ try {
112
+ parsed = new URL(url);
113
+ } catch {
114
+ return join(this.outputDir, this.hashUrl(url));
115
+ }
116
+ const parts = [];
117
+ if (this.options.protocolDirectories) {
118
+ parts.push(parsed.protocol.replace(":", ""));
119
+ }
120
+ if (!this.options.noHostDirectories && !this.options.noDirectories) {
121
+ parts.push(this.sanitizeFilename(parsed.hostname));
122
+ }
123
+ if (!this.options.noDirectories) {
124
+ let pathname = parsed.pathname;
125
+ if (this.options.cutDirs && this.options.cutDirs > 0) {
126
+ const pathParts = pathname.split("/").filter((p) => p);
127
+ pathname = "/" + pathParts.slice(this.options.cutDirs).join("/");
128
+ }
129
+ const pathParts = pathname.split("/").filter((p) => p);
130
+ for (const part of pathParts) {
131
+ parts.push(this.sanitizeFilename(part));
132
+ }
133
+ } else {
134
+ const filename = basename(parsed.pathname) || "index.html";
135
+ parts.push(this.sanitizeFilename(filename));
136
+ }
137
+ if (parts.length === 0 || parts[parts.length - 1] === "") {
138
+ parts.push("index.html");
139
+ }
140
+ const lastPart = parts[parts.length - 1];
141
+ if (!lastPart.includes(".") && !parsed.pathname.endsWith("/")) {}
142
+ if (parsed.search) {
143
+ const lastIndex = parts.length - 1;
144
+ const filename = parts[lastIndex];
145
+ const queryHash = this.hashString(parsed.search).substring(0, 8);
146
+ const ext = extname(filename);
147
+ const base = basename(filename, ext);
148
+ parts[lastIndex] = `${base}_${queryHash}${ext}`;
149
+ }
150
+ return join(this.outputDir, ...parts);
151
+ }
152
+ getRelativePath(fullPath) {
153
+ if (fullPath.startsWith(this.outputDir)) {
154
+ return fullPath.substring(this.outputDir.length).replace(/^[\/\\]/, "");
155
+ }
156
+ return fullPath;
157
+ }
158
+ isDocument(mimeType, url) {
159
+ if (mimeType) {
160
+ const normalizedMime = mimeType.split(";")[0].trim().toLowerCase();
161
+ if (normalizedMime === "text/html" || normalizedMime === "application/xhtml+xml") {
162
+ return true;
163
+ }
164
+ }
165
+ try {
166
+ const urlPath = new URL(url).pathname;
167
+ const ext = extname(urlPath).toLowerCase();
168
+ if (ext === ".html" || ext === ".htm" || ext === ".xhtml" || ext === "") {
169
+ return true;
170
+ }
171
+ } catch {}
172
+ return false;
173
+ }
174
+ getEntryFilePath(url, mimeType) {
175
+ try {
176
+ const parsed = new URL(url);
177
+ let filename = basename(parsed.pathname);
178
+ if (!filename || filename === "/") {
179
+ filename = this.isDocument(mimeType, url) ? "index.html" : "asset";
180
+ }
181
+ if (this.isDocument(mimeType, url)) {
182
+ const ext = extname(filename).toLowerCase();
183
+ if (!ext || ![".html", ".htm", ".xhtml"].includes(ext)) {
184
+ if (!ext) {
185
+ filename = filename + ".html";
186
+ }
187
+ }
188
+ }
189
+ return join(this.outputDir, this.sanitizeFilename(filename));
190
+ } catch {
191
+ return join(this.outputDir, this.isDocument(mimeType, url) ? "index.html" : "asset");
192
+ }
193
+ }
194
+ adjustExtension(path, mimeType) {
195
+ const currentExt = extname(path).toLowerCase();
196
+ const baseMime = mimeType.split(";")[0].trim().toLowerCase();
197
+ const expectedExt = MIME_EXTENSIONS[baseMime];
198
+ if (!expectedExt) {
199
+ return path;
200
+ }
201
+ if (!currentExt) {
202
+ return path + expectedExt;
203
+ }
204
+ if (currentExt === expectedExt) {
205
+ return path;
206
+ }
207
+ if (baseMime === "text/html") {
208
+ const htmlExts = [".html", ".htm", ".php", ".asp", ".aspx", ".jsp", ".xhtml"];
209
+ if (htmlExts.includes(currentExt)) {
210
+ return path;
211
+ }
212
+ if (![".txt", ".md", ".shtml"].includes(currentExt)) {
213
+ return path + ".html";
214
+ }
215
+ }
216
+ return path;
217
+ }
218
+ sanitizeFilename(name) {
219
+ const mode = this.options.restrictFileNames || "unix";
220
+ let sanitized = name;
221
+ if (mode === "windows") {
222
+ sanitized = sanitized.replace(UNSAFE_CHARS.windows, "_");
223
+ } else if (mode === "ascii") {
224
+ sanitized = sanitized.replace(UNSAFE_CHARS.ascii, "_");
225
+ } else {
226
+ sanitized = sanitized.replace(UNSAFE_CHARS.unix, "_");
227
+ }
228
+ if (mode === "lowercase") {
229
+ sanitized = sanitized.toLowerCase();
230
+ } else if (mode === "uppercase") {
231
+ sanitized = sanitized.toUpperCase();
232
+ }
233
+ try {
234
+ sanitized = decodeURIComponent(sanitized);
235
+ } catch {}
236
+ if (mode === "windows") {
237
+ sanitized = sanitized.replace(UNSAFE_CHARS.windows, "_");
238
+ } else if (mode === "ascii") {
239
+ sanitized = sanitized.replace(UNSAFE_CHARS.ascii, "_");
240
+ } else {
241
+ sanitized = sanitized.replace(UNSAFE_CHARS.unix, "_");
242
+ }
243
+ sanitized = sanitized.replace(/_+/g, "_");
244
+ sanitized = sanitized.replace(/^[_\s]+|[_\s]+$/g, "");
245
+ if (!sanitized || sanitized === "." || sanitized === "..") {
246
+ return "unnamed";
247
+ }
248
+ if (sanitized.length > 200) {
249
+ const ext = extname(sanitized);
250
+ const base = basename(sanitized, ext);
251
+ sanitized = base.substring(0, 200 - ext.length) + ext;
252
+ }
253
+ return sanitized;
254
+ }
255
+ async createBackup(path) {
256
+ const exists = await this.fileExists(path);
257
+ if (!exists)
258
+ return;
259
+ const maxBackups = this.options.backups || 1;
260
+ for (let i = maxBackups - 1;i >= 1; i--) {
261
+ const oldBackup = `${path}.${i}`;
262
+ const newBackup = `${path}.${i + 1}`;
263
+ const oldExists = await this.fileExists(oldBackup);
264
+ if (oldExists) {
265
+ if (i + 1 > maxBackups) {
266
+ await fs.unlink(oldBackup);
267
+ } else {
268
+ await fs.rename(oldBackup, newBackup);
269
+ }
270
+ }
271
+ }
272
+ await fs.rename(path, `${path}.1`);
273
+ }
274
+ async ensureDir(dir) {
275
+ try {
276
+ await fs.mkdir(dir, { recursive: true });
277
+ } catch (error) {
278
+ if (error.code !== "EEXIST") {
279
+ throw error;
280
+ }
281
+ }
282
+ }
283
+ async fileExists(path) {
284
+ try {
285
+ await fs.access(path);
286
+ return true;
287
+ } catch {
288
+ return false;
289
+ }
290
+ }
291
+ async getFileStats(path) {
292
+ try {
293
+ const stats = await fs.stat(path);
294
+ return {
295
+ size: stats.size,
296
+ mtime: stats.mtime
297
+ };
298
+ } catch {
299
+ return null;
300
+ }
301
+ }
302
+ async setMtime(path, mtime) {
303
+ try {
304
+ await fs.utimes(path, mtime, mtime);
305
+ } catch {}
306
+ }
307
+ hashUrl(url) {
308
+ return this.hashString(url) + ".html";
309
+ }
310
+ hashString(str) {
311
+ let hash = 0;
312
+ for (let i = 0;i < str.length; i++) {
313
+ const char = str.charCodeAt(i);
314
+ hash = (hash << 5) - hash + char;
315
+ hash = hash & hash;
316
+ }
317
+ return Math.abs(hash).toString(36);
318
+ }
319
+ getUrlMap() {
320
+ return new Map(this.writtenFiles);
321
+ }
322
+ getPathForUrl(url) {
323
+ return this.writtenFiles.get(url);
324
+ }
325
+ hasUrl(url) {
326
+ return this.writtenFiles.has(url);
327
+ }
328
+ async deleteFile(path) {
329
+ try {
330
+ await fs.unlink(path);
331
+ } catch {}
332
+ }
333
+ setOutputDir(dir) {
334
+ this.outputDir = dir;
335
+ }
336
+ getOutputDir() {
337
+ return this.outputDir;
338
+ }
339
+ getAssetStats() {
340
+ return this.assetOrganizer?.getStats() ?? null;
341
+ }
342
+ getAssetOrganizer() {
343
+ return this.assetOrganizer;
344
+ }
345
+ }
346
+ export default FileWriter;