vomebook commited on
Commit
8644ef8
·
verified ·
1 Parent(s): e7a1a72

Add Reader folder shortcut

Browse files
static/app.js CHANGED
@@ -708,10 +708,10 @@ function loadReaderAssets() {
708
 
709
  function applyReaderAsset(record, repo, relativePath, originalLink) {
710
  const asset = readerAssets && readerAssets[`${repo}\0${relativePath}`];
711
- if (!asset || asset.s !== 2 || !["p", "e", "d"].includes(asset.m) || !/^objects\/[0-9a-f]{2}\/[0-9a-f]{64}\/(?:[a-z0-9-]+\/)?(document\.pdf|book\.epub|document\.docx)$/.test(asset.p || "")) return record;
712
  return Object.assign({}, record, {
713
  ReaderLink: `https://huggingface.co/datasets/vomebook/Reader-Assets/resolve/main/${asset.p}`,
714
- ReaderExtension: asset.m === "p" ? "pdf" : asset.m === "e" ? "epub" : "docx",
715
  DownloadLink: originalLink,
716
  });
717
  }
@@ -721,8 +721,20 @@ function getRecordLink(rec) {
721
  return `https://huggingface.co/datasets/${rec.Repo || ""}/resolve/main/${encodeRecordPath(buildRecordRelativePath(rec))}`;
722
  }
723
 
 
 
 
 
 
 
 
 
 
 
 
 
724
  function getReaderLink(rec, returnUrl = location.href) {
725
- const readerRecord = Object.assign({}, rec, { Link: getRecordLink(rec), ReturnUrl: returnUrl });
726
  if (rec.HasTxt && String(rec.Extension || "").toLowerCase() !== "txt") {
727
  const relPath = buildRecordRelativePath(rec);
728
  const stem = relPath.includes(".") ? relPath.slice(0, relPath.lastIndexOf(".")) : relPath;
 
708
 
709
  function applyReaderAsset(record, repo, relativePath, originalLink) {
710
  const asset = readerAssets && readerAssets[`${repo}\0${relativePath}`];
711
+ if (!asset || asset.s !== 2 || !["p", "e", "d", "h"].includes(asset.m) || !/^objects\/[0-9a-f]{2}\/[0-9a-f]{64}\/(?:[a-z0-9-]+\/)?(document\.pdf|book\.epub|document\.docx|document\.html)$/.test(asset.p || "")) return record;
712
  return Object.assign({}, record, {
713
  ReaderLink: `https://huggingface.co/datasets/vomebook/Reader-Assets/resolve/main/${asset.p}`,
714
+ ReaderExtension: asset.m === "p" ? "pdf" : asset.m === "e" ? "epub" : asset.m === "d" ? "docx" : "html",
715
  DownloadLink: originalLink,
716
  });
717
  }
 
721
  return `https://huggingface.co/datasets/${rec.Repo || ""}/resolve/main/${encodeRecordPath(buildRecordRelativePath(rec))}`;
722
  }
723
 
724
+ function getReaderFolderUrl(rec) {
725
+ const repo = String(rec.Repo || "").split("/").pop();
726
+ if (!repo) return "";
727
+ const target = new URL(location.href);
728
+ target.pathname = `/${encodeURIComponent(repo)}`;
729
+ target.hash = "";
730
+ for (const key of ["repo", "folder_self", "folder_subtree", "path"]) target.searchParams.delete(key);
731
+ const folder = Array.isArray(rec.Folder) ? rec.Folder.join("/") : "";
732
+ if (folder) target.searchParams.append("folder_self", folder);
733
+ return target.href;
734
+ }
735
+
736
  function getReaderLink(rec, returnUrl = location.href) {
737
+ const readerRecord = Object.assign({}, rec, { Link: getRecordLink(rec), ReturnUrl: returnUrl, FolderUrl: getReaderFolderUrl(rec) });
738
  if (rec.HasTxt && String(rec.Extension || "").toLowerCase() !== "txt") {
739
  const relPath = buildRecordRelativePath(rec);
740
  const stem = relPath.includes(".") ? relPath.slice(0, relPath.lastIndexOf(".")) : relPath;
static/reader-contract.js CHANGED
@@ -9,7 +9,7 @@
9
  FAILED: 4,
10
  });
11
  const modes = Object.freeze({
12
- pdf: "pdf", epub: "epub", docx: "docx", txt: "text", md: "markdown", markdown: "markdown",
13
  jpg: "image", jpeg: "image", png: "image", gif: "image", bmp: "image", webp: "image",
14
  });
15
  const articleExtensions = Object.freeze(Object.keys(modes));
@@ -41,6 +41,10 @@
41
  if (record.DownloadLink || record.downloadLink) params.set("download", record.DownloadLink || record.downloadLink);
42
  if (record.OcrUrl || record.ocrUrl) params.set("ocr", record.OcrUrl || record.ocrUrl);
43
  if (record.ReturnUrl || record.returnUrl) params.set("return", record.ReturnUrl || record.returnUrl);
 
 
 
 
44
  return (basePath || "/reader.html") + "?" + params.toString();
45
  }
46
 
 
9
  FAILED: 4,
10
  });
