diff --git a/READER_IMPLEMENTATION_PLAN.md b/READER_IMPLEMENTATION_PLAN.md index ec54eed..f22ab00 100644 --- a/READER_IMPLEMENTATION_PLAN.md +++ b/READER_IMPLEMENTATION_PLAN.md @@ -4606,95 +4606,6 @@ function getElementChildren(element: Element): Element[] { --- -### 5.8 Typography Engine (Procedural) - -**File:** `web/src/reader/ebook/typography-engine.ts` - -```typescript -// Typography engine for ebook rendering -// Procedural style: Functions, not classes - -interface TypographyConfig { - fontSize: number; - lineHeight: number; - textAlign: 'left' | 'justify'; - hyphenate: boolean; - ligatures: boolean; - fontSmoothing: 'auto' | 'grayscale'; -} - -export function applyTypographyConfig( - element: HTMLElement, - config: TypographyConfig -): void { - // Enable/disable ligatures - setLigatures(element, config.ligatures); - - // Enable/disable hyphenation - if (config.hyphenate) { - enableHyphenation(element); - } - - // Apply justification settings - if (config.textAlign === 'justify') { - enableJustification(element); - } - - // Apply font smoothing - element.style.fontSmooth = config.fontSmoothing; -} - -function setLigatures(element: HTMLElement, enabled: boolean): void { - if (enabled) { - element.style.fontVariantLigatures = 'common-ligatures'; - element.style.fontFeatureSettings = '"liga", "dlig"'; - } else { - element.style.fontVariantLigatures = 'no-common-ligatures'; - element.style.fontFeatureSettings = 'normal'; - } -} - -function enableHyphenation(element: HTMLElement): void { - element.style.hyphens = 'auto'; - element.style.hyphenateLimitChars = '6 3 3'; - - // Add language attribute from EPUB metadata - const lang = element.closest('[data-language]')?.getAttribute('data-language') || 'en'; - element.setAttribute('lang', lang); -} - -function enableJustification(element: HTMLElement): void { - element.style.wordBreak = 'normal'; - element.style.overflowWrap = 'break-word'; - element.style.wordWrap = 'break-word'; - element.style.letterSpacing = '0.01em'; -} - -export function measureReadingTime( - container: HTMLElement, - wordsPerMinute: number = 250 -): number { - const content = container.querySelector('.ebook-content'); - if (!content) return 0; - - const text = content.textContent || ''; - const words = text.split(/\s+/).length; - const minutes = words / wordsPerMinute; - - return Math.ceil(minutes); -} - -export function getWordCount(container: HTMLElement): number { - const content = container.querySelector('.ebook-content'); - if (!content) return 0; - - const text = content.textContent || ''; - return text.split(/\s+/).length; -} -``` - ---- - ### 5.9 Ebook Search (Procedural) **File:** `web/src/reader/ebook/search.ts` @@ -5103,194 +5014,7 @@ All fonts use SIL Open Font License 1.1 WOFF2 format, ~1.2MB total" ``` -#### 5.10.4 Font Loading in Templates - -**File:** `templates/reader.templ` (updated) - -Add to `` section: - -```go -templ Reader(user User, metadata ReaderMetadata) { - - - - - - { metadata.title } - Bookhoard Reader - - - - - - - -
- -
-
- -

- { metadata.title } -

-
- -
- -
- -- / -- -
- - - -
-
- - - -
- - -
- -
- - - - - - - - - - - -} -``` - -#### 5.10.5 Alternative: Use Google Fonts CDN (Not Recommended) +#### 5.10.4 Alternative: Use Google Fonts CDN (Not Recommended) If you don't want to bundle fonts (slower initial load, privacy concerns): @@ -5820,166 +5544,6 @@ function getWordCount(container: HTMLElement): number { } ``` -### 5.12 Search Within Ebook - -**File:** `web/src/reader/ebook/search.ts` - -```typescript -// Search within ebook content - -interface SearchResult { - cfi: string; - snippet: string; - chapterTitle: string; -} - -// Search within ebook content -// Procedural implementation (no OOP) - -interface SearchResult { - cfi: string; - snippet: string; - chapterTitle: string; -} - -async function searchEbook( - epubPackage: EPUBPackage, - query: string -): Promise { - const results: SearchResult[] = []; - const lowerQuery = query.toLowerCase(); - - for (const [index, spineItem] of epubPackage.spine.entries()) { - const doc = await getSpineItemDocument(epubPackage, spineItem); - - if (!doc) continue; - - const chapterTitle = getChapterTitle(epubPackage, spineItem); - const textNodes = findTextNodes(doc.body); - - for (const node of textNodes) { - const text = node.textContent || ''; - const lowerText = text.toLowerCase(); - - let foundAt = 0; - while ((foundAt = lowerText.indexOf(lowerQuery, foundAt)) !== -1) { - const cfi = generateSearchCFI(node, foundAt); - const snippet = extractSearchSnippet(text, foundAt, query.length); - - results.push({ - cfi, - snippet, - chapterTitle - }); - - foundAt += lowerQuery.length; - } - } - } - - return results; -} - -async function getSpineItemDocument( - epubPackage: EPUBPackage, - spineItem: EPUBSpineItem -): Promise { - try { - const content = await epubPackage.resources.get(spineItem.href)?.text(); - if (!content) return null; - - const parser = new DOMParser(); - return parser.parseFromString(content, 'text/html'); - } catch (error) { - console.error('Failed to load spine item:', spineItem.href, error); - return null; - } -} - -function findTextNodes(root: Node): Text[] { - const textNodes: Text[] = []; - - const walker = document.createTreeWalker( - root, - NodeFilter.SHOW_TEXT, - { - acceptNode: (node) => { - const parent = node.parentElement; - if (parent && ['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(parent.tagName)) { - return NodeFilter.FILTER_REJECT; - } - - if (!node.textContent?.trim()) { - return NodeFilter.FILTER_REJECT; - } - - return NodeFilter.FILTER_ACCEPT; - } - } - ); - - let node: Node | null; - while ((node = walker.nextNode())) { - textNodes.push(node as Text); - } - - return textNodes; -} - -function generateSearchCFI(node: Text, offset: number): string { - const path: number[] = []; - let current: Node | null = node; - - while (current && current.parentNode) { - const parent = current.parentNode; - const siblings = Array.from(parent.childNodes) - .filter(n => n.nodeType === Node.ELEMENT_NODE); - const index = siblings.indexOf(current as Node); - - path.unshift(index); - current = parent; - } - - const spineIndex = 0; - - return generateCFI(spineIndex, path, offset); -} - -function extractSearchSnippet(text: string, offset: number, length: number): string { - const contextBefore = 30; - const contextAfter = 50; - - const start = Math.max(0, offset - contextBefore); - const end = Math.min(text.length, offset + length + contextAfter); - - let snippet = text.substring(start, end); - - if (start > 0) snippet = '...' + snippet; - if (end < text.length) snippet = snippet + '...'; - - return snippet; -} - -function getChapterTitle( - epubPackage: EPUBPackage, - spineItem: EPUBSpineItem -): string { - for (const toc of epubPackage.toc) { - if (toc.href === spineItem.href) { - return toc.label; - } - - for (const child of toc.children) { - if (child.href === spineItem.href) { - return child.label; - } - } - } - - return 'Chapter ' + (epubPackage.spine.indexOf(spineItem) + 1); -} -``` - ### 5.13 Copy Text Handler **File:** `web/src/reader/ebook/copy-handler.ts`