// Ported from FilesPage.html lines 3478 (formatBytes), 3811-3857, 3979. /** Escape a string for safe insertion into XML attribute values / text content. (3811) */ export function xmlEscape(str) { return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } /** (3815) */ export function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Decode a URI-encoded href (e.g., "my%20image.jpg" -> "my image.jpg"). * Handles double-encoding gracefully. (3821) */ export function decodeHref(href) { try { return decodeURIComponent(href); } catch (e) { return href; } } /** * Safely read a text file from the zip, handling BOM and encoding. * Strips UTF-8 BOM. Detects encoding from XML declaration or meta tag. (3830) * `fileObj` is a JSZip file object. */ export async function safeReadText(fileObj) { const raw = await fileObj.async('uint8array'); // Detect and strip UTF-8 BOM (EF BB BF) let offset = 0; if (raw.length >= 3 && raw[0] === 0xEF && raw[1] === 0xBB && raw[2] === 0xBF) { offset = 3; } // Try UTF-8 first (vast majority of EPUBs) const utf8 = new TextDecoder('utf-8', { fatal: true }); try { return utf8.decode(raw.subarray(offset)); } catch (e) { /* not valid UTF-8 */ } // Peek at XML declaration or meta charset for encoding hint const ascii = new TextDecoder('ascii', { fatal: false }).decode(raw.subarray(offset, offset + 512)); const encodingMatch = ascii.match(/encoding=["']([^"']+)["']/i) || ascii.match(/charset=["']?([^"'\s;]+)/i); const encoding = encodingMatch ? encodingMatch[1].toLowerCase() : 'windows-1252'; try { return new TextDecoder(encoding, { fatal: false }).decode(raw.subarray(offset)); } catch (e) { // Last resort: lossy latin1 return new TextDecoder('iso-8859-1', { fatal: false }).decode(raw.subarray(offset)); } } /** * Resolve a relative href against a base file path. * Handles multiple ../, ./, absolute /, and bare relative paths. (3979) */ export function resolvePath(basePath, href) { if (href.startsWith('/')) return href.substring(1); href = href.replace(/^\.\//, ''); const baseDir = basePath.includes('/') ? basePath.substring(0, basePath.lastIndexOf('/')) : ''; const baseParts = baseDir ? baseDir.split('/') : []; const hrefParts = href.split('/'); while (hrefParts.length > 0 && hrefParts[0] === '..') { hrefParts.shift(); if (baseParts.length > 0) baseParts.pop(); } const resolved = [...baseParts, ...hrefParts].join('/'); return resolved.replace(/\/+/g, '/'); } /** (3478) */ export function formatBytes(b) { if (!b) return '0 B'; const k = 1024; const s = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(b) / Math.log(k)); return (b / Math.pow(k, i)).toFixed(1) + ' ' + s[i]; }