// 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];
}