// HTML rendering with theme support, font loading, and image handling // Procedural style: Functions, not classes interface RendererConfig { readingTheme: "light" | "sepia" | "dark" | "night" | "high-contrast"; readingFont: | "literata" | "crimson" | "source-serif" | "eb-garamond" | "libertinus" | "noto-serif" | "charis-sil" | "ibm-plex"; fontSize: number; lineHeight: number; marginWidth: number; textAlign: "left" | "justify"; columnCount: 1 | 2; } // ============================================================ // Main Render Function // ============================================================ export async function renderHTMLDocument( doc: HTMLDocument, container: HTMLElement, config: RendererConfig, ): Promise { // Apply theme applyHTMLTheme(container, config.readingTheme); // Apply typography settings applyHTMLTypography(container, config); // Inject custom styles for reader injectHTMLReaderStyles(container); // Handle embedded fonts await loadEmbeddedHTMLFonts(doc, container); // Handle images processHTMLImages(doc, container); // Clear container and append content container.innerHTML = ""; container.appendChild(doc.body); // Apply column layout applyHTMLColumnLayout(container, config.columnCount); } // ============================================================ // Theme Application // ============================================================ function applyHTMLTheme(container: HTMLElement, theme: string): void { const readingThemes: Record> = { light: { "--bg-primary": "#ffffff", "--text-primary": "#1a1a1a", "--text-secondary": "#666666", "--accent": "#0066cc", }, sepia: { "--bg-primary": "#f4ecd8", "--text-primary": "#5f4b32", "--text-secondary": "#8b7355", "--accent": "#8b4513", }, dark: { "--bg-primary": "#1a1b26", "--text-primary": "#c0caf5", "--text-secondary": "#565f89", "--accent": "#7aa2f7", }, night: { "--bg-primary": "#0d1117", "--text-primary": "#c9d1d9", "--text-secondary": "#8b949e", "--accent": "#58a6ff", }, "high-contrast": { "--bg-primary": "#000000", "--text-primary": "#ffffff", "--text-secondary": "#cccccc", "--accent": "#ffff00", }, }; const themeConfig = readingThemes[theme] || readingThemes["dark"]; for (const [key, value] of Object.entries(themeConfig)) { container.style.setProperty(key, value); } } function applyHTMLTypography( container: HTMLElement, config: RendererConfig, ): void { const style = document.createElement("style"); const fontStack = getFontStack(config.readingFont); style.textContent = ` .ebook-content { font-family: ${fontStack}; font-size: ${config.fontSize}px; line-height: ${config.lineHeight}; text-align: ${config.textAlign}; padding: 0 ${config.marginWidth}px; max-width: 100%; overflow-wrap: break-word; } .ebook-content p { margin-bottom: 1em; text-indent: ${config.textAlign === "justify" ? "1.5em" : "0"}; } .ebook-content img { max-width: 100%; height: auto; display: block; margin: 1em auto; } .ebook-content a { color: var(--accent); text-decoration: underline; } .ebook-content a:active { color: var(--text-secondary); } `; container.appendChild(style); } function injectHTMLReaderStyles(container: HTMLElement): void { container.setAttribute("role", "main"); container.setAttribute("aria-label", "Book content"); } async function loadEmbeddedHTMLFonts( doc: HTMLDocument, container: HTMLElement, ): Promise { const styleSheets = doc.querySelectorAll("style"); for (const sheet of styleSheets) { const fontFaceRegex = /@font-face\s*{([^}]+)}/g; const matches = sheet.textContent?.matchAll(fontFaceRegex) || []; for (const match of matches) { const fontFace = match[1]; const urlMatch = /url\(['"]?([^'")]+)['"]?\)/.exec(fontFace); if (urlMatch) { const fontUrl = urlMatch[1]; await loadHTMLFont(fontUrl, container); } } } } async function loadHTMLFont( fontUrl: string, container: HTMLElement, ): Promise { const loadedFonts = container.dataset.loadedFonts ? JSON.parse(container.dataset.loadedFonts) : []; if (loadedFonts.includes(fontUrl)) return; try { const fontFace = new FontFace("custom-font", `url(${fontUrl})`); await fontFace.load(); document.fonts.add(fontFace); loadedFonts.push(fontUrl); container.dataset.loadedFonts = JSON.stringify(loadedFonts); } catch (error) { console.error("Failed to load font:", fontUrl, error); } } function processHTMLImages(doc: HTMLDocument): void { const images = doc.querySelectorAll("img"); images.forEach((img) => { img.setAttribute("loading", "lazy"); if (!img.alt) { img.alt = "Image from book"; } img.style.cursor = "pointer"; img.addEventListener("click", () => { showImageFullscreen(img.src); }); }); } function showImageFullscreen(src: string): void { const modal = document.createElement("div"); modal.className = "fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50"; modal.onclick = () => modal.remove(); const img = document.createElement("img"); img.src = src; img.className = "max-w-full max-h-full object-contain"; modal.appendChild(img); document.body.appendChild(modal); } function applyHTMLColumnLayout( container: HTMLElement, columnCount: number, ): void { if (columnCount === 2) { container.style.columnCount = "2"; container.style.columnGap = "20px"; container.style.columnRule = "1px solid var(--text-secondary)"; } else { container.style.columnCount = "auto"; } } function getFontStack(font: string): string { const stacks: Record = { literata: '"Literata", serif', crimson: '"Crimson Text", serif', "source-serif": '"Source Serif 4", serif', "eb-garamond": '"EB Garamond", serif', libertinus: '"Libertinus Serif", serif', "noto-serif": '"Noto Serif", serif', "charis-sil": '"Charis SIL", serif', "ibm-plex": '"IBM Plex Serif", serif', }; return stacks[font] || stacks["literata"]; }