@l1yp/file-viewer 0.1.3 → 0.1.5
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.
- package/README.md +2 -0
- package/dist/components/AnsiText.d.ts +47 -0
- package/dist/components/AnsiText.js +41 -0
- package/dist/components/AnsiText.js.map +1 -0
- package/dist/components/ArchiveBrowser.js +1 -1
- package/dist/components/ArchiveBrowser.js.map +1 -1
- package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js +105 -94
- package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/CodeView.js +1 -1
- package/dist/components/CodeView.js.map +1 -1
- package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js +80 -71
- package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/DocxEmbeddedFiles.js +9 -0
- package/dist/components/DocxEmbeddedFiles.js.map +1 -0
- package/dist/components/DocxEmbeddedFiles.vue.d.ts +15 -0
- package/dist/components/DocxEmbeddedFiles.vue_vue_type_script_setup_true_lang.js +69 -0
- package/dist/components/DocxEmbeddedFiles.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DocxViewer.js +1 -1
- package/dist/components/DocxViewer.js.map +1 -1
- package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js +180 -50
- package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/FileViewerBody.js +1 -1
- package/dist/components/FileViewerBody.js.map +1 -1
- package/dist/components/FileViewerBody.vue.d.ts +1 -0
- package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js +117 -114
- package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/FileViewerPanel.js +1 -1
- package/dist/components/FileViewerPanel.js.map +1 -1
- package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js +18 -18
- package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ImageLightbox.js +1 -1
- package/dist/components/ImageLightbox.js.map +1 -1
- package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js +101 -55
- package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/LightboxGalleryStrip.js +9 -0
- package/dist/components/LightboxGalleryStrip.js.map +1 -0
- package/dist/components/LightboxGalleryStrip.vue.d.ts +12 -0
- package/dist/components/LightboxGalleryStrip.vue_vue_type_script_setup_true_lang.js +60 -0
- package/dist/components/LightboxGalleryStrip.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/LogViewer.js +1 -1
- package/dist/components/LogViewer.js.map +1 -1
- package/dist/components/LogViewer.vue.d.ts +3 -1
- package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js +284 -280
- package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/SevenZipBrowser.js +1 -1
- package/dist/components/SevenZipBrowser.js.map +1 -1
- package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js +157 -146
- package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ZipBrowser.js +1 -1
- package/dist/components/ZipBrowser.js.map +1 -1
- package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js +70 -61
- package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ZoomImage.js +1 -1
- package/dist/components/ZoomImage.js.map +1 -1
- package/dist/components/ZoomImage.vue.d.ts +2 -0
- package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js +94 -85
- package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/composables/useArchiveImageGallery.d.ts +15 -0
- package/dist/composables/useArchiveImageGallery.js +62 -0
- package/dist/composables/useArchiveImageGallery.js.map +1 -0
- package/dist/composables/useLightbox.d.ts +29 -21
- package/dist/composables/useLightbox.js +72 -12
- package/dist/composables/useLightbox.js.map +1 -1
- package/dist/lib/ansi.d.ts +44 -0
- package/dist/lib/ansi.js +224 -0
- package/dist/lib/ansi.js.map +1 -0
- package/dist/lib/archiveImages.d.ts +14 -0
- package/dist/lib/archiveImages.js +15 -0
- package/dist/lib/archiveImages.js.map +1 -0
- package/dist/lib/cfb.d.ts +37 -0
- package/dist/lib/cfb.js +112 -0
- package/dist/lib/cfb.js.map +1 -0
- package/dist/lib/docxEmbeddedDom.d.ts +22 -0
- package/dist/lib/docxEmbeddedDom.js +128 -0
- package/dist/lib/docxEmbeddedDom.js.map +1 -0
- package/dist/lib/docxEmbeddings.d.ts +41 -0
- package/dist/lib/docxEmbeddings.js +226 -0
- package/dist/lib/docxEmbeddings.js.map +1 -0
- package/dist/lib/msi.js +62 -154
- package/dist/lib/msi.js.map +1 -1
- package/dist/lib/olePackage.d.ts +8 -0
- package/dist/lib/olePackage.js +37 -0
- package/dist/lib/olePackage.js.map +1 -0
- package/dist/lib/pptxFirstSlide.d.ts +11 -0
- package/dist/lib/pptxFirstSlide.js +34 -0
- package/dist/lib/pptxFirstSlide.js.map +1 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ArchiveBrowser.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/ArchiveBrowser.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n computed,\n defineAsyncComponent,\n onBeforeUnmount,\n onMounted,\n reactive,\n ref,\n watch,\n} from 'vue'\nimport type { ArchiveEntry, OpenedArchive } from '@/lib/archive'\nimport type { DicomSlice } from '@/lib/dicom'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { fileExt, isDicomFile, isTextFile } from '@/lib/fileType'\nimport { canToggleView, classifyViewerFile, mediaMimeForName } from '@/lib/viewerKind'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n// A folder of sibling .dcm files browses as one scrollable series — handled directly (the body has no slice model).\nconst DicomViewer = defineAsyncComponent(() => import('@/components/DicomViewer.vue'))\n// The shared side-viewer body renders a selected entry with the SAME viewers as a top-level file. Lazy to break\n// the module cycle (the body lazy-imports this browser back, for nested archives).\nconst FileViewerBody = defineAsyncComponent(() => import('@/components/FileViewerBody.vue'))\n\n/**\n * The side viewer's generic archive mode — the multi-file browser shared by the tar family (tar/gz/xz/bz2) and\n * rar. Same master→detail UX as {@link ZipBrowser}: a collapsible file tree; selecting an entry extracts just\n * that one, wraps it in a blob URL, and hands it to {@link FileViewerBody} (so a file inside the archive\n * previews with every viewer a top-level file gets).\n *\n * The format-specific work lives behind {@link OpenedArchive}: this component fetches the bytes, lazy-imports\n * the right opener by `kind` (so neither the tar codecs nor the rar WASM ship in the main bundle), and drives\n * everything through `entries` + `extract()`. Extraction may be sync (tar) or async (rar) — every call is awaited.\n */\nconst props = defineProps<{ url: string; name: string; kind: 'tar' | 'rar' }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nlet archive: OpenedArchive | null = null\nconst entries = ref<ArchiveEntry[]>([])\n\ninterface TreeNode {\n name: string\n path: string\n isDir: boolean\n size: number\n entry?: ArchiveEntry\n children: TreeNode[]\n}\nconst root = ref<TreeNode>({ name: '', path: '', isDir: true, size: 0, children: [] })\nconst expanded = reactive(new Set<string>())\n\nconst fileCount = computed(() => entries.value.reduce((n, e) => n + (e.isDir ? 0 : 1), 0))\nconst totalSize = computed(() => entries.value.reduce((s, e) => s + (e.isDir ? 0 : e.size), 0))\n\n// Build a folder tree from the flat entry paths (directories may be implicit — derive them either way).\nfunction buildTree(list: ArchiveEntry[]): TreeNode {\n const rootNode: TreeNode = { name: '', path: '', isDir: true, size: 0, children: [] }\n const dirs = new Map<string, TreeNode>([['', rootNode]])\n const ensureDir = (segs: string[]): TreeNode => {\n let cur = rootNode\n let acc = ''\n for (const seg of segs) {\n acc = acc ? `${acc}/${seg}` : seg\n let node = dirs.get(acc)\n if (!node) {\n node = { name: seg, path: acc, isDir: true, size: 0, children: [] }\n dirs.set(acc, node)\n cur.children.push(node)\n }\n cur = node\n }\n return cur\n }\n for (const e of list) {\n const segs = e.path.split('/').filter(Boolean)\n if (e.isDir) {\n ensureDir(segs)\n continue\n }\n const fname = segs.pop()\n if (!fname) continue\n ensureDir(segs).children.push({\n name: fname,\n path: e.path,\n isDir: false,\n size: e.size,\n entry: e,\n children: [],\n })\n }\n sortNode(rootNode)\n return rootNode\n}\n// Folders first, then files, each alphabetical (case-insensitive).\nfunction sortNode(n: TreeNode): void {\n n.children.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))\n for (const c of n.children) if (c.isDir) sortNode(c)\n}\n\n// Flatten the tree to the rows currently visible (respecting which folders are expanded).\ninterface Row {\n node: TreeNode\n depth: number\n}\nconst rows = computed<Row[]>(() => {\n const out: Row[] = []\n const walk = (nodes: TreeNode[], depth: number): void => {\n for (const n of nodes) {\n out.push({ node: n, depth })\n if (n.isDir && expanded.has(n.path)) walk(n.children, depth + 1)\n }\n }\n walk(root.value.children, 0)\n return out\n})\n\nasync function load(): Promise<void> {\n status.value = 'loading'\n errorMsg.value = ''\n selected.value = null\n archive = null\n revokeBlob()\n stackSlices.value = []\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buffer = await res.arrayBuffer()\n // Lazy-import the opener so the tar codecs / rar WASM stay out of the main bundle until needed.\n const opened =\n props.kind === 'rar'\n ? await (await import('@/lib/rar')).openRar(buffer)\n : await (await import('@/lib/tarball')).openTarball(buffer, props.name)\n if (props.url !== url) return // a newer file superseded us while decoding\n archive = opened\n entries.value = opened.entries\n root.value = buildTree(opened.entries)\n expanded.clear()\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// ── selected entry (detail view) ──────────────────────────────────────────────────────────────\nconst selected = ref<ArchiveEntry | null>(null)\nconst entryStatus = ref<Status>('ready')\nconst entryErr = ref('')\nconst entryText = ref('')\nconst entryFile = ref<ViewerFile | null>(null)\nconst entryStack = ref(false)\nconst entryMode = ref<'render' | 'source'>('render')\nconst blobUrl = ref('')\n\n// DICOM series stack: a folder of .dcm slices browsed as one scrollable series (see ZipBrowser).\nconst stackSlices = ref<DicomSlice[]>([])\nconst stackStart = ref(0)\nconst stackName = ref('')\nconst stackTotalSize = computed(() => stackSlices.value.reduce((s, sl) => s + (sl.size ?? 0), 0))\n\nconst canToggle = computed(() => canToggleView(entryFile.value, entryText.value))\n\nfunction revokeBlob(): void {\n if (blobUrl.value) {\n URL.revokeObjectURL(blobUrl.value)\n blobUrl.value = ''\n }\n}\n\nfunction dirOf(path: string): string {\n const i = path.lastIndexOf('/')\n return i < 0 ? '' : path.slice(0, i)\n}\n// Sibling .dcm files in the same folder, natural-sorted (numeric-aware) so e.g. img2 precedes img10.\nfunction dicomSiblings(e: ArchiveEntry): ArchiveEntry[] {\n const dir = dirOf(e.path)\n return entries.value\n .filter((x) => !x.isDir && isDicomFile(x.name) && dirOf(x.path) === dir)\n .sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true, sensitivity: 'base' }))\n}\n\n// Treat as text if the name says so; for extensionless entries, sniff the first bytes for NULs.\nfunction looksText(bytes: Uint8Array, name: string): boolean {\n if (isTextFile(name)) return true\n if (fileExt(name)) return false\n const n = Math.min(bytes.length, 4096)\n for (let i = 0; i < n; i++) if (bytes[i] === 0) return false\n return true\n}\n\n// Copy out a freshly-allocated ArrayBuffer (a nested viewer/blob must own its bytes, not alias the archive).\nfunction toArrayBuffer(u: Uint8Array): ArrayBuffer {\n const out = new ArrayBuffer(u.byteLength)\n new Uint8Array(out).set(u)\n return out\n}\n\nasync function selectEntry(e: ArchiveEntry): Promise<void> {\n selected.value = e\n entryStatus.value = 'loading'\n entryErr.value = ''\n entryText.value = ''\n entryFile.value = null\n entryStack.value = false\n entryMode.value = 'render'\n revokeBlob()\n stackSlices.value = []\n if (!archive) return\n const arc = archive\n try {\n if (isDicomFile(e.name)) {\n const siblings = dicomSiblings(e)\n if (siblings.length > 1) {\n stackSlices.value = siblings.map((se) => ({\n name: se.name,\n size: se.size,\n load: async () => toArrayBuffer(await arc.extract(se)),\n }))\n stackStart.value = Math.max(\n 0,\n siblings.findIndex((se) => se.path === e.path),\n )\n stackName.value = dirOf(e.path).split('/').pop() || e.name\n entryStack.value = true\n entryStatus.value = 'ready'\n return\n }\n }\n const bytes = await arc.extract(e)\n if (selected.value !== e) return // a newer selection superseded us mid-extract\n blobUrl.value = URL.createObjectURL(new Blob([bytes], { type: mediaMimeForName(e.name) }))\n entryFile.value = classifyViewerFile({\n name: e.name,\n url: blobUrl.value,\n size: e.size,\n textHint: looksText(bytes, e.name),\n })\n entryStatus.value = 'ready'\n } catch (err) {\n entryErr.value = err instanceof Error ? err.message : String(err)\n entryStatus.value = 'error'\n }\n}\n\nfunction onRow(node: TreeNode): void {\n if (node.isDir) {\n if (expanded.has(node.path)) expanded.delete(node.path)\n else expanded.add(node.path)\n } else if (node.entry) {\n void selectEntry(node.entry)\n }\n}\n\nfunction back(): void {\n selected.value = null\n entryText.value = ''\n entryFile.value = null\n entryStack.value = false\n revokeBlob()\n stackSlices.value = []\n}\n\nasync function downloadEntry(e: ArchiveEntry): Promise<void> {\n if (!archive) return\n try {\n const url = URL.createObjectURL(new Blob([await archive.extract(e)]))\n const a = document.createElement('a')\n a.href = url\n a.download = e.name\n document.body.appendChild(a)\n a.click()\n a.remove()\n setTimeout(() => URL.revokeObjectURL(url), 1000)\n } catch {\n /* extraction failed — nothing to download */\n }\n}\n\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyEntry(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(entryText.value)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = false), 1400)\n}\n\nonMounted(load)\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n revokeBlob()\n clearTimeout(copyTimer)\n})\n</script>\n\n<template>\n <div class=\"ar\">\n <p v-if=\"status === 'loading'\" class=\"ar__msg\">正在解压…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"ar__msg ar__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"ar__retry\" @click=\"load\">重试</button>\n </div>\n\n <!-- detail: one selected entry -->\n <template v-else-if=\"selected\">\n <div class=\"ar__bar\">\n <button type=\"button\" class=\"ar__back\" title=\"返回列表\" @click=\"back\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"ar__path\" :title=\"selected.path\">{{ selected.path }}</span>\n <span class=\"ar__bsize\">{{ formatBytes(selected.size) }}</span>\n <div v-if=\"canToggle\" class=\"ar__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"ar__vbtn\"\n :class=\"{ 'ar__vbtn--on': entryMode === 'render' }\"\n title=\"渲染\"\n @click=\"entryMode = 'render'\"\n >\n <Icon name=\"eye\" :size=\"14\" />\n </button>\n <button\n type=\"button\"\n class=\"ar__vbtn\"\n :class=\"{ 'ar__vbtn--on': entryMode === 'source' }\"\n title=\"源码\"\n @click=\"entryMode = 'source'\"\n >\n <Icon name=\"code\" :size=\"14\" />\n </button>\n </div>\n <button\n v-if=\"entryText && entryStatus === 'ready'\"\n type=\"button\"\n class=\"ar__act\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"copyEntry\"\n >\n <Icon name=\"copy\" :size=\"14\" />\n </button>\n <button type=\"button\" class=\"ar__act\" title=\"下载\" @click=\"downloadEntry(selected)\">\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"entryStatus === 'loading'\" class=\"ar__msg\">读取中…</p>\n <div v-else-if=\"entryStatus === 'error'\" class=\"ar__msg ar__msg--error\">\n <p>{{ entryErr }}</p>\n </div>\n\n <!-- dicom series: a whole folder of .dcm slices browsed as one scrollable series -->\n <DicomViewer\n v-else-if=\"entryStack\"\n :slices=\"stackSlices\"\n :start=\"stackStart\"\n :name=\"stackName\"\n :size=\"stackTotalSize\"\n />\n\n <!-- everything else: the shared body picks the right viewer from the extracted blob -->\n <FileViewerBody\n v-else-if=\"entryFile\"\n :file=\"entryFile\"\n :mode=\"entryMode\"\n @loaded=\"entryText = $event\"\n />\n </template>\n\n <!-- list: the file tree -->\n <template v-else>\n <div class=\"ar__summary\">{{ fileCount }} 个文件 · {{ formatBytes(totalSize) }}</div>\n <div class=\"ar__scroll ar__tree\">\n <p v-if=\"rows.length === 0\" class=\"ar__msg\">空压缩包</p>\n <button\n v-for=\"row in rows\"\n :key=\"row.node.path\"\n type=\"button\"\n class=\"ar__row\"\n :class=\"{ 'ar__row--dir': row.node.isDir }\"\n :style=\"{ paddingLeft: `${12 + row.depth * 14}px` }\"\n @click=\"onRow(row.node)\"\n >\n <span class=\"ar__twisty\">\n <Icon\n v-if=\"row.node.isDir\"\n name=\"chevronRight\"\n :size=\"13\"\n :class=\"{ 'ar__twisty--open': expanded.has(row.node.path) }\"\n />\n </span>\n <span class=\"ar__rowicon\">\n <Icon :name=\"row.node.isDir ? 'folder' : 'fileOutline'\" :size=\"15\" />\n </span>\n <span class=\"ar__rowname\">{{ row.node.name }}</span>\n <span v-if=\"!row.node.isDir\" class=\"ar__rowsize\">{{ formatBytes(row.node.size) }}</span>\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.ar {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.ar__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.ar__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.ar__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.ar__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* List view ------------------------------------------------------------------------------------ */\n.ar__summary {\n flex-shrink: 0;\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.ar__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.ar__tree {\n padding: 6px 0;\n}\n.ar__row {\n display: flex;\n align-items: center;\n gap: 7px;\n width: 100%;\n padding: 5px 12px 5px 12px;\n background: transparent;\n border: none;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n text-align: left;\n cursor: pointer;\n}\n.ar__row:hover {\n background: var(--surface-2);\n}\n.ar__twisty {\n flex-shrink: 0;\n display: inline-flex;\n width: 13px;\n color: var(--faint);\n}\n.ar__twisty--open {\n transform: rotate(90deg);\n transition: transform 0.12s;\n}\n.ar__rowicon {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--faint);\n}\n.ar__row--dir .ar__rowicon {\n color: var(--accent);\n}\n.ar__rowname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ar__row--dir .ar__rowname {\n font-weight: 500;\n}\n.ar__rowsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n/* Detail bar ----------------------------------------------------------------------------------- */\n.ar__bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 7px 10px 7px 8px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.ar__back,\n.ar__act {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 30px;\n height: 28px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--muted);\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.ar__back:hover,\n.ar__act:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.ar__path {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n text-align: left;\n font-size: 12.5px;\n font-weight: 500;\n color: var(--text-strong);\n}\n.ar__bsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.ar__view {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 7px;\n background: var(--surface-2);\n}\n.ar__vbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 24px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 5px;\n color: var(--faint);\n cursor: pointer;\n}\n.ar__vbtn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ArchiveBrowser.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/ArchiveBrowser.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n computed,\n defineAsyncComponent,\n onBeforeUnmount,\n onMounted,\n reactive,\n ref,\n watch,\n} from 'vue'\nimport type { ArchiveEntry, OpenedArchive } from '@/lib/archive'\nimport type { DicomSlice } from '@/lib/dicom'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { useArchiveImageGallery } from '@/composables/useArchiveImageGallery'\nimport { fileExt, isDicomFile, isTextFile } from '@/lib/fileType'\nimport { canToggleView, classifyViewerFile, mediaMimeForName } from '@/lib/viewerKind'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n// A folder of sibling .dcm files browses as one scrollable series — handled directly (the body has no slice model).\nconst DicomViewer = defineAsyncComponent(() => import('@/components/DicomViewer.vue'))\n// The shared side-viewer body renders a selected entry with the SAME viewers as a top-level file. Lazy to break\n// the module cycle (the body lazy-imports this browser back, for nested archives).\nconst FileViewerBody = defineAsyncComponent(() => import('@/components/FileViewerBody.vue'))\n\n/**\n * The side viewer's generic archive mode — the multi-file browser shared by the tar family (tar/gz/xz/bz2) and\n * rar. Same master→detail UX as {@link ZipBrowser}: a collapsible file tree; selecting an entry extracts just\n * that one, wraps it in a blob URL, and hands it to {@link FileViewerBody} (so a file inside the archive\n * previews with every viewer a top-level file gets).\n *\n * The format-specific work lives behind {@link OpenedArchive}: this component fetches the bytes, lazy-imports\n * the right opener by `kind` (so neither the tar codecs nor the rar WASM ship in the main bundle), and drives\n * everything through `entries` + `extract()`. Extraction may be sync (tar) or async (rar) — every call is awaited.\n */\nconst props = defineProps<{ url: string; name: string; kind: 'tar' | 'rar' }>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nlet archive: OpenedArchive | null = null\nconst entries = ref<ArchiveEntry[]>([])\n\ninterface TreeNode {\n name: string\n path: string\n isDir: boolean\n size: number\n entry?: ArchiveEntry\n children: TreeNode[]\n}\nconst root = ref<TreeNode>({ name: '', path: '', isDir: true, size: 0, children: [] })\nconst expanded = reactive(new Set<string>())\n\nconst fileCount = computed(() => entries.value.reduce((n, e) => n + (e.isDir ? 0 : 1), 0))\nconst totalSize = computed(() => entries.value.reduce((s, e) => s + (e.isDir ? 0 : e.size), 0))\n\n// Build a folder tree from the flat entry paths (directories may be implicit — derive them either way).\nfunction buildTree(list: ArchiveEntry[]): TreeNode {\n const rootNode: TreeNode = { name: '', path: '', isDir: true, size: 0, children: [] }\n const dirs = new Map<string, TreeNode>([['', rootNode]])\n const ensureDir = (segs: string[]): TreeNode => {\n let cur = rootNode\n let acc = ''\n for (const seg of segs) {\n acc = acc ? `${acc}/${seg}` : seg\n let node = dirs.get(acc)\n if (!node) {\n node = { name: seg, path: acc, isDir: true, size: 0, children: [] }\n dirs.set(acc, node)\n cur.children.push(node)\n }\n cur = node\n }\n return cur\n }\n for (const e of list) {\n const segs = e.path.split('/').filter(Boolean)\n if (e.isDir) {\n ensureDir(segs)\n continue\n }\n const fname = segs.pop()\n if (!fname) continue\n ensureDir(segs).children.push({\n name: fname,\n path: e.path,\n isDir: false,\n size: e.size,\n entry: e,\n children: [],\n })\n }\n sortNode(rootNode)\n return rootNode\n}\n// Folders first, then files, each alphabetical (case-insensitive).\nfunction sortNode(n: TreeNode): void {\n n.children.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))\n for (const c of n.children) if (c.isDir) sortNode(c)\n}\n\n// Flatten the tree to the rows currently visible (respecting which folders are expanded).\ninterface Row {\n node: TreeNode\n depth: number\n}\nconst rows = computed<Row[]>(() => {\n const out: Row[] = []\n const walk = (nodes: TreeNode[], depth: number): void => {\n for (const n of nodes) {\n out.push({ node: n, depth })\n if (n.isDir && expanded.has(n.path)) walk(n.children, depth + 1)\n }\n }\n walk(root.value.children, 0)\n return out\n})\n\nasync function load(): Promise<void> {\n imageGallery.clear()\n status.value = 'loading'\n errorMsg.value = ''\n selected.value = null\n archive = null\n revokeBlob()\n stackSlices.value = []\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buffer = await res.arrayBuffer()\n // Lazy-import the opener so the tar codecs / rar WASM stay out of the main bundle until needed.\n const opened =\n props.kind === 'rar'\n ? await (await import('@/lib/rar')).openRar(buffer)\n : await (await import('@/lib/tarball')).openTarball(buffer, props.name)\n if (props.url !== url) return // a newer file superseded us while decoding\n archive = opened\n entries.value = opened.entries\n root.value = buildTree(opened.entries)\n expanded.clear()\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// ── selected entry (detail view) ──────────────────────────────────────────────────────────────\nconst selected = ref<ArchiveEntry | null>(null)\nconst entryStatus = ref<Status>('ready')\nconst entryErr = ref('')\nconst entryText = ref('')\nconst entryFile = ref<ViewerFile | null>(null)\nconst entryStack = ref(false)\nconst entryMode = ref<'render' | 'source'>('render')\nconst blobUrl = ref('')\n\n// DICOM series stack: a folder of .dcm slices browsed as one scrollable series (see ZipBrowser).\nconst stackSlices = ref<DicomSlice[]>([])\nconst stackStart = ref(0)\nconst stackName = ref('')\nconst stackTotalSize = computed(() => stackSlices.value.reduce((s, sl) => s + (sl.size ?? 0), 0))\n\nconst canToggle = computed(() => canToggleView(entryFile.value, entryText.value))\n\nfunction revokeBlob(): void {\n if (blobUrl.value) {\n URL.revokeObjectURL(blobUrl.value)\n blobUrl.value = ''\n }\n}\n\nfunction dirOf(path: string): string {\n const i = path.lastIndexOf('/')\n return i < 0 ? '' : path.slice(0, i)\n}\n// Sibling .dcm files in the same folder, natural-sorted (numeric-aware) so e.g. img2 precedes img10.\nfunction dicomSiblings(e: ArchiveEntry): ArchiveEntry[] {\n const dir = dirOf(e.path)\n return entries.value\n .filter((x) => !x.isDir && isDicomFile(x.name) && dirOf(x.path) === dir)\n .sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true, sensitivity: 'base' }))\n}\n\n// Treat as text if the name says so; for extensionless entries, sniff the first bytes for NULs.\nfunction looksText(bytes: Uint8Array, name: string): boolean {\n if (isTextFile(name)) return true\n if (fileExt(name)) return false\n const n = Math.min(bytes.length, 4096)\n for (let i = 0; i < n; i++) if (bytes[i] === 0) return false\n return true\n}\n\nconst imageGallery = useArchiveImageGallery<ArchiveEntry>({\n entries: () => entries.value,\n archiveName: () => props.name,\n extract: async (entry) => {\n if (!archive) throw new Error('压缩包尚未加载')\n return archive.extract(entry)\n },\n})\n\n// Copy out a freshly-allocated ArrayBuffer (a nested viewer/blob must own its bytes, not alias the archive).\nfunction toArrayBuffer(u: Uint8Array): ArrayBuffer {\n const out = new ArrayBuffer(u.byteLength)\n new Uint8Array(out).set(u)\n return out\n}\n\nasync function selectEntry(e: ArchiveEntry): Promise<void> {\n selected.value = e\n entryStatus.value = 'loading'\n entryErr.value = ''\n entryText.value = ''\n entryFile.value = null\n entryStack.value = false\n entryMode.value = 'render'\n revokeBlob()\n stackSlices.value = []\n if (!archive) return\n const arc = archive\n try {\n if (isDicomFile(e.name)) {\n const siblings = dicomSiblings(e)\n if (siblings.length > 1) {\n stackSlices.value = siblings.map((se) => ({\n name: se.name,\n size: se.size,\n load: async () => toArrayBuffer(await arc.extract(se)),\n }))\n stackStart.value = Math.max(\n 0,\n siblings.findIndex((se) => se.path === e.path),\n )\n stackName.value = dirOf(e.path).split('/').pop() || e.name\n entryStack.value = true\n entryStatus.value = 'ready'\n return\n }\n }\n const bytes = await arc.extract(e)\n if (selected.value !== e) return // a newer selection superseded us mid-extract\n blobUrl.value = URL.createObjectURL(new Blob([bytes], { type: mediaMimeForName(e.name) }))\n const file = classifyViewerFile({\n name: e.name,\n url: blobUrl.value,\n size: e.size,\n textHint: looksText(bytes, e.name),\n })\n entryFile.value = file\n entryStatus.value = 'ready'\n if (file.image) void imageGallery.open(e, bytes)\n } catch (err) {\n entryErr.value = err instanceof Error ? err.message : String(err)\n entryStatus.value = 'error'\n }\n}\n\nfunction onRow(node: TreeNode): void {\n if (node.isDir) {\n if (expanded.has(node.path)) expanded.delete(node.path)\n else expanded.add(node.path)\n } else if (node.entry) {\n void selectEntry(node.entry)\n }\n}\n\nfunction back(): void {\n selected.value = null\n entryText.value = ''\n entryFile.value = null\n entryStack.value = false\n revokeBlob()\n stackSlices.value = []\n}\n\nasync function downloadEntry(e: ArchiveEntry): Promise<void> {\n if (!archive) return\n try {\n const url = URL.createObjectURL(new Blob([await archive.extract(e)]))\n const a = document.createElement('a')\n a.href = url\n a.download = e.name\n document.body.appendChild(a)\n a.click()\n a.remove()\n setTimeout(() => URL.revokeObjectURL(url), 1000)\n } catch {\n /* extraction failed — nothing to download */\n }\n}\n\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyEntry(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(entryText.value)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = false), 1400)\n}\n\nonMounted(load)\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n revokeBlob()\n clearTimeout(copyTimer)\n})\n</script>\n\n<template>\n <div class=\"ar\">\n <p v-if=\"status === 'loading'\" class=\"ar__msg\">正在解压…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"ar__msg ar__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"ar__retry\" @click=\"load\">重试</button>\n </div>\n\n <!-- detail: one selected entry -->\n <template v-else-if=\"selected\">\n <div class=\"ar__bar\">\n <button type=\"button\" class=\"ar__back\" title=\"返回列表\" @click=\"back\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"ar__path\" :title=\"selected.path\">{{ selected.path }}</span>\n <span class=\"ar__bsize\">{{ formatBytes(selected.size) }}</span>\n <div v-if=\"canToggle\" class=\"ar__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"ar__vbtn\"\n :class=\"{ 'ar__vbtn--on': entryMode === 'render' }\"\n title=\"渲染\"\n @click=\"entryMode = 'render'\"\n >\n <Icon name=\"eye\" :size=\"14\" />\n </button>\n <button\n type=\"button\"\n class=\"ar__vbtn\"\n :class=\"{ 'ar__vbtn--on': entryMode === 'source' }\"\n title=\"源码\"\n @click=\"entryMode = 'source'\"\n >\n <Icon name=\"code\" :size=\"14\" />\n </button>\n </div>\n <button\n v-if=\"entryText && entryStatus === 'ready'\"\n type=\"button\"\n class=\"ar__act\"\n :title=\"copied ? '已复制' : '复制'\"\n @click=\"copyEntry\"\n >\n <Icon name=\"copy\" :size=\"14\" />\n </button>\n <button type=\"button\" class=\"ar__act\" title=\"下载\" @click=\"downloadEntry(selected)\">\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"entryStatus === 'loading'\" class=\"ar__msg\">读取中…</p>\n <div v-else-if=\"entryStatus === 'error'\" class=\"ar__msg ar__msg--error\">\n <p>{{ entryErr }}</p>\n </div>\n\n <!-- dicom series: a whole folder of .dcm slices browsed as one scrollable series -->\n <DicomViewer\n v-else-if=\"entryStack\"\n :slices=\"stackSlices\"\n :start=\"stackStart\"\n :name=\"stackName\"\n :size=\"stackTotalSize\"\n />\n\n <!-- everything else: the shared body picks the right viewer from the extracted blob -->\n <FileViewerBody\n v-else-if=\"entryFile\"\n :file=\"entryFile\"\n :mode=\"entryMode\"\n @loaded=\"entryText = $event\"\n />\n </template>\n\n <!-- list: the file tree -->\n <template v-else>\n <div class=\"ar__summary\">{{ fileCount }} 个文件 · {{ formatBytes(totalSize) }}</div>\n <div class=\"ar__scroll ar__tree\">\n <p v-if=\"rows.length === 0\" class=\"ar__msg\">空压缩包</p>\n <button\n v-for=\"row in rows\"\n :key=\"row.node.path\"\n type=\"button\"\n class=\"ar__row\"\n :class=\"{ 'ar__row--dir': row.node.isDir }\"\n :style=\"{ paddingLeft: `${12 + row.depth * 14}px` }\"\n @click=\"onRow(row.node)\"\n >\n <span class=\"ar__twisty\">\n <Icon\n v-if=\"row.node.isDir\"\n name=\"chevronRight\"\n :size=\"13\"\n :class=\"{ 'ar__twisty--open': expanded.has(row.node.path) }\"\n />\n </span>\n <span class=\"ar__rowicon\">\n <Icon :name=\"row.node.isDir ? 'folder' : 'fileOutline'\" :size=\"15\" />\n </span>\n <span class=\"ar__rowname\">{{ row.node.name }}</span>\n <span v-if=\"!row.node.isDir\" class=\"ar__rowsize\">{{ formatBytes(row.node.size) }}</span>\n </button>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.ar {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.ar__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.ar__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.ar__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.ar__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* List view ------------------------------------------------------------------------------------ */\n.ar__summary {\n flex-shrink: 0;\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.ar__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.ar__tree {\n padding: 6px 0;\n}\n.ar__row {\n display: flex;\n align-items: center;\n gap: 7px;\n width: 100%;\n padding: 5px 12px 5px 12px;\n background: transparent;\n border: none;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n text-align: left;\n cursor: pointer;\n}\n.ar__row:hover {\n background: var(--surface-2);\n}\n.ar__twisty {\n flex-shrink: 0;\n display: inline-flex;\n width: 13px;\n color: var(--faint);\n}\n.ar__twisty--open {\n transform: rotate(90deg);\n transition: transform 0.12s;\n}\n.ar__rowicon {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--faint);\n}\n.ar__row--dir .ar__rowicon {\n color: var(--accent);\n}\n.ar__rowname {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ar__row--dir .ar__rowname {\n font-weight: 500;\n}\n.ar__rowsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n/* Detail bar ----------------------------------------------------------------------------------- */\n.ar__bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 7px 10px 7px 8px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.ar__back,\n.ar__act {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 30px;\n height: 28px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--muted);\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.ar__back:hover,\n.ar__act:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.ar__path {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n direction: rtl;\n text-align: left;\n font-size: 12.5px;\n font-weight: 500;\n color: var(--text-strong);\n}\n.ar__bsize {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.ar__view {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 7px;\n background: var(--surface-2);\n}\n.ar__vbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 24px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 5px;\n color: var(--faint);\n cursor: pointer;\n}\n.ar__vbtn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\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 "./CodeView.vue_vue_type_script_setup_true_lang.js";
|
|
3
3
|
/* empty css */
|
|
4
4
|
//#region src/components/CodeView.vue
|
|
5
|
-
var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-
|
|
5
|
+
var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-b7b2f434"]]);
|
|
6
6
|
//#endregion
|
|
7
7
|
export { n as default };
|
|
8
8
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeView.js","names":[],"sources":["../../src/components/CodeView.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'\nimport { escapeHtml, highlightCode, highlightLines } from '@/lib/highlight'\nimport { computeFoldRegions } from '@/lib/codefold'\nimport { canFormat, formatCode } from '@/lib/codeformat'\nimport { fileExt } from '@/lib/fileType'\nimport { langLabel, searchLangs } from '@/lib/highlightLangs'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's source-code view: a line-number gutter + syntax-highlighted code, with indentation-based\n * code FOLDING (click a chevron to collapse a block; see lib/codefold.ts), on-demand Prettier FORMATTING (the\n * 格式化 button, shown only for languages Prettier can parse — see lib/codeformat.ts; loaded lazily), and a\n * manual HIGHLIGHT-LANGUAGE picker (the 语言 button — auto-detection by extension/mime is often empty or wrong,\n * so any file can be re-colorized as JSON / XML / Python / … or forced to plain text; see lib/highlightLangs).\n * Used for every non-rendered file (plain text, code, or markdown/html/svg viewed as source). Highlighting is\n * split per line (see highlightLines) so rows can be hidden individually.\n *\n * Very large files (> {@link MAX_FOLD_LINES}) skip the per-line DOM and fall back to a single highlighted block\n * — folding a 10k-line file isn't worth tens of thousands of row elements, and such files mostly reach the\n * streaming log viewer anyway (see isLogFile).\n */\nconst props = defineProps<{ code: string; lang: string; name?: string }>()\n\nconst MAX_FOLD_LINES = 4000\n\n// Manual highlight-language override (null = use the auto-detected `props.lang`; '' = forced plain text). The\n// effective language feeds highlighting AND the 格式化 button (so \"force JSON, then format\" works). Reset when a\n// different file / detected language arrives (see the reset watch below).\nconst langOverride = ref<string | null>(null)\nconst effLang = computed(() => langOverride.value ?? props.lang)\n\n// Formatted output (null = showing the original). The view always renders `displayCode`.\nconst formatted = ref<string | null>(null)\nconst formatting = ref(false)\nconst formatErr = ref(false)\nconst displayCode = computed(() => formatted.value ?? props.code)\n\nconst ext = computed(() => fileExt(props.name ?? ''))\nconst canFmt = computed(() => canFormat(ext.value, effLang.value))\n\nconst rawLines = computed(() => displayCode.value.split('\\n'))\nconst lineCount = computed(() => rawLines.value.length)\nconst foldable = computed(() => lineCount.value <= MAX_FOLD_LINES)\n\nconst lineHtml = computed(() => highlightLines(displayCode.value, effLang.value))\nconst regions = computed(() => computeFoldRegions(displayCode.value))\nconst folded = reactive(new Set<number>())\n\n// A new file (or a re-detected language) resets the formatting toggle AND the manual language override; any\n// change to the shown code clears stale fold state.\nwatch(\n () => [props.code, props.lang],\n () => {\n formatted.value = null\n formatErr.value = false\n langOverride.value = null\n },\n)\nwatch(displayCode, () => folded.clear())\n\n// ── highlight-language picker ────────────────────────────────────────────────────────────────────\nconst langMenuOpen = ref(false)\nconst langMenuRef = ref<HTMLElement | null>(null)\nconst langQuery = ref('')\nconst langResults = computed(() => searchLangs(langQuery.value))\n\nfunction chooseLang(id: string): void {\n langOverride.value = id\n langMenuOpen.value = false\n langQuery.value = ''\n}\n\n// Close the menu on an outside click or Esc — listeners live only while it's open (SSR-guarded). The toggle\n// button + popup both sit inside langMenuRef, so a click on either is \"inside\" and never self-closes.\nfunction onLangDocPointer(e: MouseEvent): void {\n if (langMenuRef.value && !langMenuRef.value.contains(e.target as Node)) langMenuOpen.value = false\n}\nfunction onLangKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') langMenuOpen.value = false\n}\nwatch(langMenuOpen, (on) => {\n if (typeof document === 'undefined') return\n if (on) {\n document.addEventListener('click', onLangDocPointer)\n document.addEventListener('keydown', onLangKey)\n } else {\n document.removeEventListener('click', onLangDocPointer)\n document.removeEventListener('keydown', onLangKey)\n }\n})\nonBeforeUnmount(() => {\n if (typeof document === 'undefined') return\n document.removeEventListener('click', onLangDocPointer)\n document.removeEventListener('keydown', onLangKey)\n})\n\nlet errTimer: ReturnType<typeof setTimeout> | undefined\nasync function toggleFormat(): Promise<void> {\n if (formatted.value != null) {\n formatted.value = null // revert to the original\n return\n }\n if (formatting.value) return\n formatting.value = true\n formatErr.value = false\n try {\n formatted.value = await formatCode(props.code, { ext: ext.value, lang: effLang.value })\n } catch {\n formatErr.value = true\n clearTimeout(errTimer)\n errTimer = setTimeout(() => (formatErr.value = false), 2200)\n } finally {\n formatting.value = false\n }\n}\n\n// Visible line indices, skipping the bodies of folded regions (and any nested folds within them).\nconst visible = computed(() => {\n const out: number[] = []\n const n = lineCount.value\n let i = 0\n while (i < n) {\n out.push(i)\n const end = folded.has(i) ? regions.value.get(i) : undefined\n i = end != null ? end + 1 : i + 1\n }\n return out\n})\n\nfunction toggle(i: number): void {\n if (folded.has(i)) folded.delete(i)\n else folded.add(i)\n}\n\n// Blob fallback for huge files: one highlighted block + a newline-joined gutter.\nconst blobHtml = computed(() =>\n effLang.value ? highlightCode(displayCode.value, effLang.value) : escapeHtml(displayCode.value),\n)\nconst blobGutter = computed(() =>\n Array.from({ length: lineCount.value }, (_, i) => i + 1).join('\\n'),\n)\n</script>\n\n<template>\n <div class=\"cv\">\n <!-- floating tools, top-right over the code: highlight-language picker + Prettier format toggle -->\n <div class=\"cv__tools\">\n <!-- highlight-language picker: force which grammar colorizes the source (auto-detect is often empty/wrong) -->\n <div ref=\"langMenuRef\" class=\"cv__lang\">\n <button\n type=\"button\"\n class=\"cv__toolbtn\"\n :class=\"{ 'cv__toolbtn--on': langMenuOpen }\"\n title=\"高亮语言 —— 选择用哪种语言着色(自动识别不准时手动指定)\"\n :aria-haspopup=\"true\"\n :aria-expanded=\"langMenuOpen\"\n @click=\"langMenuOpen = !langMenuOpen\"\n >\n <Icon name=\"code\" :size=\"12\" /><span>{{ langLabel(effLang) }}</span>\n </button>\n <div v-if=\"langMenuOpen\" class=\"cv__langpop\" role=\"menu\">\n <input\n v-model=\"langQuery\"\n class=\"cv__langsearch\"\n type=\"text\"\n placeholder=\"搜索语言…\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n />\n <div class=\"cv__langlist\">\n <button\n v-if=\"!langQuery\"\n type=\"button\"\n class=\"cv__langitem\"\n :class=\"{ 'cv__langitem--on': effLang === '' }\"\n role=\"menuitemradio\"\n :aria-checked=\"effLang === ''\"\n @click=\"chooseLang('')\"\n >\n <span class=\"cv__langitem-label\">纯文本(无高亮)</span>\n <Icon v-if=\"effLang === ''\" name=\"check\" :size=\"14\" class=\"cv__langitem-check\" />\n </button>\n <button\n v-for=\"l in langResults\"\n :key=\"l.id\"\n type=\"button\"\n class=\"cv__langitem\"\n :class=\"{ 'cv__langitem--on': effLang === l.id }\"\n role=\"menuitemradio\"\n :aria-checked=\"effLang === l.id\"\n @click=\"chooseLang(l.id)\"\n >\n <span class=\"cv__langitem-label\">{{ l.label }}</span>\n <Icon v-if=\"effLang === l.id\" name=\"check\" :size=\"14\" class=\"cv__langitem-check\" />\n </button>\n <p v-if=\"langQuery && !langResults.length\" class=\"cv__langempty\">无匹配语言</p>\n </div>\n </div>\n </div>\n\n <!-- format toggle (only for languages Prettier can parse) -->\n <button\n v-if=\"canFmt\"\n type=\"button\"\n class=\"cv__toolbtn cv__fmt\"\n :class=\"{ 'cv__toolbtn--on': formatted != null, 'cv__fmt--err': formatErr }\"\n :disabled=\"formatting\"\n :title=\"\n formatErr\n ? '格式化失败:源码可能无法解析'\n : formatted != null\n ? '还原为原始格式'\n : '用 Prettier 格式化'\n \"\n @click=\"toggleFormat\"\n >\n {{\n formatErr\n ? '格式化失败'\n : formatting\n ? '格式化中…'\n : formatted != null\n ? '还原'\n : '格式化'\n }}\n </button>\n </div>\n\n <!-- huge file: single highlighted block (no folding) -->\n <div v-if=\"!foldable\" class=\"cv__scroll cv__scroll--blob\">\n <div class=\"cv__codewrap\">\n <pre class=\"cv__gutter\" aria-hidden=\"true\">{{ blobGutter }}</pre>\n <div class=\"cv__codecol\">\n <pre class=\"cv__pre\"><code class=\"hljs\" v-html=\"blobHtml\" /></pre>\n </div>\n </div>\n </div>\n\n <!-- foldable: one row per visible line, with a fold chevron on block headers -->\n <div v-else class=\"cv__scroll\">\n <div class=\"cv__rows\">\n <div v-for=\"i in visible\" :key=\"i\" class=\"cv__row\">\n <span class=\"cv__gut\">\n <button\n v-if=\"regions.has(i)\"\n type=\"button\"\n class=\"cv__fold\"\n :title=\"folded.has(i) ? '展开' : '折叠'\"\n @click=\"toggle(i)\"\n >\n <Icon name=\"chevronRight\" :size=\"12\" :class=\"{ 'cv__fold--open': !folded.has(i) }\" />\n </button>\n <span v-else class=\"cv__fold cv__fold--empty\" />\n <span class=\"cv__ln\">{{ i + 1 }}</span>\n </span>\n <code class=\"cv__code hljs\" v-html=\"lineHtml[i]\" />\n <button\n v-if=\"folded.has(i)\"\n type=\"button\"\n class=\"cv__ellipsis\"\n :title=\"`展开折叠的 ${(regions.get(i) ?? i) - i} 行`\"\n @click=\"toggle(i)\"\n >\n ⋯\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.cv {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* Floating tools bar — top-right, above the code (highlight-language picker + format toggle). */\n.cv__tools {\n position: absolute;\n top: 8px;\n right: 14px;\n z-index: 3;\n display: flex;\n align-items: flex-start;\n gap: 6px;\n}\n\n/* Shared small pill used by both floating tools. */\n.cv__toolbtn {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n max-width: 190px;\n padding: 3px 10px;\n font-family: inherit;\n font-size: 11.5px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n box-shadow: var(--shadow);\n transition:\n background 0.15s,\n color 0.15s,\n border-color 0.15s;\n}\n.cv__toolbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.cv__toolbtn span {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.cv__toolbtn--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.cv__toolbtn--on:hover {\n color: #fff;\n}\n\n/* Format toggle specifics (disabled + error states). */\n.cv__fmt:disabled {\n cursor: default;\n opacity: 0.7;\n}\n.cv__fmt--err {\n color: var(--danger);\n border-color: var(--danger);\n}\n\n/* Highlight-language picker dropdown (anchored under its pill, right-aligned). */\n.cv__lang {\n position: relative;\n display: inline-flex;\n}\n.cv__langpop {\n position: absolute;\n top: calc(100% + 6px);\n right: 0;\n z-index: 4;\n width: 224px;\n padding: 7px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 10px;\n box-shadow: var(--shadow);\n}\n.cv__langsearch {\n width: 100%;\n box-sizing: border-box;\n padding: 6px 9px;\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: 7px;\n outline: none;\n}\n.cv__langsearch:focus {\n border-color: var(--accent);\n}\n.cv__langlist {\n max-height: 288px;\n overflow-y: auto;\n overscroll-behavior: contain;\n margin-top: 6px;\n}\n.cv__langitem {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 6px 8px;\n background: transparent;\n border: none;\n border-radius: 6px;\n color: var(--text);\n font-family: inherit;\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n transition:\n background 0.12s,\n color 0.12s;\n}\n.cv__langitem:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.cv__langitem--on {\n color: var(--accent);\n}\n.cv__langitem-label {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.cv__langitem-check {\n flex-shrink: 0;\n color: var(--accent);\n}\n.cv__langempty {\n margin: 0;\n padding: 10px 8px;\n font-size: 12px;\n color: var(--faint);\n text-align: center;\n}\n\n.cv__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--code-bg);\n}\n\n/* Foldable per-line view -------------------------------------------------------------------------- */\n.cv__rows {\n min-width: max-content;\n padding: 14px 0;\n font-family: var(--font-mono);\n font-size: var(--fv-code-fs, 12.5px);\n line-height: var(--fv-code-lh, 21px);\n}\n.cv__row {\n display: flex;\n align-items: flex-start;\n}\n/* Gutter (fold chevron + line number) sticks to the left so it stays put while long lines scroll. */\n.cv__gut {\n position: sticky;\n left: 0;\n z-index: 1;\n flex-shrink: 0;\n display: flex;\n align-items: flex-start;\n background: var(--code-bg);\n user-select: none;\n}\n.cv__fold {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 16px;\n height: var(--fv-code-lh, 21px);\n padding: 0;\n border: none;\n background: transparent;\n color: var(--faint);\n cursor: pointer;\n}\n.cv__fold--empty {\n cursor: default;\n}\n.cv__fold :deep(svg) {\n transition: transform 0.12s;\n}\n.cv__fold--open {\n transform: rotate(90deg);\n}\n.cv__fold:hover :deep(svg) {\n color: var(--accent);\n}\n.cv__ln {\n flex-shrink: 0;\n width: 48px;\n padding: 0 12px 0 4px;\n text-align: right;\n color: var(--faint);\n}\n.cv__code {\n flex: 1 1 auto;\n padding: 0 18px 0 14px;\n white-space: pre;\n color: var(--text);\n background: transparent;\n border: none;\n font-family: inherit;\n tab-size: 2;\n}\n.cv__ellipsis {\n align-self: center;\n margin: 0 0 0 2px;\n padding: 0 7px;\n font-family: inherit;\n line-height: 1.3;\n color: var(--muted);\n background: var(--surface-2);\n border: 1px solid var(--border-2);\n border-radius: 5px;\n cursor: pointer;\n}\n.cv__ellipsis:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Huge-file blob fallback (mirrors the panel's original source layout) ----------------------------- */\n.cv__scroll--blob {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.cv__codewrap {\n display: flex;\n align-items: stretch;\n min-height: 100%;\n padding: 14px 0;\n}\n.cv__gutter {\n flex-shrink: 0;\n margin: 0;\n padding: 0 12px 0 16px;\n border-right: 1px solid var(--code-border);\n text-align: right;\n font-family: var(--font-mono);\n font-size: var(--fv-code-fs, 12.5px);\n line-height: var(--fv-code-lh, 21px);\n color: var(--faint);\n user-select: none;\n white-space: pre;\n}\n.cv__codecol {\n flex: 1 1 auto;\n min-width: 0;\n overflow-x: auto;\n}\n.cv__pre {\n margin: 0;\n padding: 0 18px 0 14px;\n font-family: var(--font-mono);\n font-size: var(--fv-code-fs, 12.5px);\n line-height: var(--fv-code-lh, 21px);\n color: var(--text);\n white-space: pre;\n tab-size: 2;\n}\n.cv__pre code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"CodeView.js","names":[],"sources":["../../src/components/CodeView.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'\nimport { escapeHtml, highlightCode, highlightLines } from '@/lib/highlight'\nimport { computeFoldRegions } from '@/lib/codefold'\nimport { canFormat, formatCode } from '@/lib/codeformat'\nimport { fileExt } from '@/lib/fileType'\nimport { hasAnsiSequences, parseAnsi } from '@/lib/ansi'\nimport { langLabel, searchLangs } from '@/lib/highlightLangs'\nimport Icon from '@/components/icons/Icon.vue'\nimport AnsiText from '@/components/AnsiText'\n\n/**\n * The side viewer's source-code view: a line-number gutter + syntax-highlighted code, with indentation-based\n * code FOLDING (click a chevron to collapse a block; see lib/codefold.ts), on-demand Prettier FORMATTING (the\n * 格式化 button, shown only for languages Prettier can parse — see lib/codeformat.ts; loaded lazily), and a\n * manual HIGHLIGHT-LANGUAGE picker (the 语言 button — auto-detection by extension/mime is often empty or wrong,\n * so any file can be re-colorized as JSON / XML / Python / … or forced to plain text; see lib/highlightLangs).\n * Used for every non-rendered file (plain text, code, or markdown/html/svg viewed as source). Highlighting is\n * split per line (see highlightLines) so rows can be hidden individually.\n *\n * Very large files (> {@link MAX_FOLD_LINES}) skip the per-line DOM and fall back to a single highlighted block\n * — folding a 10k-line file isn't worth tens of thousands of row elements, and such files mostly reach the\n * streaming log viewer anyway (see isLogFile).\n */\nconst props = defineProps<{ code: string; lang: string; name?: string }>()\n\nconst MAX_FOLD_LINES = 4000\n\n// Manual highlight-language override (null = use the auto-detected `props.lang`; '' = forced plain text). The\n// effective language feeds highlighting AND the 格式化 button (so \"force JSON, then format\" works). Reset when a\n// different file / detected language arrives (see the reset watch below).\nconst langOverride = ref<string | null>(null)\nconst effLang = computed(() => langOverride.value ?? props.lang)\n\n// Formatted output (null = showing the original). The view always renders `displayCode`.\nconst formatted = ref<string | null>(null)\nconst formatting = ref(false)\nconst formatErr = ref(false)\nconst displayCode = computed(() => formatted.value ?? props.code)\n\nconst ext = computed(() => fileExt(props.name ?? ''))\n// ANSI presentation is meaningful only in plain-text mode. Selecting a syntax language deliberately returns to\n// highlight.js; forcing \"纯文本\" on an otherwise-highlighted file enables ANSI restoration when controls exist.\nconst ansiDocument = computed(() =>\n !effLang.value && hasAnsiSequences(displayCode.value) ? parseAnsi(displayCode.value) : null,\n)\nconst ansiMode = computed(() => ansiDocument.value !== null)\nconst plainDisplayCode = computed(() => ansiDocument.value?.text ?? displayCode.value)\nconst canFmt = computed(() => !ansiMode.value && canFormat(ext.value, effLang.value))\n\nconst rawLines = computed(() => plainDisplayCode.value.split('\\n'))\nconst lineCount = computed(() => rawLines.value.length)\nconst foldable = computed(() => lineCount.value <= MAX_FOLD_LINES)\n\nconst lineHtml = computed(() =>\n ansiMode.value ? [] : highlightLines(displayCode.value, effLang.value),\n)\nconst regions = computed(() => computeFoldRegions(plainDisplayCode.value))\nconst folded = reactive(new Set<number>())\n\n// A new file (or a re-detected language) resets the formatting toggle AND the manual language override; any\n// change to the shown code clears stale fold state.\nwatch(\n () => [props.code, props.lang],\n () => {\n formatted.value = null\n formatErr.value = false\n langOverride.value = null\n },\n)\nwatch(displayCode, () => folded.clear())\n\n// ── highlight-language picker ────────────────────────────────────────────────────────────────────\nconst langMenuOpen = ref(false)\nconst langMenuRef = ref<HTMLElement | null>(null)\nconst langQuery = ref('')\nconst langResults = computed(() => searchLangs(langQuery.value))\n\nfunction chooseLang(id: string): void {\n langOverride.value = id\n langMenuOpen.value = false\n langQuery.value = ''\n}\n\n// Close the menu on an outside click or Esc — listeners live only while it's open (SSR-guarded). The toggle\n// button + popup both sit inside langMenuRef, so a click on either is \"inside\" and never self-closes.\nfunction onLangDocPointer(e: MouseEvent): void {\n if (langMenuRef.value && !langMenuRef.value.contains(e.target as Node)) langMenuOpen.value = false\n}\nfunction onLangKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') langMenuOpen.value = false\n}\nwatch(langMenuOpen, (on) => {\n if (typeof document === 'undefined') return\n if (on) {\n document.addEventListener('click', onLangDocPointer)\n document.addEventListener('keydown', onLangKey)\n } else {\n document.removeEventListener('click', onLangDocPointer)\n document.removeEventListener('keydown', onLangKey)\n }\n})\nonBeforeUnmount(() => {\n if (typeof document === 'undefined') return\n document.removeEventListener('click', onLangDocPointer)\n document.removeEventListener('keydown', onLangKey)\n})\n\nlet errTimer: ReturnType<typeof setTimeout> | undefined\nasync function toggleFormat(): Promise<void> {\n if (formatted.value != null) {\n formatted.value = null // revert to the original\n return\n }\n if (formatting.value) return\n formatting.value = true\n formatErr.value = false\n try {\n formatted.value = await formatCode(props.code, { ext: ext.value, lang: effLang.value })\n } catch {\n formatErr.value = true\n clearTimeout(errTimer)\n errTimer = setTimeout(() => (formatErr.value = false), 2200)\n } finally {\n formatting.value = false\n }\n}\n\n// Visible line indices, skipping the bodies of folded regions (and any nested folds within them).\nconst visible = computed(() => {\n const out: number[] = []\n const n = lineCount.value\n let i = 0\n while (i < n) {\n out.push(i)\n const end = folded.has(i) ? regions.value.get(i) : undefined\n i = end != null ? end + 1 : i + 1\n }\n return out\n})\n\nfunction toggle(i: number): void {\n if (folded.has(i)) folded.delete(i)\n else folded.add(i)\n}\n\n// Blob fallback for huge files: one highlighted block + a newline-joined gutter.\nconst blobHtml = computed(() =>\n effLang.value ? highlightCode(displayCode.value, effLang.value) : escapeHtml(displayCode.value),\n)\nconst blobGutter = computed(() =>\n Array.from({ length: lineCount.value }, (_, i) => i + 1).join('\\n'),\n)\n</script>\n\n<template>\n <div class=\"cv\">\n <!-- floating tools, top-right over the code: highlight-language picker + Prettier format toggle -->\n <div class=\"cv__tools\">\n <!-- highlight-language picker: force which grammar colorizes the source (auto-detect is often empty/wrong) -->\n <div ref=\"langMenuRef\" class=\"cv__lang\">\n <button\n type=\"button\"\n class=\"cv__toolbtn\"\n :class=\"{ 'cv__toolbtn--on': langMenuOpen }\"\n title=\"高亮语言 —— 选择用哪种语言着色(自动识别不准时手动指定)\"\n :aria-haspopup=\"true\"\n :aria-expanded=\"langMenuOpen\"\n @click=\"langMenuOpen = !langMenuOpen\"\n >\n <Icon name=\"code\" :size=\"12\" /><span>{{ langLabel(effLang) }}</span>\n </button>\n <div v-if=\"langMenuOpen\" class=\"cv__langpop\" role=\"menu\">\n <input\n v-model=\"langQuery\"\n class=\"cv__langsearch\"\n type=\"text\"\n placeholder=\"搜索语言…\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n />\n <div class=\"cv__langlist\">\n <button\n v-if=\"!langQuery\"\n type=\"button\"\n class=\"cv__langitem\"\n :class=\"{ 'cv__langitem--on': effLang === '' }\"\n role=\"menuitemradio\"\n :aria-checked=\"effLang === ''\"\n @click=\"chooseLang('')\"\n >\n <span class=\"cv__langitem-label\">纯文本(无高亮)</span>\n <Icon v-if=\"effLang === ''\" name=\"check\" :size=\"14\" class=\"cv__langitem-check\" />\n </button>\n <button\n v-for=\"l in langResults\"\n :key=\"l.id\"\n type=\"button\"\n class=\"cv__langitem\"\n :class=\"{ 'cv__langitem--on': effLang === l.id }\"\n role=\"menuitemradio\"\n :aria-checked=\"effLang === l.id\"\n @click=\"chooseLang(l.id)\"\n >\n <span class=\"cv__langitem-label\">{{ l.label }}</span>\n <Icon v-if=\"effLang === l.id\" name=\"check\" :size=\"14\" class=\"cv__langitem-check\" />\n </button>\n <p v-if=\"langQuery && !langResults.length\" class=\"cv__langempty\">无匹配语言</p>\n </div>\n </div>\n </div>\n\n <!-- format toggle (only for languages Prettier can parse) -->\n <button\n v-if=\"canFmt\"\n type=\"button\"\n class=\"cv__toolbtn cv__fmt\"\n :class=\"{ 'cv__toolbtn--on': formatted != null, 'cv__fmt--err': formatErr }\"\n :disabled=\"formatting\"\n :title=\"\n formatErr\n ? '格式化失败:源码可能无法解析'\n : formatted != null\n ? '还原为原始格式'\n : '用 Prettier 格式化'\n \"\n @click=\"toggleFormat\"\n >\n {{\n formatErr\n ? '格式化失败'\n : formatting\n ? '格式化中…'\n : formatted != null\n ? '还原'\n : '格式化'\n }}\n </button>\n </div>\n\n <!-- huge file: single highlighted block (no folding) -->\n <div v-if=\"!foldable\" class=\"cv__scroll cv__scroll--blob\">\n <div class=\"cv__codewrap\">\n <pre class=\"cv__gutter\" aria-hidden=\"true\">{{ blobGutter }}</pre>\n <div class=\"cv__codecol\">\n <pre\n class=\"cv__pre\"\n ><code v-if=\"ansiDocument\" class=\"hljs\"><AnsiText :lines=\"ansiDocument.lines\" /></code><code v-else class=\"hljs\" v-html=\"blobHtml\" /></pre>\n </div>\n </div>\n </div>\n\n <!-- foldable: one row per visible line, with a fold chevron on block headers -->\n <div v-else class=\"cv__scroll\">\n <div class=\"cv__rows\">\n <div v-for=\"i in visible\" :key=\"i\" class=\"cv__row\">\n <span class=\"cv__gut\">\n <button\n v-if=\"regions.has(i)\"\n type=\"button\"\n class=\"cv__fold\"\n :title=\"folded.has(i) ? '展开' : '折叠'\"\n @click=\"toggle(i)\"\n >\n <Icon name=\"chevronRight\" :size=\"12\" :class=\"{ 'cv__fold--open': !folded.has(i) }\" />\n </button>\n <span v-else class=\"cv__fold cv__fold--empty\" />\n <span class=\"cv__ln\">{{ i + 1 }}</span>\n </span>\n <code class=\"cv__code hljs\">\n <AnsiText v-if=\"ansiDocument\" :line=\"ansiDocument.lines[i] ?? ''\" />\n <span v-else v-html=\"lineHtml[i]\" />\n </code>\n <button\n v-if=\"folded.has(i)\"\n type=\"button\"\n class=\"cv__ellipsis\"\n :title=\"`展开折叠的 ${(regions.get(i) ?? i) - i} 行`\"\n @click=\"toggle(i)\"\n >\n ⋯\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.cv {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* Floating tools bar — top-right, above the code (highlight-language picker + format toggle). */\n.cv__tools {\n position: absolute;\n top: 8px;\n right: 14px;\n z-index: 3;\n display: flex;\n align-items: flex-start;\n gap: 6px;\n}\n\n/* Shared small pill used by both floating tools. */\n.cv__toolbtn {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n max-width: 190px;\n padding: 3px 10px;\n font-family: inherit;\n font-size: 11.5px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n box-shadow: var(--shadow);\n transition:\n background 0.15s,\n color 0.15s,\n border-color 0.15s;\n}\n.cv__toolbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.cv__toolbtn span {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.cv__toolbtn--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.cv__toolbtn--on:hover {\n color: #fff;\n}\n\n/* Format toggle specifics (disabled + error states). */\n.cv__fmt:disabled {\n cursor: default;\n opacity: 0.7;\n}\n.cv__fmt--err {\n color: var(--danger);\n border-color: var(--danger);\n}\n\n/* Highlight-language picker dropdown (anchored under its pill, right-aligned). */\n.cv__lang {\n position: relative;\n display: inline-flex;\n}\n.cv__langpop {\n position: absolute;\n top: calc(100% + 6px);\n right: 0;\n z-index: 4;\n width: 224px;\n padding: 7px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 10px;\n box-shadow: var(--shadow);\n}\n.cv__langsearch {\n width: 100%;\n box-sizing: border-box;\n padding: 6px 9px;\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: 7px;\n outline: none;\n}\n.cv__langsearch:focus {\n border-color: var(--accent);\n}\n.cv__langlist {\n max-height: 288px;\n overflow-y: auto;\n overscroll-behavior: contain;\n margin-top: 6px;\n}\n.cv__langitem {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 6px 8px;\n background: transparent;\n border: none;\n border-radius: 6px;\n color: var(--text);\n font-family: inherit;\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n transition:\n background 0.12s,\n color 0.12s;\n}\n.cv__langitem:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.cv__langitem--on {\n color: var(--accent);\n}\n.cv__langitem-label {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.cv__langitem-check {\n flex-shrink: 0;\n color: var(--accent);\n}\n.cv__langempty {\n margin: 0;\n padding: 10px 8px;\n font-size: 12px;\n color: var(--faint);\n text-align: center;\n}\n\n.cv__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--code-bg);\n}\n\n/* Foldable per-line view -------------------------------------------------------------------------- */\n.cv__rows {\n min-width: max-content;\n padding: 14px 0;\n font-family: var(--font-mono);\n font-size: var(--fv-code-fs, 12.5px);\n line-height: var(--fv-code-lh, 21px);\n}\n.cv__row {\n display: flex;\n align-items: flex-start;\n}\n/* Gutter (fold chevron + line number) sticks to the left so it stays put while long lines scroll. */\n.cv__gut {\n position: sticky;\n left: 0;\n z-index: 1;\n flex-shrink: 0;\n display: flex;\n align-items: flex-start;\n background: var(--code-bg);\n user-select: none;\n}\n.cv__fold {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 16px;\n height: var(--fv-code-lh, 21px);\n padding: 0;\n border: none;\n background: transparent;\n color: var(--faint);\n cursor: pointer;\n}\n.cv__fold--empty {\n cursor: default;\n}\n.cv__fold :deep(svg) {\n transition: transform 0.12s;\n}\n.cv__fold--open {\n transform: rotate(90deg);\n}\n.cv__fold:hover :deep(svg) {\n color: var(--accent);\n}\n.cv__ln {\n flex-shrink: 0;\n width: 48px;\n padding: 0 12px 0 4px;\n text-align: right;\n color: var(--faint);\n}\n.cv__code {\n flex: 1 1 auto;\n padding: 0 18px 0 14px;\n white-space: pre;\n color: var(--text);\n background: transparent;\n border: none;\n font-family: inherit;\n tab-size: 2;\n}\n.cv__ellipsis {\n align-self: center;\n margin: 0 0 0 2px;\n padding: 0 7px;\n font-family: inherit;\n line-height: 1.3;\n color: var(--muted);\n background: var(--surface-2);\n border: 1px solid var(--border-2);\n border-radius: 5px;\n cursor: pointer;\n}\n.cv__ellipsis:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Huge-file blob fallback (mirrors the panel's original source layout) ----------------------------- */\n.cv__scroll--blob {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.cv__codewrap {\n display: flex;\n align-items: stretch;\n min-height: 100%;\n padding: 14px 0;\n}\n.cv__gutter {\n flex-shrink: 0;\n margin: 0;\n padding: 0 12px 0 16px;\n border-right: 1px solid var(--code-border);\n text-align: right;\n font-family: var(--font-mono);\n font-size: var(--fv-code-fs, 12.5px);\n line-height: var(--fv-code-lh, 21px);\n color: var(--faint);\n user-select: none;\n white-space: pre;\n}\n.cv__codecol {\n flex: 1 1 auto;\n min-width: 0;\n overflow-x: auto;\n}\n.cv__pre {\n margin: 0;\n padding: 0 18px 0 14px;\n font-family: var(--font-mono);\n font-size: var(--fv-code-fs, 12.5px);\n line-height: var(--fv-code-lh, 21px);\n color: var(--text);\n white-space: pre;\n tab-size: 2;\n}\n.cv__pre code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n</style>\n"],"mappings":""}
|
|
@@ -1,45 +1,50 @@
|
|
|
1
1
|
import { fileExt as e } from "../lib/fileType.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { hasAnsiSequences as t, parseAnsi as n } from "../lib/ansi.js";
|
|
3
|
+
import { escapeHtml as ee, highlightCode as r, highlightLines as te } from "../lib/highlight.js";
|
|
4
|
+
import { computeFoldRegions as ne } from "../lib/codefold.js";
|
|
5
|
+
import { canFormat as re, formatCode as ie } from "../lib/codeformat.js";
|
|
6
|
+
import { langLabel as ae, searchLangs as oe } from "../lib/highlightLangs.js";
|
|
6
7
|
import i from "./icons/Icon.js";
|
|
7
|
-
import
|
|
8
|
+
import a from "./AnsiText.js";
|
|
9
|
+
import { Fragment as o, computed as s, createBlock as c, createCommentVNode as l, createElementBlock as u, createElementVNode as d, createVNode as f, defineComponent as p, normalizeClass as m, onBeforeUnmount as se, openBlock as h, reactive as ce, ref as g, renderList as _, toDisplayString as v, unref as y, vModelText as le, watch as b, withDirectives as ue } from "vue";
|
|
8
10
|
//#region src/components/CodeView.vue?vue&type=script&setup=true&lang.ts
|
|
9
|
-
var
|
|
11
|
+
var de = { class: "cv" }, fe = { class: "cv__tools" }, pe = ["aria-expanded"], me = {
|
|
10
12
|
key: 0,
|
|
11
13
|
class: "cv__langpop",
|
|
12
14
|
role: "menu"
|
|
13
|
-
},
|
|
15
|
+
}, he = { class: "cv__langlist" }, ge = ["aria-checked"], _e = ["aria-checked", "onClick"], ve = { class: "cv__langitem-label" }, ye = {
|
|
14
16
|
key: 1,
|
|
15
17
|
class: "cv__langempty"
|
|
16
|
-
},
|
|
18
|
+
}, be = ["disabled", "title"], xe = {
|
|
17
19
|
key: 0,
|
|
18
20
|
class: "cv__scroll cv__scroll--blob"
|
|
19
|
-
},
|
|
21
|
+
}, Se = { class: "cv__codewrap" }, Ce = {
|
|
20
22
|
class: "cv__gutter",
|
|
21
23
|
"aria-hidden": "true"
|
|
22
|
-
},
|
|
24
|
+
}, x = { class: "cv__codecol" }, S = { class: "cv__pre" }, C = {
|
|
25
|
+
key: 0,
|
|
26
|
+
class: "hljs"
|
|
27
|
+
}, w = ["innerHTML"], T = {
|
|
23
28
|
key: 1,
|
|
24
29
|
class: "cv__scroll"
|
|
25
30
|
}, E = { class: "cv__rows" }, D = { class: "cv__gut" }, O = ["title", "onClick"], k = {
|
|
26
31
|
key: 1,
|
|
27
32
|
class: "cv__fold cv__fold--empty"
|
|
28
|
-
}, A = { class: "cv__ln" },
|
|
33
|
+
}, A = { class: "cv__ln" }, we = { class: "cv__code hljs" }, Te = ["innerHTML"], Ee = ["title", "onClick"], De = 4e3, j = /*@__PURE__*/ p({
|
|
29
34
|
__name: "CodeView",
|
|
30
35
|
props: {
|
|
31
36
|
code: {},
|
|
32
37
|
lang: {},
|
|
33
38
|
name: {}
|
|
34
39
|
},
|
|
35
|
-
setup(
|
|
36
|
-
let
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}),
|
|
40
|
-
let G = g(!1), K = g(null), q = g(""), J =
|
|
40
|
+
setup(p) {
|
|
41
|
+
let j = p, M = g(null), N = s(() => M.value ?? j.lang), P = g(null), F = g(!1), I = g(!1), L = s(() => P.value ?? j.code), R = s(() => e(j.name ?? "")), z = s(() => !N.value && t(L.value) ? n(L.value) : null), B = s(() => z.value !== null), V = s(() => z.value?.text ?? L.value), Oe = s(() => !B.value && re(R.value, N.value)), ke = s(() => V.value.split("\n")), H = s(() => ke.value.length), Ae = s(() => H.value <= De), je = s(() => B.value ? [] : te(L.value, N.value)), U = s(() => ne(V.value)), W = ce(/* @__PURE__ */ new Set());
|
|
42
|
+
b(() => [j.code, j.lang], () => {
|
|
43
|
+
P.value = null, I.value = !1, M.value = null;
|
|
44
|
+
}), b(L, () => W.clear());
|
|
45
|
+
let G = g(!1), K = g(null), q = g(""), J = s(() => oe(q.value));
|
|
41
46
|
function Y(e) {
|
|
42
|
-
|
|
47
|
+
M.value = e, G.value = !1, q.value = "";
|
|
43
48
|
}
|
|
44
49
|
function X(e) {
|
|
45
50
|
K.value && !K.value.contains(e.target) && (G.value = !1);
|
|
@@ -47,32 +52,32 @@ var le = { class: "cv" }, ue = { class: "cv__tools" }, de = ["aria-expanded"], f
|
|
|
47
52
|
function Z(e) {
|
|
48
53
|
e.key === "Escape" && (G.value = !1);
|
|
49
54
|
}
|
|
50
|
-
|
|
55
|
+
b(G, (e) => {
|
|
51
56
|
typeof document > "u" || (e ? (document.addEventListener("click", X), document.addEventListener("keydown", Z)) : (document.removeEventListener("click", X), document.removeEventListener("keydown", Z)));
|
|
52
|
-
}),
|
|
57
|
+
}), se(() => {
|
|
53
58
|
typeof document > "u" || (document.removeEventListener("click", X), document.removeEventListener("keydown", Z));
|
|
54
59
|
});
|
|
55
60
|
let Q;
|
|
56
|
-
async function
|
|
57
|
-
if (
|
|
58
|
-
|
|
61
|
+
async function Me() {
|
|
62
|
+
if (P.value != null) {
|
|
63
|
+
P.value = null;
|
|
59
64
|
return;
|
|
60
65
|
}
|
|
61
|
-
if (!
|
|
62
|
-
|
|
66
|
+
if (!F.value) {
|
|
67
|
+
F.value = !0, I.value = !1;
|
|
63
68
|
try {
|
|
64
|
-
|
|
65
|
-
ext:
|
|
66
|
-
lang:
|
|
69
|
+
P.value = await ie(j.code, {
|
|
70
|
+
ext: R.value,
|
|
71
|
+
lang: N.value
|
|
67
72
|
});
|
|
68
73
|
} catch {
|
|
69
|
-
|
|
74
|
+
I.value = !0, clearTimeout(Q), Q = setTimeout(() => I.value = !1, 2200);
|
|
70
75
|
} finally {
|
|
71
|
-
|
|
76
|
+
F.value = !1;
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
|
-
let
|
|
80
|
+
let Ne = s(() => {
|
|
76
81
|
let e = [], t = H.value, n = 0;
|
|
77
82
|
for (; n < t;) {
|
|
78
83
|
e.push(n);
|
|
@@ -84,99 +89,103 @@ var le = { class: "cv" }, ue = { class: "cv__tools" }, de = ["aria-expanded"], f
|
|
|
84
89
|
function $(e) {
|
|
85
90
|
W.has(e) ? W.delete(e) : W.add(e);
|
|
86
91
|
}
|
|
87
|
-
let
|
|
88
|
-
return (e, t) => (h(),
|
|
92
|
+
let Pe = s(() => N.value ? r(L.value, N.value) : ee(L.value)), Fe = s(() => Array.from({ length: H.value }, (e, t) => t + 1).join("\n"));
|
|
93
|
+
return (e, t) => (h(), u("div", de, [d("div", fe, [d("div", {
|
|
89
94
|
ref_key: "langMenuRef",
|
|
90
95
|
ref: K,
|
|
91
96
|
class: "cv__lang"
|
|
92
|
-
}, [
|
|
97
|
+
}, [d("button", {
|
|
93
98
|
type: "button",
|
|
94
|
-
class:
|
|
99
|
+
class: m(["cv__toolbtn", { "cv__toolbtn--on": G.value }]),
|
|
95
100
|
title: "高亮语言 —— 选择用哪种语言着色(自动识别不准时手动指定)",
|
|
96
101
|
"aria-haspopup": !0,
|
|
97
102
|
"aria-expanded": G.value,
|
|
98
103
|
onClick: t[0] ||= (e) => G.value = !G.value
|
|
99
|
-
}, [
|
|
104
|
+
}, [f(i, {
|
|
100
105
|
name: "code",
|
|
101
106
|
size: 12
|
|
102
|
-
}),
|
|
107
|
+
}), d("span", null, v(y(ae)(N.value)), 1)], 10, pe), G.value ? (h(), u("div", me, [ue(d("input", {
|
|
103
108
|
"onUpdate:modelValue": t[1] ||= (e) => q.value = e,
|
|
104
109
|
class: "cv__langsearch",
|
|
105
110
|
type: "text",
|
|
106
111
|
placeholder: "搜索语言…",
|
|
107
112
|
spellcheck: "false",
|
|
108
113
|
autocomplete: "off"
|
|
109
|
-
}, null, 512), [[
|
|
110
|
-
q.value ?
|
|
114
|
+
}, null, 512), [[le, q.value]]), d("div", he, [
|
|
115
|
+
q.value ? l("", !0) : (h(), u("button", {
|
|
111
116
|
key: 0,
|
|
112
117
|
type: "button",
|
|
113
|
-
class:
|
|
118
|
+
class: m(["cv__langitem", { "cv__langitem--on": N.value === "" }]),
|
|
114
119
|
role: "menuitemradio",
|
|
115
|
-
"aria-checked":
|
|
120
|
+
"aria-checked": N.value === "",
|
|
116
121
|
onClick: t[2] ||= (e) => Y("")
|
|
117
|
-
}, [t[3] ||=
|
|
122
|
+
}, [t[3] ||= d("span", { class: "cv__langitem-label" }, "纯文本(无高亮)", -1), N.value === "" ? (h(), c(i, {
|
|
118
123
|
key: 0,
|
|
119
124
|
name: "check",
|
|
120
125
|
size: 14,
|
|
121
126
|
class: "cv__langitem-check"
|
|
122
|
-
})) :
|
|
123
|
-
(h(!0),
|
|
127
|
+
})) : l("", !0)], 10, ge)),
|
|
128
|
+
(h(!0), u(o, null, _(J.value, (e) => (h(), u("button", {
|
|
124
129
|
key: e.id,
|
|
125
130
|
type: "button",
|
|
126
|
-
class:
|
|
131
|
+
class: m(["cv__langitem", { "cv__langitem--on": N.value === e.id }]),
|
|
127
132
|
role: "menuitemradio",
|
|
128
|
-
"aria-checked":
|
|
133
|
+
"aria-checked": N.value === e.id,
|
|
129
134
|
onClick: (t) => Y(e.id)
|
|
130
|
-
}, [
|
|
135
|
+
}, [d("span", ve, v(e.label), 1), N.value === e.id ? (h(), c(i, {
|
|
131
136
|
key: 0,
|
|
132
137
|
name: "check",
|
|
133
138
|
size: 14,
|
|
134
139
|
class: "cv__langitem-check"
|
|
135
|
-
})) :
|
|
136
|
-
q.value && !J.value.length ? (h(),
|
|
137
|
-
])])) :
|
|
140
|
+
})) : l("", !0)], 10, _e))), 128)),
|
|
141
|
+
q.value && !J.value.length ? (h(), u("p", ye, "无匹配语言")) : l("", !0)
|
|
142
|
+
])])) : l("", !0)], 512), Oe.value ? (h(), u("button", {
|
|
138
143
|
key: 0,
|
|
139
144
|
type: "button",
|
|
140
|
-
class:
|
|
141
|
-
"cv__toolbtn--on":
|
|
142
|
-
"cv__fmt--err":
|
|
145
|
+
class: m(["cv__toolbtn cv__fmt", {
|
|
146
|
+
"cv__toolbtn--on": P.value != null,
|
|
147
|
+
"cv__fmt--err": I.value
|
|
143
148
|
}]),
|
|
144
|
-
disabled:
|
|
145
|
-
title:
|
|
146
|
-
onClick:
|
|
147
|
-
}, v(
|
|
149
|
+
disabled: F.value,
|
|
150
|
+
title: I.value ? "格式化失败:源码可能无法解析" : P.value == null ? "用 Prettier 格式化" : "还原为原始格式",
|
|
151
|
+
onClick: Me
|
|
152
|
+
}, v(I.value ? "格式化失败" : F.value ? "格式化中…" : P.value == null ? "格式化" : "还原"), 11, be)) : l("", !0)]), Ae.value ? (h(), u("div", T, [d("div", E, [(h(!0), u(o, null, _(Ne.value, (e) => (h(), u("div", {
|
|
148
153
|
key: e,
|
|
149
154
|
class: "cv__row"
|
|
150
155
|
}, [
|
|
151
|
-
|
|
156
|
+
d("span", D, [U.value.has(e) ? (h(), u("button", {
|
|
152
157
|
key: 0,
|
|
153
158
|
type: "button",
|
|
154
159
|
class: "cv__fold",
|
|
155
160
|
title: W.has(e) ? "展开" : "折叠",
|
|
156
161
|
onClick: (t) => $(e)
|
|
157
|
-
}, [
|
|
162
|
+
}, [f(i, {
|
|
158
163
|
name: "chevronRight",
|
|
159
164
|
size: 12,
|
|
160
|
-
class:
|
|
161
|
-
}, null, 8, ["class"])], 8, O)) : (h(),
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}, null, 8,
|
|
166
|
-
|
|
165
|
+
class: m({ "cv__fold--open": !W.has(e) })
|
|
166
|
+
}, null, 8, ["class"])], 8, O)) : (h(), u("span", k)), d("span", A, v(e + 1), 1)]),
|
|
167
|
+
d("code", we, [z.value ? (h(), c(y(a), {
|
|
168
|
+
key: 0,
|
|
169
|
+
line: z.value.lines[e] ?? ""
|
|
170
|
+
}, null, 8, ["line"])) : (h(), u("span", {
|
|
171
|
+
key: 1,
|
|
172
|
+
innerHTML: je.value[e]
|
|
173
|
+
}, null, 8, Te))]),
|
|
174
|
+
W.has(e) ? (h(), u("button", {
|
|
167
175
|
key: 0,
|
|
168
176
|
type: "button",
|
|
169
177
|
class: "cv__ellipsis",
|
|
170
178
|
title: `展开折叠的 ${(U.value.get(e) ?? e) - e} 行`,
|
|
171
179
|
onClick: (t) => $(e)
|
|
172
|
-
}, " ⋯ ", 8,
|
|
173
|
-
]))), 128))])])) : (h(),
|
|
180
|
+
}, " ⋯ ", 8, Ee)) : l("", !0)
|
|
181
|
+
]))), 128))])])) : (h(), u("div", xe, [d("div", Se, [d("pre", Ce, v(Fe.value), 1), d("div", x, [d("pre", S, [z.value ? (h(), u("code", C, [f(y(a), { lines: z.value.lines }, null, 8, ["lines"])])) : (h(), u("code", {
|
|
182
|
+
key: 1,
|
|
174
183
|
class: "hljs",
|
|
175
|
-
innerHTML:
|
|
176
|
-
}, null, 8, w)])])])]))]));
|
|
184
|
+
innerHTML: Pe.value
|
|
185
|
+
}, null, 8, w))])])])]))]));
|
|
177
186
|
}
|
|
178
187
|
});
|
|
179
188
|
//#endregion
|
|
180
|
-
export {
|
|
189
|
+
export { j as default };
|
|
181
190
|
|
|
182
191
|
//# sourceMappingURL=CodeView.vue_vue_type_script_setup_true_lang.js.map
|