smippo 0.0.1

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.
@@ -0,0 +1,76 @@
1
+ import {dirname, relative, join, extname, basename} from 'path';
2
+
3
+ /**
4
+ * Calculate relative path from one file to another
5
+ */
6
+ export function getRelativePath(from, to) {
7
+ const fromDir = dirname(from);
8
+ let rel = relative(fromDir, to);
9
+
10
+ // Ensure forward slashes
11
+ rel = rel.replace(/\\/g, '/');
12
+
13
+ // Add ./ prefix if needed
14
+ if (!rel.startsWith('.') && !rel.startsWith('/')) {
15
+ rel = './' + rel;
16
+ }
17
+
18
+ return rel;
19
+ }
20
+
21
+ /**
22
+ * Sanitize a path component for the filesystem
23
+ */
24
+ export function sanitizePath(str) {
25
+ return str
26
+ .replace(/[<>:"|?*]/g, '_') // Invalid chars
27
+ .replace(/\.\./g, '_') // No directory traversal
28
+ .replace(/\/+/g, '/') // Collapse multiple slashes
29
+ .slice(0, 200); // Limit length
30
+ }
31
+
32
+ /**
33
+ * Ensure a path has an extension, adding the default if needed
34
+ */
35
+ export function ensureExtension(filePath, defaultExt = '.html') {
36
+ const ext = extname(filePath);
37
+ if (!ext) {
38
+ return filePath + defaultExt;
39
+ }
40
+ return filePath;
41
+ }
42
+
43
+ /**
44
+ * Get a unique filename by adding a number suffix if needed
45
+ */
46
+ export function getUniqueFilename(basePath, existingPaths) {
47
+ if (!existingPaths.has(basePath)) {
48
+ return basePath;
49
+ }
50
+
51
+ const ext = extname(basePath);
52
+ const base = basePath.slice(0, -ext.length || undefined);
53
+
54
+ let counter = 1;
55
+ let newPath;
56
+ do {
57
+ newPath = `${base}-${counter}${ext}`;
58
+ counter++;
59
+ } while (existingPaths.has(newPath));
60
+
61
+ return newPath;
62
+ }
63
+
64
+ /**
65
+ * Join paths and normalize
66
+ */
67
+ export function joinPath(...parts) {
68
+ return join(...parts).replace(/\\/g, '/');
69
+ }
70
+
71
+ /**
72
+ * Get file basename without extension
73
+ */
74
+ export function getBasename(filePath) {
75
+ return basename(filePath, extname(filePath));
76
+ }
@@ -0,0 +1,295 @@
1
+ /**
2
+ * URL utilities for Smippo
3
+ */
4
+
5
+ /**
6
+ * Normalize a URL for comparison and storage
7
+ */
8
+ export function normalizeUrl(url) {
9
+ try {
10
+ const parsed = new URL(url);
11
+ // Remove trailing slash for non-root paths
12
+ if (parsed.pathname !== '/' && parsed.pathname.endsWith('/')) {
13
+ parsed.pathname = parsed.pathname.slice(0, -1);
14
+ }
15
+ // Remove default ports
16
+ if (
17
+ (parsed.protocol === 'http:' && parsed.port === '80') ||
18
+ (parsed.protocol === 'https:' && parsed.port === '443')
19
+ ) {
20
+ parsed.port = '';
21
+ }
22
+ // Sort query params for consistency
23
+ parsed.searchParams.sort();
24
+ return parsed.href;
25
+ } catch {
26
+ return url;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Get the base URL (origin + pathname without file)
32
+ */
33
+ export function getBaseUrl(url) {
34
+ try {
35
+ const parsed = new URL(url);
36
+ const pathParts = parsed.pathname.split('/');
37
+ // Remove filename if present
38
+ if (pathParts[pathParts.length - 1].includes('.')) {
39
+ pathParts.pop();
40
+ }
41
+ parsed.pathname = pathParts.join('/');
42
+ parsed.search = '';
43
+ parsed.hash = '';
44
+ return parsed.href;
45
+ } catch {
46
+ return url;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Resolve a relative URL against a base URL
52
+ */
53
+ export function resolveUrl(relative, base) {
54
+ try {
55
+ return new URL(relative, base).href;
56
+ } catch {
57
+ return relative;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Check if two URLs are on the same origin
63
+ */
64
+ export function isSameOrigin(url1, url2) {
65
+ try {
66
+ const parsed1 = new URL(url1);
67
+ const parsed2 = new URL(url2);
68
+ return parsed1.origin === parsed2.origin;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Check if URL is on the same domain (ignores subdomain)
76
+ */
77
+ export function isSameDomain(url1, url2) {
78
+ try {
79
+ const parsed1 = new URL(url1);
80
+ const parsed2 = new URL(url2);
81
+ return getRootDomain(parsed1.hostname) === getRootDomain(parsed2.hostname);
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Check if URL is on the same TLD
89
+ */
90
+ export function isSameTld(url1, url2) {
91
+ try {
92
+ const parsed1 = new URL(url1);
93
+ const parsed2 = new URL(url2);
94
+ return getTld(parsed1.hostname) === getTld(parsed2.hostname);
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Check if URL is within the same directory tree
102
+ */
103
+ export function isInDirectory(url, baseUrl) {
104
+ try {
105
+ const parsed = new URL(url);
106
+ const baseParsed = new URL(baseUrl);
107
+
108
+ if (parsed.origin !== baseParsed.origin) return false;
109
+
110
+ const basePath = baseParsed.pathname.endsWith('/')
111
+ ? baseParsed.pathname
112
+ : baseParsed.pathname.replace(/\/[^/]*$/, '/');
113
+
114
+ return parsed.pathname.startsWith(basePath);
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Extract root domain from hostname (e.g., "www.example.com" -> "example.com")
122
+ */
123
+ export function getRootDomain(hostname) {
124
+ const parts = hostname.split('.');
125
+ // Handle special cases like co.uk, com.au
126
+ const specialTlds = ['co.uk', 'com.au', 'co.nz', 'org.uk'];
127
+ const lastTwo = parts.slice(-2).join('.');
128
+
129
+ if (specialTlds.includes(lastTwo)) {
130
+ return parts.slice(-3).join('.');
131
+ }
132
+
133
+ return parts.slice(-2).join('.');
134
+ }
135
+
136
+ /**
137
+ * Get TLD from hostname
138
+ */
139
+ export function getTld(hostname) {
140
+ const parts = hostname.split('.');
141
+ return parts[parts.length - 1];
142
+ }
143
+
144
+ /**
145
+ * Check if URL should be followed based on scope
146
+ */
147
+ export function isInScope(url, baseUrl, scope, stayInDir = false) {
148
+ if (stayInDir && !isInDirectory(url, baseUrl)) {
149
+ return false;
150
+ }
151
+
152
+ switch (scope) {
153
+ case 'subdomain':
154
+ return isSameOrigin(url, baseUrl);
155
+ case 'domain':
156
+ return isSameDomain(url, baseUrl);
157
+ case 'tld':
158
+ return isSameTld(url, baseUrl);
159
+ case 'all':
160
+ return true;
161
+ default:
162
+ return isSameDomain(url, baseUrl);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Convert URL to a local file path
168
+ */
169
+ export function urlToPath(url, structure = 'original') {
170
+ try {
171
+ const parsed = new URL(url);
172
+ let pathname = parsed.pathname;
173
+
174
+ // Handle root path
175
+ if (pathname === '/' || pathname === '') {
176
+ pathname = '/index.html';
177
+ }
178
+
179
+ // Add index.html for directory paths
180
+ if (pathname.endsWith('/')) {
181
+ pathname += 'index.html';
182
+ }
183
+
184
+ // Add .html extension if no extension and not a known file type
185
+ const hasExtension = /\.[a-z0-9]+$/i.test(pathname);
186
+ if (!hasExtension) {
187
+ pathname += '.html';
188
+ }
189
+
190
+ // Handle query strings
191
+ if (parsed.search) {
192
+ const hash = simpleHash(parsed.search);
193
+ const ext = pathname.match(/\.[^.]+$/)?.[0] || '';
194
+ const base = pathname.slice(0, -ext.length || undefined);
195
+ pathname = `${base}-${hash}${ext}`;
196
+ }
197
+
198
+ switch (structure) {
199
+ case 'flat': {
200
+ // Flatten to single directory with hashed names
201
+ const flatName = pathname.replace(/\//g, '-').replace(/^-/, '');
202
+ return flatName;
203
+ }
204
+
205
+ case 'domain':
206
+ // Include full hostname
207
+ return `${parsed.hostname}${pathname}`;
208
+
209
+ case 'original':
210
+ default: {
211
+ // Include hostname without www
212
+ const host = parsed.hostname.replace(/^www\./, '');
213
+ return `${host}${pathname}`;
214
+ }
215
+ }
216
+ } catch {
217
+ return 'unknown/index.html';
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Simple hash function for query strings
223
+ */
224
+ function simpleHash(str) {
225
+ let hash = 0;
226
+ for (let i = 0; i < str.length; i++) {
227
+ const char = str.charCodeAt(i);
228
+ hash = (hash << 5) - hash + char;
229
+ hash = hash & hash;
230
+ }
231
+ return Math.abs(hash).toString(16).slice(0, 8);
232
+ }
233
+
234
+ /**
235
+ * Check if URL is likely a page (vs asset)
236
+ */
237
+ export function isLikelyPage(url) {
238
+ const assetExtensions = [
239
+ '.css',
240
+ '.js',
241
+ '.json',
242
+ '.xml',
243
+ '.png',
244
+ '.jpg',
245
+ '.jpeg',
246
+ '.gif',
247
+ '.webp',
248
+ '.svg',
249
+ '.ico',
250
+ '.bmp',
251
+ '.woff',
252
+ '.woff2',
253
+ '.ttf',
254
+ '.eot',
255
+ '.otf',
256
+ '.mp3',
257
+ '.mp4',
258
+ '.webm',
259
+ '.ogg',
260
+ '.wav',
261
+ '.pdf',
262
+ '.zip',
263
+ '.tar',
264
+ '.gz',
265
+ '.map',
266
+ ];
267
+
268
+ try {
269
+ const parsed = new URL(url);
270
+ const pathname = parsed.pathname.toLowerCase();
271
+ return !assetExtensions.some(ext => pathname.endsWith(ext));
272
+ } catch {
273
+ return true;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Check if URL is an asset
279
+ */
280
+ export function isAsset(url) {
281
+ return !isLikelyPage(url);
282
+ }
283
+
284
+ /**
285
+ * Get file extension from URL
286
+ */
287
+ export function getExtension(url) {
288
+ try {
289
+ const parsed = new URL(url);
290
+ const match = parsed.pathname.match(/\.([a-z0-9]+)$/i);
291
+ return match ? match[1].toLowerCase() : '';
292
+ } catch {
293
+ return '';
294
+ }
295
+ }
@@ -0,0 +1,14 @@
1
+ import {readFileSync} from 'fs';
2
+ import {fileURLToPath} from 'url';
3
+ import {dirname, join} from 'path';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+
7
+ let pkg;
8
+ try {
9
+ pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8'));
10
+ } catch {
11
+ pkg = {version: '1.0.0'};
12
+ }
13
+
14
+ export const version = pkg.version;