@l1yp/file-viewer 0.1.0 → 0.1.2

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 (439) hide show
  1. package/README.md +2 -0
  2. package/dist/FileViewer.js +2 -0
  3. package/dist/FileViewer.js.map +1 -0
  4. package/dist/FileViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  5. package/dist/FileViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  6. package/dist/components/AppxViewer.js +2 -0
  7. package/dist/components/AppxViewer.js.map +1 -0
  8. package/dist/components/AppxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  9. package/dist/components/AppxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  10. package/dist/components/ArchiveBrowser.js +2 -0
  11. package/dist/components/ArchiveBrowser.js.map +1 -0
  12. package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
  13. package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
  14. package/dist/components/AudioViewer.js +2 -0
  15. package/dist/components/AudioViewer.js.map +1 -0
  16. package/dist/components/AudioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  17. package/dist/components/AudioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  18. package/dist/components/BinObjBrowser.js +2 -0
  19. package/dist/components/BinObjBrowser.js.map +1 -0
  20. package/dist/components/BinObjBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
  21. package/dist/components/BinObjBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
  22. package/dist/components/CertViewer.js +2 -0
  23. package/dist/components/CertViewer.js.map +1 -0
  24. package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  25. package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  26. package/dist/components/ClassViewer.js +2 -0
  27. package/dist/components/ClassViewer.js.map +1 -0
  28. package/dist/components/ClassViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  29. package/dist/components/ClassViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  30. package/dist/components/CodeBlock.js +2 -0
  31. package/dist/components/CodeBlock.js.map +1 -0
  32. package/dist/components/CodeBlock.vue_vue_type_script_setup_true_lang.js +2 -0
  33. package/dist/components/CodeBlock.vue_vue_type_script_setup_true_lang.js.map +1 -0
  34. package/dist/components/CodeView.js +2 -0
  35. package/dist/components/CodeView.js.map +1 -0
  36. package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js +2 -0
  37. package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js.map +1 -0
  38. package/dist/components/CsvViewer.js +2 -0
  39. package/dist/components/CsvViewer.js.map +1 -0
  40. package/dist/components/CsvViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  41. package/dist/components/CsvViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  42. package/dist/components/DicomViewer.js +2 -0
  43. package/dist/components/DicomViewer.js.map +1 -0
  44. package/dist/components/DicomViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  45. package/dist/components/DicomViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  46. package/dist/components/DocxNavigationPane.js +9 -0
  47. package/dist/components/DocxNavigationPane.js.map +1 -0
  48. package/dist/components/DocxNavigationPane.vue.d.ts +23 -0
  49. package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js +98 -0
  50. package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
  51. package/dist/components/DocxViewer.js +3 -1
  52. package/dist/components/DocxViewer.js.map +1 -0
  53. package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js +72 -37
  54. package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  55. package/dist/components/DrawioViewer.js +2 -0
  56. package/dist/components/DrawioViewer.js.map +1 -0
  57. package/dist/components/DrawioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  58. package/dist/components/DrawioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  59. package/dist/components/DxfViewer.js +2 -0
  60. package/dist/components/DxfViewer.js.map +1 -0
  61. package/dist/components/DxfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  62. package/dist/components/DxfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  63. package/dist/components/EbookViewer.js +2 -0
  64. package/dist/components/EbookViewer.js.map +1 -0
  65. package/dist/components/EbookViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  66. package/dist/components/EbookViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  67. package/dist/components/EmlViewer.js +2 -0
  68. package/dist/components/EmlViewer.js.map +1 -0
  69. package/dist/components/EmlViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  70. package/dist/components/EmlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  71. package/dist/components/EplViewer.js +2 -0
  72. package/dist/components/EplViewer.js.map +1 -0
  73. package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  74. package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  75. package/dist/components/FileViewerBody.js +3 -1
  76. package/dist/components/FileViewerBody.js.map +1 -0
  77. package/dist/components/FileViewerBody.vue.d.ts +5 -0
  78. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js +250 -247
  79. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -0
  80. package/dist/components/FileViewerPanel.js +2 -0
  81. package/dist/components/FileViewerPanel.js.map +1 -0
  82. package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js +2 -0
  83. package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js.map +1 -0
  84. package/dist/components/FontViewer.js +2 -0
  85. package/dist/components/FontViewer.js.map +1 -0
  86. package/dist/components/FontViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  87. package/dist/components/FontViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  88. package/dist/components/FrontmatterCard.js +2 -0
  89. package/dist/components/FrontmatterCard.js.map +1 -0
  90. package/dist/components/FrontmatterCard.vue_vue_type_script_setup_true_lang.js +2 -0
  91. package/dist/components/FrontmatterCard.vue_vue_type_script_setup_true_lang.js.map +1 -0
  92. package/dist/components/GeoViewer.js +2 -0
  93. package/dist/components/GeoViewer.js.map +1 -0
  94. package/dist/components/GeoViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  95. package/dist/components/GeoViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  96. package/dist/components/HarViewer.js +2 -0
  97. package/dist/components/HarViewer.js.map +1 -0
  98. package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  99. package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  100. package/dist/components/HexViewer.js +2 -0
  101. package/dist/components/HexViewer.js.map +1 -0
  102. package/dist/components/HexViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  103. package/dist/components/HexViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  104. package/dist/components/HprofViewer.js +2 -0
  105. package/dist/components/HprofViewer.js.map +1 -0
  106. package/dist/components/HprofViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  107. package/dist/components/HprofViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  108. package/dist/components/ImageLightbox.js +2 -0
  109. package/dist/components/ImageLightbox.js.map +1 -0
  110. package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js +2 -0
  111. package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js.map +1 -0
  112. package/dist/components/InlineText.js +2 -0
  113. package/dist/components/InlineText.js.map +1 -0
  114. package/dist/components/JksViewer.js +2 -0
  115. package/dist/components/JksViewer.js.map +1 -0
  116. package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  117. package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  118. package/dist/components/JsonlViewer.js +2 -0
  119. package/dist/components/JsonlViewer.js.map +1 -0
  120. package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  121. package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  122. package/dist/components/LogViewer.js +2 -0
  123. package/dist/components/LogViewer.js.map +1 -0
  124. package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  125. package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  126. package/dist/components/LrcViewer.js +2 -0
  127. package/dist/components/LrcViewer.js.map +1 -0
  128. package/dist/components/LrcViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  129. package/dist/components/LrcViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  130. package/dist/components/MarkdownTable.js +2 -0
  131. package/dist/components/MarkdownTable.js.map +1 -0
  132. package/dist/components/MarkdownTable.vue_vue_type_script_setup_true_lang.js +2 -0
  133. package/dist/components/MarkdownTable.vue_vue_type_script_setup_true_lang.js.map +1 -0
  134. package/dist/components/MarkdownText.js +2 -0
  135. package/dist/components/MarkdownText.js.map +1 -0
  136. package/dist/components/MhtViewer.js +2 -0
  137. package/dist/components/MhtViewer.js.map +1 -0
  138. package/dist/components/MhtViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  139. package/dist/components/MhtViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  140. package/dist/components/MindMapView.js +2 -0
  141. package/dist/components/MindMapView.js.map +1 -0
  142. package/dist/components/MindMapView.vue_vue_type_script_setup_true_lang.js +2 -0
  143. package/dist/components/MindMapView.vue_vue_type_script_setup_true_lang.js.map +1 -0
  144. package/dist/components/MissingDependency.js +2 -0
  145. package/dist/components/MissingDependency.js.map +1 -0
  146. package/dist/components/MissingDependency.vue_vue_type_script_setup_true_lang.js +2 -0
  147. package/dist/components/MissingDependency.vue_vue_type_script_setup_true_lang.js.map +1 -0
  148. package/dist/components/ModelViewer.js +2 -0
  149. package/dist/components/ModelViewer.js.map +1 -0
  150. package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  151. package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  152. package/dist/components/MsiViewer.js +2 -0
  153. package/dist/components/MsiViewer.js.map +1 -0
  154. package/dist/components/MsiViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  155. package/dist/components/MsiViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  156. package/dist/components/NotebookViewer.js +2 -0
  157. package/dist/components/NotebookViewer.js.map +1 -0
  158. package/dist/components/NotebookViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  159. package/dist/components/NotebookViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  160. package/dist/components/PdfViewer.js +2 -0
  161. package/dist/components/PdfViewer.js.map +1 -0
  162. package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  163. package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  164. package/dist/components/PptxNavigationPane.js +9 -0
  165. package/dist/components/PptxNavigationPane.js.map +1 -0
  166. package/dist/components/PptxNavigationPane.vue.d.ts +20 -0
  167. package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js +120 -0
  168. package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
  169. package/dist/components/PptxViewer.js +3 -1
  170. package/dist/components/PptxViewer.js.map +1 -0
  171. package/dist/components/PptxViewer.vue_vue_type_script_setup_true_lang.js +71 -39
  172. package/dist/components/PptxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  173. package/dist/components/PsdViewer.js +2 -0
  174. package/dist/components/PsdViewer.js.map +1 -0
  175. package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  176. package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  177. package/dist/components/QmcAudioViewer.js +2 -0
  178. package/dist/components/QmcAudioViewer.js.map +1 -0
  179. package/dist/components/QmcAudioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  180. package/dist/components/QmcAudioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  181. package/dist/components/RasterViewer.js +2 -0
  182. package/dist/components/RasterViewer.js.map +1 -0
  183. package/dist/components/RasterViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  184. package/dist/components/RasterViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  185. package/dist/components/RtfViewer.js +2 -0
  186. package/dist/components/RtfViewer.js.map +1 -0
  187. package/dist/components/RtfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  188. package/dist/components/RtfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  189. package/dist/components/SevenZipBrowser.js +2 -0
  190. package/dist/components/SevenZipBrowser.js.map +1 -0
  191. package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
  192. package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
  193. package/dist/components/SqliteViewer.js +2 -0
  194. package/dist/components/SqliteViewer.js.map +1 -0
  195. package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  196. package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  197. package/dist/components/SubtitleViewer.js +2 -0
  198. package/dist/components/SubtitleViewer.js.map +1 -0
  199. package/dist/components/SubtitleViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  200. package/dist/components/SubtitleViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  201. package/dist/components/XlsxChart.js +2 -0
  202. package/dist/components/XlsxChart.js.map +1 -0
  203. package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js +2 -0
  204. package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js.map +1 -0
  205. package/dist/components/XlsxViewer.js +3 -1
  206. package/dist/components/XlsxViewer.js.map +1 -0
  207. package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js +301 -304
  208. package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  209. package/dist/components/XmindViewer.js +2 -0
  210. package/dist/components/XmindViewer.js.map +1 -0
  211. package/dist/components/XmindViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  212. package/dist/components/XmindViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  213. package/dist/components/ZipBrowser.js +3 -1
  214. package/dist/components/ZipBrowser.js.map +1 -0
  215. package/dist/components/ZipBrowser.vue.d.ts +3 -1
  216. package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js +154 -122
  217. package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
  218. package/dist/components/ZoomImage.js +2 -0
  219. package/dist/components/ZoomImage.js.map +1 -0
  220. package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js +2 -0
  221. package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js.map +1 -0
  222. package/dist/components/epl/EplCodeBlock.js +2 -0
  223. package/dist/components/epl/EplCodeBlock.js.map +1 -0
  224. package/dist/components/epl/EplCodeBlock.vue_vue_type_script_setup_true_lang.js +2 -0
  225. package/dist/components/epl/EplCodeBlock.vue_vue_type_script_setup_true_lang.js.map +1 -0
  226. package/dist/components/epl/EplDoc.js +2 -0
  227. package/dist/components/epl/EplDoc.js.map +1 -0
  228. package/dist/components/epl/EplDoc.vue_vue_type_script_setup_true_lang.js +2 -0
  229. package/dist/components/epl/EplDoc.vue_vue_type_script_setup_true_lang.js.map +1 -0
  230. package/dist/components/epl/EplMethod.js +2 -0
  231. package/dist/components/epl/EplMethod.js.map +1 -0
  232. package/dist/components/epl/EplMethod.vue_vue_type_script_setup_true_lang.js +2 -0
  233. package/dist/components/epl/EplMethod.vue_vue_type_script_setup_true_lang.js.map +1 -0
  234. package/dist/components/epl/EplResource.js +2 -0
  235. package/dist/components/epl/EplResource.js.map +1 -0
  236. package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js +2 -0
  237. package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js.map +1 -0
  238. package/dist/components/epl/EplTree.js +2 -0
  239. package/dist/components/epl/EplTree.js.map +1 -0
  240. package/dist/components/epl/EplTree.vue_vue_type_script_setup_true_lang.js +2 -0
  241. package/dist/components/epl/EplTree.vue_vue_type_script_setup_true_lang.js.map +1 -0
  242. package/dist/components/epl/EplVarTable.js +2 -0
  243. package/dist/components/epl/EplVarTable.js.map +1 -0
  244. package/dist/components/epl/EplVarTable.vue_vue_type_script_setup_true_lang.js +2 -0
  245. package/dist/components/epl/EplVarTable.vue_vue_type_script_setup_true_lang.js.map +1 -0
  246. package/dist/components/epl/numbering.js +2 -0
  247. package/dist/components/epl/numbering.js.map +1 -0
  248. package/dist/components/icons/Icon.js +2 -0
  249. package/dist/components/icons/Icon.js.map +1 -0
  250. package/dist/components/icons/Icon.vue_vue_type_script_setup_true_lang.js +2 -0
  251. package/dist/components/icons/Icon.vue_vue_type_script_setup_true_lang.js.map +1 -0
  252. package/dist/components/icons/iconPaths.js +2 -0
  253. package/dist/components/icons/iconPaths.js.map +1 -0
  254. package/dist/components/optionalViewer.js +2 -0
  255. package/dist/components/optionalViewer.js.map +1 -0
  256. package/dist/components/ui/ClampBlock.js +2 -0
  257. package/dist/components/ui/ClampBlock.js.map +1 -0
  258. package/dist/composables/useElementNavigation.d.ts +21 -0
  259. package/dist/composables/useElementNavigation.js +63 -0
  260. package/dist/composables/useElementNavigation.js.map +1 -0
  261. package/dist/composables/useFileViewer.js +2 -0
  262. package/dist/composables/useFileViewer.js.map +1 -0
  263. package/dist/composables/useLightbox.js +2 -0
  264. package/dist/composables/useLightbox.js.map +1 -0
  265. package/dist/composables/useLogRules.js +2 -0
  266. package/dist/composables/useLogRules.js.map +1 -0
  267. package/dist/lib/aes.js +2 -0
  268. package/dist/lib/aes.js.map +1 -0
  269. package/dist/lib/appx.js +2 -0
  270. package/dist/lib/appx.js.map +1 -0
  271. package/dist/lib/archiveHtml.d.ts +49 -0
  272. package/dist/lib/archiveHtml.js +377 -0
  273. package/dist/lib/archiveHtml.js.map +1 -0
  274. package/dist/lib/asn1.js +2 -0
  275. package/dist/lib/asn1.js.map +1 -0
  276. package/dist/lib/binobj.js +2 -0
  277. package/dist/lib/binobj.js.map +1 -0
  278. package/dist/lib/brotli.js +2 -0
  279. package/dist/lib/brotli.js.map +1 -0
  280. package/dist/lib/bzip2.js +2 -0
  281. package/dist/lib/bzip2.js.map +1 -0
  282. package/dist/lib/classfile.js +2 -0
  283. package/dist/lib/classfile.js.map +1 -0
  284. package/dist/lib/codePreview.js +2 -0
  285. package/dist/lib/codePreview.js.map +1 -0
  286. package/dist/lib/codefold.js +2 -0
  287. package/dist/lib/codefold.js.map +1 -0
  288. package/dist/lib/codeformat.js +2 -0
  289. package/dist/lib/codeformat.js.map +1 -0
  290. package/dist/lib/csv.js +2 -0
  291. package/dist/lib/csv.js.map +1 -0
  292. package/dist/lib/des.js +2 -0
  293. package/dist/lib/des.js.map +1 -0
  294. package/dist/lib/diagram.js +2 -0
  295. package/dist/lib/diagram.js.map +1 -0
  296. package/dist/lib/dicom.js +2 -0
  297. package/dist/lib/dicom.js.map +1 -0
  298. package/dist/lib/dicomCodecs.js +2 -0
  299. package/dist/lib/dicomCodecs.js.map +1 -0
  300. package/dist/lib/dicomRender.js +2 -0
  301. package/dist/lib/dicomRender.js.map +1 -0
  302. package/dist/lib/docxNavigation.d.ts +31 -0
  303. package/dist/lib/docxNavigation.js +65 -0
  304. package/dist/lib/docxNavigation.js.map +1 -0
  305. package/dist/lib/dxf.js +2 -0
  306. package/dist/lib/dxf.js.map +1 -0
  307. package/dist/lib/eml.js +2 -0
  308. package/dist/lib/eml.js.map +1 -0
  309. package/dist/lib/encoding.js +2 -0
  310. package/dist/lib/encoding.js.map +1 -0
  311. package/dist/lib/epl/code/codeDataParser.js +2 -0
  312. package/dist/lib/epl/code/codeDataParser.js.map +1 -0
  313. package/dist/lib/epl/code/codeLines.js +2 -0
  314. package/dist/lib/epl/code/codeLines.js.map +1 -0
  315. package/dist/lib/epl/code/textCode.js +2 -0
  316. package/dist/lib/epl/code/textCode.js.map +1 -0
  317. package/dist/lib/epl/eplDocument.js +2 -0
  318. package/dist/lib/epl/eplDocument.js.map +1 -0
  319. package/dist/lib/epl/imageList.js +2 -0
  320. package/dist/lib/epl/imageList.js.map +1 -0
  321. package/dist/lib/epl/index.js +2 -0
  322. package/dist/lib/epl/index.js.map +1 -0
  323. package/dist/lib/epl/io/binaryReader.js +2 -0
  324. package/dist/lib/epl/io/binaryReader.js.map +1 -0
  325. package/dist/lib/epl/io/encoding.js +2 -0
  326. package/dist/lib/epl/io/encoding.js.map +1 -0
  327. package/dist/lib/epl/model/eplSystemId.js +2 -0
  328. package/dist/lib/epl/model/eplSystemId.js.map +1 -0
  329. package/dist/lib/epl/names/eplLibData.js +2 -0
  330. package/dist/lib/epl/names/eplLibData.js.map +1 -0
  331. package/dist/lib/epl/names/idToNameMap.js +2 -0
  332. package/dist/lib/epl/names/idToNameMap.js.map +1 -0
  333. package/dist/lib/epl/names/libNameProvider.js +2 -0
  334. package/dist/lib/epl/names/libNameProvider.js.map +1 -0
  335. package/dist/lib/epl/program.js +2 -0
  336. package/dist/lib/epl/program.js.map +1 -0
  337. package/dist/lib/epl/projectFileReader.js +2 -0
  338. package/dist/lib/epl/projectFileReader.js.map +1 -0
  339. package/dist/lib/fb2.js +2 -0
  340. package/dist/lib/fb2.js.map +1 -0
  341. package/dist/lib/fileType.js +2 -0
  342. package/dist/lib/fileType.js.map +1 -0
  343. package/dist/lib/fonts.js +2 -0
  344. package/dist/lib/fonts.js.map +1 -0
  345. package/dist/lib/format.js +2 -0
  346. package/dist/lib/format.js.map +1 -0
  347. package/dist/lib/frontmatter.js +2 -0
  348. package/dist/lib/frontmatter.js.map +1 -0
  349. package/dist/lib/geo.js +2 -0
  350. package/dist/lib/geo.js.map +1 -0
  351. package/dist/lib/har.js +2 -0
  352. package/dist/lib/har.js.map +1 -0
  353. package/dist/lib/highlight.js +2 -0
  354. package/dist/lib/highlight.js.map +1 -0
  355. package/dist/lib/highlightLangs.js +2 -0
  356. package/dist/lib/highlightLangs.js.map +1 -0
  357. package/dist/lib/hprof.js +2 -0
  358. package/dist/lib/hprof.js.map +1 -0
  359. package/dist/lib/ipynb.js +2 -0
  360. package/dist/lib/ipynb.js.map +1 -0
  361. package/dist/lib/jks.js +2 -0
  362. package/dist/lib/jks.js.map +1 -0
  363. package/dist/lib/jsonl.js +2 -0
  364. package/dist/lib/jsonl.js.map +1 -0
  365. package/dist/lib/lrc.js +2 -0
  366. package/dist/lib/lrc.js.map +1 -0
  367. package/dist/lib/lzma.js +2 -0
  368. package/dist/lib/lzma.js.map +1 -0
  369. package/dist/lib/markdown/blocks.js +2 -0
  370. package/dist/lib/markdown/blocks.js.map +1 -0
  371. package/dist/lib/markdown/inline.js +2 -0
  372. package/dist/lib/markdown/inline.js.map +1 -0
  373. package/dist/lib/markdown/math.js +2 -0
  374. package/dist/lib/markdown/math.js.map +1 -0
  375. package/dist/lib/markdown/segmentize.js +2 -0
  376. package/dist/lib/markdown/segmentize.js.map +1 -0
  377. package/dist/lib/markdown/styles.js +2 -0
  378. package/dist/lib/markdown/styles.js.map +1 -0
  379. package/dist/lib/mht.js +2 -0
  380. package/dist/lib/mht.js.map +1 -0
  381. package/dist/lib/midi.js +2 -0
  382. package/dist/lib/midi.js.map +1 -0
  383. package/dist/lib/mime.js +2 -0
  384. package/dist/lib/mime.js.map +1 -0
  385. package/dist/lib/mindmap.js +2 -0
  386. package/dist/lib/mindmap.js.map +1 -0
  387. package/dist/lib/mobi.js +2 -0
  388. package/dist/lib/mobi.js.map +1 -0
  389. package/dist/lib/msi.js +2 -0
  390. package/dist/lib/msi.js.map +1 -0
  391. package/dist/lib/optionalDep.js +2 -0
  392. package/dist/lib/optionalDep.js.map +1 -0
  393. package/dist/lib/pkcs12.js +2 -0
  394. package/dist/lib/pkcs12.js.map +1 -0
  395. package/dist/lib/pptxNavigation.d.ts +7 -0
  396. package/dist/lib/pptxNavigation.js +29 -0
  397. package/dist/lib/pptxNavigation.js.map +1 -0
  398. package/dist/lib/qmc.js +2 -0
  399. package/dist/lib/qmc.js.map +1 -0
  400. package/dist/lib/rar.js +2 -0
  401. package/dist/lib/rar.js.map +1 -0
  402. package/dist/lib/rawPreview.js +2 -0
  403. package/dist/lib/rawPreview.js.map +1 -0
  404. package/dist/lib/rc2.js +2 -0
  405. package/dist/lib/rc2.js.map +1 -0
  406. package/dist/lib/resolveInput.js +2 -0
  407. package/dist/lib/resolveInput.js.map +1 -0
  408. package/dist/lib/rtf.js +2 -0
  409. package/dist/lib/rtf.js.map +1 -0
  410. package/dist/lib/sevenZip.js +2 -0
  411. package/dist/lib/sevenZip.js.map +1 -0
  412. package/dist/lib/sha1.js +2 -0
  413. package/dist/lib/sha1.js.map +1 -0
  414. package/dist/lib/sha256.js +2 -0
  415. package/dist/lib/sha256.js.map +1 -0
  416. package/dist/lib/speechAudio.js +2 -0
  417. package/dist/lib/speechAudio.js.map +1 -0
  418. package/dist/lib/sqlite.js +2 -0
  419. package/dist/lib/sqlite.js.map +1 -0
  420. package/dist/lib/subtitle.js +2 -0
  421. package/dist/lib/subtitle.js.map +1 -0
  422. package/dist/lib/tarball.js +2 -0
  423. package/dist/lib/tarball.js.map +1 -0
  424. package/dist/lib/viewerFormats.js +2 -0
  425. package/dist/lib/viewerFormats.js.map +1 -0
  426. package/dist/lib/viewerKind.js +2 -0
  427. package/dist/lib/viewerKind.js.map +1 -0
  428. package/dist/lib/x509.js +2 -0
  429. package/dist/lib/x509.js.map +1 -0
  430. package/dist/lib/xlsx.js +2 -0
  431. package/dist/lib/xlsx.js.map +1 -0
  432. package/dist/lib/xlsxCharts.js +2 -0
  433. package/dist/lib/xlsxCharts.js.map +1 -0
  434. package/dist/lib/xmind.js +2 -0
  435. package/dist/lib/xmind.js.map +1 -0
  436. package/dist/lib/zip.js +2 -0
  437. package/dist/lib/zip.js.map +1 -0
  438. package/dist/style.css +1 -1
  439. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JksViewer.js","names":[],"sources":["../../src/components/JksViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue'\nimport {\n decryptPrivateKey,\n JCEKS_MAGIC,\n JKS_MAGIC,\n JksError,\n readJks,\n verifyStorePassword,\n type JksEntry,\n type JksStore,\n type KeyDecryptResult,\n} from '@/lib/jks'\nimport { looksLikePkcs12, readPkcs12, type Pkcs12Result } from '@/lib/pkcs12'\nimport { parseCertificate, sha256Hex, type CertSummary } from '@/lib/x509'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer for keystores. Fetches the BYTES itself (binary-safe) and sniffs the format:\n * - JKS / JCEKS (magic 0xFEEDFEED / 0xCECECECE): the Sun binary format (see lib/jks.ts). Certificates are\n * plaintext, so the whole inventory shows immediately; a password verifies the store MAC and decrypts JKS\n * private keys.\n * - PKCS#12 / PFX (`.p12`/`.pfx`, and the very common `.jks`-that-is-actually-PKCS#12 — keytool defaults to\n * PKCS12 since JDK 9): everything is encrypted (see lib/pkcs12.ts), so nothing shows until the password\n * unlocks it; then certs + private keys appear and the integrity MAC is verified.\n *\n * Both render the same cards: per entry the alias / kind / (JKS) creation date, the private-key algorithm once\n * decrypted, and each certificate's X.509 fields (subject/issuer, validity + expiry badge, key + signature\n * algorithm, SANs, SHA-256 fingerprint — reusing lib/x509.ts). All parsing/decryption is in-browser; the\n * password and key material never leave the page.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Format = 'jks' | 'jceks' | 'pkcs12'\n\ninterface CertView {\n summary: CertSummary\n id: string\n}\ninterface EntryView {\n entry: JksEntry\n index: number\n certs: CertView[]\n}\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst format = ref<Format | null>(null)\nconst rawBuf = shallowRef<ArrayBuffer | null>(null) // kept for PKCS#12 (decrypted on password submit)\nconst jksStore = shallowRef<JksStore | null>(null)\nconst entryViews = shallowRef<EntryView[]>([])\nconst fingerprints = reactive<Record<string, string>>({})\n\n// Password state: one field drives store-MAC verification and per-key decryption (keytool defaults the store\n// password equal to each key password — the common case).\nconst password = ref('')\nconst pwTried = ref(false)\nconst integrity = ref<'unknown' | 'ok' | 'bad'>('unknown')\nconst unlocked = ref(false) // PKCS#12: bags decrypted\nconst keyResults = reactive<Record<number, KeyDecryptResult>>({})\nconst p12meta = shallowRef<Pkcs12Result | null>(null)\nconst p12error = ref('') // unsupported / malformed (distinct from wrong-password)\n\nconst SIZE_LIMIT = 8 * 1024 * 1024\n\nconst isPkcs12 = computed(() => format.value === 'pkcs12')\nconst storeTypeLabel = computed(() =>\n format.value === 'pkcs12' ? 'PKCS#12' : format.value === 'jceks' ? 'JCEKS' : 'JKS',\n)\nconst hasPrivateKeys = computed(() => entryViews.value.some((e) => e.entry.type === 'privateKey'))\nconst storeMeta = computed(() => {\n const s = jksStore.value\n if (format.value === 'pkcs12') {\n const m = p12meta.value\n if (m) return [...m.encryptionAlgos, m.macAlgorithm].filter(Boolean).join(' · ')\n return '证书与私钥均加密,输入密码解密'\n }\n return s ? `版本 ${s.version} · ${s.entries.length} 个条目` : ''\n})\n\nfunction fingerprintAll(): void {\n for (const ev of entryViews.value) {\n for (const cv of ev.certs) {\n if (cv.summary.ok) {\n void sha256Hex(cv.summary.der)\n .then((hex) => {\n fingerprints[cv.id] = hex\n })\n .catch(() => {})\n }\n }\n }\n}\n\nfunction buildJksEntryViews(store: JksStore): void {\n entryViews.value = store.entries.map((entry, index) => ({\n entry,\n index,\n certs: entry.certs.map((c, ci) => ({ summary: parseCertificate(c.der), id: `${index}.${ci}` })),\n }))\n}\n\nfunction processBuffer(buf: ArrayBuffer): void {\n const bytes = new Uint8Array(buf)\n const magic =\n bytes.length >= 4 ? ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0 : 0\n if (magic === JKS_MAGIC || magic === JCEKS_MAGIC) {\n const store = readJks(buf)\n format.value = store.storeType\n jksStore.value = store\n buildJksEntryViews(store)\n fingerprintAll()\n } else if (looksLikePkcs12(bytes)) {\n format.value = 'pkcs12'\n rawBuf.value = buf // wait for the password before decrypting\n } else {\n throw new JksError(\n `文件头不匹配(magic=0x${magic.toString(16).padStart(8, '0')})—— 不是 JKS/JCEKS/PKCS#12 密钥库`,\n )\n }\n status.value = 'ready'\n}\n\nfunction resetState(): void {\n errorMsg.value = ''\n format.value = null\n rawBuf.value = null\n jksStore.value = null\n entryViews.value = []\n password.value = ''\n pwTried.value = false\n integrity.value = 'unknown'\n unlocked.value = false\n p12meta.value = null\n p12error.value = ''\n for (const k of Object.keys(fingerprints)) delete fingerprints[k]\n for (const k of Object.keys(keyResults)) delete keyResults[Number(k)]\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n resetState()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n processBuffer(await res.arrayBuffer())\n } catch (e) {\n errorMsg.value = e instanceof JksError || e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nfunction submitJksPassword(store: JksStore): void {\n integrity.value = verifyStorePassword(store, password.value) ? 'ok' : 'bad'\n for (const ev of entryViews.value) {\n if (ev.entry.type === 'privateKey' && ev.entry.encryptedKey) {\n keyResults[ev.index] = decryptPrivateKey(ev.entry.encryptedKey, password.value)\n }\n }\n}\n\nfunction buildPkcs12EntryViews(result: Pkcs12Result): void {\n const views: EntryView[] = []\n const usedCerts = new Set<number>()\n let idx = 0\n for (const k of result.keys) {\n const chain = result.certs.filter((c, ci) => {\n const match = !!k.localKeyId && c.localKeyId === k.localKeyId\n if (match) usedCerts.add(ci)\n return match\n })\n const entry: JksEntry = {\n type: 'privateKey',\n alias: k.friendlyName || k.localKeyId || `私钥 ${idx + 1}`,\n createdMs: 0,\n certs: chain.map((c) => ({ type: 'X.509', der: c.der })),\n }\n views.push({\n entry,\n index: idx,\n certs: entry.certs.map((c, ci) => ({ summary: parseCertificate(c.der), id: `${idx}.${ci}` })),\n })\n keyResults[idx] = {\n ok: true,\n key: { pkcs8: k.pkcs8, algorithm: k.algorithm, bits: k.bits, curve: k.curve },\n }\n idx++\n }\n result.certs.forEach((c, ci) => {\n if (usedCerts.has(ci)) return\n const entry: JksEntry = {\n type: 'trustedCert',\n alias: c.friendlyName || `证书 ${idx + 1}`,\n createdMs: 0,\n certs: [{ type: 'X.509', der: c.der }],\n }\n views.push({ entry, index: idx, certs: [{ summary: parseCertificate(c.der), id: `${idx}.0` }] })\n idx++\n })\n entryViews.value = views\n}\n\nfunction submitPkcs12Password(buf: ArrayBuffer): void {\n const r = readPkcs12(buf, password.value)\n if (!r.ok) {\n unlocked.value = false\n entryViews.value = []\n for (const k of Object.keys(keyResults)) delete keyResults[Number(k)]\n if (r.reason === 'wrong-password') {\n integrity.value = 'bad'\n p12error.value = ''\n } else {\n integrity.value = 'unknown'\n p12error.value =\n (r.reason === 'unsupported' ? '暂不支持的加密方式:' : '解析失败:') + (r.detail ?? '')\n }\n return\n }\n p12error.value = ''\n p12meta.value = r.result\n buildPkcs12EntryViews(r.result)\n integrity.value = r.result.macVerified === true ? 'ok' : 'unknown'\n unlocked.value = true\n fingerprintAll()\n}\n\n// Apply the password to whichever format this keystore is.\nfunction submitPassword(): void {\n if (!password.value) return\n pwTried.value = true\n if (format.value === 'pkcs12') {\n if (rawBuf.value) submitPkcs12Password(rawBuf.value)\n } else if (jksStore.value) {\n submitJksPassword(jksStore.value)\n }\n}\n\n// ── friendly-field formatting (shared with the cert cards) ─────────────────────────────────────────\nfunction formatDn(rdns: CertSummary['subject']): string {\n return rdns.map((r) => `${r.name}=${r.value}`).join(', ')\n}\n\nconst ENTRY_LABEL: Record<JksEntry['type'], string> = {\n privateKey: '私钥条目',\n trustedCert: '受信任证书',\n secretKey: '密钥条目',\n}\nconst ENTRY_CLASS: Record<JksEntry['type'], string> = {\n privateKey: 'jks__badge--key',\n trustedCert: 'jks__badge--cert',\n secretKey: 'jks__badge--secret',\n}\n\nfunction formatDate(ms: number): string {\n if (!ms) return ''\n try {\n return new Date(ms).toLocaleString('zh-CN', { hour12: false })\n } catch {\n return ''\n }\n}\n\nfunction publicKeyText(s: CertSummary): string {\n const parts: string[] = []\n if (s.pubKeyAlg) parts.push(s.pubKeyAlg)\n if (s.pubKeyBits) parts.push(`${s.pubKeyBits} 位`)\n if (s.pubKeyCurve && s.pubKeyCurve !== s.pubKeyAlg) parts.push(s.pubKeyCurve)\n return parts.join(' · ')\n}\n\n// Validity badge: future / expired / soon (≤30 days) / valid, from notBefore/notAfter vs now.\nfunction expiry(s: CertSummary): { text: string; cls: string } | null {\n if (s.notAfterMs == null) return null\n const now = Date.now()\n if (s.notBeforeMs != null && now < s.notBeforeMs)\n return { text: '尚未生效', cls: 'jks__cbadge--warn' }\n if (now > s.notAfterMs) return { text: '已过期', cls: 'jks__cbadge--bad' }\n const days = Math.floor((s.notAfterMs - now) / 86_400_000)\n if (days <= 30) return { text: `${days} 天后过期`, cls: 'jks__cbadge--warn' }\n return { text: `有效(剩 ${days} 天)`, cls: 'jks__cbadge--ok' }\n}\n\n// The private-key row's decrypted/locked/error state for one entry.\ntype KeyState =\n | { kind: 'locked' }\n | { kind: 'ok'; text: string }\n | { kind: 'wrong' }\n | { kind: 'unsupported'; detail?: string }\n | { kind: 'malformed' }\n\nfunction keyState(index: number): KeyState {\n const r = keyResults[index]\n if (!r) return { kind: 'locked' }\n if (r.ok) {\n const parts = [r.key.algorithm]\n if (r.key.bits) parts.push(`${r.key.bits} 位`)\n if (r.key.curve && r.key.curve !== r.key.algorithm) parts.push(r.key.curve)\n return { kind: 'ok', text: parts.join(' · ') }\n }\n if (r.reason === 'wrong-password') return { kind: 'wrong' }\n if (r.reason === 'unsupported') return { kind: 'unsupported', detail: r.detail }\n return { kind: 'malformed' }\n}\n\nconst pwPlaceholder = computed(() => {\n if (isPkcs12.value)\n return unlocked.value ? '已解锁(可重新输入)' : '输入 PKCS#12 密码解密证书与私钥'\n return hasPrivateKeys.value ? '输入密码:解密私钥并校验完整性' : '输入库密码校验完整性(可选)'\n})\n\n// Integrity/unlock status line after a password attempt (format-aware).\nconst integrityText = computed(() => {\n if (integrity.value === 'bad') {\n return isPkcs12.value ? '密码错误 —— 无法解密' : '完整性校验未通过 —— 密码错误或文件已被修改'\n }\n if (integrity.value === 'ok') {\n if (isPkcs12.value) return `密码正确,完整性校验通过(${p12meta.value?.macAlgorithm ?? 'MAC'})`\n return '完整性校验通过 —— 密码正确,keystore 未被篡改'\n }\n // PKCS#12 unlocked but MAC absent/unsupported: still a successful decrypt.\n if (isPkcs12.value && unlocked.value) return '已解密(该文件无 MAC 或摘要不支持,密码经解密验证)'\n return ''\n})\nconst integrityOk = computed(() => integrity.value === 'ok' || (isPkcs12.value && unlocked.value))\n\nwatch(() => [props.url, props.name], load, { immediate: true })\nonBeforeUnmount(() => {\n // nothing to revoke; bytes are not retained as object URLs\n})\n</script>\n\n<template>\n <div class=\"jks\">\n <p v-if=\"status === 'loading'\" class=\"jks__msg\">正在解析 keystore…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"jks__msg jks__gate\">\n <p>该文件较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"jks__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"jks__msg jks__msg--error\">\n <p>无法解析:{{ errorMsg }}</p>\n <button type=\"button\" class=\"jks__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else-if=\"format\" class=\"jks__scroll\">\n <!-- store header + password -->\n <section class=\"jks__store\">\n <header class=\"jks__storehead\">\n <span class=\"jks__badge jks__badge--type\">{{ storeTypeLabel }}</span>\n <span class=\"jks__storetitle\">{{ isPkcs12 ? 'PKCS#12 密钥库' : 'Java KeyStore' }}</span>\n <span class=\"jks__storemeta\">{{ storeMeta }}</span>\n </header>\n\n <div class=\"jks__pw\" :class=\"{ 'jks__pw--ok': integrityOk }\">\n <Icon name=\"key\" :size=\"14\" />\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"jks__pwinput\"\n :placeholder=\"pwPlaceholder\"\n autocomplete=\"off\"\n @keyup.enter=\"submitPassword\"\n />\n <button type=\"button\" class=\"jks__pwbtn\" @click=\"submitPassword\">\n {{ isPkcs12 && unlocked ? '重新解锁' : '解锁' }}\n </button>\n </div>\n\n <p\n v-if=\"pwTried && integrityText\"\n class=\"jks__integrity\"\n :class=\"`jks__integrity--${integrity}`\"\n >\n <Icon :name=\"integrityOk ? 'check' : 'close'\" :size=\"13\" />\n <span>{{ integrityText }}</span>\n </p>\n\n <p class=\"jks__hint\">全部解析与解密均在浏览器本地完成,密码与密钥不会上传。</p>\n <p v-if=\"jksStore?.truncatedNote\" class=\"jks__warn\">{{ jksStore.truncatedNote }}</p>\n <p v-if=\"p12error\" class=\"jks__warn\">{{ p12error }}</p>\n <p v-for=\"(w, i) in p12meta?.warnings ?? []\" :key=\"i\" class=\"jks__warn\">{{ w }}</p>\n </section>\n\n <!-- PKCS#12 pre-unlock: nothing is visible until decrypted -->\n <div v-if=\"isPkcs12 && !unlocked\" class=\"jks__locked\">\n <Icon name=\"key\" :size=\"26\" class=\"jks__lockicon\" />\n <p>PKCS#12 密钥库的证书与私钥均已加密,输入上方密码后在此显示内容。</p>\n </div>\n\n <!-- entries -->\n <section v-for=\"ev in entryViews\" :key=\"ev.index\" class=\"jks__entry\">\n <header class=\"jks__entryhead\">\n <span class=\"jks__badge\" :class=\"ENTRY_CLASS[ev.entry.type]\">{{\n ENTRY_LABEL[ev.entry.type]\n }}</span>\n <span class=\"jks__alias\" :title=\"ev.entry.alias\">{{ ev.entry.alias }}</span>\n <span v-if=\"ev.entry.createdMs\" class=\"jks__date\">{{\n formatDate(ev.entry.createdMs)\n }}</span>\n </header>\n\n <!-- private key material -->\n <div v-if=\"ev.entry.type === 'privateKey'\" class=\"jks__key\">\n <template v-if=\"keyState(ev.index).kind === 'locked'\">\n <Icon name=\"key\" :size=\"13\" class=\"jks__keyicon\" />\n <span class=\"jks__keytext\">私钥已加密 —— 输入密码后在此显示密钥算法</span>\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'ok'\">\n <Icon name=\"check\" :size=\"13\" class=\"jks__keyicon jks__keyicon--ok\" />\n <span class=\"jks__keytext jks__keytext--ok\"\n >私钥已解密 · {{ (keyState(ev.index) as { text: string }).text }}</span\n >\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'wrong'\">\n <Icon name=\"close\" :size=\"13\" class=\"jks__keyicon jks__keyicon--bad\" />\n <span class=\"jks__keytext jks__keytext--bad\">密码错误 —— 无法解密该私钥</span>\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'unsupported'\">\n <Icon name=\"key\" :size=\"13\" class=\"jks__keyicon\" />\n <span class=\"jks__keytext\"\n >该私钥加密方式暂不支持浏览器内解密({{\n (keyState(ev.index) as { detail?: string }).detail\n }})—— 证书链仍可查看</span\n >\n </template>\n <template v-else>\n <Icon name=\"close\" :size=\"13\" class=\"jks__keyicon jks__keyicon--bad\" />\n <span class=\"jks__keytext jks__keytext--bad\">私钥数据无法解析</span>\n </template>\n </div>\n\n <p v-else-if=\"ev.entry.type === 'secretKey'\" class=\"jks__note\">\n JCEKS 对称密钥条目(SecretKey)—— 以 Java 序列化对象存储,暂不解析。\n </p>\n\n <!-- certificate chain -->\n <div v-if=\"ev.certs.length\" class=\"jks__chain\">\n <div v-if=\"ev.entry.type === 'privateKey'\" class=\"jks__chainlabel\">\n 证书链({{ ev.certs.length }})\n </div>\n <article v-for=\"cv in ev.certs\" :key=\"cv.id\" class=\"jks__cert\">\n <div class=\"jks__certhead\">\n <span class=\"jks__certcn\">{{ cv.summary.subjectCN || cv.summary.label }}</span>\n <span v-if=\"expiry(cv.summary)\" class=\"jks__cbadge\" :class=\"expiry(cv.summary)?.cls\">\n {{ expiry(cv.summary)?.text }}\n </span>\n </div>\n <p v-if=\"cv.summary.note && !cv.summary.ok\" class=\"jks__note\">{{ cv.summary.note }}</p>\n <dl class=\"jks__dl\">\n <template v-if=\"cv.summary.subject.length\">\n <dt>主题</dt>\n <dd>{{ formatDn(cv.summary.subject) }}</dd>\n </template>\n <template v-if=\"cv.summary.issuer.length\">\n <dt>颁发者</dt>\n <dd>{{ formatDn(cv.summary.issuer) }}</dd>\n </template>\n <template v-if=\"cv.summary.notBefore || cv.summary.notAfter\">\n <dt>有效期</dt>\n <dd>{{ cv.summary.notBefore ?? '?' }} → {{ cv.summary.notAfter ?? '?' }}</dd>\n </template>\n <template v-if=\"publicKeyText(cv.summary)\">\n <dt>公钥</dt>\n <dd>{{ publicKeyText(cv.summary) }}</dd>\n </template>\n <template v-if=\"cv.summary.sigAlg\">\n <dt>签名算法</dt>\n <dd>{{ cv.summary.sigAlg }}</dd>\n </template>\n <template v-if=\"cv.summary.serialHex\">\n <dt>序列号</dt>\n <dd class=\"jks__mono\">{{ cv.summary.serialHex }}</dd>\n </template>\n <template v-if=\"cv.summary.sans.length\">\n <dt>备用名 (SAN)</dt>\n <dd>{{ cv.summary.sans.join(',') }}</dd>\n </template>\n <template v-if=\"fingerprints[cv.id]\">\n <dt>SHA-256</dt>\n <dd class=\"jks__mono jks__fp\">{{ fingerprints[cv.id] }}</dd>\n </template>\n </dl>\n </article>\n </div>\n </section>\n </div>\n </div>\n</template>\n\n<style scoped>\n.jks {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.jks__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.jks__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.jks__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.jks__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.jks__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.jks__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 14px 14px 60px;\n}\n\n/* Store header + password ---------------------------------------------------------------------- */\n.jks__store {\n margin-bottom: 16px;\n border: 1px solid var(--border);\n border-radius: 10px;\n background: var(--surface);\n overflow: hidden;\n}\n.jks__storehead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n background: var(--surface-2);\n border-bottom: 1px solid var(--border);\n}\n.jks__storetitle {\n flex-shrink: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__storemeta {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n text-align: right;\n font-size: 11.5px;\n color: var(--faint);\n}\n\n.jks__pw {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 12px;\n color: var(--muted);\n}\n.jks__pw--ok {\n color: var(--accent);\n}\n.jks__pwinput {\n flex: 1 1 auto;\n min-width: 0;\n height: 28px;\n padding: 0 9px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.jks__pwinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.jks__pwbtn {\n flex-shrink: 0;\n height: 28px;\n padding: 0 14px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.jks__pwbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.jks__integrity {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n padding: 0 12px 8px;\n font-size: 12px;\n}\n.jks__integrity--ok {\n color: #15803d;\n}\n.jks__integrity--bad {\n color: #b91c1c;\n}\n.jks__integrity--unknown {\n color: var(--accent);\n}\n.jks__hint {\n margin: 0;\n padding: 0 12px 10px;\n font-size: 11.5px;\n line-height: 1.5;\n color: var(--faint);\n}\n.jks__warn {\n margin: 0;\n padding: 9px 12px;\n font-size: 12px;\n line-height: 1.5;\n color: #b45309;\n background: color-mix(in srgb, #f59e0b 12%, transparent);\n border-top: 1px solid var(--border);\n}\n\n.jks__locked {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n padding: 40px 24px;\n text-align: center;\n color: var(--muted);\n font-size: 12.5px;\n}\n.jks__lockicon {\n color: var(--faint);\n}\n\n/* Badges ------------------------------------------------------------------------------------- */\n.jks__badge {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n border-radius: 5px;\n padding: 2px 7px;\n}\n.jks__badge--type {\n color: var(--accent);\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n}\n.jks__badge--key {\n color: #b45309;\n background: color-mix(in srgb, #f59e0b 16%, transparent);\n}\n.jks__badge--cert {\n color: #15803d;\n background: color-mix(in srgb, #22c55e 16%, transparent);\n}\n.jks__badge--secret {\n color: var(--muted);\n background: var(--surface-2);\n}\n\n/* Entry cards -------------------------------------------------------------------------------- */\n.jks__entry {\n margin-bottom: 14px;\n border: 1px solid var(--border);\n border-radius: 10px;\n background: var(--surface);\n overflow: hidden;\n}\n.jks__entryhead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n border-bottom: 1px solid var(--border);\n}\n.jks__alias {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__date {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n.jks__key {\n display: flex;\n align-items: center;\n gap: 7px;\n padding: 9px 12px;\n font-size: 12px;\n background: color-mix(in srgb, var(--accent) 4%, transparent);\n border-bottom: 1px solid var(--border);\n}\n.jks__keyicon {\n flex-shrink: 0;\n color: var(--faint);\n}\n.jks__keyicon--ok {\n color: #15803d;\n}\n.jks__keyicon--bad {\n color: #b91c1c;\n}\n.jks__keytext {\n min-width: 0;\n color: var(--muted);\n}\n.jks__keytext--ok {\n color: #15803d;\n font-weight: 500;\n}\n.jks__keytext--bad {\n color: #b91c1c;\n}\n\n.jks__note {\n margin: 0;\n padding: 9px 12px;\n font-size: 12px;\n line-height: 1.5;\n color: var(--muted);\n}\n\n/* Certificate chain -------------------------------------------------------------------------- */\n.jks__chain {\n padding: 10px 12px 12px;\n}\n.jks__chainlabel {\n margin-bottom: 8px;\n font-size: 11.5px;\n color: var(--faint);\n}\n.jks__cert {\n margin-bottom: 10px;\n border: 1px solid var(--border);\n border-radius: 8px;\n overflow: hidden;\n}\n.jks__cert:last-child {\n margin-bottom: 0;\n}\n.jks__certhead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 10px;\n background: var(--surface-2);\n border-bottom: 1px solid var(--border);\n}\n.jks__certcn {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12.5px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__cbadge {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n border-radius: 5px;\n padding: 2px 8px;\n}\n.jks__cbadge--ok {\n color: #15803d;\n background: color-mix(in srgb, #22c55e 16%, transparent);\n}\n.jks__cbadge--warn {\n color: #b45309;\n background: color-mix(in srgb, #f59e0b 18%, transparent);\n}\n.jks__cbadge--bad {\n color: #b91c1c;\n background: color-mix(in srgb, #ef4444 16%, transparent);\n}\n.jks__dl {\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 6px 14px;\n margin: 0;\n padding: 10px;\n}\n.jks__dl dt {\n font-size: 12px;\n color: var(--faint);\n white-space: nowrap;\n}\n.jks__dl dd {\n margin: 0;\n font-size: 12.5px;\n color: var(--text);\n word-break: break-word;\n}\n.jks__mono {\n font-family: var(--font-mono);\n font-size: 11.5px;\n}\n.jks__fp {\n word-break: break-all;\n}\n</style>\n"],"mappings":""}
