@l1yp/file-viewer 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ import e from "../_virtual/_plugin-vue_export-helper.js";
2
2
  import t from "./FileViewerBody.vue_vue_type_script_setup_true_lang.js";
3
3
  /* empty css */
4
4
  //#region src/components/FileViewerBody.vue
5
- var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-af870145"]]);
5
+ var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-82d079e3"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
8
 
@@ -1 +1 @@
1
- {"version":3,"file":"FileViewerBody.js","names":[],"sources":["../../src/components/FileViewerBody.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, onBeforeUnmount, ref, watch } from 'vue'\nimport { optionalViewer } from '@/components/optionalViewer'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { skipsTextLoad } from '@/lib/viewerKind'\nimport {\n VIEWER_FORMATTERS as FORMATTERS,\n applyFormatter,\n findFormatter,\n type ViewerFormatter,\n} from '@/lib/viewerFormats'\nimport { fileExt } from '@/lib/fileType'\nimport { looksLikeMindMap } from '@/lib/mindmap'\nimport { parseFrontmatter } from '@/lib/frontmatter'\nimport { formatBytes } from '@/lib/format'\nimport CodeView from '@/components/CodeView.vue'\nimport MarkdownText from '@/components/MarkdownText'\nimport FrontmatterCard from '@/components/FrontmatterCard.vue'\nimport ZoomImage from '@/components/ZoomImage.vue'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's CONTENT area — the type dispatch shared by {@link FileViewerPanel} (a top-level file) and\n * the archive browsers (a file inside a zip/7z/tar/rar, extracted to a blob URL). Given a {@link ViewerFile}\n * it renders the right thing: a self-fetching sub-viewer (docx/pdf/dicom/…), an inline image/video, or — for\n * the text-backed kinds — markdown / html / svg / mind-map / subtitle / geo / csv (rendered), or highlighted,\n * foldable source. It fetches its own text for the text-backed kinds and emits it via `loaded` so the host's\n * copy button can use it. The host owns the header/bar, the render↔source `mode`, and copy/download.\n *\n * Extracted from FileViewerPanel so an archive entry previews with the EXACT same viewers as a top-level file\n * (the old archive detail view only did text/image/office/dicom). The archive browsers import this lazily to\n * break the import cycle (this component lazy-imports them right back for nested archives).\n *\n * \"Open with\" override: ANY file can be force-opened as a different formatter (hex / text / markdown / html /\n * svg / image / audio / video — {@link VIEWER_FORMATTERS}), since auto-detection by extension/mime can be\n * wrong. The choice becomes an EFFECTIVE file ({@link vf}) that re-runs the dispatch. Two entry points drive\n * the SAME override: (1) the host's header menu (FileViewerPanel) calls the exposed `pickFormat`/`resetFormat`\n * — the primary, universal entry; (2) the unpreviewable-binary card's own chip row, for the archive-browser\n * context where there is no host header. The effective file is emitted via `formatchange` (so the host tracks\n * toggle/copy visibility) and the active key via `formatpick` (so the host menu highlights it). `hostChooser`\n * hides the in-body switcher bar when the host already shows the chooser in its header (no duplicate UI).\n */\nconst props = withDefaults(\n defineProps<{\n file: ViewerFile\n mode?: 'render' | 'source'\n hostChooser?: boolean\n /**\n * Render-only HTML whose archive-relative dependencies have already been rewritten. Source/copy continue to\n * use `file.content` (or fetched text), so opening an HTML entry inside a ZIP still shows its original source.\n */\n htmlPreview?: string\n }>(),\n { mode: 'render', hostChooser: false },\n)\nconst emit = defineEmits<{\n loaded: [text: string]\n formatchange: [file: ViewerFile]\n formatpick: [key: string]\n}>()\n\n// Archives import this body; lazy-import them back so the module cycle never evaluates eagerly.\nconst ZipBrowser = defineAsyncComponent(() => import('@/components/ZipBrowser.vue'))\nconst SevenZipBrowser = defineAsyncComponent(() => import('@/components/SevenZipBrowser.vue'))\nconst ArchiveBrowser = defineAsyncComponent(() => import('@/components/ArchiveBrowser.vue'))\nconst BinObjBrowser = defineAsyncComponent(() => import('@/components/BinObjBrowser.vue'))\nconst EplViewer = defineAsyncComponent(() => import('@/components/EplViewer.vue'))\nconst ClassViewer = defineAsyncComponent(() => import('@/components/ClassViewer.vue'))\nconst HprofViewer = defineAsyncComponent(() => import('@/components/HprofViewer.vue'))\nconst MsiViewer = defineAsyncComponent(() => import('@/components/MsiViewer.vue'))\nconst AppxViewer = defineAsyncComponent(() => import('@/components/AppxViewer.vue'))\n// Heavy viewers whose engine is a STATIC optional peer dep: wrap so a missing dep shows an install card.\nconst DicomViewer = optionalViewer(\n () => import('@/components/DicomViewer.vue'),\n 'dicom-parser',\n 'DICOM 影像',\n)\nconst DocxViewer = optionalViewer(\n () => import('@/components/DocxViewer.vue'),\n 'docx-preview',\n 'Word 文档',\n)\nconst XlsxViewer = optionalViewer(\n () => import('@/components/XlsxViewer.vue'),\n ['exceljs', 'numfmt'],\n 'Excel 表格',\n)\nconst PptxViewer = optionalViewer(\n () => import('@/components/PptxViewer.vue'),\n ['@aiden0z/pptx-renderer', 'pdfjs-dist'],\n 'PowerPoint',\n)\nconst RtfViewer = defineAsyncComponent(() => import('@/components/RtfViewer.vue'))\nconst PdfViewer = optionalViewer(() => import('@/components/PdfViewer.vue'), 'pdfjs-dist', 'PDF')\nconst MindMapView = defineAsyncComponent(() => import('@/components/MindMapView.vue'))\nconst XmindViewer = defineAsyncComponent(() => import('@/components/XmindViewer.vue'))\nconst EmlViewer = defineAsyncComponent(() => import('@/components/EmlViewer.vue'))\nconst MhtViewer = defineAsyncComponent(() => import('@/components/MhtViewer.vue'))\nconst HarViewer = defineAsyncComponent(() => import('@/components/HarViewer.vue'))\nconst SqliteViewer = optionalViewer(\n () => import('@/components/SqliteViewer.vue'),\n 'sql.js',\n 'SQLite 数据库',\n)\nconst SubtitleViewer = defineAsyncComponent(() => import('@/components/SubtitleViewer.vue'))\nconst FontViewer = defineAsyncComponent(() => import('@/components/FontViewer.vue'))\nconst NotebookViewer = defineAsyncComponent(() => import('@/components/NotebookViewer.vue'))\nconst GeoViewer = defineAsyncComponent(() => import('@/components/GeoViewer.vue'))\nconst CsvViewer = defineAsyncComponent(() => import('@/components/CsvViewer.vue'))\nconst JsonlViewer = defineAsyncComponent(() => import('@/components/JsonlViewer.vue'))\nconst CertViewer = defineAsyncComponent(() => import('@/components/CertViewer.vue'))\nconst JksViewer = defineAsyncComponent(() => import('@/components/JksViewer.vue'))\nconst AudioViewer = defineAsyncComponent(() => import('@/components/AudioViewer.vue'))\nconst QmcAudioViewer = defineAsyncComponent(() => import('@/components/QmcAudioViewer.vue'))\nconst LrcViewer = defineAsyncComponent(() => import('@/components/LrcViewer.vue'))\nconst LogViewer = defineAsyncComponent(() => import('@/components/LogViewer.vue'))\nconst ModelViewer = optionalViewer(() => import('@/components/ModelViewer.vue'), 'three', '3D 模型')\nconst RasterViewer = defineAsyncComponent(() => import('@/components/RasterViewer.vue'))\nconst PsdViewer = defineAsyncComponent(() => import('@/components/PsdViewer.vue'))\nconst EbookViewer = defineAsyncComponent(() => import('@/components/EbookViewer.vue'))\nconst DrawioViewer = defineAsyncComponent(() => import('@/components/DrawioViewer.vue'))\nconst DxfViewer = defineAsyncComponent(() => import('@/components/DxfViewer.vue'))\nconst HexViewer = defineAsyncComponent(() => import('@/components/HexViewer.vue'))\n\n// ── \"open with\" override ──────────────────────────────────────────────────────────────────────\n// A user-chosen formatter ({@link VIEWER_FORMATTERS}). When set it replaces props.file as the EFFECTIVE file\n// the dispatch (and load) use; cleared whenever a different file is opened. Driven by the host header menu\n// (via the exposed pickFormat/resetFormat) and/or the in-body binary-card chips — both funnel through `pick`.\nconst pickedKey = ref('')\nconst picked = ref<ViewerFile | null>(null)\n// The effective file: the user's chosen formatter, else the file passed in.\nconst vf = computed<ViewerFile>(() => picked.value ?? props.file)\n\nfunction pick(f: ViewerFormatter): void {\n pickedKey.value = f.key\n picked.value = applyFormatter(props.file, f)\n}\n// Host-driven entry points (FileViewerPanel's header menu): pick by key, or fall back to auto-detection.\nfunction pickFormat(key: string): void {\n const f = findFormatter(key)\n if (f) pick(f)\n}\nfunction resetFormat(): void {\n pickedKey.value = ''\n picked.value = null\n}\n// Announce the active formatter so the host header can highlight it (fires on pick, reset, and file change).\nwatch(pickedKey, (k) => emit('formatpick', k))\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst text = ref('')\nconst errorMsg = ref('')\n\n// Mind map only renders the diagram once the loaded text actually looks like one (a non-mind-map `.mm` reads\n// as plain source). Same content sniff the panel used.\nconst isMindMap = computed(\n () =>\n vf.value.mindmap === true &&\n status.value === 'ready' &&\n looksLikeMindMap(text.value, fileExt(vf.value.name)),\n)\n\n// Markdown render: lift any leading YAML frontmatter into a card; the source view still shows the raw file.\nconst frontmatter = computed(() =>\n vf.value.lang === 'markdown' ? parseFrontmatter(text.value) : { data: {}, body: text.value },\n)\nconst hasFrontmatter = computed(() => Object.keys(frontmatter.value.data).length > 0)\n\n// Drop a single trailing newline so the source view doesn't show a phantom last line.\nconst sourceText = computed(() => text.value.replace(/\\n$/, ''))\n\n// SVG render: rasterize the markup via a data-URL <img> (an <img> never runs SVG scripts).\nconst svgSrc = computed(() =>\n text.value ? `data:image/svg+xml,${encodeURIComponent(text.value)}` : '',\n)\n\n// HTML preview, two modes (default = inert):\n// - INERT (default): the iframe carries an empty `sandbox` (no `allow-scripts`) plus an injected\n// `script-src 'none'` CSP, so NO upstream script runs — only layout/CSS/images render.\n// - INTERACTIVE (opt-in via the toolbar): scripts run inside a HARDENED sandbox — `allow-scripts` WITHOUT\n// `allow-same-origin` gives the frame an opaque origin (no access to our cookies/storage/session/DOM); that\n// opaque origin is the REAL isolation. The CSP allows external sub-resources AND network so CDN-based pages\n// fully work — scripts/styles/fonts/images from unpkg/jsdelivr/Google Fonts, plus fetch/XHR/WebSocket and\n// source-map loads (`connect-src` open to https/http/ws/wss). Only the structural hardening stays:\n// `object-src 'none'` (no plugins) and `base-uri 'none'` (no <base> hijack); forms are already blocked by\n// the sandbox (no `allow-forms`). Because the origin is opaque, even with network open the page can't reach\n// our session/cookies — it acts only with its own (already-visible) content. Review the source first\n// (render/source toggle), then enable. The ⛶ button opens this SAME sandboxed frame as a full-screen\n// in-app overlay (still an iframe → still opaque-origin), so a big interactive view stays safe — there's\n// no top-level / new-tab path that could ever run scripts in our origin.\nconst INERT_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'\">`\nconst INTERACTIVE_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"default-src 'unsafe-inline' 'unsafe-eval' data: blob: https: http:; ` +\n `connect-src data: blob: https: http: ws: wss:; object-src 'none'; base-uri 'none'\">`\n\nfunction injectCsp(html: string, csp: string): string {\n const head = html.match(/<head[^>]*>/i)\n if (head) {\n const at = (head.index ?? 0) + head[0].length\n return html.slice(0, at) + csp + html.slice(at)\n }\n return `<!doctype html><html><head><meta charset=\"utf-8\">${csp}</head><body>${html}</body></html>`\n}\n\nconst inertBlobUrl = ref('') // the inert (non-interactive) iframe + full-screen overlay\nconst interactiveBlobUrl = ref('') // built only when the user opts in\nconst interactive = ref(false)\nconst fullscreen = ref(false) // in-app full-screen overlay (Teleport to body) over the SAME sandboxed frame\nconst frameBlobUrl = computed(() =>\n interactive.value ? interactiveBlobUrl.value : inertBlobUrl.value,\n)\nconst previewHtml = computed(() => props.htmlPreview ?? text.value)\n\nfunction revokeHtmlBlobs(): void {\n if (inertBlobUrl.value) {\n URL.revokeObjectURL(inertBlobUrl.value)\n inertBlobUrl.value = ''\n }\n if (interactiveBlobUrl.value) {\n URL.revokeObjectURL(interactiveBlobUrl.value)\n interactiveBlobUrl.value = ''\n }\n}\n\nwatch([() => vf.value.html, previewHtml], ([html, preview]) => {\n revokeHtmlBlobs()\n interactive.value = false // a new file / changed text always returns to the safe inert default\n fullscreen.value = false // …and closes any open full-screen overlay\n if (html && typeof URL !== 'undefined' && URL.createObjectURL) {\n inertBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(preview, INERT_CSP)], { type: 'text/html' }),\n )\n }\n})\n\n// Toggle the sandboxed iframe between inert and interactive (builds the interactive blob lazily on first use).\nfunction toggleInteractive(): void {\n if (interactive.value) {\n interactive.value = false\n return\n }\n if (typeof URL !== 'undefined' && URL.createObjectURL && !interactiveBlobUrl.value) {\n interactiveBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(previewHtml.value, INTERACTIVE_CSP)], { type: 'text/html' }),\n )\n }\n interactive.value = true\n}\n\n// Full-screen overlay key handling + body scroll lock — only while open, SSR-guarded (no window in smoke).\nfunction onFsKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') fullscreen.value = false\n}\nwatch(fullscreen, (on) => {\n if (typeof window === 'undefined') return\n if (on) {\n window.addEventListener('keydown', onFsKey)\n document.body.style.overflow = 'hidden'\n } else {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\nasync function load(noStore = false): Promise<void> {\n const f = vf.value\n // Self-fetching sub-viewers + image/video/binary render straight from the URL — no text to load.\n if (skipsTextLoad(f)) {\n text.value = ''\n status.value = 'ready'\n return\n }\n if (f.content != null) {\n text.value = f.content\n status.value = 'ready'\n emit('loaded', f.content)\n return\n }\n if (!f.url) {\n text.value = ''\n status.value = 'ready'\n return\n }\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(f.url, noStore ? { cache: 'no-store' } : {})\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const t = await res.text()\n text.value = t\n status.value = 'ready'\n emit('loaded', t)\n } catch (e) {\n errorMsg.value = String(e)\n status.value = 'error'\n }\n}\n\n// A new file resets any \"open with\" override (registered BEFORE the vf watcher so picked is cleared before the\n// effective file is re-read below).\nwatch(\n () => props.file,\n () => {\n picked.value = null\n pickedKey.value = ''\n },\n)\n// Load + announce the effective file whenever it changes (a new file, or a picked formatter).\nwatch(\n vf,\n (v) => {\n emit('formatchange', v)\n void load()\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => {\n revokeHtmlBlobs()\n if (typeof window !== 'undefined') {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\n// The host's refresh button re-fetches (cache-busting) through here; its header menu drives the \"open with\"\n// override through pickFormat / resetFormat (see the override block above).\ndefineExpose({ reload: () => load(true), pickFormat, resetFormat })\n</script>\n\n<template>\n <div class=\"fvb\">\n <!-- forced-format switcher: shown once the user picks a formatter, so they can swap. Hidden when the host\n provides the chooser in its header (FileViewerPanel) — only the archive browsers render it in-body. -->\n <div v-if=\"picked && !hostChooser\" class=\"fvb__fmtbar\">\n <span class=\"fvb__fmtbar-label\">格式</span>\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n :class=\"{ 'fvb__fmtchip--on': pickedKey === f.key }\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n\n <!-- zip / 7z / tar-rar: the archive browsers fetch the bytes, decompress in-browser, and browse entries -->\n <ZipBrowser v-if=\"vf.zip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <SevenZipBrowser v-else-if=\"vf.sevenZip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <ArchiveBrowser v-else-if=\"vf.archive\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :kind=\"vf.archive\" />\n\n <!-- native binary object (.so/.dll): parse + list symbols -->\n <BinObjBrowser v-else-if=\"vf.binobj\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- e-language (.e/.ec): parse the binary section format + decompile each method to 易语言 source -->\n <EplViewer v-else-if=\"vf.epl\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- java bytecode (.class): parse the structural tables + list fields/methods (no decompilation) -->\n <ClassViewer v-else-if=\"vf.javaClass\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- jvm heap dump (.hprof): stream-parse → class histogram + per-class object drill-in (paginated/filterable) -->\n <HprofViewer v-else-if=\"vf.hprof\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows installer (.msi): parse the OLE compound file → product identity + summary info -->\n <MsiViewer v-else-if=\"vf.msi\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows app package (.appx/.msix): unzip the manifest → identity / deps / capabilities -->\n <AppxViewer v-else-if=\"vf.appx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- xmind workbook (.xmind): unzip content.json/xml → render as an interactive mind map -->\n <XmindViewer v-else-if=\"vf.xmind\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- dxf (.dxf): parse the ASCII CAD drawing → SVG with pan/zoom + layer toggles -->\n <DxfViewer v-else-if=\"vf.dxf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <DicomViewer v-else-if=\"vf.dicom\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <DocxViewer v-else-if=\"vf.docx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <XlsxViewer v-else-if=\"vf.xlsx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PptxViewer v-else-if=\"vf.pptx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <RtfViewer v-else-if=\"vf.rtf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PdfViewer v-else-if=\"vf.pdf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <EmlViewer v-else-if=\"vf.eml\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <MhtViewer v-else-if=\"vf.mht\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <HarViewer v-else-if=\"vf.har\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <SqliteViewer v-else-if=\"vf.sqlite\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <CertViewer v-else-if=\"vf.cert\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- java keystore (.jks/.keystore/.jceks): parse the inventory + certs; password decrypts private keys -->\n <JksViewer v-else-if=\"vf.jks\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <FontViewer v-else-if=\"vf.font\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <NotebookViewer v-else-if=\"vf.ipynb\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <AudioViewer v-else-if=\"vf.audio\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- QQ Music encrypted audio (.mflac/.mgg): decrypt in-browser → play + download decrypted -->\n <QmcAudioViewer v-else-if=\"vf.qmc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- LRC lyrics (.lrc): self-fetch + sniff GBK → synced lyric list (own render/source toggle) -->\n <LrcViewer v-else-if=\"vf.lrc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <LogViewer v-else-if=\"vf.log\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <ModelViewer v-else-if=\"vf.model\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- PSD: dedicated viewer with a live layer tree (toggle visibility → re-composite); other raster kinds\n (TIFF/HEIC/RAW) use the generic decoding viewer. -->\n <PsdViewer\n v-else-if=\"vf.raster === 'psd'\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n />\n <RasterViewer\n v-else-if=\"vf.raster\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.raster\"\n />\n <EbookViewer\n v-else-if=\"vf.ebook\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.ebook\"\n />\n\n <!-- hex dump: offset + raw bytes + ASCII (the universal \"open with\" fall-back; never auto-detected) -->\n <HexViewer v-else-if=\"vf.hex\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- image (archive entry, or forced via \"open with\"): a zoomable inline picture -->\n <div v-else-if=\"vf.image\" class=\"fvb__imagezoom\">\n <ZoomImage :src=\"vf.url ?? ''\" :alt=\"vf.name\" />\n </div>\n\n <!-- video (archive entry): native player from the blob URL -->\n <div v-else-if=\"vf.video\" class=\"fvb__video\">\n <video class=\"fvb__videoel\" :src=\"vf.url ?? ''\" controls preload=\"metadata\" />\n </div>\n\n <!-- unpreviewable binary (archive entry / unmatched file): download card + \"open with\" formatter chips -->\n <div v-else-if=\"vf.binary\" class=\"fvb__binary\">\n <Icon name=\"archive\" :size=\"34\" />\n <p>无法预览的二进制文件</p>\n <p v-if=\"vf.size\" class=\"fvb__binary-sub\">{{ formatBytes(vf.size) }}</p>\n <a class=\"fvb__dlbtn\" :href=\"vf.url\" :download=\"vf.name\">下载</a>\n <div class=\"fvb__openas\">\n <span class=\"fvb__openas-label\">用查看器打开:</span>\n <div class=\"fvb__openas-grid\">\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n </div>\n </div>\n\n <p v-else-if=\"status === 'loading'\" class=\"fvb__msg\">加载文件中…</p>\n <div v-else-if=\"status === 'error'\" class=\"fvb__msg fvb__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"fvb__retry\" @click=\"load(true)\">重试</button>\n </div>\n\n <!-- markdown render: frontmatter card (if any) + rendered document -->\n <div\n v-else-if=\"vf.lang === 'markdown' && mode === 'render'\"\n class=\"fvb__scroll fvb__scroll--doc\"\n >\n <div class=\"fvb__markdown\">\n <FrontmatterCard v-if=\"hasFrontmatter\" :data=\"frontmatter.data\" />\n <MarkdownText :text=\"frontmatter.body\" kp=\"fv\" />\n </div>\n </div>\n\n <!-- html render: sandboxed iframe fed by a blob. Default INERT (no scripts); the ▶ button opts into a\n HARDENED interactive sandbox (allow-scripts, opaque origin, CDN + network OK), and ⛶ opens the same\n sandboxed frame as a full-screen in-app overlay (no top-level/new-tab path). -->\n <div v-else-if=\"vf.html && mode === 'render'\" class=\"fvb__html\">\n <div class=\"fvb__htmlbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行:可加载外部 CDN 并发起网络请求(fetch/XHR/WebSocket),但无法访问本站会话 / cookie(不透明源隔离)'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,但无法访问本站会话 / cookie;建议先切到源码视图审查再启用)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"全屏预览(在应用内放大查看;可在全屏内开启交互脚本)\"\n @click=\"fullscreen = true\"\n >\n <Icon name=\"maximize\" :size=\"13\" />\n </button>\n </div>\n <!-- :key forces a full remount when toggling interactive: a `sandbox` change on an existing iframe does\n NOT re-apply to the already-loaded document, so the new doc must load on a fresh element that already\n carries `allow-scripts` (set before the element is inserted, i.e. before navigation commits). -->\n <iframe\n :key=\"interactive ? 'interactive' : 'inert'\"\n class=\"fvb__frame\"\n :src=\"frameBlobUrl\"\n title=\"HTML 预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n loading=\"lazy\"\n referrerpolicy=\"no-referrer\"\n />\n\n <!-- In-app full-screen overlay: the SAME sandboxed frame at viewport size (an iframe → opaque origin,\n never a top-level same-origin document). Esc / ✕ close it; the toolbar keeps the ▶ interactive\n toggle so scripts can be enabled while full-screen. Teleported to <body> to clear stacking contexts. -->\n <Teleport to=\"body\">\n <div v-if=\"fullscreen\" class=\"fvb__fs\">\n <div class=\"fvb__fsbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行,可联网但访问不到本站会话 / cookie'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,访问不到本站会话 / cookie)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"15\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"关闭全屏 (Esc)\"\n @click=\"fullscreen = false\"\n >\n <Icon name=\"close\" :size=\"15\" :stroke-width=\"2.2\" />\n </button>\n </div>\n <iframe\n :key=\"interactive ? 'fs-interactive' : 'fs-inert'\"\n class=\"fvb__fsframe\"\n :src=\"frameBlobUrl\"\n title=\"HTML 全屏预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n referrerpolicy=\"no-referrer\"\n />\n </div>\n </Teleport>\n </div>\n\n <!-- svg render: the markup as a picture (data-URL <img>) -->\n <div v-else-if=\"vf.svg && mode === 'render'\" class=\"fvb__svg\">\n <img class=\"fvb__svgimg\" :src=\"svgSrc\" :alt=\"vf.name\" />\n </div>\n\n <!-- mind-map render: the OPML/FreeMind XML drawn as an interactive SVG tree -->\n <div v-else-if=\"isMindMap && mode === 'render'\" class=\"fvb__fill\">\n <MindMapView :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- subtitle render: .srt/.vtt/.ass parsed into a timecoded transcript -->\n <div v-else-if=\"vf.subtitle && mode === 'render'\" class=\"fvb__fill\">\n <SubtitleViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- geo render: .geojson/.kml/.gpx drawn as an offline SVG map -->\n <div v-else-if=\"vf.geo && mode === 'render'\" class=\"fvb__fill\">\n <GeoViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- csv render: .csv/.tsv parsed into a table -->\n <div v-else-if=\"vf.csv && mode === 'render'\" class=\"fvb__fill\">\n <CsvViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- jsonl render: .jsonl/.ndjson parsed into a record list (truncated rows, click to expand) -->\n <div v-else-if=\"vf.jsonl && mode === 'render'\" class=\"fvb__fill\">\n <JsonlViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- diagram render: .drawio/.excalidraw drawn as an SVG (source toggle shows the raw XML/JSON) -->\n <div v-else-if=\"vf.diagram && mode === 'render'\" class=\"fvb__fill\">\n <DrawioViewer :text=\"text\" :name=\"vf.name\" :kind=\"vf.diagram\" />\n </div>\n\n <!-- source: everything else, or markdown/html/svg/… viewed as source — highlighted + folding + format -->\n <CodeView v-else :code=\"sourceText\" :lang=\"vf.lang ?? ''\" :name=\"vf.name\" />\n </div>\n</template>\n\n<style scoped>\n/* Fills the area below the host header/bar; each branch is itself a flex child that grows. */\n.fvb {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* Forced-format switcher bar (shown when the user opened an unhandled file via the \"open with\" chooser). */\n.fvb__fmtbar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n flex-shrink: 0;\n padding: 7px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.fvb__fmtbar-label {\n margin-right: 2px;\n font-size: 11.5px;\n color: var(--faint);\n}\n/* Chip used both in the switcher bar and on the unpreviewable-binary card's \"open with\" row. */\n.fvb__fmtchip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 999px;\n padding: 5px 11px;\n cursor: pointer;\n transition:\n background 0.15s,\n border-color 0.15s,\n color 0.15s;\n}\n.fvb__fmtchip:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.fvb__fmtchip--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__fmtchip--on:hover {\n color: #fff;\n}\n\n.fvb__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.fvb__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n}\n.fvb__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.fvb__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.fvb__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overscroll-behavior: contain;\n background: var(--code-bg);\n}\n.fvb__scroll--doc {\n overflow: auto;\n}\n.fvb__markdown {\n padding: 18px 24px 80px;\n}\n\n/* HTML render: the iframe fills the area; a small overlay toolbar (top-right) holds the open-in-new-tab\n * button. White canvas so documents that don't set their own background read as a normal page. */\n.fvb__html {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n background: #fff;\n}\n.fvb__htmlbar {\n position: absolute;\n top: 8px;\n right: 8px;\n z-index: 2;\n display: flex;\n align-items: center;\n gap: 4px;\n}\n.fvb__htmlbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n text-decoration: none;\n cursor: pointer;\n box-shadow: var(--shadow);\n}\n.fvb__htmlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n/* Interactive scripts enabled: accent the toggle so the elevated (but sandboxed) state is obvious. */\n.fvb__htmlbtn--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__htmlbtn--on:hover {\n color: #fff;\n filter: brightness(1.06);\n}\n.fvb__frame {\n display: block;\n width: 100%;\n height: 100%;\n border: none;\n background: #fff;\n}\n\n/* In-app full-screen HTML overlay (Teleport to body) — same sandboxed frame at viewport size. */\n.fvb__fs {\n position: fixed;\n inset: 0;\n z-index: 2000;\n display: flex;\n flex-direction: column;\n background: rgba(0, 0, 0, 0.82);\n animation: fvb-fs-in 0.12s ease-out;\n}\n@keyframes fvb-fs-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n.fvb__fsbar {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 6px;\n padding: 8px 10px;\n}\n.fvb__fsframe {\n flex: 1 1 auto;\n width: 100%;\n min-height: 0;\n border: none;\n background: #fff;\n}\n\n/* Generic \"viewer owns the area below\" host (mind map / subtitle / geo / csv). */\n.fvb__fill {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n.fvb__svg {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: var(--surface-2);\n}\n.fvb__svgimg {\n max-width: 100%;\n height: auto;\n object-fit: contain;\n}\n\n.fvb__imagezoom {\n flex: 1 1 auto;\n min-height: 0;\n position: relative;\n background: var(--code-bg);\n}\n\n.fvb__video {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: #000;\n}\n.fvb__videoel {\n max-width: 100%;\n max-height: 100%;\n}\n\n.fvb__binary {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 32px;\n color: var(--faint);\n text-align: center;\n}\n.fvb__binary p {\n margin: 0;\n font-size: 13px;\n color: var(--muted);\n}\n.fvb__binary-sub {\n font-size: 11.5px;\n color: var(--faint) !important;\n}\n.fvb__dlbtn {\n margin-top: 8px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 6px 16px;\n text-decoration: none;\n cursor: pointer;\n}\n.fvb__dlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* \"Open with\" formatter chooser on the unpreviewable-binary card. */\n.fvb__openas {\n margin-top: 18px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 9px;\n}\n.fvb__openas-label {\n font-size: 12px;\n color: var(--muted);\n}\n.fvb__openas-grid {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 7px;\n max-width: 320px;\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"FileViewerBody.js","names":[],"sources":["../../src/components/FileViewerBody.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, onBeforeUnmount, ref, watch } from 'vue'\nimport { optionalViewer } from '@/components/optionalViewer'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { skipsTextLoad } from '@/lib/viewerKind'\nimport {\n VIEWER_FORMATTERS as FORMATTERS,\n applyFormatter,\n findFormatter,\n type ViewerFormatter,\n} from '@/lib/viewerFormats'\nimport { fileExt } from '@/lib/fileType'\nimport { looksLikeMindMap } from '@/lib/mindmap'\nimport { parseFrontmatter } from '@/lib/frontmatter'\nimport { formatBytes } from '@/lib/format'\nimport CodeView from '@/components/CodeView.vue'\nimport MarkdownText from '@/components/MarkdownText'\nimport FrontmatterCard from '@/components/FrontmatterCard.vue'\nimport ZoomImage from '@/components/ZoomImage.vue'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's CONTENT area — the type dispatch shared by {@link FileViewerPanel} (a top-level file) and\n * the archive browsers (a file inside a zip/7z/tar/rar, extracted to a blob URL). Given a {@link ViewerFile}\n * it renders the right thing: a self-fetching sub-viewer (docx/pdf/dicom/…), an inline image/video, or — for\n * the text-backed kinds — markdown / html / svg / mind-map / subtitle / geo / csv (rendered), or highlighted,\n * foldable source. It fetches its own text for the text-backed kinds and emits it via `loaded` so the host's\n * copy button can use it. The host owns the header/bar, the render↔source `mode`, and copy/download.\n *\n * Extracted from FileViewerPanel so an archive entry previews with the EXACT same viewers as a top-level file\n * (the old archive detail view only did text/image/office/dicom). The archive browsers import this lazily to\n * break the import cycle (this component lazy-imports them right back for nested archives).\n *\n * \"Open with\" override: ANY file can be force-opened as a different formatter (hex / text / markdown / html /\n * svg / image / audio / video — {@link VIEWER_FORMATTERS}), since auto-detection by extension/mime can be\n * wrong. The choice becomes an EFFECTIVE file ({@link vf}) that re-runs the dispatch. Two entry points drive\n * the SAME override: (1) the host's header menu (FileViewerPanel) calls the exposed `pickFormat`/`resetFormat`\n * — the primary, universal entry; (2) the unpreviewable-binary card's own chip row, for the archive-browser\n * context where there is no host header. The effective file is emitted via `formatchange` (so the host tracks\n * toggle/copy visibility) and the active key via `formatpick` (so the host menu highlights it). `hostChooser`\n * hides the in-body switcher bar when the host already shows the chooser in its header (no duplicate UI).\n */\nconst props = withDefaults(\n defineProps<{\n file: ViewerFile\n mode?: 'render' | 'source'\n hostChooser?: boolean\n /**\n * Render-only HTML whose archive-relative dependencies have already been rewritten. Source/copy continue to\n * use `file.content` (or fetched text), so opening an HTML entry inside a ZIP still shows its original source.\n */\n htmlPreview?: string\n }>(),\n { mode: 'render', hostChooser: false },\n)\nconst emit = defineEmits<{\n loaded: [text: string]\n formatchange: [file: ViewerFile]\n formatpick: [key: string]\n}>()\n\n// Archives import this body; lazy-import them back so the module cycle never evaluates eagerly.\nconst ZipBrowser = defineAsyncComponent(() => import('@/components/ZipBrowser.vue'))\nconst SevenZipBrowser = defineAsyncComponent(() => import('@/components/SevenZipBrowser.vue'))\nconst ArchiveBrowser = defineAsyncComponent(() => import('@/components/ArchiveBrowser.vue'))\nconst BinObjBrowser = defineAsyncComponent(() => import('@/components/BinObjBrowser.vue'))\nconst EplViewer = defineAsyncComponent(() => import('@/components/EplViewer.vue'))\nconst ClassViewer = defineAsyncComponent(() => import('@/components/ClassViewer.vue'))\nconst HprofViewer = defineAsyncComponent(() => import('@/components/HprofViewer.vue'))\nconst MsiViewer = defineAsyncComponent(() => import('@/components/MsiViewer.vue'))\nconst AppxViewer = defineAsyncComponent(() => import('@/components/AppxViewer.vue'))\n// Heavy viewers whose engine is a STATIC optional peer dep: wrap so a missing dep shows an install card.\nconst DicomViewer = optionalViewer(\n () => import('@/components/DicomViewer.vue'),\n 'dicom-parser',\n 'DICOM 影像',\n)\nconst DocxViewer = optionalViewer(\n () => import('@/components/DocxViewer.vue'),\n 'docx-preview',\n 'Word 文档',\n)\nconst XlsxViewer = optionalViewer(\n () => import('@/components/XlsxViewer.vue'),\n ['exceljs', 'numfmt'],\n 'Excel 表格',\n)\nconst PptxViewer = optionalViewer(\n () => import('@/components/PptxViewer.vue'),\n ['@aiden0z/pptx-renderer', 'pdfjs-dist'],\n 'PowerPoint',\n)\nconst RtfViewer = defineAsyncComponent(() => import('@/components/RtfViewer.vue'))\nconst PdfViewer = optionalViewer(() => import('@/components/PdfViewer.vue'), 'pdfjs-dist', 'PDF')\nconst MindMapView = defineAsyncComponent(() => import('@/components/MindMapView.vue'))\nconst XmindViewer = defineAsyncComponent(() => import('@/components/XmindViewer.vue'))\nconst EmlViewer = defineAsyncComponent(() => import('@/components/EmlViewer.vue'))\nconst MhtViewer = defineAsyncComponent(() => import('@/components/MhtViewer.vue'))\nconst HarViewer = defineAsyncComponent(() => import('@/components/HarViewer.vue'))\nconst SqliteViewer = optionalViewer(\n () => import('@/components/SqliteViewer.vue'),\n 'sql.js',\n 'SQLite 数据库',\n)\nconst SubtitleViewer = defineAsyncComponent(() => import('@/components/SubtitleViewer.vue'))\nconst FontViewer = defineAsyncComponent(() => import('@/components/FontViewer.vue'))\nconst NotebookViewer = defineAsyncComponent(() => import('@/components/NotebookViewer.vue'))\nconst GeoViewer = defineAsyncComponent(() => import('@/components/GeoViewer.vue'))\nconst CsvViewer = defineAsyncComponent(() => import('@/components/CsvViewer.vue'))\nconst JsonlViewer = defineAsyncComponent(() => import('@/components/JsonlViewer.vue'))\nconst CertViewer = defineAsyncComponent(() => import('@/components/CertViewer.vue'))\nconst JksViewer = defineAsyncComponent(() => import('@/components/JksViewer.vue'))\nconst AudioViewer = defineAsyncComponent(() => import('@/components/AudioViewer.vue'))\nconst QmcAudioViewer = defineAsyncComponent(() => import('@/components/QmcAudioViewer.vue'))\nconst LrcViewer = defineAsyncComponent(() => import('@/components/LrcViewer.vue'))\nconst LogViewer = defineAsyncComponent(() => import('@/components/LogViewer.vue'))\nconst ModelViewer = optionalViewer(() => import('@/components/ModelViewer.vue'), 'three', '3D 模型')\nconst RasterViewer = defineAsyncComponent(() => import('@/components/RasterViewer.vue'))\nconst PsdViewer = defineAsyncComponent(() => import('@/components/PsdViewer.vue'))\nconst EbookViewer = defineAsyncComponent(() => import('@/components/EbookViewer.vue'))\nconst DrawioViewer = defineAsyncComponent(() => import('@/components/DrawioViewer.vue'))\nconst DxfViewer = defineAsyncComponent(() => import('@/components/DxfViewer.vue'))\nconst HexViewer = defineAsyncComponent(() => import('@/components/HexViewer.vue'))\n\n// ── \"open with\" override ──────────────────────────────────────────────────────────────────────\n// A user-chosen formatter ({@link VIEWER_FORMATTERS}). When set it replaces props.file as the EFFECTIVE file\n// the dispatch (and load) use; cleared whenever a different file is opened. Driven by the host header menu\n// (via the exposed pickFormat/resetFormat) and/or the in-body binary-card chips — both funnel through `pick`.\nconst pickedKey = ref('')\nconst picked = ref<ViewerFile | null>(null)\n// The effective file: the user's chosen formatter, else the file passed in.\nconst vf = computed<ViewerFile>(() => picked.value ?? props.file)\n\nfunction pick(f: ViewerFormatter): void {\n pickedKey.value = f.key\n picked.value = applyFormatter(props.file, f)\n}\n// Host-driven entry points (FileViewerPanel's header menu): pick by key, or fall back to auto-detection.\nfunction pickFormat(key: string): void {\n const f = findFormatter(key)\n if (f) pick(f)\n}\nfunction resetFormat(): void {\n pickedKey.value = ''\n picked.value = null\n}\n// Announce the active formatter so the host header can highlight it (fires on pick, reset, and file change).\nwatch(pickedKey, (k) => emit('formatpick', k))\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst text = ref('')\nconst errorMsg = ref('')\n\n// Mind map only renders the diagram once the loaded text actually looks like one (a non-mind-map `.mm` reads\n// as plain source). Same content sniff the panel used.\nconst isMindMap = computed(\n () =>\n vf.value.mindmap === true &&\n status.value === 'ready' &&\n looksLikeMindMap(text.value, fileExt(vf.value.name)),\n)\n\n// Markdown render: lift any leading YAML frontmatter into a card; the source view still shows the raw file.\nconst frontmatter = computed(() =>\n vf.value.lang === 'markdown' ? parseFrontmatter(text.value) : { data: {}, body: text.value },\n)\nconst hasFrontmatter = computed(() => Object.keys(frontmatter.value.data).length > 0)\n\n// Drop a single trailing newline so the source view doesn't show a phantom last line.\nconst sourceText = computed(() => text.value.replace(/\\n$/, ''))\n\n// SVG render: rasterize the markup via a data-URL <img> (an <img> never runs SVG scripts).\nconst svgSrc = computed(() =>\n text.value ? `data:image/svg+xml,${encodeURIComponent(text.value)}` : '',\n)\n\n// HTML preview, two modes (default = inert):\n// - INERT (default): the iframe carries an empty `sandbox` (no `allow-scripts`) plus an injected\n// `script-src 'none'` CSP, so NO upstream script runs — only layout/CSS/images render.\n// - INTERACTIVE (opt-in via the toolbar): scripts run inside a HARDENED sandbox — `allow-scripts` WITHOUT\n// `allow-same-origin` gives the frame an opaque origin (no access to our cookies/storage/session/DOM); that\n// opaque origin is the REAL isolation. The CSP allows external sub-resources AND network so CDN-based pages\n// fully work — scripts/styles/fonts/images from unpkg/jsdelivr/Google Fonts, plus fetch/XHR/WebSocket and\n// source-map loads (`connect-src` open to https/http/ws/wss). Only the structural hardening stays:\n// `object-src 'none'` (no plugins) and `base-uri 'none'` (no <base> hijack); forms are already blocked by\n// the sandbox (no `allow-forms`). Because the origin is opaque, even with network open the page can't reach\n// our session/cookies — it acts only with its own (already-visible) content. Review the source first\n// (render/source toggle), then enable. The ⛶ button opens this SAME sandboxed frame as a full-screen\n// in-app overlay (still an iframe → still opaque-origin), so a big interactive view stays safe — there's\n// no top-level / new-tab path that could ever run scripts in our origin.\nconst INERT_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'\">`\nconst INTERACTIVE_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"default-src 'unsafe-inline' 'unsafe-eval' data: blob: https: http:; ` +\n `connect-src data: blob: https: http: ws: wss:; object-src 'none'; base-uri 'none'\">`\n\nfunction injectCsp(html: string, csp: string): string {\n const head = html.match(/<head[^>]*>/i)\n if (head) {\n const at = (head.index ?? 0) + head[0].length\n return html.slice(0, at) + csp + html.slice(at)\n }\n return `<!doctype html><html><head><meta charset=\"utf-8\">${csp}</head><body>${html}</body></html>`\n}\n\nconst inertBlobUrl = ref('') // the inert (non-interactive) iframe + full-screen overlay\nconst interactiveBlobUrl = ref('') // built only when the user opts in\nconst interactive = ref(false)\nconst fullscreen = ref(false) // in-app full-screen overlay (Teleport to body) over the SAME sandboxed frame\nconst frameBlobUrl = computed(() =>\n interactive.value ? interactiveBlobUrl.value : inertBlobUrl.value,\n)\nconst previewHtml = computed(() => props.htmlPreview ?? text.value)\n\nfunction revokeHtmlBlobs(): void {\n if (inertBlobUrl.value) {\n URL.revokeObjectURL(inertBlobUrl.value)\n inertBlobUrl.value = ''\n }\n if (interactiveBlobUrl.value) {\n URL.revokeObjectURL(interactiveBlobUrl.value)\n interactiveBlobUrl.value = ''\n }\n}\n\nwatch([() => vf.value.html, previewHtml], ([html, preview]) => {\n revokeHtmlBlobs()\n interactive.value = false // a new file / changed text always returns to the safe inert default\n fullscreen.value = false // …and closes any open full-screen overlay\n if (html && typeof URL !== 'undefined' && URL.createObjectURL) {\n inertBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(preview, INERT_CSP)], { type: 'text/html' }),\n )\n }\n})\n\n// Toggle the sandboxed iframe between inert and interactive (builds the interactive blob lazily on first use).\nfunction toggleInteractive(): void {\n if (interactive.value) {\n interactive.value = false\n return\n }\n if (typeof URL !== 'undefined' && URL.createObjectURL && !interactiveBlobUrl.value) {\n interactiveBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(previewHtml.value, INTERACTIVE_CSP)], { type: 'text/html' }),\n )\n }\n interactive.value = true\n}\n\n// Full-screen overlay key handling + body scroll lock — only while open, SSR-guarded (no window in smoke).\nfunction onFsKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') fullscreen.value = false\n}\nwatch(fullscreen, (on) => {\n if (typeof window === 'undefined') return\n if (on) {\n window.addEventListener('keydown', onFsKey)\n document.body.style.overflow = 'hidden'\n } else {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\nasync function load(noStore = false): Promise<void> {\n const f = vf.value\n // Self-fetching sub-viewers + image/video/binary render straight from the URL — no text to load.\n if (skipsTextLoad(f)) {\n text.value = ''\n status.value = 'ready'\n return\n }\n if (f.content != null) {\n text.value = f.content\n status.value = 'ready'\n emit('loaded', f.content)\n return\n }\n if (!f.url) {\n text.value = ''\n status.value = 'ready'\n return\n }\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(f.url, noStore ? { cache: 'no-store' } : {})\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const t = await res.text()\n text.value = t\n status.value = 'ready'\n emit('loaded', t)\n } catch (e) {\n errorMsg.value = String(e)\n status.value = 'error'\n }\n}\n\n// A new file resets any \"open with\" override (registered BEFORE the vf watcher so picked is cleared before the\n// effective file is re-read below).\nwatch(\n () => props.file,\n () => {\n picked.value = null\n pickedKey.value = ''\n },\n)\n// Load + announce the effective file whenever it changes (a new file, or a picked formatter).\nwatch(\n vf,\n (v) => {\n emit('formatchange', v)\n void load()\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => {\n revokeHtmlBlobs()\n if (typeof window !== 'undefined') {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\n// The host's refresh button re-fetches (cache-busting) through here; its header menu drives the \"open with\"\n// override through pickFormat / resetFormat (see the override block above).\ndefineExpose({ reload: () => load(true), pickFormat, resetFormat })\n</script>\n\n<template>\n <div class=\"fvb\">\n <!-- forced-format switcher: shown once the user picks a formatter, so they can swap. Hidden when the host\n provides the chooser in its header (FileViewerPanel) — only the archive browsers render it in-body. -->\n <div v-if=\"picked && !hostChooser\" class=\"fvb__fmtbar\">\n <span class=\"fvb__fmtbar-label\">格式</span>\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n :class=\"{ 'fvb__fmtchip--on': pickedKey === f.key }\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n\n <!-- zip / 7z / tar-rar: the archive browsers fetch the bytes, decompress in-browser, and browse entries -->\n <ZipBrowser v-if=\"vf.zip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <SevenZipBrowser v-else-if=\"vf.sevenZip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <ArchiveBrowser v-else-if=\"vf.archive\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :kind=\"vf.archive\" />\n\n <!-- native binary object (.so/.dll): parse + list symbols -->\n <BinObjBrowser v-else-if=\"vf.binobj\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- e-language (.e/.ec): parse the binary section format + decompile each method to 易语言 source -->\n <EplViewer v-else-if=\"vf.epl\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- java bytecode (.class): parse the structural tables + list fields/methods (no decompilation) -->\n <ClassViewer v-else-if=\"vf.javaClass\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- jvm heap dump (.hprof): stream-parse → class histogram + per-class object drill-in (paginated/filterable) -->\n <HprofViewer v-else-if=\"vf.hprof\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows installer (.msi): parse the OLE compound file → product identity + summary info -->\n <MsiViewer v-else-if=\"vf.msi\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows app package (.appx/.msix): unzip the manifest → identity / deps / capabilities -->\n <AppxViewer v-else-if=\"vf.appx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- xmind workbook (.xmind): unzip content.json/xml → render as an interactive mind map -->\n <XmindViewer v-else-if=\"vf.xmind\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- dxf (.dxf): parse the ASCII CAD drawing → SVG with pan/zoom + layer toggles -->\n <DxfViewer v-else-if=\"vf.dxf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <DicomViewer v-else-if=\"vf.dicom\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <DocxViewer v-else-if=\"vf.docx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <XlsxViewer v-else-if=\"vf.xlsx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PptxViewer v-else-if=\"vf.pptx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <RtfViewer v-else-if=\"vf.rtf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PdfViewer v-else-if=\"vf.pdf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <EmlViewer v-else-if=\"vf.eml\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <MhtViewer v-else-if=\"vf.mht\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <HarViewer v-else-if=\"vf.har\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <SqliteViewer v-else-if=\"vf.sqlite\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <CertViewer v-else-if=\"vf.cert\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- java keystore (.jks/.keystore/.jceks): parse the inventory + certs; password decrypts private keys -->\n <JksViewer v-else-if=\"vf.jks\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <FontViewer v-else-if=\"vf.font\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <NotebookViewer v-else-if=\"vf.ipynb\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <AudioViewer v-else-if=\"vf.audio\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- QQ Music encrypted audio (.mflac/.mgg): decrypt in-browser → play + download decrypted -->\n <QmcAudioViewer v-else-if=\"vf.qmc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- LRC lyrics (.lrc): self-fetch + sniff GBK → synced lyric list (own render/source toggle) -->\n <LrcViewer v-else-if=\"vf.lrc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <LogViewer v-else-if=\"vf.log\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <ModelViewer v-else-if=\"vf.model\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- PSD: dedicated viewer with a live layer tree (toggle visibility → re-composite); other raster kinds\n (TIFF/HEIC/RAW) use the generic decoding viewer. -->\n <PsdViewer\n v-else-if=\"vf.raster === 'psd'\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n />\n <RasterViewer\n v-else-if=\"vf.raster\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.raster\"\n />\n <EbookViewer\n v-else-if=\"vf.ebook\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.ebook\"\n />\n\n <!-- hex dump: offset + raw bytes + ASCII (the universal \"open with\" fall-back; never auto-detected) -->\n <HexViewer v-else-if=\"vf.hex\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- image (archive entry, or forced via \"open with\"): a zoomable inline picture -->\n <div v-else-if=\"vf.image\" class=\"fvb__imagezoom\">\n <ZoomImage :src=\"vf.url ?? ''\" :alt=\"vf.name\" />\n </div>\n\n <!-- video (archive entry): native player from the blob URL -->\n <div v-else-if=\"vf.video\" class=\"fvb__video\">\n <video class=\"fvb__videoel\" :src=\"vf.url ?? ''\" controls preload=\"metadata\" />\n </div>\n\n <!-- unpreviewable binary (archive entry / unmatched file): download card + \"open with\" formatter chips -->\n <div v-else-if=\"vf.binary\" class=\"fvb__binary\">\n <Icon name=\"archive\" :size=\"34\" />\n <p>无法预览的二进制文件</p>\n <p v-if=\"vf.size\" class=\"fvb__binary-sub\">{{ formatBytes(vf.size) }}</p>\n <a class=\"fvb__dlbtn\" :href=\"vf.url\" :download=\"vf.name\">下载</a>\n <div class=\"fvb__openas\">\n <span class=\"fvb__openas-label\">用查看器打开:</span>\n <div class=\"fvb__openas-grid\">\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n </div>\n </div>\n\n <p v-else-if=\"status === 'loading'\" class=\"fvb__msg\">加载文件中…</p>\n <div v-else-if=\"status === 'error'\" class=\"fvb__msg fvb__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"fvb__retry\" @click=\"load(true)\">重试</button>\n </div>\n\n <!-- markdown render: frontmatter card (if any) + rendered document -->\n <div\n v-else-if=\"vf.lang === 'markdown' && mode === 'render'\"\n class=\"fvb__scroll fvb__scroll--doc\"\n >\n <div class=\"fvb__markdown\">\n <FrontmatterCard v-if=\"hasFrontmatter\" :data=\"frontmatter.data\" />\n <MarkdownText :text=\"frontmatter.body\" kp=\"fv\" />\n </div>\n </div>\n\n <!-- html render: sandboxed iframe fed by a blob. Default INERT (no scripts); the ▶ button opts into a\n HARDENED interactive sandbox (allow-scripts, opaque origin, CDN + network OK), and ⛶ opens the same\n sandboxed frame as a full-screen in-app overlay (no top-level/new-tab path). -->\n <div v-else-if=\"vf.html && mode === 'render'\" class=\"fvb__html\">\n <div class=\"fvb__htmlbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行:可加载外部 CDN 并发起网络请求(fetch/XHR/WebSocket),但无法访问本站会话 / cookie(不透明源隔离)'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,但无法访问本站会话 / cookie;建议先切到源码视图审查再启用)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"全屏预览(在应用内放大查看;可在全屏内开启交互脚本)\"\n @click=\"fullscreen = true\"\n >\n <Icon name=\"maximize\" :size=\"13\" />\n </button>\n </div>\n <!-- :key forces a full remount when toggling interactive: a `sandbox` change on an existing iframe does\n NOT re-apply to the already-loaded document, so the new doc must load on a fresh element that already\n carries `allow-scripts` (set before the element is inserted, i.e. before navigation commits). -->\n <iframe\n :key=\"interactive ? 'interactive' : 'inert'\"\n class=\"fvb__frame\"\n :src=\"frameBlobUrl\"\n title=\"HTML 预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n loading=\"lazy\"\n referrerpolicy=\"no-referrer\"\n />\n\n <!-- In-app full-screen overlay: the SAME sandboxed frame at viewport size (an iframe → opaque origin,\n never a top-level same-origin document). Esc / ✕ close it; the toolbar keeps the ▶ interactive\n toggle so scripts can be enabled while full-screen. Teleported to <body> to clear stacking contexts. -->\n <Teleport to=\"body\">\n <div v-if=\"fullscreen\" class=\"fvb__fs\">\n <div class=\"fvb__fsbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行,可联网但访问不到本站会话 / cookie'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,访问不到本站会话 / cookie)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"15\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"关闭全屏 (Esc)\"\n @click=\"fullscreen = false\"\n >\n <Icon name=\"close\" :size=\"15\" :stroke-width=\"2.2\" />\n </button>\n </div>\n <iframe\n :key=\"interactive ? 'fs-interactive' : 'fs-inert'\"\n class=\"fvb__fsframe\"\n :src=\"frameBlobUrl\"\n title=\"HTML 全屏预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n referrerpolicy=\"no-referrer\"\n />\n </div>\n </Teleport>\n </div>\n\n <!-- svg render: the markup as a picture (data-URL <img>) -->\n <div v-else-if=\"vf.svg && mode === 'render'\" class=\"fvb__svg\">\n <img class=\"fvb__svgimg\" :src=\"svgSrc\" :alt=\"vf.name\" />\n </div>\n\n <!-- mind-map render: the OPML/FreeMind XML drawn as an interactive SVG tree -->\n <div v-else-if=\"isMindMap && mode === 'render'\" class=\"fvb__fill\">\n <MindMapView :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- subtitle render: .srt/.vtt/.ass parsed into a timecoded transcript -->\n <div v-else-if=\"vf.subtitle && mode === 'render'\" class=\"fvb__fill\">\n <SubtitleViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- geo render: .geojson/.kml/.gpx drawn as an offline SVG map -->\n <div v-else-if=\"vf.geo && mode === 'render'\" class=\"fvb__fill\">\n <GeoViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- csv render: .csv/.tsv parsed into a table -->\n <div v-else-if=\"vf.csv && mode === 'render'\" class=\"fvb__fill\">\n <CsvViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- jsonl render: .jsonl/.ndjson parsed into a record list (truncated rows, click to expand) -->\n <div v-else-if=\"vf.jsonl && mode === 'render'\" class=\"fvb__fill\">\n <JsonlViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- diagram render: .drawio/.excalidraw drawn as an SVG (source toggle shows the raw XML/JSON) -->\n <div v-else-if=\"vf.diagram && mode === 'render'\" class=\"fvb__fill\">\n <DrawioViewer :text=\"text\" :name=\"vf.name\" :kind=\"vf.diagram\" />\n </div>\n\n <!-- source: everything else, or markdown/html/svg/… viewed as source — highlighted + folding + format -->\n <CodeView v-else :code=\"sourceText\" :lang=\"vf.lang ?? ''\" :name=\"vf.name\" />\n </div>\n</template>\n\n<style scoped>\n/* Fills the area below the host header/bar; each branch is itself a flex child that grows. */\n.fvb {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* Forced-format switcher bar (shown when the user opened an unhandled file via the \"open with\" chooser). */\n.fvb__fmtbar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n flex-shrink: 0;\n padding: 7px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.fvb__fmtbar-label {\n margin-right: 2px;\n font-size: 11.5px;\n color: var(--faint);\n}\n/* Chip used both in the switcher bar and on the unpreviewable-binary card's \"open with\" row. */\n.fvb__fmtchip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 999px;\n padding: 5px 11px;\n cursor: pointer;\n transition:\n background 0.15s,\n border-color 0.15s,\n color 0.15s;\n}\n.fvb__fmtchip:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.fvb__fmtchip--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__fmtchip--on:hover {\n color: #fff;\n}\n\n.fvb__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.fvb__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n}\n.fvb__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.fvb__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.fvb__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overscroll-behavior: contain;\n background: var(--code-bg);\n}\n.fvb__scroll--doc {\n overflow: auto;\n}\n.fvb__markdown {\n padding: 18px 24px 80px;\n}\n\n/* HTML render: the iframe fills the area; a small overlay toolbar (top-right) holds the open-in-new-tab\n * button. White canvas so documents that don't set their own background read as a normal page. */\n.fvb__html {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n background: #fff;\n}\n.fvb__htmlbar {\n position: absolute;\n top: 8px;\n right: 8px;\n z-index: 2;\n display: flex;\n align-items: center;\n gap: 4px;\n}\n.fvb__htmlbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n text-decoration: none;\n cursor: pointer;\n box-shadow: var(--shadow);\n}\n.fvb__htmlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n/* Interactive scripts enabled: accent the toggle so the elevated (but sandboxed) state is obvious. */\n.fvb__htmlbtn--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__htmlbtn--on:hover {\n color: #fff;\n filter: brightness(1.06);\n}\n.fvb__frame {\n display: block;\n width: 100%;\n height: 100%;\n border: none;\n background: #fff;\n}\n\n/* In-app full-screen HTML overlay (Teleport to body) — same sandboxed frame at viewport size. */\n.fvb__fs {\n position: fixed;\n inset: 0;\n z-index: 2000;\n display: flex;\n flex-direction: column;\n background: rgba(0, 0, 0, 0.82);\n animation: fvb-fs-in 0.12s ease-out;\n}\n@keyframes fvb-fs-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n.fvb__fsbar {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 6px;\n padding: 8px 10px;\n}\n.fvb__fsframe {\n flex: 1 1 auto;\n width: 100%;\n min-height: 0;\n border: none;\n background: #fff;\n}\n\n/* Generic \"viewer owns the area below\" host (mind map / subtitle / geo / csv). */\n.fvb__fill {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n.fvb__svg {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: var(--surface-2);\n}\n.fvb__svgimg {\n max-width: 100%;\n height: auto;\n object-fit: contain;\n}\n\n.fvb__imagezoom {\n width: 100%;\n min-width: 0;\n flex: 1 1 auto;\n min-height: 0;\n position: relative;\n overflow: hidden;\n background: var(--code-bg);\n}\n\n.fvb__video {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: #000;\n}\n.fvb__videoel {\n max-width: 100%;\n max-height: 100%;\n}\n\n.fvb__binary {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 32px;\n color: var(--faint);\n text-align: center;\n}\n.fvb__binary p {\n margin: 0;\n font-size: 13px;\n color: var(--muted);\n}\n.fvb__binary-sub {\n font-size: 11.5px;\n color: var(--faint) !important;\n}\n.fvb__dlbtn {\n margin-top: 8px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 6px 16px;\n text-decoration: none;\n cursor: pointer;\n}\n.fvb__dlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* \"Open with\" formatter chooser on the unpreviewable-binary card. */\n.fvb__openas {\n margin-top: 18px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 9px;\n}\n.fvb__openas-label {\n font-size: 12px;\n color: var(--muted);\n}\n.fvb__openas-grid {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 7px;\n max-width: 320px;\n}\n</style>\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"FileViewerBody.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/FileViewerBody.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, onBeforeUnmount, ref, watch } from 'vue'\nimport { optionalViewer } from '@/components/optionalViewer'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { skipsTextLoad } from '@/lib/viewerKind'\nimport {\n VIEWER_FORMATTERS as FORMATTERS,\n applyFormatter,\n findFormatter,\n type ViewerFormatter,\n} from '@/lib/viewerFormats'\nimport { fileExt } from '@/lib/fileType'\nimport { looksLikeMindMap } from '@/lib/mindmap'\nimport { parseFrontmatter } from '@/lib/frontmatter'\nimport { formatBytes } from '@/lib/format'\nimport CodeView from '@/components/CodeView.vue'\nimport MarkdownText from '@/components/MarkdownText'\nimport FrontmatterCard from '@/components/FrontmatterCard.vue'\nimport ZoomImage from '@/components/ZoomImage.vue'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's CONTENT area — the type dispatch shared by {@link FileViewerPanel} (a top-level file) and\n * the archive browsers (a file inside a zip/7z/tar/rar, extracted to a blob URL). Given a {@link ViewerFile}\n * it renders the right thing: a self-fetching sub-viewer (docx/pdf/dicom/…), an inline image/video, or — for\n * the text-backed kinds — markdown / html / svg / mind-map / subtitle / geo / csv (rendered), or highlighted,\n * foldable source. It fetches its own text for the text-backed kinds and emits it via `loaded` so the host's\n * copy button can use it. The host owns the header/bar, the render↔source `mode`, and copy/download.\n *\n * Extracted from FileViewerPanel so an archive entry previews with the EXACT same viewers as a top-level file\n * (the old archive detail view only did text/image/office/dicom). The archive browsers import this lazily to\n * break the import cycle (this component lazy-imports them right back for nested archives).\n *\n * \"Open with\" override: ANY file can be force-opened as a different formatter (hex / text / markdown / html /\n * svg / image / audio / video — {@link VIEWER_FORMATTERS}), since auto-detection by extension/mime can be\n * wrong. The choice becomes an EFFECTIVE file ({@link vf}) that re-runs the dispatch. Two entry points drive\n * the SAME override: (1) the host's header menu (FileViewerPanel) calls the exposed `pickFormat`/`resetFormat`\n * — the primary, universal entry; (2) the unpreviewable-binary card's own chip row, for the archive-browser\n * context where there is no host header. The effective file is emitted via `formatchange` (so the host tracks\n * toggle/copy visibility) and the active key via `formatpick` (so the host menu highlights it). `hostChooser`\n * hides the in-body switcher bar when the host already shows the chooser in its header (no duplicate UI).\n */\nconst props = withDefaults(\n defineProps<{\n file: ViewerFile\n mode?: 'render' | 'source'\n hostChooser?: boolean\n /**\n * Render-only HTML whose archive-relative dependencies have already been rewritten. Source/copy continue to\n * use `file.content` (or fetched text), so opening an HTML entry inside a ZIP still shows its original source.\n */\n htmlPreview?: string\n }>(),\n { mode: 'render', hostChooser: false },\n)\nconst emit = defineEmits<{\n loaded: [text: string]\n formatchange: [file: ViewerFile]\n formatpick: [key: string]\n}>()\n\n// Archives import this body; lazy-import them back so the module cycle never evaluates eagerly.\nconst ZipBrowser = defineAsyncComponent(() => import('@/components/ZipBrowser.vue'))\nconst SevenZipBrowser = defineAsyncComponent(() => import('@/components/SevenZipBrowser.vue'))\nconst ArchiveBrowser = defineAsyncComponent(() => import('@/components/ArchiveBrowser.vue'))\nconst BinObjBrowser = defineAsyncComponent(() => import('@/components/BinObjBrowser.vue'))\nconst EplViewer = defineAsyncComponent(() => import('@/components/EplViewer.vue'))\nconst ClassViewer = defineAsyncComponent(() => import('@/components/ClassViewer.vue'))\nconst HprofViewer = defineAsyncComponent(() => import('@/components/HprofViewer.vue'))\nconst MsiViewer = defineAsyncComponent(() => import('@/components/MsiViewer.vue'))\nconst AppxViewer = defineAsyncComponent(() => import('@/components/AppxViewer.vue'))\n// Heavy viewers whose engine is a STATIC optional peer dep: wrap so a missing dep shows an install card.\nconst DicomViewer = optionalViewer(\n () => import('@/components/DicomViewer.vue'),\n 'dicom-parser',\n 'DICOM 影像',\n)\nconst DocxViewer = optionalViewer(\n () => import('@/components/DocxViewer.vue'),\n 'docx-preview',\n 'Word 文档',\n)\nconst XlsxViewer = optionalViewer(\n () => import('@/components/XlsxViewer.vue'),\n ['exceljs', 'numfmt'],\n 'Excel 表格',\n)\nconst PptxViewer = optionalViewer(\n () => import('@/components/PptxViewer.vue'),\n ['@aiden0z/pptx-renderer', 'pdfjs-dist'],\n 'PowerPoint',\n)\nconst RtfViewer = defineAsyncComponent(() => import('@/components/RtfViewer.vue'))\nconst PdfViewer = optionalViewer(() => import('@/components/PdfViewer.vue'), 'pdfjs-dist', 'PDF')\nconst MindMapView = defineAsyncComponent(() => import('@/components/MindMapView.vue'))\nconst XmindViewer = defineAsyncComponent(() => import('@/components/XmindViewer.vue'))\nconst EmlViewer = defineAsyncComponent(() => import('@/components/EmlViewer.vue'))\nconst MhtViewer = defineAsyncComponent(() => import('@/components/MhtViewer.vue'))\nconst HarViewer = defineAsyncComponent(() => import('@/components/HarViewer.vue'))\nconst SqliteViewer = optionalViewer(\n () => import('@/components/SqliteViewer.vue'),\n 'sql.js',\n 'SQLite 数据库',\n)\nconst SubtitleViewer = defineAsyncComponent(() => import('@/components/SubtitleViewer.vue'))\nconst FontViewer = defineAsyncComponent(() => import('@/components/FontViewer.vue'))\nconst NotebookViewer = defineAsyncComponent(() => import('@/components/NotebookViewer.vue'))\nconst GeoViewer = defineAsyncComponent(() => import('@/components/GeoViewer.vue'))\nconst CsvViewer = defineAsyncComponent(() => import('@/components/CsvViewer.vue'))\nconst JsonlViewer = defineAsyncComponent(() => import('@/components/JsonlViewer.vue'))\nconst CertViewer = defineAsyncComponent(() => import('@/components/CertViewer.vue'))\nconst JksViewer = defineAsyncComponent(() => import('@/components/JksViewer.vue'))\nconst AudioViewer = defineAsyncComponent(() => import('@/components/AudioViewer.vue'))\nconst QmcAudioViewer = defineAsyncComponent(() => import('@/components/QmcAudioViewer.vue'))\nconst LrcViewer = defineAsyncComponent(() => import('@/components/LrcViewer.vue'))\nconst LogViewer = defineAsyncComponent(() => import('@/components/LogViewer.vue'))\nconst ModelViewer = optionalViewer(() => import('@/components/ModelViewer.vue'), 'three', '3D 模型')\nconst RasterViewer = defineAsyncComponent(() => import('@/components/RasterViewer.vue'))\nconst PsdViewer = defineAsyncComponent(() => import('@/components/PsdViewer.vue'))\nconst EbookViewer = defineAsyncComponent(() => import('@/components/EbookViewer.vue'))\nconst DrawioViewer = defineAsyncComponent(() => import('@/components/DrawioViewer.vue'))\nconst DxfViewer = defineAsyncComponent(() => import('@/components/DxfViewer.vue'))\nconst HexViewer = defineAsyncComponent(() => import('@/components/HexViewer.vue'))\n\n// ── \"open with\" override ──────────────────────────────────────────────────────────────────────\n// A user-chosen formatter ({@link VIEWER_FORMATTERS}). When set it replaces props.file as the EFFECTIVE file\n// the dispatch (and load) use; cleared whenever a different file is opened. Driven by the host header menu\n// (via the exposed pickFormat/resetFormat) and/or the in-body binary-card chips — both funnel through `pick`.\nconst pickedKey = ref('')\nconst picked = ref<ViewerFile | null>(null)\n// The effective file: the user's chosen formatter, else the file passed in.\nconst vf = computed<ViewerFile>(() => picked.value ?? props.file)\n\nfunction pick(f: ViewerFormatter): void {\n pickedKey.value = f.key\n picked.value = applyFormatter(props.file, f)\n}\n// Host-driven entry points (FileViewerPanel's header menu): pick by key, or fall back to auto-detection.\nfunction pickFormat(key: string): void {\n const f = findFormatter(key)\n if (f) pick(f)\n}\nfunction resetFormat(): void {\n pickedKey.value = ''\n picked.value = null\n}\n// Announce the active formatter so the host header can highlight it (fires on pick, reset, and file change).\nwatch(pickedKey, (k) => emit('formatpick', k))\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst text = ref('')\nconst errorMsg = ref('')\n\n// Mind map only renders the diagram once the loaded text actually looks like one (a non-mind-map `.mm` reads\n// as plain source). Same content sniff the panel used.\nconst isMindMap = computed(\n () =>\n vf.value.mindmap === true &&\n status.value === 'ready' &&\n looksLikeMindMap(text.value, fileExt(vf.value.name)),\n)\n\n// Markdown render: lift any leading YAML frontmatter into a card; the source view still shows the raw file.\nconst frontmatter = computed(() =>\n vf.value.lang === 'markdown' ? parseFrontmatter(text.value) : { data: {}, body: text.value },\n)\nconst hasFrontmatter = computed(() => Object.keys(frontmatter.value.data).length > 0)\n\n// Drop a single trailing newline so the source view doesn't show a phantom last line.\nconst sourceText = computed(() => text.value.replace(/\\n$/, ''))\n\n// SVG render: rasterize the markup via a data-URL <img> (an <img> never runs SVG scripts).\nconst svgSrc = computed(() =>\n text.value ? `data:image/svg+xml,${encodeURIComponent(text.value)}` : '',\n)\n\n// HTML preview, two modes (default = inert):\n// - INERT (default): the iframe carries an empty `sandbox` (no `allow-scripts`) plus an injected\n// `script-src 'none'` CSP, so NO upstream script runs — only layout/CSS/images render.\n// - INTERACTIVE (opt-in via the toolbar): scripts run inside a HARDENED sandbox — `allow-scripts` WITHOUT\n// `allow-same-origin` gives the frame an opaque origin (no access to our cookies/storage/session/DOM); that\n// opaque origin is the REAL isolation. The CSP allows external sub-resources AND network so CDN-based pages\n// fully work — scripts/styles/fonts/images from unpkg/jsdelivr/Google Fonts, plus fetch/XHR/WebSocket and\n// source-map loads (`connect-src` open to https/http/ws/wss). Only the structural hardening stays:\n// `object-src 'none'` (no plugins) and `base-uri 'none'` (no <base> hijack); forms are already blocked by\n// the sandbox (no `allow-forms`). Because the origin is opaque, even with network open the page can't reach\n// our session/cookies — it acts only with its own (already-visible) content. Review the source first\n// (render/source toggle), then enable. The ⛶ button opens this SAME sandboxed frame as a full-screen\n// in-app overlay (still an iframe → still opaque-origin), so a big interactive view stays safe — there's\n// no top-level / new-tab path that could ever run scripts in our origin.\nconst INERT_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'\">`\nconst INTERACTIVE_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"default-src 'unsafe-inline' 'unsafe-eval' data: blob: https: http:; ` +\n `connect-src data: blob: https: http: ws: wss:; object-src 'none'; base-uri 'none'\">`\n\nfunction injectCsp(html: string, csp: string): string {\n const head = html.match(/<head[^>]*>/i)\n if (head) {\n const at = (head.index ?? 0) + head[0].length\n return html.slice(0, at) + csp + html.slice(at)\n }\n return `<!doctype html><html><head><meta charset=\"utf-8\">${csp}</head><body>${html}</body></html>`\n}\n\nconst inertBlobUrl = ref('') // the inert (non-interactive) iframe + full-screen overlay\nconst interactiveBlobUrl = ref('') // built only when the user opts in\nconst interactive = ref(false)\nconst fullscreen = ref(false) // in-app full-screen overlay (Teleport to body) over the SAME sandboxed frame\nconst frameBlobUrl = computed(() =>\n interactive.value ? interactiveBlobUrl.value : inertBlobUrl.value,\n)\nconst previewHtml = computed(() => props.htmlPreview ?? text.value)\n\nfunction revokeHtmlBlobs(): void {\n if (inertBlobUrl.value) {\n URL.revokeObjectURL(inertBlobUrl.value)\n inertBlobUrl.value = ''\n }\n if (interactiveBlobUrl.value) {\n URL.revokeObjectURL(interactiveBlobUrl.value)\n interactiveBlobUrl.value = ''\n }\n}\n\nwatch([() => vf.value.html, previewHtml], ([html, preview]) => {\n revokeHtmlBlobs()\n interactive.value = false // a new file / changed text always returns to the safe inert default\n fullscreen.value = false // …and closes any open full-screen overlay\n if (html && typeof URL !== 'undefined' && URL.createObjectURL) {\n inertBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(preview, INERT_CSP)], { type: 'text/html' }),\n )\n }\n})\n\n// Toggle the sandboxed iframe between inert and interactive (builds the interactive blob lazily on first use).\nfunction toggleInteractive(): void {\n if (interactive.value) {\n interactive.value = false\n return\n }\n if (typeof URL !== 'undefined' && URL.createObjectURL && !interactiveBlobUrl.value) {\n interactiveBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(previewHtml.value, INTERACTIVE_CSP)], { type: 'text/html' }),\n )\n }\n interactive.value = true\n}\n\n// Full-screen overlay key handling + body scroll lock — only while open, SSR-guarded (no window in smoke).\nfunction onFsKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') fullscreen.value = false\n}\nwatch(fullscreen, (on) => {\n if (typeof window === 'undefined') return\n if (on) {\n window.addEventListener('keydown', onFsKey)\n document.body.style.overflow = 'hidden'\n } else {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\nasync function load(noStore = false): Promise<void> {\n const f = vf.value\n // Self-fetching sub-viewers + image/video/binary render straight from the URL — no text to load.\n if (skipsTextLoad(f)) {\n text.value = ''\n status.value = 'ready'\n return\n }\n if (f.content != null) {\n text.value = f.content\n status.value = 'ready'\n emit('loaded', f.content)\n return\n }\n if (!f.url) {\n text.value = ''\n status.value = 'ready'\n return\n }\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(f.url, noStore ? { cache: 'no-store' } : {})\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const t = await res.text()\n text.value = t\n status.value = 'ready'\n emit('loaded', t)\n } catch (e) {\n errorMsg.value = String(e)\n status.value = 'error'\n }\n}\n\n// A new file resets any \"open with\" override (registered BEFORE the vf watcher so picked is cleared before the\n// effective file is re-read below).\nwatch(\n () => props.file,\n () => {\n picked.value = null\n pickedKey.value = ''\n },\n)\n// Load + announce the effective file whenever it changes (a new file, or a picked formatter).\nwatch(\n vf,\n (v) => {\n emit('formatchange', v)\n void load()\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => {\n revokeHtmlBlobs()\n if (typeof window !== 'undefined') {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\n// The host's refresh button re-fetches (cache-busting) through here; its header menu drives the \"open with\"\n// override through pickFormat / resetFormat (see the override block above).\ndefineExpose({ reload: () => load(true), pickFormat, resetFormat })\n</script>\n\n<template>\n <div class=\"fvb\">\n <!-- forced-format switcher: shown once the user picks a formatter, so they can swap. Hidden when the host\n provides the chooser in its header (FileViewerPanel) — only the archive browsers render it in-body. -->\n <div v-if=\"picked && !hostChooser\" class=\"fvb__fmtbar\">\n <span class=\"fvb__fmtbar-label\">格式</span>\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n :class=\"{ 'fvb__fmtchip--on': pickedKey === f.key }\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n\n <!-- zip / 7z / tar-rar: the archive browsers fetch the bytes, decompress in-browser, and browse entries -->\n <ZipBrowser v-if=\"vf.zip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <SevenZipBrowser v-else-if=\"vf.sevenZip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <ArchiveBrowser v-else-if=\"vf.archive\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :kind=\"vf.archive\" />\n\n <!-- native binary object (.so/.dll): parse + list symbols -->\n <BinObjBrowser v-else-if=\"vf.binobj\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- e-language (.e/.ec): parse the binary section format + decompile each method to 易语言 source -->\n <EplViewer v-else-if=\"vf.epl\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- java bytecode (.class): parse the structural tables + list fields/methods (no decompilation) -->\n <ClassViewer v-else-if=\"vf.javaClass\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- jvm heap dump (.hprof): stream-parse → class histogram + per-class object drill-in (paginated/filterable) -->\n <HprofViewer v-else-if=\"vf.hprof\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows installer (.msi): parse the OLE compound file → product identity + summary info -->\n <MsiViewer v-else-if=\"vf.msi\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows app package (.appx/.msix): unzip the manifest → identity / deps / capabilities -->\n <AppxViewer v-else-if=\"vf.appx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- xmind workbook (.xmind): unzip content.json/xml → render as an interactive mind map -->\n <XmindViewer v-else-if=\"vf.xmind\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- dxf (.dxf): parse the ASCII CAD drawing → SVG with pan/zoom + layer toggles -->\n <DxfViewer v-else-if=\"vf.dxf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <DicomViewer v-else-if=\"vf.dicom\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <DocxViewer v-else-if=\"vf.docx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <XlsxViewer v-else-if=\"vf.xlsx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PptxViewer v-else-if=\"vf.pptx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <RtfViewer v-else-if=\"vf.rtf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PdfViewer v-else-if=\"vf.pdf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <EmlViewer v-else-if=\"vf.eml\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <MhtViewer v-else-if=\"vf.mht\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <HarViewer v-else-if=\"vf.har\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <SqliteViewer v-else-if=\"vf.sqlite\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <CertViewer v-else-if=\"vf.cert\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- java keystore (.jks/.keystore/.jceks): parse the inventory + certs; password decrypts private keys -->\n <JksViewer v-else-if=\"vf.jks\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <FontViewer v-else-if=\"vf.font\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <NotebookViewer v-else-if=\"vf.ipynb\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <AudioViewer v-else-if=\"vf.audio\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- QQ Music encrypted audio (.mflac/.mgg): decrypt in-browser → play + download decrypted -->\n <QmcAudioViewer v-else-if=\"vf.qmc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- LRC lyrics (.lrc): self-fetch + sniff GBK → synced lyric list (own render/source toggle) -->\n <LrcViewer v-else-if=\"vf.lrc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <LogViewer v-else-if=\"vf.log\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <ModelViewer v-else-if=\"vf.model\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- PSD: dedicated viewer with a live layer tree (toggle visibility → re-composite); other raster kinds\n (TIFF/HEIC/RAW) use the generic decoding viewer. -->\n <PsdViewer\n v-else-if=\"vf.raster === 'psd'\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n />\n <RasterViewer\n v-else-if=\"vf.raster\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.raster\"\n />\n <EbookViewer\n v-else-if=\"vf.ebook\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.ebook\"\n />\n\n <!-- hex dump: offset + raw bytes + ASCII (the universal \"open with\" fall-back; never auto-detected) -->\n <HexViewer v-else-if=\"vf.hex\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- image (archive entry, or forced via \"open with\"): a zoomable inline picture -->\n <div v-else-if=\"vf.image\" class=\"fvb__imagezoom\">\n <ZoomImage :src=\"vf.url ?? ''\" :alt=\"vf.name\" />\n </div>\n\n <!-- video (archive entry): native player from the blob URL -->\n <div v-else-if=\"vf.video\" class=\"fvb__video\">\n <video class=\"fvb__videoel\" :src=\"vf.url ?? ''\" controls preload=\"metadata\" />\n </div>\n\n <!-- unpreviewable binary (archive entry / unmatched file): download card + \"open with\" formatter chips -->\n <div v-else-if=\"vf.binary\" class=\"fvb__binary\">\n <Icon name=\"archive\" :size=\"34\" />\n <p>无法预览的二进制文件</p>\n <p v-if=\"vf.size\" class=\"fvb__binary-sub\">{{ formatBytes(vf.size) }}</p>\n <a class=\"fvb__dlbtn\" :href=\"vf.url\" :download=\"vf.name\">下载</a>\n <div class=\"fvb__openas\">\n <span class=\"fvb__openas-label\">用查看器打开:</span>\n <div class=\"fvb__openas-grid\">\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n </div>\n </div>\n\n <p v-else-if=\"status === 'loading'\" class=\"fvb__msg\">加载文件中…</p>\n <div v-else-if=\"status === 'error'\" class=\"fvb__msg fvb__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"fvb__retry\" @click=\"load(true)\">重试</button>\n </div>\n\n <!-- markdown render: frontmatter card (if any) + rendered document -->\n <div\n v-else-if=\"vf.lang === 'markdown' && mode === 'render'\"\n class=\"fvb__scroll fvb__scroll--doc\"\n >\n <div class=\"fvb__markdown\">\n <FrontmatterCard v-if=\"hasFrontmatter\" :data=\"frontmatter.data\" />\n <MarkdownText :text=\"frontmatter.body\" kp=\"fv\" />\n </div>\n </div>\n\n <!-- html render: sandboxed iframe fed by a blob. Default INERT (no scripts); the ▶ button opts into a\n HARDENED interactive sandbox (allow-scripts, opaque origin, CDN + network OK), and ⛶ opens the same\n sandboxed frame as a full-screen in-app overlay (no top-level/new-tab path). -->\n <div v-else-if=\"vf.html && mode === 'render'\" class=\"fvb__html\">\n <div class=\"fvb__htmlbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行:可加载外部 CDN 并发起网络请求(fetch/XHR/WebSocket),但无法访问本站会话 / cookie(不透明源隔离)'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,但无法访问本站会话 / cookie;建议先切到源码视图审查再启用)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"全屏预览(在应用内放大查看;可在全屏内开启交互脚本)\"\n @click=\"fullscreen = true\"\n >\n <Icon name=\"maximize\" :size=\"13\" />\n </button>\n </div>\n <!-- :key forces a full remount when toggling interactive: a `sandbox` change on an existing iframe does\n NOT re-apply to the already-loaded document, so the new doc must load on a fresh element that already\n carries `allow-scripts` (set before the element is inserted, i.e. before navigation commits). -->\n <iframe\n :key=\"interactive ? 'interactive' : 'inert'\"\n class=\"fvb__frame\"\n :src=\"frameBlobUrl\"\n title=\"HTML 预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n loading=\"lazy\"\n referrerpolicy=\"no-referrer\"\n />\n\n <!-- In-app full-screen overlay: the SAME sandboxed frame at viewport size (an iframe → opaque origin,\n never a top-level same-origin document). Esc / ✕ close it; the toolbar keeps the ▶ interactive\n toggle so scripts can be enabled while full-screen. Teleported to <body> to clear stacking contexts. -->\n <Teleport to=\"body\">\n <div v-if=\"fullscreen\" class=\"fvb__fs\">\n <div class=\"fvb__fsbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行,可联网但访问不到本站会话 / cookie'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,访问不到本站会话 / cookie)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"15\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"关闭全屏 (Esc)\"\n @click=\"fullscreen = false\"\n >\n <Icon name=\"close\" :size=\"15\" :stroke-width=\"2.2\" />\n </button>\n </div>\n <iframe\n :key=\"interactive ? 'fs-interactive' : 'fs-inert'\"\n class=\"fvb__fsframe\"\n :src=\"frameBlobUrl\"\n title=\"HTML 全屏预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n referrerpolicy=\"no-referrer\"\n />\n </div>\n </Teleport>\n </div>\n\n <!-- svg render: the markup as a picture (data-URL <img>) -->\n <div v-else-if=\"vf.svg && mode === 'render'\" class=\"fvb__svg\">\n <img class=\"fvb__svgimg\" :src=\"svgSrc\" :alt=\"vf.name\" />\n </div>\n\n <!-- mind-map render: the OPML/FreeMind XML drawn as an interactive SVG tree -->\n <div v-else-if=\"isMindMap && mode === 'render'\" class=\"fvb__fill\">\n <MindMapView :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- subtitle render: .srt/.vtt/.ass parsed into a timecoded transcript -->\n <div v-else-if=\"vf.subtitle && mode === 'render'\" class=\"fvb__fill\">\n <SubtitleViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- geo render: .geojson/.kml/.gpx drawn as an offline SVG map -->\n <div v-else-if=\"vf.geo && mode === 'render'\" class=\"fvb__fill\">\n <GeoViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- csv render: .csv/.tsv parsed into a table -->\n <div v-else-if=\"vf.csv && mode === 'render'\" class=\"fvb__fill\">\n <CsvViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- jsonl render: .jsonl/.ndjson parsed into a record list (truncated rows, click to expand) -->\n <div v-else-if=\"vf.jsonl && mode === 'render'\" class=\"fvb__fill\">\n <JsonlViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- diagram render: .drawio/.excalidraw drawn as an SVG (source toggle shows the raw XML/JSON) -->\n <div v-else-if=\"vf.diagram && mode === 'render'\" class=\"fvb__fill\">\n <DrawioViewer :text=\"text\" :name=\"vf.name\" :kind=\"vf.diagram\" />\n </div>\n\n <!-- source: everything else, or markdown/html/svg/… viewed as source — highlighted + folding + format -->\n <CodeView v-else :code=\"sourceText\" :lang=\"vf.lang ?? ''\" :name=\"vf.name\" />\n </div>\n</template>\n\n<style scoped>\n/* Fills the area below the host header/bar; each branch is itself a flex child that grows. */\n.fvb {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* Forced-format switcher bar (shown when the user opened an unhandled file via the \"open with\" chooser). */\n.fvb__fmtbar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n flex-shrink: 0;\n padding: 7px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.fvb__fmtbar-label {\n margin-right: 2px;\n font-size: 11.5px;\n color: var(--faint);\n}\n/* Chip used both in the switcher bar and on the unpreviewable-binary card's \"open with\" row. */\n.fvb__fmtchip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 999px;\n padding: 5px 11px;\n cursor: pointer;\n transition:\n background 0.15s,\n border-color 0.15s,\n color 0.15s;\n}\n.fvb__fmtchip:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.fvb__fmtchip--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__fmtchip--on:hover {\n color: #fff;\n}\n\n.fvb__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.fvb__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n}\n.fvb__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.fvb__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.fvb__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overscroll-behavior: contain;\n background: var(--code-bg);\n}\n.fvb__scroll--doc {\n overflow: auto;\n}\n.fvb__markdown {\n padding: 18px 24px 80px;\n}\n\n/* HTML render: the iframe fills the area; a small overlay toolbar (top-right) holds the open-in-new-tab\n * button. White canvas so documents that don't set their own background read as a normal page. */\n.fvb__html {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n background: #fff;\n}\n.fvb__htmlbar {\n position: absolute;\n top: 8px;\n right: 8px;\n z-index: 2;\n display: flex;\n align-items: center;\n gap: 4px;\n}\n.fvb__htmlbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n text-decoration: none;\n cursor: pointer;\n box-shadow: var(--shadow);\n}\n.fvb__htmlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n/* Interactive scripts enabled: accent the toggle so the elevated (but sandboxed) state is obvious. */\n.fvb__htmlbtn--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__htmlbtn--on:hover {\n color: #fff;\n filter: brightness(1.06);\n}\n.fvb__frame {\n display: block;\n width: 100%;\n height: 100%;\n border: none;\n background: #fff;\n}\n\n/* In-app full-screen HTML overlay (Teleport to body) — same sandboxed frame at viewport size. */\n.fvb__fs {\n position: fixed;\n inset: 0;\n z-index: 2000;\n display: flex;\n flex-direction: column;\n background: rgba(0, 0, 0, 0.82);\n animation: fvb-fs-in 0.12s ease-out;\n}\n@keyframes fvb-fs-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n.fvb__fsbar {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 6px;\n padding: 8px 10px;\n}\n.fvb__fsframe {\n flex: 1 1 auto;\n width: 100%;\n min-height: 0;\n border: none;\n background: #fff;\n}\n\n/* Generic \"viewer owns the area below\" host (mind map / subtitle / geo / csv). */\n.fvb__fill {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n.fvb__svg {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: var(--surface-2);\n}\n.fvb__svgimg {\n max-width: 100%;\n height: auto;\n object-fit: contain;\n}\n\n.fvb__imagezoom {\n flex: 1 1 auto;\n min-height: 0;\n position: relative;\n background: var(--code-bg);\n}\n\n.fvb__video {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: #000;\n}\n.fvb__videoel {\n max-width: 100%;\n max-height: 100%;\n}\n\n.fvb__binary {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 32px;\n color: var(--faint);\n text-align: center;\n}\n.fvb__binary p {\n margin: 0;\n font-size: 13px;\n color: var(--muted);\n}\n.fvb__binary-sub {\n font-size: 11.5px;\n color: var(--faint) !important;\n}\n.fvb__dlbtn {\n margin-top: 8px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 6px 16px;\n text-decoration: none;\n cursor: pointer;\n}\n.fvb__dlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* \"Open with\" formatter chooser on the unpreviewable-binary card. */\n.fvb__openas {\n margin-top: 18px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 9px;\n}\n.fvb__openas-label {\n font-size: 12px;\n color: var(--muted);\n}\n.fvb__openas-grid {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 7px;\n max-width: 320px;\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"FileViewerBody.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/FileViewerBody.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, onBeforeUnmount, ref, watch } from 'vue'\nimport { optionalViewer } from '@/components/optionalViewer'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { skipsTextLoad } from '@/lib/viewerKind'\nimport {\n VIEWER_FORMATTERS as FORMATTERS,\n applyFormatter,\n findFormatter,\n type ViewerFormatter,\n} from '@/lib/viewerFormats'\nimport { fileExt } from '@/lib/fileType'\nimport { looksLikeMindMap } from '@/lib/mindmap'\nimport { parseFrontmatter } from '@/lib/frontmatter'\nimport { formatBytes } from '@/lib/format'\nimport CodeView from '@/components/CodeView.vue'\nimport MarkdownText from '@/components/MarkdownText'\nimport FrontmatterCard from '@/components/FrontmatterCard.vue'\nimport ZoomImage from '@/components/ZoomImage.vue'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's CONTENT area — the type dispatch shared by {@link FileViewerPanel} (a top-level file) and\n * the archive browsers (a file inside a zip/7z/tar/rar, extracted to a blob URL). Given a {@link ViewerFile}\n * it renders the right thing: a self-fetching sub-viewer (docx/pdf/dicom/…), an inline image/video, or — for\n * the text-backed kinds — markdown / html / svg / mind-map / subtitle / geo / csv (rendered), or highlighted,\n * foldable source. It fetches its own text for the text-backed kinds and emits it via `loaded` so the host's\n * copy button can use it. The host owns the header/bar, the render↔source `mode`, and copy/download.\n *\n * Extracted from FileViewerPanel so an archive entry previews with the EXACT same viewers as a top-level file\n * (the old archive detail view only did text/image/office/dicom). The archive browsers import this lazily to\n * break the import cycle (this component lazy-imports them right back for nested archives).\n *\n * \"Open with\" override: ANY file can be force-opened as a different formatter (hex / text / markdown / html /\n * svg / image / audio / video — {@link VIEWER_FORMATTERS}), since auto-detection by extension/mime can be\n * wrong. The choice becomes an EFFECTIVE file ({@link vf}) that re-runs the dispatch. Two entry points drive\n * the SAME override: (1) the host's header menu (FileViewerPanel) calls the exposed `pickFormat`/`resetFormat`\n * — the primary, universal entry; (2) the unpreviewable-binary card's own chip row, for the archive-browser\n * context where there is no host header. The effective file is emitted via `formatchange` (so the host tracks\n * toggle/copy visibility) and the active key via `formatpick` (so the host menu highlights it). `hostChooser`\n * hides the in-body switcher bar when the host already shows the chooser in its header (no duplicate UI).\n */\nconst props = withDefaults(\n defineProps<{\n file: ViewerFile\n mode?: 'render' | 'source'\n hostChooser?: boolean\n /**\n * Render-only HTML whose archive-relative dependencies have already been rewritten. Source/copy continue to\n * use `file.content` (or fetched text), so opening an HTML entry inside a ZIP still shows its original source.\n */\n htmlPreview?: string\n }>(),\n { mode: 'render', hostChooser: false },\n)\nconst emit = defineEmits<{\n loaded: [text: string]\n formatchange: [file: ViewerFile]\n formatpick: [key: string]\n}>()\n\n// Archives import this body; lazy-import them back so the module cycle never evaluates eagerly.\nconst ZipBrowser = defineAsyncComponent(() => import('@/components/ZipBrowser.vue'))\nconst SevenZipBrowser = defineAsyncComponent(() => import('@/components/SevenZipBrowser.vue'))\nconst ArchiveBrowser = defineAsyncComponent(() => import('@/components/ArchiveBrowser.vue'))\nconst BinObjBrowser = defineAsyncComponent(() => import('@/components/BinObjBrowser.vue'))\nconst EplViewer = defineAsyncComponent(() => import('@/components/EplViewer.vue'))\nconst ClassViewer = defineAsyncComponent(() => import('@/components/ClassViewer.vue'))\nconst HprofViewer = defineAsyncComponent(() => import('@/components/HprofViewer.vue'))\nconst MsiViewer = defineAsyncComponent(() => import('@/components/MsiViewer.vue'))\nconst AppxViewer = defineAsyncComponent(() => import('@/components/AppxViewer.vue'))\n// Heavy viewers whose engine is a STATIC optional peer dep: wrap so a missing dep shows an install card.\nconst DicomViewer = optionalViewer(\n () => import('@/components/DicomViewer.vue'),\n 'dicom-parser',\n 'DICOM 影像',\n)\nconst DocxViewer = optionalViewer(\n () => import('@/components/DocxViewer.vue'),\n 'docx-preview',\n 'Word 文档',\n)\nconst XlsxViewer = optionalViewer(\n () => import('@/components/XlsxViewer.vue'),\n ['exceljs', 'numfmt'],\n 'Excel 表格',\n)\nconst PptxViewer = optionalViewer(\n () => import('@/components/PptxViewer.vue'),\n ['@aiden0z/pptx-renderer', 'pdfjs-dist'],\n 'PowerPoint',\n)\nconst RtfViewer = defineAsyncComponent(() => import('@/components/RtfViewer.vue'))\nconst PdfViewer = optionalViewer(() => import('@/components/PdfViewer.vue'), 'pdfjs-dist', 'PDF')\nconst MindMapView = defineAsyncComponent(() => import('@/components/MindMapView.vue'))\nconst XmindViewer = defineAsyncComponent(() => import('@/components/XmindViewer.vue'))\nconst EmlViewer = defineAsyncComponent(() => import('@/components/EmlViewer.vue'))\nconst MhtViewer = defineAsyncComponent(() => import('@/components/MhtViewer.vue'))\nconst HarViewer = defineAsyncComponent(() => import('@/components/HarViewer.vue'))\nconst SqliteViewer = optionalViewer(\n () => import('@/components/SqliteViewer.vue'),\n 'sql.js',\n 'SQLite 数据库',\n)\nconst SubtitleViewer = defineAsyncComponent(() => import('@/components/SubtitleViewer.vue'))\nconst FontViewer = defineAsyncComponent(() => import('@/components/FontViewer.vue'))\nconst NotebookViewer = defineAsyncComponent(() => import('@/components/NotebookViewer.vue'))\nconst GeoViewer = defineAsyncComponent(() => import('@/components/GeoViewer.vue'))\nconst CsvViewer = defineAsyncComponent(() => import('@/components/CsvViewer.vue'))\nconst JsonlViewer = defineAsyncComponent(() => import('@/components/JsonlViewer.vue'))\nconst CertViewer = defineAsyncComponent(() => import('@/components/CertViewer.vue'))\nconst JksViewer = defineAsyncComponent(() => import('@/components/JksViewer.vue'))\nconst AudioViewer = defineAsyncComponent(() => import('@/components/AudioViewer.vue'))\nconst QmcAudioViewer = defineAsyncComponent(() => import('@/components/QmcAudioViewer.vue'))\nconst LrcViewer = defineAsyncComponent(() => import('@/components/LrcViewer.vue'))\nconst LogViewer = defineAsyncComponent(() => import('@/components/LogViewer.vue'))\nconst ModelViewer = optionalViewer(() => import('@/components/ModelViewer.vue'), 'three', '3D 模型')\nconst RasterViewer = defineAsyncComponent(() => import('@/components/RasterViewer.vue'))\nconst PsdViewer = defineAsyncComponent(() => import('@/components/PsdViewer.vue'))\nconst EbookViewer = defineAsyncComponent(() => import('@/components/EbookViewer.vue'))\nconst DrawioViewer = defineAsyncComponent(() => import('@/components/DrawioViewer.vue'))\nconst DxfViewer = defineAsyncComponent(() => import('@/components/DxfViewer.vue'))\nconst HexViewer = defineAsyncComponent(() => import('@/components/HexViewer.vue'))\n\n// ── \"open with\" override ──────────────────────────────────────────────────────────────────────\n// A user-chosen formatter ({@link VIEWER_FORMATTERS}). When set it replaces props.file as the EFFECTIVE file\n// the dispatch (and load) use; cleared whenever a different file is opened. Driven by the host header menu\n// (via the exposed pickFormat/resetFormat) and/or the in-body binary-card chips — both funnel through `pick`.\nconst pickedKey = ref('')\nconst picked = ref<ViewerFile | null>(null)\n// The effective file: the user's chosen formatter, else the file passed in.\nconst vf = computed<ViewerFile>(() => picked.value ?? props.file)\n\nfunction pick(f: ViewerFormatter): void {\n pickedKey.value = f.key\n picked.value = applyFormatter(props.file, f)\n}\n// Host-driven entry points (FileViewerPanel's header menu): pick by key, or fall back to auto-detection.\nfunction pickFormat(key: string): void {\n const f = findFormatter(key)\n if (f) pick(f)\n}\nfunction resetFormat(): void {\n pickedKey.value = ''\n picked.value = null\n}\n// Announce the active formatter so the host header can highlight it (fires on pick, reset, and file change).\nwatch(pickedKey, (k) => emit('formatpick', k))\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst text = ref('')\nconst errorMsg = ref('')\n\n// Mind map only renders the diagram once the loaded text actually looks like one (a non-mind-map `.mm` reads\n// as plain source). Same content sniff the panel used.\nconst isMindMap = computed(\n () =>\n vf.value.mindmap === true &&\n status.value === 'ready' &&\n looksLikeMindMap(text.value, fileExt(vf.value.name)),\n)\n\n// Markdown render: lift any leading YAML frontmatter into a card; the source view still shows the raw file.\nconst frontmatter = computed(() =>\n vf.value.lang === 'markdown' ? parseFrontmatter(text.value) : { data: {}, body: text.value },\n)\nconst hasFrontmatter = computed(() => Object.keys(frontmatter.value.data).length > 0)\n\n// Drop a single trailing newline so the source view doesn't show a phantom last line.\nconst sourceText = computed(() => text.value.replace(/\\n$/, ''))\n\n// SVG render: rasterize the markup via a data-URL <img> (an <img> never runs SVG scripts).\nconst svgSrc = computed(() =>\n text.value ? `data:image/svg+xml,${encodeURIComponent(text.value)}` : '',\n)\n\n// HTML preview, two modes (default = inert):\n// - INERT (default): the iframe carries an empty `sandbox` (no `allow-scripts`) plus an injected\n// `script-src 'none'` CSP, so NO upstream script runs — only layout/CSS/images render.\n// - INTERACTIVE (opt-in via the toolbar): scripts run inside a HARDENED sandbox — `allow-scripts` WITHOUT\n// `allow-same-origin` gives the frame an opaque origin (no access to our cookies/storage/session/DOM); that\n// opaque origin is the REAL isolation. The CSP allows external sub-resources AND network so CDN-based pages\n// fully work — scripts/styles/fonts/images from unpkg/jsdelivr/Google Fonts, plus fetch/XHR/WebSocket and\n// source-map loads (`connect-src` open to https/http/ws/wss). Only the structural hardening stays:\n// `object-src 'none'` (no plugins) and `base-uri 'none'` (no <base> hijack); forms are already blocked by\n// the sandbox (no `allow-forms`). Because the origin is opaque, even with network open the page can't reach\n// our session/cookies — it acts only with its own (already-visible) content. Review the source first\n// (render/source toggle), then enable. The ⛶ button opens this SAME sandboxed frame as a full-screen\n// in-app overlay (still an iframe → still opaque-origin), so a big interactive view stays safe — there's\n// no top-level / new-tab path that could ever run scripts in our origin.\nconst INERT_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'\">`\nconst INTERACTIVE_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"default-src 'unsafe-inline' 'unsafe-eval' data: blob: https: http:; ` +\n `connect-src data: blob: https: http: ws: wss:; object-src 'none'; base-uri 'none'\">`\n\nfunction injectCsp(html: string, csp: string): string {\n const head = html.match(/<head[^>]*>/i)\n if (head) {\n const at = (head.index ?? 0) + head[0].length\n return html.slice(0, at) + csp + html.slice(at)\n }\n return `<!doctype html><html><head><meta charset=\"utf-8\">${csp}</head><body>${html}</body></html>`\n}\n\nconst inertBlobUrl = ref('') // the inert (non-interactive) iframe + full-screen overlay\nconst interactiveBlobUrl = ref('') // built only when the user opts in\nconst interactive = ref(false)\nconst fullscreen = ref(false) // in-app full-screen overlay (Teleport to body) over the SAME sandboxed frame\nconst frameBlobUrl = computed(() =>\n interactive.value ? interactiveBlobUrl.value : inertBlobUrl.value,\n)\nconst previewHtml = computed(() => props.htmlPreview ?? text.value)\n\nfunction revokeHtmlBlobs(): void {\n if (inertBlobUrl.value) {\n URL.revokeObjectURL(inertBlobUrl.value)\n inertBlobUrl.value = ''\n }\n if (interactiveBlobUrl.value) {\n URL.revokeObjectURL(interactiveBlobUrl.value)\n interactiveBlobUrl.value = ''\n }\n}\n\nwatch([() => vf.value.html, previewHtml], ([html, preview]) => {\n revokeHtmlBlobs()\n interactive.value = false // a new file / changed text always returns to the safe inert default\n fullscreen.value = false // …and closes any open full-screen overlay\n if (html && typeof URL !== 'undefined' && URL.createObjectURL) {\n inertBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(preview, INERT_CSP)], { type: 'text/html' }),\n )\n }\n})\n\n// Toggle the sandboxed iframe between inert and interactive (builds the interactive blob lazily on first use).\nfunction toggleInteractive(): void {\n if (interactive.value) {\n interactive.value = false\n return\n }\n if (typeof URL !== 'undefined' && URL.createObjectURL && !interactiveBlobUrl.value) {\n interactiveBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(previewHtml.value, INTERACTIVE_CSP)], { type: 'text/html' }),\n )\n }\n interactive.value = true\n}\n\n// Full-screen overlay key handling + body scroll lock — only while open, SSR-guarded (no window in smoke).\nfunction onFsKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') fullscreen.value = false\n}\nwatch(fullscreen, (on) => {\n if (typeof window === 'undefined') return\n if (on) {\n window.addEventListener('keydown', onFsKey)\n document.body.style.overflow = 'hidden'\n } else {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\nasync function load(noStore = false): Promise<void> {\n const f = vf.value\n // Self-fetching sub-viewers + image/video/binary render straight from the URL — no text to load.\n if (skipsTextLoad(f)) {\n text.value = ''\n status.value = 'ready'\n return\n }\n if (f.content != null) {\n text.value = f.content\n status.value = 'ready'\n emit('loaded', f.content)\n return\n }\n if (!f.url) {\n text.value = ''\n status.value = 'ready'\n return\n }\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(f.url, noStore ? { cache: 'no-store' } : {})\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const t = await res.text()\n text.value = t\n status.value = 'ready'\n emit('loaded', t)\n } catch (e) {\n errorMsg.value = String(e)\n status.value = 'error'\n }\n}\n\n// A new file resets any \"open with\" override (registered BEFORE the vf watcher so picked is cleared before the\n// effective file is re-read below).\nwatch(\n () => props.file,\n () => {\n picked.value = null\n pickedKey.value = ''\n },\n)\n// Load + announce the effective file whenever it changes (a new file, or a picked formatter).\nwatch(\n vf,\n (v) => {\n emit('formatchange', v)\n void load()\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => {\n revokeHtmlBlobs()\n if (typeof window !== 'undefined') {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\n// The host's refresh button re-fetches (cache-busting) through here; its header menu drives the \"open with\"\n// override through pickFormat / resetFormat (see the override block above).\ndefineExpose({ reload: () => load(true), pickFormat, resetFormat })\n</script>\n\n<template>\n <div class=\"fvb\">\n <!-- forced-format switcher: shown once the user picks a formatter, so they can swap. Hidden when the host\n provides the chooser in its header (FileViewerPanel) — only the archive browsers render it in-body. -->\n <div v-if=\"picked && !hostChooser\" class=\"fvb__fmtbar\">\n <span class=\"fvb__fmtbar-label\">格式</span>\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n :class=\"{ 'fvb__fmtchip--on': pickedKey === f.key }\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n\n <!-- zip / 7z / tar-rar: the archive browsers fetch the bytes, decompress in-browser, and browse entries -->\n <ZipBrowser v-if=\"vf.zip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <SevenZipBrowser v-else-if=\"vf.sevenZip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <ArchiveBrowser v-else-if=\"vf.archive\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :kind=\"vf.archive\" />\n\n <!-- native binary object (.so/.dll): parse + list symbols -->\n <BinObjBrowser v-else-if=\"vf.binobj\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- e-language (.e/.ec): parse the binary section format + decompile each method to 易语言 source -->\n <EplViewer v-else-if=\"vf.epl\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- java bytecode (.class): parse the structural tables + list fields/methods (no decompilation) -->\n <ClassViewer v-else-if=\"vf.javaClass\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- jvm heap dump (.hprof): stream-parse → class histogram + per-class object drill-in (paginated/filterable) -->\n <HprofViewer v-else-if=\"vf.hprof\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows installer (.msi): parse the OLE compound file → product identity + summary info -->\n <MsiViewer v-else-if=\"vf.msi\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows app package (.appx/.msix): unzip the manifest → identity / deps / capabilities -->\n <AppxViewer v-else-if=\"vf.appx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- xmind workbook (.xmind): unzip content.json/xml → render as an interactive mind map -->\n <XmindViewer v-else-if=\"vf.xmind\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- dxf (.dxf): parse the ASCII CAD drawing → SVG with pan/zoom + layer toggles -->\n <DxfViewer v-else-if=\"vf.dxf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <DicomViewer v-else-if=\"vf.dicom\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <DocxViewer v-else-if=\"vf.docx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <XlsxViewer v-else-if=\"vf.xlsx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PptxViewer v-else-if=\"vf.pptx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <RtfViewer v-else-if=\"vf.rtf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PdfViewer v-else-if=\"vf.pdf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <EmlViewer v-else-if=\"vf.eml\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <MhtViewer v-else-if=\"vf.mht\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <HarViewer v-else-if=\"vf.har\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <SqliteViewer v-else-if=\"vf.sqlite\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <CertViewer v-else-if=\"vf.cert\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- java keystore (.jks/.keystore/.jceks): parse the inventory + certs; password decrypts private keys -->\n <JksViewer v-else-if=\"vf.jks\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <FontViewer v-else-if=\"vf.font\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <NotebookViewer v-else-if=\"vf.ipynb\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <AudioViewer v-else-if=\"vf.audio\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- QQ Music encrypted audio (.mflac/.mgg): decrypt in-browser → play + download decrypted -->\n <QmcAudioViewer v-else-if=\"vf.qmc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- LRC lyrics (.lrc): self-fetch + sniff GBK → synced lyric list (own render/source toggle) -->\n <LrcViewer v-else-if=\"vf.lrc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <LogViewer v-else-if=\"vf.log\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <ModelViewer v-else-if=\"vf.model\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- PSD: dedicated viewer with a live layer tree (toggle visibility → re-composite); other raster kinds\n (TIFF/HEIC/RAW) use the generic decoding viewer. -->\n <PsdViewer\n v-else-if=\"vf.raster === 'psd'\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n />\n <RasterViewer\n v-else-if=\"vf.raster\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.raster\"\n />\n <EbookViewer\n v-else-if=\"vf.ebook\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.ebook\"\n />\n\n <!-- hex dump: offset + raw bytes + ASCII (the universal \"open with\" fall-back; never auto-detected) -->\n <HexViewer v-else-if=\"vf.hex\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- image (archive entry, or forced via \"open with\"): a zoomable inline picture -->\n <div v-else-if=\"vf.image\" class=\"fvb__imagezoom\">\n <ZoomImage :src=\"vf.url ?? ''\" :alt=\"vf.name\" />\n </div>\n\n <!-- video (archive entry): native player from the blob URL -->\n <div v-else-if=\"vf.video\" class=\"fvb__video\">\n <video class=\"fvb__videoel\" :src=\"vf.url ?? ''\" controls preload=\"metadata\" />\n </div>\n\n <!-- unpreviewable binary (archive entry / unmatched file): download card + \"open with\" formatter chips -->\n <div v-else-if=\"vf.binary\" class=\"fvb__binary\">\n <Icon name=\"archive\" :size=\"34\" />\n <p>无法预览的二进制文件</p>\n <p v-if=\"vf.size\" class=\"fvb__binary-sub\">{{ formatBytes(vf.size) }}</p>\n <a class=\"fvb__dlbtn\" :href=\"vf.url\" :download=\"vf.name\">下载</a>\n <div class=\"fvb__openas\">\n <span class=\"fvb__openas-label\">用查看器打开:</span>\n <div class=\"fvb__openas-grid\">\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n </div>\n </div>\n\n <p v-else-if=\"status === 'loading'\" class=\"fvb__msg\">加载文件中…</p>\n <div v-else-if=\"status === 'error'\" class=\"fvb__msg fvb__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"fvb__retry\" @click=\"load(true)\">重试</button>\n </div>\n\n <!-- markdown render: frontmatter card (if any) + rendered document -->\n <div\n v-else-if=\"vf.lang === 'markdown' && mode === 'render'\"\n class=\"fvb__scroll fvb__scroll--doc\"\n >\n <div class=\"fvb__markdown\">\n <FrontmatterCard v-if=\"hasFrontmatter\" :data=\"frontmatter.data\" />\n <MarkdownText :text=\"frontmatter.body\" kp=\"fv\" />\n </div>\n </div>\n\n <!-- html render: sandboxed iframe fed by a blob. Default INERT (no scripts); the ▶ button opts into a\n HARDENED interactive sandbox (allow-scripts, opaque origin, CDN + network OK), and ⛶ opens the same\n sandboxed frame as a full-screen in-app overlay (no top-level/new-tab path). -->\n <div v-else-if=\"vf.html && mode === 'render'\" class=\"fvb__html\">\n <div class=\"fvb__htmlbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行:可加载外部 CDN 并发起网络请求(fetch/XHR/WebSocket),但无法访问本站会话 / cookie(不透明源隔离)'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,但无法访问本站会话 / cookie;建议先切到源码视图审查再启用)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"全屏预览(在应用内放大查看;可在全屏内开启交互脚本)\"\n @click=\"fullscreen = true\"\n >\n <Icon name=\"maximize\" :size=\"13\" />\n </button>\n </div>\n <!-- :key forces a full remount when toggling interactive: a `sandbox` change on an existing iframe does\n NOT re-apply to the already-loaded document, so the new doc must load on a fresh element that already\n carries `allow-scripts` (set before the element is inserted, i.e. before navigation commits). -->\n <iframe\n :key=\"interactive ? 'interactive' : 'inert'\"\n class=\"fvb__frame\"\n :src=\"frameBlobUrl\"\n title=\"HTML 预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n loading=\"lazy\"\n referrerpolicy=\"no-referrer\"\n />\n\n <!-- In-app full-screen overlay: the SAME sandboxed frame at viewport size (an iframe → opaque origin,\n never a top-level same-origin document). Esc / ✕ close it; the toolbar keeps the ▶ interactive\n toggle so scripts can be enabled while full-screen. Teleported to <body> to clear stacking contexts. -->\n <Teleport to=\"body\">\n <div v-if=\"fullscreen\" class=\"fvb__fs\">\n <div class=\"fvb__fsbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行,可联网但访问不到本站会话 / cookie'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,访问不到本站会话 / cookie)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"15\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"关闭全屏 (Esc)\"\n @click=\"fullscreen = false\"\n >\n <Icon name=\"close\" :size=\"15\" :stroke-width=\"2.2\" />\n </button>\n </div>\n <iframe\n :key=\"interactive ? 'fs-interactive' : 'fs-inert'\"\n class=\"fvb__fsframe\"\n :src=\"frameBlobUrl\"\n title=\"HTML 全屏预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n referrerpolicy=\"no-referrer\"\n />\n </div>\n </Teleport>\n </div>\n\n <!-- svg render: the markup as a picture (data-URL <img>) -->\n <div v-else-if=\"vf.svg && mode === 'render'\" class=\"fvb__svg\">\n <img class=\"fvb__svgimg\" :src=\"svgSrc\" :alt=\"vf.name\" />\n </div>\n\n <!-- mind-map render: the OPML/FreeMind XML drawn as an interactive SVG tree -->\n <div v-else-if=\"isMindMap && mode === 'render'\" class=\"fvb__fill\">\n <MindMapView :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- subtitle render: .srt/.vtt/.ass parsed into a timecoded transcript -->\n <div v-else-if=\"vf.subtitle && mode === 'render'\" class=\"fvb__fill\">\n <SubtitleViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- geo render: .geojson/.kml/.gpx drawn as an offline SVG map -->\n <div v-else-if=\"vf.geo && mode === 'render'\" class=\"fvb__fill\">\n <GeoViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- csv render: .csv/.tsv parsed into a table -->\n <div v-else-if=\"vf.csv && mode === 'render'\" class=\"fvb__fill\">\n <CsvViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- jsonl render: .jsonl/.ndjson parsed into a record list (truncated rows, click to expand) -->\n <div v-else-if=\"vf.jsonl && mode === 'render'\" class=\"fvb__fill\">\n <JsonlViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- diagram render: .drawio/.excalidraw drawn as an SVG (source toggle shows the raw XML/JSON) -->\n <div v-else-if=\"vf.diagram && mode === 'render'\" class=\"fvb__fill\">\n <DrawioViewer :text=\"text\" :name=\"vf.name\" :kind=\"vf.diagram\" />\n </div>\n\n <!-- source: everything else, or markdown/html/svg/… viewed as source — highlighted + folding + format -->\n <CodeView v-else :code=\"sourceText\" :lang=\"vf.lang ?? ''\" :name=\"vf.name\" />\n </div>\n</template>\n\n<style scoped>\n/* Fills the area below the host header/bar; each branch is itself a flex child that grows. */\n.fvb {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* Forced-format switcher bar (shown when the user opened an unhandled file via the \"open with\" chooser). */\n.fvb__fmtbar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n flex-shrink: 0;\n padding: 7px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.fvb__fmtbar-label {\n margin-right: 2px;\n font-size: 11.5px;\n color: var(--faint);\n}\n/* Chip used both in the switcher bar and on the unpreviewable-binary card's \"open with\" row. */\n.fvb__fmtchip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 999px;\n padding: 5px 11px;\n cursor: pointer;\n transition:\n background 0.15s,\n border-color 0.15s,\n color 0.15s;\n}\n.fvb__fmtchip:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.fvb__fmtchip--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__fmtchip--on:hover {\n color: #fff;\n}\n\n.fvb__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.fvb__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n}\n.fvb__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.fvb__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.fvb__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overscroll-behavior: contain;\n background: var(--code-bg);\n}\n.fvb__scroll--doc {\n overflow: auto;\n}\n.fvb__markdown {\n padding: 18px 24px 80px;\n}\n\n/* HTML render: the iframe fills the area; a small overlay toolbar (top-right) holds the open-in-new-tab\n * button. White canvas so documents that don't set their own background read as a normal page. */\n.fvb__html {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n background: #fff;\n}\n.fvb__htmlbar {\n position: absolute;\n top: 8px;\n right: 8px;\n z-index: 2;\n display: flex;\n align-items: center;\n gap: 4px;\n}\n.fvb__htmlbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n text-decoration: none;\n cursor: pointer;\n box-shadow: var(--shadow);\n}\n.fvb__htmlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n/* Interactive scripts enabled: accent the toggle so the elevated (but sandboxed) state is obvious. */\n.fvb__htmlbtn--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__htmlbtn--on:hover {\n color: #fff;\n filter: brightness(1.06);\n}\n.fvb__frame {\n display: block;\n width: 100%;\n height: 100%;\n border: none;\n background: #fff;\n}\n\n/* In-app full-screen HTML overlay (Teleport to body) — same sandboxed frame at viewport size. */\n.fvb__fs {\n position: fixed;\n inset: 0;\n z-index: 2000;\n display: flex;\n flex-direction: column;\n background: rgba(0, 0, 0, 0.82);\n animation: fvb-fs-in 0.12s ease-out;\n}\n@keyframes fvb-fs-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n.fvb__fsbar {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 6px;\n padding: 8px 10px;\n}\n.fvb__fsframe {\n flex: 1 1 auto;\n width: 100%;\n min-height: 0;\n border: none;\n background: #fff;\n}\n\n/* Generic \"viewer owns the area below\" host (mind map / subtitle / geo / csv). */\n.fvb__fill {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n.fvb__svg {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: var(--surface-2);\n}\n.fvb__svgimg {\n max-width: 100%;\n height: auto;\n object-fit: contain;\n}\n\n.fvb__imagezoom {\n width: 100%;\n min-width: 0;\n flex: 1 1 auto;\n min-height: 0;\n position: relative;\n overflow: hidden;\n background: var(--code-bg);\n}\n\n.fvb__video {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: #000;\n}\n.fvb__videoel {\n max-width: 100%;\n max-height: 100%;\n}\n\n.fvb__binary {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 32px;\n color: var(--faint);\n text-align: center;\n}\n.fvb__binary p {\n margin: 0;\n font-size: 13px;\n color: var(--muted);\n}\n.fvb__binary-sub {\n font-size: 11.5px;\n color: var(--faint) !important;\n}\n.fvb__dlbtn {\n margin-top: 8px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 6px 16px;\n text-decoration: none;\n cursor: pointer;\n}\n.fvb__dlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* \"Open with\" formatter chooser on the unpreviewable-binary card. */\n.fvb__openas {\n margin-top: 18px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 9px;\n}\n.fvb__openas-label {\n font-size: 12px;\n color: var(--muted);\n}\n.fvb__openas-grid {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 7px;\n max-width: 320px;\n}\n</style>\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import e from "../_virtual/_plugin-vue_export-helper.js";
2
2
  import t from "./JsonlViewer.vue_vue_type_script_setup_true_lang.js";
