@l1yp/file-viewer 0.1.4 → 0.1.6

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.
Files changed (177) hide show
  1. package/README.md +57 -6
  2. package/dist/assets/step.worker-CfDTYOf_.js.map +1 -0
  3. package/dist/components/AnsiText.d.ts +47 -0
  4. package/dist/components/AnsiText.js +41 -0
  5. package/dist/components/AnsiText.js.map +1 -0
  6. package/dist/components/CertViewer.js +1 -1
  7. package/dist/components/CertViewer.js.map +1 -1
  8. package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  9. package/dist/components/CodeView.js +1 -1
  10. package/dist/components/CodeView.js.map +1 -1
  11. package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js +80 -71
  12. package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js.map +1 -1
  13. package/dist/components/DocxEmbeddedFiles.js +9 -0
  14. package/dist/components/DocxEmbeddedFiles.js.map +1 -0
  15. package/dist/components/DocxEmbeddedFiles.vue.d.ts +15 -0
  16. package/dist/components/DocxEmbeddedFiles.vue_vue_type_script_setup_true_lang.js +69 -0
  17. package/dist/components/DocxEmbeddedFiles.vue_vue_type_script_setup_true_lang.js.map +1 -0
  18. package/dist/components/DocxNavigationPane.js +1 -1
  19. package/dist/components/DocxNavigationPane.js.map +1 -1
  20. package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -1
  21. package/dist/components/DocxViewer.js +1 -1
  22. package/dist/components/DocxViewer.js.map +1 -1
  23. package/dist/components/DocxViewer.vue.d.ts +1 -1
  24. package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js +182 -50
  25. package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  26. package/dist/components/EplViewer.js +1 -1
  27. package/dist/components/EplViewer.js.map +1 -1
  28. package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  29. package/dist/components/FileViewerBody.js +1 -1
  30. package/dist/components/FileViewerBody.js.map +1 -1
  31. package/dist/components/FileViewerBody.vue.d.ts +1 -0
  32. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js +292 -281
  33. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -1
  34. package/dist/components/FileViewerPanel.js +1 -1
  35. package/dist/components/FileViewerPanel.js.map +1 -1
  36. package/dist/components/FileViewerPanel.vue.d.ts +6 -1
  37. package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js +53 -51
  38. package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  39. package/dist/components/HarViewer.js +1 -1
  40. package/dist/components/HarViewer.js.map +1 -1
  41. package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  42. package/dist/components/JksViewer.js +1 -1
  43. package/dist/components/JksViewer.js.map +1 -1
  44. package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  45. package/dist/components/LogViewer.js +1 -1
  46. package/dist/components/LogViewer.js.map +1 -1
  47. package/dist/components/LogViewer.vue.d.ts +3 -1
  48. package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js +284 -280
  49. package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  50. package/dist/components/ModelViewModes.js +9 -0
  51. package/dist/components/ModelViewModes.js.map +1 -0
  52. package/dist/components/ModelViewModes.vue.d.ts +11 -0
  53. package/dist/components/ModelViewModes.vue_vue_type_script_setup_true_lang.js +49 -0
  54. package/dist/components/ModelViewModes.vue_vue_type_script_setup_true_lang.js.map +1 -0
  55. package/dist/components/ModelViewer.js +1 -1
  56. package/dist/components/ModelViewer.js.map +1 -1
  57. package/dist/components/ModelViewer.vue.d.ts +2 -1
  58. package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js +183 -96
  59. package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  60. package/dist/components/PdfNavigationPane.js +1 -1
  61. package/dist/components/PdfNavigationPane.js.map +1 -1
  62. package/dist/components/PdfNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -1
  63. package/dist/components/PdfViewer.js +1 -1
  64. package/dist/components/PdfViewer.js.map +1 -1
  65. package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  66. package/dist/components/PptxNavigationPane.js +1 -1
  67. package/dist/components/PptxNavigationPane.js.map +1 -1
  68. package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -1
  69. package/dist/components/PsdViewer.js +1 -1
  70. package/dist/components/PsdViewer.js.map +1 -1
  71. package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  72. package/dist/components/SqliteViewer.js +1 -1
  73. package/dist/components/SqliteViewer.js.map +1 -1
  74. package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  75. package/dist/components/ViewerLoadError.js +9 -0
  76. package/dist/components/ViewerLoadError.js.map +1 -0
  77. package/dist/components/ViewerLoadError.vue.d.ts +17 -0
  78. package/dist/components/ViewerLoadError.vue_vue_type_script_setup_true_lang.js +58 -0
  79. package/dist/components/ViewerLoadError.vue_vue_type_script_setup_true_lang.js.map +1 -0
  80. package/dist/components/XlsxChart.js +1 -1
  81. package/dist/components/XlsxChart.js.map +1 -1
  82. package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js +9 -9
  83. package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js.map +1 -1
  84. package/dist/components/XlsxImageLayer.js +9 -0
  85. package/dist/components/XlsxImageLayer.js.map +1 -0
  86. package/dist/components/XlsxImageLayer.vue.d.ts +9 -0
  87. package/dist/components/XlsxImageLayer.vue_vue_type_script_setup_true_lang.js +46 -0
  88. package/dist/components/XlsxImageLayer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  89. package/dist/components/XlsxViewer.js +1 -1
  90. package/dist/components/XlsxViewer.js.map +1 -1
  91. package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js +230 -216
  92. package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  93. package/dist/components/epl/EplResource.js +1 -1
  94. package/dist/components/epl/EplResource.js.map +1 -1
  95. package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js.map +1 -1
  96. package/dist/components/optionalViewer.d.ts +3 -2
  97. package/dist/components/optionalViewer.js +6 -3
  98. package/dist/components/optionalViewer.js.map +1 -1
  99. package/dist/composables/useFileViewer.d.ts +11 -3
  100. package/dist/composables/useFileViewer.js.map +1 -1
  101. package/dist/composables/useTeleportTheme.d.ts +7 -0
  102. package/dist/composables/useTeleportTheme.js +42 -0
  103. package/dist/composables/useTeleportTheme.js.map +1 -0
  104. package/dist/index.d.ts +1 -0
  105. package/dist/index.js +29 -28
  106. package/dist/lib/ansi.d.ts +44 -0
  107. package/dist/lib/ansi.js +224 -0
  108. package/dist/lib/ansi.js.map +1 -0
  109. package/dist/lib/cfb.d.ts +37 -0
  110. package/dist/lib/cfb.js +112 -0
  111. package/dist/lib/cfb.js.map +1 -0
  112. package/dist/lib/docxEmbeddedDom.d.ts +22 -0
  113. package/dist/lib/docxEmbeddedDom.js +128 -0
  114. package/dist/lib/docxEmbeddedDom.js.map +1 -0
  115. package/dist/lib/docxEmbeddings.d.ts +41 -0
  116. package/dist/lib/docxEmbeddings.js +226 -0
  117. package/dist/lib/docxEmbeddings.js.map +1 -0
  118. package/dist/lib/docxTheme.d.ts +6 -0
  119. package/dist/lib/docxTheme.js +64 -0
  120. package/dist/lib/docxTheme.js.map +1 -0
  121. package/dist/lib/epl/code/structures.js +82 -0
  122. package/dist/lib/epl/code/structures.js.map +1 -0
  123. package/dist/lib/epl/code/variable.js +16 -0
  124. package/dist/lib/epl/code/variable.js.map +1 -0
  125. package/dist/lib/epl/eplDocument.js +28 -22
  126. package/dist/lib/epl/eplDocument.js.map +1 -1
  127. package/dist/lib/epl/index.d.ts +2 -1
  128. package/dist/lib/epl/index.js.map +1 -1
  129. package/dist/lib/epl/sections/code.js +37 -0
  130. package/dist/lib/epl/sections/code.js.map +1 -0
  131. package/dist/lib/epl/sections/esystemInfo.js +29 -0
  132. package/dist/lib/epl/sections/esystemInfo.js.map +1 -0
  133. package/dist/lib/epl/sections/losable.js +33 -0
  134. package/dist/lib/epl/sections/losable.js.map +1 -0
  135. package/dist/lib/epl/sections/projectConfig.js +39 -0
  136. package/dist/lib/epl/sections/projectConfig.js.map +1 -0
  137. package/dist/lib/epl/sections/resource.js +84 -0
  138. package/dist/lib/epl/sections/resource.js.map +1 -0
  139. package/dist/lib/fileType.d.ts +3 -1
  140. package/dist/lib/fileType.js +150 -137
  141. package/dist/lib/fileType.js.map +1 -1
  142. package/dist/lib/modelAppearance.d.ts +7 -0
  143. package/dist/lib/modelAppearance.js +41 -0
  144. package/dist/lib/modelAppearance.js.map +1 -0
  145. package/dist/lib/msi.js +62 -154
  146. package/dist/lib/msi.js.map +1 -1
  147. package/dist/lib/olePackage.d.ts +8 -0
  148. package/dist/lib/olePackage.js +37 -0
  149. package/dist/lib/olePackage.js.map +1 -0
  150. package/dist/lib/pptxFirstSlide.d.ts +11 -0
  151. package/dist/lib/pptxFirstSlide.js +34 -0
  152. package/dist/lib/pptxFirstSlide.js.map +1 -0
  153. package/dist/lib/step.d.ts +10 -0
  154. package/dist/lib/step.js +35 -0
  155. package/dist/lib/step.js.map +1 -0
  156. package/dist/lib/step.worker.d.ts +1 -0
  157. package/dist/lib/step.worker.js +18 -0
  158. package/dist/lib/step.worker.js.map +1 -0
  159. package/dist/lib/stepObject.d.ts +4 -0
  160. package/dist/lib/stepObject.js +52 -0
  161. package/dist/lib/stepObject.js.map +1 -0
  162. package/dist/lib/stepTypes.d.ts +60 -0
  163. package/dist/lib/viewerKind.js +10 -5
  164. package/dist/lib/viewerKind.js.map +1 -1
  165. package/dist/lib/xlsx.d.ts +1 -1
  166. package/dist/lib/xlsx.js +106 -89
  167. package/dist/lib/xlsx.js.map +1 -1
  168. package/dist/lib/xlsxCharts.js +71 -119
  169. package/dist/lib/xlsxCharts.js.map +1 -1
  170. package/dist/lib/xlsxImages.d.ts +43 -0
  171. package/dist/lib/xlsxImages.js +114 -0
  172. package/dist/lib/xlsxImages.js.map +1 -0
  173. package/dist/lib/xlsxParts.d.ts +17 -0
  174. package/dist/lib/xlsxParts.js +86 -0
  175. package/dist/lib/xlsxParts.js.map +1 -0
  176. package/dist/style.css +1 -1
  177. package/package.json +8 -3