@@ -351,3 +351,5 @@ var ce = { class: "jks" }, le = {
351
351
  });
352
352
  //#endregion
353
353
  export { O as default };
354
+
355
+ //# sourceMappingURL=JksViewer.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JksViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/JksViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue'\nimport {\n decryptPrivateKey,\n JCEKS_MAGIC,\n JKS_MAGIC,\n JksError,\n readJks,\n verifyStorePassword,\n type JksEntry,\n type JksStore,\n type KeyDecryptResult,\n} from '@/lib/jks'\nimport { looksLikePkcs12, readPkcs12, type Pkcs12Result } from '@/lib/pkcs12'\nimport { parseCertificate, sha256Hex, type CertSummary } from '@/lib/x509'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer for keystores. Fetches the BYTES itself (binary-safe) and sniffs the format:\n * - JKS / JCEKS (magic 0xFEEDFEED / 0xCECECECE): the Sun binary format (see lib/jks.ts). Certificates are\n * plaintext, so the whole inventory shows immediately; a password verifies the store MAC and decrypts JKS\n * private keys.\n * - PKCS#12 / PFX (`.p12`/`.pfx`, and the very common `.jks`-that-is-actually-PKCS#12 — keytool defaults to\n * PKCS12 since JDK 9): everything is encrypted (see lib/pkcs12.ts), so nothing shows until the password\n * unlocks it; then certs + private keys appear and the integrity MAC is verified.\n *\n * Both render the same cards: per entry the alias / kind / (JKS) creation date, the private-key algorithm once\n * decrypted, and each certificate's X.509 fields (subject/issuer, validity + expiry badge, key + signature\n * algorithm, SANs, SHA-256 fingerprint — reusing lib/x509.ts). All parsing/decryption is in-browser; the\n * password and key material never leave the page.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Format = 'jks' | 'jceks' | 'pkcs12'\n\ninterface CertView {\n summary: CertSummary\n id: string\n}\ninterface EntryView {\n entry: JksEntry\n index: number\n certs: CertView[]\n}\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst format = ref<Format | null>(null)\nconst rawBuf = shallowRef<ArrayBuffer | null>(null) // kept for PKCS#12 (decrypted on password submit)\nconst jksStore = shallowRef<JksStore | null>(null)\nconst entryViews = shallowRef<EntryView[]>([])\nconst fingerprints = reactive<Record<string, string>>({})\n\n// Password state: one field drives store-MAC verification and per-key decryption (keytool defaults the store\n// password equal to each key password — the common case).\nconst password = ref('')\nconst pwTried = ref(false)\nconst integrity = ref<'unknown' | 'ok' | 'bad'>('unknown')\nconst unlocked = ref(false) // PKCS#12: bags decrypted\nconst keyResults = reactive<Record<number, KeyDecryptResult>>({})\nconst p12meta = shallowRef<Pkcs12Result | null>(null)\nconst p12error = ref('') // unsupported / malformed (distinct from wrong-password)\n\nconst SIZE_LIMIT = 8 * 1024 * 1024\n\nconst isPkcs12 = computed(() => format.value === 'pkcs12')\nconst storeTypeLabel = computed(() =>\n format.value === 'pkcs12' ? 'PKCS#12' : format.value === 'jceks' ? 'JCEKS' : 'JKS',\n)\nconst hasPrivateKeys = computed(() => entryViews.value.some((e) => e.entry.type === 'privateKey'))\nconst storeMeta = computed(() => {\n const s = jksStore.value\n if (format.value === 'pkcs12') {\n const m = p12meta.value\n if (m) return [...m.encryptionAlgos, m.macAlgorithm].filter(Boolean).join(' · ')\n return '证书与私钥均加密,输入密码解密'\n }\n return s ? `版本 ${s.version} · ${s.entries.length} 个条目` : ''\n})\n\nfunction fingerprintAll(): void {\n for (const ev of entryViews.value) {\n for (const cv of ev.certs) {\n if (cv.summary.ok) {\n void sha256Hex(cv.summary.der)\n .then((hex) => {\n fingerprints[cv.id] = hex\n })\n .catch(() => {})\n }\n }\n }\n}\n\nfunction buildJksEntryViews(store: JksStore): void {\n entryViews.value = store.entries.map((entry, index) => ({\n entry,\n index,\n certs: entry.certs.map((c, ci) => ({ summary: parseCertificate(c.der), id: `${index}.${ci}` })),\n }))\n}\n\nfunction processBuffer(buf: ArrayBuffer): void {\n const bytes = new Uint8Array(buf)\n const magic =\n bytes.length >= 4 ? ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0 : 0\n if (magic === JKS_MAGIC || magic === JCEKS_MAGIC) {\n const store = readJks(buf)\n format.value = store.storeType\n jksStore.value = store\n buildJksEntryViews(store)\n fingerprintAll()\n } else if (looksLikePkcs12(bytes)) {\n format.value = 'pkcs12'\n rawBuf.value = buf // wait for the password before decrypting\n } else {\n throw new JksError(\n `文件头不匹配(magic=0x${magic.toString(16).padStart(8, '0')})—— 不是 JKS/JCEKS/PKCS#12 密钥库`,\n )\n }\n status.value = 'ready'\n}\n\nfunction resetState(): void {\n errorMsg.value = ''\n format.value = null\n rawBuf.value = null\n jksStore.value = null\n entryViews.value = []\n password.value = ''\n pwTried.value = false\n integrity.value = 'unknown'\n unlocked.value = false\n p12meta.value = null\n p12error.value = ''\n for (const k of Object.keys(fingerprints)) delete fingerprints[k]\n for (const k of Object.keys(keyResults)) delete keyResults[Number(k)]\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n resetState()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n processBuffer(await res.arrayBuffer())\n } catch (e) {\n errorMsg.value = e instanceof JksError || e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nfunction submitJksPassword(store: JksStore): void {\n integrity.value = verifyStorePassword(store, password.value) ? 'ok' : 'bad'\n for (const ev of entryViews.value) {\n if (ev.entry.type === 'privateKey' && ev.entry.encryptedKey) {\n keyResults[ev.index] = decryptPrivateKey(ev.entry.encryptedKey, password.value)\n }\n }\n}\n\nfunction buildPkcs12EntryViews(result: Pkcs12Result): void {\n const views: EntryView[] = []\n const usedCerts = new Set<number>()\n let idx = 0\n for (const k of result.keys) {\n const chain = result.certs.filter((c, ci) => {\n const match = !!k.localKeyId && c.localKeyId === k.localKeyId\n if (match) usedCerts.add(ci)\n return match\n })\n const entry: JksEntry = {\n type: 'privateKey',\n alias: k.friendlyName || k.localKeyId || `私钥 ${idx + 1}`,\n createdMs: 0,\n certs: chain.map((c) => ({ type: 'X.509', der: c.der })),\n }\n views.push({\n entry,\n index: idx,\n certs: entry.certs.map((c, ci) => ({ summary: parseCertificate(c.der), id: `${idx}.${ci}` })),\n })\n keyResults[idx] = {\n ok: true,\n key: { pkcs8: k.pkcs8, algorithm: k.algorithm, bits: k.bits, curve: k.curve },\n }\n idx++\n }\n result.certs.forEach((c, ci) => {\n if (usedCerts.has(ci)) return\n const entry: JksEntry = {\n type: 'trustedCert',\n alias: c.friendlyName || `证书 ${idx + 1}`,\n createdMs: 0,\n certs: [{ type: 'X.509', der: c.der }],\n }\n views.push({ entry, index: idx, certs: [{ summary: parseCertificate(c.der), id: `${idx}.0` }] })\n idx++\n })\n entryViews.value = views\n}\n\nfunction submitPkcs12Password(buf: ArrayBuffer): void {\n const r = readPkcs12(buf, password.value)\n if (!r.ok) {\n unlocked.value = false\n entryViews.value = []\n for (const k of Object.keys(keyResults)) delete keyResults[Number(k)]\n if (r.reason === 'wrong-password') {\n integrity.value = 'bad'\n p12error.value = ''\n } else {\n integrity.value = 'unknown'\n p12error.value =\n (r.reason === 'unsupported' ? '暂不支持的加密方式:' : '解析失败:') + (r.detail ?? '')\n }\n return\n }\n p12error.value = ''\n p12meta.value = r.result\n buildPkcs12EntryViews(r.result)\n integrity.value = r.result.macVerified === true ? 'ok' : 'unknown'\n unlocked.value = true\n fingerprintAll()\n}\n\n// Apply the password to whichever format this keystore is.\nfunction submitPassword(): void {\n if (!password.value) return\n pwTried.value = true\n if (format.value === 'pkcs12') {\n if (rawBuf.value) submitPkcs12Password(rawBuf.value)\n } else if (jksStore.value) {\n submitJksPassword(jksStore.value)\n }\n}\n\n// ── friendly-field formatting (shared with the cert cards) ─────────────────────────────────────────\nfunction formatDn(rdns: CertSummary['subject']): string {\n return rdns.map((r) => `${r.name}=${r.value}`).join(', ')\n}\n\nconst ENTRY_LABEL: Record<JksEntry['type'], string> = {\n privateKey: '私钥条目',\n trustedCert: '受信任证书',\n secretKey: '密钥条目',\n}\nconst ENTRY_CLASS: Record<JksEntry['type'], string> = {\n privateKey: 'jks__badge--key',\n trustedCert: 'jks__badge--cert',\n secretKey: 'jks__badge--secret',\n}\n\nfunction formatDate(ms: number): string {\n if (!ms) return ''\n try {\n return new Date(ms).toLocaleString('zh-CN', { hour12: false })\n } catch {\n return ''\n }\n}\n\nfunction publicKeyText(s: CertSummary): string {\n const parts: string[] = []\n if (s.pubKeyAlg) parts.push(s.pubKeyAlg)\n if (s.pubKeyBits) parts.push(`${s.pubKeyBits} 位`)\n if (s.pubKeyCurve && s.pubKeyCurve !== s.pubKeyAlg) parts.push(s.pubKeyCurve)\n return parts.join(' · ')\n}\n\n// Validity badge: future / expired / soon (≤30 days) / valid, from notBefore/notAfter vs now.\nfunction expiry(s: CertSummary): { text: string; cls: string } | null {\n if (s.notAfterMs == null) return null\n const now = Date.now()\n if (s.notBeforeMs != null && now < s.notBeforeMs)\n return { text: '尚未生效', cls: 'jks__cbadge--warn' }\n if (now > s.notAfterMs) return { text: '已过期', cls: 'jks__cbadge--bad' }\n const days = Math.floor((s.notAfterMs - now) / 86_400_000)\n if (days <= 30) return { text: `${days} 天后过期`, cls: 'jks__cbadge--warn' }\n return { text: `有效(剩 ${days} 天)`, cls: 'jks__cbadge--ok' }\n}\n\n// The private-key row's decrypted/locked/error state for one entry.\ntype KeyState =\n | { kind: 'locked' }\n | { kind: 'ok'; text: string }\n | { kind: 'wrong' }\n | { kind: 'unsupported'; detail?: string }\n | { kind: 'malformed' }\n\nfunction keyState(index: number): KeyState {\n const r = keyResults[index]\n if (!r) return { kind: 'locked' }\n if (r.ok) {\n const parts = [r.key.algorithm]\n if (r.key.bits) parts.push(`${r.key.bits} 位`)\n if (r.key.curve && r.key.curve !== r.key.algorithm) parts.push(r.key.curve)\n return { kind: 'ok', text: parts.join(' · ') }\n }\n if (r.reason === 'wrong-password') return { kind: 'wrong' }\n if (r.reason === 'unsupported') return { kind: 'unsupported', detail: r.detail }\n return { kind: 'malformed' }\n}\n\nconst pwPlaceholder = computed(() => {\n if (isPkcs12.value)\n return unlocked.value ? '已解锁(可重新输入)' : '输入 PKCS#12 密码解密证书与私钥'\n return hasPrivateKeys.value ? '输入密码:解密私钥并校验完整性' : '输入库密码校验完整性(可选)'\n})\n\n// Integrity/unlock status line after a password attempt (format-aware).\nconst integrityText = computed(() => {\n if (integrity.value === 'bad') {\n return isPkcs12.value ? '密码错误 —— 无法解密' : '完整性校验未通过 —— 密码错误或文件已被修改'\n }\n if (integrity.value === 'ok') {\n if (isPkcs12.value) return `密码正确,完整性校验通过(${p12meta.value?.macAlgorithm ?? 'MAC'})`\n return '完整性校验通过 —— 密码正确,keystore 未被篡改'\n }\n // PKCS#12 unlocked but MAC absent/unsupported: still a successful decrypt.\n if (isPkcs12.value && unlocked.value) return '已解密(该文件无 MAC 或摘要不支持,密码经解密验证)'\n return ''\n})\nconst integrityOk = computed(() => integrity.value === 'ok' || (isPkcs12.value && unlocked.value))\n\nwatch(() => [props.url, props.name], load, { immediate: true })\nonBeforeUnmount(() => {\n // nothing to revoke; bytes are not retained as object URLs\n})\n</script>\n\n<template>\n <div class=\"jks\">\n <p v-if=\"status === 'loading'\" class=\"jks__msg\">正在解析 keystore…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"jks__msg jks__gate\">\n <p>该文件较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"jks__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"jks__msg jks__msg--error\">\n <p>无法解析:{{ errorMsg }}</p>\n <button type=\"button\" class=\"jks__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else-if=\"format\" class=\"jks__scroll\">\n <!-- store header + password -->\n <section class=\"jks__store\">\n <header class=\"jks__storehead\">\n <span class=\"jks__badge jks__badge--type\">{{ storeTypeLabel }}</span>\n <span class=\"jks__storetitle\">{{ isPkcs12 ? 'PKCS#12 密钥库' : 'Java KeyStore' }}</span>\n <span class=\"jks__storemeta\">{{ storeMeta }}</span>\n </header>\n\n <div class=\"jks__pw\" :class=\"{ 'jks__pw--ok': integrityOk }\">\n <Icon name=\"key\" :size=\"14\" />\n <input\n v-model=\"password\"\n type=\"password\"\n class=\"jks__pwinput\"\n :placeholder=\"pwPlaceholder\"\n autocomplete=\"off\"\n @keyup.enter=\"submitPassword\"\n />\n <button type=\"button\" class=\"jks__pwbtn\" @click=\"submitPassword\">\n {{ isPkcs12 && unlocked ? '重新解锁' : '解锁' }}\n </button>\n </div>\n\n <p\n v-if=\"pwTried && integrityText\"\n class=\"jks__integrity\"\n :class=\"`jks__integrity--${integrity}`\"\n >\n <Icon :name=\"integrityOk ? 'check' : 'close'\" :size=\"13\" />\n <span>{{ integrityText }}</span>\n </p>\n\n <p class=\"jks__hint\">全部解析与解密均在浏览器本地完成,密码与密钥不会上传。</p>\n <p v-if=\"jksStore?.truncatedNote\" class=\"jks__warn\">{{ jksStore.truncatedNote }}</p>\n <p v-if=\"p12error\" class=\"jks__warn\">{{ p12error }}</p>\n <p v-for=\"(w, i) in p12meta?.warnings ?? []\" :key=\"i\" class=\"jks__warn\">{{ w }}</p>\n </section>\n\n <!-- PKCS#12 pre-unlock: nothing is visible until decrypted -->\n <div v-if=\"isPkcs12 && !unlocked\" class=\"jks__locked\">\n <Icon name=\"key\" :size=\"26\" class=\"jks__lockicon\" />\n <p>PKCS#12 密钥库的证书与私钥均已加密,输入上方密码后在此显示内容。</p>\n </div>\n\n <!-- entries -->\n <section v-for=\"ev in entryViews\" :key=\"ev.index\" class=\"jks__entry\">\n <header class=\"jks__entryhead\">\n <span class=\"jks__badge\" :class=\"ENTRY_CLASS[ev.entry.type]\">{{\n ENTRY_LABEL[ev.entry.type]\n }}</span>\n <span class=\"jks__alias\" :title=\"ev.entry.alias\">{{ ev.entry.alias }}</span>\n <span v-if=\"ev.entry.createdMs\" class=\"jks__date\">{{\n formatDate(ev.entry.createdMs)\n }}</span>\n </header>\n\n <!-- private key material -->\n <div v-if=\"ev.entry.type === 'privateKey'\" class=\"jks__key\">\n <template v-if=\"keyState(ev.index).kind === 'locked'\">\n <Icon name=\"key\" :size=\"13\" class=\"jks__keyicon\" />\n <span class=\"jks__keytext\">私钥已加密 —— 输入密码后在此显示密钥算法</span>\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'ok'\">\n <Icon name=\"check\" :size=\"13\" class=\"jks__keyicon jks__keyicon--ok\" />\n <span class=\"jks__keytext jks__keytext--ok\"\n >私钥已解密 · {{ (keyState(ev.index) as { text: string }).text }}</span\n >\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'wrong'\">\n <Icon name=\"close\" :size=\"13\" class=\"jks__keyicon jks__keyicon--bad\" />\n <span class=\"jks__keytext jks__keytext--bad\">密码错误 —— 无法解密该私钥</span>\n </template>\n <template v-else-if=\"keyState(ev.index).kind === 'unsupported'\">\n <Icon name=\"key\" :size=\"13\" class=\"jks__keyicon\" />\n <span class=\"jks__keytext\"\n >该私钥加密方式暂不支持浏览器内解密({{\n (keyState(ev.index) as { detail?: string }).detail\n }})—— 证书链仍可查看</span\n >\n </template>\n <template v-else>\n <Icon name=\"close\" :size=\"13\" class=\"jks__keyicon jks__keyicon--bad\" />\n <span class=\"jks__keytext jks__keytext--bad\">私钥数据无法解析</span>\n </template>\n </div>\n\n <p v-else-if=\"ev.entry.type === 'secretKey'\" class=\"jks__note\">\n JCEKS 对称密钥条目(SecretKey)—— 以 Java 序列化对象存储,暂不解析。\n </p>\n\n <!-- certificate chain -->\n <div v-if=\"ev.certs.length\" class=\"jks__chain\">\n <div v-if=\"ev.entry.type === 'privateKey'\" class=\"jks__chainlabel\">\n 证书链({{ ev.certs.length }})\n </div>\n <article v-for=\"cv in ev.certs\" :key=\"cv.id\" class=\"jks__cert\">\n <div class=\"jks__certhead\">\n <span class=\"jks__certcn\">{{ cv.summary.subjectCN || cv.summary.label }}</span>\n <span v-if=\"expiry(cv.summary)\" class=\"jks__cbadge\" :class=\"expiry(cv.summary)?.cls\">\n {{ expiry(cv.summary)?.text }}\n </span>\n </div>\n <p v-if=\"cv.summary.note && !cv.summary.ok\" class=\"jks__note\">{{ cv.summary.note }}</p>\n <dl class=\"jks__dl\">\n <template v-if=\"cv.summary.subject.length\">\n <dt>主题</dt>\n <dd>{{ formatDn(cv.summary.subject) }}</dd>\n </template>\n <template v-if=\"cv.summary.issuer.length\">\n <dt>颁发者</dt>\n <dd>{{ formatDn(cv.summary.issuer) }}</dd>\n </template>\n <template v-if=\"cv.summary.notBefore || cv.summary.notAfter\">\n <dt>有效期</dt>\n <dd>{{ cv.summary.notBefore ?? '?' }} → {{ cv.summary.notAfter ?? '?' }}</dd>\n </template>\n <template v-if=\"publicKeyText(cv.summary)\">\n <dt>公钥</dt>\n <dd>{{ publicKeyText(cv.summary) }}</dd>\n </template>\n <template v-if=\"cv.summary.sigAlg\">\n <dt>签名算法</dt>\n <dd>{{ cv.summary.sigAlg }}</dd>\n </template>\n <template v-if=\"cv.summary.serialHex\">\n <dt>序列号</dt>\n <dd class=\"jks__mono\">{{ cv.summary.serialHex }}</dd>\n </template>\n <template v-if=\"cv.summary.sans.length\">\n <dt>备用名 (SAN)</dt>\n <dd>{{ cv.summary.sans.join(',') }}</dd>\n </template>\n <template v-if=\"fingerprints[cv.id]\">\n <dt>SHA-256</dt>\n <dd class=\"jks__mono jks__fp\">{{ fingerprints[cv.id] }}</dd>\n </template>\n </dl>\n </article>\n </div>\n </section>\n </div>\n </div>\n</template>\n\n<style scoped>\n.jks {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.jks__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.jks__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.jks__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.jks__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.jks__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.jks__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 14px 14px 60px;\n}\n\n/* Store header + password ---------------------------------------------------------------------- */\n.jks__store {\n margin-bottom: 16px;\n border: 1px solid var(--border);\n border-radius: 10px;\n background: var(--surface);\n overflow: hidden;\n}\n.jks__storehead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n background: var(--surface-2);\n border-bottom: 1px solid var(--border);\n}\n.jks__storetitle {\n flex-shrink: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__storemeta {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n text-align: right;\n font-size: 11.5px;\n color: var(--faint);\n}\n\n.jks__pw {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 12px;\n color: var(--muted);\n}\n.jks__pw--ok {\n color: var(--accent);\n}\n.jks__pwinput {\n flex: 1 1 auto;\n min-width: 0;\n height: 28px;\n padding: 0 9px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.jks__pwinput:focus {\n outline: none;\n border-color: var(--accent);\n}\n.jks__pwbtn {\n flex-shrink: 0;\n height: 28px;\n padding: 0 14px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n}\n.jks__pwbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.jks__integrity {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n padding: 0 12px 8px;\n font-size: 12px;\n}\n.jks__integrity--ok {\n color: #15803d;\n}\n.jks__integrity--bad {\n color: #b91c1c;\n}\n.jks__integrity--unknown {\n color: var(--accent);\n}\n.jks__hint {\n margin: 0;\n padding: 0 12px 10px;\n font-size: 11.5px;\n line-height: 1.5;\n color: var(--faint);\n}\n.jks__warn {\n margin: 0;\n padding: 9px 12px;\n font-size: 12px;\n line-height: 1.5;\n color: #b45309;\n background: color-mix(in srgb, #f59e0b 12%, transparent);\n border-top: 1px solid var(--border);\n}\n\n.jks__locked {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n padding: 40px 24px;\n text-align: center;\n color: var(--muted);\n font-size: 12.5px;\n}\n.jks__lockicon {\n color: var(--faint);\n}\n\n/* Badges ------------------------------------------------------------------------------------- */\n.jks__badge {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n border-radius: 5px;\n padding: 2px 7px;\n}\n.jks__badge--type {\n color: var(--accent);\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n}\n.jks__badge--key {\n color: #b45309;\n background: color-mix(in srgb, #f59e0b 16%, transparent);\n}\n.jks__badge--cert {\n color: #15803d;\n background: color-mix(in srgb, #22c55e 16%, transparent);\n}\n.jks__badge--secret {\n color: var(--muted);\n background: var(--surface-2);\n}\n\n/* Entry cards -------------------------------------------------------------------------------- */\n.jks__entry {\n margin-bottom: 14px;\n border: 1px solid var(--border);\n border-radius: 10px;\n background: var(--surface);\n overflow: hidden;\n}\n.jks__entryhead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n border-bottom: 1px solid var(--border);\n}\n.jks__alias {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__date {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n.jks__key {\n display: flex;\n align-items: center;\n gap: 7px;\n padding: 9px 12px;\n font-size: 12px;\n background: color-mix(in srgb, var(--accent) 4%, transparent);\n border-bottom: 1px solid var(--border);\n}\n.jks__keyicon {\n flex-shrink: 0;\n color: var(--faint);\n}\n.jks__keyicon--ok {\n color: #15803d;\n}\n.jks__keyicon--bad {\n color: #b91c1c;\n}\n.jks__keytext {\n min-width: 0;\n color: var(--muted);\n}\n.jks__keytext--ok {\n color: #15803d;\n font-weight: 500;\n}\n.jks__keytext--bad {\n color: #b91c1c;\n}\n\n.jks__note {\n margin: 0;\n padding: 9px 12px;\n font-size: 12px;\n line-height: 1.5;\n color: var(--muted);\n}\n\n/* Certificate chain -------------------------------------------------------------------------- */\n.jks__chain {\n padding: 10px 12px 12px;\n}\n.jks__chainlabel {\n margin-bottom: 8px;\n font-size: 11.5px;\n color: var(--faint);\n}\n.jks__cert {\n margin-bottom: 10px;\n border: 1px solid var(--border);\n border-radius: 8px;\n overflow: hidden;\n}\n.jks__cert:last-child {\n margin-bottom: 0;\n}\n.jks__certhead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 10px;\n background: var(--surface-2);\n border-bottom: 1px solid var(--border);\n}\n.jks__certcn {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12.5px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.jks__cbadge {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n border-radius: 5px;\n padding: 2px 8px;\n}\n.jks__cbadge--ok {\n color: #15803d;\n background: color-mix(in srgb, #22c55e 16%, transparent);\n}\n.jks__cbadge--warn {\n color: #b45309;\n background: color-mix(in srgb, #f59e0b 18%, transparent);\n}\n.jks__cbadge--bad {\n color: #b91c1c;\n background: color-mix(in srgb, #ef4444 16%, transparent);\n}\n.jks__dl {\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 6px 14px;\n margin: 0;\n padding: 10px;\n}\n.jks__dl dt {\n font-size: 12px;\n color: var(--faint);\n white-space: nowrap;\n}\n.jks__dl dd {\n margin: 0;\n font-size: 12.5px;\n color: var(--text);\n word-break: break-word;\n}\n.jks__mono {\n font-family: var(--font-mono);\n font-size: 11.5px;\n}\n.jks__fp {\n word-break: break-all;\n}\n</style>\n"],"mappings":"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"}
@@ -5,3 +5,5 @@ import t from "./JsonlViewer.vue_vue_type_script_setup_true_lang.js";
5
5
  var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-326ae959"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