3
3
  /* empty css */
4
4
  //#region src/components/JsonlViewer.vue
5
- var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-326ae959"]]);
5
+ var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-870e2c91"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
8
 
@@ -1 +1 @@
1
- {"version":3,"file":"JsonlViewer.js","names":[],"sources":["../../src/components/JsonlViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { parseJsonl, type JsonlRecord } from '@/lib/jsonl'\nimport { highlightCode } from '@/lib/highlight'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer JSON Lines (`.jsonl` / `.ndjson`) renderer (render mode). Receives the fetched text (the panel\n * loads it, like CsvViewer / SubtitleViewer), parses it into per-line records (see lib/jsonl.ts), and shows a\n * compact LIST — one truncated single-line preview per record. Clicking a row expands it to the pretty-printed,\n * syntax-highlighted value. Big files stay responsive two ways: each row's preview is a short single line, and\n * only an initial window of rows is in the DOM (a \"load more\" button grows it). The source toggle (header) shows\n * the raw file highlighted as JSON.\n */\nconst props = defineProps<{ text: string; name: string }>()\n\nconst PREVIEW_CHARS = 200 // truncate each row's inline preview so even a huge record is one short line\nconst INITIAL_ROWS = 200 // rows mounted initially; \"load more\" grows the window\nconst ROW_STEP = 400\n\nconst doc = computed(() => parseJsonl(props.text))\nconst shown = ref(INITIAL_ROWS)\nconst expanded = ref<Set<number>>(new Set())\n\n// A new file resets the window + any open rows.\nwatch(\n () => props.text,\n () => {\n shown.value = INITIAL_ROWS\n expanded.value = new Set()\n },\n)\n\nconst visible = computed(() => doc.value.records.slice(0, shown.value))\nconst remaining = computed(() => Math.max(0, doc.value.records.length - shown.value))\n\nfunction loadMore(): void {\n shown.value += ROW_STEP\n}\n\nfunction toggle(line: number): void {\n const s = new Set(expanded.value)\n if (s.has(line)) s.delete(line)\n else s.add(line)\n expanded.value = s\n}\n\n// Per-row copy: write the record's RAW line (the faithful on-disk text, not the re-stringified value) to the\n// clipboard, and flash a ✓ on that row. Mirrors the CodeBlock copy pattern (best-effort, cleared on a timer).\nconst copiedLine = ref<number | null>(null)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyRow(r: JsonlRecord): Promise<void> {\n try {\n await navigator.clipboard?.writeText(r.raw)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copiedLine.value = r.line\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copiedLine.value = null), 1400)\n}\nonBeforeUnmount(() => clearTimeout(copyTimer))\n\n// Compact one-line preview: stringify the parsed value (whitespace collapsed) or, for an error row, the raw\n// line — then hard-truncate. Building it from the value normalizes spacing so the list reads cleanly.\nfunction preview(r: JsonlRecord): string {\n let s: string\n if (r.error) s = r.raw\n else {\n try {\n s = JSON.stringify(r.value) ?? String(r.value)\n } catch {\n s = r.raw\n }\n }\n s = s.replace(/\\s+/g, ' ').trim()\n return s.length > PREVIEW_CHARS ? s.slice(0, PREVIEW_CHARS) + '…' : s\n}\n\n// Pretty value, highlighted as JSON — built lazily, only for an expanded row.\nfunction expandedHtml(r: JsonlRecord): string {\n if (r.error) return highlightCode(r.raw, 'json')\n try {\n return highlightCode(JSON.stringify(r.value, null, 2) ?? '', 'json')\n } catch {\n return highlightCode(r.raw, 'json')\n }\n}\n</script>\n\n<template>\n <div class=\"jl\">\n <div class=\"jl__summary\">\n <span class=\"jl__count\">{{ doc.records.length }} 条记录</span>\n <span v-if=\"doc.errorCount\" class=\"jl__errtag\">{{ doc.errorCount }} 行无法解析</span>\n </div>\n\n <div class=\"jl__list\">\n <p v-if=\"!doc.records.length\" class=\"jl__msg\">(空文件,无 JSON 记录)</p>\n\n <div v-for=\"r in visible\" :key=\"r.line\" class=\"jl__rowwrap\">\n <div\n class=\"jl__rowhead\"\n :class=\"{ 'jl__rowhead--open': expanded.has(r.line), 'jl__rowhead--err': r.error }\"\n >\n <button\n type=\"button\"\n class=\"jl__row\"\n :title=\"r.error ? `第 ${r.line} 行:JSON 无效` : `第 ${r.line} 行 —— 点击展开`\"\n @click=\"toggle(r.line)\"\n >\n <span class=\"jl__caret\" :class=\"{ 'jl__caret--open': expanded.has(r.line) }\">▶</span>\n <span class=\"jl__ln\">{{ r.line }}</span>\n <span v-if=\"r.error\" class=\"jl__badge\">无效</span>\n <span class=\"jl__preview\">{{ preview(r) }}</span>\n </button>\n <button\n type=\"button\"\n class=\"jl__copy\"\n :class=\"{ 'jl__copy--done': copiedLine === r.line }\"\n :title=\"copiedLine === r.line ? '已复制' : '复制此行'\"\n @click=\"copyRow(r)\"\n >\n <Icon :name=\"copiedLine === r.line ? 'check' : 'copy'\" :size=\"13\" />\n </button>\n </div>\n <pre\n v-if=\"expanded.has(r.line)\"\n class=\"jl__full\"\n ><code class=\"hljs\" v-html=\"expandedHtml(r)\" /></pre>\n </div>\n\n <button v-if=\"remaining\" type=\"button\" class=\"jl__more\" @click=\"loadMore\">\n 显示更多(还有 {{ remaining }} 条)\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.jl {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.jl__summary {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 6px 12px;\n flex-wrap: wrap;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n.jl__count {\n font-weight: 600;\n color: var(--accent);\n}\n.jl__errtag {\n padding: 1px 7px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--danger) 14%, var(--surface));\n color: var(--danger);\n font-size: 10.5px;\n}\n\n.jl__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n\n.jl__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: scroll; /* always reserve the gutter so the draggable bar is present, not wheel-only */\n overscroll-behavior: contain;\n padding: 4px 0;\n /* Always-visible, draggable scrollbar — the record list gets long and an auto-hidden overlay bar reads\n * as \"no scrollbar\". Thin styled bar in Firefox; a classic (gutter-reserving) bar in Chromium/WebKit. */\n scrollbar-width: thin;\n scrollbar-color: var(--border-2) transparent;\n}\n.jl__list::-webkit-scrollbar {\n width: 12px;\n}\n.jl__list::-webkit-scrollbar-thumb {\n background: var(--border-2);\n background-clip: padding-box;\n border: 3px solid transparent;\n border-radius: 999px;\n}\n.jl__list::-webkit-scrollbar-thumb:hover {\n background: var(--muted);\n background-clip: padding-box;\n border: 3px solid transparent;\n}\n.jl__rowwrap {\n border-bottom: 1px solid var(--border);\n}\n\n/* Row header = the clickable preview button + a trailing copy button (a button can't nest inside a button,\n * so they're siblings in this flex row; the hover/open/error tint lives here, spanning both). */\n.jl__rowhead {\n display: flex;\n align-items: stretch;\n}\n.jl__rowhead:hover,\n.jl__rowhead--open {\n background: var(--surface-2);\n}\n\n.jl__row {\n display: flex;\n align-items: baseline;\n gap: 9px;\n flex: 1 1 auto;\n min-width: 0;\n padding: 6px 14px;\n background: transparent;\n border: none;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n/* Trailing per-row copy button: faint by default, brightening on hover; accent flash when just copied. */\n.jl__copy {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n background: transparent;\n border: none;\n color: var(--faint);\n cursor: pointer;\n transition: color 0.15s;\n}\n.jl__copy:hover {\n color: var(--accent);\n}\n.jl__copy--done {\n color: var(--accent);\n}\n.jl__caret {\n flex-shrink: 0;\n width: 9px;\n font-size: 9px;\n color: var(--faint);\n transition: transform 0.12s;\n transform: rotate(0deg);\n align-self: center;\n}\n.jl__caret--open {\n transform: rotate(90deg);\n}\n.jl__ln {\n flex-shrink: 0;\n min-width: 30px;\n text-align: right;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n user-select: none;\n}\n.jl__badge {\n flex-shrink: 0;\n padding: 0 6px;\n border-radius: 5px;\n background: color-mix(in srgb, var(--danger) 16%, var(--surface));\n color: var(--danger);\n font-size: 9.5px;\n font-weight: 700;\n letter-spacing: 0.04em;\n align-self: center;\n}\n.jl__preview {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-family: var(--font-mono);\n font-size: 12.5px;\n color: var(--text);\n}\n.jl__rowhead--err .jl__preview {\n color: var(--muted);\n}\n\n.jl__full {\n margin: 0;\n padding: 8px 14px 12px 32px;\n overflow-x: auto;\n background: var(--code-bg);\n font-family: var(--font-mono);\n font-size: 12px;\n line-height: 1.55;\n white-space: pre;\n tab-size: 2;\n}\n.jl__full code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n\n.jl__more {\n display: block;\n width: 100%;\n padding: 10px 16px;\n font-family: inherit;\n font-size: 12px;\n color: var(--accent);\n background: transparent;\n border: none;\n cursor: pointer;\n}\n.jl__more:hover {\n background: var(--surface-2);\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"JsonlViewer.js","names":[],"sources":["../../src/components/JsonlViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { parseJsonl, type JsonlRecord } from '@/lib/jsonl'\nimport { highlightCode } from '@/lib/highlight'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer JSON Lines (`.jsonl` / `.ndjson`) renderer (render mode). Receives the fetched text (the panel\n * loads it, like CsvViewer / SubtitleViewer), parses it into per-line records (see lib/jsonl.ts), and shows a\n * compact LIST — one truncated single-line preview per record. Clicking a row expands it to the pretty-printed,\n * syntax-highlighted value. Big files stay responsive two ways: each row's preview is a short single line, and\n * only an initial window of rows is in the DOM (a \"load more\" button grows it). The source toggle (header) shows\n * the raw file highlighted as JSON.\n */\nconst props = defineProps<{ text: string; name: string }>()\n\nconst PREVIEW_CHARS = 200 // truncate each row's inline preview so even a huge record is one short line\nconst INITIAL_ROWS = 200 // rows mounted initially; \"load more\" grows the window\nconst ROW_STEP = 400\n\nconst doc = computed(() => parseJsonl(props.text))\nconst shown = ref(INITIAL_ROWS)\nconst expanded = ref<Set<number>>(new Set())\n\n// A new file resets the window + any open rows.\nwatch(\n () => props.text,\n () => {\n shown.value = INITIAL_ROWS\n expanded.value = new Set()\n },\n)\n\nconst visible = computed(() => doc.value.records.slice(0, shown.value))\nconst remaining = computed(() => Math.max(0, doc.value.records.length - shown.value))\n\nfunction loadMore(): void {\n shown.value += ROW_STEP\n}\n\nfunction toggle(line: number): void {\n const s = new Set(expanded.value)\n if (s.has(line)) s.delete(line)\n else s.add(line)\n expanded.value = s\n}\n\n// Per-row copy: write the record's RAW line (the faithful on-disk text, not the re-stringified value) to the\n// clipboard, and flash a ✓ on that row. Mirrors the CodeBlock copy pattern (best-effort, cleared on a timer).\nconst copiedLine = ref<number | null>(null)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyRow(r: JsonlRecord): Promise<void> {\n try {\n await navigator.clipboard?.writeText(r.raw)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copiedLine.value = r.line\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copiedLine.value = null), 1400)\n}\nonBeforeUnmount(() => clearTimeout(copyTimer))\n\n// Compact one-line preview: stringify the parsed value (whitespace collapsed) or, for an error row, the raw\n// line — then hard-truncate. Building it from the value normalizes spacing so the list reads cleanly.\nfunction preview(r: JsonlRecord): string {\n let s: string\n if (r.error) s = r.raw\n else {\n try {\n s = JSON.stringify(r.value) ?? String(r.value)\n } catch {\n s = r.raw\n }\n }\n s = s.replace(/\\s+/g, ' ').trim()\n return s.length > PREVIEW_CHARS ? s.slice(0, PREVIEW_CHARS) + '…' : s\n}\n\n// Pretty value, highlighted as JSON — built lazily, only for an expanded row.\nfunction expandedHtml(r: JsonlRecord): string {\n if (r.error) return highlightCode(r.raw, 'json')\n try {\n return highlightCode(JSON.stringify(r.value, null, 2) ?? '', 'json')\n } catch {\n return highlightCode(r.raw, 'json')\n }\n}\n</script>\n\n<template>\n <div class=\"jl\">\n <div class=\"jl__summary\">\n <span class=\"jl__count\">{{ doc.records.length }} 条记录</span>\n <span v-if=\"doc.errorCount\" class=\"jl__errtag\">{{ doc.errorCount }} 行无法解析</span>\n </div>\n\n <div class=\"jl__list\">\n <p v-if=\"!doc.records.length\" class=\"jl__msg\">(空文件,无 JSON 记录)</p>\n\n <div v-for=\"r in visible\" :key=\"r.line\" class=\"jl__rowwrap\">\n <div\n class=\"jl__rowhead\"\n :class=\"{ 'jl__rowhead--open': expanded.has(r.line), 'jl__rowhead--err': r.error }\"\n >\n <button\n type=\"button\"\n class=\"jl__row\"\n :title=\"r.error ? `第 ${r.line} 行:JSON 无效` : `第 ${r.line} 行 —— 点击展开`\"\n @click=\"toggle(r.line)\"\n >\n <span class=\"jl__caret\" :class=\"{ 'jl__caret--open': expanded.has(r.line) }\">▶</span>\n <span class=\"jl__ln\">{{ r.line }}</span>\n <span v-if=\"r.error\" class=\"jl__badge\">无效</span>\n <span class=\"jl__preview\">{{ preview(r) }}</span>\n </button>\n <button\n type=\"button\"\n class=\"jl__copy\"\n :class=\"{ 'jl__copy--done': copiedLine === r.line }\"\n :title=\"copiedLine === r.line ? '已复制' : '复制此行'\"\n @click=\"copyRow(r)\"\n >\n <Icon :name=\"copiedLine === r.line ? 'check' : 'copy'\" :size=\"13\" />\n </button>\n </div>\n <pre\n v-if=\"expanded.has(r.line)\"\n class=\"jl__full\"\n ><code class=\"hljs\" v-html=\"expandedHtml(r)\" /></pre>\n </div>\n\n <button v-if=\"remaining\" type=\"button\" class=\"jl__more\" @click=\"loadMore\">\n 显示更多(还有 {{ remaining }} 条)\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.jl {\n width: 100%;\n min-width: 0;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background: var(--surface);\n}\n\n.jl__summary {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 6px 12px;\n flex-wrap: wrap;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n.jl__count {\n font-weight: 600;\n color: var(--accent);\n}\n.jl__errtag {\n padding: 1px 7px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--danger) 14%, var(--surface));\n color: var(--danger);\n font-size: 10.5px;\n}\n\n.jl__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n\n.jl__list {\n width: 100%;\n min-width: 0;\n flex: 1 1 auto;\n min-height: 0;\n box-sizing: border-box;\n overflow-x: hidden;\n overflow-y: scroll; /* always reserve the gutter so the draggable bar is present, not wheel-only */\n overscroll-behavior: contain;\n padding: 4px 0;\n /* Always-visible, draggable scrollbar — the record list gets long and an auto-hidden overlay bar reads\n * as \"no scrollbar\". Thin styled bar in Firefox; a classic (gutter-reserving) bar in Chromium/WebKit. */\n scrollbar-width: thin;\n scrollbar-color: var(--border-2) transparent;\n}\n.jl__list::-webkit-scrollbar {\n width: 12px;\n}\n.jl__list::-webkit-scrollbar-thumb {\n background: var(--border-2);\n background-clip: padding-box;\n border: 3px solid transparent;\n border-radius: 999px;\n}\n.jl__list::-webkit-scrollbar-thumb:hover {\n background: var(--muted);\n background-clip: padding-box;\n border: 3px solid transparent;\n}\n.jl__rowwrap {\n width: 100%;\n min-width: 0;\n max-width: 100%;\n overflow: hidden;\n box-sizing: border-box;\n border-bottom: 1px solid var(--border);\n}\n\n/* Row header = the clickable preview button + a trailing copy button (a button can't nest inside a button,\n * so they're siblings in this flex row; the hover/open/error tint lives here, spanning both). */\n.jl__rowhead {\n min-width: 0;\n display: flex;\n align-items: stretch;\n}\n.jl__rowhead:hover,\n.jl__rowhead--open {\n background: var(--surface-2);\n}\n\n.jl__row {\n display: flex;\n align-items: baseline;\n gap: 9px;\n flex: 1 1 0;\n min-width: 0;\n padding: 6px 14px;\n background: transparent;\n border: none;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n/* Trailing per-row copy button: faint by default, brightening on hover; accent flash when just copied. */\n.jl__copy {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n background: transparent;\n border: none;\n color: var(--faint);\n cursor: pointer;\n transition: color 0.15s;\n}\n.jl__copy:hover {\n color: var(--accent);\n}\n.jl__copy--done {\n color: var(--accent);\n}\n.jl__caret {\n flex-shrink: 0;\n width: 9px;\n font-size: 9px;\n color: var(--faint);\n transition: transform 0.12s;\n transform: rotate(0deg);\n align-self: center;\n}\n.jl__caret--open {\n transform: rotate(90deg);\n}\n.jl__ln {\n flex-shrink: 0;\n min-width: 30px;\n text-align: right;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n user-select: none;\n}\n.jl__badge {\n flex-shrink: 0;\n padding: 0 6px;\n border-radius: 5px;\n background: color-mix(in srgb, var(--danger) 16%, var(--surface));\n color: var(--danger);\n font-size: 9.5px;\n font-weight: 700;\n letter-spacing: 0.04em;\n align-self: center;\n}\n.jl__preview {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-family: var(--font-mono);\n font-size: 12.5px;\n color: var(--text);\n}\n.jl__rowhead--err .jl__preview {\n color: var(--muted);\n}\n\n.jl__full {\n width: 100%;\n min-width: 0;\n max-width: 100%;\n box-sizing: border-box;\n margin: 0;\n padding: 8px 14px 12px 32px;\n overflow: auto;\n background: var(--code-bg);\n font-family: var(--font-mono);\n font-size: 12px;\n line-height: 1.55;\n white-space: pre;\n tab-size: 2;\n}\n.jl__full code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n\n.jl__more {\n display: block;\n width: 100%;\n padding: 10px 16px;\n font-family: inherit;\n font-size: 12px;\n color: var(--accent);\n background: transparent;\n border: none;\n cursor: pointer;\n}\n.jl__more:hover {\n background: var(--surface-2);\n}\n</style>\n"],"mappings":""}