Skip to content
This repository was archived by the owner on May 20, 2026. It is now read-only.

Commit be4f56b

Browse files
authored
refactor: code consolidation and performance optimizations (#15)
* feat: add Tumblr playlist support via Google Docs Load a Google Doc containing Tumblr URLs as a navigable playlist with ToC support. URLs are extracted from HTML export to preserve hyperlinks, with automatic unwrapping of Google redirect URLs. - Add tumblr-playlist attribute for loading playlists - Create TumblrPlaylistReader with index-based navigation - Generate ToC from playlist entries with labels from URL slugs - Support parallel EPUB export with blog name as default filename - Add prefetch all button and skeleton loading states - Update demo page with playlist button and auto-detect URL input * refactor: extract shared utilities and optimize caching - Extract escapeHtml/sanitizeHtml to src/core/utils/html-utils.ts - Extract renderBlock/renderReblogEntry to src/core/tumblr/tumblr-render.ts - Remove static reader exports from index.ts for better code splitting - Cache parsed localStorage in TumblrCache to avoid re-parsing on every read - Add rollup-plugin-visualizer to vite config for bundle analysis - Update ROADMAP.md with completed accessibility items - Add missing error types to API reference - Consolidate duplicate content in getting-started.md * perf: optimize readers and extract UI utilities - Extract loading/error/skeleton HTML to src/core/utils/ui-utils.ts - Increase CBZ LRU cache from 5 to 10 for fewer re-extractions - Parallelize PDF 2-page rendering with Promise.all for faster loads - Refactor Tumblr readers to use shared UI utilities * refactor: extract keyboard and zoom utilities - Add keyboard-utils.ts with setupKeyboardNavigation helper - Add zoom-utils.ts with clampZoom, zoomIn, zoomOut helpers - Refactor all readers to use shared keyboard navigation - Refactor all readers to use clampZoom for consistent bounds - Reduces duplication and ensures consistent behavior * perf: add adaptive preloading and AbortController for prefetch - CBZ reader: adaptive preload based on navigation direction (forward/back) - CBZ reader: preload 3 pages in travel direction, 1 in opposite - Tumblr fetcher: add AbortSignal support to FetchOptions - Tumblr playlist reader: cancel stale prefetch requests on navigation - Uses manual AbortController combination for jsdom compatibility * docs: update getting-started guide - Add Tumblr posts section with usage examples - Reference API reference for error types (removes partial duplicate) * perf: add HiDPI support for PDF rendering - Render PDFs at device pixel ratio for sharp display on Retina screens - Set canvas backing store to actual pixels, CSS size to logical pixels - Improves clarity on high-density displays without layout changes
1 parent 630bbb8 commit be4f56b

23 files changed

Lines changed: 1921 additions & 348 deletions

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
### 2.2 Accessibility
4040
- [x] Full keyboard navigation (focus management)
4141
- [x] ARIA labels and roles
42-
- [ ] Screen reader testing
42+
- [x] Screen reader testing
4343
- [x] High contrast mode support
4444
- [x] Reduced motion support
4545

demo/index.html

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@
235235
<body>
236236
<div class="container">
237237
<h1>Speed-Read</h1>
238-
<p class="tagline">Lightweight, embeddable document reader for EPUB, PDF, CBZ, and Tumblr posts</p>
238+
<p class="tagline">Lightweight, embeddable document reader for EPUB, PDF, CBZ, Tumblr posts, and Tumblr playlists</p>
239239

240240
<div class="demo-section">
241241
<h2>Try it out</h2>
@@ -245,6 +245,7 @@ <h2>Try it out</h2>
245245
<button onclick="loadSample('pdf')" id="btn-pdf">Load Sample PDF</button>
246246
<button onclick="loadSample('cbz')" id="btn-cbz">Load Sample CBZ</button>
247247
<button onclick="loadTumblr()" id="btn-tumblr" style="background: #35465c; border-color: #35465c;">Load Tumblr Post</button>
248+
<button onclick="loadTumblrPlaylist()" id="btn-playlist" style="background: #35465c; border-color: #35465c;">Load Tumblr Playlist</button>
248249
</div>
249250

250251
<div class="divider"><span>or</span></div>
@@ -255,11 +256,11 @@ <h2>Try it out</h2>
255256
<p class="formats">Supports: EPUB, PDF, CBZ</p>
256257
</div>
257258

258-
<div class="divider"><span>or enter a Tumblr URL</span></div>
259+
<div class="divider"><span>or enter a Tumblr URL or Google Docs playlist</span></div>
259260

260261
<div class="url-input-area">
261-
<input type="url" id="tumblrInput" placeholder="https://www.tumblr.com/username/post-id/slug">
262-
<button onclick="loadTumblrUrl()" id="btn-load-url">Load</button>
262+
<input type="url" id="urlInput" placeholder="Tumblr post URL or Google Docs playlist URL">
263+
<button onclick="loadUrl()" id="btn-load-url">Load</button>
263264
</div>
264265

265266
<div class="reader-container" id="readerContainer" style="display: none;">
@@ -281,7 +282,7 @@ <h2>Quick Start</h2>
281282
<div class="features">
282283
<div class="feature">
283284
<h3>Multi-Format</h3>
284-
<p>EPUB, PDF, CBZ, and Tumblr post support</p>
285+
<p>EPUB, PDF, CBZ, Tumblr posts, and Tumblr playlists</p>
285286
</div>
286287
<div class="feature">
287288
<h3>Lightweight</h3>
@@ -335,41 +336,69 @@ <h2>Documentation</h2>
335336
// Tumblr test URL
336337
const tumblrUrl = 'https://www.tumblr.com/revelboo/762688045814317056/everything-is-alright-pt1';
337338

339+
// Tumblr playlist test URL (Google Doc)
340+
const playlistUrl = 'https://docs.google.com/document/d/15FrbEgGaTBRPtvJtNNeyRKXW-Zcs-TVvtTIQ7koznZg/edit?usp=sharing';
341+
338342
// Make loadTumblr available globally
339343
window.loadTumblr = async function() {
340344
status.textContent = `Loading Tumblr post...`;
341345
status.style.background = '#f0f0f0';
342346
status.style.color = '#666';
343347

344-
// Set the tumblr attribute on the reader
348+
// Clear any previous attributes and set the tumblr attribute
349+
reader.removeAttribute('tumblr-playlist');
345350
reader.setAttribute('tumblr', tumblrUrl);
346351
readerContainer.style.display = 'block';
347352
};
348353

349-
// Load Tumblr URL from input field
350-
window.loadTumblrUrl = async function() {
351-
const input = document.getElementById('tumblrInput');
354+
// Load Tumblr playlist from Google Docs
355+
window.loadTumblrPlaylist = async function() {
356+
status.textContent = `Loading Tumblr playlist...`;
357+
status.style.background = '#f0f0f0';
358+
status.style.color = '#666';
359+
360+
// Clear any previous attributes and set the tumblr-playlist attribute
361+
reader.removeAttribute('tumblr');
362+
reader.setAttribute('tumblr-playlist', playlistUrl);
363+
readerContainer.style.display = 'block';
364+
};
365+
366+
// Load URL from input field - auto-detects Tumblr vs Google Docs
367+
window.loadUrl = async function() {
368+
const input = document.getElementById('urlInput');
352369
const url = input.value.trim();
353370

354371
if (!url) {
355-
status.textContent = 'Please enter a Tumblr URL';
372+
status.textContent = 'Please enter a URL';
356373
status.style.background = '#fff0f0';
357374
status.style.color = '#cc0000';
358375
return;
359376
}
360377

361-
if (!url.includes('tumblr.com')) {
362-
status.textContent = 'Please enter a valid Tumblr URL';
378+
// Detect URL type
379+
const isGoogleDocs = url.includes('docs.google.com/document');
380+
const isTumblr = url.includes('tumblr.com');
381+
382+
if (!isGoogleDocs && !isTumblr) {
383+
status.textContent = 'Please enter a valid Tumblr URL or Google Docs URL';
363384
status.style.background = '#fff0f0';
364385
status.style.color = '#cc0000';
365386
return;
366387
}
367388

368-
status.textContent = `Loading Tumblr post...`;
369389
status.style.background = '#f0f0f0';
370390
status.style.color = '#666';
371391

372-
reader.setAttribute('tumblr', url);
392+
if (isGoogleDocs) {
393+
status.textContent = `Loading Tumblr playlist from Google Docs...`;
394+
reader.removeAttribute('tumblr');
395+
reader.setAttribute('tumblr-playlist', url);
396+
} else {
397+
status.textContent = `Loading Tumblr post...`;
398+
reader.removeAttribute('tumblr-playlist');
399+
reader.setAttribute('tumblr', url);
400+
}
401+
373402
readerContainer.style.display = 'block';
374403
};
375404

@@ -401,9 +430,9 @@ <h2>Documentation</h2>
401430
};
402431

403432
// Enter key on URL input
404-
document.getElementById('tumblrInput').addEventListener('keypress', (e) => {
433+
document.getElementById('urlInput').addEventListener('keypress', (e) => {
405434
if (e.key === 'Enter') {
406-
window.loadTumblrUrl();
435+
window.loadUrl();
407436
}
408437
});
409438

docs/api-reference.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ type ReaderErrorType =
150150
| 'DRM_PROTECTED'
151151
| 'LOAD_FAILED'
152152
| 'CORS_ERROR'
153+
| 'NETWORK_ERROR'
154+
| 'TIMEOUT'
155+
| 'MALFORMED_FILE'
156+
| 'RENDER_ERROR'
153157
| 'UNKNOWN';
154158

155159
type DocumentFormat = 'epub' | 'pdf' | 'cbz';

docs/getting-started.md

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,27 @@ export default App;
122122
</script>
123123
```
124124

125+
## Tumblr Posts
126+
127+
Read Tumblr post series with automatic navigation between linked posts:
128+
129+
```html
130+
<speed-reader tumblr="https://www.tumblr.com/username/post-id/slug"></speed-reader>
131+
```
132+
133+
Posts are fetched via CORS proxy and cached locally. The reader automatically detects and follows "next" and "previous" post links for series navigation.
134+
135+
### Custom CORS Proxy
136+
137+
If the default proxies don't work, specify your own:
138+
139+
```html
140+
<speed-reader
141+
tumblr="https://www.tumblr.com/username/post-id/slug"
142+
tumblr-proxy="https://my-cors-proxy.com/?url=">
143+
</speed-reader>
144+
```
145+
125146
## Episodic Content
126147

127148
For serial content like web novels or webcomics, use a manifest:
@@ -153,36 +174,7 @@ The reader will:
153174

154175
## Theming
155176

156-
Customize the appearance with CSS custom properties:
157-
158-
```css
159-
speed-reader {
160-
/* Background and text colors */
161-
--speed-reader-bg: #ffffff;
162-
--speed-reader-text: #000000;
163-
164-
/* Accent color for buttons */
165-
--speed-reader-accent: #0066cc;
166-
167-
/* Error state colors */
168-
--speed-reader-error-bg: #fff0f0;
169-
--speed-reader-error-text: #cc0000;
170-
}
171-
```
172-
173-
### Dark Mode
174-
175-
```css
176-
@media (prefers-color-scheme: dark) {
177-
speed-reader {
178-
--speed-reader-bg: #1a1a1a;
179-
--speed-reader-text: #e0e0e0;
180-
--speed-reader-accent: #66b3ff;
181-
--speed-reader-error-bg: #2d1a1a;
182-
--speed-reader-error-text: #ff6666;
183-
}
184-
}
185-
```
177+
Speed-Read supports theming via CSS custom properties. See the [README](../README.md#theming) for available properties and dark mode examples.
186178

187179
## Handling Events
188180

@@ -222,15 +214,7 @@ reader.addEventListener('error', (e) => {
222214

223215
## Error Handling
224216

225-
The reader detects and reports various error conditions:
226-
227-
| Error Type | Description |
228-
|------------|-------------|
229-
| `FILE_TOO_LARGE` | File exceeds size limit |
230-
| `INVALID_FORMAT` | Unrecognized file format |
231-
| `DRM_PROTECTED` | File has DRM protection |
232-
| `LOAD_FAILED` | Network or parsing error |
233-
| `CORS_ERROR` | Cross-origin request blocked |
217+
The reader detects and reports various error conditions. See the [API Reference](./api-reference.md#readererror) for the complete list of error types.
234218

235219
### Example error handler
236220

0 commit comments

Comments
 (0)