+
9
+ //# sourceMappingURL=JsonlViewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JsonlViewer.js","names":[],"sources":["../../src/components/JsonlViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { parseJsonl, type JsonlRecord } from '@/lib/jsonl'\nimport { highlightCode } from '@/lib/highlight'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer JSON Lines (`.jsonl` / `.ndjson`) renderer (render mode). Receives the fetched text (the panel\n * loads it, like CsvViewer / SubtitleViewer), parses it into per-line records (see lib/jsonl.ts), and shows a\n * compact LIST — one truncated single-line preview per record. Clicking a row expands it to the pretty-printed,\n * syntax-highlighted value. Big files stay responsive two ways: each row's preview is a short single line, and\n * only an initial window of rows is in the DOM (a \"load more\" button grows it). The source toggle (header) shows\n * the raw file highlighted as JSON.\n */\nconst props = defineProps<{ text: string; name: string }>()\n\nconst PREVIEW_CHARS = 200 // truncate each row's inline preview so even a huge record is one short line\nconst INITIAL_ROWS = 200 // rows mounted initially; \"load more\" grows the window\nconst ROW_STEP = 400\n\nconst doc = computed(() => parseJsonl(props.text))\nconst shown = ref(INITIAL_ROWS)\nconst expanded = ref<Set<number>>(new Set())\n\n// A new file resets the window + any open rows.\nwatch(\n () => props.text,\n () => {\n shown.value = INITIAL_ROWS\n expanded.value = new Set()\n },\n)\n\nconst visible = computed(() => doc.value.records.slice(0, shown.value))\nconst remaining = computed(() => Math.max(0, doc.value.records.length - shown.value))\n\nfunction loadMore(): void {\n shown.value += ROW_STEP\n}\n\nfunction toggle(line: number): void {\n const s = new Set(expanded.value)\n if (s.has(line)) s.delete(line)\n else s.add(line)\n expanded.value = s\n}\n\n// Per-row copy: write the record's RAW line (the faithful on-disk text, not the re-stringified value) to the\n// clipboard, and flash a ✓ on that row. Mirrors the CodeBlock copy pattern (best-effort, cleared on a timer).\nconst copiedLine = ref<number | null>(null)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyRow(r: JsonlRecord): Promise<void> {\n try {\n await navigator.clipboard?.writeText(r.raw)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copiedLine.value = r.line\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copiedLine.value = null), 1400)\n}\nonBeforeUnmount(() => clearTimeout(copyTimer))\n\n// Compact one-line preview: stringify the parsed value (whitespace collapsed) or, for an error row, the raw\n// line — then hard-truncate. Building it from the value normalizes spacing so the list reads cleanly.\nfunction preview(r: JsonlRecord): string {\n let s: string\n if (r.error) s = r.raw\n else {\n try {\n s = JSON.stringify(r.value) ?? String(r.value)\n } catch {\n s = r.raw\n }\n }\n s = s.replace(/\\s+/g, ' ').trim()\n return s.length > PREVIEW_CHARS ? s.slice(0, PREVIEW_CHARS) + '…' : s\n}\n\n// Pretty value, highlighted as JSON — built lazily, only for an expanded row.\nfunction expandedHtml(r: JsonlRecord): string {\n if (r.error) return highlightCode(r.raw, 'json')\n try {\n return highlightCode(JSON.stringify(r.value, null, 2) ?? '', 'json')\n } catch {\n return highlightCode(r.raw, 'json')\n }\n}\n</script>\n\n<template>\n <div class=\"jl\">\n <div class=\"jl__summary\">\n <span class=\"jl__count\">{{ doc.records.length }} 条记录</span>\n <span v-if=\"doc.errorCount\" class=\"jl__errtag\">{{ doc.errorCount }} 行无法解析</span>\n </div>\n\n <div class=\"jl__list\">\n <p v-if=\"!doc.records.length\" class=\"jl__msg\">(空文件,无 JSON 记录)</p>\n\n <div v-for=\"r in visible\" :key=\"r.line\" class=\"jl__rowwrap\">\n <div\n class=\"jl__rowhead\"\n :class=\"{ 'jl__rowhead--open': expanded.has(r.line), 'jl__rowhead--err': r.error }\"\n >\n <button\n type=\"button\"\n class=\"jl__row\"\n :title=\"r.error ? `第 ${r.line} 行:JSON 无效` : `第 ${r.line} 行 —— 点击展开`\"\n @click=\"toggle(r.line)\"\n >\n <span class=\"jl__caret\" :class=\"{ 'jl__caret--open': expanded.has(r.line) }\">▶</span>\n <span class=\"jl__ln\">{{ r.line }}</span>\n <span v-if=\"r.error\" class=\"jl__badge\">无效</span>\n <span class=\"jl__preview\">{{ preview(r) }}</span>\n </button>\n <button\n type=\"button\"\n class=\"jl__copy\"\n :class=\"{ 'jl__copy--done': copiedLine === r.line }\"\n :title=\"copiedLine === r.line ? '已复制' : '复制此行'\"\n @click=\"copyRow(r)\"\n >\n <Icon :name=\"copiedLine === r.line ? 'check' : 'copy'\" :size=\"13\" />\n </button>\n </div>\n <pre\n v-if=\"expanded.has(r.line)\"\n class=\"jl__full\"\n ><code class=\"hljs\" v-html=\"expandedHtml(r)\" /></pre>\n </div>\n\n <button v-if=\"remaining\" type=\"button\" class=\"jl__more\" @click=\"loadMore\">\n 显示更多(还有 {{ remaining }} 条)\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.jl {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.jl__summary {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 6px 12px;\n flex-wrap: wrap;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n.jl__count {\n font-weight: 600;\n color: var(--accent);\n}\n.jl__errtag {\n padding: 1px 7px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--danger) 14%, var(--surface));\n color: var(--danger);\n font-size: 10.5px;\n}\n\n.jl__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n\n.jl__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: scroll; /* always reserve the gutter so the draggable bar is present, not wheel-only */\n overscroll-behavior: contain;\n padding: 4px 0;\n /* Always-visible, draggable scrollbar — the record list gets long and an auto-hidden overlay bar reads\n * as \"no scrollbar\". Thin styled bar in Firefox; a classic (gutter-reserving) bar in Chromium/WebKit. */\n scrollbar-width: thin;\n scrollbar-color: var(--border-2) transparent;\n}\n.jl__list::-webkit-scrollbar {\n width: 12px;\n}\n.jl__list::-webkit-scrollbar-thumb {\n background: var(--border-2);\n background-clip: padding-box;\n border: 3px solid transparent;\n border-radius: 999px;\n}\n.jl__list::-webkit-scrollbar-thumb:hover {\n background: var(--muted);\n background-clip: padding-box;\n border: 3px solid transparent;\n}\n.jl__rowwrap {\n border-bottom: 1px solid var(--border);\n}\n\n/* Row header = the clickable preview button + a trailing copy button (a button can't nest inside a button,\n * so they're siblings in this flex row; the hover/open/error tint lives here, spanning both). */\n.jl__rowhead {\n display: flex;\n align-items: stretch;\n}\n.jl__rowhead:hover,\n.jl__rowhead--open {\n background: var(--surface-2);\n}\n\n.jl__row {\n display: flex;\n align-items: baseline;\n gap: 9px;\n flex: 1 1 auto;\n min-width: 0;\n padding: 6px 14px;\n background: transparent;\n border: none;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n/* Trailing per-row copy button: faint by default, brightening on hover; accent flash when just copied. */\n.jl__copy {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n background: transparent;\n border: none;\n color: var(--faint);\n cursor: pointer;\n transition: color 0.15s;\n}\n.jl__copy:hover {\n color: var(--accent);\n}\n.jl__copy--done {\n color: var(--accent);\n}\n.jl__caret {\n flex-shrink: 0;\n width: 9px;\n font-size: 9px;\n color: var(--faint);\n transition: transform 0.12s;\n transform: rotate(0deg);\n align-self: center;\n}\n.jl__caret--open {\n transform: rotate(90deg);\n}\n.jl__ln {\n flex-shrink: 0;\n min-width: 30px;\n text-align: right;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n user-select: none;\n}\n.jl__badge {\n flex-shrink: 0;\n padding: 0 6px;\n border-radius: 5px;\n background: color-mix(in srgb, var(--danger) 16%, var(--surface));\n color: var(--danger);\n font-size: 9.5px;\n font-weight: 700;\n letter-spacing: 0.04em;\n align-self: center;\n}\n.jl__preview {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-family: var(--font-mono);\n font-size: 12.5px;\n color: var(--text);\n}\n.jl__rowhead--err .jl__preview {\n color: var(--muted);\n}\n\n.jl__full {\n margin: 0;\n padding: 8px 14px 12px 32px;\n overflow-x: auto;\n background: var(--code-bg);\n font-family: var(--font-mono);\n font-size: 12px;\n line-height: 1.55;\n white-space: pre;\n tab-size: 2;\n}\n.jl__full code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n\n.jl__more {\n display: block;\n width: 100%;\n padding: 10px 16px;\n font-family: inherit;\n font-size: 12px;\n color: var(--accent);\n background: transparent;\n border: none;\n cursor: pointer;\n}\n.jl__more:hover {\n background: var(--surface-2);\n}\n</style>\n"],"mappings":""}
@@ -101,3 +101,5 @@ var _ = { class: "jl" }, v = { class: "jl__summary" }, y = { class: "jl__count"
101
101
  });
