@lamberl-lee/file-preview 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COPYING +674 -0
- package/LICENSE +165 -0
- package/README.md +165 -0
- package/dist/AudioPreview.d.ts +9 -0
- package/dist/AudioPreview.js +29 -0
- package/dist/AudioPreview.js.map +1 -0
- package/dist/CodePreview.d.ts +10 -0
- package/dist/CodePreview.js +121 -0
- package/dist/CodePreview.js.map +1 -0
- package/dist/CsvPreview.d.ts +9 -0
- package/dist/CsvPreview.js +117 -0
- package/dist/CsvPreview.js.map +1 -0
- package/dist/DocxPreview.d.ts +11 -0
- package/dist/DocxPreview.js +89 -0
- package/dist/DocxPreview.js.map +1 -0
- package/dist/EpubPreview.d.ts +9 -0
- package/dist/EpubPreview.js +693 -0
- package/dist/EpubPreview.js.map +1 -0
- package/dist/HtmlPreview.d.ts +9 -0
- package/dist/HtmlPreview.js +60 -0
- package/dist/HtmlPreview.js.map +1 -0
- package/dist/ImagePreview.d.ts +9 -0
- package/dist/ImagePreview.js +44 -0
- package/dist/ImagePreview.js.map +1 -0
- package/dist/LargeFileGate.d.ts +12 -0
- package/dist/LargeFileGate.js +88 -0
- package/dist/LargeFileGate.js.map +1 -0
- package/dist/MarkdownPreview.d.ts +8 -0
- package/dist/MarkdownPreview.js +140 -0
- package/dist/MarkdownPreview.js.map +1 -0
- package/dist/PdfPreview.d.ts +11 -0
- package/dist/PdfPreview.js +206 -0
- package/dist/PdfPreview.js.map +1 -0
- package/dist/PlainTextLargePreview.d.ts +9 -0
- package/dist/PlainTextLargePreview.js +62 -0
- package/dist/PlainTextLargePreview.js.map +1 -0
- package/dist/PluginPreviewRenderer.d.ts +13 -0
- package/dist/PluginPreviewRenderer.js +89 -0
- package/dist/PluginPreviewRenderer.js.map +1 -0
- package/dist/PptxPreview.d.ts +16 -0
- package/dist/PptxPreview.js +376 -0
- package/dist/PptxPreview.js.map +1 -0
- package/dist/PreviewErrorBoundary.d.ts +29 -0
- package/dist/PreviewErrorBoundary.js +53 -0
- package/dist/PreviewErrorBoundary.js.map +1 -0
- package/dist/PreviewFallback.d.ts +18 -0
- package/dist/PreviewFallback.js +143 -0
- package/dist/PreviewFallback.js.map +1 -0
- package/dist/PreviewLoading.d.ts +8 -0
- package/dist/PreviewLoading.js +14 -0
- package/dist/PreviewLoading.js.map +1 -0
- package/dist/RtfPreview.d.ts +10 -0
- package/dist/RtfPreview.js +240 -0
- package/dist/RtfPreview.js.map +1 -0
- package/dist/ShikiSourceView.d.ts +11 -0
- package/dist/ShikiSourceView.js +112 -0
- package/dist/ShikiSourceView.js.map +1 -0
- package/dist/SvgPreview.d.ts +9 -0
- package/dist/SvgPreview.js +89 -0
- package/dist/SvgPreview.js.map +1 -0
- package/dist/TextPreview.d.ts +9 -0
- package/dist/TextPreview.js +9 -0
- package/dist/TextPreview.js.map +1 -0
- package/dist/VideoPreview.d.ts +9 -0
- package/dist/VideoPreview.js +18 -0
- package/dist/VideoPreview.js.map +1 -0
- package/dist/XlsxPreview.d.ts +12 -0
- package/dist/XlsxPreview.js +856 -0
- package/dist/XlsxPreview.js.map +1 -0
- package/dist/ZipPreview.d.ts +9 -0
- package/dist/ZipPreview.js +153 -0
- package/dist/ZipPreview.js.map +1 -0
- package/dist/core/binary.d.ts +10 -0
- package/dist/core/binary.js +27 -0
- package/dist/core/binary.js.map +1 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.js +19 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/download.d.ts +5 -0
- package/dist/core/download.js +20 -0
- package/dist/core/download.js.map +1 -0
- package/dist/core/i18n.d.ts +101 -0
- package/dist/core/i18n.js +200 -0
- package/dist/core/i18n.js.map +1 -0
- package/dist/core/plugin.d.ts +38 -0
- package/dist/core/plugin.js +46 -0
- package/dist/core/plugin.js.map +1 -0
- package/dist/core/registry.d.ts +13 -0
- package/dist/core/registry.js +33 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/source.d.ts +14 -0
- package/dist/core/source.js +131 -0
- package/dist/core/source.js.map +1 -0
- package/dist/core/types.d.ts +88 -0
- package/dist/core/types.js +27 -0
- package/dist/core/types.js.map +1 -0
- package/dist/hooks/useObjectUrlFromSource.d.ts +9 -0
- package/dist/hooks/useObjectUrlFromSource.js +50 -0
- package/dist/hooks/useObjectUrlFromSource.js.map +1 -0
- package/dist/hooks/useSourceBase64.d.ts +10 -0
- package/dist/hooks/useSourceBase64.js +32 -0
- package/dist/hooks/useSourceBase64.js.map +1 -0
- package/dist/hooks/useSourceText.d.ts +10 -0
- package/dist/hooks/useSourceText.js +32 -0
- package/dist/hooks/useSourceText.js.map +1 -0
- package/dist/icons.d.ts +44 -0
- package/dist/icons.js +248 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +147 -0
- package/dist/index.js.map +1 -0
- package/dist/limits.d.ts +26 -0
- package/dist/limits.js +45 -0
- package/dist/limits.js.map +1 -0
- package/dist/performance-limits.d.ts +27 -0
- package/dist/performance-limits.js +54 -0
- package/dist/performance-limits.js.map +1 -0
- package/dist/plugins/audio-plugin.d.ts +7 -0
- package/dist/plugins/audio-plugin.js +11 -0
- package/dist/plugins/audio-plugin.js.map +1 -0
- package/dist/plugins/builtin-plugins.d.ts +9 -0
- package/dist/plugins/builtin-plugins.js +43 -0
- package/dist/plugins/builtin-plugins.js.map +1 -0
- package/dist/plugins/csv-plugin.d.ts +7 -0
- package/dist/plugins/csv-plugin.js +11 -0
- package/dist/plugins/csv-plugin.js.map +1 -0
- package/dist/plugins/docx-plugin.d.ts +7 -0
- package/dist/plugins/docx-plugin.js +15 -0
- package/dist/plugins/docx-plugin.js.map +1 -0
- package/dist/plugins/epub-plugin.d.ts +7 -0
- package/dist/plugins/epub-plugin.js +15 -0
- package/dist/plugins/epub-plugin.js.map +1 -0
- package/dist/plugins/html-plugin.d.ts +7 -0
- package/dist/plugins/html-plugin.js +11 -0
- package/dist/plugins/html-plugin.js.map +1 -0
- package/dist/plugins/image-plugin.d.ts +7 -0
- package/dist/plugins/image-plugin.js +11 -0
- package/dist/plugins/image-plugin.js.map +1 -0
- package/dist/plugins/markdown-plugin.d.ts +7 -0
- package/dist/plugins/markdown-plugin.js +11 -0
- package/dist/plugins/markdown-plugin.js.map +1 -0
- package/dist/plugins/pdf-plugin.d.ts +7 -0
- package/dist/plugins/pdf-plugin.js +15 -0
- package/dist/plugins/pdf-plugin.js.map +1 -0
- package/dist/plugins/pptx-plugin.d.ts +7 -0
- package/dist/plugins/pptx-plugin.js +15 -0
- package/dist/plugins/pptx-plugin.js.map +1 -0
- package/dist/plugins/rtf-plugin.d.ts +7 -0
- package/dist/plugins/rtf-plugin.js +15 -0
- package/dist/plugins/rtf-plugin.js.map +1 -0
- package/dist/plugins/source-code-plugin.d.ts +7 -0
- package/dist/plugins/source-code-plugin.js +11 -0
- package/dist/plugins/source-code-plugin.js.map +1 -0
- package/dist/plugins/svg-plugin.d.ts +7 -0
- package/dist/plugins/svg-plugin.js +11 -0
- package/dist/plugins/svg-plugin.js.map +1 -0
- package/dist/plugins/text-plugin.d.ts +7 -0
- package/dist/plugins/text-plugin.js +11 -0
- package/dist/plugins/text-plugin.js.map +1 -0
- package/dist/plugins/video-plugin.d.ts +7 -0
- package/dist/plugins/video-plugin.js +11 -0
- package/dist/plugins/video-plugin.js.map +1 -0
- package/dist/plugins/xlsx-plugin.d.ts +7 -0
- package/dist/plugins/xlsx-plugin.js +15 -0
- package/dist/plugins/xlsx-plugin.js.map +1 -0
- package/dist/plugins/zip-plugin.d.ts +7 -0
- package/dist/plugins/zip-plugin.js +15 -0
- package/dist/plugins/zip-plugin.js.map +1 -0
- package/dist/preview-adapters/AudioPreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/AudioPreviewAdapter.js +31 -0
- package/dist/preview-adapters/AudioPreviewAdapter.js.map +1 -0
- package/dist/preview-adapters/CsvPreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/CsvPreviewAdapter.js +28 -0
- package/dist/preview-adapters/CsvPreviewAdapter.js.map +1 -0
- package/dist/preview-adapters/DocxPreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/DocxPreviewAdapter.js +16 -0
- package/dist/preview-adapters/DocxPreviewAdapter.js.map +1 -0
- package/dist/preview-adapters/EpubPreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/EpubPreviewAdapter.js +28 -0
- package/dist/preview-adapters/EpubPreviewAdapter.js.map +1 -0
- package/dist/preview-adapters/HtmlPreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/HtmlPreviewAdapter.js +28 -0
- package/dist/preview-adapters/HtmlPreviewAdapter.js.map +1 -0
- package/dist/preview-adapters/ImagePreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/ImagePreviewAdapter.js +31 -0
- package/dist/preview-adapters/ImagePreviewAdapter.js.map +1 -0
- package/dist/preview-adapters/MarkdownPreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/MarkdownPreviewAdapter.js +28 -0
- package/dist/preview-adapters/MarkdownPreviewAdapter.js.map +1 -0
- package/dist/preview-adapters/PdfPreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/PdfPreviewAdapter.js +16 -0
- package/dist/preview-adapters/PdfPreviewAdapter.js.map +1 -0
- package/dist/preview-adapters/PptxPreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/PptxPreviewAdapter.js +16 -0
- package/dist/preview-adapters/PptxPreviewAdapter.js.map +1 -0
- package/dist/preview-adapters/RtfPreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/RtfPreviewAdapter.js +54 -0
- package/dist/preview-adapters/RtfPreviewAdapter.js.map +1 -0
- package/dist/preview-adapters/SourceCodePreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/SourceCodePreviewAdapter.js +35 -0
- package/dist/preview-adapters/SourceCodePreviewAdapter.js.map +1 -0
- package/dist/preview-adapters/SvgPreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/SvgPreviewAdapter.js +28 -0
- package/dist/preview-adapters/SvgPreviewAdapter.js.map +1 -0
- package/dist/preview-adapters/TextPreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/TextPreviewAdapter.js +28 -0
- package/dist/preview-adapters/TextPreviewAdapter.js.map +1 -0
- package/dist/preview-adapters/UnsupportedPluginPreview.d.ts +11 -0
- package/dist/preview-adapters/UnsupportedPluginPreview.js +34 -0
- package/dist/preview-adapters/UnsupportedPluginPreview.js.map +1 -0
- package/dist/preview-adapters/VideoPreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/VideoPreviewAdapter.js +31 -0
- package/dist/preview-adapters/VideoPreviewAdapter.js.map +1 -0
- package/dist/preview-adapters/XlsxPreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/XlsxPreviewAdapter.js +17 -0
- package/dist/preview-adapters/XlsxPreviewAdapter.js.map +1 -0
- package/dist/preview-adapters/ZipPreviewAdapter.d.ts +8 -0
- package/dist/preview-adapters/ZipPreviewAdapter.js +28 -0
- package/dist/preview-adapters/ZipPreviewAdapter.js.map +1 -0
- package/dist/remote-url.d.ts +20 -0
- package/dist/remote-url.js +478 -0
- package/dist/remote-url.js.map +1 -0
- package/dist/rtf/load-rtfjs.d.ts +42 -0
- package/dist/rtf/load-rtfjs.js +71 -0
- package/dist/rtf/load-rtfjs.js.map +1 -0
- package/dist/rtf/normalize-codepage.d.ts +33 -0
- package/dist/rtf/normalize-codepage.js +88 -0
- package/dist/rtf/normalize-codepage.js.map +1 -0
- package/dist/shiki.d.ts +35 -0
- package/dist/shiki.js +128 -0
- package/dist/shiki.js.map +1 -0
- package/dist/styles/AudioPreview.css +35 -0
- package/dist/styles/CsvPreview.css +106 -0
- package/dist/styles/DocxPreview.css +93 -0
- package/dist/styles/EpubPreview.css +509 -0
- package/dist/styles/HtmlPreview.css +15 -0
- package/dist/styles/ImagePreview.css +45 -0
- package/dist/styles/LargeFileGate.css +55 -0
- package/dist/styles/MarkdownPreview.css +291 -0
- package/dist/styles/PdfPreview.css +68 -0
- package/dist/styles/PlainTextLargePreview.css +85 -0
- package/dist/styles/PluginDebugBar.css +30 -0
- package/dist/styles/PptxPreview.css +207 -0
- package/dist/styles/PreviewFallback.css +88 -0
- package/dist/styles/PreviewLoading.css +13 -0
- package/dist/styles/RtfPreview.css +99 -0
- package/dist/styles/ShikiSourceView.css +159 -0
- package/dist/styles/SvgPreview.css +99 -0
- package/dist/styles/VideoPreview.css +19 -0
- package/dist/styles/ViewModeBar.css +38 -0
- package/dist/styles/XlsxPreview.css +361 -0
- package/dist/styles/ZipPreview.css +86 -0
- package/dist/styles/base.css +238 -0
- package/dist/styles/index.css +39 -0
- package/dist/support-status.d.ts +19 -0
- package/dist/support-status.js +174 -0
- package/dist/support-status.js.map +1 -0
- package/dist/utils.d.ts +17 -0
- package/dist/utils.js +205 -0
- package/dist/utils.js.map +1 -0
- package/package.json +125 -0
- package/scripts/copy-pdf-worker.mjs +31 -0
- package/scripts/copy-rtfjs-bundles.mjs +49 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/EpubPreview.tsx"],"sourcesContent":["\nimport { useEffect, useState, useCallback, useRef } from \"react\";\nimport JSZip from \"jszip\";\nimport {\n BookOpenIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n ListIcon,\n SearchIcon,\n XIcon,\n ChevronDownIcon,\n} from \"./icons\";\nimport { base64ToUint8Array } from \"./utils\";\nimport { useLocale } from \"./core/i18n\";\nimport \"./styles/EpubPreview.css\";\n\ninterface EpubPreviewProps {\n content: string; // base64 encoded\n fileName: string;\n}\n\ninterface EpubChapter {\n index: number;\n title: string;\n htmlContent: string;\n filePath: string;\n}\n\ninterface TocItem {\n title: string;\n src: string;\n children?: TocItem[];\n}\n\nasync function parseEpub(base64Content: string): Promise<{\n title: string;\n author: string;\n chapters: EpubChapter[];\n toc: TocItem[];\n stylesheets: string[];\n imageMap: Record<string, string>; // relative path -> blob URL\n}> {\n const bytes = base64ToUint8Array(base64Content);\n\n const zip = await JSZip.loadAsync(bytes);\n const chapters: EpubChapter[] = [];\n const toc: TocItem[] = [];\n let bookTitle = \"\";\n let bookAuthor = \"\";\n const stylesheets: string[] = [];\n const imageMap: Record<string, string> = {};\n\n // Helper: extract XML tag attributes (order-independent)\n function getAttr(tag: string, attrName: string): string | null {\n const re = new RegExp(`${attrName}=\"([^\"]*)\"`, \"i\");\n const m = tag.match(re);\n return m ? m[1] : null;\n }\n\n // Helper: resolve relative path against a base directory\n function resolvePath(baseDir: string, relativePath: string): string {\n if (relativePath.startsWith(\"/\")) return relativePath.substring(1);\n // Decode URL-encoded paths (e.g. %20 -> space)\n const decoded = decodeURIComponent(relativePath);\n // Handle anchor-only references\n const pathPart = decoded.split(\"#\")[0];\n if (!pathPart) return baseDir;\n const baseParts = baseDir.split(\"/\").filter(Boolean);\n const relParts = pathPart.split(\"/\");\n for (const part of relParts) {\n if (part === \"..\") {\n baseParts.pop();\n } else if (part !== \".\" && part !== \"\") {\n baseParts.push(part);\n }\n }\n return baseParts.join(\"/\");\n }\n\n // Helper: try to find a file in the zip with flexible matching\n function findZipFile(path: string): JSZip.JSZipObject | null {\n // Try exact match first\n let f = zip.file(path);\n if (f) return f;\n // Try with URL decoding\n try {\n f = zip.file(decodeURIComponent(path));\n if (f) return f;\n } catch {}\n // Try case-insensitive search\n const lowerPath = path.toLowerCase();\n let found: JSZip.JSZipObject | null = null;\n zip.forEach((p, file) => {\n if (!found && p.toLowerCase() === lowerPath) {\n found = file;\n }\n });\n return found;\n }\n\n // ── Step 1: Build image map (blob URLs for all images) ──\n const imageExtensions = [\".jpg\", \".jpeg\", \".png\", \".gif\", \".svg\", \".webp\", \".bmp\"];\n const imagePromises: Promise<void>[] = [];\n zip.forEach((path, file) => {\n if (file.dir) return;\n const lowerPath = path.toLowerCase();\n if (imageExtensions.some((ext) => lowerPath.endsWith(ext))) {\n imagePromises.push(\n (async () => {\n try {\n const blob = await file.async(\"blob\");\n const url = URL.createObjectURL(blob);\n // Map both full path and filename-only\n imageMap[path] = url;\n const filename = path.split(\"/\").pop()!;\n imageMap[filename] = url;\n // Also store lowercase versions for case-insensitive matching\n imageMap[lowerPath] = url;\n imageMap[filename.toLowerCase()] = url;\n } catch {\n // skip unreadable images\n }\n })()\n );\n }\n });\n await Promise.all(imagePromises);\n\n // ── Step 2: Parse container.xml → OPF ──\n const containerFile = zip.file(\"META-INF/container.xml\");\n if (!containerFile) {\n throw new Error(\"Invalid EPUB: missing container.xml\");\n }\n\n const containerXml = await containerFile.async(\"string\");\n const rootfileMatch = containerXml.match(/full-path=\"([^\"]+)\"/);\n if (!rootfileMatch) {\n throw new Error(\"Invalid EPUB: no rootfile in container.xml\");\n }\n\n const opfPath = rootfileMatch[1];\n const opfFile = findZipFile(opfPath);\n if (!opfFile) {\n throw new Error(\"Invalid EPUB: OPF file not found\");\n }\n\n const opfXml = await opfFile.async(\"string\");\n const opfDir = opfPath.includes(\"/\")\n ? opfPath.substring(0, opfPath.lastIndexOf(\"/\") + 1)\n : \"\";\n\n // Extract title & author\n const titleMatch = opfXml.match(/<dc:title[^>]*>([\\s\\S]*?)<\\/dc:title>/);\n if (titleMatch) bookTitle = titleMatch[1].trim();\n const authorMatch = opfXml.match(/<dc:creator[^>]*>([\\s\\S]*?)<\\/dc:creator>/);\n if (authorMatch) bookAuthor = authorMatch[1].trim();\n\n // ── Step 3: Parse manifest (order-independent attribute extraction) ──\n const manifestMatch = opfXml.match(/<manifest[^>]*>([\\s\\S]*?)<\\/manifest>/);\n const manifestMap: Record<string, { href: string; mediaType: string }> = {};\n const manifestHrefToId: Record<string, string> = {};\n if (manifestMatch) {\n const itemRegex = /<item\\s+([^>]+?)\\/?>/g;\n let itemMatch;\n while ((itemMatch = itemRegex.exec(manifestMatch[1])) !== null) {\n const attrs = itemMatch[1];\n const id = getAttr(attrs, \"id\");\n const href = getAttr(attrs, \"href\");\n const mediaType = getAttr(attrs, \"media-type\") || \"\";\n if (id && href) {\n manifestMap[id] = { href, mediaType };\n manifestHrefToId[href] = id;\n }\n }\n }\n\n // ── Step 4: Parse spine (reading order) ──\n const spineMatch = opfXml.match(/<spine[^>]*>([\\s\\S]*?)<\\/spine>/);\n const spineItems: string[] = [];\n if (spineMatch) {\n const itemrefRegex = /<itemref\\s+([^>]+?)\\/?>/g;\n let refMatch;\n while ((refMatch = itemrefRegex.exec(spineMatch[1])) !== null) {\n const idref = getAttr(refMatch[1], \"idref\");\n if (idref) spineItems.push(idref);\n }\n }\n\n // ── Step 5: Parse TOC from toc.ncx ──\n const spineTocAttr = spineMatch\n ? (spineMatch[0].match(/toc=\"([^\"]+)\"/)?.[1] ?? null)\n : null;\n if (spineTocAttr) {\n const tocInfo = manifestMap[spineTocAttr];\n if (tocInfo) {\n const tocPath = resolvePath(opfDir, tocInfo.href);\n const tocFile = findZipFile(tocPath);\n if (tocFile) {\n const tocXml = await tocFile.async(\"string\");\n // Parse navPoints recursively\n function parseNavPoints(xml: string, parentPath: string): TocItem[] {\n const items: TocItem[] = [];\n const navRegex = /<navPoint[^>]*>([\\s\\S]*?)<\\/navPoint>/g;\n let navMatch;\n while ((navMatch = navRegex.exec(xml)) !== null) {\n const block = navMatch[1];\n const labelMatch = block.match(\n /<navLabel[^>]*>[\\s\\S]*?<text>([^<]*)<\\/text>/\n );\n const contentMatch = block.match(/<content\\s+src=\"([^\"]+)\"/);\n if (labelMatch && contentMatch) {\n const src = contentMatch[1].split(\"#\")[0]; // remove anchor\n const item: TocItem = {\n title: labelMatch[1].trim(),\n src: resolvePath(parentPath, src),\n };\n // Parse children\n const children = parseNavPoints(block, parentPath);\n if (children.length > 0) item.children = children;\n items.push(item);\n }\n }\n return items;\n }\n const parsed = parseNavPoints(tocXml, opfDir);\n toc.push(...parsed);\n }\n }\n }\n\n // ── Step 6: Collect CSS stylesheets ──\n for (const [, info] of Object.entries(manifestMap)) {\n if (info.mediaType === \"text/css\" || info.href.endsWith(\".css\")) {\n const cssPath = resolvePath(opfDir, info.href);\n const cssFile = findZipFile(cssPath);\n if (cssFile) {\n try {\n const cssText = await cssFile.async(\"string\");\n stylesheets.push(cssText);\n } catch {\n // skip unreadable CSS\n }\n }\n }\n }\n\n // ── Step 7: Build a TOC src → title map for better chapter titles ──\n const tocTitleMap: Record<string, string> = {};\n function flattenToc(items: TocItem[]) {\n for (const item of items) {\n if (item.title) {\n tocTitleMap[item.src] = item.title;\n // Also map just the filename\n const filename = item.src.split(\"/\").pop()!;\n tocTitleMap[filename] = item.title;\n }\n if (item.children) flattenToc(item.children);\n }\n }\n flattenToc(toc);\n\n // ── Step 8: Read chapters in spine order ──\n for (let i = 0; i < spineItems.length; i++) {\n const idref = spineItems[i];\n const info = manifestMap[idref];\n if (!info) continue;\n\n const filePath = resolvePath(opfDir, info.href);\n const chapterFile = findZipFile(filePath);\n if (!chapterFile) continue;\n\n const chapterXml = await chapterFile.async(\"string\");\n\n // Get chapter title: TOC > h1/h2/h3 > <title> > fallback\n let chapterTitle = `Chapter ${i + 1}`;\n // Try TOC first (most accurate)\n const tocTitle =\n tocTitleMap[filePath] ||\n tocTitleMap[filePath.toLowerCase()] ||\n tocTitleMap[info.href] ||\n tocTitleMap[info.href.toLowerCase()];\n if (tocTitle) {\n chapterTitle = tocTitle;\n } else {\n // Try h1/h2/h3\n const hMatch = chapterXml.match(/<h[1-3][^>]*>([\\s\\S]*?)<\\/h[1-3]>/);\n if (hMatch && hMatch[1].trim()) {\n // Strip HTML tags from heading\n chapterTitle = hMatch[1].replace(/<[^>]+>/g, \"\").trim();\n } else {\n const titleTagMatch = chapterXml.match(/<title>([^<]*)<\\/title>/);\n if (titleTagMatch && titleTagMatch[1].trim()) {\n chapterTitle = titleTagMatch[1].trim();\n }\n }\n }\n\n // Extract body content\n const bodyMatch = chapterXml.match(/<body[^>]*>([\\s\\S]*?)<\\/body>/);\n let bodyHtml = bodyMatch ? bodyMatch[1] : chapterXml;\n\n // Clean up: remove scripts only, keep styles for proper rendering\n bodyHtml = bodyHtml\n .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, \"\")\n // Remove page-break divs that calibre adds\n .replace(/<div\\s+class=\"mbp_pagebreak\"[^>]*><\\/div>/gi, \"\");\n\n // Resolve relative image paths in HTML to blob URLs\n const chapterDir = filePath.includes(\"/\")\n ? filePath.substring(0, filePath.lastIndexOf(\"/\") + 1)\n : \"\";\n bodyHtml = bodyHtml.replace(\n /(<img\\s+[^>]*?)src=\"([^\"]+)\"/gi,\n (_match, prefix, src) => {\n // Skip external URLs\n if (src.startsWith(\"http://\") || src.startsWith(\"https://\") || src.startsWith(\"data:\")) {\n return `${prefix}src=\"${src}\"`;\n }\n const resolvedSrc = resolvePath(chapterDir, src);\n const blobUrl =\n imageMap[resolvedSrc] ||\n imageMap[resolvedSrc.toLowerCase()] ||\n imageMap[src] ||\n imageMap[src.toLowerCase()] ||\n imageMap[src.split(\"/\").pop()!] ||\n imageMap[src.split(\"/\").pop()!.toLowerCase()];\n if (blobUrl) {\n return `${prefix}src=\"${blobUrl}\"`;\n }\n return `${prefix}src=\"${src}\"`;\n }\n );\n\n // Resolve relative anchor links to chapter navigation\n bodyHtml = bodyHtml.replace(\n /(<a\\s+[^>]*?)href=\"([^\"]+)\"/gi,\n (_match, prefix, href) => {\n if (\n href.startsWith(\"http://\") ||\n href.startsWith(\"https://\") ||\n href.startsWith(\"#\") ||\n href.startsWith(\"mailto:\")\n ) {\n return `${prefix}href=\"${href}\"`;\n }\n // Mark internal links with data attribute for handling\n const resolvedHref = resolvePath(chapterDir, href.split(\"#\")[0]);\n const anchor = href.includes(\"#\") ? \"#\" + href.split(\"#\")[1] : \"\";\n return `${prefix}href=\"${resolvedHref}${anchor}\" data-epub-link=\"true\"`;\n }\n );\n\n chapters.push({\n index: i,\n title: chapterTitle,\n htmlContent: bodyHtml,\n filePath,\n });\n }\n\n // ── Fallback: if no chapters via OPF, scan all HTML files ──\n if (chapters.length === 0) {\n const htmlFiles: string[] = [];\n zip.forEach((path) => {\n if (path.endsWith(\".html\") || path.endsWith(\".xhtml\") || path.endsWith(\".htm\")) {\n htmlFiles.push(path);\n }\n });\n htmlFiles.sort();\n\n for (let i = 0; i < htmlFiles.length; i++) {\n const file = zip.file(htmlFiles[i]);\n if (!file) continue;\n\n const html = await file.async(\"string\");\n const bodyMatch = html.match(/<body[^>]*>([\\s\\S]*?)<\\/body>/);\n let bodyHtml = (bodyMatch ? bodyMatch[1] : html)\n .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, \"\")\n .replace(/<div\\s+class=\"mbp_pagebreak\"[^>]*><\\/div>/gi, \"\");\n\n // Resolve images in fallback mode too\n const chapterDir = htmlFiles[i].includes(\"/\")\n ? htmlFiles[i].substring(0, htmlFiles[i].lastIndexOf(\"/\") + 1)\n : \"\";\n bodyHtml = bodyHtml.replace(\n /(<img\\s+[^>]*?)src=\"([^\"]+)\"/gi,\n (_match, prefix, src) => {\n if (src.startsWith(\"http://\") || src.startsWith(\"https://\") || src.startsWith(\"data:\")) {\n return `${prefix}src=\"${src}\"`;\n }\n const resolvedSrc = resolvePath(chapterDir, src);\n const blobUrl =\n imageMap[resolvedSrc] ||\n imageMap[resolvedSrc.toLowerCase()] ||\n imageMap[src] ||\n imageMap[src.toLowerCase()];\n if (blobUrl) {\n return `${prefix}src=\"${blobUrl}\"`;\n }\n return `${prefix}src=\"${src}\"`;\n }\n );\n\n // Try TOC for title\n const tocTitle =\n tocTitleMap[htmlFiles[i]] || tocTitleMap[htmlFiles[i].toLowerCase()];\n const hMatch = html.match(/<h[1-3][^>]*>([\\s\\S]*?)<\\/h[1-3]>/);\n let chTitle = `Chapter ${i + 1}`;\n if (tocTitle) {\n chTitle = tocTitle;\n } else if (hMatch && hMatch[1].trim()) {\n chTitle = hMatch[1].replace(/<[^>]+>/g, \"\").trim();\n }\n\n chapters.push({\n index: i,\n title: chTitle,\n htmlContent: bodyHtml,\n filePath: htmlFiles[i],\n });\n }\n }\n\n return { title: bookTitle, author: bookAuthor, chapters, toc, stylesheets, imageMap };\n}\n\nexport function EpubPreview({ content, fileName }: EpubPreviewProps) {\n const [bookData, setBookData] = useState<Awaited<ReturnType<typeof parseEpub>> | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [currentChapter, setCurrentChapter] = useState(0);\n const [showToc, setShowToc] = useState(false);\n const [showChapterDropdown, setShowChapterDropdown] = useState(false);\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [searchResults, setSearchResults] = useState<{ chapterIndex: number; snippet: string }[]>([]);\n const contentRef = useRef<HTMLDivElement>(null);\n const dropdownRef = useRef<HTMLDivElement>(null);\n const t = useLocale();\n\n // Reset state when content changes — derived state during render\n const [prevContent, setPrevContent] = useState(content);\n if (prevContent !== content) {\n setPrevContent(content);\n setLoading(true);\n setError(null);\n setBookData(null);\n setCurrentChapter(0);\n setSearchResults([]);\n }\n\n useEffect(() => {\n let cancelled = false;\n parseEpub(content).then(\n (result) => {\n if (cancelled) return;\n setBookData(result);\n setLoading(false);\n },\n (err) => {\n if (cancelled) return;\n console.error(\"Error parsing EPUB:\", err);\n setError(err instanceof Error ? err.message : \"Failed to parse e-book\");\n setLoading(false);\n }\n );\n return () => {\n cancelled = true;\n };\n }, [content]);\n\n // Close dropdown when clicking outside\n useEffect(() => {\n function handleClickOutside(e: MouseEvent) {\n if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {\n setShowChapterDropdown(false);\n }\n }\n document.addEventListener(\"mousedown\", handleClickOutside);\n return () => document.removeEventListener(\"mousedown\", handleClickOutside);\n }, []);\n\n // Cleanup blob URLs on unmount\n useEffect(() => {\n return () => {\n if (bookData?.imageMap) {\n Object.values(bookData.imageMap).forEach((url) => {\n if (url.startsWith(\"blob:\")) URL.revokeObjectURL(url);\n });\n }\n };\n }, [bookData?.imageMap]);\n\n // Scroll to top when chapter changes\n useEffect(() => {\n if (contentRef.current) {\n contentRef.current.scrollTop = 0;\n }\n }, [currentChapter]);\n\n // Search functionality\n const handleSearch = useCallback(\n (query: string) => {\n setSearchQuery(query);\n if (!bookData || !query.trim()) {\n setSearchResults([]);\n return;\n }\n const lowerQuery = query.toLowerCase();\n const results: { chapterIndex: number; snippet: string }[] = [];\n for (let i = 0; i < bookData.chapters.length; i++) {\n // Strip HTML for text search\n const text = bookData.chapters[i].htmlContent.replace(/<[^>]+>/g, \"\");\n const idx = text.toLowerCase().indexOf(lowerQuery);\n if (idx !== -1) {\n const start = Math.max(0, idx - 30);\n const end = Math.min(text.length, idx + query.length + 30);\n const snippet =\n (start > 0 ? \"...\" : \"\") +\n text.substring(start, end) +\n (end < text.length ? \"...\" : \"\");\n results.push({ chapterIndex: i, snippet });\n if (results.length >= 20) break; // limit results\n }\n }\n setSearchResults(results);\n },\n [bookData]\n );\n\n // Handle internal EPUB link clicks\n const handleContentClick = useCallback(\n (e: React.MouseEvent) => {\n const target = e.target as HTMLElement;\n const anchor = target.closest(\"a\") as HTMLAnchorElement | null;\n if (!anchor || !bookData) return;\n\n const isEpubLink = anchor.getAttribute(\"data-epub-link\") === \"true\";\n if (isEpubLink) {\n e.preventDefault();\n const href = anchor.getAttribute(\"href\") || \"\";\n const targetPath = href.split(\"#\")[0];\n // Find the chapter that matches this path\n const chapterIdx = bookData.chapters.findIndex(\n (ch) =>\n ch.filePath === targetPath ||\n ch.filePath.toLowerCase() === targetPath.toLowerCase()\n );\n if (chapterIdx !== -1) {\n setCurrentChapter(chapterIdx);\n setTimeout(() => {\n if (contentRef.current) contentRef.current.scrollTop = 0;\n }, 100);\n }\n }\n },\n [bookData]\n );\n\n if (loading) {\n return (\n <div className=\"fv-epub__state fv-epub__state--loading\">\n <div className=\"fv-spinner fv-spinner--lg\" />\n <p className=\"fv-epub__state-msg\">{t.loadingEbook}</p>\n </div>\n );\n }\n\n if (error || !bookData) {\n return (\n <div className=\"fv-epub__state fv-epub__state--error\">\n <BookOpenIcon size={48} className=\"fv-epub__state-icon\" />\n <p className=\"fv-epub__state-title\">{t.ebookLoadFailed}</p>\n <p className=\"fv-epub__state-msg\">{error || t.unknownError}</p>\n </div>\n );\n }\n\n if (bookData.chapters.length === 0) {\n return (\n <div className=\"fv-epub__state fv-epub__state--empty\">\n <BookOpenIcon size={48} />\n <p className=\"fv-epub__state-title\">{t.noChaptersFound}</p>\n </div>\n );\n }\n\n const chapter = bookData.chapters[currentChapter];\n\n // Build combined CSS for inline injection\n // Override any absolute positioning or overflow issues in book CSS\n const combinedCss = bookData.stylesheets.join(\"\\n\") + `\n /* Override book styles that may cause horizontal overflow */\n .fv-epub__article * {\n max-width: 100% !important;\n overflow-wrap: break-word !important;\n word-wrap: break-word !important;\n }\n .fv-epub__article pre, .fv-epub__article code {\n overflow-x: auto !important;\n max-width: 100% !important;\n }\n .fv-epub__article img {\n max-width: 100% !important;\n height: auto !important;\n }\n .fv-epub__article table {\n display: block;\n overflow-x: auto;\n max-width: 100%;\n }\n `;\n\n return (\n <div className=\"fv-epub\">\n {/* Book info bar */}\n <div className=\"fv-epub__info-bar\">\n <div className=\"fv-epub__info-row\">\n <div className=\"fv-epub__info-left\">\n <BookOpenIcon size={14} className=\"fv-epub__info-icon\" />\n <span className=\"fv-epub__info-title\">\n {bookData.title || fileName}\n </span>\n {bookData.author && (\n <span className=\"fv-epub__info-author\">\n — {bookData.author}\n </span>\n )}\n </div>\n <div className=\"fv-epub__info-right\">\n {/* Search */}\n <div className=\"fv-epub__search-wrap\">\n <SearchIcon size={14} className=\"fv-epub__search-icon\" />\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => handleSearch(e.target.value)}\n placeholder={t.searchPlaceholder}\n className=\"fv-epub__search-input\"\n />\n {searchQuery && (\n <button\n onClick={() => {\n setSearchQuery(\"\");\n setSearchResults([]);\n }}\n className=\"fv-epub__search-clear\"\n >\n <XIcon size={12} />\n </button>\n )}\n </div>\n {/* TOC toggle */}\n <button\n onClick={() => setShowToc(!showToc)}\n className={`fv-epub__toc-btn ${showToc ? \"fv-epub__toc-btn--active\" : \"\"}`}\n title={t.tableOfContents}\n >\n <ListIcon size={16} />\n </button>\n </div>\n </div>\n </div>\n\n {/* Search results */}\n {searchQuery && searchResults.length > 0 && (\n <div className=\"fv-epub__search-results\">\n <p className=\"fv-epub__search-results-label\">\n {t.foundInChapters.replace(\"{count}\", searchResults.length.toLocaleString())}\n </p>\n <div className=\"fv-epub__search-results-list\">\n {searchResults.map((r, i) => (\n <button\n key={i}\n onClick={() => {\n setCurrentChapter(r.chapterIndex);\n setSearchQuery(\"\");\n setSearchResults([]);\n }}\n className=\"fv-epub__search-result-item\"\n >\n <span className=\"fv-epub__search-result-title\">\n {bookData.chapters[r.chapterIndex].title}\n </span>\n <span className=\"fv-epub__search-result-snippet\">{r.snippet}</span>\n </button>\n ))}\n </div>\n </div>\n )}\n\n {searchQuery && searchResults.length === 0 && (\n <div className=\"fv-epub__search-empty\">\n <p className=\"fv-epub__search-empty-text\">{t.noResultsFound}</p>\n </div>\n )}\n\n <div className=\"fv-epub__body\">\n {/* TOC sidebar */}\n {showToc && (\n <div className=\"fv-epub__toc-sidebar\">\n <div className=\"fv-epub__toc-inner\">\n <h3 className=\"fv-epub__toc-heading\">\n {t.tableOfContents}\n </h3>\n <TocTree\n items={bookData.toc}\n chapters={bookData.chapters}\n currentChapter={currentChapter}\n onSelect={(idx) => {\n setCurrentChapter(idx);\n setShowChapterDropdown(false);\n }}\n />\n {/* Fallback: if no TOC items, show chapter list */}\n {bookData.toc.length === 0 && (\n <div className=\"fv-epub__toc-list\">\n {bookData.chapters.map((ch, i) => (\n <button\n key={i}\n onClick={() => setCurrentChapter(i)}\n className={`fv-epub__toc-item ${i === currentChapter ? \"fv-epub__toc-item--active\" : \"\"}`}\n >\n {ch.title}\n </button>\n ))}\n </div>\n )}\n </div>\n </div>\n )}\n\n {/* Main content area */}\n <div className=\"fv-epub__main\">\n {/* Compact chapter selector bar */}\n <div className=\"fv-epub__chapter-bar\">\n <button\n onClick={() => setCurrentChapter(Math.max(0, currentChapter - 1))}\n disabled={currentChapter === 0}\n className=\"fv-epub__chapter-nav-btn\"\n >\n <ChevronLeftIcon size={16} />\n </button>\n\n {/* Chapter dropdown */}\n <div className=\"fv-epub__dropdown-wrap\" ref={dropdownRef}>\n <button\n onClick={() => setShowChapterDropdown(!showChapterDropdown)}\n className=\"fv-epub__dropdown-trigger\"\n >\n <span className=\"fv-epub__dropdown-index\">\n {currentChapter + 1}/{bookData.chapters.length}\n </span>\n <span className=\"fv-epub__dropdown-title\">\n {chapter.title}\n </span>\n <ChevronDownIcon\n size={14}\n className={`fv-epub__dropdown-chevron ${showChapterDropdown ? \"fv-epub__dropdown-chevron--open\" : \"\"}`}\n />\n </button>\n\n {/* Dropdown list */}\n {showChapterDropdown && (\n <div className=\"fv-epub__dropdown-list\">\n {bookData.chapters.map((ch, i) => (\n <button\n key={i}\n onClick={() => {\n setCurrentChapter(i);\n setShowChapterDropdown(false);\n }}\n className={`fv-epub__dropdown-item ${i === currentChapter ? \"fv-epub__dropdown-item--active\" : \"\"}`}\n >\n <span className=\"fv-epub__dropdown-item-index\">{i + 1}.</span>\n {ch.title}\n </button>\n ))}\n </div>\n )}\n </div>\n\n <button\n onClick={() =>\n setCurrentChapter(Math.min(bookData.chapters.length - 1, currentChapter + 1))\n }\n disabled={currentChapter === bookData.chapters.length - 1}\n className=\"fv-epub__chapter-nav-btn\"\n >\n <ChevronRightIcon size={16} />\n </button>\n </div>\n\n {/* Chapter content */}\n <div className=\"fv-epub__content\" ref={contentRef} onClick={handleContentClick}>\n <div className=\"fv-epub__content-inner\">\n <h2 className=\"fv-epub__chapter-title\">{chapter.title}</h2>\n {/* Inject book CSS */}\n {combinedCss && (\n <style dangerouslySetInnerHTML={{ __html: combinedCss }} />\n )}\n <div\n className=\"fv-epub__article\"\n dangerouslySetInnerHTML={{ __html: chapter.htmlContent }}\n />\n </div>\n </div>\n\n {/* Navigation footer */}\n <div className=\"fv-epub__footer\">\n <button\n onClick={() => setCurrentChapter(Math.max(0, currentChapter - 1))}\n disabled={currentChapter === 0}\n className=\"fv-epub__footer-btn\"\n >\n <ChevronLeftIcon size={16} />\n <span className=\"fv-epub__footer-btn-text\">{t.previous}</span>\n </button>\n <span className=\"fv-epub__footer-label\">\n {currentChapter + 1} / {bookData.chapters.length}\n </span>\n <button\n onClick={() =>\n setCurrentChapter(Math.min(bookData.chapters.length - 1, currentChapter + 1))\n }\n disabled={currentChapter === bookData.chapters.length - 1}\n className=\"fv-epub__footer-btn\"\n >\n <span className=\"fv-epub__footer-btn-text\">{t.next}</span>\n <ChevronRightIcon size={16} />\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n// ── TOC Tree Component ──\nfunction TocTree({\n items,\n chapters,\n currentChapter,\n onSelect,\n depth = 0,\n}: {\n items: TocItem[];\n chapters: EpubChapter[];\n currentChapter: number;\n onSelect: (index: number) => void;\n depth?: number;\n}) {\n return (\n <div className={depth > 0 ? \"fv-epub__toc-indent\" : \"fv-epub__toc-list\"}>\n {items.map((item, i) => {\n // Find matching chapter index\n const chapterIdx = chapters.findIndex(\n (ch) =>\n ch.filePath === item.src ||\n ch.filePath.toLowerCase() === item.src.toLowerCase() ||\n item.src.endsWith(ch.filePath.split(\"/\").pop()!) ||\n ch.filePath.endsWith(item.src.split(\"/\").pop()!)\n );\n const isCurrent = chapterIdx === currentChapter;\n\n return (\n <div key={`${depth}-${i}`}>\n <button\n onClick={() => {\n if (chapterIdx !== -1) onSelect(chapterIdx);\n }}\n disabled={chapterIdx === -1}\n className={`fv-epub__toc-item ${isCurrent ? \"fv-epub__toc-item--active\" : \"\"}`}\n style={{ paddingLeft: `${8 + depth * 4}px` }}\n >\n {item.title}\n </button>\n {item.children && item.children.length > 0 && (\n <TocTree\n items={item.children}\n chapters={chapters}\n currentChapter={currentChapter}\n onSelect={onSelect}\n depth={depth + 1}\n />\n )}\n </div>\n );\n })}\n </div>\n );\n}\n"],"mappings":"AAgjBM,SACE,KADF;AA/iBN,SAAS,WAAW,UAAU,aAAa,cAAc;AACzD,OAAO,WAAW;AAClB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,0BAA0B;AACnC,SAAS,iBAAiB;AAC1B,OAAO;AAoBP,eAAe,UAAU,eAOtB;AACD,QAAM,QAAQ,mBAAmB,aAAa;AAE9C,QAAM,MAAM,MAAM,MAAM,UAAU,KAAK;AACvC,QAAM,WAA0B,CAAC;AACjC,QAAM,MAAiB,CAAC;AACxB,MAAI,YAAY;AAChB,MAAI,aAAa;AACjB,QAAM,cAAwB,CAAC;AAC/B,QAAM,WAAmC,CAAC;AAG1C,WAAS,QAAQ,KAAa,UAAiC;AAC7D,UAAM,KAAK,IAAI,OAAO,GAAG,QAAQ,cAAc,GAAG;AAClD,UAAM,IAAI,IAAI,MAAM,EAAE;AACtB,WAAO,IAAI,EAAE,CAAC,IAAI;AAAA,EACpB;AAGA,WAAS,YAAY,SAAiB,cAA8B;AAClE,QAAI,aAAa,WAAW,GAAG,EAAG,QAAO,aAAa,UAAU,CAAC;AAEjE,UAAM,UAAU,mBAAmB,YAAY;AAE/C,UAAM,WAAW,QAAQ,MAAM,GAAG,EAAE,CAAC;AACrC,QAAI,CAAC,SAAU,QAAO;AACtB,UAAM,YAAY,QAAQ,MAAM,GAAG,EAAE,OAAO,OAAO;AACnD,UAAM,WAAW,SAAS,MAAM,GAAG;AACnC,eAAW,QAAQ,UAAU;AAC3B,UAAI,SAAS,MAAM;AACjB,kBAAU,IAAI;AAAA,MAChB,WAAW,SAAS,OAAO,SAAS,IAAI;AACtC,kBAAU,KAAK,IAAI;AAAA,MACrB;AAAA,IACF;AACA,WAAO,UAAU,KAAK,GAAG;AAAA,EAC3B;AAGA,WAAS,YAAY,MAAwC;AAE3D,QAAI,IAAI,IAAI,KAAK,IAAI;AACrB,QAAI,EAAG,QAAO;AAEd,QAAI;AACF,UAAI,IAAI,KAAK,mBAAmB,IAAI,CAAC;AACrC,UAAI,EAAG,QAAO;AAAA,IAChB,QAAQ;AAAA,IAAC;AAET,UAAM,YAAY,KAAK,YAAY;AACnC,QAAI,QAAkC;AACtC,QAAI,QAAQ,CAAC,GAAG,SAAS;AACvB,UAAI,CAAC,SAAS,EAAE,YAAY,MAAM,WAAW;AAC3C,gBAAQ;AAAA,MACV;AAAA,IACF,CAAC;AACD,WAAO;AAAA,EACT;AAGA,QAAM,kBAAkB,CAAC,QAAQ,SAAS,QAAQ,QAAQ,QAAQ,SAAS,MAAM;AACjF,QAAM,gBAAiC,CAAC;AACxC,MAAI,QAAQ,CAAC,MAAM,SAAS;AAC1B,QAAI,KAAK,IAAK;AACd,UAAM,YAAY,KAAK,YAAY;AACnC,QAAI,gBAAgB,KAAK,CAAC,QAAQ,UAAU,SAAS,GAAG,CAAC,GAAG;AAC1D,oBAAc;AAAA,SACX,YAAY;AACX,cAAI;AACF,kBAAM,OAAO,MAAM,KAAK,MAAM,MAAM;AACpC,kBAAM,MAAM,IAAI,gBAAgB,IAAI;AAEpC,qBAAS,IAAI,IAAI;AACjB,kBAAM,WAAW,KAAK,MAAM,GAAG,EAAE,IAAI;AACrC,qBAAS,QAAQ,IAAI;AAErB,qBAAS,SAAS,IAAI;AACtB,qBAAS,SAAS,YAAY,CAAC,IAAI;AAAA,UACrC,QAAQ;AAAA,UAER;AAAA,QACF,GAAG;AAAA,MACL;AAAA,IACF;AAAA,EACF,CAAC;AACD,QAAM,QAAQ,IAAI,aAAa;AAG/B,QAAM,gBAAgB,IAAI,KAAK,wBAAwB;AACvD,MAAI,CAAC,eAAe;AAClB,UAAM,IAAI,MAAM,qCAAqC;AAAA,EACvD;AAEA,QAAM,eAAe,MAAM,cAAc,MAAM,QAAQ;AACvD,QAAM,gBAAgB,aAAa,MAAM,qBAAqB;AAC9D,MAAI,CAAC,eAAe;AAClB,UAAM,IAAI,MAAM,4CAA4C;AAAA,EAC9D;AAEA,QAAM,UAAU,cAAc,CAAC;AAC/B,QAAM,UAAU,YAAY,OAAO;AACnC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,kCAAkC;AAAA,EACpD;AAEA,QAAM,SAAS,MAAM,QAAQ,MAAM,QAAQ;AAC3C,QAAM,SAAS,QAAQ,SAAS,GAAG,IAC/B,QAAQ,UAAU,GAAG,QAAQ,YAAY,GAAG,IAAI,CAAC,IACjD;AAGJ,QAAM,aAAa,OAAO,MAAM,uCAAuC;AACvE,MAAI,WAAY,aAAY,WAAW,CAAC,EAAE,KAAK;AAC/C,QAAM,cAAc,OAAO,MAAM,2CAA2C;AAC5E,MAAI,YAAa,cAAa,YAAY,CAAC,EAAE,KAAK;AAGlD,QAAM,gBAAgB,OAAO,MAAM,uCAAuC;AAC1E,QAAM,cAAmE,CAAC;AAC1E,QAAM,mBAA2C,CAAC;AAClD,MAAI,eAAe;AACjB,UAAM,YAAY;AAClB,QAAI;AACJ,YAAQ,YAAY,UAAU,KAAK,cAAc,CAAC,CAAC,OAAO,MAAM;AAC9D,YAAM,QAAQ,UAAU,CAAC;AACzB,YAAM,KAAK,QAAQ,OAAO,IAAI;AAC9B,YAAM,OAAO,QAAQ,OAAO,MAAM;AAClC,YAAM,YAAY,QAAQ,OAAO,YAAY,KAAK;AAClD,UAAI,MAAM,MAAM;AACd,oBAAY,EAAE,IAAI,EAAE,MAAM,UAAU;AACpC,yBAAiB,IAAI,IAAI;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAGA,QAAM,aAAa,OAAO,MAAM,iCAAiC;AACjE,QAAM,aAAuB,CAAC;AAC9B,MAAI,YAAY;AACd,UAAM,eAAe;AACrB,QAAI;AACJ,YAAQ,WAAW,aAAa,KAAK,WAAW,CAAC,CAAC,OAAO,MAAM;AAC7D,YAAM,QAAQ,QAAQ,SAAS,CAAC,GAAG,OAAO;AAC1C,UAAI,MAAO,YAAW,KAAK,KAAK;AAAA,IAClC;AAAA,EACF;AAGA,QAAM,eAAe,aAChB,WAAW,CAAC,EAAE,MAAM,eAAe,IAAI,CAAC,KAAK,OAC9C;AACJ,MAAI,cAAc;AAChB,UAAM,UAAU,YAAY,YAAY;AACxC,QAAI,SAAS;AACX,YAAM,UAAU,YAAY,QAAQ,QAAQ,IAAI;AAChD,YAAM,UAAU,YAAY,OAAO;AACnC,UAAI,SAAS;AAGX,YAASA,kBAAT,SAAwB,KAAa,YAA+B;AAClE,gBAAM,QAAmB,CAAC;AAC1B,gBAAM,WAAW;AACjB,cAAI;AACJ,kBAAQ,WAAW,SAAS,KAAK,GAAG,OAAO,MAAM;AAC/C,kBAAM,QAAQ,SAAS,CAAC;AACxB,kBAAM,aAAa,MAAM;AAAA,cACvB;AAAA,YACF;AACA,kBAAM,eAAe,MAAM,MAAM,0BAA0B;AAC3D,gBAAI,cAAc,cAAc;AAC9B,oBAAM,MAAM,aAAa,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC;AACxC,oBAAM,OAAgB;AAAA,gBACpB,OAAO,WAAW,CAAC,EAAE,KAAK;AAAA,gBAC1B,KAAK,YAAY,YAAY,GAAG;AAAA,cAClC;AAEA,oBAAM,WAAWA,gBAAe,OAAO,UAAU;AACjD,kBAAI,SAAS,SAAS,EAAG,MAAK,WAAW;AACzC,oBAAM,KAAK,IAAI;AAAA,YACjB;AAAA,UACF;AACA,iBAAO;AAAA,QACT;AAvBS,6BAAAA;AAFT,cAAM,SAAS,MAAM,QAAQ,MAAM,QAAQ;AA0B3C,cAAM,SAASA,gBAAe,QAAQ,MAAM;AAC5C,YAAI,KAAK,GAAG,MAAM;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AAGA,aAAW,CAAC,EAAE,IAAI,KAAK,OAAO,QAAQ,WAAW,GAAG;AAClD,QAAI,KAAK,cAAc,cAAc,KAAK,KAAK,SAAS,MAAM,GAAG;AAC/D,YAAM,UAAU,YAAY,QAAQ,KAAK,IAAI;AAC7C,YAAM,UAAU,YAAY,OAAO;AACnC,UAAI,SAAS;AACX,YAAI;AACF,gBAAM,UAAU,MAAM,QAAQ,MAAM,QAAQ;AAC5C,sBAAY,KAAK,OAAO;AAAA,QAC1B,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,QAAM,cAAsC,CAAC;AAC7C,WAAS,WAAW,OAAkB;AACpC,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,OAAO;AACd,oBAAY,KAAK,GAAG,IAAI,KAAK;AAE7B,cAAM,WAAW,KAAK,IAAI,MAAM,GAAG,EAAE,IAAI;AACzC,oBAAY,QAAQ,IAAI,KAAK;AAAA,MAC/B;AACA,UAAI,KAAK,SAAU,YAAW,KAAK,QAAQ;AAAA,IAC7C;AAAA,EACF;AACA,aAAW,GAAG;AAGd,WAAS,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;AAC1C,UAAM,QAAQ,WAAW,CAAC;AAC1B,UAAM,OAAO,YAAY,KAAK;AAC9B,QAAI,CAAC,KAAM;AAEX,UAAM,WAAW,YAAY,QAAQ,KAAK,IAAI;AAC9C,UAAM,cAAc,YAAY,QAAQ;AACxC,QAAI,CAAC,YAAa;AAElB,UAAM,aAAa,MAAM,YAAY,MAAM,QAAQ;AAGnD,QAAI,eAAe,WAAW,IAAI,CAAC;AAEnC,UAAM,WACJ,YAAY,QAAQ,KACpB,YAAY,SAAS,YAAY,CAAC,KAClC,YAAY,KAAK,IAAI,KACrB,YAAY,KAAK,KAAK,YAAY,CAAC;AACrC,QAAI,UAAU;AACZ,qBAAe;AAAA,IACjB,OAAO;AAEL,YAAM,SAAS,WAAW,MAAM,mCAAmC;AACnE,UAAI,UAAU,OAAO,CAAC,EAAE,KAAK,GAAG;AAE9B,uBAAe,OAAO,CAAC,EAAE,QAAQ,YAAY,EAAE,EAAE,KAAK;AAAA,MACxD,OAAO;AACL,cAAM,gBAAgB,WAAW,MAAM,yBAAyB;AAChE,YAAI,iBAAiB,cAAc,CAAC,EAAE,KAAK,GAAG;AAC5C,yBAAe,cAAc,CAAC,EAAE,KAAK;AAAA,QACvC;AAAA,MACF;AAAA,IACF;AAGA,UAAM,YAAY,WAAW,MAAM,+BAA+B;AAClE,QAAI,WAAW,YAAY,UAAU,CAAC,IAAI;AAG1C,eAAW,SACR,QAAQ,qCAAqC,EAAE,EAE/C,QAAQ,+CAA+C,EAAE;AAG5D,UAAM,aAAa,SAAS,SAAS,GAAG,IACpC,SAAS,UAAU,GAAG,SAAS,YAAY,GAAG,IAAI,CAAC,IACnD;AACJ,eAAW,SAAS;AAAA,MAClB;AAAA,MACA,CAAC,QAAQ,QAAQ,QAAQ;AAEvB,YAAI,IAAI,WAAW,SAAS,KAAK,IAAI,WAAW,UAAU,KAAK,IAAI,WAAW,OAAO,GAAG;AACtF,iBAAO,GAAG,MAAM,QAAQ,GAAG;AAAA,QAC7B;AACA,cAAM,cAAc,YAAY,YAAY,GAAG;AAC/C,cAAM,UACJ,SAAS,WAAW,KACpB,SAAS,YAAY,YAAY,CAAC,KAClC,SAAS,GAAG,KACZ,SAAS,IAAI,YAAY,CAAC,KAC1B,SAAS,IAAI,MAAM,GAAG,EAAE,IAAI,CAAE,KAC9B,SAAS,IAAI,MAAM,GAAG,EAAE,IAAI,EAAG,YAAY,CAAC;AAC9C,YAAI,SAAS;AACX,iBAAO,GAAG,MAAM,QAAQ,OAAO;AAAA,QACjC;AACA,eAAO,GAAG,MAAM,QAAQ,GAAG;AAAA,MAC7B;AAAA,IACF;AAGA,eAAW,SAAS;AAAA,MAClB;AAAA,MACA,CAAC,QAAQ,QAAQ,SAAS;AACxB,YACE,KAAK,WAAW,SAAS,KACzB,KAAK,WAAW,UAAU,KAC1B,KAAK,WAAW,GAAG,KACnB,KAAK,WAAW,SAAS,GACzB;AACA,iBAAO,GAAG,MAAM,SAAS,IAAI;AAAA,QAC/B;AAEA,cAAM,eAAe,YAAY,YAAY,KAAK,MAAM,GAAG,EAAE,CAAC,CAAC;AAC/D,cAAM,SAAS,KAAK,SAAS,GAAG,IAAI,MAAM,KAAK,MAAM,GAAG,EAAE,CAAC,IAAI;AAC/D,eAAO,GAAG,MAAM,SAAS,YAAY,GAAG,MAAM;AAAA,MAChD;AAAA,IACF;AAEA,aAAS,KAAK;AAAA,MACZ,OAAO;AAAA,MACP,OAAO;AAAA,MACP,aAAa;AAAA,MACb;AAAA,IACF,CAAC;AAAA,EACH;AAGA,MAAI,SAAS,WAAW,GAAG;AACzB,UAAM,YAAsB,CAAC;AAC7B,QAAI,QAAQ,CAAC,SAAS;AACpB,UAAI,KAAK,SAAS,OAAO,KAAK,KAAK,SAAS,QAAQ,KAAK,KAAK,SAAS,MAAM,GAAG;AAC9E,kBAAU,KAAK,IAAI;AAAA,MACrB;AAAA,IACF,CAAC;AACD,cAAU,KAAK;AAEf,aAAS,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;AACzC,YAAM,OAAO,IAAI,KAAK,UAAU,CAAC,CAAC;AAClC,UAAI,CAAC,KAAM;AAEX,YAAM,OAAO,MAAM,KAAK,MAAM,QAAQ;AACtC,YAAM,YAAY,KAAK,MAAM,+BAA+B;AAC5D,UAAI,YAAY,YAAY,UAAU,CAAC,IAAI,MACxC,QAAQ,qCAAqC,EAAE,EAC/C,QAAQ,+CAA+C,EAAE;AAG5D,YAAM,aAAa,UAAU,CAAC,EAAE,SAAS,GAAG,IACxC,UAAU,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC,IAC3D;AACJ,iBAAW,SAAS;AAAA,QAClB;AAAA,QACA,CAAC,QAAQ,QAAQ,QAAQ;AACvB,cAAI,IAAI,WAAW,SAAS,KAAK,IAAI,WAAW,UAAU,KAAK,IAAI,WAAW,OAAO,GAAG;AACtF,mBAAO,GAAG,MAAM,QAAQ,GAAG;AAAA,UAC7B;AACA,gBAAM,cAAc,YAAY,YAAY,GAAG;AAC/C,gBAAM,UACJ,SAAS,WAAW,KACpB,SAAS,YAAY,YAAY,CAAC,KAClC,SAAS,GAAG,KACZ,SAAS,IAAI,YAAY,CAAC;AAC5B,cAAI,SAAS;AACX,mBAAO,GAAG,MAAM,QAAQ,OAAO;AAAA,UACjC;AACA,iBAAO,GAAG,MAAM,QAAQ,GAAG;AAAA,QAC7B;AAAA,MACF;AAGA,YAAM,WACJ,YAAY,UAAU,CAAC,CAAC,KAAK,YAAY,UAAU,CAAC,EAAE,YAAY,CAAC;AACrE,YAAM,SAAS,KAAK,MAAM,mCAAmC;AAC7D,UAAI,UAAU,WAAW,IAAI,CAAC;AAC9B,UAAI,UAAU;AACZ,kBAAU;AAAA,MACZ,WAAW,UAAU,OAAO,CAAC,EAAE,KAAK,GAAG;AACrC,kBAAU,OAAO,CAAC,EAAE,QAAQ,YAAY,EAAE,EAAE,KAAK;AAAA,MACnD;AAEA,eAAS,KAAK;AAAA,QACZ,OAAO;AAAA,QACP,OAAO;AAAA,QACP,aAAa;AAAA,QACb,UAAU,UAAU,CAAC;AAAA,MACvB,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,WAAW,QAAQ,YAAY,UAAU,KAAK,aAAa,SAAS;AACtF;AAEO,SAAS,YAAY,EAAE,SAAS,SAAS,GAAqB;AACnE,QAAM,CAAC,UAAU,WAAW,IAAI,SAAuD,IAAI;AAC3F,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,IAAI;AAC3C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAS,CAAC;AACtD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,qBAAqB,sBAAsB,IAAI,SAAS,KAAK;AACpE,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,EAAE;AACjD,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAsD,CAAC,CAAC;AAClG,QAAM,aAAa,OAAuB,IAAI;AAC9C,QAAM,cAAc,OAAuB,IAAI;AAC/C,QAAM,IAAI,UAAU;AAGpB,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,OAAO;AACtD,MAAI,gBAAgB,SAAS;AAC3B,mBAAe,OAAO;AACtB,eAAW,IAAI;AACf,aAAS,IAAI;AACb,gBAAY,IAAI;AAChB,sBAAkB,CAAC;AACnB,qBAAiB,CAAC,CAAC;AAAA,EACrB;AAEA,YAAU,MAAM;AACd,QAAI,YAAY;AAChB,cAAU,OAAO,EAAE;AAAA,MACjB,CAAC,WAAW;AACV,YAAI,UAAW;AACf,oBAAY,MAAM;AAClB,mBAAW,KAAK;AAAA,MAClB;AAAA,MACA,CAAC,QAAQ;AACP,YAAI,UAAW;AACf,gBAAQ,MAAM,uBAAuB,GAAG;AACxC,iBAAS,eAAe,QAAQ,IAAI,UAAU,wBAAwB;AACtE,mBAAW,KAAK;AAAA,MAClB;AAAA,IACF;AACA,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AAGZ,YAAU,MAAM;AACd,aAAS,mBAAmB,GAAe;AACzC,UAAI,YAAY,WAAW,CAAC,YAAY,QAAQ,SAAS,EAAE,MAAc,GAAG;AAC1E,+BAAuB,KAAK;AAAA,MAC9B;AAAA,IACF;AACA,aAAS,iBAAiB,aAAa,kBAAkB;AACzD,WAAO,MAAM,SAAS,oBAAoB,aAAa,kBAAkB;AAAA,EAC3E,GAAG,CAAC,CAAC;AAGL,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,UAAU,UAAU;AACtB,eAAO,OAAO,SAAS,QAAQ,EAAE,QAAQ,CAAC,QAAQ;AAChD,cAAI,IAAI,WAAW,OAAO,EAAG,KAAI,gBAAgB,GAAG;AAAA,QACtD,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,GAAG,CAAC,UAAU,QAAQ,CAAC;AAGvB,YAAU,MAAM;AACd,QAAI,WAAW,SAAS;AACtB,iBAAW,QAAQ,YAAY;AAAA,IACjC;AAAA,EACF,GAAG,CAAC,cAAc,CAAC;AAGnB,QAAM,eAAe;AAAA,IACnB,CAAC,UAAkB;AACjB,qBAAe,KAAK;AACpB,UAAI,CAAC,YAAY,CAAC,MAAM,KAAK,GAAG;AAC9B,yBAAiB,CAAC,CAAC;AACnB;AAAA,MACF;AACA,YAAM,aAAa,MAAM,YAAY;AACrC,YAAM,UAAuD,CAAC;AAC9D,eAAS,IAAI,GAAG,IAAI,SAAS,SAAS,QAAQ,KAAK;AAEjD,cAAM,OAAO,SAAS,SAAS,CAAC,EAAE,YAAY,QAAQ,YAAY,EAAE;AACpE,cAAM,MAAM,KAAK,YAAY,EAAE,QAAQ,UAAU;AACjD,YAAI,QAAQ,IAAI;AACd,gBAAM,QAAQ,KAAK,IAAI,GAAG,MAAM,EAAE;AAClC,gBAAM,MAAM,KAAK,IAAI,KAAK,QAAQ,MAAM,MAAM,SAAS,EAAE;AACzD,gBAAM,WACH,QAAQ,IAAI,QAAQ,MACrB,KAAK,UAAU,OAAO,GAAG,KACxB,MAAM,KAAK,SAAS,QAAQ;AAC/B,kBAAQ,KAAK,EAAE,cAAc,GAAG,QAAQ,CAAC;AACzC,cAAI,QAAQ,UAAU,GAAI;AAAA,QAC5B;AAAA,MACF;AACA,uBAAiB,OAAO;AAAA,IAC1B;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AAGA,QAAM,qBAAqB;AAAA,IACzB,CAAC,MAAwB;AACvB,YAAM,SAAS,EAAE;AACjB,YAAM,SAAS,OAAO,QAAQ,GAAG;AACjC,UAAI,CAAC,UAAU,CAAC,SAAU;AAE1B,YAAM,aAAa,OAAO,aAAa,gBAAgB,MAAM;AAC7D,UAAI,YAAY;AACd,UAAE,eAAe;AACjB,cAAM,OAAO,OAAO,aAAa,MAAM,KAAK;AAC5C,cAAM,aAAa,KAAK,MAAM,GAAG,EAAE,CAAC;AAEpC,cAAM,aAAa,SAAS,SAAS;AAAA,UACnC,CAAC,OACC,GAAG,aAAa,cAChB,GAAG,SAAS,YAAY,MAAM,WAAW,YAAY;AAAA,QACzD;AACA,YAAI,eAAe,IAAI;AACrB,4BAAkB,UAAU;AAC5B,qBAAW,MAAM;AACf,gBAAI,WAAW,QAAS,YAAW,QAAQ,YAAY;AAAA,UACzD,GAAG,GAAG;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AAEA,MAAI,SAAS;AACX,WACE,qBAAC,SAAI,WAAU,0CACb;AAAA,0BAAC,SAAI,WAAU,6BAA4B;AAAA,MAC3C,oBAAC,OAAE,WAAU,sBAAsB,YAAE,cAAa;AAAA,OACpD;AAAA,EAEJ;AAEA,MAAI,SAAS,CAAC,UAAU;AACtB,WACE,qBAAC,SAAI,WAAU,wCACb;AAAA,0BAAC,gBAAa,MAAM,IAAI,WAAU,uBAAsB;AAAA,MACxD,oBAAC,OAAE,WAAU,wBAAwB,YAAE,iBAAgB;AAAA,MACvD,oBAAC,OAAE,WAAU,sBAAsB,mBAAS,EAAE,cAAa;AAAA,OAC7D;AAAA,EAEJ;AAEA,MAAI,SAAS,SAAS,WAAW,GAAG;AAClC,WACE,qBAAC,SAAI,WAAU,wCACb;AAAA,0BAAC,gBAAa,MAAM,IAAI;AAAA,MACxB,oBAAC,OAAE,WAAU,wBAAwB,YAAE,iBAAgB;AAAA,OACzD;AAAA,EAEJ;AAEA,QAAM,UAAU,SAAS,SAAS,cAAc;AAIhD,QAAM,cAAc,SAAS,YAAY,KAAK,IAAI,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsBtD,SACE,qBAAC,SAAI,WAAU,WAEb;AAAA,wBAAC,SAAI,WAAU,qBACb,+BAAC,SAAI,WAAU,qBACb;AAAA,2BAAC,SAAI,WAAU,sBACb;AAAA,4BAAC,gBAAa,MAAM,IAAI,WAAU,sBAAqB;AAAA,QACvD,oBAAC,UAAK,WAAU,uBACb,mBAAS,SAAS,UACrB;AAAA,QACC,SAAS,UACR,qBAAC,UAAK,WAAU,wBAAuB;AAAA;AAAA,UAClC,SAAS;AAAA,WACd;AAAA,SAEJ;AAAA,MACA,qBAAC,SAAI,WAAU,uBAEb;AAAA,6BAAC,SAAI,WAAU,wBACb;AAAA,8BAAC,cAAW,MAAM,IAAI,WAAU,wBAAuB;AAAA,UACvD;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,OAAO;AAAA,cACP,UAAU,CAAC,MAAM,aAAa,EAAE,OAAO,KAAK;AAAA,cAC5C,aAAa,EAAE;AAAA,cACf,WAAU;AAAA;AAAA,UACZ;AAAA,UACC,eACC;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,MAAM;AACb,+BAAe,EAAE;AACjB,iCAAiB,CAAC,CAAC;AAAA,cACrB;AAAA,cACA,WAAU;AAAA,cAEV,8BAAC,SAAM,MAAM,IAAI;AAAA;AAAA,UACnB;AAAA,WAEJ;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,SAAS,MAAM,WAAW,CAAC,OAAO;AAAA,YAClC,WAAW,oBAAoB,UAAU,6BAA6B,EAAE;AAAA,YACxE,OAAO,EAAE;AAAA,YAET,8BAAC,YAAS,MAAM,IAAI;AAAA;AAAA,QACtB;AAAA,SACF;AAAA,OACF,GACF;AAAA,IAGC,eAAe,cAAc,SAAS,KACrC,qBAAC,SAAI,WAAU,2BACb;AAAA,0BAAC,OAAE,WAAU,iCACV,YAAE,gBAAgB,QAAQ,WAAW,cAAc,OAAO,eAAe,CAAC,GAC7E;AAAA,MACA,oBAAC,SAAI,WAAU,gCACZ,wBAAc,IAAI,CAAC,GAAG,MACrB;AAAA,QAAC;AAAA;AAAA,UAEC,SAAS,MAAM;AACb,8BAAkB,EAAE,YAAY;AAChC,2BAAe,EAAE;AACjB,6BAAiB,CAAC,CAAC;AAAA,UACrB;AAAA,UACA,WAAU;AAAA,UAEV;AAAA,gCAAC,UAAK,WAAU,gCACb,mBAAS,SAAS,EAAE,YAAY,EAAE,OACrC;AAAA,YACA,oBAAC,UAAK,WAAU,kCAAkC,YAAE,SAAQ;AAAA;AAAA;AAAA,QAXvD;AAAA,MAYP,CACD,GACH;AAAA,OACF;AAAA,IAGD,eAAe,cAAc,WAAW,KACvC,oBAAC,SAAI,WAAU,yBACb,8BAAC,OAAE,WAAU,8BAA8B,YAAE,gBAAe,GAC9D;AAAA,IAGF,qBAAC,SAAI,WAAU,iBAEZ;AAAA,iBACC,oBAAC,SAAI,WAAU,wBACb,+BAAC,SAAI,WAAU,sBACb;AAAA,4BAAC,QAAG,WAAU,wBACX,YAAE,iBACL;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,SAAS;AAAA,YAChB,UAAU,SAAS;AAAA,YACnB;AAAA,YACA,UAAU,CAAC,QAAQ;AACjB,gCAAkB,GAAG;AACrB,qCAAuB,KAAK;AAAA,YAC9B;AAAA;AAAA,QACF;AAAA,QAEC,SAAS,IAAI,WAAW,KACvB,oBAAC,SAAI,WAAU,qBACZ,mBAAS,SAAS,IAAI,CAAC,IAAI,MAC1B;AAAA,UAAC;AAAA;AAAA,YAEC,SAAS,MAAM,kBAAkB,CAAC;AAAA,YAClC,WAAW,qBAAqB,MAAM,iBAAiB,8BAA8B,EAAE;AAAA,YAEtF,aAAG;AAAA;AAAA,UAJC;AAAA,QAKP,CACD,GACH;AAAA,SAEJ,GACF;AAAA,MAIF,qBAAC,SAAI,WAAU,iBAEb;AAAA,6BAAC,SAAI,WAAU,wBACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,MAAM,kBAAkB,KAAK,IAAI,GAAG,iBAAiB,CAAC,CAAC;AAAA,cAChE,UAAU,mBAAmB;AAAA,cAC7B,WAAU;AAAA,cAEV,8BAAC,mBAAgB,MAAM,IAAI;AAAA;AAAA,UAC7B;AAAA,UAGA,qBAAC,SAAI,WAAU,0BAAyB,KAAK,aAC3C;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,SAAS,MAAM,uBAAuB,CAAC,mBAAmB;AAAA,gBAC1D,WAAU;AAAA,gBAEV;AAAA,uCAAC,UAAK,WAAU,2BACb;AAAA,qCAAiB;AAAA,oBAAE;AAAA,oBAAE,SAAS,SAAS;AAAA,qBAC1C;AAAA,kBACA,oBAAC,UAAK,WAAU,2BACb,kBAAQ,OACX;AAAA,kBACA;AAAA,oBAAC;AAAA;AAAA,sBACC,MAAM;AAAA,sBACN,WAAW,6BAA6B,sBAAsB,oCAAoC,EAAE;AAAA;AAAA,kBACtG;AAAA;AAAA;AAAA,YACF;AAAA,YAGC,uBACC,oBAAC,SAAI,WAAU,0BACZ,mBAAS,SAAS,IAAI,CAAC,IAAI,MAC1B;AAAA,cAAC;AAAA;AAAA,gBAEC,SAAS,MAAM;AACb,oCAAkB,CAAC;AACnB,yCAAuB,KAAK;AAAA,gBAC9B;AAAA,gBACA,WAAW,0BAA0B,MAAM,iBAAiB,mCAAmC,EAAE;AAAA,gBAEjG;AAAA,uCAAC,UAAK,WAAU,gCAAgC;AAAA,wBAAI;AAAA,oBAAE;AAAA,qBAAC;AAAA,kBACtD,GAAG;AAAA;AAAA;AAAA,cARC;AAAA,YASP,CACD,GACH;AAAA,aAEJ;AAAA,UAEA;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,MACP,kBAAkB,KAAK,IAAI,SAAS,SAAS,SAAS,GAAG,iBAAiB,CAAC,CAAC;AAAA,cAE9E,UAAU,mBAAmB,SAAS,SAAS,SAAS;AAAA,cACxD,WAAU;AAAA,cAEV,8BAAC,oBAAiB,MAAM,IAAI;AAAA;AAAA,UAC9B;AAAA,WACF;AAAA,QAGA,oBAAC,SAAI,WAAU,oBAAmB,KAAK,YAAY,SAAS,oBAC1D,+BAAC,SAAI,WAAU,0BACb;AAAA,8BAAC,QAAG,WAAU,0BAA0B,kBAAQ,OAAM;AAAA,UAErD,eACC,oBAAC,WAAM,yBAAyB,EAAE,QAAQ,YAAY,GAAG;AAAA,UAE3D;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,yBAAyB,EAAE,QAAQ,QAAQ,YAAY;AAAA;AAAA,UACzD;AAAA,WACF,GACF;AAAA,QAGA,qBAAC,SAAI,WAAU,mBACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,MAAM,kBAAkB,KAAK,IAAI,GAAG,iBAAiB,CAAC,CAAC;AAAA,cAChE,UAAU,mBAAmB;AAAA,cAC7B,WAAU;AAAA,cAEV;AAAA,oCAAC,mBAAgB,MAAM,IAAI;AAAA,gBAC3B,oBAAC,UAAK,WAAU,4BAA4B,YAAE,UAAS;AAAA;AAAA;AAAA,UACzD;AAAA,UACA,qBAAC,UAAK,WAAU,yBACb;AAAA,6BAAiB;AAAA,YAAE;AAAA,YAAI,SAAS,SAAS;AAAA,aAC5C;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,MACP,kBAAkB,KAAK,IAAI,SAAS,SAAS,SAAS,GAAG,iBAAiB,CAAC,CAAC;AAAA,cAE9E,UAAU,mBAAmB,SAAS,SAAS,SAAS;AAAA,cACxD,WAAU;AAAA,cAEV;AAAA,oCAAC,UAAK,WAAU,4BAA4B,YAAE,MAAK;AAAA,gBACnD,oBAAC,oBAAiB,MAAM,IAAI;AAAA;AAAA;AAAA,UAC9B;AAAA,WACF;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;AAGA,SAAS,QAAQ;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAMG;AACD,SACE,oBAAC,SAAI,WAAW,QAAQ,IAAI,wBAAwB,qBACjD,gBAAM,IAAI,CAAC,MAAM,MAAM;AAEtB,UAAM,aAAa,SAAS;AAAA,MAC1B,CAAC,OACC,GAAG,aAAa,KAAK,OACrB,GAAG,SAAS,YAAY,MAAM,KAAK,IAAI,YAAY,KACnD,KAAK,IAAI,SAAS,GAAG,SAAS,MAAM,GAAG,EAAE,IAAI,CAAE,KAC/C,GAAG,SAAS,SAAS,KAAK,IAAI,MAAM,GAAG,EAAE,IAAI,CAAE;AAAA,IACnD;AACA,UAAM,YAAY,eAAe;AAEjC,WACE,qBAAC,SACC;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,MAAM;AACb,gBAAI,eAAe,GAAI,UAAS,UAAU;AAAA,UAC5C;AAAA,UACA,UAAU,eAAe;AAAA,UACzB,WAAW,qBAAqB,YAAY,8BAA8B,EAAE;AAAA,UAC5E,OAAO,EAAE,aAAa,GAAG,IAAI,QAAQ,CAAC,KAAK;AAAA,UAE1C,eAAK;AAAA;AAAA,MACR;AAAA,MACC,KAAK,YAAY,KAAK,SAAS,SAAS,KACvC;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,KAAK;AAAA,UACZ;AAAA,UACA;AAAA,UACA;AAAA,UACA,OAAO,QAAQ;AAAA;AAAA,MACjB;AAAA,SAlBM,GAAG,KAAK,IAAI,CAAC,EAoBvB;AAAA,EAEJ,CAAC,GACH;AAEJ;","names":["parseNavPoints"]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useMemo } from "react";
|
|
3
|
+
import { EyeIcon, Code2Icon } from "./icons";
|
|
4
|
+
import { ShikiSourceView } from "./ShikiSourceView";
|
|
5
|
+
import { useLocale } from "./core/i18n";
|
|
6
|
+
import "./styles/HtmlPreview.css";
|
|
7
|
+
import "./styles/ViewModeBar.css";
|
|
8
|
+
function HtmlPreview({ content, fileName }) {
|
|
9
|
+
const [viewMode, setViewMode] = useState("preview");
|
|
10
|
+
const t = useLocale();
|
|
11
|
+
const [securityMode] = useState("safe");
|
|
12
|
+
const sandbox = securityMode === "trusted" ? "allow-scripts allow-same-origin" : "";
|
|
13
|
+
const blobUrl = useMemo(() => {
|
|
14
|
+
const blob = new Blob([content], { type: "text/html" });
|
|
15
|
+
return URL.createObjectURL(blob);
|
|
16
|
+
}, [content]);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
return () => URL.revokeObjectURL(blobUrl);
|
|
19
|
+
}, [blobUrl]);
|
|
20
|
+
return /* @__PURE__ */ jsxs("div", { className: "fv-html", children: [
|
|
21
|
+
/* @__PURE__ */ jsx("div", { className: "fv-view-mode-bar", children: /* @__PURE__ */ jsxs("div", { className: "fv-view-mode-group", children: [
|
|
22
|
+
/* @__PURE__ */ jsxs(
|
|
23
|
+
"button",
|
|
24
|
+
{
|
|
25
|
+
onClick: () => setViewMode("preview"),
|
|
26
|
+
className: `fv-view-mode-btn ${viewMode === "preview" ? "fv-view-mode-btn--active" : ""}`,
|
|
27
|
+
children: [
|
|
28
|
+
/* @__PURE__ */ jsx(EyeIcon, { size: 13 }),
|
|
29
|
+
t.preview
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
),
|
|
33
|
+
/* @__PURE__ */ jsxs(
|
|
34
|
+
"button",
|
|
35
|
+
{
|
|
36
|
+
onClick: () => setViewMode("source"),
|
|
37
|
+
className: `fv-view-mode-btn ${viewMode === "source" ? "fv-view-mode-btn--active" : ""}`,
|
|
38
|
+
children: [
|
|
39
|
+
/* @__PURE__ */ jsx(Code2Icon, { size: 13 }),
|
|
40
|
+
t.source
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
] }) }),
|
|
45
|
+
/* @__PURE__ */ jsx("div", { className: "fv-html__content", children: viewMode === "preview" ? /* @__PURE__ */ jsx(
|
|
46
|
+
"iframe",
|
|
47
|
+
{
|
|
48
|
+
src: blobUrl,
|
|
49
|
+
sandbox,
|
|
50
|
+
referrerPolicy: "no-referrer",
|
|
51
|
+
className: "fv-html__iframe",
|
|
52
|
+
title: fileName
|
|
53
|
+
}
|
|
54
|
+
) : /* @__PURE__ */ jsx(ShikiSourceView, { content, fileName, language: "html" }) })
|
|
55
|
+
] });
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
HtmlPreview
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=HtmlPreview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/HtmlPreview.tsx"],"sourcesContent":["import { useState, useEffect, useMemo } from \"react\";\nimport { EyeIcon, Code2Icon } from \"./icons\";\nimport { ShikiSourceView } from \"./ShikiSourceView\";\nimport { useLocale } from \"./core/i18n\";\nimport \"./styles/HtmlPreview.css\";\nimport \"./styles/ViewModeBar.css\";\n\ninterface HtmlPreviewProps {\n content: string;\n fileName: string;\n}\n\ntype ViewMode = \"preview\" | \"source\";\ntype HtmlSecurityMode = \"safe\" | \"trusted\";\n\nexport function HtmlPreview({ content, fileName }: HtmlPreviewProps) {\n const [viewMode, setViewMode] = useState<ViewMode>(\"preview\");\n const t = useLocale();\n\n const [securityMode] = useState<HtmlSecurityMode>(\"safe\");\n const sandbox =\n securityMode === \"trusted\" ? \"allow-scripts allow-same-origin\" : \"\";\n\n const blobUrl = useMemo(() => {\n const blob = new Blob([content], { type: \"text/html\" });\n return URL.createObjectURL(blob);\n }, [content]);\n\n useEffect(() => {\n return () => URL.revokeObjectURL(blobUrl);\n }, [blobUrl]);\n\n return (\n <div className=\"fv-html\">\n <div className=\"fv-view-mode-bar\">\n <div className=\"fv-view-mode-group\">\n <button\n onClick={() => setViewMode(\"preview\")}\n className={`fv-view-mode-btn ${viewMode === \"preview\" ? \"fv-view-mode-btn--active\" : \"\"}`}\n >\n <EyeIcon size={13} />\n {t.preview}\n </button>\n <button\n onClick={() => setViewMode(\"source\")}\n className={`fv-view-mode-btn ${viewMode === \"source\" ? \"fv-view-mode-btn--active\" : \"\"}`}\n >\n <Code2Icon size={13} />\n {t.source}\n </button>\n </div>\n </div>\n\n <div className=\"fv-html__content\">\n {viewMode === \"preview\" ? (\n <iframe\n src={blobUrl}\n sandbox={sandbox}\n referrerPolicy=\"no-referrer\"\n className=\"fv-html__iframe\"\n title={fileName}\n />\n ) : (\n <ShikiSourceView content={content} fileName={fileName} language=\"html\" />\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"AAoCU,SAIE,KAJF;AApCV,SAAS,UAAU,WAAW,eAAe;AAC7C,SAAS,SAAS,iBAAiB;AACnC,SAAS,uBAAuB;AAChC,SAAS,iBAAiB;AAC1B,OAAO;AACP,OAAO;AAUA,SAAS,YAAY,EAAE,SAAS,SAAS,GAAqB;AACnE,QAAM,CAAC,UAAU,WAAW,IAAI,SAAmB,SAAS;AAC5D,QAAM,IAAI,UAAU;AAEpB,QAAM,CAAC,YAAY,IAAI,SAA2B,MAAM;AACxD,QAAM,UACJ,iBAAiB,YAAY,oCAAoC;AAEnE,QAAM,UAAU,QAAQ,MAAM;AAC5B,UAAM,OAAO,IAAI,KAAK,CAAC,OAAO,GAAG,EAAE,MAAM,YAAY,CAAC;AACtD,WAAO,IAAI,gBAAgB,IAAI;AAAA,EACjC,GAAG,CAAC,OAAO,CAAC;AAEZ,YAAU,MAAM;AACd,WAAO,MAAM,IAAI,gBAAgB,OAAO;AAAA,EAC1C,GAAG,CAAC,OAAO,CAAC;AAEZ,SACE,qBAAC,SAAI,WAAU,WACb;AAAA,wBAAC,SAAI,WAAU,oBACb,+BAAC,SAAI,WAAU,sBACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,MAAM,YAAY,SAAS;AAAA,UACpC,WAAW,oBAAoB,aAAa,YAAY,6BAA6B,EAAE;AAAA,UAEvF;AAAA,gCAAC,WAAQ,MAAM,IAAI;AAAA,YAClB,EAAE;AAAA;AAAA;AAAA,MACL;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,MAAM,YAAY,QAAQ;AAAA,UACnC,WAAW,oBAAoB,aAAa,WAAW,6BAA6B,EAAE;AAAA,UAEtF;AAAA,gCAAC,aAAU,MAAM,IAAI;AAAA,YACpB,EAAE;AAAA;AAAA;AAAA,MACL;AAAA,OACF,GACF;AAAA,IAEA,oBAAC,SAAI,WAAU,oBACZ,uBAAa,YACZ;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL;AAAA,QACA,gBAAe;AAAA,QACf,WAAU;AAAA,QACV,OAAO;AAAA;AAAA,IACT,IAEA,oBAAC,mBAAgB,SAAkB,UAAoB,UAAS,QAAO,GAE3E;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ZoomInIcon, ZoomOutIcon, RotateCwIcon } from "./icons";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import "./styles/ImagePreview.css";
|
|
5
|
+
function ImagePreview({ url, fileName }) {
|
|
6
|
+
const [zoom, setZoom] = useState(100);
|
|
7
|
+
const [rotation, setRotation] = useState(0);
|
|
8
|
+
const handleZoomIn = () => setZoom((prev) => Math.min(prev + 25, 400));
|
|
9
|
+
const handleZoomOut = () => setZoom((prev) => Math.max(prev - 25, 25));
|
|
10
|
+
const handleRotate = () => setRotation((prev) => (prev + 90) % 360);
|
|
11
|
+
const handleReset = () => {
|
|
12
|
+
setZoom(100);
|
|
13
|
+
setRotation(0);
|
|
14
|
+
};
|
|
15
|
+
return /* @__PURE__ */ jsxs("div", { className: "fv-image", children: [
|
|
16
|
+
/* @__PURE__ */ jsxs("div", { className: "fv-image__toolbar", children: [
|
|
17
|
+
/* @__PURE__ */ jsx("button", { onClick: handleZoomOut, className: "fv-btn fv-btn--icon", title: "Zoom Out", children: /* @__PURE__ */ jsx(ZoomOutIcon, { size: 16 }) }),
|
|
18
|
+
/* @__PURE__ */ jsxs("span", { className: "fv-image__zoom-label", children: [
|
|
19
|
+
zoom,
|
|
20
|
+
"%"
|
|
21
|
+
] }),
|
|
22
|
+
/* @__PURE__ */ jsx("button", { onClick: handleZoomIn, className: "fv-btn fv-btn--icon", title: "Zoom In", children: /* @__PURE__ */ jsx(ZoomInIcon, { size: 16 }) }),
|
|
23
|
+
/* @__PURE__ */ jsx("div", { className: "fv-toolbar__separator" }),
|
|
24
|
+
/* @__PURE__ */ jsx("button", { onClick: handleRotate, className: "fv-btn fv-btn--icon", title: "Rotate", children: /* @__PURE__ */ jsx(RotateCwIcon, { size: 16 }) }),
|
|
25
|
+
/* @__PURE__ */ jsx("button", { onClick: handleReset, className: "fv-image__reset-btn", children: "Reset" })
|
|
26
|
+
] }),
|
|
27
|
+
/* @__PURE__ */ jsx("div", { className: "fv-image__canvas", children: /* @__PURE__ */ jsx(
|
|
28
|
+
"img",
|
|
29
|
+
{
|
|
30
|
+
src: url,
|
|
31
|
+
alt: fileName,
|
|
32
|
+
className: "fv-image__img",
|
|
33
|
+
style: {
|
|
34
|
+
transform: `scale(${zoom / 100}) rotate(${rotation}deg)`,
|
|
35
|
+
transformOrigin: "center center"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
) })
|
|
39
|
+
] });
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
ImagePreview
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=ImagePreview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ImagePreview.tsx"],"sourcesContent":["import { ZoomInIcon, ZoomOutIcon, RotateCwIcon } from \"./icons\";\nimport { useState } from \"react\";\nimport \"./styles/ImagePreview.css\";\n\ninterface ImagePreviewProps {\n url: string;\n fileName: string;\n}\n\nexport function ImagePreview({ url, fileName }: ImagePreviewProps) {\n const [zoom, setZoom] = useState(100);\n const [rotation, setRotation] = useState(0);\n\n const handleZoomIn = () => setZoom((prev) => Math.min(prev + 25, 400));\n const handleZoomOut = () => setZoom((prev) => Math.max(prev - 25, 25));\n const handleRotate = () => setRotation((prev) => (prev + 90) % 360);\n const handleReset = () => {\n setZoom(100);\n setRotation(0);\n };\n\n return (\n <div className=\"fv-image\">\n <div className=\"fv-image__toolbar\">\n <button onClick={handleZoomOut} className=\"fv-btn fv-btn--icon\" title=\"Zoom Out\">\n <ZoomOutIcon size={16} />\n </button>\n <span className=\"fv-image__zoom-label\">{zoom}%</span>\n <button onClick={handleZoomIn} className=\"fv-btn fv-btn--icon\" title=\"Zoom In\">\n <ZoomInIcon size={16} />\n </button>\n <div className=\"fv-toolbar__separator\" />\n <button onClick={handleRotate} className=\"fv-btn fv-btn--icon\" title=\"Rotate\">\n <RotateCwIcon size={16} />\n </button>\n <button onClick={handleReset} className=\"fv-image__reset-btn\">\n Reset\n </button>\n </div>\n\n <div className=\"fv-image__canvas\">\n <img\n src={url}\n alt={fileName}\n className=\"fv-image__img\"\n style={{\n transform: `scale(${zoom / 100}) rotate(${rotation}deg)`,\n transformOrigin: \"center center\",\n }}\n />\n </div>\n </div>\n );\n}\n"],"mappings":"AAyBU,cAEF,YAFE;AAzBV,SAAS,YAAY,aAAa,oBAAoB;AACtD,SAAS,gBAAgB;AACzB,OAAO;AAOA,SAAS,aAAa,EAAE,KAAK,SAAS,GAAsB;AACjE,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,GAAG;AACpC,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,CAAC;AAE1C,QAAM,eAAe,MAAM,QAAQ,CAAC,SAAS,KAAK,IAAI,OAAO,IAAI,GAAG,CAAC;AACrE,QAAM,gBAAgB,MAAM,QAAQ,CAAC,SAAS,KAAK,IAAI,OAAO,IAAI,EAAE,CAAC;AACrE,QAAM,eAAe,MAAM,YAAY,CAAC,UAAU,OAAO,MAAM,GAAG;AAClE,QAAM,cAAc,MAAM;AACxB,YAAQ,GAAG;AACX,gBAAY,CAAC;AAAA,EACf;AAEA,SACE,qBAAC,SAAI,WAAU,YACb;AAAA,yBAAC,SAAI,WAAU,qBACb;AAAA,0BAAC,YAAO,SAAS,eAAe,WAAU,uBAAsB,OAAM,YACpE,8BAAC,eAAY,MAAM,IAAI,GACzB;AAAA,MACA,qBAAC,UAAK,WAAU,wBAAwB;AAAA;AAAA,QAAK;AAAA,SAAC;AAAA,MAC9C,oBAAC,YAAO,SAAS,cAAc,WAAU,uBAAsB,OAAM,WACnE,8BAAC,cAAW,MAAM,IAAI,GACxB;AAAA,MACA,oBAAC,SAAI,WAAU,yBAAwB;AAAA,MACvC,oBAAC,YAAO,SAAS,cAAc,WAAU,uBAAsB,OAAM,UACnE,8BAAC,gBAAa,MAAM,IAAI,GAC1B;AAAA,MACA,oBAAC,YAAO,SAAS,aAAa,WAAU,uBAAsB,mBAE9D;AAAA,OACF;AAAA,IAEA,oBAAC,SAAI,WAAU,oBACb;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,KAAK;AAAA,QACL,WAAU;AAAA,QACV,OAAO;AAAA,UACL,WAAW,SAAS,OAAO,GAAG,YAAY,QAAQ;AAAA,UAClD,iBAAiB;AAAA,QACnB;AAAA;AAAA,IACF,GACF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { FileInfo } from './core/types.js';
|
|
3
|
+
|
|
4
|
+
interface LargeFileGateProps {
|
|
5
|
+
file: FileInfo;
|
|
6
|
+
confirmed: boolean;
|
|
7
|
+
onConfirm: () => void;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
declare function LargeFileGate({ file, confirmed, onConfirm, children, }: LargeFileGateProps): react.JSX.Element;
|
|
11
|
+
|
|
12
|
+
export { LargeFileGate };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AlertTriangleIcon, DownloadIcon } from "./icons";
|
|
3
|
+
import { formatFileSize } from "./utils";
|
|
4
|
+
import { getPreviewSizePolicy } from "./performance-limits";
|
|
5
|
+
import { downloadSource } from "./core/download";
|
|
6
|
+
import "./styles/LargeFileGate.css";
|
|
7
|
+
function LargeFileGate({
|
|
8
|
+
file,
|
|
9
|
+
confirmed,
|
|
10
|
+
onConfirm,
|
|
11
|
+
children
|
|
12
|
+
}) {
|
|
13
|
+
const policy = getPreviewSizePolicy({
|
|
14
|
+
size: file.size,
|
|
15
|
+
fileType: file.fileType
|
|
16
|
+
});
|
|
17
|
+
if (!policy.shouldWarn) {
|
|
18
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
19
|
+
}
|
|
20
|
+
if (policy.level === "warning") {
|
|
21
|
+
return /* @__PURE__ */ jsx("div", { className: "fv-gate-warning", children: /* @__PURE__ */ jsxs("div", { className: "fv-gate-warning__inner", children: [
|
|
22
|
+
/* @__PURE__ */ jsx(AlertTriangleIcon, { size: 14 }),
|
|
23
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
24
|
+
"Large file: ",
|
|
25
|
+
formatFileSize(file.size),
|
|
26
|
+
". Preview may be slower."
|
|
27
|
+
] })
|
|
28
|
+
] }) });
|
|
29
|
+
}
|
|
30
|
+
if (policy.shouldConfirm && !confirmed) {
|
|
31
|
+
return /* @__PURE__ */ jsxs("div", { className: "fv-gate-confirm", children: [
|
|
32
|
+
/* @__PURE__ */ jsx(AlertTriangleIcon, { size: 48, className: "fv-gate-confirm__icon" }),
|
|
33
|
+
/* @__PURE__ */ jsxs("div", { className: "fv-gate-confirm__body", children: [
|
|
34
|
+
/* @__PURE__ */ jsx("h3", { className: "fv-gate-confirm__title", children: "Large file preview" }),
|
|
35
|
+
/* @__PURE__ */ jsx("p", { className: "fv-gate-confirm__desc", children: policy.message }),
|
|
36
|
+
/* @__PURE__ */ jsxs("p", { className: "fv-gate-confirm__meta", children: [
|
|
37
|
+
file.name,
|
|
38
|
+
" \xB7 ",
|
|
39
|
+
formatFileSize(file.size)
|
|
40
|
+
] })
|
|
41
|
+
] }),
|
|
42
|
+
/* @__PURE__ */ jsxs("div", { className: "fv-gate-confirm__actions", children: [
|
|
43
|
+
/* @__PURE__ */ jsx("button", { className: "fv-btn fv-btn--primary", onClick: onConfirm, children: "Preview anyway" }),
|
|
44
|
+
/* @__PURE__ */ jsxs(
|
|
45
|
+
"button",
|
|
46
|
+
{
|
|
47
|
+
className: "fv-btn fv-btn--outline",
|
|
48
|
+
onClick: () => downloadSource(file.source, file.name, file.type),
|
|
49
|
+
children: [
|
|
50
|
+
/* @__PURE__ */ jsx(DownloadIcon, { size: 16 }),
|
|
51
|
+
" Download"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
] })
|
|
56
|
+
] });
|
|
57
|
+
}
|
|
58
|
+
if (policy.shouldBlock) {
|
|
59
|
+
return /* @__PURE__ */ jsxs("div", { className: "fv-gate-confirm", children: [
|
|
60
|
+
/* @__PURE__ */ jsx(AlertTriangleIcon, { size: 48, className: "fv-gate-confirm__icon fv-gate-confirm__icon--block" }),
|
|
61
|
+
/* @__PURE__ */ jsxs("div", { className: "fv-gate-confirm__body", children: [
|
|
62
|
+
/* @__PURE__ */ jsx("h3", { className: "fv-gate-confirm__title", children: "File too large to preview" }),
|
|
63
|
+
/* @__PURE__ */ jsx("p", { className: "fv-gate-confirm__desc", children: policy.message }),
|
|
64
|
+
/* @__PURE__ */ jsxs("p", { className: "fv-gate-confirm__meta", children: [
|
|
65
|
+
file.name,
|
|
66
|
+
" \xB7 ",
|
|
67
|
+
formatFileSize(file.size)
|
|
68
|
+
] })
|
|
69
|
+
] }),
|
|
70
|
+
/* @__PURE__ */ jsxs(
|
|
71
|
+
"button",
|
|
72
|
+
{
|
|
73
|
+
className: "fv-btn fv-btn--outline",
|
|
74
|
+
onClick: () => downloadSource(file.source, file.name, file.type),
|
|
75
|
+
children: [
|
|
76
|
+
/* @__PURE__ */ jsx(DownloadIcon, { size: 16 }),
|
|
77
|
+
" Download original file"
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
] });
|
|
82
|
+
}
|
|
83
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
LargeFileGate
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=LargeFileGate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/LargeFileGate.tsx"],"sourcesContent":["import { AlertTriangleIcon, DownloadIcon } from \"./icons\";\nimport type { FileInfo } from \"./utils\";\nimport { formatFileSize } from \"./utils\";\nimport { getPreviewSizePolicy } from \"./performance-limits\";\nimport { downloadSource } from \"./core/download\";\nimport \"./styles/LargeFileGate.css\";\n\ninterface LargeFileGateProps {\n file: FileInfo;\n confirmed: boolean;\n onConfirm: () => void;\n children: React.ReactNode;\n}\n\nexport function LargeFileGate({\n file,\n confirmed,\n onConfirm,\n children,\n}: LargeFileGateProps) {\n const policy = getPreviewSizePolicy({\n size: file.size,\n fileType: file.fileType,\n });\n\n if (!policy.shouldWarn) {\n return <>{children}</>;\n }\n\n if (policy.level === \"warning\") {\n return (\n <div className=\"fv-gate-warning\">\n <div className=\"fv-gate-warning__inner\">\n <AlertTriangleIcon size={14} />\n <span>\n Large file: {formatFileSize(file.size)}. Preview may be slower.\n </span>\n </div>\n </div>\n );\n }\n\n if (policy.shouldConfirm && !confirmed) {\n return (\n <div className=\"fv-gate-confirm\">\n <AlertTriangleIcon size={48} className=\"fv-gate-confirm__icon\" />\n <div className=\"fv-gate-confirm__body\">\n <h3 className=\"fv-gate-confirm__title\">Large file preview</h3>\n <p className=\"fv-gate-confirm__desc\">\n {policy.message}\n </p>\n <p className=\"fv-gate-confirm__meta\">\n {file.name} · {formatFileSize(file.size)}\n </p>\n </div>\n <div className=\"fv-gate-confirm__actions\">\n <button className=\"fv-btn fv-btn--primary\" onClick={onConfirm}>Preview anyway</button>\n <button\n className=\"fv-btn fv-btn--outline\"\n onClick={() => downloadSource(file.source, file.name, file.type)}\n >\n <DownloadIcon size={16} /> Download\n </button>\n </div>\n </div>\n );\n }\n\n if (policy.shouldBlock) {\n return (\n <div className=\"fv-gate-confirm\">\n <AlertTriangleIcon size={48} className=\"fv-gate-confirm__icon fv-gate-confirm__icon--block\" />\n <div className=\"fv-gate-confirm__body\">\n <h3 className=\"fv-gate-confirm__title\">File too large to preview</h3>\n <p className=\"fv-gate-confirm__desc\">\n {policy.message}\n </p>\n <p className=\"fv-gate-confirm__meta\">\n {file.name} · {formatFileSize(file.size)}\n </p>\n </div>\n <button\n className=\"fv-btn fv-btn--outline\"\n onClick={() => downloadSource(file.source, file.name, file.type)}\n >\n <DownloadIcon size={16} /> Download original file\n </button>\n </div>\n );\n }\n\n return <>{children}</>;\n}\n"],"mappings":"AA0BW,wBAQD,YARC;AA1BX,SAAS,mBAAmB,oBAAoB;AAEhD,SAAS,sBAAsB;AAC/B,SAAS,4BAA4B;AACrC,SAAS,sBAAsB;AAC/B,OAAO;AASA,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,SAAS,qBAAqB;AAAA,IAClC,MAAM,KAAK;AAAA,IACX,UAAU,KAAK;AAAA,EACjB,CAAC;AAED,MAAI,CAAC,OAAO,YAAY;AACtB,WAAO,gCAAG,UAAS;AAAA,EACrB;AAEA,MAAI,OAAO,UAAU,WAAW;AAC9B,WACE,oBAAC,SAAI,WAAU,mBACb,+BAAC,SAAI,WAAU,0BACb;AAAA,0BAAC,qBAAkB,MAAM,IAAI;AAAA,MAC7B,qBAAC,UAAK;AAAA;AAAA,QACS,eAAe,KAAK,IAAI;AAAA,QAAE;AAAA,SACzC;AAAA,OACF,GACF;AAAA,EAEJ;AAEA,MAAI,OAAO,iBAAiB,CAAC,WAAW;AACtC,WACE,qBAAC,SAAI,WAAU,mBACb;AAAA,0BAAC,qBAAkB,MAAM,IAAI,WAAU,yBAAwB;AAAA,MAC/D,qBAAC,SAAI,WAAU,yBACb;AAAA,4BAAC,QAAG,WAAU,0BAAyB,gCAAkB;AAAA,QACzD,oBAAC,OAAE,WAAU,yBACV,iBAAO,SACV;AAAA,QACA,qBAAC,OAAE,WAAU,yBACV;AAAA,eAAK;AAAA,UAAK;AAAA,UAAI,eAAe,KAAK,IAAI;AAAA,WACzC;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,4BACb;AAAA,4BAAC,YAAO,WAAU,0BAAyB,SAAS,WAAW,4BAAc;AAAA,QAC7E;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,SAAS,MAAM,eAAe,KAAK,QAAQ,KAAK,MAAM,KAAK,IAAI;AAAA,YAE/D;AAAA,kCAAC,gBAAa,MAAM,IAAI;AAAA,cAAE;AAAA;AAAA;AAAA,QAC5B;AAAA,SACF;AAAA,OACF;AAAA,EAEJ;AAEA,MAAI,OAAO,aAAa;AACtB,WACE,qBAAC,SAAI,WAAU,mBACb;AAAA,0BAAC,qBAAkB,MAAM,IAAI,WAAU,sDAAqD;AAAA,MAC5F,qBAAC,SAAI,WAAU,yBACb;AAAA,4BAAC,QAAG,WAAU,0BAAyB,uCAAyB;AAAA,QAChE,oBAAC,OAAE,WAAU,yBACV,iBAAO,SACV;AAAA,QACA,qBAAC,OAAE,WAAU,yBACV;AAAA,eAAK;AAAA,UAAK;AAAA,UAAI,eAAe,KAAK,IAAI;AAAA,WACzC;AAAA,SACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,SAAS,MAAM,eAAe,KAAK,QAAQ,KAAK,MAAM,KAAK,IAAI;AAAA,UAE/D;AAAA,gCAAC,gBAAa,MAAM,IAAI;AAAA,YAAE;AAAA;AAAA;AAAA,MAC5B;AAAA,OACF;AAAA,EAEJ;AAEA,SAAO,gCAAG,UAAS;AACrB;","names":[]}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useState, useRef, useMemo } from "react";
|
|
3
|
+
import ReactMarkdown from "react-markdown";
|
|
4
|
+
import remarkGfm from "remark-gfm";
|
|
5
|
+
import { highlightCode } from "./shiki";
|
|
6
|
+
import { EyeIcon, Code2Icon } from "./icons";
|
|
7
|
+
import { ShikiSourceView } from "./ShikiSourceView";
|
|
8
|
+
import { FILE_PREVIEW_LIMITS } from "./limits";
|
|
9
|
+
import { useLocale } from "./core/i18n";
|
|
10
|
+
import "./styles/MarkdownPreview.css";
|
|
11
|
+
import "./styles/ViewModeBar.css";
|
|
12
|
+
function getTextContent(node) {
|
|
13
|
+
if (typeof node === "string") return node;
|
|
14
|
+
if (typeof node === "number") return String(node);
|
|
15
|
+
if (!node) return "";
|
|
16
|
+
if (Array.isArray(node)) return node.map(getTextContent).join("");
|
|
17
|
+
if (React.isValidElement(node)) {
|
|
18
|
+
const props = node.props;
|
|
19
|
+
if (props.children) return getTextContent(props.children);
|
|
20
|
+
}
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
function ShikiPreBlock({ children, ...rest }) {
|
|
24
|
+
const childArray = React.Children.toArray(children);
|
|
25
|
+
const codeElement = childArray.find(
|
|
26
|
+
(child) => React.isValidElement(child) && (child.type === "code" || child.props?.className?.includes("language-"))
|
|
27
|
+
);
|
|
28
|
+
const codeClassName = codeElement && React.isValidElement(codeElement) ? codeElement.props?.className || "" : "";
|
|
29
|
+
const langMatch = /language-(\w+)/.exec(codeClassName);
|
|
30
|
+
const language = langMatch ? langMatch[1] : "";
|
|
31
|
+
const codeText = codeElement && React.isValidElement(codeElement) ? getTextContent(codeElement.props.children).replace(/\n$/, "") : getTextContent(children).replace(/\n$/, "");
|
|
32
|
+
if (!language) {
|
|
33
|
+
return /* @__PURE__ */ jsx("pre", { ...rest, children });
|
|
34
|
+
}
|
|
35
|
+
return /* @__PURE__ */ jsx(ShikiPreContent, { code: codeText, language }, `${language}:${codeText}`);
|
|
36
|
+
}
|
|
37
|
+
function ShikiPreContent({ code, language }) {
|
|
38
|
+
const [html, setHtml] = useState("");
|
|
39
|
+
const [loading, setLoading] = useState(true);
|
|
40
|
+
const [error, setError] = useState(false);
|
|
41
|
+
const mountedRef = useRef(true);
|
|
42
|
+
const t = useLocale();
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
mountedRef.current = true;
|
|
45
|
+
let cancelled = false;
|
|
46
|
+
if (code.length > FILE_PREVIEW_LIMITS.SHIKI_MAX_CODE_BLOCK_SIZE) {
|
|
47
|
+
queueMicrotask(() => {
|
|
48
|
+
if (mountedRef.current) {
|
|
49
|
+
setLoading(false);
|
|
50
|
+
setHtml("");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
highlightCode(code, language).then((result) => {
|
|
56
|
+
if (!cancelled && mountedRef.current) {
|
|
57
|
+
setHtml(result);
|
|
58
|
+
setLoading(false);
|
|
59
|
+
}
|
|
60
|
+
}).catch((err) => {
|
|
61
|
+
console.warn("[MarkdownPreview] Shiki highlight error:", err);
|
|
62
|
+
if (!cancelled && mountedRef.current) {
|
|
63
|
+
setError(true);
|
|
64
|
+
setLoading(false);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return () => {
|
|
68
|
+
cancelled = true;
|
|
69
|
+
mountedRef.current = false;
|
|
70
|
+
};
|
|
71
|
+
}, [code, language]);
|
|
72
|
+
const [copied, setCopied] = useState(false);
|
|
73
|
+
const handleCopy = () => {
|
|
74
|
+
navigator.clipboard.writeText(code);
|
|
75
|
+
setCopied(true);
|
|
76
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
77
|
+
};
|
|
78
|
+
const isOversized = code.length > FILE_PREVIEW_LIMITS.SHIKI_MAX_CODE_BLOCK_SIZE;
|
|
79
|
+
if (loading || error || !html && !isOversized) {
|
|
80
|
+
return /* @__PURE__ */ jsxs("pre", { className: "md-pre-loading", children: [
|
|
81
|
+
/* @__PURE__ */ jsx("div", { className: "md-pre-header", children: /* @__PURE__ */ jsx("span", { className: "md-lang-badge", children: language }) }),
|
|
82
|
+
/* @__PURE__ */ jsx("code", { className: `language-${language}`, children: code })
|
|
83
|
+
] });
|
|
84
|
+
}
|
|
85
|
+
if (isOversized) {
|
|
86
|
+
return /* @__PURE__ */ jsxs("pre", { className: "md-pre-loading", children: [
|
|
87
|
+
/* @__PURE__ */ jsxs("div", { className: "md-pre-header", children: [
|
|
88
|
+
/* @__PURE__ */ jsx("span", { className: "md-lang-badge", children: language }),
|
|
89
|
+
/* @__PURE__ */ jsx("span", { className: "md-lang-badge", style: { fontSize: "0.6em", opacity: 0.7 }, children: t.oversizedCodeBlock })
|
|
90
|
+
] }),
|
|
91
|
+
/* @__PURE__ */ jsx("code", { className: `language-${language}`, children: code })
|
|
92
|
+
] });
|
|
93
|
+
}
|
|
94
|
+
return /* @__PURE__ */ jsxs("div", { className: "md-code-block", children: [
|
|
95
|
+
/* @__PURE__ */ jsxs("div", { className: "md-pre-header", children: [
|
|
96
|
+
/* @__PURE__ */ jsx("span", { className: "md-lang-badge", children: language }),
|
|
97
|
+
/* @__PURE__ */ jsx("button", { onClick: handleCopy, className: "md-copy-btn", title: "Copy code", children: copied ? /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" }) }) : /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
98
|
+
/* @__PURE__ */ jsx("rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2" }),
|
|
99
|
+
/* @__PURE__ */ jsx("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" })
|
|
100
|
+
] }) })
|
|
101
|
+
] }),
|
|
102
|
+
/* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: html } })
|
|
103
|
+
] });
|
|
104
|
+
}
|
|
105
|
+
function MarkdownPreview({ content }) {
|
|
106
|
+
const [viewMode, setViewMode] = useState("preview");
|
|
107
|
+
const components = useMemo(() => ({ pre: ShikiPreBlock }), []);
|
|
108
|
+
const t = useLocale();
|
|
109
|
+
return /* @__PURE__ */ jsxs("div", { className: "fv-markdown", children: [
|
|
110
|
+
/* @__PURE__ */ jsx("div", { className: "fv-view-mode-bar", children: /* @__PURE__ */ jsxs("div", { className: "fv-view-mode-group", children: [
|
|
111
|
+
/* @__PURE__ */ jsxs(
|
|
112
|
+
"button",
|
|
113
|
+
{
|
|
114
|
+
onClick: () => setViewMode("preview"),
|
|
115
|
+
className: `fv-view-mode-btn ${viewMode === "preview" ? "fv-view-mode-btn--active" : ""}`,
|
|
116
|
+
children: [
|
|
117
|
+
/* @__PURE__ */ jsx(EyeIcon, { size: 13 }),
|
|
118
|
+
t.preview
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
),
|
|
122
|
+
/* @__PURE__ */ jsxs(
|
|
123
|
+
"button",
|
|
124
|
+
{
|
|
125
|
+
onClick: () => setViewMode("source"),
|
|
126
|
+
className: `fv-view-mode-btn ${viewMode === "source" ? "fv-view-mode-btn--active" : ""}`,
|
|
127
|
+
children: [
|
|
128
|
+
/* @__PURE__ */ jsx(Code2Icon, { size: 13 }),
|
|
129
|
+
t.source
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
] }) }),
|
|
134
|
+
/* @__PURE__ */ jsx("div", { className: "fv-markdown__content", children: viewMode === "preview" ? /* @__PURE__ */ jsx("div", { className: "fv-markdown__preview", children: /* @__PURE__ */ jsx("article", { className: "fv-prose", children: /* @__PURE__ */ jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], components, children: content }) }) }) : /* @__PURE__ */ jsx(ShikiSourceView, { content, fileName: "markdown.md", language: "markdown" }) })
|
|
135
|
+
] });
|
|
136
|
+
}
|
|
137
|
+
export {
|
|
138
|
+
MarkdownPreview
|
|
139
|
+
};
|
|
140
|
+
//# sourceMappingURL=MarkdownPreview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/MarkdownPreview.tsx"],"sourcesContent":["import React, { useEffect, useState, useRef, useMemo, type ReactNode } from \"react\";\nimport ReactMarkdown from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\nimport { highlightCode } from \"./shiki\";\nimport { EyeIcon, Code2Icon } from \"./icons\";\nimport { ShikiSourceView } from \"./ShikiSourceView\";\nimport { FILE_PREVIEW_LIMITS } from \"./limits\";\nimport { useLocale } from \"./core/i18n\";\nimport \"./styles/MarkdownPreview.css\";\nimport \"./styles/ViewModeBar.css\";\n\ninterface MarkdownPreviewProps {\n content: string;\n}\n\ntype ViewMode = \"preview\" | \"source\";\n\nfunction getTextContent(node: ReactNode): string {\n if (typeof node === \"string\") return node;\n if (typeof node === \"number\") return String(node);\n if (!node) return \"\";\n if (Array.isArray(node)) return node.map(getTextContent).join(\"\");\n if (React.isValidElement(node)) {\n const props = node.props as { children?: ReactNode };\n if (props.children) return getTextContent(props.children);\n }\n return \"\";\n}\n\nfunction ShikiPreBlock({ children, ...rest }: React.HTMLAttributes<HTMLPreElement> & { children?: ReactNode }) {\n const childArray = React.Children.toArray(children);\n const codeElement = childArray.find(\n (child) => React.isValidElement(child) && (child.type === \"code\" || (child.props as { className?: string })?.className?.includes(\"language-\"))\n );\n\n const codeClassName = codeElement && React.isValidElement(codeElement)\n ? (codeElement.props as { className?: string })?.className || \"\"\n : \"\";\n const langMatch = /language-(\\w+)/.exec(codeClassName);\n const language = langMatch ? langMatch[1] : \"\";\n\n const codeText = codeElement && React.isValidElement(codeElement)\n ? getTextContent((codeElement.props as { children?: ReactNode }).children).replace(/\\n$/, \"\")\n : getTextContent(children).replace(/\\n$/, \"\");\n\n if (!language) {\n return <pre {...rest}>{children}</pre>;\n }\n\n return <ShikiPreContent key={`${language}:${codeText}`} code={codeText} language={language} />;\n}\n\nfunction ShikiPreContent({ code, language }: { code: string; language: string }) {\n const [html, setHtml] = useState<string>(\"\");\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState(false);\n const mountedRef = useRef(true);\n const t = useLocale();\n\n useEffect(() => {\n mountedRef.current = true;\n let cancelled = false;\n\n if (code.length > FILE_PREVIEW_LIMITS.SHIKI_MAX_CODE_BLOCK_SIZE) {\n queueMicrotask(() => {\n if (mountedRef.current) {\n setLoading(false);\n setHtml(\"\");\n }\n });\n return;\n }\n\n highlightCode(code, language)\n .then((result) => {\n if (!cancelled && mountedRef.current) {\n setHtml(result);\n setLoading(false);\n }\n })\n .catch((err) => {\n console.warn(\"[MarkdownPreview] Shiki highlight error:\", err);\n if (!cancelled && mountedRef.current) {\n setError(true);\n setLoading(false);\n }\n });\n\n return () => {\n cancelled = true;\n mountedRef.current = false;\n };\n }, [code, language]);\n\n const [copied, setCopied] = useState(false);\n\n const handleCopy = () => {\n navigator.clipboard.writeText(code);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const isOversized = code.length > FILE_PREVIEW_LIMITS.SHIKI_MAX_CODE_BLOCK_SIZE;\n\n if (loading || error || (!html && !isOversized)) {\n return (\n <pre className=\"md-pre-loading\">\n <div className=\"md-pre-header\">\n <span className=\"md-lang-badge\">{language}</span>\n </div>\n <code className={`language-${language}`}>{code}</code>\n </pre>\n );\n }\n\n if (isOversized) {\n return (\n <pre className=\"md-pre-loading\">\n <div className=\"md-pre-header\">\n <span className=\"md-lang-badge\">{language}</span>\n <span className=\"md-lang-badge\" style={{ fontSize: \"0.6em\", opacity: 0.7 }}>{t.oversizedCodeBlock}</span>\n </div>\n <code className={`language-${language}`}>{code}</code>\n </pre>\n );\n }\n\n return (\n <div className=\"md-code-block\">\n <div className=\"md-pre-header\">\n <span className=\"md-lang-badge\">{language}</span>\n <button onClick={handleCopy} className=\"md-copy-btn\" title=\"Copy code\">\n {copied ? (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n ) : (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg>\n )}\n </button>\n </div>\n <div dangerouslySetInnerHTML={{ __html: html }} />\n </div>\n );\n}\n\nexport function MarkdownPreview({ content }: MarkdownPreviewProps) {\n const [viewMode, setViewMode] = useState<ViewMode>(\"preview\");\n const components = useMemo(() => ({ pre: ShikiPreBlock }), []);\n const t = useLocale();\n\n return (\n <div className=\"fv-markdown\">\n <div className=\"fv-view-mode-bar\">\n <div className=\"fv-view-mode-group\">\n <button\n onClick={() => setViewMode(\"preview\")}\n className={`fv-view-mode-btn ${viewMode === \"preview\" ? \"fv-view-mode-btn--active\" : \"\"}`}\n >\n <EyeIcon size={13} />\n {t.preview}\n </button>\n <button\n onClick={() => setViewMode(\"source\")}\n className={`fv-view-mode-btn ${viewMode === \"source\" ? \"fv-view-mode-btn--active\" : \"\"}`}\n >\n <Code2Icon size={13} />\n {t.source}\n </button>\n </div>\n </div>\n\n <div className=\"fv-markdown__content\">\n {viewMode === \"preview\" ? (\n <div className=\"fv-markdown__preview\">\n <article className=\"fv-prose\">\n <ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>\n {content}\n </ReactMarkdown>\n </article>\n </div>\n ) : (\n <ShikiSourceView content={content} fileName=\"markdown.md\" language=\"markdown\" />\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { PreviewSource } from './core/types.js';
|
|
3
|
+
|
|
4
|
+
interface PdfPreviewProps {
|
|
5
|
+
content?: string | null;
|
|
6
|
+
source?: PreviewSource;
|
|
7
|
+
fileName: string;
|
|
8
|
+
}
|
|
9
|
+
declare function PdfPreview({ content, source, fileName }: PdfPreviewProps): react.JSX.Element;
|
|
10
|
+
|
|
11
|
+
export { PdfPreview };
|