Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
at main 15 kB view raw
1import { describe, test, expect } from 'bun:test' 2import { rewriteHtmlPaths, isHtmlContent } from './html-rewriter' 3 4describe('rewriteHtmlPaths', () => { 5 const basePath = '/identifier/site/' 6 7 describe('absolute paths', () => { 8 test('rewrites absolute paths with leading slash', () => { 9 const html = '<img src="/image.png">' 10 const result = rewriteHtmlPaths(html, basePath, 'index.html') 11 expect(result).toBe('<img src="/identifier/site/image.png">') 12 }) 13 14 test('rewrites nested absolute paths', () => { 15 const html = '<link href="/css/style.css">' 16 const result = rewriteHtmlPaths(html, basePath, 'index.html') 17 expect(result).toBe('<link href="/identifier/site/css/style.css">') 18 }) 19 }) 20 21 describe('relative paths from root document', () => { 22 test('rewrites relative paths with ./ prefix', () => { 23 const html = '<img src="./image.png">' 24 const result = rewriteHtmlPaths(html, basePath, 'index.html') 25 expect(result).toBe('<img src="/identifier/site/image.png">') 26 }) 27 28 test('rewrites relative paths without prefix', () => { 29 const html = '<img src="image.png">' 30 const result = rewriteHtmlPaths(html, basePath, 'index.html') 31 expect(result).toBe('<img src="/identifier/site/image.png">') 32 }) 33 34 test('rewrites relative paths with ../ (should stay at root)', () => { 35 const html = '<img src="../image.png">' 36 const result = rewriteHtmlPaths(html, basePath, 'index.html') 37 expect(result).toBe('<img src="/identifier/site/image.png">') 38 }) 39 }) 40 41 describe('relative paths from nested documents', () => { 42 test('rewrites relative path from nested document', () => { 43 const html = '<img src="./photo.jpg">' 44 const result = rewriteHtmlPaths( 45 html, 46 basePath, 47 'folder1/folder2/index.html' 48 ) 49 expect(result).toBe( 50 '<img src="/identifier/site/folder1/folder2/photo.jpg">' 51 ) 52 }) 53 54 test('rewrites plain filename from nested document', () => { 55 const html = '<script src="app.js"></script>' 56 const result = rewriteHtmlPaths( 57 html, 58 basePath, 59 'folder1/folder2/index.html' 60 ) 61 expect(result).toBe( 62 '<script src="/identifier/site/folder1/folder2/app.js"></script>' 63 ) 64 }) 65 66 test('rewrites ../ to go up one level', () => { 67 const html = '<img src="../image.png">' 68 const result = rewriteHtmlPaths( 69 html, 70 basePath, 71 'folder1/folder2/folder3/index.html' 72 ) 73 expect(result).toBe( 74 '<img src="/identifier/site/folder1/folder2/image.png">' 75 ) 76 }) 77 78 test('rewrites multiple ../ to go up multiple levels', () => { 79 const html = '<link href="../../css/style.css">' 80 const result = rewriteHtmlPaths( 81 html, 82 basePath, 83 'folder1/folder2/folder3/index.html' 84 ) 85 expect(result).toBe( 86 '<link href="/identifier/site/folder1/css/style.css">' 87 ) 88 }) 89 90 test('rewrites ../ with additional path segments', () => { 91 const html = '<img src="../assets/logo.png">' 92 const result = rewriteHtmlPaths( 93 html, 94 basePath, 95 'pages/about/index.html' 96 ) 97 expect(result).toBe( 98 '<img src="/identifier/site/pages/assets/logo.png">' 99 ) 100 }) 101 102 test('handles complex nested relative paths', () => { 103 const html = '<script src="../../lib/vendor/jquery.js"></script>' 104 const result = rewriteHtmlPaths( 105 html, 106 basePath, 107 'pages/blog/post/index.html' 108 ) 109 expect(result).toBe( 110 '<script src="/identifier/site/pages/lib/vendor/jquery.js"></script>' 111 ) 112 }) 113 114 test('handles ../ going past root (stays at root)', () => { 115 const html = '<img src="../../../image.png">' 116 const result = rewriteHtmlPaths(html, basePath, 'folder1/index.html') 117 expect(result).toBe('<img src="/identifier/site/image.png">') 118 }) 119 }) 120 121 describe('external URLs and special schemes', () => { 122 test('does not rewrite http URLs', () => { 123 const html = '<img src="http://example.com/image.png">' 124 const result = rewriteHtmlPaths(html, basePath, 'index.html') 125 expect(result).toBe('<img src="http://example.com/image.png">') 126 }) 127 128 test('does not rewrite https URLs', () => { 129 const html = '<link href="https://cdn.example.com/style.css">' 130 const result = rewriteHtmlPaths(html, basePath, 'index.html') 131 expect(result).toBe( 132 '<link href="https://cdn.example.com/style.css">' 133 ) 134 }) 135 136 test('does not rewrite protocol-relative URLs', () => { 137 const html = '<script src="//cdn.example.com/script.js"></script>' 138 const result = rewriteHtmlPaths(html, basePath, 'index.html') 139 expect(result).toBe( 140 '<script src="//cdn.example.com/script.js"></script>' 141 ) 142 }) 143 144 test('does not rewrite data URIs', () => { 145 const html = 146 '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA">' 147 const result = rewriteHtmlPaths(html, basePath, 'index.html') 148 expect(result).toBe( 149 '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA">' 150 ) 151 }) 152 153 test('does not rewrite mailto links', () => { 154 const html = '<a href="mailto:test@example.com">Email</a>' 155 const result = rewriteHtmlPaths(html, basePath, 'index.html') 156 expect(result).toBe('<a href="mailto:test@example.com">Email</a>') 157 }) 158 159 test('does not rewrite tel links', () => { 160 const html = '<a href="tel:+1234567890">Call</a>' 161 const result = rewriteHtmlPaths(html, basePath, 'index.html') 162 expect(result).toBe('<a href="tel:+1234567890">Call</a>') 163 }) 164 }) 165 166 describe('different HTML attributes', () => { 167 test('rewrites src attribute', () => { 168 const html = '<img src="/image.png">' 169 const result = rewriteHtmlPaths(html, basePath, 'index.html') 170 expect(result).toBe('<img src="/identifier/site/image.png">') 171 }) 172 173 test('rewrites href attribute', () => { 174 const html = '<a href="/page.html">Link</a>' 175 const result = rewriteHtmlPaths(html, basePath, 'index.html') 176 expect(result).toBe('<a href="/identifier/site/page.html">Link</a>') 177 }) 178 179 test('rewrites action attribute', () => { 180 const html = '<form action="/submit"></form>' 181 const result = rewriteHtmlPaths(html, basePath, 'index.html') 182 expect(result).toBe('<form action="/identifier/site/submit"></form>') 183 }) 184 185 test('rewrites data attribute', () => { 186 const html = '<object data="/document.pdf"></object>' 187 const result = rewriteHtmlPaths(html, basePath, 'index.html') 188 expect(result).toBe( 189 '<object data="/identifier/site/document.pdf"></object>' 190 ) 191 }) 192 193 test('rewrites poster attribute', () => { 194 const html = '<video poster="/thumbnail.jpg"></video>' 195 const result = rewriteHtmlPaths(html, basePath, 'index.html') 196 expect(result).toBe( 197 '<video poster="/identifier/site/thumbnail.jpg"></video>' 198 ) 199 }) 200 201 test('rewrites srcset attribute with single URL', () => { 202 const html = '<img srcset="/image.png 1x">' 203 const result = rewriteHtmlPaths(html, basePath, 'index.html') 204 expect(result).toBe( 205 '<img srcset="/identifier/site/image.png 1x">' 206 ) 207 }) 208 209 test('rewrites srcset attribute with multiple URLs', () => { 210 const html = '<img srcset="/image-1x.png 1x, /image-2x.png 2x">' 211 const result = rewriteHtmlPaths(html, basePath, 'index.html') 212 expect(result).toBe( 213 '<img srcset="/identifier/site/image-1x.png 1x, /identifier/site/image-2x.png 2x">' 214 ) 215 }) 216 217 test('rewrites srcset with width descriptors', () => { 218 const html = '<img srcset="/small.jpg 320w, /large.jpg 1024w">' 219 const result = rewriteHtmlPaths(html, basePath, 'index.html') 220 expect(result).toBe( 221 '<img srcset="/identifier/site/small.jpg 320w, /identifier/site/large.jpg 1024w">' 222 ) 223 }) 224 225 test('rewrites srcset with relative paths from nested document', () => { 226 const html = '<img srcset="../img1.png 1x, ../img2.png 2x">' 227 const result = rewriteHtmlPaths( 228 html, 229 basePath, 230 'folder1/folder2/index.html' 231 ) 232 expect(result).toBe( 233 '<img srcset="/identifier/site/folder1/img1.png 1x, /identifier/site/folder1/img2.png 2x">' 234 ) 235 }) 236 }) 237 238 describe('quote handling', () => { 239 test('handles double quotes', () => { 240 const html = '<img src="/image.png">' 241 const result = rewriteHtmlPaths(html, basePath, 'index.html') 242 expect(result).toBe('<img src="/identifier/site/image.png">') 243 }) 244 245 test('handles single quotes', () => { 246 const html = "<img src='/image.png'>" 247 const result = rewriteHtmlPaths(html, basePath, 'index.html') 248 expect(result).toBe("<img src='/identifier/site/image.png'>") 249 }) 250 251 test('handles mixed quotes in same document', () => { 252 const html = '<img src="/img1.png"><link href=\'/style.css\'>' 253 const result = rewriteHtmlPaths(html, basePath, 'index.html') 254 expect(result).toBe( 255 '<img src="/identifier/site/img1.png"><link href=\'/identifier/site/style.css\'>' 256 ) 257 }) 258 }) 259 260 describe('multiple rewrites in same document', () => { 261 test('rewrites multiple attributes in complex HTML', () => { 262 const html = ` 263<!DOCTYPE html> 264<html> 265<head> 266 <link href="/css/style.css" rel="stylesheet"> 267 <script src="/js/app.js"></script> 268</head> 269<body> 270 <img src="/images/logo.png" alt="Logo"> 271 <a href="/about.html">About</a> 272 <form action="/submit"> 273 <button type="submit">Submit</button> 274 </form> 275</body> 276</html> 277 ` 278 const result = rewriteHtmlPaths(html, basePath, 'index.html') 279 expect(result).toContain('href="/identifier/site/css/style.css"') 280 expect(result).toContain('src="/identifier/site/js/app.js"') 281 expect(result).toContain('src="/identifier/site/images/logo.png"') 282 expect(result).toContain('href="/identifier/site/about.html"') 283 expect(result).toContain('action="/identifier/site/submit"') 284 }) 285 286 test('handles mix of relative and absolute paths', () => { 287 const html = ` 288 <img src="/abs/image.png"> 289 <img src="./rel/image.png"> 290 <img src="../parent/image.png"> 291 <img src="https://external.com/image.png"> 292 ` 293 const result = rewriteHtmlPaths( 294 html, 295 basePath, 296 'folder1/folder2/page.html' 297 ) 298 expect(result).toContain('src="/identifier/site/abs/image.png"') 299 expect(result).toContain( 300 'src="/identifier/site/folder1/folder2/rel/image.png"' 301 ) 302 expect(result).toContain( 303 'src="/identifier/site/folder1/parent/image.png"' 304 ) 305 expect(result).toContain('src="https://external.com/image.png"') 306 }) 307 }) 308 309 describe('edge cases', () => { 310 test('handles empty src attribute', () => { 311 const html = '<img src="">' 312 const result = rewriteHtmlPaths(html, basePath, 'index.html') 313 expect(result).toBe('<img src="">') 314 }) 315 316 test('handles basePath without trailing slash', () => { 317 const html = '<img src="/image.png">' 318 const result = rewriteHtmlPaths(html, '/identifier/site', 'index.html') 319 expect(result).toBe('<img src="/identifier/site/image.png">') 320 }) 321 322 test('handles basePath with trailing slash', () => { 323 const html = '<img src="/image.png">' 324 const result = rewriteHtmlPaths( 325 html, 326 '/identifier/site/', 327 'index.html' 328 ) 329 expect(result).toBe('<img src="/identifier/site/image.png">') 330 }) 331 332 test('handles whitespace around equals sign', () => { 333 const html = '<img src = "/image.png">' 334 const result = rewriteHtmlPaths(html, basePath, 'index.html') 335 expect(result).toBe('<img src="/identifier/site/image.png">') 336 }) 337 338 test('preserves query strings in URLs', () => { 339 const html = '<img src="/image.png?v=123">' 340 const result = rewriteHtmlPaths(html, basePath, 'index.html') 341 expect(result).toBe('<img src="/identifier/site/image.png?v=123">') 342 }) 343 344 test('preserves hash fragments in URLs', () => { 345 const html = '<a href="/page.html#section">Link</a>' 346 const result = rewriteHtmlPaths(html, basePath, 'index.html') 347 expect(result).toBe( 348 '<a href="/identifier/site/page.html#section">Link</a>' 349 ) 350 }) 351 352 test('handles paths with special characters', () => { 353 const html = '<img src="/folder-name/file_name.png">' 354 const result = rewriteHtmlPaths(html, basePath, 'index.html') 355 expect(result).toBe( 356 '<img src="/identifier/site/folder-name/file_name.png">' 357 ) 358 }) 359 }) 360 361 describe('real-world scenario', () => { 362 test('handles the example from the bug report', () => { 363 // HTML file at: /folder1/folder2/folder3/index.html 364 // Image at: /folder1/folder2/img.png 365 // Reference: src="../img.png" 366 const html = '<img src="../img.png">' 367 const result = rewriteHtmlPaths( 368 html, 369 basePath, 370 'folder1/folder2/folder3/index.html' 371 ) 372 expect(result).toBe( 373 '<img src="/identifier/site/folder1/folder2/img.png">' 374 ) 375 }) 376 377 test('handles deeply nested static site structure', () => { 378 // A typical static site with nested pages and shared assets 379 const html = ` 380<!DOCTYPE html> 381<html> 382<head> 383 <link href="../../css/style.css" rel="stylesheet"> 384 <link href="../../css/theme.css" rel="stylesheet"> 385 <script src="../../js/main.js"></script> 386</head> 387<body> 388 <img src="../../images/logo.png" alt="Logo"> 389 <img src="./post-image.jpg" alt="Post"> 390 <a href="../index.html">Back to Blog</a> 391 <a href="../../index.html">Home</a> 392</body> 393</html> 394 ` 395 const result = rewriteHtmlPaths( 396 html, 397 basePath, 398 'blog/posts/my-post.html' 399 ) 400 401 // Assets two levels up 402 expect(result).toContain('href="/identifier/site/css/style.css"') 403 expect(result).toContain('href="/identifier/site/css/theme.css"') 404 expect(result).toContain('src="/identifier/site/js/main.js"') 405 expect(result).toContain('src="/identifier/site/images/logo.png"') 406 407 // Same directory 408 expect(result).toContain( 409 'src="/identifier/site/blog/posts/post-image.jpg"' 410 ) 411 412 // One level up 413 expect(result).toContain('href="/identifier/site/blog/index.html"') 414 415 // Two levels up 416 expect(result).toContain('href="/identifier/site/index.html"') 417 }) 418 }) 419}) 420 421describe('isHtmlContent', () => { 422 test('identifies HTML by content type', () => { 423 expect(isHtmlContent('file.txt', 'text/html')).toBe(true) 424 expect(isHtmlContent('file.txt', 'text/html; charset=utf-8')).toBe( 425 true 426 ) 427 }) 428 429 test('identifies HTML by .html extension', () => { 430 expect(isHtmlContent('index.html')).toBe(true) 431 expect(isHtmlContent('page.html', undefined)).toBe(true) 432 expect(isHtmlContent('/path/to/file.html')).toBe(true) 433 }) 434 435 test('identifies HTML by .htm extension', () => { 436 expect(isHtmlContent('index.htm')).toBe(true) 437 expect(isHtmlContent('page.htm', undefined)).toBe(true) 438 }) 439 440 test('handles case-insensitive extensions', () => { 441 expect(isHtmlContent('INDEX.HTML')).toBe(true) 442 expect(isHtmlContent('page.HTM')).toBe(true) 443 expect(isHtmlContent('File.HtMl')).toBe(true) 444 }) 445 446 test('returns false for non-HTML files', () => { 447 expect(isHtmlContent('script.js')).toBe(false) 448 expect(isHtmlContent('style.css')).toBe(false) 449 expect(isHtmlContent('image.png')).toBe(false) 450 expect(isHtmlContent('data.json')).toBe(false) 451 }) 452 453 test('returns false for files with no extension', () => { 454 expect(isHtmlContent('README')).toBe(false) 455 expect(isHtmlContent('Makefile')).toBe(false) 456 }) 457})