102
102
  //#endregion
103
103
  export { N as default };
104
+
105
+ //# sourceMappingURL=JsonlViewer.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JsonlViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/JsonlViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { parseJsonl, type JsonlRecord } from '@/lib/jsonl'\nimport { highlightCode } from '@/lib/highlight'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer JSON Lines (`.jsonl` / `.ndjson`) renderer (render mode). Receives the fetched text (the panel\n * loads it, like CsvViewer / SubtitleViewer), parses it into per-line records (see lib/jsonl.ts), and shows a\n * compact LIST — one truncated single-line preview per record. Clicking a row expands it to the pretty-printed,\n * syntax-highlighted value. Big files stay responsive two ways: each row's preview is a short single line, and\n * only an initial window of rows is in the DOM (a \"load more\" button grows it). The source toggle (header) shows\n * the raw file highlighted as JSON.\n */\nconst props = defineProps<{ text: string; name: string }>()\n\nconst PREVIEW_CHARS = 200 // truncate each row's inline preview so even a huge record is one short line\nconst INITIAL_ROWS = 200 // rows mounted initially; \"load more\" grows the window\nconst ROW_STEP = 400\n\nconst doc = computed(() => parseJsonl(props.text))\nconst shown = ref(INITIAL_ROWS)\nconst expanded = ref<Set<number>>(new Set())\n\n// A new file resets the window + any open rows.\nwatch(\n () => props.text,\n () => {\n shown.value = INITIAL_ROWS\n expanded.value = new Set()\n },\n)\n\nconst visible = computed(() => doc.value.records.slice(0, shown.value))\nconst remaining = computed(() => Math.max(0, doc.value.records.length - shown.value))\n\nfunction loadMore(): void {\n shown.value += ROW_STEP\n}\n\nfunction toggle(line: number): void {\n const s = new Set(expanded.value)\n if (s.has(line)) s.delete(line)\n else s.add(line)\n expanded.value = s\n}\n\n// Per-row copy: write the record's RAW line (the faithful on-disk text, not the re-stringified value) to the\n// clipboard, and flash a ✓ on that row. Mirrors the CodeBlock copy pattern (best-effort, cleared on a timer).\nconst copiedLine = ref<number | null>(null)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyRow(r: JsonlRecord): Promise<void> {\n try {\n await navigator.clipboard?.writeText(r.raw)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copiedLine.value = r.line\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copiedLine.value = null), 1400)\n}\nonBeforeUnmount(() => clearTimeout(copyTimer))\n\n// Compact one-line preview: stringify the parsed value (whitespace collapsed) or, for an error row, the raw\n// line — then hard-truncate. Building it from the value normalizes spacing so the list reads cleanly.\nfunction preview(r: JsonlRecord): string {\n let s: string\n if (r.error) s = r.raw\n else {\n try {\n s = JSON.stringify(r.value) ?? String(r.value)\n } catch {\n s = r.raw\n }\n }\n s = s.replace(/\\s+/g, ' ').trim()\n return s.length > PREVIEW_CHARS ? s.slice(0, PREVIEW_CHARS) + '…' : s\n}\n\n// Pretty value, highlighted as JSON — built lazily, only for an expanded row.\nfunction expandedHtml(r: JsonlRecord): string {\n if (r.error) return highlightCode(r.raw, 'json')\n try {\n return highlightCode(JSON.stringify(r.value, null, 2) ?? '', 'json')\n } catch {\n return highlightCode(r.raw, 'json')\n }\n}\n</script>\n\n<template>\n <div class=\"jl\">\n <div class=\"jl__summary\">\n <span class=\"jl__count\">{{ doc.records.length }} 条记录</span>\n <span v-if=\"doc.errorCount\" class=\"jl__errtag\">{{ doc.errorCount }} 行无法解析</span>\n </div>\n\n <div class=\"jl__list\">\n <p v-if=\"!doc.records.length\" class=\"jl__msg\">(空文件,无 JSON 记录)</p>\n\n <div v-for=\"r in visible\" :key=\"r.line\" class=\"jl__rowwrap\">\n <div\n class=\"jl__rowhead\"\n :class=\"{ 'jl__rowhead--open': expanded.has(r.line), 'jl__rowhead--err': r.error }\"\n >\n <button\n type=\"button\"\n class=\"jl__row\"\n :title=\"r.error ? `第 ${r.line} 行:JSON 无效` : `第 ${r.line} 行 —— 点击展开`\"\n @click=\"toggle(r.line)\"\n >\n <span class=\"jl__caret\" :class=\"{ 'jl__caret--open': expanded.has(r.line) }\">▶</span>\n <span class=\"jl__ln\">{{ r.line }}</span>\n <span v-if=\"r.error\" class=\"jl__badge\">无效</span>\n <span class=\"jl__preview\">{{ preview(r) }}</span>\n </button>\n <button\n type=\"button\"\n class=\"jl__copy\"\n :class=\"{ 'jl__copy--done': copiedLine === r.line }\"\n :title=\"copiedLine === r.line ? '已复制' : '复制此行'\"\n @click=\"copyRow(r)\"\n >\n <Icon :name=\"copiedLine === r.line ? 'check' : 'copy'\" :size=\"13\" />\n </button>\n </div>\n <pre\n v-if=\"expanded.has(r.line)\"\n class=\"jl__full\"\n ><code class=\"hljs\" v-html=\"expandedHtml(r)\" /></pre>\n </div>\n\n <button v-if=\"remaining\" type=\"button\" class=\"jl__more\" @click=\"loadMore\">\n 显示更多(还有 {{ remaining }} 条)\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.jl {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.jl__summary {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 6px 12px;\n flex-wrap: wrap;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n.jl__count {\n font-weight: 600;\n color: var(--accent);\n}\n.jl__errtag {\n padding: 1px 7px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--danger) 14%, var(--surface));\n color: var(--danger);\n font-size: 10.5px;\n}\n\n.jl__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n\n.jl__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: scroll; /* always reserve the gutter so the draggable bar is present, not wheel-only */\n overscroll-behavior: contain;\n padding: 4px 0;\n /* Always-visible, draggable scrollbar — the record list gets long and an auto-hidden overlay bar reads\n * as \"no scrollbar\". Thin styled bar in Firefox; a classic (gutter-reserving) bar in Chromium/WebKit. */\n scrollbar-width: thin;\n scrollbar-color: var(--border-2) transparent;\n}\n.jl__list::-webkit-scrollbar {\n width: 12px;\n}\n.jl__list::-webkit-scrollbar-thumb {\n background: var(--border-2);\n background-clip: padding-box;\n border: 3px solid transparent;\n border-radius: 999px;\n}\n.jl__list::-webkit-scrollbar-thumb:hover {\n background: var(--muted);\n background-clip: padding-box;\n border: 3px solid transparent;\n}\n.jl__rowwrap {\n border-bottom: 1px solid var(--border);\n}\n\n/* Row header = the clickable preview button + a trailing copy button (a button can't nest inside a button,\n * so they're siblings in this flex row; the hover/open/error tint lives here, spanning both). */\n.jl__rowhead {\n display: flex;\n align-items: stretch;\n}\n.jl__rowhead:hover,\n.jl__rowhead--open {\n background: var(--surface-2);\n}\n\n.jl__row {\n display: flex;\n align-items: baseline;\n gap: 9px;\n flex: 1 1 auto;\n min-width: 0;\n padding: 6px 14px;\n background: transparent;\n border: none;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n/* Trailing per-row copy button: faint by default, brightening on hover; accent flash when just copied. */\n.jl__copy {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n background: transparent;\n border: none;\n color: var(--faint);\n cursor: pointer;\n transition: color 0.15s;\n}\n.jl__copy:hover {\n color: var(--accent);\n}\n.jl__copy--done {\n color: var(--accent);\n}\n.jl__caret {\n flex-shrink: 0;\n width: 9px;\n font-size: 9px;\n color: var(--faint);\n transition: transform 0.12s;\n transform: rotate(0deg);\n align-self: center;\n}\n.jl__caret--open {\n transform: rotate(90deg);\n}\n.jl__ln {\n flex-shrink: 0;\n min-width: 30px;\n text-align: right;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n user-select: none;\n}\n.jl__badge {\n flex-shrink: 0;\n padding: 0 6px;\n border-radius: 5px;\n background: color-mix(in srgb, var(--danger) 16%, var(--surface));\n color: var(--danger);\n font-size: 9.5px;\n font-weight: 700;\n letter-spacing: 0.04em;\n align-self: center;\n}\n.jl__preview {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-family: var(--font-mono);\n font-size: 12.5px;\n color: var(--text);\n}\n.jl__rowhead--err .jl__preview {\n color: var(--muted);\n}\n\n.jl__full {\n margin: 0;\n padding: 8px 14px 12px 32px;\n overflow-x: auto;\n background: var(--code-bg);\n font-family: var(--font-mono);\n font-size: 12px;\n line-height: 1.55;\n white-space: pre;\n tab-size: 2;\n}\n.jl__full code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n\n.jl__more {\n display: block;\n width: 100%;\n padding: 10px 16px;\n font-family: inherit;\n font-size: 12px;\n color: var(--accent);\n background: transparent;\n border: none;\n cursor: pointer;\n}\n.jl__more:hover {\n background: var(--surface-2);\n}\n</style>\n"],"mappings":"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"}
@@ -5,3 +5,5 @@ import t from "./LogViewer.vue_vue_type_script_setup_true_lang.js";
5
5
  var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-b19d397d"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
