Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

use node-html-parser for html rewriting

+407 -115
+1
apps/firehose-service/package.json
··· 28 28 "hono": "^4.10.4", 29 29 "ioredis": "^5.9.2", 30 30 "multiformats": "^13.4.1", 31 + "node-html-parser": "^7.1.0", 31 32 "postgres": "^3.4.5" 32 33 }, 33 34 "devDependencies": {
+313
apps/firehose-service/src/lib/html-rewriter.test.ts
··· 1 + import { describe, expect, test } from 'bun:test' 2 + import { isHtmlFile, rewriteHtmlPaths } from './html-rewriter' 3 + 4 + const BASE = '/did:plc:abc123/mysite/' 5 + const ROOT_DOC = 'index.html' 6 + const NESTED_DOC = 'blog/posts/index.html' 7 + 8 + function rewrite(html: string, doc = ROOT_DOC) { 9 + return rewriteHtmlPaths(html, BASE, doc) 10 + } 11 + 12 + describe('rewritten attributes', () => { 13 + test('src', () => { 14 + expect(rewrite('<img src="/photo.jpg">')).toBe('<img src="/did:plc:abc123/mysite/photo.jpg">') 15 + }) 16 + 17 + test('href', () => { 18 + expect(rewrite('<a href="/about">About</a>')).toBe('<a href="/did:plc:abc123/mysite/about">About</a>') 19 + }) 20 + 21 + test('action', () => { 22 + expect(rewrite('<form action="/submit"></form>')).toBe('<form action="/did:plc:abc123/mysite/submit"></form>') 23 + }) 24 + 25 + test('data (object)', () => { 26 + expect(rewrite('<object data="/file.pdf"></object>')).toBe( 27 + '<object data="/did:plc:abc123/mysite/file.pdf"></object>', 28 + ) 29 + }) 30 + 31 + test('poster', () => { 32 + expect(rewrite('<video poster="/thumb.jpg"></video>')).toBe( 33 + '<video poster="/did:plc:abc123/mysite/thumb.jpg"></video>', 34 + ) 35 + }) 36 + 37 + test('link href', () => { 38 + expect(rewrite('<link rel="stylesheet" href="/style.css">')).toBe( 39 + '<link rel="stylesheet" href="/did:plc:abc123/mysite/style.css">', 40 + ) 41 + }) 42 + 43 + test('script src', () => { 44 + expect(rewrite('<script src="/app.js"></script>')).toBe('<script src="/did:plc:abc123/mysite/app.js"></script>') 45 + }) 46 + 47 + test('source src', () => { 48 + expect(rewrite('<video><source src="/clip.mp4"></video>')).toBe( 49 + '<video><source src="/did:plc:abc123/mysite/clip.mp4"></video>', 50 + ) 51 + }) 52 + }) 53 + 54 + describe('srcset', () => { 55 + test('single entry no descriptor', () => { 56 + expect(rewrite('<img srcset="/img.jpg">')).toBe('<img srcset="/did:plc:abc123/mysite/img.jpg">') 57 + }) 58 + 59 + test('single entry with pixel density descriptor', () => { 60 + expect(rewrite('<img srcset="/img.jpg 2x">')).toBe('<img srcset="/did:plc:abc123/mysite/img.jpg 2x">') 61 + }) 62 + 63 + test('multiple entries with pixel density descriptors', () => { 64 + expect(rewrite('<img srcset="/img.jpg 1x, /img@2x.jpg 2x">')).toBe( 65 + '<img srcset="/did:plc:abc123/mysite/img.jpg 1x, /did:plc:abc123/mysite/img@2x.jpg 2x">', 66 + ) 67 + }) 68 + 69 + test('multiple entries with width descriptors', () => { 70 + expect(rewrite('<img srcset="/small.jpg 320w, /large.jpg 1024w">')).toBe( 71 + '<img srcset="/did:plc:abc123/mysite/small.jpg 320w, /did:plc:abc123/mysite/large.jpg 1024w">', 72 + ) 73 + }) 74 + 75 + test('relative entries are left alone', () => { 76 + const html = '<img srcset="../img.jpg 1x, ./img@2x.jpg 2x">' 77 + expect(rewrite(html, NESTED_DOC)).toBe(html) 78 + }) 79 + 80 + test('mixed: absolute entries rewritten, relative left alone', () => { 81 + expect(rewrite('<img srcset="/abs.jpg 1x, ./rel.jpg 2x">')).toBe( 82 + '<img srcset="/did:plc:abc123/mysite/abs.jpg 1x, ./rel.jpg 2x">', 83 + ) 84 + }) 85 + }) 86 + 87 + describe('absolute (root-relative) paths', () => { 88 + test('root file', () => { 89 + expect(rewrite('<img src="/image.png">')).toBe('<img src="/did:plc:abc123/mysite/image.png">') 90 + }) 91 + 92 + test('nested file', () => { 93 + expect(rewrite('<img src="/assets/photo.jpg">')).toBe('<img src="/did:plc:abc123/mysite/assets/photo.jpg">') 94 + }) 95 + 96 + test('deeply nested file', () => { 97 + expect(rewrite('<link href="/a/b/c/style.css">')).toBe('<link href="/did:plc:abc123/mysite/a/b/c/style.css">') 98 + }) 99 + 100 + test('same result regardless of which document it appears in', () => { 101 + const html = '<img src="/image.png">' 102 + const expected = '<img src="/did:plc:abc123/mysite/image.png">' 103 + expect(rewrite(html, ROOT_DOC)).toBe(expected) 104 + expect(rewrite(html, NESTED_DOC)).toBe(expected) 105 + }) 106 + }) 107 + 108 + describe('relative paths are not rewritten', () => { 109 + test('./ prefix', () => { 110 + const html = '<img src="./image.png">' 111 + expect(rewrite(html)).toBe(html) 112 + }) 113 + 114 + test('bare filename', () => { 115 + const html = '<img src="image.png">' 116 + expect(rewrite(html)).toBe(html) 117 + }) 118 + 119 + test('../ up one level', () => { 120 + const html = '<img src="../image.png">' 121 + expect(rewrite(html, NESTED_DOC)).toBe(html) 122 + }) 123 + 124 + test('../../ up two levels', () => { 125 + const html = '<link href="../../style.css">' 126 + expect(rewrite(html, NESTED_DOC)).toBe(html) 127 + }) 128 + 129 + test('../sibling/path', () => { 130 + const html = '<script src="../assets/app.js"></script>' 131 + expect(rewrite(html, NESTED_DOC)).toBe(html) 132 + }) 133 + }) 134 + 135 + describe('not rewritten', () => { 136 + describe('external / protocol-relative', () => { 137 + test('https', () => { 138 + const html = '<img src="https://cdn.example.com/img.png">' 139 + expect(rewrite(html)).toBe(html) 140 + }) 141 + 142 + test('http', () => { 143 + const html = '<link href="http://cdn.example.com/style.css">' 144 + expect(rewrite(html)).toBe(html) 145 + }) 146 + 147 + test('protocol-relative //', () => { 148 + const html = '<script src="//cdn.example.com/lib.js"></script>' 149 + expect(rewrite(html)).toBe(html) 150 + }) 151 + }) 152 + 153 + describe('URI schemes', () => { 154 + test('data:', () => { 155 + const html = '<img src="data:image/png;base64,abc123">' 156 + expect(rewrite(html)).toBe(html) 157 + }) 158 + 159 + test('mailto:', () => { 160 + const html = '<a href="mailto:hi@example.com">Email</a>' 161 + expect(rewrite(html)).toBe(html) 162 + }) 163 + 164 + test('tel:', () => { 165 + const html = '<a href="tel:+1234567890">Call</a>' 166 + expect(rewrite(html)).toBe(html) 167 + }) 168 + 169 + test('javascript:', () => { 170 + const html = '<a href="javascript:void(0)">JS</a>' 171 + expect(rewrite(html)).toBe(html) 172 + }) 173 + 174 + test('blob:', () => { 175 + const html = '<a href="blob:https://example.com/abc">Blob</a>' 176 + expect(rewrite(html)).toBe(html) 177 + }) 178 + }) 179 + 180 + describe('fragment-only', () => { 181 + test('#anchor', () => { 182 + const html = '<a href="#section">Jump</a>' 183 + expect(rewrite(html)).toBe(html) 184 + }) 185 + }) 186 + 187 + describe('already prefixed (Vite base output)', () => { 188 + test('path already starting with basePath is not double-rewritten', () => { 189 + const html = '<script src="/did:plc:abc123/mysite/assets/app.js"></script>' 190 + expect(rewrite(html)).toBe(html) 191 + }) 192 + }) 193 + 194 + describe('inline script and style content', () => { 195 + test('paths inside <script> text are not rewritten', () => { 196 + const html = '<script>\nvar path = "/api/data"\nfetch("/api/endpoint")\n</script>' 197 + expect(rewrite(html)).toBe(html) 198 + }) 199 + 200 + test('url() inside <style> text is not rewritten', () => { 201 + const html = '<style>.hero { background: url(\'/images/hero.jpg\') }</style>' 202 + expect(rewrite(html)).toBe(html) 203 + }) 204 + }) 205 + }) 206 + 207 + describe('<base> tag', () => { 208 + test('root-relative base href is rewritten', () => { 209 + const result = rewrite('<head><base href="/"></head>') 210 + expect(result).toContain('href="/did:plc:abc123/mysite/"') 211 + }) 212 + 213 + test('subdirectory base href is rewritten', () => { 214 + const result = rewrite('<head><base href="/app/"></head>') 215 + expect(result).toContain('href="/did:plc:abc123/mysite/app/"') 216 + }) 217 + 218 + test('external base href is left untouched', () => { 219 + const html = '<head><base href="https://example.com/"></head>' 220 + expect(rewrite(html)).toBe(html) 221 + }) 222 + 223 + test('relative base href is left untouched', () => { 224 + const html = '<head><base href="./subdir/"></head>' 225 + expect(rewrite(html)).toBe(html) 226 + }) 227 + }) 228 + 229 + 230 + describe('URL features preserved', () => { 231 + test('query string', () => { 232 + expect(rewrite('<img src="/img.png?v=3">')).toBe('<img src="/did:plc:abc123/mysite/img.png?v=3">') 233 + }) 234 + 235 + test('hash fragment on a path URL', () => { 236 + expect(rewrite('<a href="/page#section">Link</a>')).toBe( 237 + '<a href="/did:plc:abc123/mysite/page#section">Link</a>', 238 + ) 239 + }) 240 + 241 + test('query string and hash fragment together', () => { 242 + expect(rewrite('<a href="/page?q=1#section">Link</a>')).toBe( 243 + '<a href="/did:plc:abc123/mysite/page?q=1#section">Link</a>', 244 + ) 245 + }) 246 + }) 247 + 248 + 249 + describe('basePath normalisation', () => { 250 + test('basePath without trailing slash is normalised', () => { 251 + const result = rewriteHtmlPaths('<img src="/img.png">', '/did:plc:abc123/mysite', ROOT_DOC) 252 + expect(result).toBe('<img src="/did:plc:abc123/mysite/img.png">') 253 + }) 254 + 255 + test('basePath with trailing slash is unchanged', () => { 256 + const result = rewriteHtmlPaths('<img src="/img.png">', '/did:plc:abc123/mysite/', ROOT_DOC) 257 + expect(result).toBe('<img src="/did:plc:abc123/mysite/img.png">') 258 + }) 259 + }) 260 + 261 + 262 + describe('real-world scenarios', () => { 263 + test('Vite SPA with already-prefixed paths not double-rewritten', () => { 264 + const html = [ 265 + '<link rel="stylesheet" href="/did:plc:abc123/mysite/assets/index.css">', 266 + '<script src="/did:plc:abc123/mysite/assets/index.js"></script>', 267 + ].join('\n') 268 + expect(rewrite(html)).toBe(html) 269 + }) 270 + 271 + test('static site: absolute paths rewritten, relative paths left alone', () => { 272 + const html = ` 273 + <link href="/css/style.css" rel="stylesheet"> 274 + <script src="/js/main.js"></script> 275 + <img src="/images/logo.png"> 276 + <img src="./post-image.jpg"> 277 + <a href="../index.html">Blog</a> 278 + <a href="/index.html">Home</a>`.trim() 279 + 280 + const result = rewrite(html, NESTED_DOC) 281 + expect(result).toContain('href="/did:plc:abc123/mysite/css/style.css"') 282 + expect(result).toContain('src="/did:plc:abc123/mysite/js/main.js"') 283 + expect(result).toContain('src="/did:plc:abc123/mysite/images/logo.png"') 284 + expect(result).toContain('src="./post-image.jpg"') 285 + expect(result).toContain('href="../index.html"') 286 + expect(result).toContain('href="/did:plc:abc123/mysite/index.html"') 287 + }) 288 + 289 + test('inline script alongside rewritable elements', () => { 290 + const html = ` 291 + <link href="/style.css" rel="stylesheet"> 292 + <script> 293 + var API = '/api/v1' 294 + fetch('/api/data').then(r => r.json()) 295 + </script> 296 + <img src="/hero.jpg">`.trim() 297 + 298 + const result = rewrite(html) 299 + expect(result).toContain('href="/did:plc:abc123/mysite/style.css"') 300 + expect(result).toContain('src="/did:plc:abc123/mysite/hero.jpg"') 301 + expect(result).toContain("var API = '/api/v1'") 302 + expect(result).toContain("fetch('/api/data')") 303 + }) 304 + }) 305 + 306 + describe('isHtmlFile', () => { 307 + test('.html returns true', () => expect(isHtmlFile('index.html')).toBe(true)) 308 + test('.htm returns true', () => expect(isHtmlFile('page.htm')).toBe(true)) 309 + test('uppercase .HTML returns true', () => expect(isHtmlFile('INDEX.HTML')).toBe(true)) 310 + test('nested path', () => expect(isHtmlFile('blog/posts/index.html')).toBe(true)) 311 + test('.js returns false', () => expect(isHtmlFile('app.js')).toBe(false)) 312 + test('no extension returns false', () => expect(isHtmlFile('README')).toBe(false)) 313 + })
+62 -113
apps/firehose-service/src/lib/html-rewriter.ts
··· 1 + import { parse } from 'node-html-parser' 2 + 1 3 /** 2 - * HTML path rewriting for firehose-service 3 - * Rewrites absolute/relative paths in HTML to be served from a base path 4 + * Attributes whose values are rewritten. 5 + * - `'url'` — a single URL string 6 + * - `'srcset'` — a comma-separated list of `<url> [descriptor]` entries 4 7 */ 5 - 6 - const REWRITABLE_ATTRIBUTES = ['src', 'href', 'action', 'data', 'poster', 'srcset'] as const 7 - 8 - function shouldRewritePath(path: string): boolean { 9 - if (!path) return false 10 - 11 - // Don't rewrite external URLs 12 - if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('//')) { 13 - return false 14 - } 15 - 16 - // Don't rewrite data URIs or other schemes 17 - if (path.includes(':') && !path.startsWith('./') && !path.startsWith('../')) { 18 - return false 19 - } 20 - 21 - return true 8 + const REWRITABLE_ATTRS: Record<string, 'url' | 'srcset'> = { 9 + src: 'url', 10 + href: 'url', 11 + action: 'url', 12 + data: 'url', 13 + poster: 'url', 14 + srcset: 'srcset', 22 15 } 23 16 24 - function normalizePath(path: string): string { 25 - const parts = path.split('/') 26 - const result: string[] = [] 27 - 28 - for (const part of parts) { 29 - if (part === '.' || part === '') { 30 - if (part === '' && result.length === 0) { 31 - result.push(part) 32 - } 33 - continue 34 - } 35 - if (part === '..') { 36 - if (result.length > 0 && result[result.length - 1] !== '..') { 37 - result.pop() 38 - } 39 - continue 40 - } 41 - result.push(part) 42 - } 43 - 44 - return result.join('/') 17 + /** Returns true if the URL is a root-relative path that needs prefixing (e.g. `/style.css`). */ 18 + function isRootRelative(url: string): boolean { 19 + if (!url || !url.startsWith('/')) return false 20 + // Protocol-relative (//cdn.example.com) — not a local path 21 + if (url.startsWith('//')) return false 22 + return true 45 23 } 46 24 47 - function getDirectory(filepath: string): string { 48 - const lastSlash = filepath.lastIndexOf('/') 49 - if (lastSlash === -1) { 50 - return '' 51 - } 52 - return filepath.substring(0, lastSlash + 1) 25 + /** 26 + * Prepend `basePath` to a root-relative URL, preserving query string and hash. 27 + */ 28 + function rewriteUrl(url: string, basePath: string): string { 29 + if (!isRootRelative(url)) return url 30 + if (url.startsWith(basePath)) return url 31 + const resolved = new URL(url, 'http://x') 32 + return basePath + resolved.pathname.slice(1) + resolved.search + resolved.hash 53 33 } 54 34 55 - function rewritePath(path: string, basePath: string, documentPath: string): string { 56 - if (!shouldRewritePath(path)) { 57 - return path 58 - } 59 - 60 - // Handle absolute paths: /file.js -> /base/file.js 61 - if (path.startsWith('/')) { 62 - return basePath + path.slice(1) 63 - } 64 - 65 - // Handle relative paths 66 - const documentDir = getDirectory(documentPath) 67 - let resolvedPath: string 68 - 69 - if (path.startsWith('./')) { 70 - resolvedPath = documentDir + path.slice(2) 71 - } else if (path.startsWith('../')) { 72 - resolvedPath = documentDir + path 73 - } else { 74 - resolvedPath = documentDir + path 75 - } 76 - 77 - resolvedPath = normalizePath(resolvedPath) 78 - return basePath + resolvedPath 79 - } 80 - 81 - function rewriteSrcset(srcset: string, basePath: string, documentPath: string): string { 35 + /** Rewrite each root-relative URL in a `srcset` value (comma-separated `<url> [descriptor]` list). */ 36 + function rewriteSrcset(srcset: string, basePath: string): string { 82 37 return srcset 83 38 .split(',') 84 - .map((part) => { 85 - const trimmed = part.trim() 86 - const spaceIndex = trimmed.indexOf(' ') 87 - 88 - if (spaceIndex === -1) { 89 - return rewritePath(trimmed, basePath, documentPath) 90 - } 91 - 92 - const url = trimmed.substring(0, spaceIndex) 93 - const descriptor = trimmed.substring(spaceIndex) 94 - return rewritePath(url, basePath, documentPath) + descriptor 39 + .map((entry) => { 40 + const trimmed = entry.trim() 41 + const spaceIdx = trimmed.search(/\s/) 42 + if (spaceIdx === -1) return rewriteUrl(trimmed, basePath) 43 + const url = trimmed.slice(0, spaceIdx) 44 + const descriptor = trimmed.slice(spaceIdx) // keeps leading whitespace + e.g. "2x" 45 + return rewriteUrl(url, basePath) + descriptor 95 46 }) 96 47 .join(', ') 97 48 } 98 49 99 50 /** 100 - * Rewrite paths in HTML content for serving from a base path 51 + * Rewrite root-relative paths in an HTML document so it serves correctly from `basePath`. 52 + * 53 + * @param html Raw HTML string. 54 + * @param basePath Wisp serving prefix, e.g. `/did/rkey/`. 55 + * @param documentPath Storage path of this file — unused for path resolution but 56 + * kept in the signature for potential future use (e.g. logging). 101 57 */ 102 - export function rewriteHtmlPaths(html: string, basePath: string, documentPath: string): string { 58 + export function rewriteHtmlPaths(html: string, basePath: string, _documentPath: string): string { 103 59 const normalizedBase = basePath.endsWith('/') ? basePath : `${basePath}/` 104 60 105 - let rewritten = html 61 + const root = parse(html, { 62 + comment: true, 63 + blockTextElements: { script: true, style: true, pre: true, code: true }, 64 + }) 106 65 107 - for (const attr of REWRITABLE_ATTRIBUTES) { 108 - if (attr === 'srcset') { 109 - const srcsetRegex = new RegExp(`\\b${attr}[ \\t]{0,5}=[ \\t]{0,5}"([^"]*)"`, 'gi') 110 - rewritten = rewritten.replace(srcsetRegex, (_match, value) => { 111 - const rewrittenValue = rewriteSrcset(value, normalizedBase, documentPath) 112 - return `${attr}="${rewrittenValue}"` 113 - }) 114 - } else { 115 - const doubleQuoteRegex = new RegExp(`\\b${attr}[ \\t]{0,5}=[ \\t]{0,5}"([^"]*)"`, 'gi') 116 - const singleQuoteRegex = new RegExp(`\\b${attr}[ \\t]{0,5}=[ \\t]{0,5}'([^']*)'`, 'gi') 117 - const unquotedRegex = new RegExp(`\\b${attr}[ \\t]{0,5}=[ \\t]{0,5}(?!["'])([^\\s>]+)`, 'gi') 66 + // Rewrite <base href> so the browser uses the correct base at runtime for 67 + // JS fetch, form submits, dynamic navigation, etc. 68 + const baseEl = root.querySelector('base') 69 + if (baseEl) { 70 + const baseHref = baseEl.getAttribute('href') 71 + if (baseHref) { 72 + baseEl.setAttribute('href', rewriteUrl(baseHref, normalizedBase)) 73 + } 74 + } 118 75 119 - rewritten = rewritten.replace(doubleQuoteRegex, (_match, value) => { 120 - const rewrittenValue = rewritePath(value, normalizedBase, documentPath) 121 - return `${attr}="${rewrittenValue}"` 122 - }) 123 - 124 - rewritten = rewritten.replace(singleQuoteRegex, (_match, value) => { 125 - const rewrittenValue = rewritePath(value, normalizedBase, documentPath) 126 - return `${attr}='${rewrittenValue}'` 127 - }) 128 - 129 - rewritten = rewritten.replace(unquotedRegex, (_match, value) => { 130 - const rewrittenValue = rewritePath(value, normalizedBase, documentPath) 131 - return `${attr}=${rewrittenValue}` 132 - }) 76 + for (const el of root.querySelectorAll('*')) { 77 + for (const [attr, type] of Object.entries(REWRITABLE_ATTRS)) { 78 + const value = el.getAttribute(attr) 79 + if (value == null) continue 80 + el.setAttribute(attr, type === 'srcset' ? rewriteSrcset(value, normalizedBase) : rewriteUrl(value, normalizedBase)) 133 81 } 134 82 } 135 83 136 - return rewritten 84 + return root.toString() 137 85 } 138 86 87 + /** Returns true for `.html` and `.htm` files. */ 139 88 export function isHtmlFile(filepath: string): boolean { 140 89 const ext = filepath.toLowerCase().split('.').pop() 141 90 return ext === 'html' || ext === 'htm'
+30 -2
bun.lock
··· 7 7 "dependencies": { 8 8 "@tailwindcss/cli": "^4.1.17", 9 9 "bun-plugin-tailwind": "^0.1.2", 10 + "node-html-parser": "^7.1.0", 10 11 "tailwindcss": "^4.1.17", 11 12 }, 12 13 "devDependencies": { ··· 35 36 "hono": "^4.10.4", 36 37 "ioredis": "^5.9.2", 37 38 "multiformats": "^13.4.1", 39 + "node-html-parser": "^7.1.0", 38 40 "postgres": "^3.4.5", 39 41 }, 40 42 "devDependencies": { ··· 1209 1211 1210 1212 "body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="], 1211 1213 1214 + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], 1215 + 1212 1216 "bowser": ["bowser@2.13.1", "", {}, "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw=="], 1213 1217 1214 1218 "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], ··· 1273 1277 1274 1278 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 1275 1279 1280 + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], 1281 + 1282 + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], 1283 + 1276 1284 "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], 1277 1285 1278 1286 "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], ··· 1297 1305 1298 1306 "dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="], 1299 1307 1308 + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], 1309 + 1310 + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], 1311 + 1312 + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], 1313 + 1314 + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], 1315 + 1300 1316 "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], 1301 1317 1302 1318 "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], ··· 1309 1325 1310 1326 "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], 1311 1327 1328 + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], 1329 + 1312 1330 "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], 1313 1331 1314 1332 "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], ··· 1424 1442 "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], 1425 1443 1426 1444 "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 1445 + 1446 + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], 1427 1447 1428 1448 "hono": ["hono@4.11.6", "", {}, "sha512-ofIiiHyl34SV6AuhE3YT2mhO5HRWokce+eUYE82TsP6z0/H3JeJcjVWEMSIAiw2QkjDOEpES/lYsg8eEbsLtdw=="], 1429 1449 ··· 1564 1584 "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], 1565 1585 1566 1586 "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.1.1", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-test": "build-test.js", "node-gyp-build-optional-packages-optional": "optional.js" } }, "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw=="], 1587 + 1588 + "node-html-parser": ["node-html-parser@7.1.0", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ=="], 1589 + 1590 + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], 1567 1591 1568 1592 "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], 1569 1593 ··· 2089 2113 2090 2114 "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], 2091 2115 2092 - "@wispplace/bun-firehose/@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], 2116 + "@wispplace/bun-firehose/@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], 2093 2117 2094 - "@wispplace/tiered-storage/@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], 2118 + "@wispplace/tiered-storage/@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], 2095 2119 2096 2120 "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 2097 2121 ··· 2278 2302 "@types/bun/bun-types/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], 2279 2303 2280 2304 "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 2305 + 2306 + "@wispplace/bun-firehose/@types/bun/bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], 2307 + 2308 + "@wispplace/tiered-storage/@types/bun/bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], 2281 2309 2282 2310 "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 2283 2311
+1
package.json
··· 13 13 "dependencies": { 14 14 "@tailwindcss/cli": "^4.1.17", 15 15 "bun-plugin-tailwind": "^0.1.2", 16 + "node-html-parser": "^7.1.0", 16 17 "tailwindcss": "^4.1.17" 17 18 }, 18 19 "scripts": {