11
  const modes = Object.freeze({
12
+ pdf: "pdf", epub: "epub", docx: "docx", html: "html", htm: "html", txt: "text", md: "markdown", markdown: "markdown",
13
  jpg: "image", jpeg: "image", png: "image", gif: "image", bmp: "image", webp: "image",
14
  });
15
  const articleExtensions = Object.freeze(Object.keys(modes));
 
41
  if (record.DownloadLink || record.downloadLink) params.set("download", record.DownloadLink || record.downloadLink);
42
  if (record.OcrUrl || record.ocrUrl) params.set("ocr", record.OcrUrl || record.ocrUrl);
43
  if (record.ReturnUrl || record.returnUrl) params.set("return", record.ReturnUrl || record.returnUrl);
44
+ const repo = String(record.Repo || record.repo || "").split("/").pop();
45
+ const folder = Array.isArray(record.Folder || record.folder) ? (record.Folder || record.folder).join("/") : "";
46
+ if (repo) params.set("path", repo + (folder ? "/" + folder : ""));
47
+ if (record.FolderUrl || record.folderUrl) params.set("folder_url", record.FolderUrl || record.folderUrl);
48
  return (basePath || "/reader.html") + "?" + params.toString();
49
  }
50
 
static/reader.css CHANGED
@@ -4,7 +4,9 @@ body { margin: 0; height: 100vh; height: 100dvh; overflow: hidden; display: flex
4
  .reader-toolbar { min-height: 44px; flex: none; display: flex; align-items: center; gap: 7px; padding: 3px 10px; background: #1b1e21; border-bottom: 1px solid #34383d; }
5
  .reader-heading { min-width: 0; flex: 1; display: flex; flex-direction: column; }
6
  .reader-heading strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; line-height: 18px; }
7
- .reader-heading span { color: #9ba3aa; font-size: 12px; line-height: 16px; }
 
 
8
  .reader-actions { display: flex; align-items: center; gap: 4px; }
9
  .control-group { display: inline-flex; align-items: center; gap: 2px; }
10
  .control-group[hidden] { display: none; }
@@ -36,6 +38,7 @@ body { margin: 0; height: 100vh; height: 100dvh; overflow: hidden; display: flex
36
  .reader-markdown a { color: #72a7df; }
37
  .reader-error { margin: 20vh auto; max-width: 560px; color: #d7dadd; text-align: center; line-height: 1.6; }
38
  .epub-frame { width: 100%; height: calc(100vh - 96px); background: white; }
 
39
  .docx-body { overflow-x: auto; color: #111; }
40
  .docx-body .reader-docx-wrapper { background: transparent; padding: 0; zoom: var(--reader-zoom); }
41
  .docx-body .reader-docx-wrapper > section.reader-docx { margin: 0 auto 18px; box-shadow: 0 2px 14px #0008; }
 
4
  .reader-toolbar { min-height: 44px; flex: none; display: flex; align-items: center; gap: 7px; padding: 3px 10px; background: #1b1e21; border-bottom: 1px solid #34383d; }
5
  .reader-heading { min-width: 0; flex: 1; display: flex; flex-direction: column; }
6
  .reader-heading strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; line-height: 18px; }
7
+ .reader-heading span, .reader-path { color: #9ba3aa; font-size: 12px; line-height: 16px; }
8
+ .reader-path { min-width: 0; max-width: 100%; padding: 0; border: 0; background: transparent; font-family: inherit; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
9
+ .reader-path:hover, .reader-path:focus-visible { color: #d5d9dc; text-decoration: underline; outline: none; }
10
  .reader-actions { display: flex; align-items: center; gap: 4px; }
11
  .control-group { display: inline-flex; align-items: center; gap: 2px; }
12
  .control-group[hidden] { display: none; }
 
38
  .reader-markdown a { color: #72a7df; }
39
  .reader-error { margin: 20vh auto; max-width: 560px; color: #d7dadd; text-align: center; line-height: 1.6; }
40
  .epub-frame { width: 100%; height: calc(100vh - 96px); background: white; }
41
+ .html-frame { display: block; width: 100%; height: calc(100vh - 96px); border: 0; background: white; }
42
  .docx-body { overflow-x: auto; color: #111; }
43
  .docx-body .reader-docx-wrapper { background: transparent; padding: 0; zoom: var(--reader-zoom); }
44
  .docx-body .reader-docx-wrapper > section.reader-docx { margin: 0 auto 18px; box-shadow: 0 2px 14px #0008; }
static/reader.html CHANGED
@@ -14,7 +14,7 @@
14
  <body>
15
  <header class="reader-toolbar">
16
  <button id="back" class="icon-button" type="button" title="返回" aria-label="返回">&#8592;</button>
17
- <div class="reader-heading"><strong id="title">在线阅读</strong><span id="status">正在准备...</span></div>
18
  <div class="reader-actions">
19
  <span class="control-group page-controls">
20
  <button id="page-prev" class="icon-button" type="button" title="上一页" aria-label="上一页">&#8249;</button>
 
14
  <body>
15
  <header class="reader-toolbar">
16
  <button id="back" class="icon-button" type="button" title="返回" aria-label="返回">&#8592;</button>
17
+ <div class="reader-heading"><strong id="title">在线阅读</strong><button id="reader-path" class="reader-path" type="button" title="在搜索中筛选此文件夹" hidden></button><span id="status">正在准备...</span></div>
18
  <div class="reader-actions">
19
  <span class="control-group page-controls">
20
  <button id="page-prev" class="icon-button" type="button" title="上一页" aria-label="上一页">&#8249;</button>
static/reader.js CHANGED
@@ -41,6 +41,8 @@ const loadingStatus = document.querySelector("#loading-status");
41
  const title = params.get("title") || "在线阅读";
42
  const ocrUrl = params.get("ocr") || "";
43
  const returnUrl = params.get("return") || "";
 
 
44
  const returnNavigationToken = params.get("nav") || "";
45
  const returnHistoryKey = returnNavigationToken ? `reader-return:${returnNavigationToken}` : "";
46
  let canReturnWithHistory = false;
@@ -68,10 +70,24 @@ let progressSaveChain = Promise.resolve();
68
  const viewport = document.querySelector("#viewport");
69
  const zoomInput = document.querySelector("#zoom");
70
  const pageInput = document.querySelector("#page-number");
 
71
  document.querySelector(".page-controls").hidden = !["pdf", "epub"].includes(capability.mode);
72
 
73
  document.querySelector("#title").textContent = title;
74
  document.title = title + " - VoiceOfML Reader";
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  document.querySelector("#back").addEventListener("click", () => {
76
  try {
77
  const target = new URL(returnUrl, location.origin);
@@ -162,7 +178,7 @@ function validSource(raw) {
162
  try {
163
  const url = new URL(raw);
164
  if (url.protocol !== "https:" || !["huggingface.co", "hf-mirror.com"].includes(url.hostname)) return false;
165
- const readerAsset = /^\/datasets\/vomebook\/Reader-Assets\/resolve\/[^/]+\/objects\/[0-9a-f]{2}\/[0-9a-f]{64}\/(?:[a-z0-9-]+\/)?(document\.pdf|book\.epub|document\.docx)$/.test(url.pathname);
166
  if (extension === "docx") return readerAsset;
167
  return /^\/datasets\/VoiceOfML\/[^/]+\/(resolve|raw)\//.test(url.pathname) || readerAsset;
168
  } catch (_) { return false; }
@@ -306,6 +322,26 @@ async function renderText(markdown, prepared) {
306
  }
307
  status.textContent = "已加载";
308
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  async function renderPlainText(response) {
310
  const pre = document.createElement("pre"); pre.className = "reader-text";
311
  const textNode = document.createTextNode(""); pre.appendChild(textNode); content.appendChild(pre);
@@ -423,6 +459,7 @@ async function start() {
423
  else if (capability.mode === "image") { content.appendChild(prepared); status.textContent = "图片"; }
424
  else if (capability.mode === "text") await renderText(false, prepared);
425
  else if (capability.mode === "markdown") await renderText(true, prepared);
 
426
  else if (capability.mode === "epub") await renderEpub(prepared);
427
  else if (capability.mode === "docx") await renderDocx(prepared);
428
  loadingStatus.hidden = true;
@@ -433,6 +470,7 @@ async function start() {
433
  function prepareDocument() {
434
  if (capability.mode === "pdf") return import(PDFJS_URL).then((pdfjs) => { pdfjs.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_URL; const options = (url) => ({ url, wasmUrl: PDFJS_WASM_URL, cMapUrl: PDFJS_CMAP_URL, cMapPacked: true, standardFontDataUrl: PDFJS_STANDARD_FONT_URL, withCredentials: false }); return pdfjs.getDocument(options(contentUrl)).promise.catch(() => pdfjs.getDocument(options(sourceUrl)).promise); });
435
  if (capability.mode === "markdown") return Promise.all([fetch(contentUrl), Promise.all([loadScript(MARKED_URL), loadScript(PURIFY_URL)])]).then(([response, engines]) => ({ response, engines }));
 
436
  if (capability.mode === "text") return fetch(contentUrl).then((response) => ({ response }));
437
  if (capability.mode === "epub") return Promise.all([fetch(contentUrl), loadScript(JSZIP_URL).then(() => loadScript(EPUB_URL))]);
438
  if (capability.mode === "docx") return Promise.all([fetch(contentUrl), loadScript(JSZIP_URL).then(() => loadScript(DOCX_PREVIEW_URL))]);
 
41
  const title = params.get("title") || "在线阅读";
42
  const ocrUrl = params.get("ocr") || "";
43
  const returnUrl = params.get("return") || "";
44
+ const readerPathLabel = params.get("path") || "";
45
+ const folderReturnUrl = params.get("folder_url") || "";
46
  const returnNavigationToken = params.get("nav") || "";
47
  const returnHistoryKey = returnNavigationToken ? `reader-return:${returnNavigationToken}` : "";
48
  let canReturnWithHistory = false;
 
70
  const viewport = document.querySelector("#viewport");
71
  const zoomInput = document.querySelector("#zoom");
72
  const pageInput = document.querySelector("#page-number");
73
+ const readerPath = document.querySelector("#reader-path");
74
  document.querySelector(".page-controls").hidden = !["pdf", "epub"].includes(capability.mode);
75
 
76
  document.querySelector("#title").textContent = title;
77
  document.title = title + " - VoiceOfML Reader";
78
+ try {
79
+ const folderTarget = new URL(folderReturnUrl, location.origin);
80
+ if (readerPathLabel && folderTarget.origin === location.origin && folderTarget.pathname.split("/").filter(Boolean).length === 1) {
81
+ readerPath.textContent = readerPathLabel;
82
+ readerPath.setAttribute("aria-label", `筛选文件夹:${readerPathLabel}`);
83
+ readerPath.hidden = false;
84
+ status.hidden = true;
85
+ readerPath.addEventListener("click", () => {
86
+ if (returnHistoryKey) { try { sessionStorage.removeItem(returnHistoryKey); } catch (_) {} }
87
+ location.assign(folderTarget.href);
88
+ });
89
+ }
90
+ } catch (_) {}
91
  document.querySelector("#back").addEventListener("click", () => {
92
  try {
93
  const target = new URL(returnUrl, location.origin);
 
178
  try {
179
  const url = new URL(raw);
180
  if (url.protocol !== "https:" || !["huggingface.co", "hf-mirror.com"].includes(url.hostname)) return false;
181
+ const readerAsset = /^\/datasets\/vomebook\/Reader-Assets\/resolve\/[^/]+\/objects\/[0-9a-f]{2}\/[0-9a-f]{64}\/(?:[a-z0-9-]+\/)?(document\.pdf|book\.epub|document\.docx|document\.html)$/.test(url.pathname);
182
  if (extension === "docx") return readerAsset;
183
  return /^\/datasets\/VoiceOfML\/[^/]+\/(resolve|raw)\//.test(url.pathname) || readerAsset;
184
  } catch (_) { return false; }
 
322
  }
323
  status.textContent = "已加载";
324
  }
325
+ async function renderHtml(prepared) {
326
+ const response = await prepared.response;
327
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
328
+ const bytes = new Uint8Array(await response.arrayBuffer());
329
+ const text = new TextDecoder(detectTextEncoding(bytes, title)).decode(bytes);
330
+ await prepared.engine;
331
+ const clean = DOMPurify.sanitize(text, {
332
+ USE_PROFILES: { html: true },
333
+ FORBID_TAGS: ["base", "embed", "form", "iframe", "object", "script", "style"],
334
+ FORBID_ATTR: ["action", "formaction", "srcdoc"],
335
+ ALLOWED_URI_REGEXP: /^data:image\/(?:gif|png|jpeg|webp);/i,
336
+ });
337
+ const frame = document.createElement("iframe");
338
+ frame.className = "html-frame";
339
+ frame.setAttribute("sandbox", "allow-same-origin");
340
+ frame.setAttribute("referrerpolicy", "no-referrer");
341
+ frame.srcdoc = clean;
342
+ content.appendChild(frame);
343
+ status.textContent = "HTML";
344
+ }
345
  async function renderPlainText(response) {
346
  const pre = document.createElement("pre"); pre.className = "reader-text";
347
  const textNode = document.createTextNode(""); pre.appendChild(textNode); content.appendChild(pre);
 
459
  else if (capability.mode === "image") { content.appendChild(prepared); status.textContent = "图片"; }
460
  else if (capability.mode === "text") await renderText(false, prepared);
461
  else if (capability.mode === "markdown") await renderText(true, prepared);
462
+ else if (capability.mode === "html") await renderHtml(prepared);
463
  else if (capability.mode === "epub") await renderEpub(prepared);
464
  else if (capability.mode === "docx") await renderDocx(prepared);
465
  loadingStatus.hidden = true;
 
470
  function prepareDocument() {
471
  if (capability.mode === "pdf") return import(PDFJS_URL).then((pdfjs) => { pdfjs.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_URL; const options = (url) => ({ url, wasmUrl: PDFJS_WASM_URL, cMapUrl: PDFJS_CMAP_URL, cMapPacked: true, standardFontDataUrl: PDFJS_STANDARD_FONT_URL, withCredentials: false }); return pdfjs.getDocument(options(contentUrl)).promise.catch(() => pdfjs.getDocument(options(sourceUrl)).promise); });
472
  if (capability.mode === "markdown") return Promise.all([fetch(contentUrl), Promise.all([loadScript(MARKED_URL), loadScript(PURIFY_URL)])]).then(([response, engines]) => ({ response, engines }));
473
+ if (capability.mode === "html") return Promise.all([fetch(contentUrl), loadScript(PURIFY_URL)]).then(([response, engine]) => ({ response, engine }));
474
  if (capability.mode === "text") return fetch(contentUrl).then((response) => ({ response }));
475
  if (capability.mode === "epub") return Promise.all([fetch(contentUrl), loadScript(JSZIP_URL).then(() => loadScript(EPUB_URL))]);
476
  if (capability.mode === "docx") return Promise.all([fetch(contentUrl), loadScript(JSZIP_URL).then(() => loadScript(DOCX_PREVIEW_URL))]);