+
9
+ //# sourceMappingURL=LogViewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LogViewer.js","names":[],"sources":["../../src/components/LogViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\nimport { useLogRules, type ColorRule } from '@/composables/useLogRules'\n\n/**\n * Side-viewer for large text / log files. Fetches the bytes, splits into lines, and renders them with a\n * fixed-height VIRTUAL list — only the visible window (plus a little overscan) is ever in the DOM, so a\n * multi-hundred-MB log scrolls smoothly instead of locking the page (the plain source view would mount one\n * huge highlighted block). No syntax highlight (logs don't need it, and tokenizing 500k lines is infeasible).\n *\n * Routed here for any `.log`, and for any large text file (see FileChip's size gate) so a 50MB `.txt`/`.csv`\n * doesn't freeze the normal viewer. Tools: a sticky line-number gutter, substring search (highlight, match\n * count, prev/next, optional filter-to-matches), go-to-line, plus three log-specific aids:\n *\n * 1. Call-stack folding — error logs from common languages (Java/Kotlin/Scala, JS/Node, Python, C#/.NET,\n * Ruby) are one header line (\"Exception in thread…\", \"Traceback…\", \"Caused by:\") followed by a run of\n * indented frame lines (\" at …\", ' File \"…\"', …). We detect those runs, hang a chevron off the header,\n * and collapse the frames behind a \"N 帧\" badge (collapsed by DEFAULT). A live search force-expands any\n * stack with a hit; filter mode pulls a matched frame's WHOLE stack in (see {@link filtered}).\n * 2. Context expansion (filter mode) — between filtered chunks sits a \"⋯ N 行\" expander bar; click it to\n * progressively reveal hidden lines around a match (a chunk from the top, from the bottom, or all), the\n * way JetBrains' diff reveals collapsed unchanged regions. Revealed lines are tracked in {@link revealed}.\n * 3. Color rules — user-defined substring/regex rules paint matching lines' background/text (see\n * {@link useLogRules}); applied to visible rows only via {@link styleFor}.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst lines = ref<string[]>([])\n\n// Reading the whole file into memory is unavoidable here (the endpoint has no range support); gate the\n// truly huge ones behind a confirm.\nconst SIZE_LIMIT = 64 * 1024 * 1024\nconst LINE_H = 19 // px — MUST match the .log__row CSS height\nconst OVERSCAN = 12\nconst STEP = 10 // lines revealed per directional context-expand click\n\nconst scrollEl = ref<HTMLElement | null>(null)\nconst scrollTop = ref(0)\nconst viewportH = ref(400)\nlet ro: ResizeObserver | null = null\n\n// ---- stack-trace detection ----------------------------------------------------------------------\n// A stack group spans original lines [header .. frameEnd]; `header` is always `frameStart - 1` (the error /\n// \"Caused by:\" / \"Traceback:\" line just above the run), and the collapsible frames are [frameStart .. frameEnd].\ntype Group = { header: number; frameStart: number; frameEnd: number }\n\n// Only fold a run with at least this many frame lines (folding a lone frame saves nothing).\nconst MIN_FRAMES = 2\n\n// Indented stack-frame lines across common languages, matched on the raw line:\n// RE_AT Java/Kotlin/Scala/JS/Node/C#/.NET/PHP \" at pkg.Class.method(File:12)\" / \"at fn (file.js:1:2)\"\n// (requires a '.' or '(' after the symbol so a prose line like \" at 3pm the job ran\" won't match)\n// RE_FILE Python ' File \"/x.py\", line 10, in fn'\n// RE_FROM Ruby \" from a.rb:5:in `m'\"\n// RE_MORE Java \" ... 12 more\"\n// RE_NET C#/.NET async boundary \" --- End of stack trace from previous location ---\"\nconst RE_AT = /^[ \\t]+at\\s+\\S+[.(]/\nconst RE_FILE = /^[ \\t]+File\\s[\"']/\nconst RE_FROM = /^[ \\t]+from\\s+\\S+:\\d+/\nconst RE_MORE = /^[ \\t]*\\.\\.\\.\\s+\\d+\\s+more\\b/\nconst RE_NET = /^[ \\t]+--- End of /\nfunction isFrame(line: string): boolean {\n return (\n RE_AT.test(line) ||\n RE_FILE.test(line) ||\n RE_FROM.test(line) ||\n RE_MORE.test(line) ||\n RE_NET.test(line)\n )\n}\n\n// Group consecutive frame runs. Python frames come in two lines (the `File \"…\"` line plus the indented source\n// line under it) — absorb that trailing source line into the run so it folds with its frame.\nconst groups = computed<Group[]>(() => {\n const src = lines.value\n const N = src.length\n const out: Group[] = []\n let i = 0\n while (i < N) {\n if (!isFrame(src[i])) {\n i++\n continue\n }\n const runStart = i\n let prevFile = RE_FILE.test(src[i])\n i++\n while (i < N) {\n const ln = src[i]\n if (isFrame(ln)) {\n prevFile = RE_FILE.test(ln)\n i++\n } else if (prevFile && /^[ \\t]+\\S/.test(ln)) {\n prevFile = false // Python source line under a `File \"…\"` frame\n i++\n } else break\n }\n const runEnd = i - 1\n // Prefer the (non-blank) plain line just above the run as the header; otherwise treat the run's first line\n // as the header and fold the rest.\n const above = runStart - 1\n const useAbove = above >= 0 && !isFrame(src[above]) && src[above].trim() !== ''\n const frameStart = useAbove ? runStart : runStart + 1\n if (runEnd - frameStart + 1 >= MIN_FRAMES) {\n out.push({ header: frameStart - 1, frameStart, frameEnd: runEnd })\n }\n }\n return out\n})\n\nconst hasGroups = computed(() => groups.value.length > 0)\n\n// origLine → groupId, for header lines only.\nconst headerMap = computed(() => {\n const m = new Map<number, number>()\n groups.value.forEach((g, i) => m.set(g.header, i))\n return m\n})\n\n// The group whose span [header .. frameEnd] contains `orig` (groups are position-sorted & disjoint), or -1.\nfunction groupOf(orig: number): number {\n const gs = groups.value\n let lo = 0\n let hi = gs.length - 1\n while (lo <= hi) {\n const mid = (lo + hi) >> 1\n const g = gs[mid]\n if (orig < g.header) hi = mid - 1\n else if (orig > g.frameEnd) lo = mid + 1\n else return mid\n }\n return -1\n}\n\n// User-expanded group ids (default = absent = collapsed). Reset on each load.\nconst expanded = ref<Set<number>>(new Set())\nconst allExpanded = computed(() => hasGroups.value && expanded.value.size === groups.value.length)\n\nfunction toggleGroup(id: number): void {\n const s = new Set(expanded.value)\n if (s.has(id)) s.delete(id)\n else s.add(id)\n expanded.value = s\n}\nfunction toggleAll(): void {\n expanded.value = allExpanded.value ? new Set() : new Set(groups.value.map((_, i) => i))\n}\n\n// Is this group folded right now? Collapsed unless the user expanded it — OR a live query has a hit inside it\n// (force-expand so the match can't hide behind the fold).\nfunction isCollapsed(id: number): boolean {\n if (expanded.value.has(id)) return false\n if (debounced.value && includedGroups.value.has(id)) return false\n return true\n}\n\n// ---- color rules --------------------------------------------------------------------------------\n\nconst { rules, add: addRule, remove: removeRule, move: moveRule } = useLogRules()\nconst rulesOpen = ref(false)\n\n// Enabled, non-empty rules pre-compiled to a matcher (+ colors). Invalid regex / color-less rules are dropped.\nconst compiledRules = computed(() => {\n const out: { test: (s: string) => boolean; bg: string; fg: string }[] = []\n for (const r of rules.value) {\n if (!r.enabled || !r.pattern) continue\n if (!(r.bgOn || r.fgOn)) continue\n let test: ((s: string) => boolean) | null\n if (r.regex) {\n try {\n const re = new RegExp(r.pattern, r.caseSensitive ? '' : 'i')\n test = (s) => re.test(s)\n } catch {\n test = null // invalid regex → skip until the user fixes it\n }\n } else if (r.caseSensitive) {\n const n = r.pattern\n test = (s) => s.includes(n)\n } else {\n const n = r.pattern.toLowerCase()\n test = (s) => s.toLowerCase().includes(n)\n }\n if (test) out.push({ test, bg: r.bgOn ? r.bg : '', fg: r.fgOn ? r.fg : '' })\n }\n return out\n})\n\n// First matching rule's colors for a line, or null. Called per VISIBLE row only.\nfunction styleFor(text: string): { bg: string; fg: string } | null {\n for (const c of compiledRules.value) {\n if (c.test(text)) return { bg: c.bg, fg: c.fg }\n }\n return null\n}\n\n// A regex rule with a syntax error — used to flag the input red in the editor.\nfunction regexBad(r: ColorRule): boolean {\n if (!r.regex || !r.pattern) return false\n try {\n new RegExp(r.pattern)\n return false\n } catch {\n return true\n }\n}\n\n// ---- search -------------------------------------------------------------------------------------\n\nconst query = ref('')\nconst debounced = ref('')\nlet debounceTimer: ReturnType<typeof setTimeout> | undefined\nwatch(query, (q) => {\n clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n debounced.value = q\n currentMatch.value = 0\n revealed.value = new Set() // context is relative to the current filter → drop it when the query changes\n }, 160)\n})\n\n// Clear the search box (× button / Esc) — apply immediately instead of waiting on the debounce.\nfunction clearSearch(): void {\n query.value = ''\n clearTimeout(debounceTimer)\n debounced.value = ''\n currentMatch.value = 0\n revealed.value = new Set()\n}\n\nconst filterMode = ref(false)\nconst errorOnly = ref(false)\nconst currentMatch = ref(0)\n\n// Original line indices (0-based) that contain the query.\nconst matchIdx = computed(() => {\n const q = debounced.value.toLowerCase()\n if (!q) return [] as number[]\n const out: number[] = []\n const src = lines.value\n for (let i = 0; i < src.length; i++) {\n if (src[i].toLowerCase().includes(q)) out.push(i)\n }\n return out\n})\n\nfunction hitsQuery(text: string, q: string): boolean {\n return q ? text.toLowerCase().includes(q.toLowerCase()) : false\n}\n\n// Groups with at least one match (header or any frame). Drives both force-expand and filter-keeps-stack.\nconst includedGroups = computed(() => {\n const s = new Set<number>()\n if (!debounced.value) return s\n for (const m of matchIdx.value) {\n const g = groupOf(m)\n if (g >= 0) s.add(g)\n }\n return s\n})\n\n// The currently highlighted match's original line index (for the active-row tint), or -1.\nconst activeOrig = computed(() => {\n const m = matchIdx.value\n return m.length ? m[Math.min(currentMatch.value, m.length - 1)] : -1\n})\n\n// ---- \"only errors\" filter -----------------------------------------------------------------------\n// Error / exception lines for the 只看异常 quick filter. Case-sensitive on purpose: uppercase level tokens,\n// PascalCase *Exception / *Error class names, and a few explicit markers. Lowercase \"error\"/\"warn\" are NOT\n// matched (too noisy in ordinary messages) — a stack's header line is picked up via headerMap instead.\nconst ERROR_RE =\n /\\b(?:ERROR|FATAL|SEVERE|CRITICAL|PANIC|FAILED|FAILURE)\\b|\\b\\w*(?:Exception|Error)\\b|\\bTraceback\\b|\\bCaused by\\b|^\\s*panic:/\n\n// Error-line indices (only computed while the toggle is on). Stack headers always count as errors.\nconst errorIdx = computed<number[]>(() => {\n if (!errorOnly.value) return []\n const src = lines.value\n const hdr = headerMap.value\n const out: number[] = []\n for (let i = 0; i < src.length; i++) {\n if (hdr.has(i) || ERROR_RE.test(src[i])) out.push(i)\n }\n return out\n})\nconst errorSet = computed(() => new Set(errorIdx.value))\n\n// The search \"过滤\" toggle only bites when there's a query.\nconst searchFilterActive = computed(() => filterMode.value && debounced.value !== '')\n\n// The anchor lines the filtered view is built around (null ⇒ no filter → show the whole log via `runs`).\n// Search + errors compose as an intersection (\"errors that also match the query\"); either alone is its own set.\nconst anchors = computed<number[] | null>(() => {\n const s = searchFilterActive.value\n const e = errorOnly.value\n if (!s && !e) return null\n if (s && e) {\n const es = errorSet.value\n return matchIdx.value.filter((i) => es.has(i))\n }\n return e ? errorIdx.value : matchIdx.value\n})\n\n// Keep whole stacks around anchors only when the search filter contributes (its contract: 过滤保留整条堆栈).\n// The pure 只看异常 view stays compact — one line per error, the stack peekable via the context expander.\nconst keepStacks = computed(() => searchFilterActive.value)\n\n// ---- context expansion (filter mode) ------------------------------------------------------------\n// Original line indices the user has manually revealed as context around matches. Reset on load / query change.\nconst revealed = ref<Set<number>>(new Set())\n// Switching filters changes what the gaps mean → drop revealed context.\nwatch([errorOnly, filterMode], () => {\n revealed.value = new Set()\n})\n\n// Reveal a chunk of a gap between shown lines (top, bottom are the shown lines bracketing it; -1 / N at file\n// edges): 'down' from the top match, 'up' toward the bottom match, or 'all'.\nfunction reveal(top: number, bottom: number, dir: 'up' | 'down' | 'all'): void {\n const lo = top + 1\n const hi = bottom - 1\n if (hi < lo) return\n let a = lo\n let b = hi\n if (dir === 'down') b = Math.min(hi, lo + STEP - 1)\n else if (dir === 'up') a = Math.max(lo, hi - STEP + 1)\n const s = new Set(revealed.value)\n for (let i = a; i <= b; i++) s.add(i)\n revealed.value = s\n}\n\n// ---- display layout -----------------------------------------------------------------------------\n// Two modes drive the virtual list:\n// - Non-filter: every line shows, minus the frames of collapsed groups. Expressed as `runs` — the visible\n// original-index segments between the hidden frame ranges — so display↔original stays a cheap binary search\n// (no per-line array; big files stay light).\n// - Filter: only matches show, but every included group contributes its WHOLE span so stacks stay intact,\n// plus any manually-revealed context lines; the remaining gaps become expander rows. Materialized as an\n// item list (bounded by what's shown, normally small).\n\n// Non-filter: visible segments + total row count.\nconst runs = computed(() => {\n const gs = groups.value\n const N = lines.value.length\n const out: { o0: number; o1: number; d0: number }[] = []\n let disp = 0\n let cursor = 0\n for (let id = 0; id < gs.length; id++) {\n if (!isCollapsed(id)) continue\n const g = gs[id]\n if (g.frameStart > cursor) {\n // visible run up to and including the header (header === frameStart - 1)\n out.push({ o0: cursor, o1: g.frameStart - 1, d0: disp })\n disp += g.frameStart - cursor\n }\n cursor = g.frameEnd + 1 // skip the folded frames\n }\n if (cursor < N) {\n out.push({ o0: cursor, o1: N - 1, d0: disp })\n disp += N - cursor\n }\n return { runs: out, total: disp }\n})\n\n// Filter-mode items: `line` rows plus `gap` expander rows. `key` is a parallel, strictly-increasing lookup\n// array (lines → their orig, gaps → top + 0.5) so origToDisplay can binary-search a match's row.\ntype FItem = { t: 'line'; orig: number } | { t: 'gap'; top: number; bottom: number; size: number }\nconst filtered = computed<{ items: FItem[]; key: number[] } | null>(() => {\n const anc = anchors.value\n if (anc === null) return null\n const N = lines.value.length\n const gs = groups.value\n // Stacks kept whole around anchors — only when the search filter contributes (see keepStacks).\n const ranges: [number, number][] = []\n if (keepStacks.value) {\n const inc = new Set<number>()\n for (const a of anc) {\n const g = groupOf(a)\n if (g >= 0) inc.add(g)\n }\n for (const id of inc) ranges.push([gs[id].header, gs[id].frameEnd])\n ranges.sort((a, b) => a[0] - b[0])\n }\n const covered = (o: number): boolean => {\n let lo = 0\n let hi = ranges.length - 1\n while (lo <= hi) {\n const mid = (lo + hi) >> 1\n if (o < ranges[mid][0]) hi = mid - 1\n else if (o > ranges[mid][1]) lo = mid + 1\n else return true\n }\n return false\n }\n // Standalone shown lines = anchors + revealed context, minus whatever a kept-whole stack already covers.\n const extra: number[] = []\n for (const a of anc) if (!covered(a)) extra.push(a)\n for (const r of revealed.value) if (!covered(r)) extra.push(r)\n extra.sort((a, b) => a - b)\n // Merge (sorted) range indices with (sorted) standalone lines → sorted, de-duped base set.\n const base: number[] = []\n let ri = 0\n let si = 0\n let cur = ranges.length ? ranges[0][0] : Infinity\n let last = -1\n while (ri < ranges.length || si < extra.length) {\n const rn = ri < ranges.length ? cur : Infinity\n const sn = si < extra.length ? extra[si] : Infinity\n let v: number\n if (rn <= sn) {\n v = rn\n if (cur >= ranges[ri][1]) {\n ri++\n cur = ri < ranges.length ? ranges[ri][0] : Infinity\n } else cur++\n } else {\n v = sn\n si++\n }\n if (v !== last) {\n base.push(v)\n last = v\n }\n }\n // Weave in gap expanders between non-adjacent shown lines (and at the file's head/tail).\n const items: FItem[] = []\n const key: number[] = []\n let prev = -1\n for (const o of base) {\n if (o - prev > 1) {\n items.push({ t: 'gap', top: prev, bottom: o, size: o - prev - 1 })\n key.push(prev + 0.5)\n }\n items.push({ t: 'line', orig: o })\n key.push(o)\n prev = o\n }\n if (prev < N - 1) {\n items.push({ t: 'gap', top: prev, bottom: N, size: N - 1 - prev })\n key.push(prev + 0.5)\n }\n return { items, key }\n})\n\nconst inFilter = computed(() => filtered.value !== null)\nconst total = computed(() => (filtered.value ? filtered.value.items.length : runs.value.total))\n\ntype RowKind = 'plain' | 'header' | 'frame'\ntype RowInfo = {\n n: number\n text: string\n kind: RowKind\n collapsed: boolean\n count: number\n gid: number\n}\n\nfunction rowInfo(orig: number): RowInfo {\n const text = lines.value[orig] ?? ''\n const gid = headerMap.value.get(orig)\n if (gid !== undefined) {\n const g = groups.value[gid]\n // Filter mode always shows groups expanded (frames must be visible to keep the stack), so no fold chrome.\n return {\n n: orig + 1,\n text,\n kind: 'header',\n collapsed: filtered.value ? false : isCollapsed(gid),\n count: g.frameEnd - g.frameStart + 1,\n gid,\n }\n }\n const fg = groupOf(orig)\n const kind: RowKind = fg >= 0 && orig >= groups.value[fg].frameStart ? 'frame' : 'plain'\n return { n: orig + 1, text, kind, collapsed: false, count: 0, gid: -1 }\n}\n\n// Non-filter: display row index → original line index (binary search over the visible runs).\nfunction origInRuns(rs: { o0: number; o1: number; d0: number }[], d: number): number {\n let lo = 0\n let hi = rs.length - 1\n let pick = 0\n while (lo <= hi) {\n const mid = (lo + hi) >> 1\n if (d < rs[mid].d0) hi = mid - 1\n else {\n pick = mid\n lo = mid + 1\n }\n }\n const r = rs[pick]\n return r ? r.o0 + (d - r.d0) : d\n}\n\nconst startIdx = computed(() => Math.max(0, Math.floor(scrollTop.value / LINE_H) - OVERSCAN))\nconst endIdx = computed(() =>\n Math.min(total.value, Math.ceil((scrollTop.value + viewportH.value) / LINE_H) + OVERSCAN),\n)\n\n// One flat shape for every visible row (content OR gap) so the template needs no union narrowing. Content\n// rows default the gap fields; gap rows default the content fields.\ntype VisRow = {\n i: number\n isGap: boolean\n n: number\n text: string\n kind: RowKind\n collapsed: boolean\n count: number\n gid: number\n style: { bg: string; fg: string } | null\n context: boolean\n top: number\n bottom: number\n size: number\n step: number\n}\nfunction contentRow(i: number, orig: number): VisRow {\n const info = rowInfo(orig)\n return {\n i,\n isGap: false,\n ...info,\n style: styleFor(info.text),\n // \"Context\" dimming is a filter-mode cue for manually-revealed lines; never dim in the full (non-filter)\n // view, where leftover `revealed` entries are irrelevant.\n context:\n filtered.value !== null && revealed.value.has(orig) && !hitsQuery(info.text, debounced.value),\n top: 0,\n bottom: 0,\n size: 0,\n step: 0,\n }\n}\nconst visible = computed<VisRow[]>(() => {\n const rows: VisRow[] = []\n const s = startIdx.value\n const e = endIdx.value\n const f = filtered.value\n if (f) {\n for (let i = s; i < e; i++) {\n const it = f.items[i]\n if (!it) continue\n if (it.t === 'gap') {\n rows.push({\n i,\n isGap: true,\n n: 0,\n text: '',\n kind: 'plain',\n collapsed: false,\n count: 0,\n gid: -1,\n style: null,\n context: false,\n top: it.top,\n bottom: it.bottom,\n size: it.size,\n step: Math.min(STEP, it.size),\n })\n } else {\n rows.push(contentRow(i, it.orig))\n }\n }\n } else {\n const rs = runs.value.runs\n for (let i = s; i < e; i++) rows.push(contentRow(i, origInRuns(rs, i)))\n }\n return rows\n})\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n\n// Escape the line and wrap query occurrences in <mark> (matching on raw text, escaping each piece → safe).\nfunction renderLine(text: string): string {\n const q = debounced.value\n if (!q) return escapeHtml(text)\n const lower = text.toLowerCase()\n const ql = q.toLowerCase()\n let out = ''\n let i = 0\n let idx = lower.indexOf(ql, i)\n while (idx !== -1) {\n out += escapeHtml(text.slice(i, idx))\n out += '<mark class=\"log__hit\">' + escapeHtml(text.slice(idx, idx + q.length)) + '</mark>'\n i = idx + q.length\n idx = lower.indexOf(ql, i)\n }\n out += escapeHtml(text.slice(i))\n return out\n}\n\n// ---- navigation ---------------------------------------------------------------------------------\n\nfunction scrollToDisplay(i: number): void {\n const el = scrollEl.value\n if (el) el.scrollTop = Math.max(0, i * LINE_H - viewportH.value / 2 + LINE_H)\n}\n\n// Original line index → current display row index (or -1 if not shown). Folded frames redirect to their header.\nfunction origToDisplay(orig: number): number {\n const f = filtered.value\n if (f) {\n const k = f.key\n let lo = 0\n let hi = k.length - 1\n while (lo <= hi) {\n const mid = (lo + hi) >> 1\n if (k[mid] === orig) return mid\n if (k[mid] < orig) lo = mid + 1\n else hi = mid - 1\n }\n return -1\n }\n const g = groupOf(orig)\n if (g >= 0 && orig >= groups.value[g].frameStart && isCollapsed(g)) orig = groups.value[g].header\n const rs = runs.value.runs\n let lo = 0\n let hi = rs.length - 1\n while (lo <= hi) {\n const mid = (lo + hi) >> 1\n if (orig < rs[mid].o0) hi = mid - 1\n else if (orig > rs[mid].o1) lo = mid + 1\n else return rs[mid].d0 + (orig - rs[mid].o0)\n }\n return -1\n}\n\nfunction gotoMatch(delta: number): void {\n const m = matchIdx.value\n if (!m.length) return\n currentMatch.value = (currentMatch.value + delta + m.length) % m.length\n const d = origToDisplay(m[currentMatch.value])\n if (d >= 0) scrollToDisplay(d)\n}\n\nconst gotoLineInput = ref('')\nfunction gotoLine(): void {\n const n = parseInt(gotoLineInput.value, 10)\n if (!Number.isFinite(n)) return\n const orig = Math.max(0, Math.min(lines.value.length - 1, n - 1))\n const d = origToDisplay(orig)\n if (d >= 0) scrollToDisplay(d)\n}\n\nfunction onScroll(): void {\n if (scrollEl.value) scrollTop.value = scrollEl.value.scrollTop\n}\n\n// ---- load ---------------------------------------------------------------------------------------\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const text = await res.text()\n lines.value = text.split('\\n')\n // Drop a single trailing empty line (file ended with \\n) so there's no phantom last row.\n if (lines.value.length > 1 && lines.value[lines.value.length - 1] === '') lines.value.pop()\n expanded.value = new Set() // stacks fold by default on each fresh file\n revealed.value = new Set()\n scrollTop.value = 0\n if (scrollEl.value) scrollEl.value.scrollTop = 0\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\n// scrollEl lives inside the `status === 'ready'` branch (a v-else), so it does NOT exist at mount time —\n// it appears only after the file finishes loading. Watch the ref and attach the ResizeObserver when the\n// element shows up (and re-attach if the pane is torn down + rebuilt on reload), so viewportH reflects the\n// REAL pane height. Without this it stayed at the 400px default and only ~half a screen of rows rendered,\n// leaving the rest blank until you scrolled. flush:'post' so clientHeight is read after the DOM is painted.\nwatch(\n scrollEl,\n (el) => {\n ro?.disconnect()\n if (el && typeof ResizeObserver !== 'undefined') {\n ro = new ResizeObserver(() => {\n if (scrollEl.value) viewportH.value = scrollEl.value.clientHeight\n })\n ro.observe(el)\n viewportH.value = el.clientHeight\n }\n },\n { flush: 'post' },\n)\nwatch(() => props.url, load, { immediate: true })\nonBeforeUnmount(() => {\n clearTimeout(debounceTimer)\n ro?.disconnect()\n})\n</script>\n\n<template>\n <div class=\"log\">\n <p v-if=\"status === 'loading'\" class=\"log__msg\">正在加载…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"log__msg log__gate\">\n <p>该文件较大({{ formatBytes(props.size) }}),将在浏览器内加载。是否继续?</p>\n <button type=\"button\" class=\"log__btn\" @click=\"doLoad\">加载并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"log__msg log__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"log__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else>\n <!-- toolbar: search + match nav + filter + fold-all + color rules + go-to-line -->\n <div class=\"log__bar\">\n <div class=\"log__searchwrap\">\n <input\n v-model=\"query\"\n class=\"log__search\"\n type=\"text\"\n placeholder=\"搜索…\"\n aria-label=\"搜索\"\n @keydown.esc=\"clearSearch\"\n />\n <button\n v-if=\"query\"\n type=\"button\"\n class=\"log__clear\"\n title=\"清空搜索\"\n aria-label=\"清空搜索\"\n @click=\"clearSearch\"\n >\n <Icon name=\"close\" :size=\"12\" />\n </button>\n </div>\n <span v-if=\"debounced\" class=\"log__matches\">\n {{ matchIdx.length ? currentMatch + 1 : 0 }}/{{ matchIdx.length }}\n </span>\n <button\n type=\"button\"\n class=\"log__nav\"\n title=\"上一处\"\n :disabled=\"!matchIdx.length\"\n @click=\"gotoMatch(-1)\"\n >\n ↑\n </button>\n <button\n type=\"button\"\n class=\"log__nav\"\n title=\"下一处\"\n :disabled=\"!matchIdx.length\"\n @click=\"gotoMatch(1)\"\n >\n ↓\n </button>\n <button\n type=\"button\"\n class=\"log__nav log__nav--text\"\n :class=\"{ 'log__nav--on': filterMode }\"\n :disabled=\"!debounced\"\n title=\"只看匹配行(命中的堆栈整条保留,行间可展开上下文)\"\n @click=\"filterMode = !filterMode\"\n >\n 过滤\n </button>\n <button\n type=\"button\"\n class=\"log__nav log__nav--text\"\n :class=\"{ 'log__nav--on': errorOnly }\"\n title=\"只显示异常/错误行(异常头、ERROR/FATAL、*Exception/*Error 等;可与搜索过滤叠加)\"\n @click=\"errorOnly = !errorOnly\"\n >\n 只看异常\n </button>\n <button\n v-if=\"hasGroups\"\n type=\"button\"\n class=\"log__nav log__nav--text\"\n :class=\"{ 'log__nav--on': allExpanded }\"\n :title=\"`共 ${groups.length} 处调用堆栈,点击${allExpanded ? '折叠' : '展开'}全部`\"\n @click=\"toggleAll\"\n >\n {{ allExpanded ? '折叠堆栈' : '展开堆栈' }}\n </button>\n <button\n type=\"button\"\n class=\"log__nav log__nav--text\"\n :class=\"{ 'log__nav--on': rulesOpen || compiledRules.length > 0 }\"\n title=\"按规则给行上色\"\n @click=\"rulesOpen = !rulesOpen\"\n >\n 上色\n </button>\n <span class=\"log__spacer\" />\n <input\n v-model=\"gotoLineInput\"\n class=\"log__goto\"\n type=\"text\"\n inputmode=\"numeric\"\n placeholder=\"行号\"\n aria-label=\"跳转到行\"\n @keydown.enter=\"gotoLine\"\n />\n <span class=\"log__total\">\n {{ lines.length }} 行<template v-if=\"hasGroups\"> · {{ groups.length }} 堆栈</template>\n </span>\n\n <!-- color-rules editor (dropdown). Backdrop closes on outside click. -->\n <div v-if=\"rulesOpen\" class=\"log__overlay\" @click=\"rulesOpen = false\" />\n <div v-if=\"rulesOpen\" class=\"log__rules\" @click.stop>\n <div class=\"log__rules-head\">\n <span class=\"log__rules-title\">行上色规则</span>\n <span class=\"log__rules-hint\">从上到下,首个匹配生效</span>\n </div>\n <p v-if=\"!rules.length\" class=\"log__rules-empty\">\n 还没有规则。添加一条,用关键字或正则给匹配行上色(如 <code>ERROR</code> 标红)。\n </p>\n <div v-for=\"(r, idx) in rules\" :key=\"r.id\" class=\"log__rule\">\n <input v-model=\"r.enabled\" type=\"checkbox\" title=\"启用该规则\" />\n <input\n v-model=\"r.pattern\"\n class=\"log__rule-pat\"\n :class=\"{ 'log__rule-pat--bad': regexBad(r) }\"\n type=\"text\"\n placeholder=\"关键字或正则\"\n />\n <button\n type=\"button\"\n class=\"log__rule-tog\"\n :class=\"{ 'log__rule-tog--on': r.regex }\"\n title=\"按正则匹配\"\n @click=\"r.regex = !r.regex\"\n >\n .*\n </button>\n <button\n type=\"button\"\n class=\"log__rule-tog\"\n :class=\"{ 'log__rule-tog--on': r.caseSensitive }\"\n title=\"区分大小写\"\n @click=\"r.caseSensitive = !r.caseSensitive\"\n >\n Aa\n </button>\n <label class=\"log__rule-color\" title=\"背景色\">\n <input v-model=\"r.bgOn\" type=\"checkbox\" />\n <input v-model=\"r.bg\" type=\"color\" :disabled=\"!r.bgOn\" />\n <span>底</span>\n </label>\n <label class=\"log__rule-color\" title=\"字体色\">\n <input v-model=\"r.fgOn\" type=\"checkbox\" />\n <input v-model=\"r.fg\" type=\"color\" :disabled=\"!r.fgOn\" />\n <span>字</span>\n </label>\n <button\n type=\"button\"\n class=\"log__rule-ic\"\n title=\"上移\"\n :disabled=\"idx === 0\"\n @click=\"moveRule(r.id, -1)\"\n >\n <Icon name=\"arrowUp\" :size=\"12\" />\n </button>\n <button\n type=\"button\"\n class=\"log__rule-ic\"\n title=\"下移\"\n :disabled=\"idx === rules.length - 1\"\n @click=\"moveRule(r.id, 1)\"\n >\n <Icon name=\"arrowDown\" :size=\"12\" />\n </button>\n <button\n type=\"button\"\n class=\"log__rule-ic log__rule-del\"\n title=\"删除\"\n @click=\"removeRule(r.id)\"\n >\n <Icon name=\"trash\" :size=\"12\" />\n </button>\n </div>\n <button type=\"button\" class=\"log__rules-add\" @click=\"addRule\">\n <Icon name=\"plus\" :size=\"12\" /> 添加规则\n </button>\n </div>\n </div>\n\n <!-- virtual list: a fixed-height sizer drives the scrollbar; only the visible window is mounted -->\n <div ref=\"scrollEl\" class=\"log__scroll\" @scroll=\"onScroll\">\n <div class=\"log__sizer\" :style=\"{ height: total * LINE_H + 'px' }\">\n <div class=\"log__rows\" :style=\"{ paddingTop: startIdx * LINE_H + 'px' }\">\n <div\n v-for=\"row in visible\"\n :key=\"row.i\"\n class=\"log__row\"\n :class=\"{\n log__gaprow: row.isGap,\n 'log__row--active': !row.isGap && row.n - 1 === activeOrig,\n 'log__row--header': !row.isGap && row.kind === 'header',\n 'log__row--frame': !row.isGap && row.kind === 'frame',\n 'log__row--context': row.context,\n }\"\n :style=\"\n !row.isGap && row.style && row.style.bg ? { background: row.style.bg } : undefined\n \"\n >\n <!-- gap expander (filter mode): reveal hidden context around matches -->\n <template v-if=\"row.isGap\">\n <button\n v-if=\"row.size > STEP\"\n type=\"button\"\n class=\"log__gapbtn\"\n :title=\"`向下展开 ${row.step} 行(上方匹配之后)`\"\n @click=\"reveal(row.top, row.bottom, 'down')\"\n >\n <Icon name=\"chevronDown\" :size=\"12\" />\n </button>\n <button\n type=\"button\"\n class=\"log__gapmain\"\n :title=\"`展开全部 ${row.size} 行`\"\n @click=\"reveal(row.top, row.bottom, 'all')\"\n >\n ⋯ {{ row.size }} 行\n </button>\n <button\n v-if=\"row.size > STEP\"\n type=\"button\"\n class=\"log__gapbtn log__gapbtn--up\"\n :title=\"`向上展开 ${row.step} 行(下方匹配之前)`\"\n @click=\"reveal(row.top, row.bottom, 'up')\"\n >\n <Icon name=\"chevronDown\" :size=\"12\" />\n </button>\n </template>\n\n <!-- content row -->\n <template v-else>\n <span class=\"log__ln\">{{ row.n }}</span>\n <span v-if=\"hasGroups\" class=\"log__fold-cell\">\n <button\n v-if=\"row.kind === 'header' && !inFilter\"\n type=\"button\"\n class=\"log__fold\"\n :title=\"row.collapsed ? '展开堆栈' : '折叠堆栈'\"\n @click=\"toggleGroup(row.gid)\"\n >\n <Icon\n name=\"chevronRight\"\n :size=\"12\"\n class=\"log__chev\"\n :class=\"{ 'log__chev--open': !row.collapsed }\"\n />\n </button>\n </span>\n <span\n class=\"log__tx\"\n :style=\"row.style && row.style.fg ? { color: row.style.fg } : undefined\"\n v-html=\"renderLine(row.text)\"\n />\n <button\n v-if=\"row.kind === 'header' && row.collapsed && !inFilter\"\n type=\"button\"\n class=\"log__badge\"\n title=\"展开堆栈\"\n @click=\"toggleGroup(row.gid)\"\n >\n ⋯ {{ row.count }} 帧\n </button>\n </template>\n </div>\n </div>\n </div>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.log {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--code-bg);\n}\n\n.log__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.log__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.log__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.log__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.log__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.log__bar {\n position: relative;\n display: flex;\n align-items: center;\n gap: 6px;\n flex-shrink: 0;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.log__searchwrap {\n position: relative;\n flex: 1 1 auto;\n min-width: 60px;\n display: flex;\n}\n.log__search {\n flex: 1 1 auto;\n min-width: 0;\n padding: 5px 28px 5px 10px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--code-bg);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.log__search:focus,\n.log__goto:focus {\n outline: none;\n border-color: var(--accent);\n}\n.log__clear {\n position: absolute;\n top: 50%;\n right: 5px;\n transform: translateY(-50%);\n display: flex;\n align-items: center;\n justify-content: center;\n width: 19px;\n height: 19px;\n padding: 0;\n color: var(--faint);\n background: none;\n border: none;\n border-radius: 5px;\n cursor: pointer;\n}\n.log__clear:hover {\n color: var(--text);\n background: var(--surface-2);\n}\n.log__matches {\n flex-shrink: 0;\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--faint);\n min-width: 38px;\n text-align: center;\n}\n.log__nav {\n flex-shrink: 0;\n min-width: 26px;\n height: 26px;\n padding: 0 6px;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n cursor: pointer;\n}\n.log__nav:hover:not(:disabled) {\n border-color: var(--accent);\n color: var(--accent);\n}\n.log__nav:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.log__nav--on {\n color: var(--accent);\n border-color: var(--accent);\n background: var(--surface-2);\n}\n.log__spacer {\n flex: 1 1 auto;\n}\n.log__goto {\n flex-shrink: 0;\n width: 64px;\n padding: 5px 8px;\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--text);\n background: var(--code-bg);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.log__total {\n flex-shrink: 0;\n font-size: 11.5px;\n color: var(--faint);\n white-space: nowrap;\n}\n\n/* color-rules dropdown */\n.log__overlay {\n position: fixed;\n inset: 0;\n z-index: 40;\n background: transparent;\n}\n.log__rules {\n position: absolute;\n z-index: 41;\n top: calc(100% + 4px);\n right: 12px;\n width: min(520px, calc(100vw - 24px));\n max-height: 320px;\n overflow: auto;\n padding: 10px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 10px;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);\n}\n.log__rules-head {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 8px;\n}\n.log__rules-title {\n font-size: 12.5px;\n font-weight: 600;\n color: var(--text);\n}\n.log__rules-hint {\n font-size: 11px;\n color: var(--faint);\n}\n.log__rules-empty {\n font-size: 12px;\n color: var(--muted);\n line-height: 1.5;\n margin: 2px 0;\n}\n.log__rules-empty code {\n font-family: var(--font-mono);\n font-size: 11px;\n padding: 0 4px;\n background: var(--code-bg);\n border-radius: 4px;\n}\n.log__rule {\n display: flex;\n align-items: center;\n gap: 5px;\n}\n.log__rule-pat {\n flex: 1 1 auto;\n min-width: 80px;\n padding: 4px 8px;\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--text);\n background: var(--code-bg);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n}\n.log__rule-pat:focus {\n outline: none;\n border-color: var(--accent);\n}\n.log__rule-pat--bad {\n border-color: var(--danger);\n color: var(--danger);\n}\n.log__rule-tog {\n flex-shrink: 0;\n width: 26px;\n height: 26px;\n padding: 0;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--muted);\n background: var(--code-bg);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n cursor: pointer;\n}\n.log__rule-tog--on {\n color: var(--accent);\n border-color: var(--accent);\n background: var(--surface-2);\n}\n.log__rule-color {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 11px;\n color: var(--faint);\n cursor: pointer;\n}\n.log__rule-color input[type='color'] {\n width: 22px;\n height: 22px;\n padding: 0;\n border: 1px solid var(--border-2);\n border-radius: 5px;\n background: none;\n cursor: pointer;\n}\n.log__rule-color input[type='color']:disabled {\n opacity: 0.35;\n cursor: default;\n}\n.log__rule-ic {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n padding: 0;\n color: var(--muted);\n background: none;\n border: 1px solid transparent;\n border-radius: 6px;\n cursor: pointer;\n}\n.log__rule-ic:hover:not(:disabled) {\n color: var(--accent);\n border-color: var(--border-2);\n}\n.log__rule-ic:disabled {\n opacity: 0.3;\n cursor: default;\n}\n.log__rule-del:hover:not(:disabled) {\n color: var(--danger);\n}\n.log__rules-add {\n align-self: flex-start;\n display: inline-flex;\n align-items: center;\n gap: 5px;\n margin-top: 2px;\n padding: 4px 10px;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: var(--code-bg);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n cursor: pointer;\n}\n.log__rules-add:hover {\n color: var(--accent);\n border-color: var(--accent);\n}\n\n.log__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.log__sizer {\n position: relative;\n min-width: 100%;\n}\n.log__row {\n display: flex;\n align-items: flex-start;\n min-width: 100%;\n height: 19px;\n line-height: 19px;\n white-space: nowrap;\n font-family: var(--font-mono);\n font-size: 12.5px;\n}\n.log__row--active {\n background: color-mix(in srgb, var(--accent) 14%, transparent);\n}\n.log__row--context .log__tx {\n opacity: 0.66;\n}\n.log__ln {\n position: sticky;\n left: 0;\n flex-shrink: 0;\n width: 64px;\n padding-right: 12px;\n text-align: right;\n color: var(--faint);\n background: var(--code-bg);\n user-select: none;\n}\n.log__row--active .log__ln {\n background: color-mix(in srgb, var(--accent) 14%, var(--code-bg));\n}\n\n/* fold gutter — a fixed cell right after the line number, present for every row (only when the file has\n stacks) so header/frame/plain text all stay column-aligned. Holds the chevron on headers; draws a faint\n guide line down the left edge of frame rows to read the run as a nested block. */\n.log__fold-cell {\n flex-shrink: 0;\n width: 16px;\n align-self: stretch;\n display: flex;\n align-items: center;\n justify-content: center;\n user-select: none;\n}\n.log__row--frame .log__fold-cell {\n box-shadow: inset 1px 0 0 var(--border-2);\n}\n.log__fold {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n padding: 0;\n border: none;\n background: none;\n color: var(--faint);\n cursor: pointer;\n}\n.log__chev {\n transition: transform 0.12s ease;\n}\n.log__chev--open {\n transform: rotate(90deg);\n}\n.log__fold:hover .log__chev {\n color: var(--accent);\n}\n\n.log__tx {\n flex: 1 1 auto;\n padding-right: 18px;\n color: var(--text);\n white-space: pre;\n}\n.log__row--header .log__tx {\n color: var(--text);\n font-weight: 600;\n}\n.log__row--frame .log__tx {\n color: var(--muted);\n}\n.log__tx :deep(.log__hit) {\n background: #ffd43b;\n color: #1a1a1a;\n border-radius: 2px;\n}\n\n.log__badge {\n flex-shrink: 0;\n align-self: center;\n margin-left: 8px;\n padding: 0 8px;\n height: 15px;\n font-family: var(--font-mono);\n font-size: 10.5px;\n line-height: 13px;\n color: var(--faint);\n background: var(--surface-2);\n border: 1px solid var(--border-2);\n border-radius: 9px;\n cursor: pointer;\n user-select: none;\n}\n.log__badge:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* filter-mode context expander bar (one virtual row tall) */\n.log__gaprow {\n position: sticky;\n left: 0;\n align-items: center;\n justify-content: center;\n gap: 10px;\n background: var(--surface-2);\n color: var(--faint);\n user-select: none;\n}\n.log__gapbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 15px;\n padding: 0;\n color: var(--muted);\n background: none;\n border: none;\n cursor: pointer;\n}\n.log__gapbtn:hover {\n color: var(--accent);\n}\n.log__gapbtn--up {\n transform: rotate(180deg);\n}\n.log__gapmain {\n padding: 0 10px;\n height: 15px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--faint);\n background: none;\n border: none;\n cursor: pointer;\n}\n.log__gapmain:hover {\n color: var(--accent);\n}\n</style>\n"],"mappings":""}
@@ -668,3 +668,5 @@ var re = { class: "log" }, ie = {
668
668
  });