@@ -1 +1 @@
1
- {"version":3,"file":"fileType.js","names":[],"sources":["../../src/lib/fileType.ts"],"sourcesContent":["/**\n * File-type detection for the conversation file UIs.\n *\n * The proxy's stored `content_type` is unreliable (often `application/octet-stream`), so we never\n * trust it. Instead we prefer the share JSON's own `mime_type` (present on delivered `present_files`),\n * and fall back to the filename extension — which is all we have for user uploads / intermediate files.\n *\n * Two questions this answers:\n * - {@link isTextFile}: can we open it in the side viewer? (non-text files are download-only)\n * - {@link hljsLang}: which highlight.js language to colorize with ('' = plain, no highlighting)\n */\n\n/** Lowercased file extension without the dot, or '' if the name has none. */\nexport function fileExt(name: string): string {\n const base = (name || '').split(/[\\\\/]/).pop() ?? ''\n const dot = base.lastIndexOf('.')\n return dot > 0 ? base.slice(dot + 1).toLowerCase() : ''\n}\n\n// Extension → highlight.js language id. Covers the common code/markup types; anything textual but\n// unlisted renders as plain escaped text (see hljsLang returning '').\nconst EXT_LANG: Record<string, string> = {\n js: 'javascript',\n mjs: 'javascript',\n cjs: 'javascript',\n jsx: 'javascript',\n ts: 'typescript',\n tsx: 'typescript',\n mts: 'typescript',\n cts: 'typescript',\n py: 'python',\n pyw: 'python',\n rb: 'ruby',\n rs: 'rust',\n kt: 'kotlin',\n kts: 'kotlin',\n java: 'java',\n c: 'c',\n h: 'c',\n cc: 'cpp',\n cpp: 'cpp',\n cxx: 'cpp',\n hpp: 'cpp',\n hh: 'cpp',\n cs: 'csharp',\n go: 'go',\n php: 'php',\n swift: 'swift',\n scala: 'scala',\n sh: 'bash',\n bash: 'bash',\n zsh: 'bash',\n fish: 'bash',\n ps1: 'powershell',\n psm1: 'powershell',\n bat: 'dos',\n cmd: 'dos',\n yml: 'yaml',\n yaml: 'yaml',\n toml: 'ini',\n ini: 'ini',\n cfg: 'ini',\n conf: 'ini',\n properties: 'properties',\n json: 'json',\n json5: 'json',\n xml: 'xml',\n html: 'xml',\n htm: 'xml',\n svg: 'xml',\n vue: 'xml',\n svelte: 'xml',\n // Outline / mind-map XML — viewable + XML-highlighted in source view (`.mm` is also Objective-C++, but the\n // mind-map render is content-sniffed, so a non-XML `.mm` just reads as XML-tinted source — see isMindMapFile).\n opml: 'xml',\n mm: 'xml',\n css: 'css',\n scss: 'scss',\n sass: 'scss',\n less: 'less',\n sql: 'sql',\n md: 'markdown',\n markdown: 'markdown',\n gradle: 'gradle',\n groovy: 'groovy',\n lua: 'lua',\n r: 'r',\n dart: 'dart',\n pl: 'perl',\n diff: 'diff',\n patch: 'diff',\n}\n\n// Textual extensions we let the viewer open but don't syntax-highlight (rendered as plain escaped text).\n// NOTE: `.rtf` is deliberately NOT here — it renders as rich text in the side viewer (see isRtfFile), not source.\nconst PLAIN_EXT = new Set([\n 'txt',\n 'text',\n 'log',\n 'csv',\n 'tsv',\n 'env',\n 'lock',\n 'gitignore',\n 'srt',\n 'vtt',\n])\n\n// Extension-less filenames that are nonetheless text.\nconst PLAIN_NAMES = new Set([\n 'dockerfile',\n 'makefile',\n 'license',\n 'readme',\n 'changelog',\n 'authors',\n '.gitignore',\n '.env',\n])\n\n// application/* MIME types that are actually text (text/* is handled generically).\nconst TEXTUAL_APP_MIME = new Set([\n 'application/json',\n 'application/ld+json',\n 'application/xml',\n 'application/xhtml+xml',\n 'application/javascript',\n 'application/ecmascript',\n 'application/x-sh',\n 'application/x-shellscript',\n 'application/x-python',\n 'application/x-python-code',\n 'application/x-yaml',\n 'application/yaml',\n 'application/toml',\n 'application/x-toml',\n 'application/sql',\n 'application/graphql',\n 'application/x-httpd-php',\n 'application/csv',\n 'application/markdown',\n 'image/svg+xml',\n])\n\n/** Can this file be opened in the side viewer? Prefer `mime`, fall back to the extension. */\nexport function isTextFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (m.startsWith('text/')) return true\n if (TEXTUAL_APP_MIME.has(m)) return true\n // Clearly-binary families are never viewable, regardless of extension.\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n // Other application/* (pdf, zip, msword, …): fall through to the extension as a tiebreak.\n }\n const ext = fileExt(name)\n if (ext) return ext in EXT_LANG || PLAIN_EXT.has(ext)\n return PLAIN_NAMES.has((name || '').toLowerCase())\n}\n\n// Any text file at or above this size is routed to the streaming log viewer (virtual-scrolled) instead of\n// the normal source view, so a huge .txt/.csv/.json doesn't lock the page tokenizing one giant block.\nexport const LARGE_TEXT_BYTES = 1024 * 1024\n\n/**\n * Should this open in the side viewer's streaming log viewer (virtual-scrolled, see LogViewer.vue) rather\n * than the normal highlighted source view? True for any `.log` (logs benefit from the line gutter + search),\n * and for any LARGE text file (>= {@link LARGE_TEXT_BYTES}) so big files don't freeze. Consulted in FileChip\n * BEFORE the generic text route. `size` is the manifest byte size when known (0/undefined ⇒ treat as small).\n */\nexport function isLogFile(name: string, mime?: string | null, size?: number | null): boolean {\n if (fileExt(name) === 'log') return true\n return (size ?? 0) >= LARGE_TEXT_BYTES && isTextFile(name, mime)\n}\n\n// CSV / TSV routed to the side viewer's table preview (parsed in-browser, see lib/csv.ts + CsvViewer.vue).\n// `.csv`/`.tsv` are ALSO plain text, so this is consulted in FileChip BEFORE the generic text route — but\n// AFTER the large-text/log route, so a huge CSV still streams through the virtual log viewer instead of\n// building a giant table. Extension-driven; text/csv + the tab-separated mimes also confirm it.\nconst CSV_EXT = new Set(['csv', 'tsv'])\nconst CSV_MIME = new Set(['text/csv', 'application/csv', 'text/tab-separated-values', 'text/tsv'])\n\n/**\n * Is this a CSV / TSV table (`.csv`/`.tsv`)? Such files open in the side viewer's table preview, which parses\n * the rows in-browser (see lib/csv.ts + CsvViewer.vue) with full RFC 4180 quote handling and a render/source\n * toggle. Prefer `mime`, fall back to the extension; a concrete non-text media mime rules it out (`.csv` is\n * often served as text/plain or octet-stream, so the extension is the reliable signal).\n */\nexport function isCsvFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (CSV_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return CSV_EXT.has(fileExt(name))\n}\n\n// JSON Lines / NDJSON (`.jsonl` / `.ndjson`) — one JSON value per line, routed to the side viewer's record-list\n// preview (parsed in-browser, see lib/jsonl.ts + JsonlViewer.vue). A `.jsonl` is often served as\n// application/json or octet-stream, so the extension is the reliable signal; the explicit jsonl mimes also\n// confirm it. We deliberately DON'T claim bare `.json` (that stays ordinary JSON source). Must be consulted\n// BEFORE the generic text route (a `.jsonl` served as application/json would otherwise read as JSON source).\nconst JSONL_EXT = new Set(['jsonl', 'ndjson'])\nconst JSONL_MIME = new Set([\n 'application/jsonl',\n 'application/x-jsonl',\n 'application/jsonlines',\n 'application/x-jsonlines',\n 'application/x-ndjson',\n 'application/ndjson',\n])\n\n/**\n * Is this a JSON Lines / NDJSON file (`.jsonl` / `.ndjson`)? Such files open in the side viewer's record list,\n * which parses each line independently (see lib/jsonl.ts + JsonlViewer.vue) — a truncated one-line preview per\n * record, click to expand the pretty-printed value, with a render/source toggle. Prefer `mime`, fall back to\n * the extension; a concrete non-jsonl media mime rules it out.\n */\nexport function isJsonlFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (JSONL_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return JSONL_EXT.has(fileExt(name))\n}\n\n// ZIP archive MIME types, including the JVM/Android packages that ARE zip under the hood (.jar/.war/.ear/\n// .apk/.aar). Office docs / epub are ALSO zips but have dedicated viewers, so they're matched by their own\n// detectors earlier and never reach here.\nconst ZIP_MIME = new Set([\n 'application/zip',\n 'application/x-zip',\n 'application/x-zip-compressed',\n 'multipart/x-zip',\n 'application/java-archive', // .jar / .war / .ear all commonly carry this\n 'application/x-java-archive',\n 'application/vnd.android.package-archive', // .apk\n])\n// Extensions we browse as a zip: the plain .zip plus the JVM/Android archive family (all real zip containers,\n// so the same in-browser decompressor + tree browser handles them). NOT docx/xlsx/pptx/epub — those are zips\n// too but route to their own viewers (matched before isZipFile in FileChip).\nconst ZIP_EXT = new Set(['zip', 'jar', 'war', 'ear', 'apk', 'aar'])\n\n/**\n * Is this a ZIP archive (incl. .jar/.war/.ear/.apk/.aar)? Such files open in the side viewer's archive\n * browser, which decompresses them in-browser and lists their contents. Prefer `mime`, fall back to the\n * extension (same precedence as {@link isTextFile}); a concrete non-zip mime settles it, blank/octet-stream\n * defers to the extension.\n */\nexport function isZipFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (ZIP_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return ZIP_EXT.has(fileExt(name))\n}\n\n// 7z archive MIME types. As with ZIP we key off *.7z (the extension is the reliable signal); a 7z is\n// never a zip-under-the-hood container, so there's no docx/jar-style overlap to worry about.\nconst SEVEN_ZIP_MIME = new Set(['application/x-7z-compressed', 'application/x-7z'])\n\n/**\n * Is this a 7z archive? Such files open in the side viewer's 7z browser, which decompresses them\n * in-browser (bundled LZMA decoder — browsers have no native one) and lists their contents. Prefer\n * `mime`, fall back to the extension (same precedence as {@link isZipFile}).\n */\nexport function isSevenZipFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (SEVEN_ZIP_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return fileExt(name) === '7z'\n}\n\n// tar-family MIME types (the compressed wrappers + bare tar). The reliable signal is usually the extension;\n// the stored mime is often octet-stream. We DON'T claim `application/gzip` etc. blindly — they're matched\n// here, but a lone `.svgz`/unknown `.gz` still keys off the extension set below.\nconst TARBALL_MIME = new Set([\n 'application/x-tar',\n 'application/gzip',\n 'application/x-gzip',\n 'application/x-gtar',\n 'application/x-bzip2',\n 'application/x-bzip',\n 'application/x-xz',\n 'application/x-brotli',\n])\n// Extensions we route to the tar-family browser: bare tar, the gzip/xz/bzip2/brotli wrappers, and a lone\n// compressed file (`foo.json.gz`). `fileExt` returns only the last segment, so `.tar.gz` arrives as `gz`.\nconst TARBALL_EXT = new Set([\n 'tar',\n 'gz',\n 'tgz',\n 'taz',\n 'xz',\n 'txz',\n 'bz2',\n 'tbz2',\n 'tbz',\n 'tb2',\n 'br',\n 'tbr',\n])\n\n/**\n * Is this a tar-family archive — `.tar`, a gzip/xz/bzip2/brotli wrapper (`.tar.gz`/`.tgz`/`.tar.xz`/`.txz`/\n * `.tar.bz2`/`.tbz2`/`.tar.br`), or a lone compressed file (`.gz`/`.xz`/`.bz2`/`.br`)? Such files open in the\n * side viewer's archive browser, which decompresses them in-browser (native gzip + the project's LZMA2/bzip2\n * decoders + a WASM Brotli decoder, see lib/tarball.ts) and lists their contents. Prefer `mime`, fall back to\n * the extension (same precedence as {@link isZipFile}); a concrete non-archive media/text mime rules it out.\n */\nexport function isTarballFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (TARBALL_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return TARBALL_EXT.has(fileExt(name))\n}\n\n// RAR archive MIME types. As with the other archives the extension (.rar) is the reliable signal.\nconst RAR_MIME = new Set([\n 'application/vnd.rar',\n 'application/x-rar-compressed',\n 'application/x-rar',\n])\n\n/**\n * Is this a RAR archive? Such files open in the side viewer's archive browser, which decompresses them\n * in-browser via the WASM unrar port (see lib/rar.ts) and lists their contents. Prefer `mime`, fall back to\n * the extension (same precedence as {@link isZipFile}).\n */\nexport function isRarFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (RAR_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return fileExt(name) === 'rar'\n}\n\n// DICOM medical-image MIME types. The reliable signal is usually the extension (.dcm/.dicom); the stored\n// mime is often octet-stream. `application/dicom` is the registered type when present.\nconst DICOM_MIME = new Set(['application/dicom', 'application/dicom+json', 'image/dicom'])\n\n/**\n * Is this a DICOM (.dcm) medical image? Such files open in the side viewer's DICOM viewer, which parses +\n * decodes them in-browser (dicom-parser + bundled WASM codecs for JPEG2000/JPEG-LS, see lib/dicom*) and\n * renders to a canvas with window/level. Prefer `mime`, fall back to the extension; a concrete non-DICOM\n * media/text mime rules it out, blank/octet-stream defers to the extension.\n */\nexport function isDicomFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (DICOM_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n (m.startsWith('image/') && m !== 'image/dicom') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n const ext = fileExt(name)\n return ext === 'dcm' || ext === 'dicom'\n}\n\n// Raster-image / video extensions for inline preview: images get a hover preview card + click-to-zoom\n// lightbox, videos a click-to-play modal. SVG is deliberately NOT here — it opens in the side viewer with a\n// render/source toggle (see {@link isSvgFile}), since reading its markup is often as useful as the picture.\nconst IMAGE_EXT = new Set([\n 'png',\n 'jpg',\n 'jpeg',\n 'gif',\n 'webp',\n 'bmp',\n 'ico',\n 'avif',\n 'apng',\n 'jfif',\n])\nconst VIDEO_EXT = new Set(['mp4', 'm4v', 'webm', 'ogv', 'mov', 'mkv', 'avi'])\n\n/**\n * Is this a raster image we can preview inline (a hover card on its file chip, or full-size in the lightbox)?\n * `<img src=download-url>` works despite the endpoint's octet-stream/attachment/nosniff (those don't bind\n * `<img>` subresources). SVG is excluded (it goes to the side viewer). Prefer `mime`, fall back to extension.\n */\nexport function isImageFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (m === 'image/svg+xml') return false // SVG → side viewer, not the lightbox\n if (m.startsWith('image/')) return true\n if (m.startsWith('text/') || m.startsWith('video/') || m.startsWith('audio/')) return false\n }\n return IMAGE_EXT.has(fileExt(name))\n}\n\n// Raster image formats a plain <img> can't decode — routed to the side viewer's decoding image viewer\n// (RasterViewer.vue): TIFF (utif2), PSD (ag-psd), HEIC/HEIF (heic2any), and camera RAW (best-effort embedded\n// JPEG, see lib/rawPreview.ts). NOTE bmp/ico/avif are NOT here — modern browsers decode those natively, so\n// they stay on the normal image/lightbox path (see {@link isImageFile}).\nexport type RasterKind = 'tiff' | 'psd' | 'heic' | 'raw'\nconst TIFF_EXT = new Set(['tif', 'tiff'])\nconst PSD_EXT = new Set(['psd', 'psb'])\nconst HEIC_EXT = new Set(['heic', 'heif', 'hif'])\n// Common camera RAW extensions (Canon / Nikon / Sony / Adobe / Fuji / Panasonic / Olympus / Samsung / Pentax / Sigma).\nconst RAW_EXT = new Set([\n 'cr2',\n 'cr3',\n 'crw',\n 'nef',\n 'nrw',\n 'arw',\n 'sr2',\n 'srf',\n 'dng',\n 'raf',\n 'rw2',\n 'orf',\n 'srw',\n 'pef',\n 'x3f',\n])\n\n/**\n * Which decoding image viewer (if any) should open this file? Returns the format kind for TIFF / PSD /\n * HEIC / camera-RAW (so RasterViewer can pick its decoder), or null. Prefer `mime`, fall back to the\n * extension. Must be consulted in FileChip BEFORE {@link isImageFile} — a `.tiff` carries `image/tiff`,\n * which would otherwise send it to the lightbox where a plain <img> can't decode it.\n */\nexport function rasterImageKind(name: string, mime?: string | null): RasterKind | null {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (m === 'image/tiff' || m === 'image/x-tiff') return 'tiff'\n if (\n m === 'image/vnd.adobe.photoshop' ||\n m === 'image/x-photoshop' ||\n m === 'application/x-photoshop' ||\n m === 'image/psd'\n ) {\n return 'psd'\n }\n if (m.startsWith('image/heic') || m.startsWith('image/heif')) return 'heic'\n }\n const ext = fileExt(name)\n if (TIFF_EXT.has(ext)) return 'tiff'\n if (PSD_EXT.has(ext)) return 'psd'\n if (HEIC_EXT.has(ext)) return 'heic'\n if (RAW_EXT.has(ext)) return 'raw'\n return null\n}\n\n// E-book formats routed to the side viewer's e-book reader (EbookViewer.vue): EPUB (epub.js), FictionBook\n// (.fb2, hand-rolled), MOBI/AZW3 (best-effort text extraction), and DjVu (.djvu — not previewable, shows a\n// hint). EPUB is a zip and FB2 is XML, but neither collides with the zip/text routes (we only claim these\n// extensions). DjVu carries an `image/*` mime, so like the raster formats this must beat {@link isImageFile}.\nexport type EbookKind = 'epub' | 'fb2' | 'mobi' | 'djvu'\n\n/**\n * Which e-book reader (if any) should open this file — 'epub' / 'fb2' / 'mobi' (also .azw3/.azw/.prc) /\n * 'djvu', or null. Prefer `mime`, fall back to the extension. Consulted in FileChip BEFORE the generic\n * text route (FB2 is XML) and BEFORE {@link isImageFile} (DjVu's mime is image/vnd.djvu).\n */\nexport function ebookKind(name: string, mime?: string | null): EbookKind | null {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (m === 'application/epub+zip') return 'epub'\n if (m === 'application/x-fictionbook+xml' || m === 'application/fb2+xml') return 'fb2'\n if (m === 'application/x-mobipocket-ebook' || m === 'application/vnd.amazon.ebook')\n return 'mobi'\n if (m === 'image/vnd.djvu' || m === 'image/x-djvu') return 'djvu'\n }\n const ext = fileExt(name)\n if (ext === 'epub') return 'epub'\n if (ext === 'fb2') return 'fb2'\n if (ext === 'mobi' || ext === 'azw' || ext === 'azw3' || ext === 'prc') return 'mobi'\n if (ext === 'djvu' || ext === 'djv') return 'djvu'\n return null\n}\n\n/**\n * Is this an SVG? It opens in the side viewer with a render/source toggle (like HTML) — the render view shows\n * the picture, the source view its XML. Prefer `mime`, fall back to the extension.\n */\nexport function isSvgFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') return m === 'image/svg+xml'\n return fileExt(name) === 'svg'\n}\n\n/**\n * Is this a video we can play in the modal preview? Unlike images, a direct `<video src=download-url>` can't\n * decode (octet-stream + nosniff), so the lightbox fetches the bytes and plays them from a typed blob. Prefer\n * `mime`, fall back to the extension.\n */\nexport function isVideoFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (m.startsWith('video/')) return true\n if (m.startsWith('text/') || m.startsWith('image/') || m.startsWith('audio/')) return false\n }\n return VIDEO_EXT.has(fileExt(name))\n}\n\n/**\n * Is this an HTML document? Such files get a render/source toggle in the side viewer — the rendered\n * view runs the markup in a SANDBOXED iframe (see FileViewerPanel). Prefer `mime`, fall back to the\n * extension (same precedence as {@link isTextFile}); never trust an octet-stream/blank mime.\n */\nexport function isHtmlFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n return m === 'text/html' || m === 'application/xhtml+xml'\n }\n const ext = fileExt(name)\n return ext === 'html' || ext === 'htm'\n}\n\n// Native binary objects the symbol browser parses in-browser: PE (Windows) and ELF (Unix). PE covers the\n// executable family (.exe/.dll/.sys/…); ELF covers shared objects, kernel modules and relocatables.\nconst PE_EXTS = ['exe', 'dll', 'sys', 'ocx', 'cpl', 'scr', 'efi', 'drv', 'mui', 'node']\nconst ELF_EXTS = ['elf', 'ko', 'o']\n\n/**\n * Is this a native binary object — a PE (`.exe`/`.dll`/…) or an ELF (`.so`, incl. versioned `libfoo.so.1.2.3`,\n * plus `.elf`/`.ko`/`.o`)? Such files open in the side viewer's symbol browser, which parses them in-browser\n * (see lib/binobj.ts) — listing function symbols and, for PE, executable metadata (version info + icon).\n * Detection is extension-driven: the stored mime is unreliable (usually `application/octet-stream`), so we only\n * rule it out for clearly-textual / media mimes.\n */\nexport function isBinaryObjFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n const ext = fileExt(name)\n if (PE_EXTS.includes(ext) || ELF_EXTS.includes(ext)) return true\n // Shared objects, including versioned names like libfoo.so.1.2.3.\n return /\\.so(\\.\\d+)*$/.test((name || '').toLowerCase())\n}\n\n// E-language (易语言) project files: `.e` source and `.ec` module — binary containers parsed + decompiled\n// entirely in-browser (see lib/epl + EplViewer.vue). The stored mime is unreliable (usually octet-stream),\n// so detection is extension-driven; a clearly textual / media mime rules it out (a `.e` is never text/*).\n/**\n * Is this an e-language `.e` / `.ec` project file? Such files open in the side viewer's e-language viewer,\n * which parses the binary section format and decompiles each method back to readable 易语言 source text\n * (hand-rolled port of OpenEpl/EProjectFile, see lib/epl). Extension-driven; a concrete text/media mime rules\n * it out. The viewer validates the file magic and shows a friendly message for encrypted / non-EPL bytes.\n */\nexport function isEplFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n const ext = fileExt(name)\n return ext === 'e' || ext === 'ec'\n}\n\n// Java bytecode (`.class`) — a big-endian structural container parsed in-browser (see lib/classfile.ts +\n// ClassViewer.vue), listing fields + methods (NOT decompiled). The stored mime is unreliable (usually\n// octet-stream); detection is extension-driven, with the registered JVM mimes also confirming it. A clearly\n// textual / media mime rules it out (a `.class` is never text/*).\nconst JAVA_CLASS_MIME = new Set([\n 'application/java-vm',\n 'application/x-java-vm',\n 'application/java',\n 'application/x-java-class',\n 'application/java-byte-code',\n])\n\n/**\n * Is this a Java `.class` bytecode file? Such files open in the side viewer's class viewer, which parses the\n * structure in-browser (see lib/classfile.ts) — the class signature plus its fields and methods (a lightweight\n * `javap`; no decompilation of method bodies). The viewer validates the 0xCAFEBABE magic and shows a friendly\n * message otherwise. Extension-driven (the stored mime is unreliable); a concrete text/media mime rules it out.\n */\nexport function isJavaClassFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (JAVA_CLASS_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return fileExt(name) === 'class'\n}\n\n// JVM heap dump (`.hprof`) — a big-endian record stream stream-parsed in-browser (see lib/hprof.ts +\n// HprofViewer.vue) into a class histogram + per-class object drill-in. The stored mime is unreliable (usually\n// octet-stream); detection is extension-driven, with the registered java hprof mime also confirming it. A\n// clearly textual / media mime rules it out (an .hprof is never text/*).\nconst HPROF_MIME = new Set(['application/vnd.java.hprof', 'application/x-hprof'])\n\n/**\n * Is this a JVM heap dump (`.hprof`)? Such files open in the side viewer's HPROF viewer, which STREAM-parses the\n * dump in-browser (see lib/hprof.ts) — a class histogram (instances + shallow size) plus drill-in to one class's\n * objects with decoded fields, paginated + filterable so a multi-GB dump stays memory-bounded. Extension-driven\n * (the stored mime is unreliable); a concrete text/media mime rules it out.\n */\nexport function isHprofFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (HPROF_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return fileExt(name) === 'hprof'\n}\n\n// Windows Installer database (`.msi`, plus the `.msp` patch / `.msm` merge module) — an OLE2 compound file\n// (NOT a PE; see isBinaryObjFile), parsed in-browser (see lib/msi.ts + MsiViewer.vue) to surface the product\n// identity + summary info. The stored mime is unreliable (usually octet-stream); detection is extension-driven,\n// with the registered MSI mime also confirming it. A clearly textual / media mime rules it out.\nconst MSI_MIME = new Set(['application/x-msi', 'application/vnd.ms-msi'])\nconst MSI_EXT = new Set(['msi', 'msp', 'msm'])\n\n/**\n * Is this a Windows Installer `.msi` (or `.msp`/`.msm`)? Such files open in the side viewer's MSI viewer,\n * which parses the OLE compound-file structure in-browser (see lib/msi.ts) — the product name/version/codes\n * from the Property table plus the SummaryInformation. Prefer `mime`, fall back to the extension; a concrete\n * text/media mime rules it out.\n */\nexport function isMsiFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (MSI_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return MSI_EXT.has(fileExt(name))\n}\n\n// Windows app package (`.appx`/`.msix`, and the `.appxbundle`/`.msixbundle` bundles) — an OPC ZIP archive\n// (NOT a PE, NOT an MSI), parsed in-browser (see lib/appx.ts + AppxViewer.vue) to surface the manifest\n// identity / dependencies / capabilities. An APPX is a zip under the hood, so this MUST be consulted BEFORE\n// the generic zip route (an APPX served as application/zip would otherwise open as a plain archive).\nconst APPX_MIME = new Set([\n 'application/appx',\n 'application/msix',\n 'application/appxbundle',\n 'application/msixbundle',\n 'application/vnd.ms-appx',\n 'application/windows.appx',\n])\nconst APPX_EXT = new Set(['appx', 'msix', 'appxbundle', 'msixbundle'])\n\n/**\n * Is this a Windows app package (`.appx`/`.msix`/`.appxbundle`/`.msixbundle`)? Such files open in the side\n * viewer's APPX viewer, which unzips the manifest in-browser (see lib/appx.ts) and shows the package identity,\n * dependencies, capabilities and applications. Prefer `mime`, fall back to the extension; a concrete text/media\n * mime rules it out. Must beat {@link isZipFile} (an APPX is a zip container).\n */\nexport function isAppxFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (APPX_MIME.has(m)) return true\n if (m.startsWith('text/') || m.startsWith('image/') || m.startsWith('audio/')) return false\n }\n return APPX_EXT.has(fileExt(name))\n}\n\n// Word document (OOXML) MIME. Only the modern `.docx` (a zip-under-the-hood OOXML package) is renderable —\n// the legacy binary `.doc` is NOT supported by the in-browser renderer (docx-preview), so it's excluded.\nconst DOCX_MIME = new Set([\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml',\n])\n\n/**\n * Is this a Word `.docx` document? Such files open in the side viewer's Word viewer, which unpacks +\n * renders the OOXML to HTML in-browser (docx-preview, see DocxViewer.vue) — a paginated, paper-like\n * preview. Prefer `mime`, fall back to the extension; a concrete non-docx media/text mime rules it out,\n * blank/octet-stream defers to the extension. Legacy `.doc` is deliberately NOT matched (unsupported).\n */\nexport function isDocxFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (DOCX_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return fileExt(name) === 'docx'\n}\n\n// Excel spreadsheet (OOXML) MIME. Only the modern zip-under-the-hood `.xlsx`/`.xlsm` is renderable (exceljs\n// parses SpreadsheetML); the legacy binary `.xls` is NOT supported, so it's excluded.\nconst XLSX_MIME = new Set([\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml',\n 'application/vnd.ms-excel.sheet.macroenabled.12',\n])\n\n/**\n * Is this an Excel `.xlsx` / `.xlsm` spreadsheet? Such files open in the side viewer's spreadsheet viewer,\n * which unpacks + renders the workbook to HTML tables in-browser (exceljs, see XlsxViewer.vue) — one table\n * per sheet with a tab bar. Prefer `mime`, fall back to the extension; a concrete non-xlsx media/text mime\n * rules it out, blank/octet-stream defers to the extension. Legacy `.xls` is deliberately NOT matched.\n */\nexport function isXlsxFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (XLSX_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n const ext = fileExt(name)\n return ext === 'xlsx' || ext === 'xlsm'\n}\n\n// PowerPoint presentation (OOXML) MIME. Only the modern zip-under-the-hood `.pptx`/`.pptm`/`.ppsx` is\n// renderable (@aiden0z/pptx-renderer parses PresentationML); the legacy binary `.ppt` is NOT supported, so\n// it's excluded. `.macroenabled.12` is the lowercased form of the registered `.macroEnabled.12` types.\nconst PPTX_MIME = new Set([\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml',\n 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',\n 'application/vnd.ms-powerpoint.presentation.macroenabled.12',\n 'application/vnd.ms-powerpoint.slideshow.macroenabled.12',\n])\n\n/**\n * Is this a PowerPoint `.pptx` / `.pptm` / `.ppsx` presentation? Such files open in the side viewer's slide\n * viewer, which unpacks + renders the deck to HTML/SVG in-browser (@aiden0z/pptx-renderer, see PptxViewer.vue)\n * — a scrollable list of slides. Prefer `mime`, fall back to the extension; a concrete non-pptx media/text\n * mime rules it out, blank/octet-stream defers to the extension. Legacy `.ppt` is deliberately NOT matched.\n */\nexport function isPptxFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (PPTX_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n const ext = fileExt(name)\n return ext === 'pptx' || ext === 'pptm' || ext === 'ppsx'\n}\n\n// Rich Text Format MIME types. The reliable signal is the extension (.rtf); the stored mime is often octet-\n// stream or text/plain. `application/rtf` is the registered type, `text/rtf` a common variant.\nconst RTF_MIME = new Set(['application/rtf', 'text/rtf', 'application/x-rtf', 'text/richtext'])\n\n/**\n * Is this a Rich Text Format `.rtf` document? Such files open in the side viewer's rich-text viewer, which\n * interprets the RTF in-browser (hand-rolled, see lib/rtf.ts + RtfViewer.vue) — bold/italic/underline, colors,\n * fonts, alignment and unicode rendered onto a paper-like sheet. Prefer `mime`, fall back to the extension; a\n * concrete non-rtf media mime rules it out, blank/octet-stream/text defers to the extension (an .rtf is often\n * mislabeled text/plain). Must be consulted BEFORE the generic text route so it renders, not shows raw markup.\n */\nexport function isRtfFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (RTF_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return fileExt(name) === 'rtf'\n}\n\n// PDF MIME types. The reliable signal is usually the extension (.pdf); `application/pdf` is the registered\n// type, `application/x-pdf` an older variant.\nconst PDF_MIME = new Set(['application/pdf', 'application/x-pdf', 'application/acrobat'])\n\n/**\n * Is this a PDF document? Such files open in the side viewer's PDF viewer, which renders each page to a\n * canvas in-browser (pdfjs-dist, see PdfViewer.vue) — a vertically scrollable list of pages. Prefer `mime`,\n * fall back to the extension; a concrete non-pdf media/text mime rules it out, blank/octet-stream defers to\n * the extension.\n */\nexport function isPdfFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (PDF_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return fileExt(name) === 'pdf'\n}\n\n// Outline / mind-map MIME types. The reliable signal is the extension (.opml/.mm); these MIME types appear\n// occasionally. `.mm` shares its extension with Objective-C++ source, so a match here is only a *candidate* —\n// the side panel content-sniffs the loaded text (looksLikeMindMap) before drawing the diagram.\nconst MINDMAP_MIME = new Set([\n 'text/x-opml',\n 'text/x-opml+xml',\n 'application/xml+opml',\n 'application/x-freemind',\n 'application/x-freeplane',\n])\n\n/**\n * Is this an outline / mind-map file — OPML (.opml) or FreeMind / Freeplane (.mm)? Such files open in the side\n * viewer with a render/source toggle: render draws an interactive mind map (see MindMapView.vue), source shows\n * the XML. Detection is extension-driven (the stored mime is unreliable); a concrete media mime rules it out.\n * NOTE: `.mm` is also the Objective-C++ source extension — the panel additionally sniffs the loaded text\n * (looksLikeMindMap) before rendering the diagram, so a non-mind-map `.mm` just reads as highlighted source.\n */\nexport function isMindMapFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (MINDMAP_MIME.has(m)) return true\n if (\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/') ||\n m === 'text/html'\n ) {\n return false\n }\n }\n const ext = fileExt(name)\n return ext === 'opml' || ext === 'mm'\n}\n\n// XMind workbook (.xmind) — a ZIP container (content.json modern / content.xml legacy) rendered as a mind map\n// (see lib/xmind.ts + XmindViewer.vue, reusing MindMapView). It's a zip under the hood, so this MUST be consulted\n// BEFORE the generic zip route. Extension-driven (the stored mime is unreliable); the workbook mimes confirm it.\nconst XMIND_MIME = new Set([\n 'application/vnd.xmind.workbook',\n 'application/x-xmind',\n 'application/xmind',\n])\n\n/**\n * Is this an XMind workbook (`.xmind`)? Such files open in the side viewer's XMind viewer, which unzips the\n * `content.json` / `content.xml` in-browser (see lib/xmind.ts) and renders it as an interactive mind map (shared\n * MindMapView). Prefer `mime`, fall back to the extension; a concrete media/text mime rules it out. Must beat\n * {@link isZipFile} (an XMind is a zip container).\n */\nexport function isXmindFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (XMIND_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return fileExt(name) === 'xmind'\n}\n\n// DXF (AutoCAD Drawing Exchange Format) — an ASCII CAD drawing, rendered as an SVG (see lib/dxf.ts +\n// DxfViewer.vue). `.dxf` is textual, so this MUST be consulted BEFORE the generic text route so it opens as a\n// drawing, not raw source. NOTE: `.dwg` (the binary AutoCAD format) is deliberately NOT claimed — it's a\n// different, unsupported format. Extension-driven; the registered dxf mimes also confirm it.\nconst DXF_MIME = new Set(['image/vnd.dxf', 'application/dxf', 'application/x-dxf', 'drawing/x-dxf'])\n\n/**\n * Is this a DXF (`.dxf`) CAD drawing? Such files open in the side viewer's DXF viewer, which parses the ASCII\n * DXF in-browser (see lib/dxf.ts) and draws its entities as an SVG (pan/zoom + layer toggles). Prefer `mime`,\n * fall back to the extension; a concrete non-dxf media mime rules it out (a `.dxf` is often served text/plain\n * or octet-stream, so the extension is the reliable signal).\n */\nexport function isDxfFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (DXF_MIME.has(m)) return true\n if (m.startsWith('audio/') || m.startsWith('video/') || m.startsWith('image/')) {\n if (!DXF_MIME.has(m)) return false\n }\n }\n return fileExt(name) === 'dxf'\n}\n\n// Email (.eml) MIME types. `message/rfc822` is the registered type for a raw RFC 822 message; the stored\n// mime is often octet-stream or even text/plain, so the extension (.eml) is the reliable signal. We do NOT\n// rule out text/* here (some servers label .eml as text/plain) — only clearly-binary media families.\nconst EML_MIME = new Set(['message/rfc822', 'application/eml', 'text/eml', 'message/global'])\n\n/**\n * Is this an email `.eml` (RFC 822 / MIME) message? Such files open in the side viewer's email viewer, which\n * parses the MIME tree in-browser (see lib/eml.ts + EmlViewer.vue) — a header card, the best body (HTML in a\n * sandboxed iframe, plain-text fallback), and the attachments. Prefer `mime`, fall back to the extension; a\n * concrete non-eml media mime rules it out, blank/octet-stream/text defers to the extension.\n */\nexport function isEmlFile(name: string, mime?: string | null): boolean {\n // A .mht/.mhtml web archive can also be served as message/rfc822, but it's MHT, not an email (see isMhtFile).\n const ext = fileExt(name)\n if (ext === 'mht' || ext === 'mhtml') return false\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (EML_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return ext === 'eml'\n}\n\n/**\n * Is this a web archive `.mht`/`.mhtml` (RFC 2557 MIME container)? Such files open in the side viewer's web-\n * archive viewer, which parses the `multipart/related` container in-browser (see lib/mht.ts + MhtViewer.vue)\n * and renders the saved page — resources rewritten to blob URLs — in a sandboxed iframe. Extension-driven (the\n * stored mime is unreliable); the telltale container mimes (multipart/related, application/x-mimearchive)\n * also confirm it.\n */\nexport function isMhtFile(name: string, mime?: string | null): boolean {\n const ext = fileExt(name)\n if (ext === 'mht' || ext === 'mhtml') return true\n const m = (mime || '').toLowerCase().trim()\n return m === 'multipart/related' || m === 'application/x-mimearchive'\n}\n\n// HAR (HTTP Archive) MIME types. A .har is JSON under the hood, so it's often served as application/json or\n// octet-stream — the extension (.har) is the reliable signal. The registered type is application/json; some\n// tools use application/x-har.\nconst HAR_MIME = new Set(['application/har+json', 'application/x-har'])\n\n/**\n * Is this a HAR (`.har`, HTTP Archive 1.2) capture? Such files open in the side viewer's HAR viewer, which\n * parses the JSON archive in-browser (hand-rolled, see lib/har.ts + HarViewer.vue) — a filterable request\n * list with per-request headers / query / cookies / bodies. Extension-driven (a .har is JSON, so the stored\n * mime is usually application/json or octet-stream); a concrete non-har media mime rules it out. Must be\n * checked BEFORE the generic text route (a .har served as application/json would otherwise read as JSON source).\n */\nexport function isHarFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (HAR_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return fileExt(name) === 'har'\n}\n\n// SQLite database MIME types. The reliable signal is the extension; the stored mime is usually octet-stream.\nconst SQLITE_MIME = new Set([\n 'application/x-sqlite3',\n 'application/vnd.sqlite3',\n 'application/x-sqlite',\n])\n// Extensions we route to the SQLite viewer: the conventional database suffixes.\nconst SQLITE_EXT = new Set(['sqlite', 'sqlite3', 'db', 'db3', 'sqlitedb'])\n\n/**\n * Is this a SQLite database (`.sqlite`/`.db`/`.sqlite3`/…)? Such files open in the side viewer's SQLite viewer,\n * which loads them with sql.js (WASM) in-browser (see lib/sqlite.ts + SqliteViewer.vue) — a table picker, a\n * paginated data grid, and a SQL console. Prefer `mime`, fall back to the extension; a concrete textual/media\n * mime rules it out (`.db` is a generic suffix, so a clearly-text mime means it isn't a database).\n */\nexport function isSqliteFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (SQLITE_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return SQLITE_EXT.has(fileExt(name))\n}\n\n// Certificate / key formats routed to the side viewer's certificate visualizer (parsed in-browser, hand-rolled\n// ASN.1/X.509, see lib/asn1.ts + lib/x509.ts + CertViewer.vue): PEM (.pem/.crt/.cer/.csr/.key, also PKCS#7\n// bundles) and raw DER (.der/.cer). The viewer fetches the BYTES (binary-safe), so `.der` isn't mangled.\n// Detection is extension-driven (the stored mime is unreliable); a few registered cert mimes also confirm it.\n// NOTE: PKCS#12 (`.p12`/`.pfx`) is a KEYSTORE, not a bare cert — routed to the keystore viewer ({@link isJksFile}).\nconst CERT_EXT = new Set(['pem', 'crt', 'cer', 'der', 'csr', 'req', 'key', 'p7b', 'p7c'])\nconst CERT_MIME = new Set([\n 'application/x-x509-ca-cert',\n 'application/x-x509-user-cert',\n 'application/x-x509-cert',\n 'application/pkix-cert',\n 'application/pkix-crl',\n 'application/x-pem-file',\n 'application/pkcs8',\n 'application/pkcs10',\n 'application/x-pkcs7-certificates',\n])\n\n/**\n * Is this a certificate / key file (`.pem`/`.crt`/`.cer`/`.der`/`.csr`/`.key`/`.p7b`/…)? Such files open\n * in the side viewer's certificate visualizer, which parses the ASN.1 / X.509 in-browser (see lib/x509.ts +\n * CertViewer.vue) — subject/issuer, validity, key + signature algorithms, SANs, fingerprint, and a raw ASN.1\n * structure tree. Prefer `mime`, fall back to the extension; a concrete non-cert media/text mime rules it out.\n * PKCS#12 (`.p12`/`.pfx`) is handled by the keystore viewer ({@link isJksFile}), not here.\n */\nexport function isCertFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (CERT_MIME.has(m)) return true\n // Don't rule out text/* — a PEM (.pem/.crt/.key) is often served as text/plain; the extension settles it.\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return CERT_EXT.has(fileExt(name))\n}\n\n// Keystore formats routed to the side viewer's keystore viewer (parsed in-browser, hand-rolled — see lib/jks.ts,\n// lib/pkcs12.ts + JksViewer.vue): the Sun JKS/JCEKS binary format (`.jks`/`.keystore`/`.ks`/`.truststore`/`.jceks`,\n// magic 0xFEEDFEED / 0xCECECECE) AND PKCS#12 / PFX (`.p12`/`.pfx`). The `.jks` extension is claimed even though\n// modern keytool writes PKCS#12 into it — the viewer sniffs the actual bytes and picks the right parser, so a\n// mislabeled file still opens. Detection is extension-driven (the stored mime is unreliable, usually\n// octet-stream); a concrete text/media mime rules it out (a keystore is never text/*).\nconst JKS_MIME = new Set([\n 'application/x-java-keystore',\n 'application/x-java-jce-keystore',\n 'application/x-pkcs12',\n 'application/pkcs12',\n])\nconst JKS_EXT = new Set(['jks', 'keystore', 'ks', 'jceks', 'truststore', 'p12', 'pfx'])\n\n/**\n * Is this a keystore — Java JKS/JCEKS (`.jks`/`.keystore`/`.ks`/`.jceks`/`.truststore`) or PKCS#12 (`.p12`/\n * `.pfx`)? Such files open in the side viewer's keystore viewer, which sniffs the format and parses it in-browser\n * (see lib/jks.ts / lib/pkcs12.ts) — the entry inventory (aliases / kinds / dates), each entry's X.509 chain,\n * and, with a password, store-integrity verification + private-key decryption. Prefer `mime`, fall back to the\n * extension; a concrete text/media mime rules it out.\n */\nexport function isJksFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (JKS_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return JKS_EXT.has(fileExt(name))\n}\n\n// Geodata formats routed to the side viewer's offline SVG map (parsed in-browser, see lib/geo.ts +\n// GeoViewer.vue): GeoJSON (.geojson), KML (.kml), GPX (.gpx). `.geojson`/`.kml`/`.gpx` are also textual, so\n// this must be consulted BEFORE the generic text route (in FileChip) so they open as a map, not raw source.\n// Extension-driven; we deliberately DON'T claim bare `.json` (most JSON isn't geodata) — only `.geojson`.\nconst GEO_EXT = new Set(['geojson', 'kml', 'gpx'])\nconst GEO_MIME = new Set([\n 'application/geo+json',\n 'application/vnd.geo+json',\n 'application/vnd.google-earth.kml+xml',\n 'application/gpx+xml',\n 'application/x-gpx+xml',\n])\n\n/**\n * Is this a geodata file (`.geojson`/`.kml`/`.gpx`)? Such files open in the side viewer's map viewer, which\n * parses the geometry in-browser (see lib/geo.ts + GeoViewer.vue) and draws it as an OFFLINE SVG map (no tile\n * server) with pan/zoom and a render/source toggle. Prefer `mime`, fall back to the extension; a concrete\n * non-geo media mime rules it out.\n */\nexport function isGeoFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (GEO_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return GEO_EXT.has(fileExt(name))\n}\n\n// Subtitle / caption formats routed to the side viewer's subtitle transcript (parsed in-browser, see\n// lib/subtitle.ts + SubtitleViewer.vue): SubRip (.srt), WebVTT (.vtt), SubStation Alpha (.ass/.ssa).\n// `.srt`/`.vtt` are ALSO plain text, so this must be consulted BEFORE the generic text route (in FileChip)\n// so they open as a cue list, not raw source. Extension-driven (the stored mime is unreliable).\nconst SUBTITLE_EXT = new Set(['srt', 'vtt', 'ass', 'ssa'])\nconst SUBTITLE_MIME = new Set([\n 'application/x-subrip',\n 'application/x-subrip+srt',\n 'text/vtt',\n 'text/x-ssa',\n 'text/x-ass',\n])\n\n/**\n * Is this a subtitle / caption file (`.srt`/`.vtt`/`.ass`/`.ssa`)? Such files open in the side viewer's\n * subtitle viewer, which parses the cues in-browser (see lib/subtitle.ts + SubtitleViewer.vue) and shows\n * them as a timecoded transcript (with a render/source toggle). Prefer `mime`, fall back to the extension;\n * a concrete non-subtitle media mime rules it out, blank/octet-stream/text defers to the extension.\n */\nexport function isSubtitleFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (SUBTITLE_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return SUBTITLE_EXT.has(fileExt(name))\n}\n\n// 3D model formats routed to the side viewer's model viewer (rendered with three.js, see ModelViewer.vue):\n// STL (.stl), Wavefront OBJ (.obj), PLY (.ply), glTF (.gltf) and binary glTF (.glb). Extension-driven (the\n// stored mime is unreliable; `model/*` mimes also confirm it).\nconst MODEL_EXT = new Set(['stl', 'obj', 'ply', 'glb', 'gltf'])\nconst MODEL_MIME = new Set([\n 'model/gltf-binary',\n 'model/gltf+json',\n 'model/stl',\n 'application/sla',\n 'application/vnd.ms-pki.stl',\n 'model/obj',\n 'model/ply',\n])\n\n/**\n * Is this a 3D model (`.stl`/`.obj`/`.ply`/`.glb`/`.gltf`)? Such files open in the side viewer's model viewer,\n * which renders them with three.js in-browser (see ModelViewer.vue) — auto-framed, lit, with orbit controls.\n * Prefer `mime` (`model/*`), fall back to the extension; a concrete text/image/audio/video mime rules it out.\n */\nexport function isModelFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (MODEL_MIME.has(m) || m.startsWith('model/')) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return MODEL_EXT.has(fileExt(name))\n}\n\n// Audio formats routed to the side viewer's audio player (played via a typed Blob URL through a native\n// <audio> element, since the download endpoint's octet-stream/attachment defeats a direct src — see\n// AudioViewer.vue): mp3/wav/flac/ogg/m4a/aac/opus. `.mid`/`.midi` are included too but can't be PLAYED\n// in-browser (no synth) — the viewer shows them as a piano-roll visualization (best effort).\n// `.amr`/`.awb`/`.silk`/`.slk` (3GPP AMR + Tencent SILK 微信/QQ voice) are also routed here: browsers can't\n// decode them natively, so AudioViewer DECODES the bytes to WAV in-browser (lib/speechAudio.ts, WASM) and\n// plays that. Detection at decode time is by file-header magic (a WeChat `.amr` is often actually SILK).\nconst AUDIO_EXT = new Set([\n 'mp3',\n 'wav',\n 'flac',\n 'ogg',\n 'oga',\n 'm4a',\n 'aac',\n 'opus',\n 'weba',\n 'mid',\n 'midi',\n 'amr',\n 'awb',\n 'silk',\n 'slk',\n])\n\n/**\n * Is this an audio file (mp3/wav/flac/ogg/m4a/aac/opus, a .mid/.midi, or an AMR/SILK voice clip)? Such files\n * open in the side viewer's audio player, which fetches the bytes and plays them via a typed Blob URL (MIDI is\n * visualized as a piano roll, not played; AMR/SILK are decoded to WAV in-browser first). Prefer `mime`\n * (`audio/*`), fall back to the extension; a concrete text/image/video mime rules it out.\n */\nexport function isAudioFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (m.startsWith('audio/')) return true\n if (m.startsWith('text/') || m.startsWith('image/') || m.startsWith('video/')) return false\n }\n return AUDIO_EXT.has(fileExt(name))\n}\n\n// Jupyter Notebook (.ipynb) — JSON under the hood, so often served as application/json or octet-stream; the\n// extension is the reliable signal. The registered type is application/x-ipynb+json. Must be checked BEFORE\n// the generic text route (a .ipynb served as application/json would otherwise read as JSON source).\nconst NOTEBOOK_MIME = new Set(['application/x-ipynb+json', 'application/ipynb+json'])\n\n/**\n * Is this a Jupyter Notebook (`.ipynb`)? Such files open in the side viewer's notebook viewer, which parses\n * the JSON in-browser (hand-rolled, see lib/ipynb.ts + NotebookViewer.vue) — markdown cells via the app's\n * markdown engine, code cells highlighted, outputs (stream / image / rich HTML / error) inline. Extension-\n * driven (a .ipynb is JSON, so the stored mime is usually application/json or octet-stream); a concrete\n * non-notebook media mime rules it out.\n */\nexport function isNotebookFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (NOTEBOOK_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return fileExt(name) === 'ipynb'\n}\n\n// Font formats routed to the side viewer's specimen viewer (loaded with the native FontFace API, which\n// decodes ttf/otf/woff AND woff2 in-browser — no library; see FontViewer.vue): TrueType (.ttf), OpenType\n// (.otf), and the web wrappers (.woff/.woff2). Extension-driven (stored mime is unreliable).\nconst FONT_EXT = new Set(['ttf', 'otf', 'woff', 'woff2'])\nconst FONT_MIME = new Set([\n 'font/ttf',\n 'font/otf',\n 'font/woff',\n 'font/woff2',\n 'font/sfnt',\n 'application/font-woff',\n 'application/font-woff2',\n 'application/x-font-ttf',\n 'application/x-font-otf',\n 'application/x-font-truetype',\n 'application/x-font-opentype',\n 'application/font-sfnt',\n 'application/vnd.ms-opentype',\n])\n\n/**\n * Is this a font file (`.ttf`/`.otf`/`.woff`/`.woff2`)? Such files open in the side viewer's font specimen,\n * which loads them with the native FontFace API in-browser (see FontViewer.vue) — an editable preview line,\n * a size waterfall, and the glyph sets. Prefer `mime`, fall back to the extension; a concrete non-font\n * media/text mime rules it out, blank/octet-stream defers to the extension.\n */\nexport function isFontFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (FONT_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return FONT_EXT.has(fileExt(name))\n}\n\n// QQ 音乐加密音频(QMCv2,文件尾内嵌密钥)—— 浏览器内解密后用 <audio> 播放(见 lib/qmc.ts + QmcAudioViewer.vue)。\n// 涵盖 .mflac(FLAC)/.mgg(OGG)及其音质分级变体;这些都带内嵌 EKey,可独立解出。早期 QMCv1 静态密钥格式\n// (.qmcflac/.qmc0…)不在此列(无内嵌密钥)。存储 mime 一律不可信(多为 octet-stream),故按扩展名判定。\nconst QMC_EXT = new Set(['mflac', 'mflac0', 'mflac1', 'mflaca', 'mgg', 'mgg0', 'mgg1', 'mggl'])\n\n/**\n * Is this a QQ Music encrypted audio file (`.mflac`/`.mgg`/…)? Such files open in the side viewer's QMC\n * player, which decrypts the bytes in-browser (see lib/qmc.ts) — recovering the embedded key, undoing the\n * QMCv2 stream cipher, and playing the resulting FLAC/OGG/MP3 via a typed Blob (with a \"download decrypted\"\n * action). Extension-driven (the stored mime is unreliable, usually octet-stream); a clearly textual/media\n * mime rules it out.\n */\nexport function isQmcAudioFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (m.startsWith('text/') || m.startsWith('image/') || m.startsWith('video/')) return false\n }\n return QMC_EXT.has(fileExt(name))\n}\n\n// LRC 歌词(`.lrc`)—— 行式带时间戳歌词,在侧栏渲染成同步歌词列表(见 lib/lrc.ts + LrcViewer.vue)。\n// 这类文件常见 GBK 编码且夹带 HTML 实体,故查看器自取字节、嗅探编码(见 lib/encoding.ts),不能当 UTF-8 文本直读。\nconst LRC_MIME = new Set(['application/lrc', 'text/lrc', 'application/x-lrc'])\n\n/**\n * Is this an LRC lyrics file (`.lrc`)? Such files open in the side viewer's lyrics viewer, which parses the\n * timestamped lines in-browser (see lib/lrc.ts + LrcViewer.vue) and shows a synced lyric list. Extension-\n * driven; a concrete non-text media mime rules it out. NOTE: deliberately routed BEFORE the generic text\n * route (the viewer self-fetches so it can sniff GBK — a `.lrc` read as UTF-8 text would be mojibake).\n */\nexport function isLrcFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (LRC_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return fileExt(name) === 'lrc'\n}\n\n/** highlight.js language id for this file, or '' for plain text (no highlighting). */\nexport function hljsLang(name: string, mime?: string | null): string {\n const ext = fileExt(name)\n if (ext && ext in EXT_LANG) return EXT_LANG[ext]\n\n const m = (mime || '').toLowerCase().trim()\n if (m === 'text/markdown' || m === 'application/markdown') return 'markdown'\n if (m === 'application/json' || m === 'application/ld+json') return 'json'\n if (\n m === 'application/xml' ||\n m === 'application/xhtml+xml' ||\n m === 'image/svg+xml' ||\n m === 'text/html'\n ) {\n return 'xml'\n }\n if (m.includes('yaml')) return 'yaml'\n if (m === 'text/css') return 'css'\n if (m === 'application/javascript' || m === 'application/ecmascript') return 'javascript'\n\n const lower = (name || '').toLowerCase()\n if (lower === 'dockerfile') return 'dockerfile'\n if (lower === 'makefile') return 'makefile'\n return ''\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fileType.js","names":[],"sources":["../../src/lib/fileType.ts"],"sourcesContent":["/**\n * File-type detection for the conversation file UIs.\n *\n * The proxy's stored `content_type` is unreliable (often `application/octet-stream`), so we never\n * trust it. Instead we prefer the share JSON's own `mime_type` (present on delivered `present_files`),\n * and fall back to the filename extension — which is all we have for user uploads / intermediate files.\n *\n * Two questions this answers:\n * - {@link isTextFile}: can we open it in the side viewer? (non-text files are download-only)\n * - {@link hljsLang}: which highlight.js language to colorize with ('' = plain, no highlighting)\n */\n\n/** Lowercased file extension without the dot, or '' if the name has none. */\nexport function fileExt(name: string): string {\n const base = (name || '').split(/[\\\\/]/).pop() ?? ''\n const dot = base.lastIndexOf('.')\n return dot > 0 ? base.slice(dot + 1).toLowerCase() : ''\n}\n\n// Extension → highlight.js language id. Covers the common code/markup types; anything textual but\n// unlisted renders as plain escaped text (see hljsLang returning '').\nconst EXT_LANG: Record<string, string> = {\n js: 'javascript',\n mjs: 'javascript',\n cjs: 'javascript',\n jsx: 'javascript',\n ts: 'typescript',\n tsx: 'typescript',\n mts: 'typescript',\n cts: 'typescript',\n py: 'python',\n pyw: 'python',\n rb: 'ruby',\n rs: 'rust',\n kt: 'kotlin',\n kts: 'kotlin',\n java: 'java',\n c: 'c',\n h: 'c',\n cc: 'cpp',\n cpp: 'cpp',\n cxx: 'cpp',\n hpp: 'cpp',\n hh: 'cpp',\n cs: 'csharp',\n go: 'go',\n php: 'php',\n swift: 'swift',\n scala: 'scala',\n sh: 'bash',\n bash: 'bash',\n zsh: 'bash',\n fish: 'bash',\n ps1: 'powershell',\n psm1: 'powershell',\n bat: 'dos',\n cmd: 'dos',\n yml: 'yaml',\n yaml: 'yaml',\n toml: 'ini',\n ini: 'ini',\n cfg: 'ini',\n conf: 'ini',\n properties: 'properties',\n json: 'json',\n json5: 'json',\n xml: 'xml',\n html: 'xml',\n htm: 'xml',\n svg: 'xml',\n vue: 'xml',\n svelte: 'xml',\n // Outline / mind-map XML — viewable + XML-highlighted in source view (`.mm` is also Objective-C++, but the\n // mind-map render is content-sniffed, so a non-XML `.mm` just reads as XML-tinted source — see isMindMapFile).\n opml: 'xml',\n mm: 'xml',\n css: 'css',\n scss: 'scss',\n sass: 'scss',\n less: 'less',\n sql: 'sql',\n md: 'markdown',\n markdown: 'markdown',\n gradle: 'gradle',\n groovy: 'groovy',\n lua: 'lua',\n r: 'r',\n dart: 'dart',\n pl: 'perl',\n diff: 'diff',\n patch: 'diff',\n}\n\n// Textual extensions we let the viewer open but don't syntax-highlight (rendered as plain escaped text).\n// NOTE: `.rtf` is deliberately NOT here — it renders as rich text in the side viewer (see isRtfFile), not source.\nconst PLAIN_EXT = new Set([\n 'txt',\n 'text',\n 'log',\n 'csv',\n 'tsv',\n 'env',\n 'lock',\n 'gitignore',\n 'srt',\n 'vtt',\n])\n\n// Extension-less filenames that are nonetheless text.\nconst PLAIN_NAMES = new Set([\n 'dockerfile',\n 'makefile',\n 'license',\n 'readme',\n 'changelog',\n 'authors',\n '.gitignore',\n '.env',\n])\n\n// application/* MIME types that are actually text (text/* is handled generically).\nconst TEXTUAL_APP_MIME = new Set([\n 'application/json',\n 'application/ld+json',\n 'application/xml',\n 'application/xhtml+xml',\n 'application/javascript',\n 'application/ecmascript',\n 'application/x-sh',\n 'application/x-shellscript',\n 'application/x-python',\n 'application/x-python-code',\n 'application/x-yaml',\n 'application/yaml',\n 'application/toml',\n 'application/x-toml',\n 'application/sql',\n 'application/graphql',\n 'application/x-httpd-php',\n 'application/csv',\n 'application/markdown',\n 'image/svg+xml',\n])\n\n/** Can this file be opened in the side viewer? Prefer `mime`, fall back to the extension. */\nexport function isTextFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (m.startsWith('text/')) return true\n if (TEXTUAL_APP_MIME.has(m)) return true\n // Clearly-binary families are never viewable, regardless of extension.\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n // Other application/* (pdf, zip, msword, …): fall through to the extension as a tiebreak.\n }\n const ext = fileExt(name)\n if (ext) return ext in EXT_LANG || PLAIN_EXT.has(ext)\n return PLAIN_NAMES.has((name || '').toLowerCase())\n}\n\n// Any text file at or above this size is routed to the streaming log viewer (virtual-scrolled) instead of\n// the normal source view, so a huge .txt/.csv/.json doesn't lock the page tokenizing one giant block.\nexport const LARGE_TEXT_BYTES = 1024 * 1024\n\n/**\n * Should this open in the side viewer's streaming log viewer (virtual-scrolled, see LogViewer.vue) rather\n * than the normal highlighted source view? True for any `.log` (logs benefit from the line gutter + search),\n * and for any LARGE text file (>= {@link LARGE_TEXT_BYTES}) so big files don't freeze. Consulted in FileChip\n * BEFORE the generic text route. `size` is the manifest byte size when known (0/undefined ⇒ treat as small).\n */\nexport function isLogFile(name: string, mime?: string | null, size?: number | null): boolean {\n if (fileExt(name) === 'log') return true\n return (size ?? 0) >= LARGE_TEXT_BYTES && isTextFile(name, mime)\n}\n\n// CSV / TSV routed to the side viewer's table preview (parsed in-browser, see lib/csv.ts + CsvViewer.vue).\n// `.csv`/`.tsv` are ALSO plain text, so this is consulted in FileChip BEFORE the generic text route — but\n// AFTER the large-text/log route, so a huge CSV still streams through the virtual log viewer instead of\n// building a giant table. Extension-driven; text/csv + the tab-separated mimes also confirm it.\nconst CSV_EXT = new Set(['csv', 'tsv'])\nconst CSV_MIME = new Set(['text/csv', 'application/csv', 'text/tab-separated-values', 'text/tsv'])\n\n/**\n * Is this a CSV / TSV table (`.csv`/`.tsv`)? Such files open in the side viewer's table preview, which parses\n * the rows in-browser (see lib/csv.ts + CsvViewer.vue) with full RFC 4180 quote handling and a render/source\n * toggle. Prefer `mime`, fall back to the extension; a concrete non-text media mime rules it out (`.csv` is\n * often served as text/plain or octet-stream, so the extension is the reliable signal).\n */\nexport function isCsvFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (CSV_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return CSV_EXT.has(fileExt(name))\n}\n\n// JSON Lines / NDJSON (`.jsonl` / `.ndjson`) — one JSON value per line, routed to the side viewer's record-list\n// preview (parsed in-browser, see lib/jsonl.ts + JsonlViewer.vue). A `.jsonl` is often served as\n// application/json or octet-stream, so the extension is the reliable signal; the explicit jsonl mimes also\n// confirm it. We deliberately DON'T claim bare `.json` (that stays ordinary JSON source). Must be consulted\n// BEFORE the generic text route (a `.jsonl` served as application/json would otherwise read as JSON source).\nconst JSONL_EXT = new Set(['jsonl', 'ndjson'])\nconst JSONL_MIME = new Set([\n 'application/jsonl',\n 'application/x-jsonl',\n 'application/jsonlines',\n 'application/x-jsonlines',\n 'application/x-ndjson',\n 'application/ndjson',\n])\n\n/**\n * Is this a JSON Lines / NDJSON file (`.jsonl` / `.ndjson`)? Such files open in the side viewer's record list,\n * which parses each line independently (see lib/jsonl.ts + JsonlViewer.vue) — a truncated one-line preview per\n * record, click to expand the pretty-printed value, with a render/source toggle. Prefer `mime`, fall back to\n * the extension; a concrete non-jsonl media mime rules it out.\n */\nexport function isJsonlFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (JSONL_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return JSONL_EXT.has(fileExt(name))\n}\n\n// ZIP archive MIME types, including the JVM/Android packages that ARE zip under the hood (.jar/.war/.ear/\n// .apk/.aar). Office docs / epub are ALSO zips but have dedicated viewers, so they're matched by their own\n// detectors earlier and never reach here.\nconst ZIP_MIME = new Set([\n 'application/zip',\n 'application/x-zip',\n 'application/x-zip-compressed',\n 'multipart/x-zip',\n 'application/java-archive', // .jar / .war / .ear all commonly carry this\n 'application/x-java-archive',\n 'application/vnd.android.package-archive', // .apk\n])\n// Extensions we browse as a zip: the plain .zip plus the JVM/Android archive family (all real zip containers,\n// so the same in-browser decompressor + tree browser handles them). NOT docx/xlsx/pptx/epub — those are zips\n// too but route to their own viewers (matched before isZipFile in FileChip).\nconst ZIP_EXT = new Set(['zip', 'jar', 'war', 'ear', 'apk', 'aar'])\n\n/**\n * Is this a ZIP archive (incl. .jar/.war/.ear/.apk/.aar)? Such files open in the side viewer's archive\n * browser, which decompresses them in-browser and lists their contents. Prefer `mime`, fall back to the\n * extension (same precedence as {@link isTextFile}); a concrete non-zip mime settles it, blank/octet-stream\n * defers to the extension.\n */\nexport function isZipFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (ZIP_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return ZIP_EXT.has(fileExt(name))\n}\n\n// 7z archive MIME types. As with ZIP we key off *.7z (the extension is the reliable signal); a 7z is\n// never a zip-under-the-hood container, so there's no docx/jar-style overlap to worry about.\nconst SEVEN_ZIP_MIME = new Set(['application/x-7z-compressed', 'application/x-7z'])\n\n/**\n * Is this a 7z archive? Such files open in the side viewer's 7z browser, which decompresses them\n * in-browser (bundled LZMA decoder — browsers have no native one) and lists their contents. Prefer\n * `mime`, fall back to the extension (same precedence as {@link isZipFile}).\n */\nexport function isSevenZipFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (SEVEN_ZIP_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return fileExt(name) === '7z'\n}\n\n// tar-family MIME types (the compressed wrappers + bare tar). The reliable signal is usually the extension;\n// the stored mime is often octet-stream. We DON'T claim `application/gzip` etc. blindly — they're matched\n// here, but a lone `.svgz`/unknown `.gz` still keys off the extension set below.\nconst TARBALL_MIME = new Set([\n 'application/x-tar',\n 'application/gzip',\n 'application/x-gzip',\n 'application/x-gtar',\n 'application/x-bzip2',\n 'application/x-bzip',\n 'application/x-xz',\n 'application/x-brotli',\n])\n// Extensions we route to the tar-family browser: bare tar, the gzip/xz/bzip2/brotli wrappers, and a lone\n// compressed file (`foo.json.gz`). `fileExt` returns only the last segment, so `.tar.gz` arrives as `gz`.\nconst TARBALL_EXT = new Set([\n 'tar',\n 'gz',\n 'tgz',\n 'taz',\n 'xz',\n 'txz',\n 'bz2',\n 'tbz2',\n 'tbz',\n 'tb2',\n 'br',\n 'tbr',\n])\n\n/**\n * Is this a tar-family archive — `.tar`, a gzip/xz/bzip2/brotli wrapper (`.tar.gz`/`.tgz`/`.tar.xz`/`.txz`/\n * `.tar.bz2`/`.tbz2`/`.tar.br`), or a lone compressed file (`.gz`/`.xz`/`.bz2`/`.br`)? Such files open in the\n * side viewer's archive browser, which decompresses them in-browser (native gzip + the project's LZMA2/bzip2\n * decoders + a WASM Brotli decoder, see lib/tarball.ts) and lists their contents. Prefer `mime`, fall back to\n * the extension (same precedence as {@link isZipFile}); a concrete non-archive media/text mime rules it out.\n */\nexport function isTarballFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (TARBALL_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return TARBALL_EXT.has(fileExt(name))\n}\n\n// RAR archive MIME types. As with the other archives the extension (.rar) is the reliable signal.\nconst RAR_MIME = new Set([\n 'application/vnd.rar',\n 'application/x-rar-compressed',\n 'application/x-rar',\n])\n\n/**\n * Is this a RAR archive? Such files open in the side viewer's archive browser, which decompresses them\n * in-browser via the WASM unrar port (see lib/rar.ts) and lists their contents. Prefer `mime`, fall back to\n * the extension (same precedence as {@link isZipFile}).\n */\nexport function isRarFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (RAR_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return fileExt(name) === 'rar'\n}\n\n// DICOM medical-image MIME types. The reliable signal is usually the extension (.dcm/.dicom); the stored\n// mime is often octet-stream. `application/dicom` is the registered type when present.\nconst DICOM_MIME = new Set(['application/dicom', 'application/dicom+json', 'image/dicom'])\n\n/**\n * Is this a DICOM (.dcm) medical image? Such files open in the side viewer's DICOM viewer, which parses +\n * decodes them in-browser (dicom-parser + bundled WASM codecs for JPEG2000/JPEG-LS, see lib/dicom*) and\n * renders to a canvas with window/level. Prefer `mime`, fall back to the extension; a concrete non-DICOM\n * media/text mime rules it out, blank/octet-stream defers to the extension.\n */\nexport function isDicomFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (DICOM_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n (m.startsWith('image/') && m !== 'image/dicom') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n const ext = fileExt(name)\n return ext === 'dcm' || ext === 'dicom'\n}\n\n// Raster-image / video extensions for inline preview: images get a hover preview card + click-to-zoom\n// lightbox, videos a click-to-play modal. SVG is deliberately NOT here — it opens in the side viewer with a\n// render/source toggle (see {@link isSvgFile}), since reading its markup is often as useful as the picture.\nconst IMAGE_EXT = new Set([\n 'png',\n 'jpg',\n 'jpeg',\n 'gif',\n 'webp',\n 'bmp',\n 'ico',\n 'avif',\n 'apng',\n 'jfif',\n])\nconst VIDEO_EXT = new Set(['mp4', 'm4v', 'webm', 'ogv', 'mov', 'mkv', 'avi'])\n\n/**\n * Is this a raster image we can preview inline (a hover card on its file chip, or full-size in the lightbox)?\n * `<img src=download-url>` works despite the endpoint's octet-stream/attachment/nosniff (those don't bind\n * `<img>` subresources). SVG is excluded (it goes to the side viewer). Prefer `mime`, fall back to extension.\n */\nexport function isImageFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (m === 'image/svg+xml') return false // SVG → side viewer, not the lightbox\n if (m.startsWith('image/')) return true\n if (m.startsWith('text/') || m.startsWith('video/') || m.startsWith('audio/')) return false\n }\n return IMAGE_EXT.has(fileExt(name))\n}\n\n// Raster image formats a plain <img> can't decode — routed to the side viewer's decoding image viewer\n// (RasterViewer.vue): TIFF (utif2), PSD (ag-psd), HEIC/HEIF (heic2any), and camera RAW (best-effort embedded\n// JPEG, see lib/rawPreview.ts). NOTE bmp/ico/avif are NOT here — modern browsers decode those natively, so\n// they stay on the normal image/lightbox path (see {@link isImageFile}).\nexport type RasterKind = 'tiff' | 'psd' | 'heic' | 'raw'\nconst TIFF_EXT = new Set(['tif', 'tiff'])\nconst PSD_EXT = new Set(['psd', 'psb'])\nconst HEIC_EXT = new Set(['heic', 'heif', 'hif'])\n// Common camera RAW extensions (Canon / Nikon / Sony / Adobe / Fuji / Panasonic / Olympus / Samsung / Pentax / Sigma).\nconst RAW_EXT = new Set([\n 'cr2',\n 'cr3',\n 'crw',\n 'nef',\n 'nrw',\n 'arw',\n 'sr2',\n 'srf',\n 'dng',\n 'raf',\n 'rw2',\n 'orf',\n 'srw',\n 'pef',\n 'x3f',\n])\n\n/**\n * Which decoding image viewer (if any) should open this file? Returns the format kind for TIFF / PSD /\n * HEIC / camera-RAW (so RasterViewer can pick its decoder), or null. Prefer `mime`, fall back to the\n * extension. Must be consulted in FileChip BEFORE {@link isImageFile} — a `.tiff` carries `image/tiff`,\n * which would otherwise send it to the lightbox where a plain <img> can't decode it.\n */\nexport function rasterImageKind(name: string, mime?: string | null): RasterKind | null {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (m === 'image/tiff' || m === 'image/x-tiff') return 'tiff'\n if (\n m === 'image/vnd.adobe.photoshop' ||\n m === 'image/x-photoshop' ||\n m === 'application/x-photoshop' ||\n m === 'image/psd'\n ) {\n return 'psd'\n }\n if (m.startsWith('image/heic') || m.startsWith('image/heif')) return 'heic'\n }\n const ext = fileExt(name)\n if (TIFF_EXT.has(ext)) return 'tiff'\n if (PSD_EXT.has(ext)) return 'psd'\n if (HEIC_EXT.has(ext)) return 'heic'\n if (RAW_EXT.has(ext)) return 'raw'\n return null\n}\n\n// E-book formats routed to the side viewer's e-book reader (EbookViewer.vue): EPUB (epub.js), FictionBook\n// (.fb2, hand-rolled), MOBI/AZW3 (best-effort text extraction), and DjVu (.djvu — not previewable, shows a\n// hint). EPUB is a zip and FB2 is XML, but neither collides with the zip/text routes (we only claim these\n// extensions). DjVu carries an `image/*` mime, so like the raster formats this must beat {@link isImageFile}.\nexport type EbookKind = 'epub' | 'fb2' | 'mobi' | 'djvu'\n\n/**\n * Which e-book reader (if any) should open this file — 'epub' / 'fb2' / 'mobi' (also .azw3/.azw/.prc) /\n * 'djvu', or null. Prefer `mime`, fall back to the extension. Consulted in FileChip BEFORE the generic\n * text route (FB2 is XML) and BEFORE {@link isImageFile} (DjVu's mime is image/vnd.djvu).\n */\nexport function ebookKind(name: string, mime?: string | null): EbookKind | null {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (m === 'application/epub+zip') return 'epub'\n if (m === 'application/x-fictionbook+xml' || m === 'application/fb2+xml') return 'fb2'\n if (m === 'application/x-mobipocket-ebook' || m === 'application/vnd.amazon.ebook')\n return 'mobi'\n if (m === 'image/vnd.djvu' || m === 'image/x-djvu') return 'djvu'\n }\n const ext = fileExt(name)\n if (ext === 'epub') return 'epub'\n if (ext === 'fb2') return 'fb2'\n if (ext === 'mobi' || ext === 'azw' || ext === 'azw3' || ext === 'prc') return 'mobi'\n if (ext === 'djvu' || ext === 'djv') return 'djvu'\n return null\n}\n\n/**\n * Is this an SVG? It opens in the side viewer with a render/source toggle (like HTML) — the render view shows\n * the picture, the source view its XML. Prefer `mime`, fall back to the extension.\n */\nexport function isSvgFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') return m === 'image/svg+xml'\n return fileExt(name) === 'svg'\n}\n\n/**\n * Is this a video we can play in the modal preview? Unlike images, a direct `<video src=download-url>` can't\n * decode (octet-stream + nosniff), so the lightbox fetches the bytes and plays them from a typed blob. Prefer\n * `mime`, fall back to the extension.\n */\nexport function isVideoFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (m.startsWith('video/')) return true\n if (m.startsWith('text/') || m.startsWith('image/') || m.startsWith('audio/')) return false\n }\n return VIDEO_EXT.has(fileExt(name))\n}\n\n/**\n * Is this an HTML document? Such files get a render/source toggle in the side viewer — the rendered\n * view runs the markup in a SANDBOXED iframe (see FileViewerPanel). Prefer `mime`, fall back to the\n * extension (same precedence as {@link isTextFile}); never trust an octet-stream/blank mime.\n */\nexport function isHtmlFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n return m === 'text/html' || m === 'application/xhtml+xml'\n }\n const ext = fileExt(name)\n return ext === 'html' || ext === 'htm'\n}\n\n// Native binary objects the symbol browser parses in-browser: PE (Windows) and ELF (Unix). PE covers the\n// executable family (.exe/.dll/.sys/…); ELF covers shared objects, kernel modules and relocatables.\nconst PE_EXTS = ['exe', 'dll', 'sys', 'ocx', 'cpl', 'scr', 'efi', 'drv', 'mui', 'node']\nconst ELF_EXTS = ['elf', 'ko', 'o']\n\n/**\n * Is this a native binary object — a PE (`.exe`/`.dll`/…) or an ELF (`.so`, incl. versioned `libfoo.so.1.2.3`,\n * plus `.elf`/`.ko`/`.o`)? Such files open in the side viewer's symbol browser, which parses them in-browser\n * (see lib/binobj.ts) — listing function symbols and, for PE, executable metadata (version info + icon).\n * Detection is extension-driven: the stored mime is unreliable (usually `application/octet-stream`), so we only\n * rule it out for clearly-textual / media mimes.\n */\nexport function isBinaryObjFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n const ext = fileExt(name)\n if (PE_EXTS.includes(ext) || ELF_EXTS.includes(ext)) return true\n // Shared objects, including versioned names like libfoo.so.1.2.3.\n return /\\.so(\\.\\d+)*$/.test((name || '').toLowerCase())\n}\n\n// E-language (易语言) project files: `.e` source and `.ec` module — binary containers parsed + decompiled\n// entirely in-browser (see lib/epl + EplViewer.vue). The stored mime is unreliable (usually octet-stream),\n// so detection is extension-driven; a clearly textual / media mime rules it out (a `.e` is never text/*).\n/**\n * Is this an e-language `.e` / `.ec` project file? Such files open in the side viewer's e-language viewer,\n * which parses the binary section format and decompiles each method back to readable 易语言 source text\n * (hand-rolled port of OpenEpl/EProjectFile, see lib/epl). Extension-driven; a concrete text/media mime rules\n * it out. The viewer validates the file magic and shows a friendly message for encrypted / non-EPL bytes.\n */\nexport function isEplFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n const ext = fileExt(name)\n return ext === 'e' || ext === 'ec'\n}\n\n// Java bytecode (`.class`) — a big-endian structural container parsed in-browser (see lib/classfile.ts +\n// ClassViewer.vue), listing fields + methods (NOT decompiled). The stored mime is unreliable (usually\n// octet-stream); detection is extension-driven, with the registered JVM mimes also confirming it. A clearly\n// textual / media mime rules it out (a `.class` is never text/*).\nconst JAVA_CLASS_MIME = new Set([\n 'application/java-vm',\n 'application/x-java-vm',\n 'application/java',\n 'application/x-java-class',\n 'application/java-byte-code',\n])\n\n/**\n * Is this a Java `.class` bytecode file? Such files open in the side viewer's class viewer, which parses the\n * structure in-browser (see lib/classfile.ts) — the class signature plus its fields and methods (a lightweight\n * `javap`; no decompilation of method bodies). The viewer validates the 0xCAFEBABE magic and shows a friendly\n * message otherwise. Extension-driven (the stored mime is unreliable); a concrete text/media mime rules it out.\n */\nexport function isJavaClassFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (JAVA_CLASS_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return fileExt(name) === 'class'\n}\n\n// JVM heap dump (`.hprof`) — a big-endian record stream stream-parsed in-browser (see lib/hprof.ts +\n// HprofViewer.vue) into a class histogram + per-class object drill-in. The stored mime is unreliable (usually\n// octet-stream); detection is extension-driven, with the registered java hprof mime also confirming it. A\n// clearly textual / media mime rules it out (an .hprof is never text/*).\nconst HPROF_MIME = new Set(['application/vnd.java.hprof', 'application/x-hprof'])\n\n/**\n * Is this a JVM heap dump (`.hprof`)? Such files open in the side viewer's HPROF viewer, which STREAM-parses the\n * dump in-browser (see lib/hprof.ts) — a class histogram (instances + shallow size) plus drill-in to one class's\n * objects with decoded fields, paginated + filterable so a multi-GB dump stays memory-bounded. Extension-driven\n * (the stored mime is unreliable); a concrete text/media mime rules it out.\n */\nexport function isHprofFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (HPROF_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return fileExt(name) === 'hprof'\n}\n\n// Windows Installer database (`.msi`, plus the `.msp` patch / `.msm` merge module) — an OLE2 compound file\n// (NOT a PE; see isBinaryObjFile), parsed in-browser (see lib/msi.ts + MsiViewer.vue) to surface the product\n// identity + summary info. The stored mime is unreliable (usually octet-stream); detection is extension-driven,\n// with the registered MSI mime also confirming it. A clearly textual / media mime rules it out.\nconst MSI_MIME = new Set(['application/x-msi', 'application/vnd.ms-msi'])\nconst MSI_EXT = new Set(['msi', 'msp', 'msm'])\n\n/**\n * Is this a Windows Installer `.msi` (or `.msp`/`.msm`)? Such files open in the side viewer's MSI viewer,\n * which parses the OLE compound-file structure in-browser (see lib/msi.ts) — the product name/version/codes\n * from the Property table plus the SummaryInformation. Prefer `mime`, fall back to the extension; a concrete\n * text/media mime rules it out.\n */\nexport function isMsiFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (MSI_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return MSI_EXT.has(fileExt(name))\n}\n\n// Windows app package (`.appx`/`.msix`, and the `.appxbundle`/`.msixbundle` bundles) — an OPC ZIP archive\n// (NOT a PE, NOT an MSI), parsed in-browser (see lib/appx.ts + AppxViewer.vue) to surface the manifest\n// identity / dependencies / capabilities. An APPX is a zip under the hood, so this MUST be consulted BEFORE\n// the generic zip route (an APPX served as application/zip would otherwise open as a plain archive).\nconst APPX_MIME = new Set([\n 'application/appx',\n 'application/msix',\n 'application/appxbundle',\n 'application/msixbundle',\n 'application/vnd.ms-appx',\n 'application/windows.appx',\n])\nconst APPX_EXT = new Set(['appx', 'msix', 'appxbundle', 'msixbundle'])\n\n/**\n * Is this a Windows app package (`.appx`/`.msix`/`.appxbundle`/`.msixbundle`)? Such files open in the side\n * viewer's APPX viewer, which unzips the manifest in-browser (see lib/appx.ts) and shows the package identity,\n * dependencies, capabilities and applications. Prefer `mime`, fall back to the extension; a concrete text/media\n * mime rules it out. Must beat {@link isZipFile} (an APPX is a zip container).\n */\nexport function isAppxFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (APPX_MIME.has(m)) return true\n if (m.startsWith('text/') || m.startsWith('image/') || m.startsWith('audio/')) return false\n }\n return APPX_EXT.has(fileExt(name))\n}\n\n// Word document (OOXML) MIME. Only the modern `.docx` (a zip-under-the-hood OOXML package) is renderable —\n// the legacy binary `.doc` is NOT supported by the in-browser renderer (docx-preview), so it's excluded.\nconst DOCX_MIME = new Set([\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml',\n])\n\n/**\n * Is this a Word `.docx` document? Such files open in the side viewer's Word viewer, which unpacks +\n * renders the OOXML to HTML in-browser (docx-preview, see DocxViewer.vue) — a paginated, paper-like\n * preview. Prefer `mime`, fall back to the extension; a concrete non-docx media/text mime rules it out,\n * blank/octet-stream defers to the extension. Legacy `.doc` is deliberately NOT matched (unsupported).\n */\nexport function isDocxFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (DOCX_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return fileExt(name) === 'docx'\n}\n\n// Excel spreadsheet (OOXML) MIME. Only the modern zip-under-the-hood `.xlsx`/`.xlsm` is renderable (exceljs\n// parses SpreadsheetML); the legacy binary `.xls` is NOT supported, so it's excluded.\nconst XLSX_MIME = new Set([\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml',\n 'application/vnd.ms-excel.sheet.macroenabled.12',\n])\n\n/**\n * Is this an Excel `.xlsx` / `.xlsm` spreadsheet? Such files open in the side viewer's spreadsheet viewer,\n * which unpacks + renders the workbook to HTML tables in-browser (exceljs, see XlsxViewer.vue) — one table\n * per sheet with a tab bar. Prefer `mime`, fall back to the extension; a concrete non-xlsx media/text mime\n * rules it out, blank/octet-stream defers to the extension. Legacy `.xls` is deliberately NOT matched.\n */\nexport function isXlsxFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (XLSX_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n const ext = fileExt(name)\n return ext === 'xlsx' || ext === 'xlsm'\n}\n\n// PowerPoint presentation (OOXML) MIME. Only the modern zip-under-the-hood `.pptx`/`.pptm`/`.ppsx` is\n// renderable (@aiden0z/pptx-renderer parses PresentationML); the legacy binary `.ppt` is NOT supported, so\n// it's excluded. `.macroenabled.12` is the lowercased form of the registered `.macroEnabled.12` types.\nconst PPTX_MIME = new Set([\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml',\n 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',\n 'application/vnd.ms-powerpoint.presentation.macroenabled.12',\n 'application/vnd.ms-powerpoint.slideshow.macroenabled.12',\n])\n\n/**\n * Is this a PowerPoint `.pptx` / `.pptm` / `.ppsx` presentation? Such files open in the side viewer's slide\n * viewer, which unpacks + renders the deck to HTML/SVG in-browser (@aiden0z/pptx-renderer, see PptxViewer.vue)\n * — a scrollable list of slides. Prefer `mime`, fall back to the extension; a concrete non-pptx media/text\n * mime rules it out, blank/octet-stream defers to the extension. Legacy `.ppt` is deliberately NOT matched.\n */\nexport function isPptxFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (PPTX_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n const ext = fileExt(name)\n return ext === 'pptx' || ext === 'pptm' || ext === 'ppsx'\n}\n\n// Rich Text Format MIME types. The reliable signal is the extension (.rtf); the stored mime is often octet-\n// stream or text/plain. `application/rtf` is the registered type, `text/rtf` a common variant.\nconst RTF_MIME = new Set(['application/rtf', 'text/rtf', 'application/x-rtf', 'text/richtext'])\n\n/**\n * Is this a Rich Text Format `.rtf` document? Such files open in the side viewer's rich-text viewer, which\n * interprets the RTF in-browser (hand-rolled, see lib/rtf.ts + RtfViewer.vue) — bold/italic/underline, colors,\n * fonts, alignment and unicode rendered onto a paper-like sheet. Prefer `mime`, fall back to the extension; a\n * concrete non-rtf media mime rules it out, blank/octet-stream/text defers to the extension (an .rtf is often\n * mislabeled text/plain). Must be consulted BEFORE the generic text route so it renders, not shows raw markup.\n */\nexport function isRtfFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (RTF_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return fileExt(name) === 'rtf'\n}\n\n// PDF MIME types. The reliable signal is usually the extension (.pdf); `application/pdf` is the registered\n// type, `application/x-pdf` an older variant.\nconst PDF_MIME = new Set(['application/pdf', 'application/x-pdf', 'application/acrobat'])\n\n/**\n * Is this a PDF document? Such files open in the side viewer's PDF viewer, which renders each page to a\n * canvas in-browser (pdfjs-dist, see PdfViewer.vue) — a vertically scrollable list of pages. Prefer `mime`,\n * fall back to the extension; a concrete non-pdf media/text mime rules it out, blank/octet-stream defers to\n * the extension.\n */\nexport function isPdfFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (PDF_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return fileExt(name) === 'pdf'\n}\n\n// Outline / mind-map MIME types. The reliable signal is the extension (.opml/.mm); these MIME types appear\n// occasionally. `.mm` shares its extension with Objective-C++ source, so a match here is only a *candidate* —\n// the side panel content-sniffs the loaded text (looksLikeMindMap) before drawing the diagram.\nconst MINDMAP_MIME = new Set([\n 'text/x-opml',\n 'text/x-opml+xml',\n 'application/xml+opml',\n 'application/x-freemind',\n 'application/x-freeplane',\n])\n\n/**\n * Is this an outline / mind-map file — OPML (.opml) or FreeMind / Freeplane (.mm)? Such files open in the side\n * viewer with a render/source toggle: render draws an interactive mind map (see MindMapView.vue), source shows\n * the XML. Detection is extension-driven (the stored mime is unreliable); a concrete media mime rules it out.\n * NOTE: `.mm` is also the Objective-C++ source extension — the panel additionally sniffs the loaded text\n * (looksLikeMindMap) before rendering the diagram, so a non-mind-map `.mm` just reads as highlighted source.\n */\nexport function isMindMapFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (MINDMAP_MIME.has(m)) return true\n if (\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/') ||\n m === 'text/html'\n ) {\n return false\n }\n }\n const ext = fileExt(name)\n return ext === 'opml' || ext === 'mm'\n}\n\n// XMind workbook (.xmind) — a ZIP container (content.json modern / content.xml legacy) rendered as a mind map\n// (see lib/xmind.ts + XmindViewer.vue, reusing MindMapView). It's a zip under the hood, so this MUST be consulted\n// BEFORE the generic zip route. Extension-driven (the stored mime is unreliable); the workbook mimes confirm it.\nconst XMIND_MIME = new Set([\n 'application/vnd.xmind.workbook',\n 'application/x-xmind',\n 'application/xmind',\n])\n\n/**\n * Is this an XMind workbook (`.xmind`)? Such files open in the side viewer's XMind viewer, which unzips the\n * `content.json` / `content.xml` in-browser (see lib/xmind.ts) and renders it as an interactive mind map (shared\n * MindMapView). Prefer `mime`, fall back to the extension; a concrete media/text mime rules it out. Must beat\n * {@link isZipFile} (an XMind is a zip container).\n */\nexport function isXmindFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (XMIND_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return fileExt(name) === 'xmind'\n}\n\n// DXF (AutoCAD Drawing Exchange Format) — an ASCII CAD drawing, rendered as an SVG (see lib/dxf.ts +\n// DxfViewer.vue). `.dxf` is textual, so this MUST be consulted BEFORE the generic text route so it opens as a\n// drawing, not raw source. NOTE: `.dwg` (the binary AutoCAD format) is deliberately NOT claimed — it's a\n// different, unsupported format. Extension-driven; the registered dxf mimes also confirm it.\nconst DXF_MIME = new Set(['image/vnd.dxf', 'application/dxf', 'application/x-dxf', 'drawing/x-dxf'])\n\n/**\n * Is this a DXF (`.dxf`) CAD drawing? Such files open in the side viewer's DXF viewer, which parses the ASCII\n * DXF in-browser (see lib/dxf.ts) and draws its entities as an SVG (pan/zoom + layer toggles). Prefer `mime`,\n * fall back to the extension; a concrete non-dxf media mime rules it out (a `.dxf` is often served text/plain\n * or octet-stream, so the extension is the reliable signal).\n */\nexport function isDxfFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (DXF_MIME.has(m)) return true\n if (m.startsWith('audio/') || m.startsWith('video/') || m.startsWith('image/')) {\n if (!DXF_MIME.has(m)) return false\n }\n }\n return fileExt(name) === 'dxf'\n}\n\n// Email (.eml) MIME types. `message/rfc822` is the registered type for a raw RFC 822 message; the stored\n// mime is often octet-stream or even text/plain, so the extension (.eml) is the reliable signal. We do NOT\n// rule out text/* here (some servers label .eml as text/plain) — only clearly-binary media families.\nconst EML_MIME = new Set(['message/rfc822', 'application/eml', 'text/eml', 'message/global'])\n\n/**\n * Is this an email `.eml` (RFC 822 / MIME) message? Such files open in the side viewer's email viewer, which\n * parses the MIME tree in-browser (see lib/eml.ts + EmlViewer.vue) — a header card, the best body (HTML in a\n * sandboxed iframe, plain-text fallback), and the attachments. Prefer `mime`, fall back to the extension; a\n * concrete non-eml media mime rules it out, blank/octet-stream/text defers to the extension.\n */\nexport function isEmlFile(name: string, mime?: string | null): boolean {\n // A .mht/.mhtml web archive can also be served as message/rfc822, but it's MHT, not an email (see isMhtFile).\n const ext = fileExt(name)\n if (ext === 'mht' || ext === 'mhtml') return false\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (EML_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return ext === 'eml'\n}\n\n/**\n * Is this a web archive `.mht`/`.mhtml` (RFC 2557 MIME container)? Such files open in the side viewer's web-\n * archive viewer, which parses the `multipart/related` container in-browser (see lib/mht.ts + MhtViewer.vue)\n * and renders the saved page — resources rewritten to blob URLs — in a sandboxed iframe. Extension-driven (the\n * stored mime is unreliable); the telltale container mimes (multipart/related, application/x-mimearchive)\n * also confirm it.\n */\nexport function isMhtFile(name: string, mime?: string | null): boolean {\n const ext = fileExt(name)\n if (ext === 'mht' || ext === 'mhtml') return true\n const m = (mime || '').toLowerCase().trim()\n return m === 'multipart/related' || m === 'application/x-mimearchive'\n}\n\n// HAR (HTTP Archive) MIME types. A .har is JSON under the hood, so it's often served as application/json or\n// octet-stream — the extension (.har) is the reliable signal. The registered type is application/json; some\n// tools use application/x-har.\nconst HAR_MIME = new Set(['application/har+json', 'application/x-har'])\n\n/**\n * Is this a HAR (`.har`, HTTP Archive 1.2) capture? Such files open in the side viewer's HAR viewer, which\n * parses the JSON archive in-browser (hand-rolled, see lib/har.ts + HarViewer.vue) — a filterable request\n * list with per-request headers / query / cookies / bodies. Extension-driven (a .har is JSON, so the stored\n * mime is usually application/json or octet-stream); a concrete non-har media mime rules it out. Must be\n * checked BEFORE the generic text route (a .har served as application/json would otherwise read as JSON source).\n */\nexport function isHarFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (HAR_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return fileExt(name) === 'har'\n}\n\n// SQLite database MIME types. The reliable signal is the extension; the stored mime is usually octet-stream.\nconst SQLITE_MIME = new Set([\n 'application/x-sqlite3',\n 'application/vnd.sqlite3',\n 'application/x-sqlite',\n])\n// Extensions we route to the SQLite viewer: the conventional database suffixes.\nconst SQLITE_EXT = new Set(['sqlite', 'sqlite3', 'db', 'db3', 'sqlitedb'])\n\n/**\n * Is this a SQLite database (`.sqlite`/`.db`/`.sqlite3`/…)? Such files open in the side viewer's SQLite viewer,\n * which loads them with sql.js (WASM) in-browser (see lib/sqlite.ts + SqliteViewer.vue) — a table picker, a\n * paginated data grid, and a SQL console. Prefer `mime`, fall back to the extension; a concrete textual/media\n * mime rules it out (`.db` is a generic suffix, so a clearly-text mime means it isn't a database).\n */\nexport function isSqliteFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (SQLITE_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return SQLITE_EXT.has(fileExt(name))\n}\n\n// Certificate / key formats routed to the side viewer's certificate visualizer (parsed in-browser, hand-rolled\n// ASN.1/X.509, see lib/asn1.ts + lib/x509.ts + CertViewer.vue): PEM (.pem/.crt/.cer/.csr/.key, also PKCS#7\n// bundles) and raw DER (.der/.cer). The viewer fetches the BYTES (binary-safe), so `.der` isn't mangled.\n// Detection is extension-driven (the stored mime is unreliable); a few registered cert mimes also confirm it.\n// NOTE: PKCS#12 (`.p12`/`.pfx`) is a KEYSTORE, not a bare cert — routed to the keystore viewer ({@link isJksFile}).\nconst CERT_EXT = new Set(['pem', 'crt', 'cer', 'der', 'csr', 'req', 'key', 'p7b', 'p7c'])\nconst CERT_MIME = new Set([\n 'application/x-x509-ca-cert',\n 'application/x-x509-user-cert',\n 'application/x-x509-cert',\n 'application/pkix-cert',\n 'application/pkix-crl',\n 'application/x-pem-file',\n 'application/pkcs8',\n 'application/pkcs10',\n 'application/x-pkcs7-certificates',\n])\n\n/**\n * Is this a certificate / key file (`.pem`/`.crt`/`.cer`/`.der`/`.csr`/`.key`/`.p7b`/…)? Such files open\n * in the side viewer's certificate visualizer, which parses the ASN.1 / X.509 in-browser (see lib/x509.ts +\n * CertViewer.vue) — subject/issuer, validity, key + signature algorithms, SANs, fingerprint, and a raw ASN.1\n * structure tree. Prefer `mime`, fall back to the extension; a concrete non-cert media/text mime rules it out.\n * PKCS#12 (`.p12`/`.pfx`) is handled by the keystore viewer ({@link isJksFile}), not here.\n */\nexport function isCertFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (CERT_MIME.has(m)) return true\n // Don't rule out text/* — a PEM (.pem/.crt/.key) is often served as text/plain; the extension settles it.\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return CERT_EXT.has(fileExt(name))\n}\n\n// Keystore formats routed to the side viewer's keystore viewer (parsed in-browser, hand-rolled — see lib/jks.ts,\n// lib/pkcs12.ts + JksViewer.vue): the Sun JKS/JCEKS binary format (`.jks`/`.keystore`/`.ks`/`.truststore`/`.jceks`,\n// magic 0xFEEDFEED / 0xCECECECE) AND PKCS#12 / PFX (`.p12`/`.pfx`). The `.jks` extension is claimed even though\n// modern keytool writes PKCS#12 into it — the viewer sniffs the actual bytes and picks the right parser, so a\n// mislabeled file still opens. Detection is extension-driven (the stored mime is unreliable, usually\n// octet-stream); a concrete text/media mime rules it out (a keystore is never text/*).\nconst JKS_MIME = new Set([\n 'application/x-java-keystore',\n 'application/x-java-jce-keystore',\n 'application/x-pkcs12',\n 'application/pkcs12',\n])\nconst JKS_EXT = new Set(['jks', 'keystore', 'ks', 'jceks', 'truststore', 'p12', 'pfx'])\n\n/**\n * Is this a keystore — Java JKS/JCEKS (`.jks`/`.keystore`/`.ks`/`.jceks`/`.truststore`) or PKCS#12 (`.p12`/\n * `.pfx`)? Such files open in the side viewer's keystore viewer, which sniffs the format and parses it in-browser\n * (see lib/jks.ts / lib/pkcs12.ts) — the entry inventory (aliases / kinds / dates), each entry's X.509 chain,\n * and, with a password, store-integrity verification + private-key decryption. Prefer `mime`, fall back to the\n * extension; a concrete text/media mime rules it out.\n */\nexport function isJksFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (JKS_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return JKS_EXT.has(fileExt(name))\n}\n\n// Geodata formats routed to the side viewer's offline SVG map (parsed in-browser, see lib/geo.ts +\n// GeoViewer.vue): GeoJSON (.geojson), KML (.kml), GPX (.gpx). `.geojson`/`.kml`/`.gpx` are also textual, so\n// this must be consulted BEFORE the generic text route (in FileChip) so they open as a map, not raw source.\n// Extension-driven; we deliberately DON'T claim bare `.json` (most JSON isn't geodata) — only `.geojson`.\nconst GEO_EXT = new Set(['geojson', 'kml', 'gpx'])\nconst GEO_MIME = new Set([\n 'application/geo+json',\n 'application/vnd.geo+json',\n 'application/vnd.google-earth.kml+xml',\n 'application/gpx+xml',\n 'application/x-gpx+xml',\n])\n\n/**\n * Is this a geodata file (`.geojson`/`.kml`/`.gpx`)? Such files open in the side viewer's map viewer, which\n * parses the geometry in-browser (see lib/geo.ts + GeoViewer.vue) and draws it as an OFFLINE SVG map (no tile\n * server) with pan/zoom and a render/source toggle. Prefer `mime`, fall back to the extension; a concrete\n * non-geo media mime rules it out.\n */\nexport function isGeoFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (GEO_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return GEO_EXT.has(fileExt(name))\n}\n\n// Subtitle / caption formats routed to the side viewer's subtitle transcript (parsed in-browser, see\n// lib/subtitle.ts + SubtitleViewer.vue): SubRip (.srt), WebVTT (.vtt), SubStation Alpha (.ass/.ssa).\n// `.srt`/`.vtt` are ALSO plain text, so this must be consulted BEFORE the generic text route (in FileChip)\n// so they open as a cue list, not raw source. Extension-driven (the stored mime is unreliable).\nconst SUBTITLE_EXT = new Set(['srt', 'vtt', 'ass', 'ssa'])\nconst SUBTITLE_MIME = new Set([\n 'application/x-subrip',\n 'application/x-subrip+srt',\n 'text/vtt',\n 'text/x-ssa',\n 'text/x-ass',\n])\n\n/**\n * Is this a subtitle / caption file (`.srt`/`.vtt`/`.ass`/`.ssa`)? Such files open in the side viewer's\n * subtitle viewer, which parses the cues in-browser (see lib/subtitle.ts + SubtitleViewer.vue) and shows\n * them as a timecoded transcript (with a render/source toggle). Prefer `mime`, fall back to the extension;\n * a concrete non-subtitle media mime rules it out, blank/octet-stream/text defers to the extension.\n */\nexport function isSubtitleFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (SUBTITLE_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return SUBTITLE_EXT.has(fileExt(name))\n}\n\n// 3D model formats routed to the side viewer's model viewer (rendered with three.js, see ModelViewer.vue):\n// STL (.stl), Wavefront OBJ (.obj), PLY (.ply), glTF (.gltf), GLB and STEP (.stp/.step). Extension-driven (the\n// stored mime is unreliable; `model/*` mimes also confirm it).\nconst MODEL_EXT = new Set(['stl', 'obj', 'ply', 'glb', 'gltf', 'stp', 'step'])\nconst STEP_MIME = new Set([\n 'model/step',\n 'application/step',\n 'application/x-step',\n 'application/stp',\n])\n\n/** STEP is textual CAD data; a text/plain MIME must not send it to the source/log viewer. */\nexport function isStepFile(name: string, mime?: string | null): boolean {\n const ext = fileExt(name)\n return (\n ext === 'stp' ||\n ext === 'step' ||\n STEP_MIME.has((mime ?? '').split(';')[0].trim().toLowerCase())\n )\n}\nconst MODEL_MIME = new Set([\n 'model/gltf-binary',\n 'model/gltf+json',\n 'model/stl',\n 'application/sla',\n 'application/vnd.ms-pki.stl',\n 'model/obj',\n 'model/ply',\n])\n\n/**\n * Is this a 3D model (`.stl`/`.obj`/`.ply`/`.glb`/`.gltf`/`.stp`/`.step`)? Opens the model viewer,\n * which renders them with three.js in-browser (see ModelViewer.vue) — auto-framed, lit, with orbit controls.\n * Prefer `mime` (`model/*`), fall back to the extension; a concrete text/image/audio/video mime rules it out.\n */\nexport function isModelFile(name: string, mime?: string | null): boolean {\n if (isStepFile(name, mime)) return true\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (MODEL_MIME.has(m) || m.startsWith('model/')) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return MODEL_EXT.has(fileExt(name))\n}\n\n// Audio formats routed to the side viewer's audio player (played via a typed Blob URL through a native\n// <audio> element, since the download endpoint's octet-stream/attachment defeats a direct src — see\n// AudioViewer.vue): mp3/wav/flac/ogg/m4a/aac/opus. `.mid`/`.midi` are included too but can't be PLAYED\n// in-browser (no synth) — the viewer shows them as a piano-roll visualization (best effort).\n// `.amr`/`.awb`/`.silk`/`.slk` (3GPP AMR + Tencent SILK 微信/QQ voice) are also routed here: browsers can't\n// decode them natively, so AudioViewer DECODES the bytes to WAV in-browser (lib/speechAudio.ts, WASM) and\n// plays that. Detection at decode time is by file-header magic (a WeChat `.amr` is often actually SILK).\nconst AUDIO_EXT = new Set([\n 'mp3',\n 'wav',\n 'flac',\n 'ogg',\n 'oga',\n 'm4a',\n 'aac',\n 'opus',\n 'weba',\n 'mid',\n 'midi',\n 'amr',\n 'awb',\n 'silk',\n 'slk',\n])\n\n/**\n * Is this an audio file (mp3/wav/flac/ogg/m4a/aac/opus, a .mid/.midi, or an AMR/SILK voice clip)? Such files\n * open in the side viewer's audio player, which fetches the bytes and plays them via a typed Blob URL (MIDI is\n * visualized as a piano roll, not played; AMR/SILK are decoded to WAV in-browser first). Prefer `mime`\n * (`audio/*`), fall back to the extension; a concrete text/image/video mime rules it out.\n */\nexport function isAudioFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (m.startsWith('audio/')) return true\n if (m.startsWith('text/') || m.startsWith('image/') || m.startsWith('video/')) return false\n }\n return AUDIO_EXT.has(fileExt(name))\n}\n\n// Jupyter Notebook (.ipynb) — JSON under the hood, so often served as application/json or octet-stream; the\n// extension is the reliable signal. The registered type is application/x-ipynb+json. Must be checked BEFORE\n// the generic text route (a .ipynb served as application/json would otherwise read as JSON source).\nconst NOTEBOOK_MIME = new Set(['application/x-ipynb+json', 'application/ipynb+json'])\n\n/**\n * Is this a Jupyter Notebook (`.ipynb`)? Such files open in the side viewer's notebook viewer, which parses\n * the JSON in-browser (hand-rolled, see lib/ipynb.ts + NotebookViewer.vue) — markdown cells via the app's\n * markdown engine, code cells highlighted, outputs (stream / image / rich HTML / error) inline. Extension-\n * driven (a .ipynb is JSON, so the stored mime is usually application/json or octet-stream); a concrete\n * non-notebook media mime rules it out.\n */\nexport function isNotebookFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (NOTEBOOK_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return fileExt(name) === 'ipynb'\n}\n\n// Font formats routed to the side viewer's specimen viewer (loaded with the native FontFace API, which\n// decodes ttf/otf/woff AND woff2 in-browser — no library; see FontViewer.vue): TrueType (.ttf), OpenType\n// (.otf), and the web wrappers (.woff/.woff2). Extension-driven (stored mime is unreliable).\nconst FONT_EXT = new Set(['ttf', 'otf', 'woff', 'woff2'])\nconst FONT_MIME = new Set([\n 'font/ttf',\n 'font/otf',\n 'font/woff',\n 'font/woff2',\n 'font/sfnt',\n 'application/font-woff',\n 'application/font-woff2',\n 'application/x-font-ttf',\n 'application/x-font-otf',\n 'application/x-font-truetype',\n 'application/x-font-opentype',\n 'application/font-sfnt',\n 'application/vnd.ms-opentype',\n])\n\n/**\n * Is this a font file (`.ttf`/`.otf`/`.woff`/`.woff2`)? Such files open in the side viewer's font specimen,\n * which loads them with the native FontFace API in-browser (see FontViewer.vue) — an editable preview line,\n * a size waterfall, and the glyph sets. Prefer `mime`, fall back to the extension; a concrete non-font\n * media/text mime rules it out, blank/octet-stream defers to the extension.\n */\nexport function isFontFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (FONT_MIME.has(m)) return true\n if (\n m.startsWith('text/') ||\n m.startsWith('image/') ||\n m.startsWith('audio/') ||\n m.startsWith('video/')\n ) {\n return false\n }\n }\n return FONT_EXT.has(fileExt(name))\n}\n\n// QQ 音乐加密音频(QMCv2,文件尾内嵌密钥)—— 浏览器内解密后用 <audio> 播放(见 lib/qmc.ts + QmcAudioViewer.vue)。\n// 涵盖 .mflac(FLAC)/.mgg(OGG)及其音质分级变体;这些都带内嵌 EKey,可独立解出。早期 QMCv1 静态密钥格式\n// (.qmcflac/.qmc0…)不在此列(无内嵌密钥)。存储 mime 一律不可信(多为 octet-stream),故按扩展名判定。\nconst QMC_EXT = new Set(['mflac', 'mflac0', 'mflac1', 'mflaca', 'mgg', 'mgg0', 'mgg1', 'mggl'])\n\n/**\n * Is this a QQ Music encrypted audio file (`.mflac`/`.mgg`/…)? Such files open in the side viewer's QMC\n * player, which decrypts the bytes in-browser (see lib/qmc.ts) — recovering the embedded key, undoing the\n * QMCv2 stream cipher, and playing the resulting FLAC/OGG/MP3 via a typed Blob (with a \"download decrypted\"\n * action). Extension-driven (the stored mime is unreliable, usually octet-stream); a clearly textual/media\n * mime rules it out.\n */\nexport function isQmcAudioFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (m.startsWith('text/') || m.startsWith('image/') || m.startsWith('video/')) return false\n }\n return QMC_EXT.has(fileExt(name))\n}\n\n// LRC 歌词(`.lrc`)—— 行式带时间戳歌词,在侧栏渲染成同步歌词列表(见 lib/lrc.ts + LrcViewer.vue)。\n// 这类文件常见 GBK 编码且夹带 HTML 实体,故查看器自取字节、嗅探编码(见 lib/encoding.ts),不能当 UTF-8 文本直读。\nconst LRC_MIME = new Set(['application/lrc', 'text/lrc', 'application/x-lrc'])\n\n/**\n * Is this an LRC lyrics file (`.lrc`)? Such files open in the side viewer's lyrics viewer, which parses the\n * timestamped lines in-browser (see lib/lrc.ts + LrcViewer.vue) and shows a synced lyric list. Extension-\n * driven; a concrete non-text media mime rules it out. NOTE: deliberately routed BEFORE the generic text\n * route (the viewer self-fetches so it can sniff GBK — a `.lrc` read as UTF-8 text would be mojibake).\n */\nexport function isLrcFile(name: string, mime?: string | null): boolean {\n const m = (mime || '').toLowerCase().trim()\n if (m && m !== 'application/octet-stream') {\n if (LRC_MIME.has(m)) return true\n if (m.startsWith('image/') || m.startsWith('audio/') || m.startsWith('video/')) return false\n }\n return fileExt(name) === 'lrc'\n}\n\n/** highlight.js language id for this file, or '' for plain text (no highlighting). */\nexport function hljsLang(name: string, mime?: string | null): string {\n const ext = fileExt(name)\n if (ext && ext in EXT_LANG) return EXT_LANG[ext]\n\n const m = (mime || '').toLowerCase().trim()\n if (m === 'text/markdown' || m === 'application/markdown') return 'markdown'\n if (m === 'application/json' || m === 'application/ld+json') return 'json'\n if (\n m === 'application/xml' ||\n m === 'application/xhtml+xml' ||\n m === 'image/svg+xml' ||\n m === 'text/html'\n ) {\n return 'xml'\n }\n if (m.includes('yaml')) return 'yaml'\n if (m === 'text/css') return 'css'\n if (m === 'application/javascript' || m === 'application/ecmascript') return 'javascript'\n\n const lower = (name || '').toLowerCase()\n if (lower === 'dockerfile') return 'dockerfile'\n if (lower === 'makefile') return 'makefile'\n return ''\n}\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import * as THREE from 'three';
2
+ export type ModelViewMode = 'original' | 'white' | 'wireframe';
3
+ /** Temporary display materials; geometry, transforms, original materials and textures remain untouched. */
4
+ export declare function createModelAppearance(root: THREE.Object3D): {
5
+ setMode: (mode: ModelViewMode) => void;
6
+ dispose(): void;
7
+ };
@@ -0,0 +1,41 @@
1
+ import * as e from "three";
2
+ //#region src/lib/modelAppearance.ts
3
+ function t(t) {
4
+ let n = [];
5
+ t.traverse((e) => {
6
+ let t = e;
7
+ t.isMesh && n.push({
8
+ mesh: t,
9
+ original: t.material
10
+ });
11
+ });
12
+ let r, i = /* @__PURE__ */ new Map(), a = !1;
13
+ function o(t) {
14
+ let n = i.get(t);
15
+ if (n) return n;
16
+ let r = t.clone();
17
+ return r.wireframe = !0, r.side = e.DoubleSide, r.depthTest = !1, r.depthWrite = !1, r.forceSinglePass = !0, i.set(t, r), r;
18
+ }
19
+ function s(t) {
20
+ if (a) return;
21
+ let i;
22
+ t === "white" && (i = r ??= new e.MeshStandardMaterial({
23
+ color: 15921906,
24
+ metalness: 0,
25
+ roughness: 1,
26
+ side: e.DoubleSide
27
+ }));
28
+ let s = i;
29
+ for (let { mesh: e, original: r } of n) e.material = t === "wireframe" ? Array.isArray(r) ? r.map(o) : o(r) : s ? Array.isArray(r) ? r.map(() => s) : s : r;
30
+ }
31
+ return {
32
+ setMode: s,
33
+ dispose() {
34
+ a ||= (s("original"), r?.dispose(), i.forEach((e) => e.dispose()), i.clear(), n.length = 0, !0);
35
+ }
36
+ };
37
+ }
38
+ //#endregion
39
+ export { t as createModelAppearance };
40
+
41
+ //# sourceMappingURL=modelAppearance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modelAppearance.js","names":[],"sources":["../../src/lib/modelAppearance.ts"],"sourcesContent":["import * as THREE from 'three'\n\nexport type ModelViewMode = 'original' | 'white' | 'wireframe'\n\n/** Temporary display materials; geometry, transforms, original materials and textures remain untouched. */\nexport function createModelAppearance(root: THREE.Object3D) {\n const meshes: { mesh: THREE.Mesh; original: THREE.Material | THREE.Material[] }[] = []\n root.traverse((object) => {\n const mesh = object as THREE.Mesh\n if (mesh.isMesh) meshes.push({ mesh, original: mesh.material })\n })\n let white: THREE.MeshStandardMaterial | undefined\n const wireframes = new Map<THREE.Material, THREE.Material>()\n let disposed = false\n\n function wireframeFor(original: THREE.Material): THREE.Material {\n const cached = wireframes.get(original)\n if (cached) return cached\n // The built-in mesh materials produced by our loaders support wireframe. Clone them to retain\n // face colors, vertex colors, textures, opacity and shading without mutating the source material.\n const wireframe = original.clone() as THREE.Material & { wireframe: boolean }\n wireframe.wireframe = true\n wireframe.side = THREE.DoubleSide\n // Keep back/internal triangles visible; transparent double-sided materials need only one pass.\n wireframe.depthTest = false\n wireframe.depthWrite = false\n wireframe.forceSinglePass = true\n wireframes.set(original, wireframe)\n return wireframe\n }\n\n function setMode(mode: ModelViewMode): void {\n if (disposed) return\n let replacement: THREE.Material | undefined\n if (mode === 'white') {\n replacement = white ??= new THREE.MeshStandardMaterial({\n color: 0xf2f2f2,\n metalness: 0,\n roughness: 1,\n side: THREE.DoubleSide,\n })\n }\n const displayMaterial = replacement\n for (const { mesh, original } of meshes) {\n mesh.material =\n mode === 'wireframe'\n ? Array.isArray(original)\n ? original.map(wireframeFor)\n : wireframeFor(original)\n : displayMaterial\n ? Array.isArray(original)\n ? original.map(() => displayMaterial)\n : displayMaterial\n : original\n }\n }\n\n return {\n setMode,\n dispose(): void {\n if (disposed) return\n // Restore references first so the model owner's cleanup can release all original materials.\n setMode('original')\n white?.dispose()\n wireframes.forEach((material) => material.dispose())\n wireframes.clear()\n meshes.length = 0\n disposed = true\n },\n }\n}\n"],"mappings":";;AAKA,SAAgB,EAAsB,GAAsB;CAC1D,IAAM,IAA8E,CAAC;CACrF,EAAK,UAAU,MAAW;EACxB,IAAM,IAAO;EACb,AAAI,EAAK,UAAQ,EAAO,KAAK;GAAE;GAAM,UAAU,EAAK;EAAS,CAAC;CAChE,CAAC;CACD,IAAI,GACE,oBAAa,IAAI,IAAoC,GACvD,IAAW;CAEf,SAAS,EAAa,GAA0C;EAC9D,IAAM,IAAS,EAAW,IAAI,CAAQ;EACtC,IAAI,GAAQ,OAAO;EAGnB,IAAM,IAAY,EAAS,MAAM;EAQjC,OAPA,EAAU,YAAY,IACtB,EAAU,OAAO,EAAM,YAEvB,EAAU,YAAY,IACtB,EAAU,aAAa,IACvB,EAAU,kBAAkB,IAC5B,EAAW,IAAI,GAAU,CAAS,GAC3B;CACT;CAEA,SAAS,EAAQ,GAA2B;EAC1C,IAAI,GAAU;EACd,IAAI;EACJ,AAAI,MAAS,YACX,IAAc,MAAU,IAAI,EAAM,qBAAqB;GACrD,OAAO;GACP,WAAW;GACX,WAAW;GACX,MAAM,EAAM;EACd,CAAC;EAEH,IAAM,IAAkB;EACxB,KAAK,IAAM,EAAE,SAAM,iBAAc,GAC/B,EAAK,WACH,MAAS,cACL,MAAM,QAAQ,CAAQ,IACpB,EAAS,IAAI,CAAY,IACzB,EAAa,CAAQ,IACvB,IACE,MAAM,QAAQ,CAAQ,IACpB,EAAS,UAAU,CAAe,IAClC,IACF;CAEZ;CAEA,OAAO;EACL;EACA,UAAgB;GACV,AAOJ,OALA,EAAQ,UAAU,GAClB,GAAO,QAAQ,GACf,EAAW,SAAS,MAAa,EAAS,QAAQ,CAAC,GACnD,EAAW,MAAM,GACjB,EAAO,SAAS,GACL;EACb;CACF;AACF"}