vomebook commited on
Commit
098dd06
·
verified ·
1 Parent(s): 7a96915

Speed up Reader startup and stream encoded TXT

Browse files
Files changed (3) hide show
  1. static/app.js +8 -4
  2. static/reader.js +60 -5
  3. static/sw.js +12 -0
static/app.js CHANGED
@@ -748,13 +748,17 @@ const warmedReaderAssets = new Set();
748
  function warmReaderIntent(rawUrl) {
749
  if (!rawUrl) return;
750
  let extension = "";
751
- try { extension = (new URL(rawUrl, location.origin).searchParams.get("ext") || "").toLowerCase(); } catch (_) { return; }
752
- const assets = extension === "pdf"
 
 
 
 
753
  ? ["/static/vendor/pdf.min.e0be3863c23c.mjs", "/static/pdf-worker-wrapper.mjs", "/static/vendor/pdf.worker.min.0613f41490dd.mjs"]
754
- : extension === "epub" ? ["/static/vendor/epub.min.06eae1574510.js"]
755
  : extension === "docx" ? ["/static/vendor/jszip.min.acc7e41455a8.js", "/static/vendor/docx-preview.min.3573b8d99344.js"]
756
  : ["md", "markdown"].includes(extension) ? ["/static/vendor/marked.min.eaccee2fb9fb.js", "/static/vendor/purify.min.c2f26ea4fc0d.js"] : [];
757
- for (const href of assets) {
758
  if (warmedReaderAssets.has(href)) continue;
759
  warmedReaderAssets.add(href);
760
  const link = document.createElement("link"); link.rel = "prefetch"; link.href = href; document.head.appendChild(link);
 
748
  function warmReaderIntent(rawUrl) {
749
  if (!rawUrl) return;
750
  let extension = "";
751
+ try {
752
+ const readerUrl = new URL(rawUrl, location.origin);
753
+ extension = (readerUrl.searchParams.get("ext") || "").toLowerCase();
754
+ } catch (_) { return; }
755
+ const shellAssets = ["/static/reader.css", "/static/reader-contract.js", "/static/reader-store.js", "/static/reader.js"];
756
+ const engineAssets = extension === "pdf"
757
  ? ["/static/vendor/pdf.min.e0be3863c23c.mjs", "/static/pdf-worker-wrapper.mjs", "/static/vendor/pdf.worker.min.0613f41490dd.mjs"]
758
+ : extension === "epub" ? ["/static/vendor/jszip.min.acc7e41455a8.js", "/static/vendor/epub.min.06eae1574510.js"]
759
  : extension === "docx" ? ["/static/vendor/jszip.min.acc7e41455a8.js", "/static/vendor/docx-preview.min.3573b8d99344.js"]
760
  : ["md", "markdown"].includes(extension) ? ["/static/vendor/marked.min.eaccee2fb9fb.js", "/static/vendor/purify.min.c2f26ea4fc0d.js"] : [];
761
+ for (const href of shellAssets.concat(engineAssets)) {
762
  if (warmedReaderAssets.has(href)) continue;
763
  warmedReaderAssets.add(href);
764
  const link = document.createElement("link"); link.rel = "prefetch"; link.href = href; document.head.appendChild(link);
static/reader.js CHANGED
@@ -286,19 +286,74 @@ async function renderText(markdown, prepared) {
286
  let response = await prepared.response;
287
  if (!response.ok) response = await fetch(sourceUrl);
288
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
289
- const text = await response.text();
290
- if (!markdown) { const pre = document.createElement("pre"); pre.className = "reader-text"; pre.textContent = text; content.appendChild(pre); }
291
  else {
 
292
  await prepared.engines;
293
  const article = document.createElement("article"); article.className = "reader-markdown";
294
  article.innerHTML = DOMPurify.sanitize(marked.parse(text), { USE_PROFILES: { html: true } }); content.appendChild(article);
295
  }
296
  status.textContent = "已加载";
297
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  async function renderEpub(prepared) {
299
- await prepared;
 
300
  const frame = document.createElement("div"); frame.className = "epub-frame"; content.appendChild(frame);
301
- await displayEpub(sourceUrl, frame);
302
  status.textContent = "EPUB";
303
  }
304
  async function displayEpub(url, frame) {
@@ -352,7 +407,7 @@ function prepareDocument() {
352
  if (capability.mode === "pdf") return import(PDFJS_URL).then((pdfjs) => { pdfjs.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_URL; return pdfjs.getDocument({ url: contentUrl, withCredentials: false }).promise.catch(() => pdfjs.getDocument({ url: sourceUrl, withCredentials: false }).promise); });
353
  if (capability.mode === "markdown") return Promise.all([fetch(contentUrl), Promise.all([loadScript(MARKED_URL), loadScript(PURIFY_URL)])]).then(([response, engines]) => ({ response, engines }));
354
  if (capability.mode === "text") return fetch(contentUrl).then((response) => ({ response }));
355
- if (capability.mode === "epub") return loadScript(EPUB_URL);
356
  if (capability.mode === "docx") return Promise.all([fetch(contentUrl), loadScript(JSZIP_URL).then(() => loadScript(DOCX_PREVIEW_URL))]);
357
  if (capability.mode === "image") return new Promise((resolve, reject) => { const image = new Image(); image.className = "reader-image"; image.alt = title; image.decoding = "async"; let fallback = false; image.onload = () => resolve(image); image.onerror = () => { if (!fallback) { fallback = true; image.src = sourceUrl; } else reject(new Error("image load failed")); }; image.src = contentUrl; });
358
  return Promise.resolve(null);
 
286
  let response = await prepared.response;
287
  if (!response.ok) response = await fetch(sourceUrl);
288
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
289
+ if (!markdown) await renderPlainText(response);
 
290
  else {
291
+ const text = await response.text();
292
  await prepared.engines;
293
  const article = document.createElement("article"); article.className = "reader-markdown";
294
  article.innerHTML = DOMPurify.sanitize(marked.parse(text), { USE_PROFILES: { html: true } }); content.appendChild(article);
295
  }
296
  status.textContent = "已加载";
297
  }
298
+ async function renderPlainText(response) {
299
+ const pre = document.createElement("pre"); pre.className = "reader-text";
300
+ const textNode = document.createTextNode(""); pre.appendChild(textNode); content.appendChild(pre);
301
+ if (!response.body || !response.body.getReader) {
302
+ const bytes = new Uint8Array(await response.arrayBuffer());
303
+ textNode.data = new TextDecoder(detectTextEncoding(bytes, title)).decode(bytes);
304
+ return;
305
+ }
306
+ const reader = response.body.getReader();
307
+ const first = await reader.read();
308
+ const sample = first.value || new Uint8Array();
309
+ let streamDone = first.done;
310
+ const decoder = new TextDecoder(detectTextEncoding(sample, title));
311
+ let pending = "", frame = 0;
312
+ const flush = () => { frame = 0; if (pending) { textNode.appendData(pending); pending = ""; } };
313
+ const scheduleFlush = () => { if (!frame) frame = requestAnimationFrame(flush); };
314
+ pending = decoder.decode(sample, { stream: !streamDone });
315
+ scheduleFlush();
316
+ while (!streamDone) {
317
+ const { value, done } = await reader.read();
318
+ if (done) { streamDone = true; break; }
319
+ pending += decoder.decode(value, { stream: true });
320
+ scheduleFlush();
321
+ }
322
+ pending += decoder.decode();
323
+ if (frame) cancelAnimationFrame(frame);
324
+ flush();
325
+ }
326
+ function detectTextEncoding(bytes, hint = "") {
327
+ if (bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) return "utf-8";
328
+ if (bytes[0] === 0xff && bytes[1] === 0xfe) return "utf-16le";
329
+ if (bytes[0] === 0xfe && bytes[1] === 0xff) return "utf-16be";
330
+ const evenNulls = bytes.filter((value, index) => !value && index % 2 === 0).length;
331
+ const oddNulls = bytes.filter((value, index) => !value && index % 2 === 1).length;
332
+ if (oddNulls > bytes.length / 8 && oddNulls > evenNulls * 4) return "utf-16le";
333
+ if (evenNulls > bytes.length / 8 && evenNulls > oddNulls * 4) return "utf-16be";
334
+ try { new TextDecoder("utf-8", { fatal: true }).decode(bytes, { stream: true }); return "utf-8"; } catch (_) {}
335
+ if (/[\u0400-\u04ff]/.test(hint)) return "windows-1251";
336
+ const candidates = ["gb18030", "big5", "windows-1251", "windows-1252"];
337
+ let best = "gb18030", bestScore = -Infinity;
338
+ for (const encoding of candidates) {
339
+ try {
340
+ const text = new TextDecoder(encoding, { fatal: true }).decode(bytes);
341
+ const controls = (text.match(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g) || []).length;
342
+ const cjk = (text.match(/[\u3400-\u9fff]/g) || []).length;
343
+ const cyrillic = (text.match(/[\u0400-\u04ff]/g) || []).length;
344
+ const commonCjk = (text.match(/[的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经之进着等部家自理起现实都体制当本性应开合因由然前外政社义事相全与关各重新内正反明原利质向道命此变结解问意建公系军情者立代通题党程展料员革文总品活长求老基资级图统知组别期论运农指区战任处理世]/g) || []).length;
345
+ const commonCyrillic = (text.toLowerCase().match(/[оеаинтсрвлкмдпуяызьгчбйхжюшцщэфъ]/g) || []).length;
346
+ const score = Math.max(cjk + commonCjk * 5, cyrillic + commonCyrillic) - controls * 20;
347
+ if (score > bestScore) { best = encoding; bestScore = score; }
348
+ } catch (_) {}
349
+ }
350
+ return best;
351
+ }
352
  async function renderEpub(prepared) {
353
+ const [response] = await prepared;
354
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
355
  const frame = document.createElement("div"); frame.className = "epub-frame"; content.appendChild(frame);
356
+ await displayEpub(await response.arrayBuffer(), frame);
357
  status.textContent = "EPUB";
358
  }
359
  async function displayEpub(url, frame) {
 
407
  if (capability.mode === "pdf") return import(PDFJS_URL).then((pdfjs) => { pdfjs.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_URL; return pdfjs.getDocument({ url: contentUrl, withCredentials: false }).promise.catch(() => pdfjs.getDocument({ url: sourceUrl, withCredentials: false }).promise); });
408
  if (capability.mode === "markdown") return Promise.all([fetch(contentUrl), Promise.all([loadScript(MARKED_URL), loadScript(PURIFY_URL)])]).then(([response, engines]) => ({ response, engines }));
409
  if (capability.mode === "text") return fetch(contentUrl).then((response) => ({ response }));
410
+ if (capability.mode === "epub") return Promise.all([fetch(contentUrl), loadScript(JSZIP_URL).then(() => loadScript(EPUB_URL))]);
411
  if (capability.mode === "docx") return Promise.all([fetch(contentUrl), loadScript(JSZIP_URL).then(() => loadScript(DOCX_PREVIEW_URL))]);
412
  if (capability.mode === "image") return new Promise((resolve, reject) => { const image = new Image(); image.className = "reader-image"; image.alt = title; image.decoding = "async"; let fallback = false; image.onload = () => resolve(image); image.onerror = () => { if (!fallback) { fallback = true; image.src = sourceUrl; } else reject(new Error("image load failed")); }; image.src = contentUrl; });
413
  return Promise.resolve(null);
static/sw.js CHANGED
@@ -60,6 +60,18 @@ self.addEventListener("fetch", (event) => {
60
  if (url.pathname.startsWith("/txt/")) {
61
  return;
62
  }
 
 
 
 
 
 
 
 
 
 
 
 
63
  if (event.request.mode === "navigate") {
64
  return;
65
  }
 
60
  if (url.pathname.startsWith("/txt/")) {
61
  return;
62
  }
63
+ if (event.request.mode === "navigate" && url.pathname === "/static/reader.html") {
64
+ event.respondWith(
65
+ caches.open(CACHE_NAME).then((cache) => cache.match("/static/reader.html").then((cached) => {
66
+ const fetchPromise = fetch(event.request).then((response) => {
67
+ if (response.ok) cache.put("/static/reader.html", response.clone());
68
+ return response;
69
+ }).catch(() => cached);
70
+ return cached || fetchPromise;
71
+ }))
72
+ );
73
+ return;
74
+ }
75
  if (event.request.mode === "navigate") {
76
  return;
77
  }