669
669
  //#endregion
670
670
  export { w as default };
671
+
672
+ //# sourceMappingURL=LogViewer.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LogViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/LogViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { formatBytes } from '@/lib/format'\nimport Icon from '@/components/icons/Icon.vue'\nimport { useLogRules, type ColorRule } from '@/composables/useLogRules'\n\n/**\n * Side-viewer for large text / log files. Fetches the bytes, splits into lines, and renders them with a\n * fixed-height VIRTUAL list — only the visible window (plus a little overscan) is ever in the DOM, so a\n * multi-hundred-MB log scrolls smoothly instead of locking the page (the plain source view would mount one\n * huge highlighted block). No syntax highlight (logs don't need it, and tokenizing 500k lines is infeasible).\n *\n * Routed here for any `.log`, and for any large text file (see FileChip's size gate) so a 50MB `.txt`/`.csv`\n * doesn't freeze the normal viewer. Tools: a sticky line-number gutter, substring search (highlight, match\n * count, prev/next, optional filter-to-matches), go-to-line, plus three log-specific aids:\n *\n * 1. Call-stack folding — error logs from common languages (Java/Kotlin/Scala, JS/Node, Python, C#/.NET,\n * Ruby) are one header line (\"Exception in thread…\", \"Traceback…\", \"Caused by:\") followed by a run of\n * indented frame lines (\" at …\", ' File \"…\"', …). We detect those runs, hang a chevron off the header,\n * and collapse the frames behind a \"N 帧\" badge (collapsed by DEFAULT). A live search force-expands any\n * stack with a hit; filter mode pulls a matched frame's WHOLE stack in (see {@link filtered}).\n * 2. Context expansion (filter mode) — between filtered chunks sits a \"⋯ N 行\" expander bar; click it to\n * progressively reveal hidden lines around a match (a chunk from the top, from the bottom, or all), the\n * way JetBrains' diff reveals collapsed unchanged regions. Revealed lines are tracked in {@link revealed}.\n * 3. Color rules — user-defined substring/regex rules paint matching lines' background/text (see\n * {@link useLogRules}); applied to visible rows only via {@link styleFor}.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst lines = ref<string[]>([])\n\n// Reading the whole file into memory is unavoidable here (the endpoint has no range support); gate the\n// truly huge ones behind a confirm.\nconst SIZE_LIMIT = 64 * 1024 * 1024\nconst LINE_H = 19 // px — MUST match the .log__row CSS height\nconst OVERSCAN = 12\nconst STEP = 10 // lines revealed per directional context-expand click\n\nconst scrollEl = ref<HTMLElement | null>(null)\nconst scrollTop = ref(0)\nconst viewportH = ref(400)\nlet ro: ResizeObserver | null = null\n\n// ---- stack-trace detection ----------------------------------------------------------------------\n// A stack group spans original lines [header .. frameEnd]; `header` is always `frameStart - 1` (the error /\n// \"Caused by:\" / \"Traceback:\" line just above the run), and the collapsible frames are [frameStart .. frameEnd].\ntype Group = { header: number; frameStart: number; frameEnd: number }\n\n// Only fold a run with at least this many frame lines (folding a lone frame saves nothing).\nconst MIN_FRAMES = 2\n\n// Indented stack-frame lines across common languages, matched on the raw line:\n// RE_AT Java/Kotlin/Scala/JS/Node/C#/.NET/PHP \" at pkg.Class.method(File:12)\" / \"at fn (file.js:1:2)\"\n// (requires a '.' or '(' after the symbol so a prose line like \" at 3pm the job ran\" won't match)\n// RE_FILE Python ' File \"/x.py\", line 10, in fn'\n// RE_FROM Ruby \" from a.rb:5:in `m'\"\n// RE_MORE Java \" ... 12 more\"\n// RE_NET C#/.NET async boundary \" --- End of stack trace from previous location ---\"\nconst RE_AT = /^[ \\t]+at\\s+\\S+[.(]/\nconst RE_FILE = /^[ \\t]+File\\s[\"']/\nconst RE_FROM = /^[ \\t]+from\\s+\\S+:\\d+/\nconst RE_MORE = /^[ \\t]*\\.\\.\\.\\s+\\d+\\s+more\\b/\nconst RE_NET = /^[ \\t]+--- End of /\nfunction isFrame(line: string): boolean {\n return (\n RE_AT.test(line) ||\n RE_FILE.test(line) ||\n RE_FROM.test(line) ||\n RE_MORE.test(line) ||\n RE_NET.test(line)\n )\n}\n\n// Group consecutive frame runs. Python frames come in two lines (the `File \"…\"` line plus the indented source\n// line under it) — absorb that trailing source line into the run so it folds with its frame.\nconst groups = computed<Group[]>(() => {\n const src = lines.value\n const N = src.length\n const out: Group[] = []\n let i = 0\n while (i < N) {\n if (!isFrame(src[i])) {\n i++\n continue\n }\n const runStart = i\n let prevFile = RE_FILE.test(src[i])\n i++\n while (i < N) {\n const ln = src[i]\n if (isFrame(ln)) {\n prevFile = RE_FILE.test(ln)\n i++\n } else if (prevFile && /^[ \\t]+\\S/.test(ln)) {\n prevFile = false // Python source line under a `File \"…\"` frame\n i++\n } else break\n }\n const runEnd = i - 1\n // Prefer the (non-blank) plain line just above the run as the header; otherwise treat the run's first line\n // as the header and fold the rest.\n const above = runStart - 1\n const useAbove = above >= 0 && !isFrame(src[above]) && src[above].trim() !== ''\n const frameStart = useAbove ? runStart : runStart + 1\n if (runEnd - frameStart + 1 >= MIN_FRAMES) {\n out.push({ header: frameStart - 1, frameStart, frameEnd: runEnd })\n }\n }\n return out\n})\n\nconst hasGroups = computed(() => groups.value.length > 0)\n\n// origLine → groupId, for header lines only.\nconst headerMap = computed(() => {\n const m = new Map<number, number>()\n groups.value.forEach((g, i) => m.set(g.header, i))\n return m\n})\n\n// The group whose span [header .. frameEnd] contains `orig` (groups are position-sorted & disjoint), or -1.\nfunction groupOf(orig: number): number {\n const gs = groups.value\n let lo = 0\n let hi = gs.length - 1\n while (lo <= hi) {\n const mid = (lo + hi) >> 1\n const g = gs[mid]\n if (orig < g.header) hi = mid - 1\n else if (orig > g.frameEnd) lo = mid + 1\n else return mid\n }\n return -1\n}\n\n// User-expanded group ids (default = absent = collapsed). Reset on each load.\nconst expanded = ref<Set<number>>(new Set())\nconst allExpanded = computed(() => hasGroups.value && expanded.value.size === groups.value.length)\n\nfunction toggleGroup(id: number): void {\n const s = new Set(expanded.value)\n if (s.has(id)) s.delete(id)\n else s.add(id)\n expanded.value = s\n}\nfunction toggleAll(): void {\n expanded.value = allExpanded.value ? new Set() : new Set(groups.value.map((_, i) => i))\n}\n\n// Is this group folded right now? Collapsed unless the user expanded it — OR a live query has a hit inside it\n// (force-expand so the match can't hide behind the fold).\nfunction isCollapsed(id: number): boolean {\n if (expanded.value.has(id)) return false\n if (debounced.value && includedGroups.value.has(id)) return false\n return true\n}\n\n// ---- color rules --------------------------------------------------------------------------------\n\nconst { rules, add: addRule, remove: removeRule, move: moveRule } = useLogRules()\nconst rulesOpen = ref(false)\n\n// Enabled, non-empty rules pre-compiled to a matcher (+ colors). Invalid regex / color-less rules are dropped.\nconst compiledRules = computed(() => {\n const out: { test: (s: string) => boolean; bg: string; fg: string }[] = []\n for (const r of rules.value) {\n if (!r.enabled || !r.pattern) continue\n if (!(r.bgOn || r.fgOn)) continue\n let test: ((s: string) => boolean) | null\n if (r.regex) {\n try {\n const re = new RegExp(r.pattern, r.caseSensitive ? '' : 'i')\n test = (s) => re.test(s)\n } catch {\n test = null // invalid regex → skip until the user fixes it\n }\n } else if (r.caseSensitive) {\n const n = r.pattern\n test = (s) => s.includes(n)\n } else {\n const n = r.pattern.toLowerCase()\n test = (s) => s.toLowerCase().includes(n)\n }\n if (test) out.push({ test, bg: r.bgOn ? r.bg : '', fg: r.fgOn ? r.fg : '' })\n }\n return out\n})\n\n// First matching rule's colors for a line, or null. Called per VISIBLE row only.\nfunction styleFor(text: string): { bg: string; fg: string } | null {\n for (const c of compiledRules.value) {\n if (c.test(text)) return { bg: c.bg, fg: c.fg }\n }\n return null\n}\n\n// A regex rule with a syntax error — used to flag the input red in the editor.\nfunction regexBad(r: ColorRule): boolean {\n if (!r.regex || !r.pattern) return false\n try {\n new RegExp(r.pattern)\n return false\n } catch {\n return true\n }\n}\n\n// ---- search -------------------------------------------------------------------------------------\n\nconst query = ref('')\nconst debounced = ref('')\nlet debounceTimer: ReturnType<typeof setTimeout> | undefined\nwatch(query, (q) => {\n clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n debounced.value = q\n currentMatch.value = 0\n revealed.value = new Set() // context is relative to the current filter → drop it when the query changes\n }, 160)\n})\n\n// Clear the search box (× button / Esc) — apply immediately instead of waiting on the debounce.\nfunction clearSearch(): void {\n query.value = ''\n clearTimeout(debounceTimer)\n debounced.value = ''\n currentMatch.value = 0\n revealed.value = new Set()\n}\n\nconst filterMode = ref(false)\nconst errorOnly = ref(false)\nconst currentMatch = ref(0)\n\n// Original line indices (0-based) that contain the query.\nconst matchIdx = computed(() => {\n const q = debounced.value.toLowerCase()\n if (!q) return [] as number[]\n const out: number[] = []\n const src = lines.value\n for (let i = 0; i < src.length; i++) {\n if (src[i].toLowerCase().includes(q)) out.push(i)\n }\n return out\n})\n\nfunction hitsQuery(text: string, q: string): boolean {\n return q ? text.toLowerCase().includes(q.toLowerCase()) : false\n}\n\n// Groups with at least one match (header or any frame). Drives both force-expand and filter-keeps-stack.\nconst includedGroups = computed(() => {\n const s = new Set<number>()\n if (!debounced.value) return s\n for (const m of matchIdx.value) {\n const g = groupOf(m)\n if (g >= 0) s.add(g)\n }\n return s\n})\n\n// The currently highlighted match's original line index (for the active-row tint), or -1.\nconst activeOrig = computed(() => {\n const m = matchIdx.value\n return m.length ? m[Math.min(currentMatch.value, m.length - 1)] : -1\n})\n\n// ---- \"only errors\" filter -----------------------------------------------------------------------\n// Error / exception lines for the 只看异常 quick filter. Case-sensitive on purpose: uppercase level tokens,\n// PascalCase *Exception / *Error class names, and a few explicit markers. Lowercase \"error\"/\"warn\" are NOT\n// matched (too noisy in ordinary messages) — a stack's header line is picked up via headerMap instead.\nconst ERROR_RE =\n /\\b(?:ERROR|FATAL|SEVERE|CRITICAL|PANIC|FAILED|FAILURE)\\b|\\b\\w*(?:Exception|Error)\\b|\\bTraceback\\b|\\bCaused by\\b|^\\s*panic:/\n\n// Error-line indices (only computed while the toggle is on). Stack headers always count as errors.\nconst errorIdx = computed<number[]>(() => {\n if (!errorOnly.value) return []\n const src = lines.value\n const hdr = headerMap.value\n const out: number[] = []\n for (let i = 0; i < src.length; i++) {\n if (hdr.has(i) || ERROR_RE.test(src[i])) out.push(i)\n }\n return out\n})\nconst errorSet = computed(() => new Set(errorIdx.value))\n\n// The search \"过滤\" toggle only bites when there's a query.\nconst searchFilterActive = computed(() => filterMode.value && debounced.value !== '')\n\n// The anchor lines the filtered view is built around (null ⇒ no filter → show the whole log via `runs`).\n// Search + errors compose as an intersection (\"errors that also match the query\"); either alone is its own set.\nconst anchors = computed<number[] | null>(() => {\n const s = searchFilterActive.value\n const e = errorOnly.value\n if (!s && !e) return null\n if (s && e) {\n const es = errorSet.value\n return matchIdx.value.filter((i) => es.has(i))\n }\n return e ? errorIdx.value : matchIdx.value\n})\n\n// Keep whole stacks around anchors only when the search filter contributes (its contract: 过滤保留整条堆栈).\n// The pure 只看异常 view stays compact — one line per error, the stack peekable via the context expander.\nconst keepStacks = computed(() => searchFilterActive.value)\n\n// ---- context expansion (filter mode) ------------------------------------------------------------\n// Original line indices the user has manually revealed as context around matches. Reset on load / query change.\nconst revealed = ref<Set<number>>(new Set())\n// Switching filters changes what the gaps mean → drop revealed context.\nwatch([errorOnly, filterMode], () => {\n revealed.value = new Set()\n})\n\n// Reveal a chunk of a gap between shown lines (top, bottom are the shown lines bracketing it; -1 / N at file\n// edges): 'down' from the top match, 'up' toward the bottom match, or 'all'.\nfunction reveal(top: number, bottom: number, dir: 'up' | 'down' | 'all'): void {\n const lo = top + 1\n const hi = bottom - 1\n if (hi < lo) return\n let a = lo\n let b = hi\n if (dir === 'down') b = Math.min(hi, lo + STEP - 1)\n else if (dir === 'up') a = Math.max(lo, hi - STEP + 1)\n const s = new Set(revealed.value)\n for (let i = a; i <= b; i++) s.add(i)\n revealed.value = s\n}\n\n// ---- display layout -----------------------------------------------------------------------------\n// Two modes drive the virtual list:\n// - Non-filter: every line shows, minus the frames of collapsed groups. Expressed as `runs` — the visible\n// original-index segments between the hidden frame ranges — so display↔original stays a cheap binary search\n// (no per-line array; big files stay light).\n// - Filter: only matches show, but every included group contributes its WHOLE span so stacks stay intact,\n// plus any manually-revealed context lines; the remaining gaps become expander rows. Materialized as an\n// item list (bounded by what's shown, normally small).\n\n// Non-filter: visible segments + total row count.\nconst runs = computed(() => {\n const gs = groups.value\n const N = lines.value.length\n const out: { o0: number; o1: number; d0: number }[] = []\n let disp = 0\n let cursor = 0\n for (let id = 0; id < gs.length; id++) {\n if (!isCollapsed(id)) continue\n const g = gs[id]\n if (g.frameStart > cursor) {\n // visible run up to and including the header (header === frameStart - 1)\n out.push({ o0: cursor, o1: g.frameStart - 1, d0: disp })\n disp += g.frameStart - cursor\n }\n cursor = g.frameEnd + 1 // skip the folded frames\n }\n if (cursor < N) {\n out.push({ o0: cursor, o1: N - 1, d0: disp })\n disp += N - cursor\n }\n return { runs: out, total: disp }\n})\n\n// Filter-mode items: `line` rows plus `gap` expander rows. `key` is a parallel, strictly-increasing lookup\n// array (lines → their orig, gaps → top + 0.5) so origToDisplay can binary-search a match's row.\ntype FItem = { t: 'line'; orig: number } | { t: 'gap'; top: number; bottom: number; size: number }\nconst filtered = computed<{ items: FItem[]; key: number[] } | null>(() => {\n const anc = anchors.value\n if (anc === null) return null\n const N = lines.value.length\n const gs = groups.value\n // Stacks kept whole around anchors — only when the search filter contributes (see keepStacks).\n const ranges: [number, number][] = []\n if (keepStacks.value) {\n const inc = new Set<number>()\n for (const a of anc) {\n const g = groupOf(a)\n if (g >= 0) inc.add(g)\n }\n for (const id of inc) ranges.push([gs[id].header, gs[id].frameEnd])\n ranges.sort((a, b) => a[0] - b[0])\n }\n const covered = (o: number): boolean => {\n let lo = 0\n let hi = ranges.length - 1\n while (lo <= hi) {\n const mid = (lo + hi) >> 1\n if (o < ranges[mid][0]) hi = mid - 1\n else if (o > ranges[mid][1]) lo = mid + 1\n else return true\n }\n return false\n }\n // Standalone shown lines = anchors + revealed context, minus whatever a kept-whole stack already covers.\n const extra: number[] = []\n for (const a of anc) if (!covered(a)) extra.push(a)\n for (const r of revealed.value) if (!covered(r)) extra.push(r)\n extra.sort((a, b) => a - b)\n // Merge (sorted) range indices with (sorted) standalone lines → sorted, de-duped base set.\n const base: number[] = []\n let ri = 0\n let si = 0\n let cur = ranges.length ? ranges[0][0] : Infinity\n let last = -1\n while (ri < ranges.length || si < extra.length) {\n const rn = ri < ranges.length ? cur : Infinity\n const sn = si < extra.length ? extra[si] : Infinity\n let v: number\n if (rn <= sn) {\n v = rn\n if (cur >= ranges[ri][1]) {\n ri++\n cur = ri < ranges.length ? ranges[ri][0] : Infinity\n } else cur++\n } else {\n v = sn\n si++\n }\n if (v !== last) {\n base.push(v)\n last = v\n }\n }\n // Weave in gap expanders between non-adjacent shown lines (and at the file's head/tail).\n const items: FItem[] = []\n const key: number[] = []\n let prev = -1\n for (const o of base) {\n if (o - prev > 1) {\n items.push({ t: 'gap', top: prev, bottom: o, size: o - prev - 1 })\n key.push(prev + 0.5)\n }\n items.push({ t: 'line', orig: o })\n key.push(o)\n prev = o\n }\n if (prev < N - 1) {\n items.push({ t: 'gap', top: prev, bottom: N, size: N - 1 - prev })\n key.push(prev + 0.5)\n }\n return { items, key }\n})\n\nconst inFilter = computed(() => filtered.value !== null)\nconst total = computed(() => (filtered.value ? filtered.value.items.length : runs.value.total))\n\ntype RowKind = 'plain' | 'header' | 'frame'\ntype RowInfo = {\n n: number\n text: string\n kind: RowKind\n collapsed: boolean\n count: number\n gid: number\n}\n\nfunction rowInfo(orig: number): RowInfo {\n const text = lines.value[orig] ?? ''\n const gid = headerMap.value.get(orig)\n if (gid !== undefined) {\n const g = groups.value[gid]\n // Filter mode always shows groups expanded (frames must be visible to keep the stack), so no fold chrome.\n return {\n n: orig + 1,\n text,\n kind: 'header',\n collapsed: filtered.value ? false : isCollapsed(gid),\n count: g.frameEnd - g.frameStart + 1,\n gid,\n }\n }\n const fg = groupOf(orig)\n const kind: RowKind = fg >= 0 && orig >= groups.value[fg].frameStart ? 'frame' : 'plain'\n return { n: orig + 1, text, kind, collapsed: false, count: 0, gid: -1 }\n}\n\n// Non-filter: display row index → original line index (binary search over the visible runs).\nfunction origInRuns(rs: { o0: number; o1: number; d0: number }[], d: number): number {\n let lo = 0\n let hi = rs.length - 1\n let pick = 0\n while (lo <= hi) {\n const mid = (lo + hi) >> 1\n if (d < rs[mid].d0) hi = mid - 1\n else {\n pick = mid\n lo = mid + 1\n }\n }\n const r = rs[pick]\n return r ? r.o0 + (d - r.d0) : d\n}\n\nconst startIdx = computed(() => Math.max(0, Math.floor(scrollTop.value / LINE_H) - OVERSCAN))\nconst endIdx = computed(() =>\n Math.min(total.value, Math.ceil((scrollTop.value + viewportH.value) / LINE_H) + OVERSCAN),\n)\n\n// One flat shape for every visible row (content OR gap) so the template needs no union narrowing. Content\n// rows default the gap fields; gap rows default the content fields.\ntype VisRow = {\n i: number\n isGap: boolean\n n: number\n text: string\n kind: RowKind\n collapsed: boolean\n count: number\n gid: number\n style: { bg: string; fg: string } | null\n context: boolean\n top: number\n bottom: number\n size: number\n step: number\n}\nfunction contentRow(i: number, orig: number): VisRow {\n const info = rowInfo(orig)\n return {\n i,\n isGap: false,\n ...info,\n style: styleFor(info.text),\n // \"Context\" dimming is a filter-mode cue for manually-revealed lines; never dim in the full (non-filter)\n // view, where leftover `revealed` entries are irrelevant.\n context:\n filtered.value !== null && revealed.value.has(orig) && !hitsQuery(info.text, debounced.value),\n top: 0,\n bottom: 0,\n size: 0,\n step: 0,\n }\n}\nconst visible = computed<VisRow[]>(() => {\n const rows: VisRow[] = []\n const s = startIdx.value\n const e = endIdx.value\n const f = filtered.value\n if (f) {\n for (let i = s; i < e; i++) {\n const it = f.items[i]\n if (!it) continue\n if (it.t === 'gap') {\n rows.push({\n i,\n isGap: true,\n n: 0,\n text: '',\n kind: 'plain',\n collapsed: false,\n count: 0,\n gid: -1,\n style: null,\n context: false,\n top: it.top,\n bottom: it.bottom,\n size: it.size,\n step: Math.min(STEP, it.size),\n })\n } else {\n rows.push(contentRow(i, it.orig))\n }\n }\n } else {\n const rs = runs.value.runs\n for (let i = s; i < e; i++) rows.push(contentRow(i, origInRuns(rs, i)))\n }\n return rows\n})\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n\n// Escape the line and wrap query occurrences in <mark> (matching on raw text, escaping each piece → safe).\nfunction renderLine(text: string): string {\n const q = debounced.value\n if (!q) return escapeHtml(text)\n const lower = text.toLowerCase()\n const ql = q.toLowerCase()\n let out = ''\n let i = 0\n let idx = lower.indexOf(ql, i)\n while (idx !== -1) {\n out += escapeHtml(text.slice(i, idx))\n out += '<mark class=\"log__hit\">' + escapeHtml(text.slice(idx, idx + q.length)) + '</mark>'\n i = idx + q.length\n idx = lower.indexOf(ql, i)\n }\n out += escapeHtml(text.slice(i))\n return out\n}\n\n// ---- navigation ---------------------------------------------------------------------------------\n\nfunction scrollToDisplay(i: number): void {\n const el = scrollEl.value\n if (el) el.scrollTop = Math.max(0, i * LINE_H - viewportH.value / 2 + LINE_H)\n}\n\n// Original line index → current display row index (or -1 if not shown). Folded frames redirect to their header.\nfunction origToDisplay(orig: number): number {\n const f = filtered.value\n if (f) {\n const k = f.key\n let lo = 0\n let hi = k.length - 1\n while (lo <= hi) {\n const mid = (lo + hi) >> 1\n if (k[mid] === orig) return mid\n if (k[mid] < orig) lo = mid + 1\n else hi = mid - 1\n }\n return -1\n }\n const g = groupOf(orig)\n if (g >= 0 && orig >= groups.value[g].frameStart && isCollapsed(g)) orig = groups.value[g].header\n const rs = runs.value.runs\n let lo = 0\n let hi = rs.length - 1\n while (lo <= hi) {\n const mid = (lo + hi) >> 1\n if (orig < rs[mid].o0) hi = mid - 1\n else if (orig > rs[mid].o1) lo = mid + 1\n else return rs[mid].d0 + (orig - rs[mid].o0)\n }\n return -1\n}\n\nfunction gotoMatch(delta: number): void {\n const m = matchIdx.value\n if (!m.length) return\n currentMatch.value = (currentMatch.value + delta + m.length) % m.length\n const d = origToDisplay(m[currentMatch.value])\n if (d >= 0) scrollToDisplay(d)\n}\n\nconst gotoLineInput = ref('')\nfunction gotoLine(): void {\n const n = parseInt(gotoLineInput.value, 10)\n if (!Number.isFinite(n)) return\n const orig = Math.max(0, Math.min(lines.value.length - 1, n - 1))\n const d = origToDisplay(orig)\n if (d >= 0) scrollToDisplay(d)\n}\n\nfunction onScroll(): void {\n if (scrollEl.value) scrollTop.value = scrollEl.value.scrollTop\n}\n\n// ---- load ---------------------------------------------------------------------------------------\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const text = await res.text()\n lines.value = text.split('\\n')\n // Drop a single trailing empty line (file ended with \\n) so there's no phantom last row.\n if (lines.value.length > 1 && lines.value[lines.value.length - 1] === '') lines.value.pop()\n expanded.value = new Set() // stacks fold by default on each fresh file\n revealed.value = new Set()\n scrollTop.value = 0\n if (scrollEl.value) scrollEl.value.scrollTop = 0\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\n// scrollEl lives inside the `status === 'ready'` branch (a v-else), so it does NOT exist at mount time —\n// it appears only after the file finishes loading. Watch the ref and attach the ResizeObserver when the\n// element shows up (and re-attach if the pane is torn down + rebuilt on reload), so viewportH reflects the\n// REAL pane height. Without this it stayed at the 400px default and only ~half a screen of rows rendered,\n// leaving the rest blank until you scrolled. flush:'post' so clientHeight is read after the DOM is painted.\nwatch(\n scrollEl,\n (el) => {\n ro?.disconnect()\n if (el && typeof ResizeObserver !== 'undefined') {\n ro = new ResizeObserver(() => {\n if (scrollEl.value) viewportH.value = scrollEl.value.clientHeight\n })\n ro.observe(el)\n viewportH.value = el.clientHeight\n }\n },\n { flush: 'post' },\n)\nwatch(() => props.url, load, { immediate: true })\nonBeforeUnmount(() => {\n clearTimeout(debounceTimer)\n ro?.disconnect()\n})\n</script>\n\n<template>\n <div class=\"log\">\n <p v-if=\"status === 'loading'\" class=\"log__msg\">正在加载…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"log__msg log__gate\">\n <p>该文件较大({{ formatBytes(props.size) }}),将在浏览器内加载。是否继续?</p>\n <button type=\"button\" class=\"log__btn\" @click=\"doLoad\">加载并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"log__msg log__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"log__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else>\n <!-- toolbar: search + match nav + filter + fold-all + color rules + go-to-line -->\n <div class=\"log__bar\">\n <div class=\"log__searchwrap\">\n <input\n v-model=\"query\"\n class=\"log__search\"\n type=\"text\"\n placeholder=\"搜索…\"\n aria-label=\"搜索\"\n @keydown.esc=\"clearSearch\"\n />\n <button\n v-if=\"query\"\n type=\"button\"\n class=\"log__clear\"\n title=\"清空搜索\"\n aria-label=\"清空搜索\"\n @click=\"clearSearch\"\n >\n <Icon name=\"close\" :size=\"12\" />\n </button>\n </div>\n <span v-if=\"debounced\" class=\"log__matches\">\n {{ matchIdx.length ? currentMatch + 1 : 0 }}/{{ matchIdx.length }}\n </span>\n <button\n type=\"button\"\n class=\"log__nav\"\n title=\"上一处\"\n :disabled=\"!matchIdx.length\"\n @click=\"gotoMatch(-1)\"\n >\n ↑\n </button>\n <button\n type=\"button\"\n class=\"log__nav\"\n title=\"下一处\"\n :disabled=\"!matchIdx.length\"\n @click=\"gotoMatch(1)\"\n >\n ↓\n </button>\n <button\n type=\"button\"\n class=\"log__nav log__nav--text\"\n :class=\"{ 'log__nav--on': filterMode }\"\n :disabled=\"!debounced\"\n title=\"只看匹配行(命中的堆栈整条保留,行间可展开上下文)\"\n @click=\"filterMode = !filterMode\"\n >\n 过滤\n </button>\n <button\n type=\"button\"\n class=\"log__nav log__nav--text\"\n :class=\"{ 'log__nav--on': errorOnly }\"\n title=\"只显示异常/错误行(异常头、ERROR/FATAL、*Exception/*Error 等;可与搜索过滤叠加)\"\n @click=\"errorOnly = !errorOnly\"\n >\n 只看异常\n </button>\n <button\n v-if=\"hasGroups\"\n type=\"button\"\n class=\"log__nav log__nav--text\"\n :class=\"{ 'log__nav--on': allExpanded }\"\n :title=\"`共 ${groups.length} 处调用堆栈,点击${allExpanded ? '折叠' : '展开'}全部`\"\n @click=\"toggleAll\"\n >\n {{ allExpanded ? '折叠堆栈' : '展开堆栈' }}\n </button>\n <button\n type=\"button\"\n class=\"log__nav log__nav--text\"\n :class=\"{ 'log__nav--on': rulesOpen || compiledRules.length > 0 }\"\n title=\"按规则给行上色\"\n @click=\"rulesOpen = !rulesOpen\"\n >\n 上色\n </button>\n <span class=\"log__spacer\" />\n <input\n v-model=\"gotoLineInput\"\n class=\"log__goto\"\n type=\"text\"\n inputmode=\"numeric\"\n placeholder=\"行号\"\n aria-label=\"跳转到行\"\n @keydown.enter=\"gotoLine\"\n />\n <span class=\"log__total\">\n {{ lines.length }} 行<template v-if=\"hasGroups\"> · {{ groups.length }} 堆栈</template>\n </span>\n\n <!-- color-rules editor (dropdown). Backdrop closes on outside click. -->\n <div v-if=\"rulesOpen\" class=\"log__overlay\" @click=\"rulesOpen = false\" />\n <div v-if=\"rulesOpen\" class=\"log__rules\" @click.stop>\n <div class=\"log__rules-head\">\n <span class=\"log__rules-title\">行上色规则</span>\n <span class=\"log__rules-hint\">从上到下,首个匹配生效</span>\n </div>\n <p v-if=\"!rules.length\" class=\"log__rules-empty\">\n 还没有规则。添加一条,用关键字或正则给匹配行上色(如 <code>ERROR</code> 标红)。\n </p>\n <div v-for=\"(r, idx) in rules\" :key=\"r.id\" class=\"log__rule\">\n <input v-model=\"r.enabled\" type=\"checkbox\" title=\"启用该规则\" />\n <input\n v-model=\"r.pattern\"\n class=\"log__rule-pat\"\n :class=\"{ 'log__rule-pat--bad': regexBad(r) }\"\n type=\"text\"\n placeholder=\"关键字或正则\"\n />\n <button\n type=\"button\"\n class=\"log__rule-tog\"\n :class=\"{ 'log__rule-tog--on': r.regex }\"\n title=\"按正则匹配\"\n @click=\"r.regex = !r.regex\"\n >\n .*\n </button>\n <button\n type=\"button\"\n class=\"log__rule-tog\"\n :class=\"{ 'log__rule-tog--on': r.caseSensitive }\"\n title=\"区分大小写\"\n @click=\"r.caseSensitive = !r.caseSensitive\"\n >\n Aa\n </button>\n <label class=\"log__rule-color\" title=\"背景色\">\n <input v-model=\"r.bgOn\" type=\"checkbox\" />\n <input v-model=\"r.bg\" type=\"color\" :disabled=\"!r.bgOn\" />\n <span>底</span>\n </label>\n <label class=\"log__rule-color\" title=\"字体色\">\n <input v-model=\"r.fgOn\" type=\"checkbox\" />\n <input v-model=\"r.fg\" type=\"color\" :disabled=\"!r.fgOn\" />\n <span>字</span>\n </label>\n <button\n type=\"button\"\n class=\"log__rule-ic\"\n title=\"上移\"\n :disabled=\"idx === 0\"\n @click=\"moveRule(r.id, -1)\"\n >\n <Icon name=\"arrowUp\" :size=\"12\" />\n </button>\n <button\n type=\"button\"\n class=\"log__rule-ic\"\n title=\"下移\"\n :disabled=\"idx === rules.length - 1\"\n @click=\"moveRule(r.id, 1)\"\n >\n <Icon name=\"arrowDown\" :size=\"12\" />\n </button>\n <button\n type=\"button\"\n class=\"log__rule-ic log__rule-del\"\n title=\"删除\"\n @click=\"removeRule(r.id)\"\n >\n <Icon name=\"trash\" :size=\"12\" />\n </button>\n </div>\n <button type=\"button\" class=\"log__rules-add\" @click=\"addRule\">\n <Icon name=\"plus\" :size=\"12\" /> 添加规则\n </button>\n </div>\n </div>\n\n <!-- virtual list: a fixed-height sizer drives the scrollbar; only the visible window is mounted -->\n <div ref=\"scrollEl\" class=\"log__scroll\" @scroll=\"onScroll\">\n <div class=\"log__sizer\" :style=\"{ height: total * LINE_H + 'px' }\">\n <div class=\"log__rows\" :style=\"{ paddingTop: startIdx * LINE_H + 'px' }\">\n <div\n v-for=\"row in visible\"\n :key=\"row.i\"\n class=\"log__row\"\n :class=\"{\n log__gaprow: row.isGap,\n 'log__row--active': !row.isGap && row.n - 1 === activeOrig,\n 'log__row--header': !row.isGap && row.kind === 'header',\n 'log__row--frame': !row.isGap && row.kind === 'frame',\n 'log__row--context': row.context,\n }\"\n :style=\"\n !row.isGap && row.style && row.style.bg ? { background: row.style.bg } : undefined\n \"\n >\n <!-- gap expander (filter mode): reveal hidden context around matches -->\n <template v-if=\"row.isGap\">\n <button\n v-if=\"row.size > STEP\"\n type=\"button\"\n class=\"log__gapbtn\"\n :title=\"`向下展开 ${row.step} 行(上方匹配之后)`\"\n @click=\"reveal(row.top, row.bottom, 'down')\"\n >\n <Icon name=\"chevronDown\" :size=\"12\" />\n </button>\n <button\n type=\"button\"\n class=\"log__gapmain\"\n :title=\"`展开全部 ${row.size} 行`\"\n @click=\"reveal(row.top, row.bottom, 'all')\"\n >\n ⋯ {{ row.size }} 行\n </button>\n <button\n v-if=\"row.size > STEP\"\n type=\"button\"\n class=\"log__gapbtn log__gapbtn--up\"\n :title=\"`向上展开 ${row.step} 行(下方匹配之前)`\"\n @click=\"reveal(row.top, row.bottom, 'up')\"\n >\n <Icon name=\"chevronDown\" :size=\"12\" />\n </button>\n </template>\n\n <!-- content row -->\n <template v-else>\n <span class=\"log__ln\">{{ row.n }}</span>\n <span v-if=\"hasGroups\" class=\"log__fold-cell\">\n <button\n v-if=\"row.kind === 'header' && !inFilter\"\n type=\"button\"\n class=\"log__fold\"\n :title=\"row.collapsed ? '展开堆栈' : '折叠堆栈'\"\n @click=\"toggleGroup(row.gid)\"\n >\n <Icon\n name=\"chevronRight\"\n :size=\"12\"\n class=\"log__chev\"\n :class=\"{ 'log__chev--open': !row.collapsed }\"\n />\n </button>\n </span>\n <span\n class=\"log__tx\"\n :style=\"row.style && row.style.fg ? { color: row.style.fg } : undefined\"\n v-html=\"renderLine(row.text)\"\n />\n <button\n v-if=\"row.kind === 'header' && row.collapsed && !inFilter\"\n type=\"button\"\n class=\"log__badge\"\n title=\"展开堆栈\"\n @click=\"toggleGroup(row.gid)\"\n >\n ⋯ {{ row.count }} 帧\n </button>\n </template>\n </div>\n </div>\n </div>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.log {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--code-bg);\n}\n\n.log__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.log__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.log__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.log__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.log__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.log__bar {\n position: relative;\n display: flex;\n align-items: center;\n gap: 6px;\n flex-shrink: 0;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.log__searchwrap {\n position: relative;\n flex: 1 1 auto;\n min-width: 60px;\n display: flex;\n}\n.log__search {\n flex: 1 1 auto;\n min-width: 0;\n padding: 5px 28px 5px 10px;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--code-bg);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.log__search:focus,\n.log__goto:focus {\n outline: none;\n border-color: var(--accent);\n}\n.log__clear {\n position: absolute;\n top: 50%;\n right: 5px;\n transform: translateY(-50%);\n display: flex;\n align-items: center;\n justify-content: center;\n width: 19px;\n height: 19px;\n padding: 0;\n color: var(--faint);\n background: none;\n border: none;\n border-radius: 5px;\n cursor: pointer;\n}\n.log__clear:hover {\n color: var(--text);\n background: var(--surface-2);\n}\n.log__matches {\n flex-shrink: 0;\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--faint);\n min-width: 38px;\n text-align: center;\n}\n.log__nav {\n flex-shrink: 0;\n min-width: 26px;\n height: 26px;\n padding: 0 6px;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n cursor: pointer;\n}\n.log__nav:hover:not(:disabled) {\n border-color: var(--accent);\n color: var(--accent);\n}\n.log__nav:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.log__nav--on {\n color: var(--accent);\n border-color: var(--accent);\n background: var(--surface-2);\n}\n.log__spacer {\n flex: 1 1 auto;\n}\n.log__goto {\n flex-shrink: 0;\n width: 64px;\n padding: 5px 8px;\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--text);\n background: var(--code-bg);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n}\n.log__total {\n flex-shrink: 0;\n font-size: 11.5px;\n color: var(--faint);\n white-space: nowrap;\n}\n\n/* color-rules dropdown */\n.log__overlay {\n position: fixed;\n inset: 0;\n z-index: 40;\n background: transparent;\n}\n.log__rules {\n position: absolute;\n z-index: 41;\n top: calc(100% + 4px);\n right: 12px;\n width: min(520px, calc(100vw - 24px));\n max-height: 320px;\n overflow: auto;\n padding: 10px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 10px;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);\n}\n.log__rules-head {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 8px;\n}\n.log__rules-title {\n font-size: 12.5px;\n font-weight: 600;\n color: var(--text);\n}\n.log__rules-hint {\n font-size: 11px;\n color: var(--faint);\n}\n.log__rules-empty {\n font-size: 12px;\n color: var(--muted);\n line-height: 1.5;\n margin: 2px 0;\n}\n.log__rules-empty code {\n font-family: var(--font-mono);\n font-size: 11px;\n padding: 0 4px;\n background: var(--code-bg);\n border-radius: 4px;\n}\n.log__rule {\n display: flex;\n align-items: center;\n gap: 5px;\n}\n.log__rule-pat {\n flex: 1 1 auto;\n min-width: 80px;\n padding: 4px 8px;\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--text);\n background: var(--code-bg);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n}\n.log__rule-pat:focus {\n outline: none;\n border-color: var(--accent);\n}\n.log__rule-pat--bad {\n border-color: var(--danger);\n color: var(--danger);\n}\n.log__rule-tog {\n flex-shrink: 0;\n width: 26px;\n height: 26px;\n padding: 0;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--muted);\n background: var(--code-bg);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n cursor: pointer;\n}\n.log__rule-tog--on {\n color: var(--accent);\n border-color: var(--accent);\n background: var(--surface-2);\n}\n.log__rule-color {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 11px;\n color: var(--faint);\n cursor: pointer;\n}\n.log__rule-color input[type='color'] {\n width: 22px;\n height: 22px;\n padding: 0;\n border: 1px solid var(--border-2);\n border-radius: 5px;\n background: none;\n cursor: pointer;\n}\n.log__rule-color input[type='color']:disabled {\n opacity: 0.35;\n cursor: default;\n}\n.log__rule-ic {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n padding: 0;\n color: var(--muted);\n background: none;\n border: 1px solid transparent;\n border-radius: 6px;\n cursor: pointer;\n}\n.log__rule-ic:hover:not(:disabled) {\n color: var(--accent);\n border-color: var(--border-2);\n}\n.log__rule-ic:disabled {\n opacity: 0.3;\n cursor: default;\n}\n.log__rule-del:hover:not(:disabled) {\n color: var(--danger);\n}\n.log__rules-add {\n align-self: flex-start;\n display: inline-flex;\n align-items: center;\n gap: 5px;\n margin-top: 2px;\n padding: 4px 10px;\n font-family: inherit;\n font-size: 12px;\n color: var(--muted);\n background: var(--code-bg);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n cursor: pointer;\n}\n.log__rules-add:hover {\n color: var(--accent);\n border-color: var(--accent);\n}\n\n.log__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.log__sizer {\n position: relative;\n min-width: 100%;\n}\n.log__row {\n display: flex;\n align-items: flex-start;\n min-width: 100%;\n height: 19px;\n line-height: 19px;\n white-space: nowrap;\n font-family: var(--font-mono);\n font-size: 12.5px;\n}\n.log__row--active {\n background: color-mix(in srgb, var(--accent) 14%, transparent);\n}\n.log__row--context .log__tx {\n opacity: 0.66;\n}\n.log__ln {\n position: sticky;\n left: 0;\n flex-shrink: 0;\n width: 64px;\n padding-right: 12px;\n text-align: right;\n color: var(--faint);\n background: var(--code-bg);\n user-select: none;\n}\n.log__row--active .log__ln {\n background: color-mix(in srgb, var(--accent) 14%, var(--code-bg));\n}\n\n/* fold gutter — a fixed cell right after the line number, present for every row (only when the file has\n stacks) so header/frame/plain text all stay column-aligned. Holds the chevron on headers; draws a faint\n guide line down the left edge of frame rows to read the run as a nested block. */\n.log__fold-cell {\n flex-shrink: 0;\n width: 16px;\n align-self: stretch;\n display: flex;\n align-items: center;\n justify-content: center;\n user-select: none;\n}\n.log__row--frame .log__fold-cell {\n box-shadow: inset 1px 0 0 var(--border-2);\n}\n.log__fold {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n padding: 0;\n border: none;\n background: none;\n color: var(--faint);\n cursor: pointer;\n}\n.log__chev {\n transition: transform 0.12s ease;\n}\n.log__chev--open {\n transform: rotate(90deg);\n}\n.log__fold:hover .log__chev {\n color: var(--accent);\n}\n\n.log__tx {\n flex: 1 1 auto;\n padding-right: 18px;\n color: var(--text);\n white-space: pre;\n}\n.log__row--header .log__tx {\n color: var(--text);\n font-weight: 600;\n}\n.log__row--frame .log__tx {\n color: var(--muted);\n}\n.log__tx :deep(.log__hit) {\n background: #ffd43b;\n color: #1a1a1a;\n border-radius: 2px;\n}\n\n.log__badge {\n flex-shrink: 0;\n align-self: center;\n margin-left: 8px;\n padding: 0 8px;\n height: 15px;\n font-family: var(--font-mono);\n font-size: 10.5px;\n line-height: 13px;\n color: var(--faint);\n background: var(--surface-2);\n border: 1px solid var(--border-2);\n border-radius: 9px;\n cursor: pointer;\n user-select: none;\n}\n.log__badge:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* filter-mode context expander bar (one virtual row tall) */\n.log__gaprow {\n position: sticky;\n left: 0;\n align-items: center;\n justify-content: center;\n gap: 10px;\n background: var(--surface-2);\n color: var(--faint);\n user-select: none;\n}\n.log__gapbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 15px;\n padding: 0;\n color: var(--muted);\n background: none;\n border: none;\n cursor: pointer;\n}\n.log__gapbtn:hover {\n color: var(--accent);\n}\n.log__gapbtn--up {\n transform: rotate(180deg);\n}\n.log__gapmain {\n padding: 0 10px;\n height: 15px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--faint);\n background: none;\n border: none;\n cursor: pointer;\n}\n.log__gapmain:hover {\n color: var(--accent);\n}\n</style>\n"],"mappings":"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"}
@@ -5,3 +5,5 @@ import t from "./LrcViewer.vue_vue_type_script_setup_true_lang.js";
5
5
  var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-5cb50c8a"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
+
9
+ //# sourceMappingURL=LrcViewer.js.map